tls_fprint.c   [plain text]


/*++
/* NAME
/*	tls_fprint 3
/* SUMMARY
/*	Digests fingerprints and all that.
/* SYNOPSIS
/*	#include <tls.h>
/*
/*	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_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 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 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(ret)	(ok &= ((ret) ? 1 : 0))
#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_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 EVP_MD *md;
    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.
     */
    if ((md = EVP_get_digestbyname(mdalg = "sha256")) == 0
	&& (md = EVP_get_digestbyname(mdalg = props->mdalg)) == 0)
	msg_panic("digest algorithm \"%s\" not found", mdalg);

    /* Salt the session lookup key with the OpenSSL runtime version. */
    sslversion = OpenSSL_version_num();

    mdctx = EVP_MD_CTX_create();
    checkok(EVP_DigestInit_ex(mdctx, md, NULL));
    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;
    const EVP_MD *md;
    unsigned char md_buf[EVP_MAX_MD_SIZE];
    unsigned int md_len;
    int     ok = 1;

    /* Previously available in "init" routine. */
    if ((md = EVP_get_digestbyname(mdalg)) == 0)
	msg_panic("digest algorithm \"%s\" not found", mdalg);

    mdctx = EVP_MD_CTX_create();
    checkok(EVP_DigestInit_ex(mdctx, md, NULL));
    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