summaryrefslogtreecommitdiffstats
path: root/src/tls/tls_scache.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:18:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 16:18:56 +0000
commitb7c15c31519dc44c1f691e0466badd556ffe9423 (patch)
treef944572f288bab482a615e09af627d9a2b6727d8 /src/tls/tls_scache.c
parentInitial commit. (diff)
downloadpostfix-upstream/3.7.10.tar.xz
postfix-upstream/3.7.10.zip
Adding upstream version 3.7.10.upstream/3.7.10upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r--src/tls/tls_scache.c591
1 files changed, 591 insertions, 0 deletions
diff --git a/src/tls/tls_scache.c b/src/tls/tls_scache.c
new file mode 100644
index 0000000..cf722ee
--- /dev/null
+++ b/src/tls/tls_scache.c
@@ -0,0 +1,591 @@
+/*++
+/* 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_opt(bin_data, hex_data, hex_data_len,
+ HEX_DECODE_FLAG_ALLOW_COLON) == 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.
+ */
+#define DICT_FLAGS \
+ (DICT_FLAG_DUP_REPLACE | DICT_FLAG_OPEN_LOCK | DICT_FLAG_SYNC_UPDATE \
+ | DICT_FLAG_UTF8_REQUEST)
+
+ 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