diff options
Diffstat (limited to 'src/tls/tls_scache.c')
-rw-r--r-- | src/tls/tls_scache.c | 595 |
1 files changed, 595 insertions, 0 deletions
diff --git a/src/tls/tls_scache.c b/src/tls/tls_scache.c new file mode 100644 index 0000000..dcfd3d0 --- /dev/null +++ b/src/tls/tls_scache.c @@ -0,0 +1,595 @@ +/*++ +/* NAME +/* tls_scache 3 +/* SUMMARY +/* TLS session cache manager +/* SYNOPSIS +/* #include <tls_scache.h> +/* +/* TLS_SCACHE *tls_scache_open(dbname, cache_label, verbose, timeout) +/* const char *dbname +/* const char *cache_label; +/* int verbose; +/* int timeout; +/* +/* void tls_scache_close(cache) +/* TLS_SCACHE *cache; +/* +/* int tls_scache_lookup(cache, cache_id, out_session) +/* TLS_SCACHE *cache; +/* const char *cache_id; +/* VSTRING *out_session; +/* +/* int tls_scache_update(cache, cache_id, session, session_len) +/* TLS_SCACHE *cache; +/* const char *cache_id; +/* const char *session; +/* ssize_t session_len; +/* +/* int tls_scache_sequence(cache, first_next, out_cache_id, +/* VSTRING *out_session) +/* TLS_SCACHE *cache; +/* int first_next; +/* char **out_cache_id; +/* VSTRING *out_session; +/* +/* int tls_scache_delete(cache, cache_id) +/* TLS_SCACHE *cache; +/* const char *cache_id; +/* +/* TLS_TICKET_KEY *tls_scache_key(keyname, now, timeout) +/* unsigned char *keyname; +/* time_t now; +/* int timeout; +/* +/* TLS_TICKET_KEY *tls_scache_key_rotate(newkey) +/* TLS_TICKET_KEY *newkey; +/* DESCRIPTION +/* This module maintains Postfix TLS session cache files. +/* each session is stored under a lookup key (hostname or +/* session ID). +/* +/* tls_scache_open() opens the specified TLS session cache +/* and returns a handle that must be used for subsequent +/* access. +/* +/* tls_scache_close() closes the specified TLS session cache +/* and releases memory that was allocated by tls_scache_open(). +/* +/* tls_scache_lookup() looks up the specified session in the +/* specified cache, and applies session timeout restrictions. +/* Entries that are too old are silently deleted. +/* +/* tls_scache_update() updates the specified TLS session cache +/* with the specified session information. +/* +/* tls_scache_sequence() iterates over the specified TLS session +/* cache and either returns the first or next entry that has not +/* timed out, or returns no data. Entries that are too old are +/* silently deleted. Specify TLS_SCACHE_SEQUENCE_NOTHING as the +/* third and last argument to disable saving of cache entry +/* content or cache entry ID information. This is useful when +/* purging expired entries. A result value of zero means that +/* the end of the cache was reached. +/* +/* tls_scache_delete() removes the specified cache entry from +/* the specified TLS session cache. +/* +/* tls_scache_key() locates a TLS session ticket key in a 2-element +/* in-memory cache. A null result is returned if no unexpired matching +/* key is found. +/* +/* tls_scache_key_rotate() saves a TLS session tickets key in the +/* in-memory cache. +/* +/* Arguments: +/* .IP dbname +/* The base name of the session cache file. +/* .IP cache_label +/* A string that is used in logging and error messages. +/* .IP verbose +/* Do verbose logging of cache operations? (zero == no) +/* .IP timeout +/* The time after which a session cache entry is considered too old. +/* .IP first_next +/* One of DICT_SEQ_FUN_FIRST (first cache element) or DICT_SEQ_FUN_NEXT +/* (next cache element). +/* .IP cache_id +/* Session cache lookup key. +/* .IP session +/* Storage for session information. +/* .IP session_len +/* The size of the session information in bytes. +/* .IP out_cache_id +/* .IP out_session +/* Storage for saving the cache_id or session information of the +/* current cache entry. +/* +/* Specify TLS_SCACHE_DONT_NEED_CACHE_ID to avoid saving +/* the session cache ID of the cache entry. +/* +/* Specify TLS_SCACHE_DONT_NEED_SESSION to avoid +/* saving the session information in the cache entry. +/* .IP keyname +/* Is null when requesting the current encryption keys. Otherwise, +/* keyname is a pointer to an array of TLS_TICKET_NAMELEN unsigned +/* chars (not NUL terminated) that is an identifier for a key +/* previously used to encrypt a session ticket. +/* .IP now +/* Current epoch time passed by caller. +/* .IP timeout +/* TLS session ticket encryption lifetime. +/* .IP newkey +/* TLS session ticket key obtained from tlsmgr(8) to be added to + * internal cache. +/* DIAGNOSTICS +/* These routines terminate with a fatal run-time error +/* for unrecoverable database errors. This allows the +/* program to restart and reset the database to an +/* empty initial state. +/* +/* tls_scache_open() never returns on failure. All other +/* functions return non-zero on success, zero when the +/* operation could not be completed. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System library. */ + +#include <sys_defs.h> + +#ifdef USE_TLS + +#include <string.h> +#include <stddef.h> + +/* Utility library. */ + +#include <msg.h> +#include <dict.h> +#include <stringops.h> +#include <mymalloc.h> +#include <hex_code.h> +#include <myflock.h> +#include <vstring.h> +#include <timecmp.h> + +/* Global library. */ + +/* TLS library. */ + +#include <tls_scache.h> + +/* Application-specific. */ + + /* + * Session cache entry format. + */ +typedef struct { + time_t timestamp; /* time when saved */ + char session[1]; /* actually a bunch of bytes */ +} TLS_SCACHE_ENTRY; + +static TLS_TICKET_KEY *keys[2]; + + /* + * SLMs. + */ +#define STR(x) vstring_str(x) +#define LEN(x) VSTRING_LEN(x) + +/* tls_scache_encode - encode TLS session cache entry */ + +static VSTRING *tls_scache_encode(TLS_SCACHE *cp, const char *cache_id, + const char *session, + ssize_t session_len) +{ + TLS_SCACHE_ENTRY *entry; + VSTRING *hex_data; + ssize_t binary_data_len; + + /* + * Assemble the TLS session cache entry. + * + * We could eliminate some copying by using incremental encoding, but + * sessions are so small that it really does not matter. + */ + binary_data_len = session_len + offsetof(TLS_SCACHE_ENTRY, session); + entry = (TLS_SCACHE_ENTRY *) mymalloc(binary_data_len); + entry->timestamp = time((time_t *) 0); + memcpy(entry->session, session, session_len); + + /* + * Encode the TLS session cache entry. + */ + hex_data = vstring_alloc(2 * binary_data_len + 1); + hex_encode(hex_data, (char *) entry, binary_data_len); + + /* + * Logging. + */ + if (cp->verbose) + msg_info("write %s TLS cache entry %s: time=%ld [data %ld bytes]", + cp->cache_label, cache_id, (long) entry->timestamp, + (long) session_len); + + /* + * Clean up. + */ + myfree((void *) entry); + + return (hex_data); +} + +/* tls_scache_decode - decode TLS session cache entry */ + +static int tls_scache_decode(TLS_SCACHE *cp, const char *cache_id, + const char *hex_data, ssize_t hex_data_len, + VSTRING *out_session) +{ + TLS_SCACHE_ENTRY *entry; + VSTRING *bin_data; + + /* + * Sanity check. + */ + if (hex_data_len < 2 * (offsetof(TLS_SCACHE_ENTRY, session))) { + msg_warn("%s TLS cache: truncated entry for %s: %.100s", + cp->cache_label, cache_id, hex_data); + return (0); + } + + /* + * Disassemble the TLS session cache entry. + * + * No early returns or we have a memory leak. + */ +#define FREE_AND_RETURN(ptr, x) { vstring_free(ptr); return (x); } + + bin_data = vstring_alloc(hex_data_len / 2 + 1); + if (hex_decode(bin_data, hex_data, hex_data_len) == 0) { + msg_warn("%s TLS cache: malformed entry for %s: %.100s", + cp->cache_label, cache_id, hex_data); + FREE_AND_RETURN(bin_data, 0); + } + entry = (TLS_SCACHE_ENTRY *) STR(bin_data); + + /* + * Logging. + */ + if (cp->verbose) + msg_info("read %s TLS cache entry %s: time=%ld [data %ld bytes]", + cp->cache_label, cache_id, (long) entry->timestamp, + (long) (LEN(bin_data) - offsetof(TLS_SCACHE_ENTRY, session))); + + /* + * Other mandatory restrictions. + */ + if (entry->timestamp + cp->timeout < time((time_t *) 0)) + FREE_AND_RETURN(bin_data, 0); + + /* + * Optional output. + */ + if (out_session != 0) + vstring_memcpy(out_session, entry->session, + LEN(bin_data) - offsetof(TLS_SCACHE_ENTRY, session)); + + /* + * Clean up. + */ + FREE_AND_RETURN(bin_data, 1); +} + +/* tls_scache_lookup - load session from cache */ + +int tls_scache_lookup(TLS_SCACHE *cp, const char *cache_id, + VSTRING *session) +{ + const char *hex_data; + + /* + * Logging. + */ + if (cp->verbose) + msg_info("lookup %s session id=%s", cp->cache_label, cache_id); + + /* + * Initialize. Don't leak data. + */ + if (session) + VSTRING_RESET(session); + + /* + * Search the cache database. + */ + if ((hex_data = dict_get(cp->db, cache_id)) == 0) + return (0); + + /* + * Decode entry and delete if expired or malformed. + */ + if (tls_scache_decode(cp, cache_id, hex_data, strlen(hex_data), + session) == 0) { + tls_scache_delete(cp, cache_id); + return (0); + } else { + return (1); + } +} + +/* tls_scache_update - save session to cache */ + +int tls_scache_update(TLS_SCACHE *cp, const char *cache_id, + const char *buf, ssize_t len) +{ + VSTRING *hex_data; + + /* + * Logging. + */ + if (cp->verbose) + msg_info("put %s session id=%s [data %ld bytes]", + cp->cache_label, cache_id, (long) len); + + /* + * Encode the cache entry. + */ + hex_data = tls_scache_encode(cp, cache_id, buf, len); + + /* + * Store the cache entry. + * + * XXX Berkeley DB supports huge database keys and values. SDBM seems to + * have a finite limit, and DBM simply can't be used at all. + */ + dict_put(cp->db, cache_id, STR(hex_data)); + + /* + * Clean up. + */ + vstring_free(hex_data); + + return (1); +} + +/* tls_scache_sequence - get first/next TLS session cache entry */ + +int tls_scache_sequence(TLS_SCACHE *cp, int first_next, + char **out_cache_id, + VSTRING *out_session) +{ + const char *member; + const char *value; + char *saved_cursor; + int found_entry; + int keep_entry; + char *saved_member; + + /* + * XXX Deleting entries while enumerating a map can he tricky. Some map + * types have a concept of cursor and support a "delete the current + * element" operation. Some map types without cursors don't behave well + * when the current first/next entry is deleted (example: with Berkeley + * DB < 2, the "next" operation produces garbage). To avoid trouble, we + * delete an expired entry after advancing the current first/next + * position beyond it, and ignore client requests to delete the current + * entry. + */ + + /* + * Find the first or next database entry. Activate the passivated entry + * and check the time stamp. Schedule the entry for deletion if it is too + * old. + * + * Save the member (cache id) so that it will not be clobbered by the + * tls_scache_lookup() call below. + */ + found_entry = (dict_seq(cp->db, first_next, &member, &value) == 0); + if (found_entry) { + keep_entry = tls_scache_decode(cp, member, value, strlen(value), + out_session); + if (keep_entry && out_cache_id) + *out_cache_id = mystrdup(member); + saved_member = mystrdup(member); + } + + /* + * Delete behind. This is a no-op if an expired cache entry was updated + * in the mean time. Use the saved lookup criteria so that the "delete + * behind" operation works as promised. + * + * The delete-behind strategy assumes that all updates are made by a single + * process. Otherwise, delete-behind may remove an entry that was updated + * after it was scheduled for deletion. + */ + if (cp->flags & TLS_SCACHE_FLAG_DEL_SAVED_CURSOR) { + cp->flags &= ~TLS_SCACHE_FLAG_DEL_SAVED_CURSOR; + saved_cursor = cp->saved_cursor; + cp->saved_cursor = 0; + tls_scache_lookup(cp, saved_cursor, (VSTRING *) 0); + myfree(saved_cursor); + } + + /* + * Otherwise, clean up if this is not the first iteration. + */ + else { + if (cp->saved_cursor) + myfree(cp->saved_cursor); + cp->saved_cursor = 0; + } + + /* + * Protect the current first/next entry against explicit or implied + * client delete requests, and schedule a bad or expired entry for + * deletion. Save the lookup criteria so that the "delete behind" + * operation will work as promised. + */ + if (found_entry) { + cp->saved_cursor = saved_member; + if (keep_entry == 0) + cp->flags |= TLS_SCACHE_FLAG_DEL_SAVED_CURSOR; + } + return (found_entry); +} + +/* tls_scache_delete - delete session from cache */ + +int tls_scache_delete(TLS_SCACHE *cp, const char *cache_id) +{ + + /* + * Logging. + */ + if (cp->verbose) + msg_info("delete %s session id=%s", cp->cache_label, cache_id); + + /* + * Do it, unless we would delete the current first/next entry. Some map + * types don't have cursors, and some of those don't behave when the + * "current" entry is deleted. + */ + return ((cp->saved_cursor != 0 && strcmp(cp->saved_cursor, cache_id) == 0) + || dict_del(cp->db, cache_id) == 0); +} + +/* tls_scache_open - open TLS session cache file */ + +TLS_SCACHE *tls_scache_open(const char *dbname, const char *cache_label, + int verbose, int timeout) +{ + TLS_SCACHE *cp; + DICT *dict; + + /* + * Logging. + */ + if (verbose) + msg_info("open %s TLS cache %s", cache_label, dbname); + + /* + * Open the dictionary with O_TRUNC, so that we never have to worry about + * opening a damaged file after some process terminated abnormally. + */ +#ifdef SINGLE_UPDATER +#define DICT_FLAGS (DICT_FLAG_DUP_REPLACE | DICT_FLAG_OPEN_LOCK \ + | DICT_FLAG_UTF8_REQUEST) +#else +#define DICT_FLAGS \ + (DICT_FLAG_DUP_REPLACE | DICT_FLAG_LOCK | DICT_FLAG_SYNC_UPDATE \ + | DICT_FLAG_UTF8_REQUEST) +#endif + + dict = dict_open(dbname, O_RDWR | O_CREAT | O_TRUNC, DICT_FLAGS); + + /* + * Sanity checks. + */ + if (dict->update == 0) + msg_fatal("dictionary %s does not support update operations", dbname); + if (dict->delete == 0) + msg_fatal("dictionary %s does not support delete operations", dbname); + if (dict->sequence == 0) + msg_fatal("dictionary %s does not support sequence operations", dbname); + + /* + * Create the TLS_SCACHE object. + */ + cp = (TLS_SCACHE *) mymalloc(sizeof(*cp)); + cp->flags = 0; + cp->db = dict; + cp->cache_label = mystrdup(cache_label); + cp->verbose = verbose; + cp->timeout = timeout; + cp->saved_cursor = 0; + + return (cp); +} + +/* tls_scache_close - close TLS session cache file */ + +void tls_scache_close(TLS_SCACHE *cp) +{ + + /* + * Logging. + */ + if (cp->verbose) + msg_info("close %s TLS cache %s", cp->cache_label, cp->db->name); + + /* + * Destroy the TLS_SCACHE object. + */ + dict_close(cp->db); + myfree(cp->cache_label); + if (cp->saved_cursor) + myfree(cp->saved_cursor); + myfree((void *) cp); +} + +/* tls_scache_key - find session ticket key for given key name */ + +TLS_TICKET_KEY *tls_scache_key(unsigned char *keyname, time_t now, int timeout) +{ + int i; + + /* + * The keys array contains 2 elements, the current signing key and the + * previous key. + * + * When name == 0 we are issuing a ticket, otherwise decrypting an existing + * ticket with the given key name. For new tickets we always use the + * current key if unexpired. For existing tickets, we use either the + * current or previous key with a validation expiration that is timeout + * longer than the signing expiration. + */ + if (keyname) { + for (i = 0; i < 2 && keys[i]; ++i) { + if (memcmp(keyname, keys[i]->name, TLS_TICKET_NAMELEN) == 0) { + if (timecmp(keys[i]->tout + timeout, now) > 0) + return (keys[i]); + break; + } + } + } else if (keys[0]) { + if (timecmp(keys[0]->tout, now) > 0) + return (keys[0]); + } + return (0); +} + +/* tls_scache_key_rotate - rotate session ticket keys */ + +TLS_TICKET_KEY *tls_scache_key_rotate(TLS_TICKET_KEY *newkey) +{ + + /* + * Allocate or re-use storage of retired key, then overwrite it, since + * caller's key data is ephemeral. + */ + if (keys[1] == 0) + keys[1] = (TLS_TICKET_KEY *) mymalloc(sizeof(*newkey)); + *keys[1] = *newkey; + newkey = keys[1]; + + /* + * Rotate if required, ensuring that the keys are sorted by expiration + * time with keys[0] expiring last. + */ + if (keys[0] == 0 || keys[0]->tout < keys[1]->tout) { + keys[1] = keys[0]; + keys[0] = newkey; + } + return (newkey); +} + +#endif |