diff options
Diffstat (limited to 'src/tls/tls_fprint.c')
-rw-r--r-- | src/tls/tls_fprint.c | 413 |
1 files changed, 413 insertions, 0 deletions
diff --git a/src/tls/tls_fprint.c b/src/tls/tls_fprint.c new file mode 100644 index 0000000..03e4dc6 --- /dev/null +++ b/src/tls/tls_fprint.c @@ -0,0 +1,413 @@ +/*++ +/* NAME +/* tls_fprint 3 +/* SUMMARY +/* Digests fingerprints and all that. +/* SYNOPSIS +/* #include <tls.h> +/* +/* EVP_MD *tls_digest_byname(const char *mdalg, EVP_MD_CTX **mdctxPtr) +/* const char *mdalg; +/* EVP_MD_CTX **mdctxPtr; +/* +/* char *tls_serverid_digest(props, protomask, ciphers) +/* const TLS_CLIENT_START_PROPS *props; +/* long protomask; +/* const char *ciphers; +/* +/* char *tls_digest_encode(md_buf, md_len) +/* const unsigned char *md_buf; +/* const char *md_len; +/* +/* char *tls_data_fprint(buf, len, mdalg) +/* const char *buf; +/* int len; +/* const char *mdalg; +/* +/* char *tls_cert_fprint(peercert, mdalg) +/* X509 *peercert; +/* const char *mdalg; +/* +/* char *tls_pkey_fprint(peercert, mdalg) +/* X509 *peercert; +/* const char *mdalg; +/* DESCRIPTION +/* tls_digest_byname() constructs, and optionally returns, an EVP_MD_CTX +/* handle for performing digest operations with the algorithm named by the +/* mdalg parameter. The return value is non-null on success, and holds a +/* digest algorithm handle. If the mdctxPtr argument is non-null the +/* created context is returned to the caller, who is then responsible for +/* deleting it by calling EVP_MD_ctx_free() once it is no longer needed. +/* +/* tls_digest_encode() converts a binary message digest to a hex ASCII +/* format with ':' separators between each pair of hex digits. +/* The return value is dynamically allocated with mymalloc(), +/* and the caller must eventually free it with myfree(). +/* +/* tls_data_fprint() digests unstructured data, and encodes the digested +/* result via tls_digest_encode(). The return value is dynamically +/* allocated with mymalloc(), and the caller must eventually free it +/* with myfree(). +/* +/* tls_cert_fprint() returns a fingerprint of the the given +/* certificate using the requested message digest, formatted +/* with tls_digest_encode(). Panics if the +/* (previously verified) digest algorithm is not found. The return +/* value is dynamically allocated with mymalloc(), and the caller +/* must eventually free it with myfree(). +/* +/* tls_pkey_fprint() returns a public-key fingerprint; in all +/* other respects the function behaves as tls_cert_fprint(). +/* The var_tls_bc_pkey_fprint variable enables an incorrect +/* algorithm that was used in Postfix versions 2.9.[0-5]. +/* The return value is dynamically allocated with mymalloc(), +/* and the caller must eventually free it with myfree(). +/* +/* tls_serverid_digest() suffixes props->serverid computed by the SMTP +/* client with "&" plus a digest of additional parameters +/* needed to ensure that re-used sessions are more likely to +/* be reused and that they will satisfy all protocol and +/* security requirements. +/* The return value is dynamically allocated with mymalloc(), +/* and the caller must eventually free it with myfree(). +/* +/* Arguments: +/* .IP mdalg +/* A digest algorithm name, such as "sha256". +/* .IP peercert +/* Server or client X.509 certificate. +/* .IP md_buf +/* The raw binary digest. +/* .IP md_len +/* The digest length in bytes. +/* .IP mdalg +/* Name of a message digest algorithm suitable for computing secure +/* (1st pre-image resistant) message digests of certificates. For now, +/* md5, sha1, or member of SHA-2 family if supported by OpenSSL. +/* .IP mdctxPtr +/* Pointer to an (EVP_MD_CTX *) handle, or NULL if only probing for +/* algorithm support without immediate use in mind. +/* .IP buf +/* Input data for the message digest algorithm mdalg. +/* .IP len +/* The length of the input data. +/* .IP props +/* The client start properties for the session, which contains the +/* initial serverid from the SMTP client and the DANE verification +/* parameters. +/* .IP protomask +/* The mask of protocol exclusions. +/* .IP ciphers +/* The SSL client cipherlist. +/* LICENSE +/* .ad +/* .fi +/* This software is free. You can do with it whatever you want. +/* The original author kindly requests that you acknowledge +/* the use of his software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Viktor Dukhovni +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <ctype.h> + +#ifdef USE_TLS +#include <string.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <stringops.h> + +/* Global library. */ + +#include <mail_params.h> + +/* TLS library. */ + +#define TLS_INTERNAL +#include <tls.h> + +/* Application-specific. */ + +static const char hexcodes[] = "0123456789ABCDEF"; + +#define checkok(stillok) (ok = ok && (stillok)) +#define digest_data(p, l) checkok(EVP_DigestUpdate(mdctx, (char *)(p), (l))) +#define digest_object(p) digest_data((p), sizeof(*(p))) +#define digest_string(s) digest_data((s), strlen(s)+1) + +#define digest_dane(dane, memb) do { \ + if ((dane)->memb != 0) \ + checkok(digest_tlsa_usage(mdctx, (dane)->memb, #memb)); \ + } while (0) + +#define digest_tlsa_argv(tlsa, memb) do { \ + if ((tlsa)->memb) { \ + digest_string(#memb); \ + for (dgst = (tlsa)->memb->argv; *dgst; ++dgst) \ + digest_string(*dgst); \ + } \ + } while (0) + +/* digest_tlsa_usage - digest TA or EE match list sorted by alg and value */ + +static int digest_tlsa_usage(EVP_MD_CTX * mdctx, TLS_TLSA *tlsa, + const char *usage) +{ + char **dgst; + int ok = 1; + + for (digest_string(usage); tlsa; tlsa = tlsa->next) { + digest_string(tlsa->mdalg); + digest_tlsa_argv(tlsa, pkeys); + digest_tlsa_argv(tlsa, certs); + } + return (ok); +} + +/* tls_digest_byname - test availability or prepare to use digest */ + +const EVP_MD *tls_digest_byname(const char *mdalg, EVP_MD_CTX **mdctxPtr) +{ + const EVP_MD *md; + EVP_MD_CTX *mdctx = NULL; + int ok = 1; + + /* + * In OpenSSL 3.0, because of dynamically variable algorithm providers, + * there is a time-of-check/time-of-use issue that means that abstract + * algorithm handles returned by EVP_get_digestbyname() can (and not + * infrequently do) return ultimately unusable algorithms, to check for + * actual availability, one needs to use the new EVP_MD_fetch() API, or + * indirectly check usability by creating a concrete context. We take the + * latter approach here (works for 1.1.1 without #ifdef). + * + * Note that EVP_MD_CTX_{create,destroy} were renamed to, respectively, + * EVP_MD_CTX_{new,free} in OpenSSL 1.1.0. + */ + checkok(md = EVP_get_digestbyname(mdalg)); + + /* + * Sanity check: Newer shared libraries could (hypothetical ABI break) + * allow larger digests, we avoid such poison algorithms. + */ + checkok(EVP_MD_size(md) <= EVP_MAX_MD_SIZE); + checkok(mdctx = EVP_MD_CTX_new()); + checkok(EVP_DigestInit_ex(mdctx, md, NULL)); + + + if (ok && mdctxPtr != 0) + *mdctxPtr = mdctx; + else + EVP_MD_CTX_free(mdctx); + return (ok ? md : 0); +} + +/* tls_serverid_digest - suffix props->serverid with parameter digest */ + +char *tls_serverid_digest(const TLS_CLIENT_START_PROPS *props, long protomask, + const char *ciphers) +{ + EVP_MD_CTX *mdctx; + const char *mdalg; + unsigned char md_buf[EVP_MAX_MD_SIZE]; + unsigned int md_len; + int ok = 1; + int i; + long sslversion; + VSTRING *result; + + /* + * Try to use sha256: our serverid choice should be strong enough to + * resist 2nd-preimage attacks with a difficulty comparable to that of + * DANE TLSA digests. Failing that, we compute serverid digests with the + * default digest, but DANE requires sha256 and sha512, so if we must + * fall back to our default digest, DANE support won't be available. We + * panic if the fallback algorithm is not available, as it was verified + * available in tls_client_init() and must not simply vanish. Our + * provider set is not expected to change once the OpenSSL library is + * initialized. + */ + if (tls_digest_byname(mdalg = LN_sha256, &mdctx) == 0 + && tls_digest_byname(mdalg = props->mdalg, &mdctx) == 0) + msg_panic("digest algorithm \"%s\" not found", props->mdalg); + + /* Salt the session lookup key with the OpenSSL runtime version. */ + sslversion = OpenSSL_version_num(); + + digest_string(props->helo ? props->helo : ""); + digest_object(&sslversion); + digest_object(&protomask); + digest_string(ciphers); + + /* + * All we get from the session cache is a single bit telling us whether + * the certificate is trusted or not, but we need to know whether the + * trust is CA-based (in that case we must do name checks) or whether it + * is a direct end-point match. We mustn't confuse the two, so it is + * best to process only TA trust in the verify callback and check the EE + * trust after. This works since re-used sessions always have access to + * the leaf certificate, while only the original session has the leaf and + * the full trust chain. + * + * Only the trust anchor matchlist is hashed into the session key. The end + * entity certs are not used to determine whether a certificate is + * trusted or not, rather these are rechecked against the leaf cert + * outside the verification callback, each time a session is created or + * reused. + * + * Therefore, the security context of the session does not depend on the EE + * matching data, which is checked separately each time. So we exclude + * the EE part of the DANE structure from the serverid digest. + * + * If the security level is "dane", we send SNI information to the peer. + * This may cause it to respond with a non-default certificate. Since + * certificates for sessions with no or different SNI data may not match, + * we must include the SNI name in the session id. + */ + if (props->dane) { + digest_dane(props->dane, ta); +#if 0 + digest_dane(props->dane, ee); /* See above */ +#endif + digest_string(TLS_DANE_BASED(props->tls_level) ? props->host : ""); + } + checkok(EVP_DigestFinal_ex(mdctx, md_buf, &md_len)); + EVP_MD_CTX_destroy(mdctx); + if (!ok) + msg_fatal("error computing %s message digest", mdalg); + + /* Check for OpenSSL contract violation */ + if (md_len > EVP_MAX_MD_SIZE) + msg_panic("unexpectedly large %s digest size: %u", mdalg, md_len); + + /* + * Append the digest to the serverid. We don't compare this digest to + * any user-specified fingerprints. Therefore, we don't need to use a + * colon-separated format, which saves space in the TLS session cache and + * makes logging of session cache lookup keys more readable. + * + * This does however duplicate a few lines of code from the digest encoder + * for colon-separated cert and pkey fingerprints. If that is a + * compelling reason to consolidate, we could use that and append the + * result. + */ + result = vstring_alloc(strlen(props->serverid) + 1 + 2 * md_len); + vstring_strcpy(result, props->serverid); + VSTRING_ADDCH(result, '&'); + for (i = 0; i < md_len; i++) { + VSTRING_ADDCH(result, hexcodes[(md_buf[i] & 0xf0) >> 4U]); + VSTRING_ADDCH(result, hexcodes[(md_buf[i] & 0x0f)]); + } + VSTRING_TERMINATE(result); + return (vstring_export(result)); +} + +/* tls_digest_encode - encode message digest binary blob as xx:xx:... */ + +char *tls_digest_encode(const unsigned char *md_buf, int md_len) +{ + int i; + char *result = mymalloc(md_len * 3); + + /* Check for contract violation */ + if (md_len > EVP_MAX_MD_SIZE || md_len >= INT_MAX / 3) + msg_panic("unexpectedly large message digest size: %u", md_len); + + /* No risk of overrunes, len is bounded by OpenSSL digest length */ + for (i = 0; i < md_len; i++) { + result[i * 3] = hexcodes[(md_buf[i] & 0xf0) >> 4U]; + result[(i * 3) + 1] = hexcodes[(md_buf[i] & 0x0f)]; + result[(i * 3) + 2] = (i + 1 != md_len) ? ':' : '\0'; + } + return (result); +} + +/* tls_data_fprint - compute and encode digest of binary object */ + +char *tls_data_fprint(const char *buf, int len, const char *mdalg) +{ + EVP_MD_CTX *mdctx = NULL; + unsigned char md_buf[EVP_MAX_MD_SIZE]; + unsigned int md_len; + int ok = 1; + + /* Previously available in "init" routine. */ + if (tls_digest_byname(mdalg, &mdctx) == 0) + msg_panic("digest algorithm \"%s\" not found", mdalg); + + digest_data(buf, len); + checkok(EVP_DigestFinal_ex(mdctx, md_buf, &md_len)); + EVP_MD_CTX_destroy(mdctx); + if (!ok) + msg_fatal("error computing %s message digest", mdalg); + + return (tls_digest_encode(md_buf, md_len)); +} + +/* tls_cert_fprint - extract certificate fingerprint */ + +char *tls_cert_fprint(X509 *peercert, const char *mdalg) +{ + int len; + char *buf; + char *buf2; + char *result; + + len = i2d_X509(peercert, NULL); + buf2 = buf = mymalloc(len); + i2d_X509(peercert, (unsigned char **) &buf2); + if (buf2 - buf != len) + msg_panic("i2d_X509 invalid result length"); + + result = tls_data_fprint(buf, len, mdalg); + myfree(buf); + + return (result); +} + +/* tls_pkey_fprint - extract public key fingerprint from certificate */ + +char *tls_pkey_fprint(X509 *peercert, const char *mdalg) +{ + if (var_tls_bc_pkey_fprint) { + const char *myname = "tls_pkey_fprint"; + ASN1_BIT_STRING *key; + char *result; + + key = X509_get0_pubkey_bitstr(peercert); + if (key == 0) + msg_fatal("%s: error extracting legacy public-key fingerprint: %m", + myname); + + result = tls_data_fprint((char *) key->data, key->length, mdalg); + return (result); + } else { + int len; + char *buf; + char *buf2; + char *result; + + len = i2d_X509_PUBKEY(X509_get_X509_PUBKEY(peercert), NULL); + buf2 = buf = mymalloc(len); + i2d_X509_PUBKEY(X509_get_X509_PUBKEY(peercert), (unsigned char **) &buf2); + if (buf2 - buf != len) + msg_panic("i2d_X509_PUBKEY invalid result length"); + + result = tls_data_fprint(buf, len, mdalg); + myfree(buf); + return (result); + } +} + +#endif |