diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:18:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 16:18:56 +0000 |
commit | b7c15c31519dc44c1f691e0466badd556ffe9423 (patch) | |
tree | f944572f288bab482a615e09af627d9a2b6727d8 /src/tls/tls_dane.c | |
parent | Initial commit. (diff) | |
download | postfix-b7c15c31519dc44c1f691e0466badd556ffe9423.tar.xz postfix-b7c15c31519dc44c1f691e0466badd556ffe9423.zip |
Adding upstream version 3.7.10.upstream/3.7.10
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/tls/tls_dane.c')
-rw-r--r-- | src/tls/tls_dane.c | 1357 |
1 files changed, 1357 insertions, 0 deletions
diff --git a/src/tls/tls_dane.c b/src/tls/tls_dane.c new file mode 100644 index 0000000..a2b9b80 --- /dev/null +++ b/src/tls/tls_dane.c @@ -0,0 +1,1357 @@ +/*++ +/* NAME +/* tls_dane 3 +/* SUMMARY +/* Support for RFC 6698, 7671, 7672 (DANE) certificate matching +/* SYNOPSIS +/* #include <tls.h> +/* +/* void tls_dane_loglevel(log_param, log_level); +/* const char *log_param; +/* const char *log_level; +/* +/* int tls_dane_avail() +/* +/* void tls_dane_flush() +/* +/* TLS_DANE *tls_dane_alloc() +/* +/* void tls_tlsa_free(tlsa) +/* TLS_TLSA *tlsa; +/* +/* void tls_dane_free(dane) +/* TLS_DANE *dane; +/* +/* void tls_dane_add_fpt_digests(dane, digest, delim, smtp_mode) +/* TLS_DANE *dane; +/* const char *digest; +/* const char *delim; +/* int smtp_mode; +/* +/* TLS_TLSA *tlsa_prepend(tlsa, usage, selector, mtype, data, len) +/* TLS_TLSA *tlsa; +/* uint8_t usage; +/* uint8_t selector; +/* uint8_t mtype; +/* const unsigned char *data; +/* uint16_t length; +/* +/* int tls_dane_load_trustfile(dane, tafile) +/* TLS_DANE *dane; +/* const char *tafile; +/* +/* TLS_DANE *tls_dane_resolve(port, proto, hostrr, forcetlsa) +/* unsigned port; +/* const char *proto; +/* DNS_RR *hostrr; +/* int forcetlsa; +/* +/* void tls_dane_digest_init(ctx, fpt_alg) +/* SSL_CTX *ctx; +/* const EVP_MD *fpt_alg; +/* +/* void tls_dane_enable(TLScontext) +/* TLS_SESS_STATE *TLScontext; +/* +/* void tls_dane_log(TLScontext) +/* TLS_SESS_STATE *TLScontext; +/* +/* int tls_dane_unusable(dane) +/* const TLS_DANE *dane; +/* +/* int tls_dane_notfound(dane) +/* const TLS_DANE *dane; +/* DESCRIPTION +/* tls_dane_loglevel() allows the policy lookup functions in the DANE +/* library to examine the application's TLS loglevel in and possibly +/* produce a more detailed activity log. +/* +/* tls_dane_avail() returns true if the features required to support DANE +/* are present in libresolv. +/* +/* tls_dane_flush() flushes all entries from the cache, and deletes +/* the cache. +/* +/* tls_dane_alloc() returns a pointer to a newly allocated TLS_DANE +/* structure with null ta and ee digest sublists. +/* +/* tls_tlsa_free() frees a TLSA record linked list. +/* +/* tls_dane_free() frees the structure allocated by tls_dane_alloc(). +/* +/* tls_dane_digest_init() configures OpenSSL to support the configured +/* DANE TLSA digests and private-use fingerprint digest. +/* +/* tlsa_prepend() prepends a TLSA record to the head of a linked list +/* which may be null when the list is empty. The result value is the +/* new list head. +/* +/* tls_dane_add_fpt_digests() splits "digest" using the characters in +/* "delim" as delimiters and generates corresponding synthetic DANE TLSA +/* records with matching type 255 (private-use), which we associated with +/* the configured fingerprint digest algorithm. This is an incremental +/* interface, that builds a TLS_DANE structure outside the cache by +/* manually adding entries. +/* +/* tls_dane_load_trustfile() imports trust-anchor certificates and +/* public keys from a file (rather than DNS TLSA records). +/* +/* tls_dane_resolve() maps a (port, protocol, hostrr) tuple to a +/* corresponding TLS_DANE policy structure found in the DNS. The port +/* argument is in network byte order. A null pointer is returned when +/* the DNS query for the TLSA record tempfailed. In all other cases the +/* return value is a pointer to the corresponding TLS_DANE structure. +/* The caller must free the structure via tls_dane_free(). +/* +/* tls_dane_enable() enables DANE-style certificate checks for connections +/* that are configured with TLSA records. The TLSA records may be from +/* DNS (at the "dane", "dane-only" and "half-dane" security levels), or be +/* synthetic in support of either the "fingerprint" level or local trust +/* anchor based validation with the "secure" and "verify" levels. The +/* return value is the number of "usable" TLSA records loaded, or negative +/* if a record failed to load due to an internal OpenSSL problems, rather +/* than an issue with the record making that record "unusable". +/* +/* tls_dane_log() logs successful verification via DNS-based or +/* synthetic DANE TLSA RRs (fingerprint or "tafile"). +/* +/* tls_dane_unusable() checks whether a cached TLS_DANE record is +/* the result of a validated RRset, with no usable elements. In +/* this case, TLS is mandatory, but certificate verification is +/* not DANE-based. +/* +/* tls_dane_notfound() checks whether a cached TLS_DANE record is +/* the result of a validated DNS lookup returning NODATA. In +/* this case, TLS is not required by RFC, though users may elect +/* a mandatory TLS fallback policy. +/* +/* Arguments: +/* .IP ctx +/* SSL context to be configured with the chosen digest algorithms. +/* .IP fpt_alg +/* The OpenSSL EVP digest algorithm handle for the fingerprint digest. +/* .IP tlsa +/* TLSA record linked list head, initially NULL. +/* .IP usage +/* DANE TLSA certificate usage field. +/* .IP selector +/* DANE TLSA selector field. +/* .IP mtype +/* DANE TLSA matching type field +/* .IP data +/* DANE TLSA associated data field (raw binary form), copied for internal +/* use. The caller is responsible for freeing his own copy. +/* .IP length +/* Length of DANE TLSA associated DATA field. +/* .IP dane +/* Pointer to a TLS_DANE structure that lists the valid trust-anchor +/* and end-entity full-certificate and/or public-key digests. +/* .IP port +/* The TCP port in network byte order. +/* .IP proto +/* Almost certainly "tcp". +/* .IP hostrr +/* DNS_RR pointer to TLSA base domain data. +/* .IP forcetlsa +/* When true, TLSA lookups are performed even when the qname and rname +/* are insecure. This is only useful in the unlikely case that DLV is +/* used to secure the TLSA RRset in an otherwise insecure zone. +/* .IP log_param +/* The TLS log level parameter name whose value is the log_level argument. +/* .IP log_level +/* The application TLS log level, which may affect dane lookup verbosity. +/* .IP digest +/* The digest (or list of digests concatenated with characters from +/* "delim") to be added to the TLS_DANE record. +/* .IP delim +/* The set of delimiter characters used above. +/* .IP smtp_mode +/* Is the caller an SMTP client or an LMTP client? +/* .IP tafile; +/* A file with trust anchor certificates or public keys in PEM format. +/* 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 +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* Viktor Dukhovni +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <ctype.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +#ifdef USE_TLS +#include <string.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <stringops.h> +#include <midna_domain.h> +#include <vstring.h> +#include <events.h> /* event_time() */ +#include <timecmp.h> +#include <ctable.h> +#include <hex_code.h> +#include <safe_ultostr.h> +#include <split_at.h> +#include <name_code.h> + +#define STR(x) vstring_str(x) + +/* Global library */ + +#include <mail_params.h> + +/* DNS library. */ + +#include <dns.h> + +/* TLS library. */ + +#define TLS_INTERNAL +#include <tls.h> + +/* Application-specific. */ + +#undef DANE_TLSA_SUPPORT + +#if RES_USE_DNSSEC && RES_USE_EDNS0 +#define DANE_TLSA_SUPPORT +static int dane_tlsa_support = 1; + +#else +static int dane_tlsa_support = 0; + +#endif + +/* + * A NULL alg field disables the algorithm at the codepoint passed to the + * SSL_CTX_dane_mtype_set(3) function. The ordinals are used for digest + * agility, higher is "better" (presumed stronger). + */ +typedef struct dane_mtype { + const EVP_MD *alg; + uint8_t ord; +} dane_mtype; + +/* + * This is not intended to be a long-term cache of pre-parsed TLSA data, + * rather we primarily want to avoid fetching and parsing the TLSA records + * for a single multi-homed MX host more than once per delivery. Therefore, + * we keep the table reasonably small. + */ +#define CACHE_SIZE 20 +static CTABLE *dane_cache; + +static int log_mask; + +/* tls_dane_logmask - configure policy lookup logging */ + +void tls_dane_loglevel(const char *log_param, const char *log_level) +{ + log_mask = tls_log_mask(log_param, log_level); +} + +/* tls_dane_avail - check for availability of dane required digests */ + +int tls_dane_avail(void) +{ + return (dane_tlsa_support); +} + +/* tls_dane_alloc - allocate a TLS_DANE structure */ + +TLS_DANE *tls_dane_alloc(void) +{ + TLS_DANE *dane = (TLS_DANE *) mymalloc(sizeof(*dane)); + + dane->tlsa = 0; + dane->base_domain = 0; + dane->flags = 0; + dane->expires = 0; + dane->refs = 1; + return (dane); +} + +/* tls_tlsa_free - free a TLSA RR linked list */ + +void tls_tlsa_free(TLS_TLSA *tlsa) +{ + TLS_TLSA *next; + + for (; tlsa; tlsa = next) { + next = tlsa->next; + myfree(tlsa->data); + myfree(tlsa); + } +} + +/* tls_dane_free - free a TLS_DANE structure */ + +void tls_dane_free(TLS_DANE *dane) +{ + if (--dane->refs > 0) + return; + if (dane->base_domain) + myfree(dane->base_domain); + if (dane->tlsa) + tls_tlsa_free(dane->tlsa); + myfree((void *) dane); +} + +/* tlsa_prepend - Prepend internal-form TLSA record to the RRset linked list */ + +TLS_TLSA *tlsa_prepend(TLS_TLSA *tlsa, uint8_t usage, uint8_t selector, + uint8_t mtype, const unsigned char *data, + uint16_t data_len) +{ + TLS_TLSA *head; + + head = (TLS_TLSA *) mymalloc(sizeof(*head)); + head->usage = usage; + head->selector = selector; + head->mtype = mtype; + head->length = data_len; + head->data = (unsigned char *) mymemdup(data, data_len); + head->next = tlsa; + return (head); +} + +#define MAX_HEAD_BYTES 32 +#define MAX_TAIL_BYTES 32 +#define MAX_DUMP_BYTES (MAX_HEAD_BYTES + MAX_TAIL_BYTES) + +/* tlsa_info - log import of a particular TLSA record */ + +static void tlsa_info(const char *tag, const char *msg, + uint8_t u, uint8_t s, uint8_t m, + const unsigned char *data, ssize_t dlen) +{ + static VSTRING *top; + static VSTRING *bot; + + if (top == 0) + top = vstring_alloc(2 * MAX_HEAD_BYTES); + if (bot == 0) + bot = vstring_alloc(2 * MAX_TAIL_BYTES); + + if (dlen > MAX_DUMP_BYTES) { + hex_encode(top, (char *) data, MAX_HEAD_BYTES); + hex_encode(bot, (char *) data + dlen - MAX_TAIL_BYTES, MAX_TAIL_BYTES); + } else if (dlen > 0) { + hex_encode(top, (char *) data, dlen); + } else { + vstring_sprintf(top, "..."); + } + + msg_info("%s: %s: %u %u %u %s%s%s", tag, msg, u, s, m, STR(top), + dlen > MAX_DUMP_BYTES ? "..." : "", + dlen > MAX_DUMP_BYTES ? STR(bot) : ""); +} + +/* tlsa_carp - carp about a particular TLSA record */ + +static void tlsa_carp(const char *s1, const char *s2, const char *s3, + const char *s4, uint8_t u, uint8_t s, uint8_t m, + const unsigned char *data, ssize_t dlen) +{ + static VSTRING *top; + static VSTRING *bot; + + if (top == 0) + top = vstring_alloc(2 * MAX_HEAD_BYTES); + if (bot == 0) + bot = vstring_alloc(2 * MAX_TAIL_BYTES); + + if (dlen > MAX_DUMP_BYTES) { + hex_encode(top, (char *) data, MAX_HEAD_BYTES); + hex_encode(bot, (char *) data + dlen - MAX_TAIL_BYTES, MAX_TAIL_BYTES); + } else if (dlen > 0) { + hex_encode(top, (char *) data, dlen); + } else { + vstring_sprintf(top, "..."); + } + + msg_warn("%s%s%s %s: %u %u %u %s%s%s", s1, s2, s3, s4, u, s, m, STR(top), + dlen > MAX_DUMP_BYTES ? "..." : "", + dlen > MAX_DUMP_BYTES ? STR(bot) : ""); +} + +/* tls_dane_flush - flush the cache */ + +void tls_dane_flush(void) +{ + if (dane_cache) + ctable_free(dane_cache); + dane_cache = 0; +} + +/* dane_free - ctable style */ + +static void dane_free(void *dane, void *unused_context) +{ + tls_dane_free((TLS_DANE *) dane); +} + +/* tls_dane_add_fpt_digests - map fingerprint list to DANE TLSA RRset */ + +void tls_dane_add_fpt_digests(TLS_DANE *dane, const char *digest, + const char *delim, int smtp_mode) +{ + ARGV *values = argv_split(digest, delim); + ssize_t i; + + if (smtp_mode) { + if (warn_compat_break_smtp_tls_fpt_dgst) + msg_info("using backwards-compatible default setting " + VAR_SMTP_TLS_FPT_DGST "=md5 to compute certificate " + "fingerprints"); + } else { + if (warn_compat_break_lmtp_tls_fpt_dgst) + msg_info("using backwards-compatible default setting " + VAR_LMTP_TLS_FPT_DGST "=md5 to compute certificate " + "fingerprints"); + } + + for (i = 0; i < values->argc; ++i) { + const char *cp = values->argv[i]; + size_t ilen = strlen(cp); + VSTRING *raw; + + /* + * Decode optionally colon-separated hex-encoded string, the input + * value requires at most 3 bytes per byte of payload, which must not + * exceed the size of the widest supported hash function. + */ + if (ilen > 3 * EVP_MAX_MD_SIZE) { + msg_warn("malformed fingerprint value: %.100s...", + values->argv[i]); + continue; + } + raw = vstring_alloc(ilen / 2); + if (hex_decode_opt(raw, cp, ilen, HEX_DECODE_FLAG_ALLOW_COLON) == 0) { + myfree(raw); + msg_warn("malformed fingerprint value: %.384s", values->argv[i]); + continue; + } + + /* + * At the "fingerprint" security level certificate digests and public + * key digests are interchangeable. Each leaf certificate is matched + * via either the public key digest or full certificate digest. The + * DER encoding of a certificate is not a valid public key, and + * conversely, the DER encoding of a public key is not a valid + * certificate. An attacker would need a 2nd-preimage that is + * feasible across types (given cert digest == some pkey digest) and + * yet presumably difficult within a type (e.g. given cert digest == + * some other cert digest). No such attacks are known at this time, + * and it is expected that if any are found they would work within as + * well as across the cert/pkey data types. + * + * The private-use matching type "255" is mapped to the configured + * fingerprint digest, which may (harmlessly) coincide with one of + * the standard DANE digest algorithms. The private code point is + * however unconditionally enabled. + */ + if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) + tlsa_info("fingerprint", "digest as private-use TLSA record", + 3, 0, 255, (unsigned char *) STR(raw), VSTRING_LEN(raw)); + dane->tlsa = tlsa_prepend(dane->tlsa, 3, 0, 255, + (unsigned char *) STR(raw), VSTRING_LEN(raw)); + dane->tlsa = tlsa_prepend(dane->tlsa, 3, 1, 255, + (unsigned char *) STR(raw), VSTRING_LEN(raw)); + vstring_free(raw); + } + argv_free(values); +} + +/* parse_tlsa_rr - parse a validated TLSA RRset */ + +static int parse_tlsa_rr(TLS_DANE *dane, DNS_RR *rr) +{ + const uint8_t *ip; + uint8_t usage; + uint8_t selector; + uint8_t mtype; + ssize_t dlen; + unsigned const char *data; + int iscname = strcasecmp(rr->rname, rr->qname); + const char *q = iscname ? rr->qname : ""; + const char *a = iscname ? " -> " : ""; + const char *r = rr->rname; + + if (rr->type != T_TLSA) + msg_panic("%s%s%s: unexpected non-TLSA RR type: %u", + q, a, r, rr->type); + + /* Drop truncated records */ + if ((dlen = rr->data_len - 3) < 0) { + msg_warn("%s%s%s: truncated TLSA RR length == %u", + q, a, r, (unsigned) rr->data_len); + return (0); + } + ip = (const uint8_t *) rr->data; + usage = *ip++; + selector = *ip++; + mtype = *ip++; + data = (const unsigned char *) ip; + + /*- + * Drop unsupported usages. + * Note: NO SUPPORT for usages 0/1 which do not apply to SMTP. + */ + switch (usage) { + case DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION: + case DNS_TLSA_USAGE_DOMAIN_ISSUED_CERTIFICATE: + break; + default: + tlsa_carp(q, a, r, "unsupported TLSA certificate usage", + usage, selector, mtype, data, dlen); + return (0); + } + + /* + * Drop private-use matching type, reserved for fingerprint matching. + */ + if (mtype == 255) { + tlsa_carp(q, a, r, "reserved private-use matching type", + usage, selector, mtype, data, dlen); + return (0); + } + if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) + tlsa_info("DNSSEC-signed TLSA record", r, + usage, selector, mtype, data, dlen); + dane->tlsa = tlsa_prepend(dane->tlsa, usage, selector, mtype, data, dlen); + return (1); +} + +/* dane_lookup - TLSA record lookup, ctable style */ + +static void *dane_lookup(const char *tlsa_fqdn, void *unused_ctx) +{ + static VSTRING *why = 0; + DNS_RR *rrs = 0; + DNS_RR *rr; + TLS_DANE *dane = tls_dane_alloc(); + int ret; + + if (why == 0) + why = vstring_alloc(10); + + ret = dns_lookup(tlsa_fqdn, T_TLSA, RES_USE_DNSSEC, &rrs, 0, why); + + switch (ret) { + case DNS_OK: + if (TLS_DANE_CACHE_TTL_MIN && rrs->ttl < TLS_DANE_CACHE_TTL_MIN) + rrs->ttl = TLS_DANE_CACHE_TTL_MIN; + if (TLS_DANE_CACHE_TTL_MAX && rrs->ttl > TLS_DANE_CACHE_TTL_MAX) + rrs->ttl = TLS_DANE_CACHE_TTL_MAX; + + /* One more second to account for discrete time */ + dane->expires = 1 + event_time() + rrs->ttl; + + if (rrs->dnssec_valid) { + int n = 0; + + for (rr = rrs; rr != 0; rr = rr->next) + n += parse_tlsa_rr(dane, rr); + if (n == 0) + dane->flags |= TLS_DANE_FLAG_EMPTY; + } else + dane->flags |= TLS_DANE_FLAG_NORRS; + + if (rrs) + dns_rr_free(rrs); + break; + + case DNS_NOTFOUND: + dane->flags |= TLS_DANE_FLAG_NORRS; + dane->expires = 1 + event_time() + TLS_DANE_CACHE_TTL_MIN; + break; + + default: + msg_warn("DANE TLSA lookup problem: %s", STR(why)); + dane->flags |= TLS_DANE_FLAG_ERROR; + break; + } + + return (void *) dane; +} + +/* resolve_host - resolve TLSA RRs for hostname (rname or qname) */ + +static TLS_DANE *resolve_host(const char *host, const char *proto, + unsigned port) +{ + static VSTRING *query_domain; + TLS_DANE *dane; + + if (query_domain == 0) + query_domain = vstring_alloc(64); + + vstring_sprintf(query_domain, "_%u._%s.%s", ntohs(port), proto, host); + dane = (TLS_DANE *) ctable_locate(dane_cache, STR(query_domain)); + if (timecmp(event_time(), dane->expires) > 0) + dane = (TLS_DANE *) ctable_refresh(dane_cache, STR(query_domain)); + if (dane->base_domain == 0) + dane->base_domain = mystrdup(host); + /* Increment ref-count of cached entry */ + ++dane->refs; + return (dane); +} + +/* qname_secure - Lookup qname DNSSEC status */ + +static int qname_secure(const char *qname) +{ + static VSTRING *why; + int ret = 0; + DNS_RR *rrs; + + if (!why) + why = vstring_alloc(10); + + /* + * We assume that qname is already an fqdn, and does not need any + * suffixes from RES_DEFNAME or RES_DNSRCH. This is typically the name + * of an MX host, and must be a complete DNS name. DANE initialization + * code in the SMTP client is responsible for checking that the default + * resolver flags do not include RES_DEFNAME and RES_DNSRCH. + */ + ret = dns_lookup(qname, T_CNAME, RES_USE_DNSSEC, &rrs, 0, why); + if (ret == DNS_OK) { + ret = rrs->dnssec_valid; + dns_rr_free(rrs); + return (ret); + } + if (ret == DNS_NOTFOUND) + vstring_sprintf(why, "no longer a CNAME"); + msg_warn("DNSSEC status lookup error for %s: %s", qname, STR(why)); + return (-1); +} + +/* tls_dane_resolve - cached map: (name, proto, port) -> TLS_DANE */ + +TLS_DANE *tls_dane_resolve(unsigned port, const char *proto, DNS_RR *hostrr, + int forcetlsa) +{ + TLS_DANE *dane = 0; + int iscname = strcasecmp(hostrr->rname, hostrr->qname); + int isvalid = 1; + + if (!tls_dane_avail()) + return (0); /* Error */ + + /* + * By default suppress TLSA lookups for hosts in non-DNSSEC zones. If + * the host zone is not DNSSEC validated, the TLSA qname sub-domain is + * safely assumed to not be in a DNSSEC Look-aside Validation child zone. + */ + if (!forcetlsa && !hostrr->dnssec_valid) { + isvalid = iscname ? qname_secure(hostrr->qname) : 0; + if (isvalid < 0) + return (0); /* Error */ + } + if (!isvalid) { + dane = tls_dane_alloc(); + dane->flags = TLS_DANE_FLAG_NORRS; + } else { + if (!dane_cache) + dane_cache = ctable_create(CACHE_SIZE, dane_lookup, dane_free, 0); + + /* + * Try the rname first if secure, if nothing there, try the qname if + * different. Note, lookup errors are distinct from success with + * nothing found. If the rname lookup fails we don't try the qname. + */ + if (hostrr->dnssec_valid) { + dane = resolve_host(hostrr->rname, proto, port); + if (tls_dane_notfound(dane) && iscname) { + tls_dane_free(dane); + dane = 0; + } + } + if (!dane) + dane = resolve_host(hostrr->qname, proto, port); + if (dane->flags & TLS_DANE_FLAG_ERROR) { + /* We don't return this object. */ + tls_dane_free(dane); + dane = 0; + } + } + + return (dane); +} + +/* tls_dane_load_trustfile - load trust anchor certs or keys from file */ + +int tls_dane_load_trustfile(TLS_DANE *dane, const char *tafile) +{ + BIO *bp; + char *name = 0; + char *header = 0; + unsigned char *data = 0; + long len; + int tacount; + char *errtype = 0; /* if error: cert or pkey? */ + + /* nop */ + if (tafile == 0 || *tafile == 0) + return (1); + + /* + * On each call, PEM_read() wraps a stdio file in a BIO_NOCLOSE bio, + * calls PEM_read_bio() and then frees the bio. It is just as easy to + * open a BIO as a stdio file, so we use BIOs and call PEM_read_bio() + * directly. + */ + if ((bp = BIO_new_file(tafile, "r")) == NULL) { + msg_warn("error opening trust anchor file: %s: %m", tafile); + return (0); + } + /* Don't report old news */ + ERR_clear_error(); + + /* + * OpenSSL implements DANE strictly, with DANE-TA(2) only matching issuer + * certificates, and never the leaf cert. We also allow the + * trust-anchors to directly match the leaf certificate or public key. + */ + for (tacount = 0; + errtype == 0 && PEM_read_bio(bp, &name, &header, &data, &len); + ++tacount) { + uint8_t daneta = DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION; + uint8_t daneee = DNS_TLSA_USAGE_DOMAIN_ISSUED_CERTIFICATE; + uint8_t mtype = DNS_TLSA_MATCHING_TYPE_NO_HASH_USED; + + if (strcmp(name, PEM_STRING_X509) == 0 + || strcmp(name, PEM_STRING_X509_OLD) == 0) { + uint8_t selector = DNS_TLSA_SELECTOR_FULL_CERTIFICATE; + + if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) + tlsa_info("TA cert as TLSA record", tafile, + daneta, selector, mtype, data, len); + dane->tlsa = + tlsa_prepend(dane->tlsa, daneta, selector, mtype, data, len); + dane->tlsa = + tlsa_prepend(dane->tlsa, daneee, selector, mtype, data, len); + } else if (strcmp(name, PEM_STRING_PUBLIC) == 0) { + uint8_t selector = DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO; + + if (log_mask & (TLS_LOG_VERBOSE | TLS_LOG_DANE)) + tlsa_info("TA pkey as TLSA record", tafile, + daneta, selector, mtype, data, len); + dane->tlsa = + tlsa_prepend(dane->tlsa, daneta, selector, mtype, data, len); + dane->tlsa = tlsa_prepend(dane->tlsa, daneee, selector, mtype, data, len); + } + + /* + * If any of these were null, PEM_read() would have failed. + */ + OPENSSL_free(name); + OPENSSL_free(header); + OPENSSL_free(data); + } + BIO_free(bp); + + if (errtype) { + tls_print_errors(); + msg_warn("error reading: %s: malformed trust-anchor %s", + tafile, errtype); + return (0); + } + if (ERR_GET_REASON(ERR_peek_last_error()) == PEM_R_NO_START_LINE) { + /* Reached end of PEM file */ + ERR_clear_error(); + return (tacount > 0); + } + /* Some other PEM read error */ + tls_print_errors(); + return (0); +} + +int tls_dane_enable(TLS_SESS_STATE *TLScontext) +{ + const TLS_DANE *dane = TLScontext->dane; + TLS_TLSA *tp; + SSL *ssl = TLScontext->con; + int usable = 0; + int ret; + + for (tp = dane->tlsa; tp != 0; tp = tp->next) { + ret = SSL_dane_tlsa_add(ssl, tp->usage, tp->selector, + tp->mtype, tp->data, tp->length); + if (ret > 0) { + ++usable; + continue; + } + if (ret == 0) { + tlsa_carp(TLScontext->namaddr, ":", "", "unusable TLSA RR", + tp->usage, tp->selector, tp->mtype, tp->data, + tp->length); + continue; + } + /* Internal problem in OpenSSL */ + tlsa_carp(TLScontext->namaddr, ":", "", "error loading trust settings", + tp->usage, tp->selector, tp->mtype, tp->data, tp->length); + tls_print_errors(); + return (-1); + } + return (usable); +} + +/* tls_dane_digest_init - configure supported DANE digests */ + +void tls_dane_digest_init(SSL_CTX *ctx, const EVP_MD *fpt_alg) +{ + dane_mtype mtypes[256]; + char *cp; + char *save; + char *algname; + uint8_t m; + uint8_t ord = 0; + uint8_t maxtype; + + memset((char *) mtypes, 0, sizeof(mtypes)); + + /* + * The DANE SHA2-256(1) and SHA2-512(2) algorithms are disabled, unless + * explicitly enabled. Other codepoints can be disabled explicitly by + * giving them an empty digest name, which also implicitly disables all + * smaller codepoints that are not explicitly assigned. + * + * We reserve the private-use code point (255) for use with fingerprint + * matching. It MUST NOT be accepted in DNS replies. + */ + mtypes[1].alg = NULL; + mtypes[2].alg = NULL; + mtypes[255].alg = fpt_alg; + maxtype = 2; + + save = cp = mystrdup(var_tls_dane_digests); + while ((algname = mystrtok(&cp, CHARS_COMMA_SP)) != 0) { + char *algcode = split_at(algname, '='); + int codepoint = -1; + + if (algcode && *algcode) { + unsigned long l; + char *endcp; + + /* + * XXX: safe_strtoul() does not flag empty or white-space only + * input. Since we get algcode by splitting white-space/comma + * delimited tokens, this is not a problem here. + */ + l = safe_strtoul(algcode, &endcp, 10); + if ((l == 0 && (errno == EINVAL || endcp == algcode)) + || l >= 255 || *endcp) { + msg_warn("Invalid matching type number in %s: %s=%s", + VAR_TLS_DANE_DIGESTS, algname, algcode); + continue; + } + if (l == 0 || l == 255) { + msg_warn("Reserved matching type number in %s: %s=%s", + VAR_TLS_DANE_DIGESTS, algname, algcode); + continue; + } + codepoint = l; + } + /* Disable any codepoint gaps */ + if (codepoint > maxtype) { + while (++maxtype < codepoint) + mtypes[codepoint].alg = NULL; + maxtype = codepoint; + } + /* Handle explicitly disabled codepoints */ + if (*algname == 0) { + /* Skip empty specifiers */ + if (codepoint < 0) + continue; + mtypes[codepoint].alg = NULL; + continue; + } + switch (codepoint) { + case -1: + if (strcasecmp(algname, LN_sha256) == 0) + codepoint = 1; /* SHA2-256(1) */ + else if (strcasecmp(algname, LN_sha512) == 0) + codepoint = 2; /* SHA2-512(2) */ + else { + msg_warn("%s: digest algorithm %s needs an explicit number", + VAR_TLS_DANE_DIGESTS, algname); + continue; + } + break; + case 1: + if (strcasecmp(algname, LN_sha256) != 0) { + msg_warn("%s: matching type 1 can only be %s", + VAR_TLS_DANE_DIGESTS, LN_sha256); + continue; + } + algname = LN_sha256; + break; + case 2: + if (strcasecmp(algname, LN_sha512) != 0) { + msg_warn("%s: matching type 2 can only be %s", + VAR_TLS_DANE_DIGESTS, LN_sha512); + continue; + } + algname = LN_sha512; + break; + default: + break; + } + + if (mtypes[codepoint].ord != 0) { + msg_warn("%s: matching type %d specified more than once", + VAR_TLS_DANE_DIGESTS, codepoint); + continue; + } + mtypes[codepoint].ord = ++ord; + + if ((mtypes[codepoint].alg = tls_digest_byname(algname, NULL)) == 0) { + msg_warn("%s: digest algorithm \"%s\"(%d) unknown", + VAR_TLS_DANE_DIGESTS, algname, codepoint); + continue; + } + } + myfree(save); + + for (m = 1; m != 0; m = m != maxtype ? m + 1 : 255) { + + /* + * In OpenSSL higher order ordinals are preferred, but we list the + * most preferred algorithms first, so the last ordinal becomes 1, + * next-to-last, 2, ... + * + * The ordinals of non-disabled algorithms are always positive, and the + * computed value cannot overflow 254 (the largest possible value of + * 'ord' after loading each valid codepoint at most once). + */ + if (SSL_CTX_dane_mtype_set(ctx, mtypes[m].alg, m, + ord - mtypes[m].ord + 1) <= 0) { + msg_warn("%s: error configuring matching type %d", + VAR_TLS_DANE_DIGESTS, m); + tls_print_errors(); + } + } +} + +/* tls_dane_log - log DANE-based verification success */ + +void tls_dane_log(TLS_SESS_STATE *TLScontext) +{ + static VSTRING *top; + static VSTRING *bot; + EVP_PKEY *mspki = 0; + int depth = SSL_get0_dane_authority(TLScontext->con, NULL, &mspki); + uint8_t u, s, m; + unsigned const char *data; + size_t dlen; + + if (depth < 0) + return; /* No DANE auth */ + + switch (TLScontext->level) { + case TLS_LEV_SECURE: + case TLS_LEV_VERIFY: + msg_info("%s: Matched trust anchor at depth %d", + TLScontext->namaddr, depth); + return; + } + + if (top == 0) + top = vstring_alloc(2 * MAX_HEAD_BYTES); + if (bot == 0) + bot = vstring_alloc(2 * MAX_TAIL_BYTES); + + (void) SSL_get0_dane_tlsa(TLScontext->con, &u, &s, &m, &data, &dlen); + if (dlen > MAX_DUMP_BYTES) { + hex_encode(top, (char *) data, MAX_HEAD_BYTES); + hex_encode(bot, (char *) data + dlen - MAX_TAIL_BYTES, MAX_TAIL_BYTES); + } else { + hex_encode(top, (char *) data, dlen); + } + + switch (TLScontext->level) { + case TLS_LEV_FPRINT: + msg_info("%s: Matched fingerprint: %s%s%s", TLScontext->namaddr, + STR(top), dlen > MAX_DUMP_BYTES ? "..." : "", + dlen > MAX_DUMP_BYTES ? STR(bot) : ""); + return; + + default: + msg_info("%s: Matched DANE %s at depth %d: %u %u %u %s%s%s", + TLScontext->namaddr, mspki ? + "TA public key verified certificate" : depth ? + "TA certificate" : "EE certificate", depth, u, s, m, + STR(top), dlen > MAX_DUMP_BYTES ? "..." : "", + dlen > MAX_DUMP_BYTES ? STR(bot) : ""); + return; + } +} + +#ifdef TEST + +#include <unistd.h> +#include <stdarg.h> + +#include <mail_params.h> +#include <mail_conf.h> +#include <msg_vstream.h> + +static int verify_chain(SSL *ssl, x509_stack_t *chain, TLS_SESS_STATE *tctx) +{ + int ret; + X509 *cert; + X509_STORE_CTX *store_ctx; + SSL_CTX *ssl_ctx = SSL_get_SSL_CTX(ssl); + X509_STORE *store = SSL_CTX_get_cert_store(ssl_ctx); + int store_ctx_idx = SSL_get_ex_data_X509_STORE_CTX_idx(); + + cert = sk_X509_value(chain, 0); + if ((store_ctx = X509_STORE_CTX_new()) == NULL) { + SSLerr(SSL_F_SSL_VERIFY_CERT_CHAIN, ERR_R_MALLOC_FAILURE); + return 0; + } + if (!X509_STORE_CTX_init(store_ctx, store, cert, chain)) { + X509_STORE_CTX_free(store_ctx); + return 0; + } + X509_STORE_CTX_set_ex_data(store_ctx, store_ctx_idx, ssl); + + /* We're *verifying* a server chain */ + X509_STORE_CTX_set_default(store_ctx, "ssl_server"); + X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(store_ctx), + SSL_get0_param(ssl)); + X509_STORE_CTX_set0_dane(store_ctx, SSL_get0_dane(ssl)); + + ret = X509_verify_cert(store_ctx); + + SSL_set_verify_result(ssl, X509_STORE_CTX_get_error(store_ctx)); + X509_STORE_CTX_free(store_ctx); + + return (ret); +} + +static void load_tlsa_args(SSL *ssl, char *argv[]) +{ + const EVP_MD *md = 0; + X509 *cert = 0; + BIO *bp; + unsigned char *buf; + unsigned char *buf2; + int len; + uint8_t u = atoi(argv[1]); + uint8_t s = atoi(argv[2]); + uint8_t m = atoi(argv[3]); + EVP_PKEY *pkey; + + /* Unsupported usages are fatal */ + switch (u) { + case DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION: + case DNS_TLSA_USAGE_DOMAIN_ISSUED_CERTIFICATE: + break; + default: + msg_fatal("unsupported certificate usage %u", u); + } + + /* Unsupported selectors are fatal */ + switch (s) { + case DNS_TLSA_SELECTOR_FULL_CERTIFICATE: + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + break; + default: + msg_fatal("unsupported selector %u", s); + } + + /* Unsupported selectors are fatal */ + switch (m) { + case DNS_TLSA_MATCHING_TYPE_NO_HASH_USED: + case DNS_TLSA_MATCHING_TYPE_SHA256: + case DNS_TLSA_MATCHING_TYPE_SHA512: + break; + default: + msg_fatal("unsupported matching type %u", m); + } + + if ((bp = BIO_new_file(argv[4], "r")) == NULL) + msg_fatal("error opening %s: %m", argv[4]); + if (!PEM_read_bio_X509(bp, &cert, 0, 0)) { + tls_print_errors(); + msg_fatal("error loading certificate from %s: %m", argv[4]); + } + BIO_free(bp); + + /* + * Extract ASN.1 DER form of certificate or public key. + */ + switch (s) { + case DNS_TLSA_SELECTOR_FULL_CERTIFICATE: + len = i2d_X509(cert, NULL); + if (len > 0xffff) + msg_fatal("certificate too long: %d", len); + buf2 = buf = (unsigned char *) mymalloc(len); + i2d_X509(cert, &buf2); + break; + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + pkey = X509_get_pubkey(cert); + len = i2d_PUBKEY(pkey, NULL); + if (len > 0xffff) + msg_fatal("public key too long: %d", len); + buf2 = buf = (unsigned char *) mymalloc(len); + i2d_PUBKEY(pkey, &buf2); + EVP_PKEY_free(pkey); + break; + } + X509_free(cert); + OPENSSL_assert(buf2 - buf == len); + + switch (m) { + case 0: + break; + case 1: + if ((md = tls_digest_byname(LN_sha256, NULL)) == 0) + msg_fatal("Digest %s not found", LN_sha256); + break; + case 2: + if ((md = tls_digest_byname(LN_sha512, NULL)) == 0) + msg_fatal("Digest %s not found", LN_sha512); + break; + default: + msg_fatal("Unsupported DANE mtype: %d", m); + } + + if (md != 0) { + unsigned char mdbuf[EVP_MAX_MD_SIZE]; + unsigned int mdlen = sizeof(mdbuf); + + if (!EVP_Digest(buf, len, mdbuf, &mdlen, md, 0)) + msg_fatal("Digest failure for mtype: %d", m); + myfree(buf); + buf = (unsigned char *) mymemdup(mdbuf, len = mdlen); + } + SSL_dane_tlsa_add(ssl, u, s, m, buf, len); + myfree((void *) buf); +} + +static x509_stack_t *load_chain(const char *chainfile) +{ + BIO *bp; + char *name = 0; + char *header = 0; + unsigned char *data = 0; + long len; + int count; + char *errtype = 0; /* if error: cert or pkey? */ + x509_stack_t *chain; + typedef X509 *(*d2i_X509_t) (X509 **, const unsigned char **, long); + + if ((chain = sk_X509_new_null()) == 0) { + perror("malloc"); + exit(1); + } + + /* + * On each call, PEM_read() wraps a stdio file in a BIO_NOCLOSE bio, + * calls PEM_read_bio() and then frees the bio. It is just as easy to + * open a BIO as a stdio file, so we use BIOs and call PEM_read_bio() + * directly. + */ + if ((bp = BIO_new_file(chainfile, "r")) == NULL) { + fprintf(stderr, "error opening chainfile: %s: %m\n", chainfile); + exit(1); + } + /* Don't report old news */ + ERR_clear_error(); + + for (count = 0; + errtype == 0 && PEM_read_bio(bp, &name, &header, &data, &len); + ++count) { + const unsigned char *p = data; + + if (strcmp(name, PEM_STRING_X509) == 0 + || strcmp(name, PEM_STRING_X509_TRUSTED) == 0 + || strcmp(name, PEM_STRING_X509_OLD) == 0) { + d2i_X509_t d; + X509 *cert; + + d = strcmp(name, PEM_STRING_X509_TRUSTED) ? d2i_X509_AUX : d2i_X509; + if ((cert = d(0, &p, len)) == 0 || (p - data) != len) + errtype = "certificate"; + else if (sk_X509_push(chain, cert) == 0) { + perror("malloc"); + exit(1); + } + } else { + fprintf(stderr, "unexpected chain file object: %s\n", name); + exit(1); + } + + /* + * If any of these were null, PEM_read() would have failed. + */ + OPENSSL_free(name); + OPENSSL_free(header); + OPENSSL_free(data); + } + BIO_free(bp); + + if (errtype) { + tls_print_errors(); + fprintf(stderr, "error reading: %s: malformed %s", chainfile, errtype); + exit(1); + } + if (ERR_GET_REASON(ERR_peek_last_error()) == PEM_R_NO_START_LINE) { + /* Reached end of PEM file */ + ERR_clear_error(); + if (count > 0) + return chain; + fprintf(stderr, "no certificates found in: %s\n", chainfile); + exit(1); + } + /* Some other PEM read error */ + tls_print_errors(); + fprintf(stderr, "error reading: %s\n", chainfile); + exit(1); +} + +static void usage(const char *progname) +{ + fprintf(stderr, "Usage: %s certificate-usage selector matching-type" + " certfile \\\n\t\tCAfile chainfile hostname [certname ...]\n", + progname); + fprintf(stderr, " where, certificate-usage = TLSA certificate usage,\n"); + fprintf(stderr, "\t selector = TLSA selector,\n"); + fprintf(stderr, "\t matching-type = empty string or OpenSSL digest algorithm name,\n"); + fprintf(stderr, "\t PEM certfile provides certificate association data,\n"); + fprintf(stderr, "\t PEM CAfile contains any usage 0/1 trusted roots,\n"); + fprintf(stderr, "\t PEM chainfile = server chain file to verify\n"); + fprintf(stderr, "\t hostname = destination hostname,\n"); + fprintf(stderr, "\t each certname augments the hostname for name checks.\n"); + exit(1); +} + +static SSL_CTX *ctx_init(const char *CAfile) +{ + SSL_CTX *client_ctx; + + tls_param_init(); + tls_check_version(); + + if (!tls_validate_digest(LN_sha1)) + msg_fatal("%s digest algorithm not available", LN_sha1); + + if (TLScontext_index < 0) + if ((TLScontext_index = SSL_get_ex_new_index(0, 0, 0, 0, 0)) < 0) + msg_fatal("Cannot allocate SSL application data index"); + + ERR_clear_error(); + if ((client_ctx = SSL_CTX_new(TLS_client_method())) == 0) + msg_fatal("cannot allocate client SSL_CTX"); + SSL_CTX_set_verify_depth(client_ctx, 5); + + /* Enable DANE support in OpenSSL */ + if (SSL_CTX_dane_enable(client_ctx) <= 0) { + tls_print_errors(); + msg_fatal("OpenSSL DANE initialization failed"); + } + if (tls_set_ca_certificate_info(client_ctx, CAfile, "") < 0) { + tls_print_errors(); + msg_fatal("cannot load CAfile: %s", CAfile); + } + SSL_CTX_set_verify(client_ctx, SSL_VERIFY_NONE, + tls_verify_certificate_callback); + return (client_ctx); +} + +int main(int argc, char *argv[]) +{ + SSL_CTX *ssl_ctx; + const EVP_MD *fpt_alg; + TLS_SESS_STATE *tctx; + x509_stack_t *chain; + int i; + + var_procname = mystrdup(basename(argv[0])); + set_mail_conf_str(VAR_PROCNAME, var_procname); + msg_vstream_init(var_procname, VSTREAM_OUT); + + if (argc < 8) + usage(argv[0]); + + ssl_ctx = ctx_init(argv[5]); + if (!tls_dane_avail()) + msg_fatal("DANE TLSA support not available"); + + tctx = tls_alloc_sess_context(TLS_LOG_NONE, argv[7]); + tctx->namaddr = argv[7]; + tctx->mdalg = LN_sha256; + tctx->dane = tls_dane_alloc(); + + if ((fpt_alg = tls_validate_digest(tctx->mdalg)) == 0) + msg_fatal("fingerprint digest algorithm %s not found", + tctx->mdalg); + tls_dane_digest_init(ssl_ctx, fpt_alg); + + if ((tctx->con = SSL_new(ssl_ctx)) == 0 + || !SSL_set_ex_data(tctx->con, TLScontext_index, tctx)) { + tls_print_errors(); + msg_fatal("Error allocating SSL connection"); + } + if (SSL_dane_enable(tctx->con, 0) <= 0) { + tls_print_errors(); + msg_fatal("Error enabling DANE for SSL handle"); + } + SSL_dane_set_flags(tctx->con, DANE_FLAG_NO_DANE_EE_NAMECHECKS); + SSL_dane_set_flags(tctx->con, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS); + for (i = 7; i < argc; ++i) + if (!SSL_add1_host(tctx->con, argv[i])) + msg_fatal("error adding hostname: %s", argv[i]); + load_tlsa_args(tctx->con, argv); + SSL_set_connect_state(tctx->con); + + /* Verify saved server chain */ + chain = load_chain(argv[6]); + i = verify_chain(tctx->con, chain, tctx); + tls_print_errors(); + + if (i > 0) { + const char *peername = SSL_get0_peername(tctx->con); + + if (peername == 0) + peername = argv[7]; + msg_info("Verified %s", peername); + } else { + i = SSL_get_verify_result(tctx->con); + msg_info("certificate verification failed for %s:%s: num=%d:%s", + argv[6], argv[7], i, X509_verify_cert_error_string(i)); + } + + return (i <= 0); +} + +#endif /* TEST */ + +#endif /* USE_TLS */ |