diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:06:34 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-27 12:06:34 +0000 |
commit | 5e61585d76ae77fd5e9e96ebabb57afa4d74880d (patch) | |
tree | 2b467823aaeebc7ef8bc9e3cabe8074eaef1666d /src/tls/tls_dane.c | |
parent | Initial commit. (diff) | |
download | postfix-5b7b6342ca8708be5ee306c089f8c5b3d3d122d8.tar.xz postfix-5b7b6342ca8708be5ee306c089f8c5b3d3d122d8.zip |
Adding upstream version 3.5.24.upstream/3.5.24upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/tls/tls_dane.c | 2080 |
1 files changed, 2080 insertions, 0 deletions
diff --git a/src/tls/tls_dane.c b/src/tls/tls_dane.c new file mode 100644 index 0000000..3375905 --- /dev/null +++ b/src/tls/tls_dane.c @@ -0,0 +1,2080 @@ +/*++ +/* NAME +/* tls_dane 3 +/* SUMMARY +/* Support for RFC 6698, 7671, 7672 (DANE) certificate matching +/* SYNOPSIS +/* #include <tls.h> +/* +/* int tls_dane_avail() +/* +/* void tls_dane_flush() +/* +/* void tls_dane_verbose(on) +/* int on; +/* +/* TLS_DANE *tls_dane_alloc() +/* +/* void tls_dane_free(dane) +/* TLS_DANE *dane; +/* +/* void tls_dane_add_ee_digests(dane, mdalg, digest, delim) +/* TLS_DANE *dane; +/* const char *mdalg; +/* const char *digest; +/* const char *delim; +/* +/* int tls_dane_load_trustfile(dane, tafile) +/* TLS_DANE *dane; +/* const char *tafile; +/* +/* int tls_dane_match(TLSContext, usage, cert, depth) +/* TLS_SESS_STATE *TLScontext; +/* int usage; +/* X509 *cert; +/* int depth; +/* +/* void tls_dane_set_callback(ssl_ctx, TLScontext) +/* SSL_CTX *ssl_ctx; +/* TLS_SESS_STATE *TLScontext; +/* +/* TLS_DANE *tls_dane_resolve(port, proto, hostrr, forcetlsa) +/* unsigned port; +/* const char *proto; +/* DNS_RR *hostrr; +/* int forcetlsa; +/* +/* int tls_dane_unusable(dane) +/* const TLS_DANE *dane; +/* +/* int tls_dane_notfound(dane) +/* const TLS_DANE *dane; +/* DESCRIPTION +/* tls_dane_avail() returns true if the features required to support DANE +/* are present in OpenSSL's libcrypto and in libresolv. Since OpenSSL's +/* libcrypto is not initialized until we call tls_client_init(), calls +/* to tls_dane_avail() must be deferred until this initialization is +/* completed successufully. +/* +/* tls_dane_flush() flushes all entries from the cache, and deletes +/* the cache. +/* +/* tls_dane_verbose() turns on verbose logging of TLSA record lookups. +/* +/* tls_dane_alloc() returns a pointer to a newly allocated TLS_DANE +/* structure with null ta and ee digest sublists. +/* +/* tls_dane_free() frees the structure allocated by tls_dane_alloc(). +/* +/* tls_dane_add_ee_digests() splits "digest" using the characters in +/* "delim" as delimiters and stores the results on the EE match list +/* to match either a certificate or a public key. 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_match() matches the full and/or public key digest of +/* "cert" against each candidate digest in TLScontext->dane. If usage +/* is TLS_DANE_EE, the match is against end-entity digests, otherwise +/* it is against trust-anchor digests. Returns true if a match is found, +/* false otherwise. +/* +/* tls_dane_set_callback() wraps the SSL certificate verification logic +/* in a function that modifies the input trust chain and trusted +/* certificate store to map DANE TA validation onto the existing PKI +/* verification model. When TLScontext is NULL the callback is +/* cleared, otherwise it is set. This callback should only be set +/* when out-of-band trust-anchors (via DNSSEC DANE TLSA records or +/* per-destination local configuration) are provided. Such trust +/* anchors always override the legacy public CA PKI. Otherwise, the +/* callback MUST be cleared. +/* +/* 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_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 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 TLScontext +/* Client context with TA/EE matching data and related state. +/* .IP usage +/* Trust anchor (TLS_DANE_TA) or end-entity (TLS_DANE_EE) digests? +/* .IP cert +/* Certificate from peer trust chain (CA or leaf server). +/* .IP depth +/* The certificate depth for logging. +/* .IP ssl_ctx +/* The global SSL_CTX structure used to initialize child SSL +/* conenctions. +/* .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 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. +/* 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 <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 defined(TLSEXT_MAXLEN_host_name) && RES_USE_DNSSEC && RES_USE_EDNS0 +#define DANE_TLSA_SUPPORT +static int dane_tlsa_support = 1; + +#else +static int dane_tlsa_support = 0; + +#endif + +static const char *signalg; +static ASN1_OBJECT *serverAuth; + +/* + * https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml + */ +typedef struct { + const char *mdalg; + uint8_t dane_id; +} iana_digest; + +static iana_digest iana_table[] = { + {"", DNS_TLSA_MATCHING_TYPE_NO_HASH_USED}, + {"sha256", DNS_TLSA_MATCHING_TYPE_SHA256}, + {"sha512", DNS_TLSA_MATCHING_TYPE_SHA512}, + {0, 0} +}; + +typedef struct dane_digest { + struct dane_digest *next; /* linkage */ + const char *mdalg; /* OpenSSL name */ + const EVP_MD *md; /* OpenSSL EVP handle */ + int len; /* digest octet length */ + int pref; /* tls_dane_digests index or -1 */ + uint8_t dane_id; /* IANA id */ +} dane_digest; + +#define MAXDIGESTS 256 /* RFC limit */ +static dane_digest *digest_list; + +/* + * 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 dane_initialized; +static int dane_verbose; + +/* tls_dane_verbose - enable/disable verbose logging */ + +void tls_dane_verbose(int on) +{ + dane_verbose = on; +} + +/* add_digest - validate and append digest to digest list */ + +static dane_digest *add_digest(char *mdalg, int pref) +{ + iana_digest *i; + dane_digest *d; + int dane_id = -1; + const char *dane_mdalg = mdalg; + char *value = split_at(mdalg, '='); + const EVP_MD *md = 0; + size_t mdlen = 0; + + if (value && *value) { + unsigned long l; + char *endcp; + + /* + * XXX: safe_strtoul() does not flag empty or white-space only input. + * Since we get idbuf by splitting white-space/comma delimited + * tokens, this is not a problem here. Fixed as of 210131209. + */ + l = safe_strtoul(value, &endcp, 10); + if ((l == 0 && (errno == EINVAL || endcp == value)) + || l >= MAXDIGESTS + || *endcp) { + msg_warn("Invalid matching type number in %s: %s=%s", + VAR_TLS_DANE_DIGESTS, mdalg, value); + return (0); + } + dane_id = l; + } + + /* + * Check for known IANA conflicts + */ + for (i = iana_table; i->mdalg; ++i) { + if (*mdalg && strcasecmp(i->mdalg, mdalg) == 0) { + if (dane_id >= 0 && i->dane_id != dane_id) { + msg_warn("Non-standard value in %s: %s%s%s", + VAR_TLS_DANE_DIGESTS, mdalg, + value ? "=" : "", value ? value : ""); + return (0); + } + dane_id = i->dane_id; + } else if (i->dane_id == dane_id) { + if (*mdalg) { + msg_warn("Non-standard algorithm in %s: %s%s%s", + VAR_TLS_DANE_DIGESTS, mdalg, + value ? "=" : "", value ? value : ""); + return (0); + } + dane_mdalg = i->mdalg; + } + } + + /* + * Check for unknown implicit digest or value + */ + if (dane_id < 0 || (dane_id > 0 && !*dane_mdalg)) { + msg_warn("Unknown incompletely specified element in %s: %s%s%s", + VAR_TLS_DANE_DIGESTS, mdalg, + value ? "=" : "", value ? value : ""); + return 0; + } + + /* + * Check for duplicate entries + */ + for (d = digest_list; d; d = d->next) { + if (strcasecmp(d->mdalg, dane_mdalg) == 0 + || d->dane_id == dane_id) { + msg_warn("Duplicate element in %s: %s%s%s", + VAR_TLS_DANE_DIGESTS, mdalg, + value ? "=" : "", value ? value : ""); + return (0); + } + } + + if (*dane_mdalg + && ((md = tls_digest_byname(dane_mdalg, NULL)) == 0 + || (mdlen = EVP_MD_size(md)) <= 0 + || mdlen > EVP_MAX_MD_SIZE)) { + msg_warn("Unimplemented digest algorithm in %s: %s%s%s", + VAR_TLS_DANE_DIGESTS, mdalg, + value ? "=" : "", value ? value : ""); + return (0); + } + d = (dane_digest *) mymalloc(sizeof(*d)); + d->next = digest_list; + d->mdalg = mystrdup(dane_mdalg); + d->md = md; + d->len = mdlen; + d->pref = pref; + d->dane_id = dane_id; + + return (digest_list = d); +} + +/* digest_byid - locate digest_table entry for given IANA id */ + +static dane_digest *digest_byid(uint8_t dane_id) +{ + dane_digest *d; + + for (d = digest_list; d; d = d->next) + if (d->dane_id == dane_id) + return (d); + return (0); +} + +/* digest_pref_byid - digest preference by IANA id */ + +static int digest_pref_byid(uint8_t dane_id) +{ + dane_digest *d = digest_byid(dane_id); + + return (d ? (d->pref) : (MAXDIGESTS + dane_id)); +} + +/* dane_init - initialize DANE parameters */ + +static void dane_init(void) +{ + int digest_pref = 0; + char *cp; + char *save; + char *tok; + static char fullmtype[] = "=0"; + dane_digest *d; + + /* + * Add the full matching type at highest preference and then the users + * configured list. + * + * The most preferred digest will be used for hashing full values for + * comparison. + */ + if (add_digest(fullmtype, 0)) { + save = cp = mystrdup(var_tls_dane_digests); + while ((tok = mystrtok(&cp, CHARS_COMMA_SP)) != 0) { + if ((d = add_digest(tok, ++digest_pref)) == 0) { + signalg = 0; + break; + } + if (digest_pref == 1) { + signalg = d->mdalg; + } + } + myfree(save); + } + /* Don't report old news */ + ERR_clear_error(); + + /* + * DANE TLSA support requires working DANE digests. + */ + if ((serverAuth = OBJ_nid2obj(NID_server_auth)) == 0) { + msg_warn("cannot designate intermediate TA certificates, " + "no DANE support"); + tls_print_errors(); + dane_tlsa_support = 0; + } else if (signalg == 0) { + msg_warn("digest algorithm initializaton failed, no DANE support"); + tls_print_errors(); + dane_tlsa_support = 0; + } + dane_initialized = 1; +} + +/* tls_dane_avail - check for availability of dane required digests */ + +int tls_dane_avail(void) +{ + if (!dane_initialized) + dane_init(); + return (dane_tlsa_support); +} + +/* tls_dane_flush - flush the cache */ + +void tls_dane_flush(void) +{ + if (dane_cache) + ctable_free(dane_cache); + dane_cache = 0; +} + +/* tls_dane_alloc - allocate a TLS_DANE structure */ + +TLS_DANE *tls_dane_alloc(void) +{ + TLS_DANE *dane = (TLS_DANE *) mymalloc(sizeof(*dane)); + + dane->ta = 0; + dane->ee = 0; + dane->certs = 0; + dane->pkeys = 0; + dane->base_domain = 0; + dane->flags = 0; + dane->expires = 0; + dane->refs = 1; + return (dane); +} + +static void ta_cert_insert(TLS_DANE *d, X509 *x) +{ + TLS_CERTS *new = (TLS_CERTS *) mymalloc(sizeof(*new)); + + X509_up_ref(x); + new->cert = x; + new->next = d->certs; + d->certs = new; +} + +static void free_ta_certs(TLS_DANE *d) +{ + TLS_CERTS *head; + TLS_CERTS *next; + + for (head = d->certs; head; head = next) { + next = head->next; + X509_free(head->cert); + myfree((void *) head); + } +} + +static void ta_pkey_insert(TLS_DANE *d, EVP_PKEY *k) +{ + TLS_PKEYS *new = (TLS_PKEYS *) mymalloc(sizeof(*new)); + + EVP_PKEY_up_ref(k); + new->pkey = k; + new->next = d->pkeys; + d->pkeys = new; +} + +static void free_ta_pkeys(TLS_DANE *d) +{ + TLS_PKEYS *head; + TLS_PKEYS *next; + + for (head = d->pkeys; head; head = next) { + next = head->next; + EVP_PKEY_free(head->pkey); + myfree((void *) head); + } +} + +static void tlsa_free(TLS_TLSA *tlsa) +{ + + myfree(tlsa->mdalg); + if (tlsa->certs) + argv_free(tlsa->certs); + if (tlsa->pkeys) + argv_free(tlsa->pkeys); + myfree((void *) tlsa); +} + +/* tls_dane_free - free a TLS_DANE structure */ + +void tls_dane_free(TLS_DANE *dane) +{ + TLS_TLSA *tlsa; + TLS_TLSA *next; + + if (--dane->refs > 0) + return; + + /* De-allocate TA and EE lists */ + for (tlsa = dane->ta; tlsa; tlsa = next) { + next = tlsa->next; + tlsa_free(tlsa); + } + for (tlsa = dane->ee; tlsa; tlsa = next) { + next = tlsa->next; + tlsa_free(tlsa); + } + + /* De-allocate full trust-anchor certs and pkeys */ + free_ta_certs(dane); + free_ta_pkeys(dane); + if (dane->base_domain) + myfree(dane->base_domain); + + myfree((void *) dane); +} + +/* dane_free - ctable style */ + +static void dane_free(void *dane, void *unused_context) +{ + tls_dane_free((TLS_DANE *) dane); +} + +/* dane_locate - list head address of TLSA sublist for given algorithm */ + +static TLS_TLSA **dane_locate(TLS_TLSA **tlsap, const char *mdalg) +{ + TLS_TLSA *new; + + /* + * Correct computation of the session cache serverid requires a TLSA + * digest list that is sorted by algorithm name. Below we maintain the + * sort order (by algorithm name canonicalized to lowercase). + */ + for (; *tlsap; tlsap = &(*tlsap)->next) { + int cmp = strcasecmp(mdalg, (*tlsap)->mdalg); + + if (cmp == 0) + return (tlsap); + if (cmp < 0) + break; + } + + new = (TLS_TLSA *) mymalloc(sizeof(*new)); + new->mdalg = lowercase(mystrdup(mdalg)); + new->certs = 0; + new->pkeys = 0; + new->next = *tlsap; + *tlsap = new; + + return (tlsap); +} + +/* tls_dane_add_ee_digests - split and append digests */ + +void tls_dane_add_ee_digests(TLS_DANE *dane, const char *mdalg, + const char *digest, const char *delim) +{ + TLS_TLSA **tlsap = dane_locate(&dane->ee, mdalg); + TLS_TLSA *tlsa = *tlsap; + + /* Delimited append, may append nothing */ + if (tlsa->pkeys == 0) + tlsa->pkeys = argv_split(digest, delim); + else + argv_split_append(tlsa->pkeys, digest, delim); + + /* Remove empty elements from the list */ + if (tlsa->pkeys->argc == 0) { + argv_free(tlsa->pkeys); + tlsa->pkeys = 0; + + if (tlsa->certs == 0) { + *tlsap = tlsa->next; + tlsa_free(tlsa); + } + return; + } + + /* + * 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. + */ + if (tlsa->certs == 0) + tlsa->certs = argv_split(digest, delim); + else + argv_split_append(tlsa->certs, digest, delim); +} + +/* dane_add - add a digest entry */ + +static void dane_add(TLS_DANE *dane, int certusage, int selector, + const char *mdalg, char *digest) +{ + TLS_TLSA **tlsap; + TLS_TLSA *tlsa; + ARGV **argvp; + + switch (certusage) { + case DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION: + certusage = TLS_DANE_TA; + break; + case DNS_TLSA_USAGE_DOMAIN_ISSUED_CERTIFICATE: + certusage = TLS_DANE_EE; /* Collapse 1/3 -> 3 */ + break; + default: + msg_panic("Unsupported DANE certificate usage: %d", certusage); + } + + switch (selector) { + case DNS_TLSA_SELECTOR_FULL_CERTIFICATE: + selector = TLS_DANE_CERT; + break; + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + selector = TLS_DANE_PKEY; + break; + default: + msg_panic("Unsupported DANE selector: %d", selector); + } + + tlsap = (certusage == TLS_DANE_EE) ? &dane->ee : &dane->ta; + tlsa = *(tlsap = dane_locate(tlsap, mdalg)); + argvp = (selector == TLS_DANE_PKEY) ? &tlsa->pkeys : &tlsa->certs; + + if (*argvp == 0) + *argvp = argv_alloc(1); + argv_add(*argvp, digest, ARGV_END); +} + +#define FILTER_CTX_AGILITY_OK (1<<0) +#define FILTER_CTX_APPLY_AGILITY (1<<1) +#define FILTER_CTX_PARSE_DATA (1<<2) + +#define FILTER_RR_DROP 0 +#define FILTER_RR_KEEP 1 + +typedef struct filter_ctx { + TLS_DANE *dane; /* Parsed result */ + int count; /* Digest mtype count */ + int target; /* Digest mtype target count */ + int flags; /* Action/result bitmask */ +} filter_ctx; + +typedef int (*tlsa_filter) (DNS_RR *, filter_ctx *); + +/* tlsa_apply - apply filter to each rr in turn */ + +static DNS_RR *tlsa_apply(DNS_RR *rr, tlsa_filter filter, filter_ctx *ctx) +{ + DNS_RR *head = 0; /* First retained RR */ + DNS_RR *tail = 0; /* Last retained RR */ + DNS_RR *next; + + /* + * XXX Code that modifies or destroys DNS_RR lists or entries belongs in + * the DNS library, not here. + */ + for ( /* nop */ ; rr; rr = next) { + next = rr->next; + + if (filter(rr, ctx) == FILTER_RR_KEEP) { + tail = rr; + if (!head) + head = rr; + } else { + if (tail) + tail->next = rr->next; + rr->next = 0; + dns_rr_free(rr); + } + } + return (head); +} + +/* usmdelta - packed usage/selector/mtype bits changing in next record */ + +static unsigned int usmdelta(uint8_t u, uint8_t s, uint8_t m, DNS_RR *next) +{ + uint8_t *ip = (next && next->data_len >= 3) ? (uint8_t *) next->data : 0; + uint8_t nu = ip ? *ip++ : ~u; + uint8_t ns = ip ? *ip++ : ~s; + uint8_t nm = ip ? *ip++ : ~m; + + return (((u ^ nu) << 16) | ((s ^ ns) << 8) | (m ^ nm)); +} + +/* tlsa_rr_cmp - qsort TLSA rrs in case shuffled by name server */ + +static int tlsa_rr_cmp(DNS_RR *a, DNS_RR *b) +{ + int cmp; + + /* + * Sort in ascending order, by usage, selector, matching type preference + * and payload. The usage, selector and matching type are the first + * three unsigned octets of the RR data. + */ + if (a->data_len > 2 && b->data_len > 2) { + uint8_t *ai = (uint8_t *) a->data; + uint8_t *bi = (uint8_t *) b->data; + +#define signedcmp(x, y) (((int)(x)) - ((int)(y))) + + if ((cmp = signedcmp(ai[0], bi[0])) != 0 + || (cmp = signedcmp(ai[1], bi[1])) != 0 + || (cmp = digest_pref_byid(ai[2]) - + digest_pref_byid(bi[2])) != 0) + return (cmp); + } + if ((cmp = a->data_len - b->data_len) != 0) + return (cmp); + return (memcmp(a->data, b->data, a->data_len)); +} + +/* parse_tlsa_rr - parse a validated TLSA RRset */ + +static int parse_tlsa_rr(DNS_RR *rr, filter_ctx *ctx) +{ + uint8_t *ip; + uint8_t usage; + uint8_t selector; + uint8_t mtype; + ssize_t dlen; + const unsigned char *data; + const unsigned char *p; + int iscname = strcasecmp(rr->rname, rr->qname); + const char *q = (iscname) ? (rr)->qname : ""; + const char *a = (iscname) ? " -> " : ""; + const char *r = rr->rname; + unsigned int change; + + if (rr->type != T_TLSA) + msg_panic("unexpected non-TLSA RR type %u for %s%s%s", rr->type, + q, a, r); + + /* Drop truncated records */ + if ((dlen = rr->data_len - 3) < 0) { + msg_warn("truncated length %u RR: %s%s%s IN TLSA ...", + (unsigned) rr->data_len, q, a, r); + ctx->flags &= ~FILTER_CTX_AGILITY_OK; + return (FILTER_RR_DROP); + } + ip = (uint8_t *) rr->data; + usage = *ip++; + selector = *ip++; + mtype = *ip++; + change = usmdelta(usage, selector, mtype, rr->next); + p = data = (const unsigned char *) ip; + + /* + * Handle digest agility for non-zero matching types. + */ + if (mtype) { + if (ctx->count && (ctx->flags & FILTER_CTX_APPLY_AGILITY)) { + if (change & 0xffff00) /* New usage/selector, */ + ctx->count = 0; /* disable drop */ + return (FILTER_RR_DROP); + } + } + /*- + * 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: + msg_warn("unsupported certificate usage %u in RR: " + "%s%s%s IN TLSA %u ...", usage, + q, a, r, usage); + return (FILTER_RR_DROP); + } + + /* + * Drop unsupported selectors + */ + switch (selector) { + case DNS_TLSA_SELECTOR_FULL_CERTIFICATE: + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + break; + default: + msg_warn("unsupported selector %u in RR: " + "%s%s%s IN TLSA %u %u ...", selector, + q, a, r, usage, selector); + return (FILTER_RR_DROP); + } + + if (mtype) { + dane_digest *d = digest_byid(mtype); + + if (d == 0) { + msg_warn("unsupported matching type %u in RR: " + "%s%s%s IN TLSA %u %u %u ...", mtype, + q, a, r, usage, selector, mtype); + return (FILTER_RR_DROP); + } + if (dlen != d->len) { + msg_warn("malformed %s digest, length %lu, in RR: " + "%s%s%s IN TLSA %u %u %u ...", + d->mdalg, (unsigned long) dlen, + q, a, r, usage, selector, mtype); + ctx->flags &= ~FILTER_CTX_AGILITY_OK; + return (FILTER_RR_DROP); + } + /* New digest mtype next? Prepare to drop following RRs */ + if (change && (change & 0xffff00) == 0 + && (ctx->flags & FILTER_CTX_APPLY_AGILITY)) + ++ctx->count; + + if (ctx->flags & FILTER_CTX_PARSE_DATA) { + char *digest = tls_digest_encode(data, dlen); + + dane_add(ctx->dane, usage, selector, d->mdalg, digest); + if (msg_verbose || dane_verbose) + msg_info("using DANE RR: %s%s%s IN TLSA %u %u %u %s", + q, a, r, usage, selector, mtype, digest); + myfree(digest); + } + } else { + X509 *x = 0; /* OpenSSL re-uses *x if x!=0 */ + EVP_PKEY *k = 0; /* OpenSSL re-uses *k if k!=0 */ + + /* Validate the cert or public key via d2i_mumble() */ + switch (selector) { + case DNS_TLSA_SELECTOR_FULL_CERTIFICATE: + if (!d2i_X509(&x, &p, dlen) || dlen != p - data) { + msg_warn("malformed %s in RR: " + "%s%s%s IN TLSA %u %u %u ...", "certificate", + q, a, r, usage, selector, mtype); + if (x) + X509_free(x); + return (FILTER_RR_DROP); + } + /* Also unusable if public key is malformed or unsupported */ + k = X509_get_pubkey(x); + EVP_PKEY_free(k); + if (k == 0) { + msg_warn("malformed %s in RR: %s%s%s IN TLSA %u %u %u ...", + "or unsupported certificate public key", + q, a, r, usage, selector, mtype); + X509_free(x); + return (FILTER_RR_DROP); + } + + /* + * When a full trust-anchor certificate is published via DNS, we + * may need to use it to validate the server trust chain. Store + * it away for later use. + */ + if (usage == DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION + && (ctx->flags & FILTER_CTX_PARSE_DATA)) + ta_cert_insert(ctx->dane, x); + X509_free(x); + break; + + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + if (!d2i_PUBKEY(&k, &p, dlen) || dlen != p - data) { + msg_warn("malformed %s in RR: %s%s%s IN TLSA %u %u %u ...", + "public key", q, a, r, usage, selector, mtype); + if (k) + EVP_PKEY_free(k); + return (FILTER_RR_DROP); + } + + /* + * When a full trust-anchor public key is published via DNS, we + * may need to use it to validate the server trust chain. Store + * it away for later use. + */ + if (usage == DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION + && (ctx->flags & FILTER_CTX_PARSE_DATA)) + ta_pkey_insert(ctx->dane, k); + EVP_PKEY_free(k); + break; + } + + /* + * The cert or key was valid, just digest the raw object, and encode + * the digest value. + */ + if (ctx->flags & FILTER_CTX_PARSE_DATA) { + char *digest = tls_data_fprint((char *) data, dlen, signalg); + + dane_add(ctx->dane, usage, selector, signalg, digest); + if (msg_verbose || dane_verbose) + msg_info("using DANE RR: %s%s%s IN TLSA %u %u %u <%s>; " + "%s digest %s", q, a, r, usage, selector, mtype, + (selector == DNS_TLSA_SELECTOR_FULL_CERTIFICATE) ? + "certificate" : "public key", signalg, digest); + myfree(digest); + } + } + return (FILTER_RR_KEEP); +} + +/* process_rrs - filter and parse the TLSA RRset */ + +static DNS_RR *process_rrs(TLS_DANE *dane, DNS_RR *rrset) +{ + filter_ctx ctx; + + ctx.dane = dane; + ctx.count = ctx.target = 0; + ctx.flags = FILTER_CTX_APPLY_AGILITY | FILTER_CTX_PARSE_DATA; + + rrset = tlsa_apply(rrset, parse_tlsa_rr, &ctx); + + if (dane->ta == 0 && dane->ee == 0) + dane->flags |= TLS_DANE_FLAG_EMPTY; + + return (rrset); +} + +/* dane_lookup - TLSA record lookup, ctable style */ + +static void *dane_lookup(const char *tlsa_fqdn, void *unused_ctx) +{ + static VSTRING *why = 0; + int ret; + DNS_RR *rrs = 0; + TLS_DANE *dane; + + if (why == 0) + why = vstring_alloc(10); + + dane = tls_dane_alloc(); + 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) { + + /* + * Sort for deterministic digest in session cache lookup key. In + * addition we must arrange for more preferred matching types + * (full value or digest) to precede less preferred ones for the + * same usage and selector. + */ + rrs = dns_rr_sort(rrs, tlsa_rr_cmp); + rrs = process_rrs(dane, rrs); + } 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? */ + const char *mdalg; + + /* nop */ + if (tafile == 0 || *tafile == 0) + return (1); + + if (!dane_initialized) + dane_init(); + + /* Per-destination TA support is available even when DANE is not */ + mdalg = signalg ? signalg : "sha1"; + + /* + * 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(); + + for (tacount = 0; + errtype == 0 && PEM_read_bio(bp, &name, &header, &data, &len); + ++tacount) { + const unsigned char *p = data; + int usage = DNS_TLSA_USAGE_TRUST_ANCHOR_ASSERTION; + int selector; + char *digest; + + if (strcmp(name, PEM_STRING_X509) == 0 + || strcmp(name, PEM_STRING_X509_OLD) == 0) { + X509 *cert = d2i_X509(0, &p, len); + + if (cert && (p - data) == len) { + selector = DNS_TLSA_SELECTOR_FULL_CERTIFICATE; + digest = tls_data_fprint((char *) data, len, mdalg); + dane_add(dane, usage, selector, mdalg, digest); + myfree(digest); + ta_cert_insert(dane, cert); + } else + errtype = "certificate"; + if (cert) + X509_free(cert); + } else if (strcmp(name, PEM_STRING_PUBLIC) == 0) { + EVP_PKEY *pkey = d2i_PUBKEY(0, &p, len); + + if (pkey && (p - data) == len) { + selector = DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO; + digest = tls_data_fprint((char *) data, len, mdalg); + dane_add(dane, usage, selector, mdalg, digest); + myfree(digest); + ta_pkey_insert(dane, pkey); + } else + errtype = "public key"; + if (pkey) + EVP_PKEY_free(pkey); + } + + /* + * 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); +} + +/* tls_dane_match - match cert against given list of TA or EE digests */ + +int tls_dane_match(TLS_SESS_STATE *TLScontext, int usage, + X509 *cert, int depth) +{ + const TLS_DANE *dane = TLScontext->dane; + TLS_TLSA *tlsa = (usage == TLS_DANE_EE) ? dane->ee : dane->ta; + const char *namaddr = TLScontext->namaddr; + const char *ustr = (usage == TLS_DANE_EE) ? "end entity" : "trust anchor"; + int matched; + + for (matched = 0; tlsa && !matched; tlsa = tlsa->next) { + char **dgst; + + /* + * Note, set_trust() needs to know whether the match was for a pkey + * digest or a certificate digest. We return MATCHED_PKEY or + * MATCHED_CERT accordingly. + */ +#define MATCHED_CERT 1 +#define MATCHED_PKEY 2 + + if (tlsa->pkeys) { + char *pkey_dgst = tls_pkey_fprint(cert, tlsa->mdalg); + + for (dgst = tlsa->pkeys->argv; !matched && *dgst; ++dgst) + if (strcasecmp(pkey_dgst, *dgst) == 0) + matched = MATCHED_PKEY; + if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_CERTMATCH) + && matched) + msg_info("%s: depth=%d matched %s public-key %s digest=%s", + namaddr, depth, ustr, tlsa->mdalg, pkey_dgst); + myfree(pkey_dgst); + } + if (tlsa->certs != 0 && !matched) { + char *cert_dgst = tls_cert_fprint(cert, tlsa->mdalg); + + for (dgst = tlsa->certs->argv; !matched && *dgst; ++dgst) + if (strcasecmp(cert_dgst, *dgst) == 0) + matched = MATCHED_CERT; + if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_CERTMATCH) + && matched) + msg_info("%s: depth=%d matched %s certificate %s digest %s", + namaddr, depth, ustr, tlsa->mdalg, cert_dgst); + myfree(cert_dgst); + } + } + + return (matched); +} + +/* add_ext - add simple extension (no config section references) */ + +static int add_ext(X509 *issuer, X509 *subject, int ext_nid, char *ext_val) +{ + int ret = 0; + X509V3_CTX v3ctx; + X509_EXTENSION *ext; + + X509V3_set_ctx(&v3ctx, issuer, subject, 0, 0, 0); + if ((ext = X509V3_EXT_conf_nid(0, &v3ctx, ext_nid, ext_val)) != 0) { + ret = X509_add_ext(subject, ext, -1); + X509_EXTENSION_free(ext); + } + return ret; +} + +/* set_serial - set serial number to match akid or use subject's plus 1 */ + +static int set_serial(X509 *cert, AUTHORITY_KEYID *akid, X509 *subject) +{ + int ret = 0; + BIGNUM *bn; + + if (akid && akid->serial) + return (X509_set_serialNumber(cert, akid->serial)); + + /* + * Add one to subject's serial to avoid collisions between TA serial and + * serial of signing root. + */ + if ((bn = ASN1_INTEGER_to_BN(X509_get_serialNumber(subject), 0)) != 0 + && BN_add_word(bn, 1) + && BN_to_ASN1_INTEGER(bn, X509_get_serialNumber(cert))) + ret = 1; + + if (bn) + BN_free(bn); + return (ret); +} + +/* add_akid - add authority key identifier */ + +static int add_akid(X509 *cert, AUTHORITY_KEYID *akid) +{ + ASN1_OCTET_STRING *id; + unsigned char c = 0; + int nid = NID_authority_key_identifier; + int ret = 0; + + /* + * 0 will never be our subject keyid from a SHA-1 hash, but it could be + * our subject keyid if forced from child's akid. If so, set our + * authority keyid to 1. This way we are never self-signed, and thus + * exempt from any potential (off by default for now in OpenSSL) + * self-signature checks! + */ + id = ((akid && akid->keyid) ? akid->keyid : 0); + if (id && ASN1_STRING_length(id) == 1 && *ASN1_STRING_get0_data(id) == c) + c = 1; + + if ((akid = AUTHORITY_KEYID_new()) != 0 + && (akid->keyid = ASN1_OCTET_STRING_new()) != 0 + && ASN1_OCTET_STRING_set(akid->keyid, (void *) &c, 1) + && X509_add1_ext_i2d(cert, nid, akid, 0, X509V3_ADD_DEFAULT) > 0) + ret = 1; + if (akid) + AUTHORITY_KEYID_free(akid); + return (ret); +} + +/* add_skid - add subject key identifier to match child's akid */ + +static int add_skid(X509 *cert, AUTHORITY_KEYID *akid) +{ + int nid = NID_subject_key_identifier; + + if (!akid || !akid->keyid) + return (add_ext(0, cert, nid, "hash")); + else + return (X509_add1_ext_i2d(cert, nid, akid->keyid, 0, + X509V3_ADD_DEFAULT) > 0); +} + +/* akid_issuer_name - get akid issuer directory name */ + +static X509_NAME *akid_issuer_name(AUTHORITY_KEYID *akid) +{ + if (akid && akid->issuer) { + int i; + general_name_stack_t *gens = akid->issuer; + + for (i = 0; i < sk_GENERAL_NAME_num(gens); ++i) { + GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i); + + if (gn->type == GEN_DIRNAME) + return (gn->d.dirn); + } + } + return (0); +} + +/* set_issuer - set issuer DN to match akid if specified */ + +static int set_issuer_name(X509 *cert, AUTHORITY_KEYID *akid, X509_NAME *subj) +{ + X509_NAME *name = akid_issuer_name(akid); + + /* + * If subject's akid specifies an authority key identifier issuer name, + * we must use that. + */ + if (name) + return (X509_set_issuer_name(cert, name)); + return (X509_set_issuer_name(cert, subj)); +} + +/* grow_chain - add certificate to trusted or untrusted chain */ + +static void grow_chain(TLS_SESS_STATE *TLScontext, int trusted, X509 *cert) +{ + x509_stack_t **xs = trusted ? &TLScontext->trusted : &TLScontext->untrusted; + +#define UNTRUSTED 0 +#define TRUSTED 1 + + if (!*xs && (*xs = sk_X509_new_null()) == 0) + msg_fatal("out of memory"); + if (cert) { + if (trusted && !X509_add1_trust_object(cert, serverAuth)) + msg_fatal("out of memory"); + X509_up_ref(cert); + if (!sk_X509_push(*xs, cert)) + msg_fatal("out of memory"); + } +} + +/* wrap_key - wrap TA "key" as issuer of "subject" */ + +static void wrap_key(TLS_SESS_STATE *TLScontext, int depth, + EVP_PKEY *key, X509 *subject) +{ + X509 *cert = 0; + AUTHORITY_KEYID *akid; + X509_NAME *name = X509_get_issuer_name(subject); + + /* + * The subject name is never a NULL object unless we run out of memory. + * It may be an empty sequence, but the containing object always exists + * and its storage is owned by the certificate itself. + */ + if (name == 0 || (cert = X509_new()) == 0) + msg_fatal("Out of memory"); + + /* + * Record the depth of the intermediate wrapper certificate, logged in + * the verify callback. + */ + if (TLScontext->tadepth < 0) { + TLScontext->tadepth = depth + 1; + if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_CERTMATCH)) + msg_info("%s: depth=%d chain is trust-anchor signed", + TLScontext->namaddr, depth); + } + akid = X509_get_ext_d2i(subject, NID_authority_key_identifier, 0, 0); + + ERR_clear_error(); + + /* CA cert valid for +/- 30 days. */ + if (!X509_set_version(cert, 2) + || !set_serial(cert, akid, subject) + || !set_issuer_name(cert, akid, name) + || !X509_gmtime_adj(X509_getm_notBefore(cert), -30 * 86400L) + || !X509_gmtime_adj(X509_getm_notAfter(cert), 30 * 86400L) + || !X509_set_subject_name(cert, name) + || !X509_set_pubkey(cert, key) + || !add_ext(0, cert, NID_basic_constraints, "CA:TRUE") + || (key && !add_akid(cert, akid)) + || !add_skid(cert, akid)) { + tls_print_errors(); + msg_fatal("error generating DANE wrapper certificate"); + } + if (akid) + AUTHORITY_KEYID_free(akid); + grow_chain(TLScontext, TRUSTED, cert); + if (cert) + X509_free(cert); +} + +/* wrap_cert - wrap "tacert" as trust-anchor. */ + +static void wrap_cert(TLS_SESS_STATE *TLScontext, X509 *tacert, int depth) +{ + if (TLScontext->tadepth < 0) + TLScontext->tadepth = depth + 1; + + if (TLScontext->log_mask & (TLS_LOG_VERBOSE | TLS_LOG_CERTMATCH)) + msg_info("%s: depth=%d trust-anchor certificate", + TLScontext->namaddr, depth); + + grow_chain(TLScontext, TRUSTED, tacert); + return; +} + +/* ta_signed - is certificate signed by a TLSA cert or pkey */ + +static int ta_signed(TLS_SESS_STATE *TLScontext, X509 *cert, int depth) +{ + const TLS_DANE *dane = TLScontext->dane; + EVP_PKEY *pk; + TLS_PKEYS *k; + TLS_CERTS *x; + int done = 0; + + /* + * First check whether issued and signed by a TA cert, this is cheaper + * than the bare-public key checks below, since we can determine whether + * the candidate TA certificate issued the certificate to be checked + * first (name comparisons), before we bother with signature checks + * (public key operations). + */ + for (x = dane->certs; !done && x; x = x->next) { + if (X509_check_issued(x->cert, cert) == X509_V_OK) { + if ((pk = X509_get_pubkey(x->cert)) == 0) + continue; + /* Check signature, since some other TA may work if not this. */ + if ((done = (X509_verify(cert, pk) > 0)) != 0) + wrap_cert(TLScontext, x->cert, depth); + EVP_PKEY_free(pk); + } + } + + /* + * With bare TA public keys, we can't check whether the trust chain is + * issued by the key, but we can determine whether it is signed by the + * key, so we go with that. + * + * Ideally, the corresponding certificate was presented in the chain, and we + * matched it by its public key digest one level up. This code is here + * to handle adverse conditions imposed by sloppy administrators of + * receiving systems with poorly constructed chains. + * + * We'd like to optimize out keys that should not match when the cert's + * authority key id does not match the key id of this key computed via + * the RFC keyid algorithm (SHA-1 digest of public key bit-string sans + * ASN1 tag and length thus also excluding the unused bits field that is + * logically part of the length). However, some CAs have a non-standard + * authority keyid, so we lose. Too bad. + * + * This may push errors onto the stack when the certificate signature is not + * of the right type or length, throw these away. + */ + for (k = dane->pkeys; !done && k; k = k->next) + if ((done = (X509_verify(cert, k->pkey) > 0)) != 0) + wrap_key(TLScontext, depth, k->pkey, cert); + else + ERR_clear_error(); + + return (done); +} + +/* set_trust - configure for DANE validation */ + +static void set_trust(TLS_SESS_STATE *TLScontext, X509_STORE_CTX *ctx) +{ + int n; + int i; + int match; + int depth = 0; + EVP_PKEY *takey; + X509 *ca; + X509 *cert = X509_STORE_CTX_get0_cert(ctx); + x509_stack_t *in = X509_STORE_CTX_get0_untrusted(ctx); + + /* shallow copy */ + if ((in = sk_X509_dup(in)) == 0) + msg_fatal("out of memory"); + + /* + * At each iteration we consume the issuer of the current cert. This + * reduces the length of the "in" chain by one. If no issuer is found, + * we are done. We also stop when a certificate matches a TA in the + * peer's TLSA RRset. + * + * Caller ensures that the initial certificate is not self-signed. + */ + for (n = sk_X509_num(in); n > 0; --n, ++depth) { + for (i = 0; i < n; ++i) + if (X509_check_issued(sk_X509_value(in, i), cert) == X509_V_OK) + break; + + /* + * Final untrusted element with no issuer in the peer's chain, it may + * however be signed by a pkey or cert obtained via a TLSA RR. + */ + if (i == n) + break; + + /* Peer's chain contains an issuer ca. */ + ca = sk_X509_delete(in, i); + + /* Is it a trust anchor? */ + match = tls_dane_match(TLScontext, TLS_DANE_TA, ca, depth + 1); + if (match) { + switch (match) { + case MATCHED_CERT: + wrap_cert(TLScontext, ca, depth); + break; + case MATCHED_PKEY: + if ((takey = X509_get_pubkey(ca)) == 0) + msg_panic("trust-anchor certificate has null pkey"); + wrap_key(TLScontext, depth, takey, cert); + EVP_PKEY_free(takey); + break; + default: + msg_panic("unexpected tls_dane_match result: %d", match); + } + cert = 0; + break; + } + /* Add untrusted ca. */ + grow_chain(TLScontext, UNTRUSTED, ca); + + /* Final untrusted self-signed element? */ + if (X509_check_issued(ca, ca) == X509_V_OK) { + cert = 0; + break; + } + /* Restart with issuer as subject */ + cert = ca; + } + + /* + * When the loop exits, if "cert" is set, it is not self-signed and has + * no issuer in the chain, we check for a possible signature via a DNS + * obtained TA cert or public key. Otherwise, we found no TAs and no + * issuer, so set an empty list of TAs. + */ + if (!cert || !ta_signed(TLScontext, cert, depth)) { + /* Create empty trust list if null, else NOP */ + grow_chain(TLScontext, TRUSTED, 0); + } + /* shallow free */ + if (in) + sk_X509_free(in); +} + +/* dane_cb - wrap chain verification for DANE */ + +static int dane_cb(X509_STORE_CTX *ctx, void *app_ctx) +{ + const char *myname = "dane_cb"; + TLS_SESS_STATE *TLScontext = (TLS_SESS_STATE *) app_ctx; + X509 *cert = X509_STORE_CTX_get0_cert(ctx); + + /* + * Degenerate case: depth 0 self-signed cert. + * + * XXX: Should we suppress name checks, ... when the leaf certificate is a + * TA. After all they could sign any name they want. However, this + * requires a bit of additional code. For now we allow depth 0 TAs, but + * then the peer name has to match. + */ + if (X509_check_issued(cert, cert) == X509_V_OK) { + + /* + * Empty untrusted chain, could be NULL, but then ABI check less + * reliable, we may zero some other field, ... + */ + grow_chain(TLScontext, UNTRUSTED, 0); + if (tls_dane_match(TLScontext, TLS_DANE_TA, cert, 0)) { + TLScontext->tadepth = 0; + grow_chain(TLScontext, TRUSTED, cert); + } else + grow_chain(TLScontext, TRUSTED, 0); + } else { + set_trust(TLScontext, ctx); + } + + /* + * Check that setting the untrusted chain updates the expected structure + * member at the expected offset. + */ + X509_STORE_CTX_set0_trusted_stack(ctx, TLScontext->trusted); + X509_STORE_CTX_set0_untrusted(ctx, TLScontext->untrusted); + if (X509_STORE_CTX_get0_untrusted(ctx) != TLScontext->untrusted) + msg_panic("%s: OpenSSL ABI change", myname); + + return X509_verify_cert(ctx); +} + +/* tls_dane_set_callback - set or clear verification wrapper callback */ + +void tls_dane_set_callback(SSL_CTX *ctx, TLS_SESS_STATE *TLScontext) +{ + if (TLS_DANE_HASTA(TLScontext->dane)) + SSL_CTX_set_cert_verify_callback(ctx, dane_cb, (void *) TLScontext); + else + SSL_CTX_set_cert_verify_callback(ctx, 0, 0); +} + +#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); + + X509_STORE_CTX_set_default(store_ctx, "ssl_server"); + X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(store_ctx), + SSL_get0_param(ssl)); + + if (SSL_get_verify_callback(ssl)) + X509_STORE_CTX_set_verify_cb(store_ctx, SSL_get_verify_callback(ssl)); + + ret = dane_cb(store_ctx, tctx); + + SSL_set_verify_result(ssl, X509_STORE_CTX_get_error(store_ctx)); + X509_STORE_CTX_free(store_ctx); + + return (ret); +} + +static void add_tlsa(TLS_DANE *dane, char *argv[]) +{ + char *digest; + 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]); + const char *mdname = 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 digests are fatal */ + if (*mdname && !tls_validate_digest(mdname)) + msg_fatal("unsupported digest algorithm: %s", mdname); + + 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); + buf2 = buf = (unsigned char *) mymalloc(len); + i2d_X509(cert, &buf2); + if (!*mdname) + ta_cert_insert(dane, cert); + break; + case DNS_TLSA_SELECTOR_SUBJECTPUBLICKEYINFO: + pkey = X509_get_pubkey(cert); + len = i2d_PUBKEY(pkey, NULL); + buf2 = buf = (unsigned char *) mymalloc(len); + i2d_PUBKEY(pkey, &buf2); + if (!*mdname) + ta_pkey_insert(dane, pkey); + EVP_PKEY_free(pkey); + break; + } + OPENSSL_assert(buf2 - buf == len); + + digest = tls_data_fprint((char *) buf, len, *mdname ? mdname : signalg); + dane_add(dane, u, s, *mdname ? mdname : signalg, digest); + myfree((void *) digest); + 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); +} + +/* match_servername - match servername against pattern */ + +static int match_servername(const char *certid, ARGV *margv) +{ + const char *domain; + const char *parent; + int match_subdomain; + int i; + int idlen; + int domlen; + + /* + * XXX EAI support. + */ + + /* + * Match the certid against each pattern until we find a match. + */ + for (i = 0; i < margv->argc; ++i) { + match_subdomain = 0; + domain = margv->argv[i]; + if (*domain == '.' && domain[1] != '\0') { + ++domain; + match_subdomain = 1; + } + + /* + * Sub-domain match: certid is any sub-domain of hostname. + */ + if (match_subdomain) { + if ((idlen = strlen(certid)) > (domlen = strlen(domain)) + 1 + && certid[idlen - domlen - 1] == '.' + && !strcasecmp(certid + (idlen - domlen), domain)) + return (1); + else + continue; + } + + /* + * Exact match and initial "*" match. The initial "*" in a certid + * matches one (if var_tls_multi_label is false) or more hostname + * components under the condition that the certid contains multiple + * hostname components. + */ + if (!strcasecmp(certid, domain) + || (certid[0] == '*' && certid[1] == '.' && certid[2] != 0 + && (parent = strchr(domain, '.')) != 0 + && (idlen = strlen(certid + 1)) <= (domlen = strlen(parent)) + && strcasecmp(var_tls_multi_wildcard == 0 ? parent : + parent + domlen - idlen, + certid + 1) == 0)) + return (1); + } + return (0); +} + +static void check_name(TLS_SESS_STATE *tctx, X509 *cert, ARGV *margs) +{ + char *cn; + int matched = 0; + general_name_stack_t *gens; + + if (SSL_get_verify_result(tctx->con) != X509_V_OK) + return; + + tctx->peer_status |= TLS_CERT_FLAG_TRUSTED; + + gens = X509_get_ext_d2i(cert, NID_subject_alt_name, 0, 0); + if (gens) { + int has_dnsname = 0; + int num_gens = sk_GENERAL_NAME_num(gens); + int i; + + for (i = 0; !matched && i < num_gens; ++i) { + const GENERAL_NAME *gn = sk_GENERAL_NAME_value(gens, i); + const char *dnsname; + + if (gn->type != GEN_DNS) + continue; + has_dnsname = 1; + tctx->peer_status |= TLS_CERT_FLAG_ALTNAME; + dnsname = tls_dns_name(gn, tctx); + if (dnsname && *dnsname + && (matched = match_servername(dnsname, margs)) != 0) + tctx->peer_status |= TLS_CERT_FLAG_MATCHED; + } + sk_GENERAL_NAME_pop_free(gens, GENERAL_NAME_free); + if (has_dnsname) + return; + } + cn = tls_peer_CN(cert, tctx); + if (match_servername(cn, margs)) + tctx->peer_status |= TLS_CERT_FLAG_MATCHED; + myfree(cn); +} + +static void check_print(TLS_SESS_STATE *tctx, X509 *cert) +{ + if (TLS_DANE_HASEE(tctx->dane) + && tls_dane_match(tctx, TLS_DANE_EE, cert, 0)) + tctx->peer_status |= TLS_CERT_FLAG_TRUSTED | TLS_CERT_FLAG_MATCHED; +} + +static void check_peer(TLS_SESS_STATE *tctx, X509 *cert, int argc, char **argv) +{ + ARGV match; + + tctx->peer_status |= TLS_CERT_FLAG_PRESENT; + check_print(tctx, cert); + if (!TLS_CERT_IS_MATCHED(tctx)) { + match.argc = argc; + match.argv = argv; + check_name(tctx, cert, &match); + } +} + +static SSL_CTX *ctx_init(const char *CAfile) +{ + SSL_CTX *client_ctx; + + tls_param_init(); + tls_check_version(); + +#if OPENSSL_VERSION_NUMBER < 0x10100000L + SSL_load_error_strings(); + SSL_library_init(); +#endif + + 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); + + 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; + TLS_SESS_STATE *tctx; + x509_stack_t *chain; + + 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_sha1; + tctx->dane = tls_dane_alloc(); + + 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"); + } + SSL_set_connect_state(tctx->con); + add_tlsa((TLS_DANE *) tctx->dane, argv); + tls_dane_set_callback(ssl_ctx, tctx); + + /* Verify saved server chain */ + chain = load_chain(argv[6]); + verify_chain(tctx->con, chain, tctx); + check_peer(tctx, sk_X509_value(chain, 0), argc - 7, argv + 7); + tls_print_errors(); + + msg_info("%s %s", TLS_CERT_IS_MATCHED(tctx) ? "Verified" : + TLS_CERT_IS_TRUSTED(tctx) ? "Trusted" : "Untrusted", argv[7]); + + return (TLS_CERT_IS_MATCHED(tctx) ? 0 : 1); +} + +#endif /* TEST */ + +#endif /* USE_TLS */ |