diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:46:30 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-06 01:46:30 +0000 |
commit | b5896ba9f6047e7031e2bdee0622d543e11a6734 (patch) | |
tree | fd7b460593a2fee1be579bec5697e6d887ea3421 /src/tls/tls_verify.c | |
parent | Initial commit. (diff) | |
download | postfix-upstream.tar.xz postfix-upstream.zip |
Adding upstream version 3.4.23.upstream/3.4.23upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | src/tls/tls_verify.c | 507 |
1 files changed, 507 insertions, 0 deletions
diff --git a/src/tls/tls_verify.c b/src/tls/tls_verify.c new file mode 100644 index 0000000..85d5f64 --- /dev/null +++ b/src/tls/tls_verify.c @@ -0,0 +1,507 @@ +/*++ +/* NAME +/* tls_verify 3 +/* SUMMARY +/* peer name and peer certificate verification +/* SYNOPSIS +/* #define TLS_INTERNAL +/* #include <tls.h> +/* +/* int tls_verify_certificate_callback(ok, ctx) +/* int ok; +/* X509_STORE_CTX *ctx; +/* +/* int tls_log_verify_error(TLScontext) +/* TLS_SESS_STATE *TLScontext; +/* +/* char *tls_peer_CN(peercert, TLScontext) +/* X509 *peercert; +/* TLS_SESS_STATE *TLScontext; +/* +/* char *tls_issuer_CN(peercert, TLScontext) +/* X509 *peercert; +/* TLS_SESS_STATE *TLScontext; +/* +/* const char *tls_dns_name(gn, TLScontext) +/* const GENERAL_NAME *gn; +/* TLS_SESS_STATE *TLScontext; +/* DESCRIPTION +/* tls_verify_certificate_callback() is called several times (directly +/* or indirectly) from crypto/x509/x509_vfy.c. It collects errors +/* and trust information at each element of the trust chain. +/* The last call at depth 0 sets the verification status based +/* on the cumulative winner (lowest depth) of errors vs. trust. +/* We always return 1 (continue the handshake) and handle trust +/* and peer-name verification problems at the application level. +/* +/* tls_log_verify_error() (called only when we care about the +/* peer certificate, that is not when opportunistic) logs the +/* reason why the certificate failed to be verified. +/* +/* tls_peer_CN() returns the text CommonName for the peer +/* certificate subject, or an empty string if no CommonName was +/* found. The result is allocated with mymalloc() and must be +/* freed by the caller; it contains UTF-8 without non-printable +/* ASCII characters. +/* +/* tls_issuer_CN() returns the text CommonName for the peer +/* certificate issuer, or an empty string if no CommonName was +/* found. The result is allocated with mymalloc() and must be +/* freed by the caller; it contains UTF-8 without non-printable +/* ASCII characters. +/* +/* tls_dns_name() returns the string value of a GENERAL_NAME +/* from a DNS subjectAltName extension. If non-printable characters +/* are found, a null string is returned instead. Further sanity +/* checks may be added if the need arises. +/* +/* Arguments: +/* .IP ok +/* Result of prior verification: non-zero means success. In +/* order to reduce the noise level, some tests or error reports +/* are disabled when verification failed because of some +/* earlier problem. +/* .IP ctx +/* SSL application context. This links to the Postfix TLScontext +/* with enforcement and logging options. +/* .IP gn +/* An OpenSSL GENERAL_NAME structure holding a DNS subjectAltName +/* to be decoded and checked for validity. +/* .IP peercert +/* Server or client X.509 certificate. +/* .IP TLScontext +/* Server or client context for warning messages. +/* DIAGNOSTICS +/* tls_peer_CN(), tls_issuer_CN() and tls_dns_name() log a warning +/* when 1) the requested information is not available in the specified +/* certificate, 2) the result exceeds a fixed limit, 3) the result +/* contains NUL characters or the result contains non-printable or +/* non-ASCII characters. +/* 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) +/* Originally written by: +/* Lutz Jaenicke +/* BTU Cottbus +/* Allgemeine Elektrotechnik +/* Universitaetsplatz 3-4 +/* D-03044 Cottbus, Germany +/* +/* Updated by: +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Victor Duchovni +/* Morgan Stanley +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <ctype.h> + +#ifdef USE_TLS +#include <string.h> + +/* Utility library. */ + +#include <msg.h> +#include <mymalloc.h> +#include <stringops.h> + +/* TLS library. */ + +#define TLS_INTERNAL +#include <tls.h> + +/* update_error_state - safely stash away error state */ + +static void update_error_state(TLS_SESS_STATE *TLScontext, int depth, + X509 *errorcert, int errorcode) +{ + /* No news is good news */ + if (TLScontext->errordepth >= 0 && TLScontext->errordepth <= depth) + return; + + /* + * The certificate pointer is stable during the verification callback, + * but may be freed after the callback returns. Since we delay error + * reporting till later, we bump the refcount so we can rely on it still + * being there until later. + */ + if (TLScontext->errorcert != 0) + X509_free(TLScontext->errorcert); + if (errorcert != 0) + X509_up_ref(errorcert); + TLScontext->errorcert = errorcert; + TLScontext->errorcode = errorcode; + TLScontext->errordepth = depth; +} + +/* tls_verify_certificate_callback - verify peer certificate info */ + +int tls_verify_certificate_callback(int ok, X509_STORE_CTX *ctx) +{ + char buf[CCERT_BUFSIZ]; + X509 *cert; + int err; + int depth; + int max_depth; + SSL *con; + TLS_SESS_STATE *TLScontext; + + /* May be NULL as of OpenSSL 1.0, thanks for the API change! */ + cert = X509_STORE_CTX_get_current_cert(ctx); + err = X509_STORE_CTX_get_error(ctx); + con = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); + TLScontext = SSL_get_ex_data(con, TLScontext_index); + depth = X509_STORE_CTX_get_error_depth(ctx); + + /* Don't log the internal root CA unless there's an unexpected error. */ + if (ok && TLScontext->tadepth > 0 && depth > TLScontext->tadepth) + return (1); + + /* + * Certificate chain depth limit violations are mis-reported by the + * OpenSSL library, from SSL_CTX_set_verify(3): + * + * The certificate verification depth set with SSL[_CTX]_verify_depth() + * stops the verification at a certain depth. The error message produced + * will be that of an incomplete certificate chain and not + * X509_V_ERR_CERT_CHAIN_TOO_LONG as may be expected. + * + * We set a limit that is one higher than the user requested limit. If this + * higher limit is reached, we raise an error even a trusted root CA is + * present at this depth. This disambiguates trust chain truncation from + * an incomplete trust chain. + */ + max_depth = SSL_get_verify_depth(con) - 1; + + /* + * We never terminate the SSL handshake in the verification callback, + * rather we allow the TLS handshake to continue, but mark the session as + * unverified. The application is responsible for closing any sessions + * with unverified credentials. + */ + if (max_depth >= 0 && depth > max_depth) { + X509_STORE_CTX_set_error(ctx, err = X509_V_ERR_CERT_CHAIN_TOO_LONG); + ok = 0; + } + if (ok == 0) + update_error_state(TLScontext, depth, cert, err); + + if (TLScontext->log_mask & TLS_LOG_VERBOSE) { + if (cert) + X509_NAME_oneline(X509_get_subject_name(cert), buf, sizeof(buf)); + else + strcpy(buf, "<unknown>"); + msg_info("%s: depth=%d verify=%d subject=%s", + TLScontext->namaddr, depth, ok, printable(buf, '?')); + } + return (1); +} + +/* tls_log_verify_error - Report final verification error status */ + +void tls_log_verify_error(TLS_SESS_STATE *TLScontext) +{ + char buf[CCERT_BUFSIZ]; + int err = TLScontext->errorcode; + X509 *cert = TLScontext->errorcert; + int depth = TLScontext->errordepth; + +#define PURPOSE ((depth>0) ? "CA": TLScontext->am_server ? "client": "server") + + if (err == X509_V_OK) + return; + + /* + * Specific causes for verification failure. + */ + switch (err) { + case X509_V_ERR_CERT_UNTRUSTED: + + /* + * We expect the error cert to be the leaf, but it is likely + * sufficient to omit it from the log, even less user confusion. + */ + msg_info("certificate verification failed for %s: " + "not trusted by local or TLSA policy", TLScontext->namaddr); + break; + case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT: + msg_info("certificate verification failed for %s: " + "self-signed certificate", TLScontext->namaddr); + break; + case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY: + case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN: + + /* + * There is no difference between issuing cert not provided and + * provided, but not found in CAfile/CApath. Either way, we don't + * trust it. + */ + if (cert) + X509_NAME_oneline(X509_get_issuer_name(cert), buf, sizeof(buf)); + else + strcpy(buf, "<unknown>"); + msg_info("certificate verification failed for %s: untrusted issuer %s", + TLScontext->namaddr, printable(buf, '?')); + break; + case X509_V_ERR_CERT_NOT_YET_VALID: + case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD: + msg_info("%s certificate verification failed for %s: certificate not" + " yet valid", PURPOSE, TLScontext->namaddr); + break; + case X509_V_ERR_CERT_HAS_EXPIRED: + case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD: + msg_info("%s certificate verification failed for %s: certificate has" + " expired", PURPOSE, TLScontext->namaddr); + break; + case X509_V_ERR_INVALID_PURPOSE: + msg_info("certificate verification failed for %s: not designated for " + "use as a %s certificate", TLScontext->namaddr, PURPOSE); + break; + case X509_V_ERR_CERT_CHAIN_TOO_LONG: + msg_info("certificate verification failed for %s: " + "certificate chain longer than limit(%d)", + TLScontext->namaddr, depth - 1); + break; + default: + msg_info("%s certificate verification failed for %s: num=%d:%s", + PURPOSE, TLScontext->namaddr, err, + X509_verify_cert_error_string(err)); + break; + } +} + +#ifndef DONT_GRIPE +#define DONT_GRIPE 0 +#define DO_GRIPE 1 +#endif + +/* tls_text_name - extract certificate property value by name */ + +static char *tls_text_name(X509_NAME *name, int nid, const char *label, + const TLS_SESS_STATE *TLScontext, int gripe) +{ + const char *myname = "tls_text_name"; + int pos; + X509_NAME_ENTRY *entry; + ASN1_STRING *entry_str; + int asn1_type; + int utf8_length; + unsigned char *utf8_value; + int ch; + unsigned char *cp; + + if (name == 0 || (pos = X509_NAME_get_index_by_NID(name, nid, -1)) < 0) { + if (gripe != DONT_GRIPE) { + msg_warn("%s: %s: peer certificate has no %s", + myname, TLScontext->namaddr, label); + tls_print_errors(); + } + return (0); + } +#if 0 + + /* + * If the match is required unambiguous, insist that that no other values + * be present. + */ + if (X509_NAME_get_index_by_NID(name, nid, pos) >= 0) { + msg_warn("%s: %s: multiple %ss in peer certificate", + myname, TLScontext->namaddr, label); + return (0); + } +#endif + + if ((entry = X509_NAME_get_entry(name, pos)) == 0) { + /* This should not happen */ + msg_warn("%s: %s: error reading peer certificate %s entry", + myname, TLScontext->namaddr, label); + tls_print_errors(); + return (0); + } + if ((entry_str = X509_NAME_ENTRY_get_data(entry)) == 0) { + /* This should not happen */ + msg_warn("%s: %s: error reading peer certificate %s data", + myname, TLScontext->namaddr, label); + tls_print_errors(); + return (0); + } + + /* + * XXX Convert everything into UTF-8. This is a super-set of ASCII, so we + * don't have to bother with separate code paths for ASCII-like content. + * If the payload is ASCII then we won't waste lots of CPU cycles + * converting it into UTF-8. It's up to OpenSSL to do something + * reasonable when converting ASCII formats that contain non-ASCII + * content. + * + * XXX Don't bother optimizing the string length error check. It is not + * worth the complexity. + */ + asn1_type = ASN1_STRING_type(entry_str); + if ((utf8_length = ASN1_STRING_to_UTF8(&utf8_value, entry_str)) < 0) { + msg_warn("%s: %s: error decoding peer %s of ASN.1 type=%d", + myname, TLScontext->namaddr, label, asn1_type); + tls_print_errors(); + return (0); + } + + /* + * No returns without cleaning up. A good optimizer will replace multiple + * blocks of identical code by jumps to just one such block. + */ +#define TLS_TEXT_NAME_RETURN(x) do { \ + char *__tls_text_name_temp = (x); \ + OPENSSL_free(utf8_value); \ + return (__tls_text_name_temp); \ + } while (0) + + /* + * Remove trailing null characters. They would give false alarms with the + * length check and with the embedded null check. + */ +#define TRIM0(s, l) do { while ((l) > 0 && (s)[(l)-1] == 0) --(l); } while (0) + + TRIM0(utf8_value, utf8_length); + + /* + * Enforce the length limit, because the caller will copy the result into + * a fixed-length buffer. + */ + if (utf8_length >= CCERT_BUFSIZ) { + msg_warn("%s: %s: peer %s too long: %d", + myname, TLScontext->namaddr, label, utf8_length); + TLS_TEXT_NAME_RETURN(0); + } + + /* + * Reject embedded nulls in ASCII or UTF-8 names. OpenSSL is responsible + * for producing properly-formatted UTF-8. + */ + if (utf8_length != strlen((char *) utf8_value)) { + msg_warn("%s: %s: NULL character in peer %s", + myname, TLScontext->namaddr, label); + TLS_TEXT_NAME_RETURN(0); + } + + /* + * Reject non-printable ASCII characters in UTF-8 content. + * + * Note: the code below does not find control characters in illegal UTF-8 + * sequences. It's OpenSSL's job to produce valid UTF-8, and reportedly, + * it does validation. + */ + for (cp = utf8_value; (ch = *cp) != 0; cp++) { + if (ISASCII(ch) && !ISPRINT(ch)) { + msg_warn("%s: %s: non-printable content in peer %s", + myname, TLScontext->namaddr, label); + TLS_TEXT_NAME_RETURN(0); + } + } + TLS_TEXT_NAME_RETURN(mystrdup((char *) utf8_value)); +} + +/* tls_dns_name - Extract valid DNS name from subjectAltName value */ + +const char *tls_dns_name(const GENERAL_NAME * gn, + const TLS_SESS_STATE *TLScontext) +{ + const char *myname = "tls_dns_name"; + char *cp; + const char *dnsname; + int len; + + /* + * Peername checks are security sensitive, carefully scrutinize the + * input! + */ + if (gn->type != GEN_DNS) + msg_panic("%s: Non DNS input argument", myname); + + /* + * We expect the OpenSSL library to construct GEN_DNS extension objects as + * ASN1_IA5STRING values. Check we got the right union member. + */ + if (ASN1_STRING_type(gn->d.ia5) != V_ASN1_IA5STRING) { + msg_warn("%s: %s: invalid ASN1 value type in subjectAltName", + myname, TLScontext->namaddr); + return (0); + } + + /* + * Safe to treat as an ASCII string possibly holding a DNS name + */ + dnsname = (const char *) ASN1_STRING_get0_data(gn->d.ia5); + len = ASN1_STRING_length(gn->d.ia5); + TRIM0(dnsname, len); + + /* + * Per Dr. Steven Henson of the OpenSSL development team, ASN1_IA5STRING + * values can have internal ASCII NUL values in this context because + * their length is taken from the decoded ASN1 buffer, a trailing NUL is + * always appended to make sure that the string is terminated, but the + * ASN.1 length may differ from strlen(). + */ + if (len != strlen(dnsname)) { + msg_warn("%s: %s: internal NUL in subjectAltName", + myname, TLScontext->namaddr); + return 0; + } + + /* + * XXX: Should we be more strict and call valid_hostname()? So long as + * the name is safe to handle, if it is not a valid hostname, it will not + * compare equal to the expected peername, so being more strict than + * "printable" is likely excessive... + */ + if (*dnsname && !allprint(dnsname)) { + cp = mystrdup(dnsname); + msg_warn("%s: %s: non-printable characters in subjectAltName: %.100s", + myname, TLScontext->namaddr, printable(cp, '?')); + myfree(cp); + return 0; + } + return (dnsname); +} + +/* tls_peer_CN - extract peer common name from certificate */ + +char *tls_peer_CN(X509 *peercert, const TLS_SESS_STATE *TLScontext) +{ + char *cn; + + cn = tls_text_name(X509_get_subject_name(peercert), NID_commonName, + "subject CN", TLScontext, DONT_GRIPE); + return (cn ? cn : mystrdup("")); +} + +/* tls_issuer_CN - extract issuer common name from certificate */ + +char *tls_issuer_CN(X509 *peer, const TLS_SESS_STATE *TLScontext) +{ + X509_NAME *name; + char *cn; + + name = X509_get_issuer_name(peer); + + /* + * If no issuer CN field, use Organization instead. CA certs without a CN + * are common, so we only complain if the organization is also missing. + */ + if ((cn = tls_text_name(name, NID_commonName, + "issuer CN", TLScontext, DONT_GRIPE)) == 0) + cn = tls_text_name(name, NID_organizationName, + "issuer Organization", TLScontext, DONT_GRIPE); + return (cn ? cn : mystrdup("")); +} + +#endif |