This is an automated email from the ASF dual-hosted git repository. asf-gitbox-commits pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/qpid-proton.git
commit d65168bbef23cac2dda2fbef05cdc9aa6e71651e Author: Andrew Stitcher <[email protected]> AuthorDate: Wed Sep 23 13:28:00 2026 -0400 PROTON-2974: Verify TLS peer hostnames according to RFC 9525 The hand-rolled wildcard matcher computed the start of its suffix comparison as slabel + (strlen(slabel) - suffix_len). strlen() returns size_t, so when a name in the peer certificate had a longer suffix than the corresponding label of the configured hostname the subtraction wrapped and the comparison read stack memory preceding the buffer. A certificate name of "*abcdef.example.com" checked against "x.example.com" reads six bytes starting five bytes before it. Hand the name check to OpenSSL instead, via X509_VERIFY_PARAM_set1_host() with X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS, and delete the matcher. That also settles two ways the old code diverged from RFC 9525 s6.3: a wildcard is now honoured only as the complete left-most label and spans exactly one label, so "ba*.example.com" and "a.*.example.com" are rejected; and the subject CommonName is consulted only when the certificate carries no DNS SubjectAltName, where before it was tried whenever no SubjectAltName matched, letting a permissive legacy CN override tighter SANs. A mismatch now reports X509_V_ERR_HOSTNAME_MISMATCH rather than X509_V_ERR_APPLICATION_VERIFICATION. Not SSL_set1_host(), which looks like the obvious call: from 3.0 it converts a name that parses as an IP literal into an iPAddress-SubjectAltName-only check, so a peer addressed as "127.0.0.1" presenting a certificate with CN=127.0.0.1 - which is what our own client-certificate.pem is - stops verifying. Older OpenSSL does not convert, so the same call would mean different things on different builds. X509_VERIFY_PARAM_set1_host() asks for the name check and nothing else. An iPAddress SubjectAltName is picked up in verify_callback instead, by trying X509_check_ip_asc() on a name that got no DNS match - a form the old matcher never handled at all. OpenSSL reads a reference name with a leading dot as "any sub-domain of", so delegating to it would have widened what a peer may present - ".one.com" would match "alternate.name.one.com". A DNS name cannot begin with a dot, so treat that as a configuration error and fail the handshake, alongside the existing check for VERIFY_PEER_NAME configured with no hostname at all. Both are rejected in verify_callback rather than where the name is set, so the outcome does not depend on whether the name arrives before or after the socket exists. SChannel keeps its own matcher, tightened to the same rules and given the same SubjectAltName precedence. It compares only against DNS names, as it always has, so the iPAddress match is OpenSSL-only and the two cases covering it are skipped on Windows. SSL_set_hostflags() arrived in OpenSSL 1.1.0, which becomes the minimum; Drop the version guards that distinguish between earlier versions. server-wc is reissued with "*.wildcard.domain.com" and 127.0.0.1 as SubjectAltNames. Its CommonName is left as "*.prefix*.domain.com" deliberately: a CN the old matcher would have accepted is what makes the SubjectAltName precedence testable, and it means 127.0.0.1 can only match through the iPAddress entry. The four wildcard cases become a table of eleven, plus two more where an iPAddress SubjectAltName is matched, and the two neighbouring cases that asserted nothing now assert that the handshake completed. Assisted-By: Claude Opus 5 <[email protected]> --- CMakeLists.txt | 2 +- INSTALL.md | 2 +- c/include/proton/ssl.h | 21 +- c/include/proton/tls.h | 20 +- c/src/ssl/openssl.c | 204 +++++++------------ c/src/ssl/schannel.cpp | 68 ++----- c/src/tls/openssl.c | 220 +++++++-------------- python/tests/proton_tests/ssl.py | 182 +++++++---------- python/tests/proton_tests/ssl_db/mkcerts.sh | 15 +- .../proton_tests/ssl_db/server-wc-certificate.p12 | Bin 1029 -> 1274 bytes .../proton_tests/ssl_db/server-wc-certificate.pem | 26 +-- .../proton_tests/ssl_db/server-wc-private-key.pem | 11 +- .../proton_tests/ssl_db/server-wc-request.pem | 21 +- python/tests/proton_tests/ssl_db/server-wc.pkcs12 | Bin 1026 -> 1282 bytes 14 files changed, 301 insertions(+), 491 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 647b2a1e4..d7d0cea92 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ include (CheckLibraryExists) include (CheckSymbolExists) include (CheckPythonModule) -find_package (OpenSSL) +find_package (OpenSSL 1.1) find_package (Threads) find_package (SWIG) find_package (CyrusSASL) diff --git a/INSTALL.md b/INSTALL.md index 44974291c..3d0535e13 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -20,7 +20,7 @@ Linux dependencies - GNU Make 3.81+ - GCC 9+ - Cyrus SASL 2.1.26+ (for SASL support) - - OpenSSL 1.0.2a+ (for SSL support) + - OpenSSL 1.1.0+ (for SSL support) - JsonCpp 1.8+ for C++ connection configuration file support Windows dependencies diff --git a/c/include/proton/ssl.h b/c/include/proton/ssl.h index 22becf489..550410886 100644 --- a/c/include/proton/ssl.h +++ b/c/include/proton/ssl.h @@ -374,17 +374,28 @@ PN_EXTERN pn_ssl_resume_status_t pn_ssl_resume_status(pn_ssl_t *ssl); * * The hostname is used for two purposes: 1) when set on an SSL client, it is sent to the * server during the handshake (if Server Name Indication is supported), and 2) it is used - * to check against the identifying name provided in the peer's certificate. If the - * supplied name does not exactly match a SubjectAltName (type DNS name), or the - * CommonName entry in the peer's certificate, the peer is considered unauthenticated - * (potential imposter), and the SSL connection is aborted. + * to check against the identifying name provided in the peer's certificate. If it does not + * match, the peer is considered unauthenticated (potential imposter), and the SSL + * connection is aborted. + * + * Name matching follows RFC 9525 section 6.3. The hostname is compared, case + * insensitively, against the SubjectAltName entries of type DNS name. A certificate name + * may use a wildcard, but only as the complete left-most label ("*.example.com"), and it + * then matches exactly one label ("a.example.com" but neither "example.com" nor + * "a.b.example.com"). Partial wildcards such as "ba*.example.com" are not honoured. The + * CommonName entry of the subject is consulted only when the certificate carries no + * SubjectAltName of type DNS name. + * + * On OpenSSL, a hostname given as an IP address literal is additionally accepted if it + * matches a SubjectAltName entry of type IP address. Windows SChannel compares only against + * DNS names, so such a hostname matches only a CommonName holding the same literal. * * @note Verification of the hostname is only done if PN_SSL_VERIFY_PEER_NAME is enabled. * See ::pn_ssl_domain_set_peer_authentication. * * @param[in] ssl the ssl session. * @param[in] hostname the expected identity of the remote. Must conform to the syntax as - * given in RFC1034, Section 3.5. + * given in RFC1034, Section 3.5; in particular it must not begin with a '.'. * @return 0 on success. */ PN_EXTERN int pn_ssl_set_peer_hostname(pn_ssl_t *ssl, const char *hostname); diff --git a/c/include/proton/tls.h b/c/include/proton/tls.h index 65fdb9bb3..6ae42c78c 100644 --- a/c/include/proton/tls.h +++ b/c/include/proton/tls.h @@ -309,17 +309,27 @@ PN_TLS_EXTERN bool pn_tls_get_protocol_version(pn_tls_t *tls, const char **versi * * The hostname is used for two purposes: 1) when set on an TLS client, it is sent to the * server during the handshake (if Server Name Indication is supported), and 2) it is used - * to check against the identifying name provided in the peer's certificate. If the - * supplied name does not exactly match a SubjectAltName (type DNS name), or the - * CommonName entry in the peer's certificate, the peer is considered unauthenticated - * (potential imposter), and the TLS connection is aborted. + * to check against the identifying name provided in the peer's certificate. If it does not + * match, the peer is considered unauthenticated (potential imposter), and the TLS + * connection is aborted. + * + * Name matching follows RFC 9525 section 6.3. The hostname is compared, case + * insensitively, against the SubjectAltName entries of type DNS name. A certificate name + * may use a wildcard, but only as the complete left-most label ("*.example.com"), and it + * then matches exactly one label ("a.example.com" but neither "example.com" nor + * "a.b.example.com"). Partial wildcards such as "ba*.example.com" are not honoured. The + * CommonName entry of the subject is consulted only when the certificate carries no + * SubjectAltName of type DNS name. + * + * A hostname given as an IP address literal is additionally accepted if it matches a + * SubjectAltName entry of type IP address. * * @note Verification of the hostname is only done if PN_TLS_VERIFY_PEER_NAME is enabled. * See ::pn_tls_config_set_peer_authentication. * * @param[in] tls the tls session. * @param[in] hostname the expected identity of the remote. Must conform to the syntax as - * given in RFC1034, Section 3.5. + * given in RFC1034, Section 3.5; in particular it must not begin with a '.'. * @return 0 on success. */ PN_TLS_EXTERN int pn_tls_set_peer_hostname(pn_tls_t *tls, const char *hostname); diff --git a/c/src/ssl/openssl.c b/c/src/ssl/openssl.c index 0b2dfa3c5..a9d19081e 100644 --- a/c/src/ssl/openssl.c +++ b/c/src/ssl/openssl.c @@ -51,6 +51,12 @@ #include <openssl/err.h> #include <openssl/x509v3.h> +// Peer name verification is delegated to X509_VERIFY_PARAM_set1_host()/SSL_set_hostflags(), +// the latter of which needs 1.1.0. +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) +#error "Proton requires OpenSSL 1.1.0 or later." +#endif + #if (OPENSSL_VERSION_NUMBER >= 0x30000000L) #include <openssl/provider.h> #include <openssl/store.h> @@ -85,9 +91,7 @@ struct pn_ssl_domain_t { char *ciphers; int ref_count; -#ifdef SSL_SECOP_PEER - int default_seclevel; -#endif + int default_seclevel; pn_ssl_mode_t mode; pn_ssl_verify_mode_t verify_mode; @@ -242,59 +246,41 @@ static int ssl_failed(pn_transport_t *transport, int reason) return PN_EOS; } -/* match the DNS name pattern from the peer certificate against our configured peer - hostname */ -static bool match_dns_pattern( const char *hostname, - const char *pattern, int plen ) +/* A peer hostname we are prepared to verify against. OpenSSL reads a leading '.' as "any + sub-domain of", a broader match than anything we previously accepted, and a DNS name + cannot begin with a dot anyway - so treat it as a configuration error alongside a missing + name rather than let it silently widen what the peer is allowed to present. */ +static bool usable_peer_hostname( const char *name ) { - int slen = (int) strlen(hostname); - if (memchr( pattern, '*', plen ) == NULL) - return (plen == slen && - pn_strncasecmp( pattern, hostname, plen ) == 0); - - /* dns wildcarded pattern - RFC2818 */ - char plabel[64]; /* max label length < 63 - RFC1034 */ - char slabel[64]; - - while (plen > 0 && slen > 0) { - const char *cptr; - int len; - - cptr = (const char *) memchr( pattern, '.', plen ); - len = (cptr) ? cptr - pattern : plen; - if (len > (int) sizeof(plabel) - 1) return false; - memcpy( plabel, pattern, len ); - plabel[len] = 0; - if (cptr) ++len; // skip matching '.' - pattern += len; - plen -= len; - - cptr = (const char *) memchr( hostname, '.', slen ); - len = (cptr) ? cptr - hostname : slen; - if (len > (int) sizeof(slabel) - 1) return false; - memcpy( slabel, hostname, len ); - slabel[len] = 0; - if (cptr) ++len; // skip matching '.' - hostname += len; - slen -= len; - - char *star = strchr( plabel, '*' ); - if (!star) { - if (pn_strcasecmp( plabel, slabel )) return false; - } else { - *star = '\0'; - char *prefix = plabel; - int prefix_len = strlen(prefix); - char *suffix = star + 1; - int suffix_len = strlen(suffix); - if (prefix_len && pn_strncasecmp( prefix, slabel, prefix_len )) return false; - if (suffix_len && pn_strncasecmp( suffix, - slabel + (strlen(slabel) - suffix_len), - suffix_len )) return false; - } - } + return name && *name && name[0] != '.'; +} - return plen == slen; +/* Hand peer name verification to OpenSSL. It applies the RFC 9525 s6.3 rules: a wildcard + is honoured only as the complete left-most label, and the subject CN is ignored whenever + the certificate carries a DNS SubjectAltName. NO_PARTIAL_WILDCARDS additionally rejects + "ba*.example.com" style patterns, which OpenSSL would otherwise still accept. + + Deliberately not SSL_set1_host(): from 3.0 that quietly converts a name which parses as an + IP literal into an iPAddress-SubjectAltName-only check, so a certificate naming its address + in the CommonName - which Proton has always accepted, and which our own test certificates + use - would stop verifying. Older OpenSSL does not do the conversion, so the same call + would also mean different things on different builds. Ask for the name check explicitly + and let verify_callback pick up an iPAddress SubjectAltName. */ +static bool set_verify_host( pn_transport_t *transport, pni_ssl_t *ssl ) +{ + if (!ssl->ssl || ssl->verify_mode != PN_SSL_VERIFY_PEER_NAME) return true; + /* Leave the check unarmed for an unusable name; verify_callback fails the handshake at + the point it would matter, however late the name is supplied. Arming it with an empty + name would disable name checking altogether. */ + if (!usable_peer_hostname( ssl->peer_hostname )) return true; + + SSL_set_hostflags( ssl->ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS ); + if (!X509_VERIFY_PARAM_set1_host( SSL_get0_param( ssl->ssl ), ssl->peer_hostname, 0 )) { + ssl_log(transport, PN_LEVEL_ERROR, "Error: unable to configure peer hostname '%s' for verification", + ssl->peer_hostname); + return false; + } + return true; } // Certificate chain verification callback: return 1 if verified, @@ -302,11 +288,10 @@ static bool match_dns_pattern( const char *hostname, // static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { - if (!preverify_ok || X509_STORE_CTX_get_error_depth(ctx) != 0) - // already failed, or not at peer cert in chain + if (X509_STORE_CTX_get_error_depth(ctx) != 0) + // not at peer cert in chain return preverify_ok; - X509 *cert = X509_STORE_CTX_get_current_cert(ctx); SSL *ssn = (SSL *) X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); if (!ssn) { ssl_log(NULL, PN_LEVEL_ERROR, "Error: unexpected error - SSL session info not available for peer verify!"); @@ -321,65 +306,38 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) pni_ssl_t *ssl = transport->ssl; if (ssl->verify_mode != PN_SSL_VERIFY_PEER_NAME) return preverify_ok; - if (!ssl->peer_hostname) { - ssl_log(transport, PN_LEVEL_ERROR, "Error: configuration error: PN_SSL_VERIFY_PEER_NAME configured, but no peer hostname set!"); + + /* The name match itself was done by OpenSSL, armed by set_verify_host(). A name it would + not arm the check with leaves it unarmed, which would silently accept whatever name the + peer cert carries, so reject that here rather than let the handshake through. */ + if (!usable_peer_hostname( ssl->peer_hostname )) { + if (!ssl->peer_hostname || !*ssl->peer_hostname) + ssl_log(transport, PN_LEVEL_ERROR, "Error: configuration error: PN_SSL_VERIFY_PEER_NAME configured, but no peer hostname set!"); + else + ssl_log(transport, PN_LEVEL_ERROR, "Error: configuration error: invalid peer hostname '%s' - must not start with '.'", + ssl->peer_hostname); return 0; // fail connection } - ssl_log(transport, PN_LEVEL_TRACE, "Checking identifying name in peer cert against '%s'", ssl->peer_hostname); - - bool matched = false; - - /* first check any SubjectAltName entries, as per RFC2818 */ - GENERAL_NAMES *sans = (GENERAL_NAMES *) X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); - if (sans) { - int name_ct = sk_GENERAL_NAME_num( sans ); - int i; - for (i = 0; !matched && i < name_ct; ++i) { - GENERAL_NAME *name = sk_GENERAL_NAME_value( sans, i ); - if (name->type == GEN_DNS) { - ASN1_STRING *asn1 = name->d.dNSName; - if (asn1 && ASN1_STRING_get0_data(asn1) && ASN1_STRING_length(asn1) > 0){ - unsigned char *str; - int len = ASN1_STRING_to_UTF8( &str, asn1 ); - if (len >= 0) { - ssl_log(transport, PN_LEVEL_TRACE, "SubjectAltName (dns) from peer cert = '%.*s'", len, str ); - matched = match_dns_pattern( ssl->peer_hostname, (const char *)str, len ); - OPENSSL_free( str ); - } - } - } - } - GENERAL_NAMES_free( sans ); - } - - /* if no general names match, try the CommonName from the subject */ - const X509_NAME *name = X509_get_subject_name(cert); - int i = -1; - while (!matched && (i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0) { - const X509_NAME_ENTRY *ne = X509_NAME_get_entry(name, i); - const ASN1_STRING *name_asn1 = X509_NAME_ENTRY_get_data(ne); - if (name_asn1) { - unsigned char *str; - int len = ASN1_STRING_to_UTF8( &str, name_asn1); - if (len >= 0) { - ssl_log(transport, PN_LEVEL_TRACE, "commonName from peer cert = '%.*s'", len, str); - matched = match_dns_pattern( ssl->peer_hostname, (const char *)str, len ); - OPENSSL_free(str); + if (!preverify_ok) { + if (X509_STORE_CTX_get_error(ctx) == X509_V_ERR_HOSTNAME_MISMATCH) { + /* No name matched, but a peer named by address may carry it in an iPAddress + SubjectAltName, which the name check does not look at. X509_check_ip_asc() rejects + anything that is not an address literal, so this only ever admits an exact match. */ + if (X509_check_ip_asc(X509_STORE_CTX_get_current_cert(ctx), ssl->peer_hostname, 0) == 1) { + ssl_log(transport, PN_LEVEL_TRACE, "Address in peer cert matched '%s' - peer is valid.", + ssl->peer_hostname); + X509_STORE_CTX_set_error(ctx, X509_V_OK); + return 1; } + ssl_log(transport, PN_LEVEL_ERROR, "Error: no name matching %s found in peer cert - rejecting handshake.", + ssl->peer_hostname); } + return preverify_ok; } - if (!matched) { - ssl_log(transport, PN_LEVEL_ERROR, "Error: no name matching %s found in peer cert - rejecting handshake.", - ssl->peer_hostname); - preverify_ok = 0; -#ifdef X509_V_ERR_APPLICATION_VERIFICATION - X509_STORE_CTX_set_error( ctx, X509_V_ERR_APPLICATION_VERIFICATION ); -#endif - } else { - ssl_log(transport, PN_LEVEL_TRACE, "Name from peer cert matched - peer is valid."); - } + ssl_log(transport, PN_LEVEL_TRACE, "Name from peer cert matched '%s' - peer is valid.", + SSL_get0_peername(ssn) ? SSL_get0_peername(ssn) : ssl->peer_hostname); return preverify_ok; } @@ -387,16 +345,6 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" -// This was introduced in v1.1 -#if OPENSSL_VERSION_NUMBER < 0x10100000 -int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g) -{ - dh->p = p; - dh->q = q; - dh->g = g; - return 1; -} -#endif // this code was generated using the command: // "openssl dhparam -C -2 2048" @@ -576,9 +524,7 @@ static bool pni_init_ssl_domain( pn_ssl_domain_t * domain, pn_ssl_mode_t mode ) ; SSL_CTX_set_options(domain->ctx, reject_insecure); -# ifdef SSL_SECOP_PEER domain->default_seclevel = SSL_CTX_get_security_level(domain->ctx); -# endif DH *dh = get_dh2048(); if (dh) { @@ -875,9 +821,7 @@ int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, case PN_SSL_VERIFY_PEER: case PN_SSL_VERIFY_PEER_NAME: -#ifdef SSL_SECOP_PEER SSL_CTX_set_security_level(domain->ctx, domain->default_seclevel); -#endif if (domain->mode == PN_SSL_MODE_SERVER) { // openssl requires that server connections supply a list of trusted CAs which is @@ -905,10 +849,6 @@ int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, SSL_CTX_set_verify( domain->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback); -#if (OPENSSL_VERSION_NUMBER < 0x00905100L) - SSL_CTX_set_verify_depth(domain->ctx, 1); -#endif - // A bit of a hack - If we asked for peer verification then disallow anonymous ciphers // A much more robust thing would be to ensure that we actually have a peer certificate // when we've finished the SSL handshake @@ -919,10 +859,8 @@ int pn_ssl_domain_set_peer_authentication(pn_ssl_domain_t *domain, break; case PN_SSL_ANONYMOUS_PEER: // hippie free love mode... :) -#ifdef SSL_SECOP_PEER // Must use lowest OpenSSL security level to enable anonymous ciphers. SSL_CTX_set_security_level(domain->ctx, 0); -#endif SSL_CTX_set_verify( domain->ctx, SSL_VERIFY_NONE, NULL ); // Only allow anonymous ciphers if we allow anonymous peers if (!domain->ciphers && !SSL_CTX_set_cipher_list( domain->ctx, CIPHERS_ANONYMOUS )) { @@ -1439,11 +1377,11 @@ static int init_ssl_socket(pn_transport_t* transport, pni_ssl_t *ssl, pn_ssl_dom // store backpointer to pn_transport_t in SSL object: SSL_set_ex_data(ssl->ssl, ssl_ex_data_index, transport); -#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME if (ssl->peer_hostname && ssl->mode == PN_SSL_MODE_CLIENT) { SSL_set_tlsext_host_name(ssl->ssl, ssl->peer_hostname); } -#endif + + if (!set_verify_host(transport, ssl)) return -1; // restore session, if available ssn_restore(transport, ssl); @@ -1517,11 +1455,11 @@ int pn_ssl_set_peer_hostname(pn_ssl_t *ssl0, const char *hostname) if (hostname) { ssl->peer_hostname = pn_strdup(hostname); if (!ssl->peer_hostname) return -2; -#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME if (ssl->ssl && ssl->mode == PN_SSL_MODE_CLIENT) { SSL_set_tlsext_host_name(ssl->ssl, ssl->peer_hostname); } -#endif + // may be set after the socket exists but before the handshake - re-arm the name check + if (!set_verify_host((pn_transport_t *)ssl0, ssl)) return -2; } return 0; } diff --git a/c/src/ssl/schannel.cpp b/c/src/ssl/schannel.cpp index 61ad2bf3f..71838c04f 100644 --- a/c/src/ssl/schannel.cpp +++ b/c/src/ssl/schannel.cpp @@ -1987,57 +1987,25 @@ static bool store_contains(HCERTSTORE store, PCCERT_CONTEXT cert) } /* Match the DNS name pattern from the peer certificate against our configured peer - hostname */ + hostname. RFC 9525 s6.3 permits a wildcard only as the complete left-most label, so + partial wildcards ("ba*.example.com") and stars in any other label are rejected. */ static bool match_dns_pattern(const char *hostname, const char *pattern, int plen) { int slen = (int) strlen(hostname); - if (memchr( pattern, '*', plen ) == NULL) - return (plen == slen && - pn_strncasecmp( pattern, hostname, plen ) == 0); - - /* dns wildcarded pattern - RFC2818 */ - char plabel[64]; /* max label length < 63 - RFC1034 */ - char slabel[64]; - - while (plen > 0 && slen > 0) { - const char *cptr; - int len; - - cptr = (const char *) memchr( pattern, '.', plen ); - len = (cptr) ? cptr - pattern : plen; - if (len > (int) sizeof(plabel) - 1) return false; - memcpy( plabel, pattern, len ); - plabel[len] = 0; - if (cptr) ++len; // skip matching '.' - pattern += len; - plen -= len; - - cptr = (const char *) memchr( hostname, '.', slen ); - len = (cptr) ? cptr - hostname : slen; - if (len > (int) sizeof(slabel) - 1) return false; - memcpy( slabel, hostname, len ); - slabel[len] = 0; - if (cptr) ++len; // skip matching '.' - hostname += len; - slen -= len; - - char *star = strchr( plabel, '*' ); - if (!star) { - if (pn_strcasecmp( plabel, slabel )) return false; - } else { - *star = '\0'; - char *prefix = plabel; - int prefix_len = strlen(prefix); - char *suffix = star + 1; - int suffix_len = strlen(suffix); - if (prefix_len && pn_strncasecmp( prefix, slabel, prefix_len )) return false; - if (suffix_len && pn_strncasecmp( suffix, - slabel + (strlen(slabel) - suffix_len), - suffix_len )) return false; - } + if (plen <= 0 || slen <= 0) return false; + + if (plen > 2 && pattern[0] == '*' && pattern[1] == '.') { + const char *dot = strchr( hostname, '.' ); + if (!dot || dot == hostname) return false; /* needs a non-empty label to consume */ + pattern += 2; plen -= 2; + hostname = dot + 1; slen = (int) strlen(hostname); + } else if (memchr( pattern, '*', plen ) != NULL) { + return false; /* any other wildcard form is rejected */ } - return plen == slen; + /* Comparing plen against slen rejects a pattern with an embedded NUL, whose declared + length will not equal the length of the C string it appears to be. */ + return (plen == slen && pn_strncasecmp( pattern, hostname, plen ) == 0); } // Caller must free the returned buffer @@ -2059,8 +2027,11 @@ static char* wide_to_utf8(LPWSTR wstring) static bool server_name_matches(const char *server_name, CERT_EXTENSION *alt_name_ext, PCCERT_CONTEXT cert) { - // As for openssl.c: alt names first, then CN + // As for openssl.c: DNS alt names take precedence, and the CN is consulted only when the + // certificate carries no DNS SubjectAltName at all - RFC 9525 s6.3. Falling back to the + // CN when SANs are merely unmatched lets a permissive legacy CN override tighter SANs. bool matched = false; + bool have_dns_san = false; if (alt_name_ext) { CERT_ALT_NAME_INFO* alt_name_info = NULL; @@ -2076,6 +2047,7 @@ static bool server_name_matches(const char *server_name, CERT_EXTENSION *alt_nam int name_ct = alt_name_info->cAltEntry; for (int i = 0; !matched && i < name_ct; ++i) { if (alt_name_info->rgAltEntry[i].dwAltNameChoice == CERT_ALT_NAME_DNS_NAME) { + have_dns_san = true; char *alt_name = wide_to_utf8(alt_name_info->rgAltEntry[i].pwszDNSName); if (alt_name) { matched = match_dns_pattern(server_name, (const char *) alt_name, strlen(alt_name)); @@ -2086,7 +2058,7 @@ static bool server_name_matches(const char *server_name, CERT_EXTENSION *alt_nam LocalFree(alt_name_info); } - if (!matched) { + if (!matched && !have_dns_san) { PCERT_INFO info = cert->pCertInfo; DWORD len = CertGetNameString(cert, CERT_NAME_ATTR_TYPE, 0, szOID_COMMON_NAME, 0, 0); char *name = (char *) malloc(len); diff --git a/c/src/tls/openssl.c b/c/src/tls/openssl.c index 8b78891cd..021495460 100644 --- a/c/src/tls/openssl.c +++ b/c/src/tls/openssl.c @@ -54,12 +54,17 @@ #include <openssl/dh.h> #include <openssl/err.h> #include <openssl/x509v3.h> + +// Peer name verification is delegated to X509_VERIFY_PARAM_set1_host()/SSL_set_hostflags(), +// the latter of which needs 1.1.0. The oldest platform we support (RHEL 8) carries 1.1.1. +#if (OPENSSL_VERSION_NUMBER < 0x10100000L) +#error "Proton requires OpenSSL 1.1.0 or later." +#endif #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <assert.h> #include <stdio.h> -#include <ctype.h> #include <string.h> #include <unistd.h> @@ -125,9 +130,7 @@ struct pn_tls_config_t { char *ciphers; int ref_count; -#ifdef SSL_SECOP_PEER - int default_seclevel; -#endif + int default_seclevel; pn_tls_mode_t mode; pn_tls_verify_mode_t verify_mode; @@ -537,83 +540,41 @@ static char *pni_strdup(const char *src) return strcpy(dest, src); } -static int pni_strcasecmp(const char *a, const char *b) +/* A peer hostname we are prepared to verify against. OpenSSL reads a leading '.' as "any + sub-domain of", a broader match than anything we previously accepted, and a DNS name + cannot begin with a dot anyway - so treat it as a configuration error alongside a missing + name rather than let it silently widen what the peer is allowed to present. */ +static bool usable_peer_hostname( const char *name ) { - int diff; - while (*b) { - char aa = *a++, bb = *b++; - diff = tolower(aa)-tolower(bb); - if ( diff!=0 ) return diff; - } - return *a; + return name && *name && name[0] != '.'; } -static int pni_strncasecmp(const char* a, const char* b, size_t len) -{ - int diff = 0; - while (*b && len > 0) { - char aa = *a++, bb = *b++; - diff = tolower(aa)-tolower(bb); - if ( diff!=0 ) return diff; - --len; - }; - return len==0 ? diff : *a; -} +/* Hand peer name verification to OpenSSL. It applies the RFC 9525 s6.3 rules: a wildcard + is honoured only as the complete left-most label, and the subject CN is ignored whenever + the certificate carries a DNS SubjectAltName. NO_PARTIAL_WILDCARDS additionally rejects + "ba*.example.com" style patterns, which OpenSSL would otherwise still accept. - -/* match the DNS name pattern from the peer certificate against our configured peer - hostname */ -static bool match_dns_pattern( const char *hostname, - const char *pattern, int plen ) + Deliberately not SSL_set1_host(): from 3.0 that quietly converts a name which parses as an + IP literal into an iPAddress-SubjectAltName-only check, so a certificate naming its address + in the CommonName - which Proton has always accepted, and which our own test certificates + use - would stop verifying. Older OpenSSL does not do the conversion, so the same call + would also mean different things on different builds. Ask for the name check explicitly + and let verify_callback pick up an iPAddress SubjectAltName. */ +static bool set_verify_host( pn_tls_t *ssl ) { - int slen = (int) strlen(hostname); - if (memchr( pattern, '*', plen ) == NULL) - return (plen == slen && - pni_strncasecmp( pattern, hostname, plen ) == 0); - - /* dns wildcarded pattern - RFC2818 */ - char plabel[64]; /* max label length < 63 - RFC1034 */ - char slabel[64]; - - while (plen > 0 && slen > 0) { - const char *cptr; - int len; - - cptr = (const char *) memchr( pattern, '.', plen ); - len = (cptr) ? cptr - pattern : plen; - if (len > (int) sizeof(plabel) - 1) return false; - memcpy( plabel, pattern, len ); - plabel[len] = 0; - if (cptr) ++len; // skip matching '.' - pattern += len; - plen -= len; - - cptr = (const char *) memchr( hostname, '.', slen ); - len = (cptr) ? cptr - hostname : slen; - if (len > (int) sizeof(slabel) - 1) return false; - memcpy( slabel, hostname, len ); - slabel[len] = 0; - if (cptr) ++len; // skip matching '.' - hostname += len; - slen -= len; - - char *star = strchr( plabel, '*' ); - if (!star) { - if (pni_strcasecmp( plabel, slabel )) return false; - } else { - *star = '\0'; - char *prefix = plabel; - int prefix_len = strlen(prefix); - char *suffix = star + 1; - int suffix_len = strlen(suffix); - if (prefix_len && pni_strncasecmp( prefix, slabel, prefix_len )) return false; - if (suffix_len && pni_strncasecmp( suffix, - slabel + (strlen(slabel) - suffix_len), - suffix_len )) return false; - } + if (!ssl->ssl || ssl->verify_mode != PN_TLS_VERIFY_PEER_NAME) return true; + /* Leave the check unarmed for an unusable name; verify_callback fails the handshake at + the point it would matter, however late the name is supplied. Arming it with an empty + name would disable name checking altogether. */ + if (!usable_peer_hostname( ssl->peer_hostname )) return true; + + SSL_set_hostflags( ssl->ssl, X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS ); + if (!X509_VERIFY_PARAM_set1_host( SSL_get0_param( ssl->ssl ), ssl->peer_hostname, 0 )) { + ssl_log(NULL, PN_LEVEL_ERROR, "Error: unable to configure peer hostname '%s' for verification", + ssl->peer_hostname); + return false; } - - return plen == slen; + return true; } // Certificate chain verification callback: return 1 if verified, @@ -621,11 +582,10 @@ static bool match_dns_pattern( const char *hostname, // static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) { - if (!preverify_ok || X509_STORE_CTX_get_error_depth(ctx) != 0) - // already failed, or not at peer cert in chain + if (X509_STORE_CTX_get_error_depth(ctx) != 0) + // not at peer cert in chain return preverify_ok; - X509 *cert = X509_STORE_CTX_get_current_cert(ctx); SSL *ssn = (SSL *) X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx()); if (!ssn) { ssl_log(NULL, PN_LEVEL_ERROR, "Error: unexpected error - SSL session info not available for peer verify!"); @@ -639,64 +599,36 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) } if (ssl->verify_mode != PN_TLS_VERIFY_PEER_NAME) return preverify_ok; - if (!ssl->peer_hostname) { - ssl_log(NULL, PN_LEVEL_ERROR, "Error: configuration error: PN_TLS_VERIFY_PEER_NAME configured, but no peer hostname set!"); + + /* The name match itself was done by OpenSSL, armed by set_verify_host(). A name it would + not arm the check with leaves it unarmed, which would silently accept whatever name the + peer cert carries, so reject that here rather than let the handshake through. */ + if (!usable_peer_hostname( ssl->peer_hostname )) { + if (!ssl->peer_hostname || !*ssl->peer_hostname) + ssl_log(NULL, PN_LEVEL_ERROR, "Error: configuration error: PN_TLS_VERIFY_PEER_NAME configured, but no peer hostname set!"); + else + ssl_log(NULL, PN_LEVEL_ERROR, "Error: configuration error: invalid peer hostname '%s' - must not start with '.'", + ssl->peer_hostname); return 0; // fail connection } - ssl_log(NULL, PN_LEVEL_TRACE, "Checking identifying name in peer cert against '%s'", ssl->peer_hostname); - - bool matched = false; - - /* first check any SubjectAltName entries, as per RFC2818 */ - GENERAL_NAMES *sans = (GENERAL_NAMES *) X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL); - if (sans) { - int name_ct = sk_GENERAL_NAME_num( sans ); - int i; - for (i = 0; !matched && i < name_ct; ++i) { - GENERAL_NAME *name = sk_GENERAL_NAME_value( sans, i ); - if (name->type == GEN_DNS) { - ASN1_STRING *asn1 = name->d.dNSName; - if (asn1 && asn1->data && asn1->length) { - unsigned char *str; - int len = ASN1_STRING_to_UTF8( &str, asn1 ); - if (len >= 0) { - ssl_log(NULL, PN_LEVEL_TRACE, "SubjectAltName (dns) from peer cert = '%.*s'", len, str ); - matched = match_dns_pattern( ssl->peer_hostname, (const char *)str, len ); - OPENSSL_free( str ); - } - } + if (!preverify_ok) { + if (X509_STORE_CTX_get_error(ctx) == X509_V_ERR_HOSTNAME_MISMATCH) { + /* No name matched, but a peer named by address may carry it in an iPAddress + SubjectAltName, which the name check does not look at. X509_check_ip_asc() rejects + anything that is not an address literal, so this only ever admits an exact match. */ + if (X509_check_ip_asc(X509_STORE_CTX_get_current_cert(ctx), ssl->peer_hostname, 0) == 1) { + ssl_log(NULL, PN_LEVEL_TRACE, "Address in peer cert matched '%s' - peer is valid.", + ssl->peer_hostname); + X509_STORE_CTX_set_error(ctx, X509_V_OK); + return 1; } + ssl_log(NULL, PN_LEVEL_ERROR, "Error: no name matching %s found in peer cert - rejecting handshake.", + ssl->peer_hostname); } - GENERAL_NAMES_free( sans ); - } - - /* if no general names match, try the CommonName from the subject */ - X509_NAME *name = X509_get_subject_name(cert); - int i = -1; - while (!matched && (i = X509_NAME_get_index_by_NID(name, NID_commonName, i)) >= 0) { - X509_NAME_ENTRY *ne = X509_NAME_get_entry(name, i); - ASN1_STRING *name_asn1 = X509_NAME_ENTRY_get_data(ne); - if (name_asn1) { - unsigned char *str; - int len = ASN1_STRING_to_UTF8( &str, name_asn1); - if (len >= 0) { - ssl_log(NULL, PN_LEVEL_TRACE, "commonName from peer cert = '%.*s'", len, str); - matched = match_dns_pattern( ssl->peer_hostname, (const char *)str, len ); - OPENSSL_free(str); - } - } - } - - if (!matched) { - ssl_log(NULL, PN_LEVEL_ERROR, "Error: no name matching %s found in peer cert - rejecting handshake.", - ssl->peer_hostname); - preverify_ok = 0; -#ifdef X509_V_ERR_APPLICATION_VERIFICATION - X509_STORE_CTX_set_error( ctx, X509_V_ERR_APPLICATION_VERIFICATION ); -#endif } else { - ssl_log(NULL, PN_LEVEL_TRACE, "Name from peer cert matched - peer is valid."); + ssl_log(NULL, PN_LEVEL_TRACE, "Name from peer cert matched '%s' - peer is valid.", + SSL_get0_peername(ssn) ? SSL_get0_peername(ssn) : ssl->peer_hostname); } return preverify_ok; @@ -706,16 +638,6 @@ static int verify_callback(int preverify_ok, X509_STORE_CTX *ctx) #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wdeprecated-declarations" -// This was introduced in v1.1 -#if OPENSSL_VERSION_NUMBER < 0x10100000 -int DH_set0_pqg(DH *dh, BIGNUM *p, BIGNUM *q, BIGNUM *g) -{ - dh->p = p; - dh->q = q; - dh->g = g; - return 1; -} -#endif // this code was generated using the command: // "openssl dhparam -C -2 2048" @@ -872,9 +794,7 @@ static bool pni_init_ssl_domain( pn_tls_config_t * domain, pn_tls_mode_t mode ) ; SSL_CTX_set_options(domain->ctx, reject_insecure); -# ifdef SSL_SECOP_PEER domain->default_seclevel = SSL_CTX_get_security_level(domain->ctx); -# endif DH *dh = get_dh2048(); if (dh) { @@ -1011,9 +931,7 @@ int pn_tls_config_set_peer_authentication(pn_tls_config_t *domain, case PN_TLS_VERIFY_PEER: case PN_TLS_VERIFY_PEER_NAME: -#ifdef SSL_SECOP_PEER SSL_CTX_set_security_level(domain->ctx, domain->default_seclevel); -#endif if (domain->mode == PN_TLS_MODE_SERVER) { // openssl requires that server connections supply a list of trusted CAs which is @@ -1041,10 +959,6 @@ int pn_tls_config_set_peer_authentication(pn_tls_config_t *domain, SSL_CTX_set_verify( domain->ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, verify_callback); -#if (OPENSSL_VERSION_NUMBER < 0x00905100L) - SSL_CTX_set_verify_depth(domain->ctx, 1); -#endif - // A bit of a hack - If we asked for peer verification then disallow anonymous ciphers // A much more robust thing would be to ensure that we actually have a peer certificate // when we've finished the SSL handshake @@ -1055,10 +969,8 @@ int pn_tls_config_set_peer_authentication(pn_tls_config_t *domain, break; case PN_TLS_ANONYMOUS_PEER: // hippie free love mode... :) -#ifdef SSL_SECOP_PEER // Must use lowest OpenSSL security level to enable anonymous ciphers. SSL_CTX_set_security_level(domain->ctx, 0); -#endif SSL_CTX_set_verify( domain->ctx, SSL_VERIFY_NONE, NULL ); // Only allow anonymous ciphers if we allow anonymous peers if (!domain->ciphers && !SSL_CTX_set_cipher_list( domain->ctx, CIPHERS_ANONYMOUS )) { @@ -1231,11 +1143,11 @@ static int init_ssl_socket(pn_tls_t *ssl, pn_tls_config_t *domain) // store backpointer SSL_set_ex_data(ssl->ssl, tls_ex_data_index, ssl); -#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME if (ssl->peer_hostname && ssl->mode == PN_TLS_MODE_CLIENT) { SSL_set_tlsext_host_name(ssl->ssl, ssl->peer_hostname); } -#endif + + if (!set_verify_host(ssl)) return -1; if (ssl->alpn_list && ssl->mode == PN_TLS_MODE_CLIENT) { if (SSL_set_alpn_protos(ssl->ssl, ssl->alpn_list, ssl->alpn_list_len) != 0) { @@ -1294,11 +1206,11 @@ int pn_tls_set_peer_hostname(pn_tls_t *ssl, const char *hostname) if (hostname) { ssl->peer_hostname = pni_strdup(hostname); if (!ssl->peer_hostname) return -2; -#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME if (ssl->ssl && ssl->mode == PN_TLS_MODE_CLIENT) { SSL_set_tlsext_host_name(ssl->ssl, ssl->peer_hostname); } -#endif + // may be set after the socket exists but before the handshake - re-arm the name check + if (!set_verify_host(ssl)) return -2; } return 0; } diff --git a/python/tests/proton_tests/ssl.py b/python/tests/proton_tests/ssl.py index 289a4da43..903104ba8 100644 --- a/python/tests/proton_tests/ssl.py +++ b/python/tests/proton_tests/ssl.py @@ -723,119 +723,69 @@ class SslTest(common.Test): self.tearDown() # Wildcarded Certificate - # Assumes: - # 1) certificate contains Server Alternate Names: - # "alternate.name.one.com" and "another.name.com" - # 2) certificate has wildcarded CommonName "*.prefix*.domain.com" + # Assumes the server-wc certificate: + # 1) has SubjectAltNames "alternate.name.one.com", "another.name.com", + # "*.wildcard.domain.com" and the IP address 127.0.0.1 + # 2) has the CommonName "*.prefix*.domain.com" # - - # Pass: match an alternate - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "alternate.Name.one.com" - self._do_handshake(client, server) - del client - del server - self.tearDown() - - # Pass: match an alternate - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "ANOTHER.NAME.COM" - self._do_handshake(client, server) - del client - del server - self.tearDown() - - # Pass: match the pattern - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "SOME.PREfix.domain.COM" - self._do_handshake(client, server) - del client - del server - self.tearDown() - - # Pass: match the pattern - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "FOO.PREfixZZZ.domain.com" - self._do_handshake(client, server) - del client - del server - self.tearDown() - - # Fail: must match prefix on wildcard - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "FOO.PREfi.domain.com" - self._do_handshake(client, server) - assert client.transport.closed - assert server.transport.closed - assert client.connection.state & Endpoint.REMOTE_UNINIT - assert server.connection.state & Endpoint.REMOTE_UNINIT - del server - del client - self.tearDown() - - # Fail: leading wildcards are not optional - self.setUp() - self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), - self._testpath("server-wc-private-key.pem"), - "server-password") - self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) - self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) - - server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) - client = SslTest.SslTestConnection(self.client_domain) - - client.ssl.peer_hostname = "PREfix.domain.COM" - self._do_handshake(client, server) - assert client.transport.closed - assert server.transport.closed - assert client.connection.state & Endpoint.REMOTE_UNINIT - assert server.connection.state & Endpoint.REMOTE_UNINIT - self.tearDown() + # RFC 9525 s6.3 honours a wildcard only as the complete left-most label, and requires + # the CommonName to be ignored entirely when the certificate carries DNS SANs. The CN + # here would match "*.prefix*.domain.com" patterns under the old matcher, so the cases + # below double as a regression test for that precedence. + wildcard_names = [ + # (peer hostname, handshake succeeds?, rationale) + ("alternate.Name.one.com", True, "exact SAN, case insensitive"), + ("ANOTHER.NAME.COM", True, "exact SAN, case insensitive"), + ("foo.wildcard.domain.com", True, "wildcard SAN matches one label"), + ("FOO.Wildcard.Domain.Com", True, "wildcard SAN, case insensitive"), + ("wildcard.domain.com", False, "wildcard must not match the bare domain"), + ("a.b.wildcard.domain.com", False, "wildcard spans a single label only"), + ("foo.WILDCARD.domain.com.evil.com", False, "wildcard is anchored at the end"), + (".wildcard.domain.com", False, "a leading dot must not mean 'any sub-domain'"), + (".one.com", False, "a leading dot must not mean 'any sub-domain'"), + ("SOME.PREfix.domain.COM", False, "CN is ignored when the cert carries SANs"), + ("FOO.PREfixZZZ.domain.com", False, "partial wildcards are not honoured"), + ] + + if os.name != "nt": + # SChannel's matcher only ever looks at DNS names, so an address literal reaches + # it as a name that matches nothing. It has never matched an iPAddress SAN. + wildcard_names += [ + ("127.0.0.1", True, "an address literal matches an iPAddress SAN"), + ("127.0.0.2", False, "a different address does not"), + ] + + for peer_hostname, should_pass, rationale in wildcard_names: + reason = "%s (%s)" % (peer_hostname, rationale) + self.setUp() + self.server_domain.set_credentials(self._testpath("server-wc-certificate.pem"), + self._testpath("server-wc-private-key.pem"), + "server-password") + self.client_domain.set_trusted_ca_db(self._testpath("ca-certificate.pem")) + self.client_domain.set_peer_authentication(SSLDomain.VERIFY_PEER_NAME) + + server = SslTest.SslTestConnection(self.server_domain, mode=Transport.SERVER) + client = SslTest.SslTestConnection(self.client_domain) + + client.ssl.peer_hostname = peer_hostname + self._do_handshake(client, server) + # On success _do_handshake closes the connection again, so transport.closed is + # true either way; REMOTE_UNINIT is what distinguishes "peer never opened". + if should_pass: + assert not client.connection.state & Endpoint.REMOTE_UNINIT, \ + "should have matched: " + reason + assert not server.connection.state & Endpoint.REMOTE_UNINIT, \ + "should have matched: " + reason + else: + assert client.transport.closed, "should have been rejected: " + reason + assert server.transport.closed, "should have been rejected: " + reason + assert client.connection.state & Endpoint.REMOTE_UNINIT, \ + "should have been rejected: " + reason + assert server.connection.state & Endpoint.REMOTE_UNINIT, \ + "should have been rejected: " + reason + del server + del client + self.tearDown() # Pass: ensure that the user can give an alternate name that overrides # the connection's configured hostname @@ -851,12 +801,16 @@ class SslTest(common.Test): conn_hostname="This.Name.Does.not.Match", ssl_peername="alternate.name.one.com") self._do_handshake(client, server) + assert not client.connection.state & Endpoint.REMOTE_UNINIT + assert not server.connection.state & Endpoint.REMOTE_UNINIT del client del server self.tearDown() # Pass: ensure that the hostname supplied by the connection is used if - # none has been specified for the SSL instance + # none has been specified for the SSL instance. server-certificate has no + # SubjectAltName at all, so this also covers the CommonName still being consulted + # when - and only when - there is no DNS SAN to consult instead. self.setUp() self.server_domain.set_credentials(self._testpath("server-certificate.pem"), self._testpath("server-private-key.pem"), @@ -868,6 +822,8 @@ class SslTest(common.Test): client = SslTest.SslTestConnection(self.client_domain, conn_hostname="a1.good.server.domain.com") self._do_handshake(client, server) + assert not client.connection.state & Endpoint.REMOTE_UNINIT + assert not server.connection.state & Endpoint.REMOTE_UNINIT del client del server self.tearDown() diff --git a/python/tests/proton_tests/ssl_db/mkcerts.sh b/python/tests/proton_tests/ssl_db/mkcerts.sh index 0fe91aba7..a78d4e391 100644 --- a/python/tests/proton_tests/ssl_db/mkcerts.sh +++ b/python/tests/proton_tests/ssl_db/mkcerts.sh @@ -37,10 +37,17 @@ keytool -storetype pkcs12 -keystore bad-server.pkcs12 -storepass server-password openssl pkcs12 -nocerts -passin pass:server-password -in bad-server.pkcs12 -passout pass:server-password -out bad-server-private-key.pem openssl pkcs12 -nokeys -passin pass:server-password -in bad-server.pkcs12 -passout pass:server-password -out bad-server-certificate.pem -# Create a server certificate with several alternate names, including a wildcarded common name: -keytool -ext san=dns:alternate.name.one.com,dns:another.name.com -storetype pkcs12 -keystore server-wc.pkcs12 -storepass server-password -alias server-wc-certificate -keypass server-password -keyalg EC -genkeypair -dname "O=Server,CN=*.prefix*.domain.com" -validity 99999 -keytool -ext san=dns:alternate.name.one.com,dns:another.name.com -storetype pkcs12 -keystore server-wc.pkcs12 -storepass server-password -alias server-wc-certificate -keypass server-password -certreq -file server-wc-request.pem -keytool -ext san=dns:alternate.name.one.com,dns:another.name.com -storetype pkcs12 -keystore ca.pkcs12 -storepass ca-password -alias ca -keypass ca-password -gencert -rfc -validity 99999 -infile server-wc-request.pem -outfile server-wc-certificate.pem +# Create a server certificate with several alternate names, one of them a wildcard. +# +# RFC 9525 s6.3 allows a wildcard only as the complete left-most label, so "*.wildcard.domain.com" +# is the only form the library honours. The common name is deliberately left as the older +# "*.prefix*.domain.com": because this certificate carries DNS SubjectAltNames the CN must be +# ignored altogether, and a CN that would otherwise match makes that a testable property. +# The iPAddress name covers a peer named by address rather than by hostname. +WC_SAN=san=dns:alternate.name.one.com,dns:another.name.com,dns:*.wildcard.domain.com,ip:127.0.0.1 +keytool -ext "$WC_SAN" -storetype pkcs12 -keystore server-wc.pkcs12 -storepass server-password -alias server-wc-certificate -keypass server-password -keyalg EC -genkeypair -dname "O=Server,CN=*.prefix*.domain.com" -validity 99999 +keytool -ext "$WC_SAN" -storetype pkcs12 -keystore server-wc.pkcs12 -storepass server-password -alias server-wc-certificate -keypass server-password -certreq -file server-wc-request.pem +keytool -ext "$WC_SAN" -storetype pkcs12 -keystore ca.pkcs12 -storepass ca-password -alias ca -keypass ca-password -gencert -rfc -validity 99999 -infile server-wc-request.pem -outfile server-wc-certificate.pem openssl pkcs12 -nocerts -passin pass:server-password -in server-wc.pkcs12 -passout pass:server-password -out server-wc-private-key.pem # Create a certificate for a subordinate (intermediate) CA certificate issued by the root CA diff --git a/python/tests/proton_tests/ssl_db/server-wc-certificate.p12 b/python/tests/proton_tests/ssl_db/server-wc-certificate.p12 index b4d3287b5..f84723ee7 100644 Binary files a/python/tests/proton_tests/ssl_db/server-wc-certificate.p12 and b/python/tests/proton_tests/ssl_db/server-wc-certificate.p12 differ diff --git a/python/tests/proton_tests/ssl_db/server-wc-certificate.pem b/python/tests/proton_tests/ssl_db/server-wc-certificate.pem index b03f9921b..7bfb480ee 100644 --- a/python/tests/proton_tests/ssl_db/server-wc-certificate.pem +++ b/python/tests/proton_tests/ssl_db/server-wc-certificate.pem @@ -1,12 +1,14 @@ ------BEGIN CERTIFICATE----- -MIIB0TCCAXSgAwIBAgIEL5KM1zAMBggqhkjOPQQDAgUAMDExFzAVBgNVBAMTDlRy -dXN0ZWQuQ0EuY29tMRYwFAYDVQQKEw1UcnVzdCBNZSBJbmMuMCAXDTE5MDMxNDA0 -NDU1M1oYDzIyOTIxMjI2MDQ0NTUzWjAwMR0wGwYDVQQDDBQqLnByZWZpeCouZG9t -YWluLmNvbTEPMA0GA1UEChMGU2VydmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD -QgAEH6HU/fmheSsvQmcypcVi5ya6htApdLxyVTnQ5NjuCoXnsr6mm9cPEH19A43+ -JBVWsdHg9sibx1JInh/JU9BYK6N3MHUwHwYDVR0jBBgwFoAUpc3FsKH3CFshhQ// -NGv22VnJClUwMwYDVR0RBCwwKoIWYWx0ZXJuYXRlLm5hbWUub25lLmNvbYIQYW5v -dGhlci5uYW1lLmNvbTAdBgNVHQ4EFgQUqgEQSNq/wnTz2TXH8tdDCLwnvWUwDAYI -KoZIzj0EAwIFAANJADBGAiEA2AU1P7pwfO70WFHlEyFTEQ5I5FyctS2vYiJN2rH+ -opYCIQD4niWqR+WA+2bkxDmoRST5A3qGCsAuADU5Br11UU9MHQ== ------END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICDTCCAbOgAwIBAgIJAOMB5VK1FXe/MAoGCCqGSM49BAMDMDExFzAVBgNVBAMT +DlRydXN0ZWQuQ0EuY29tMRYwFAYDVQQKEw1UcnVzdCBNZSBJbmMuMCAXDTI2MDky +MzIyMjIwOVoYDzIzMDAwNzA4MjIyMjA5WjAwMR0wGwYDVQQDDBQqLnByZWZpeCou +ZG9tYWluLmNvbTEPMA0GA1UEChMGU2VydmVyMHYwEAYHKoZIzj0CAQYFK4EEACID +YgAEPghkBUYfPCXDgkqQvQjhaHTPShAqI6K8IqnWxtDHMI0rC/UuJT/rRovvj4wF +z3jfWvCHz6Ci9j3cHoACc28aGzxDPFZ5jqGcQ68wXxI83D+KlibOA2yTNL//67tN +hblBo4GVMIGSMB0GA1UdDgQWBBSRSoFInNhJWg6XkCgyXplnhoiJKjBQBgNVHREE +STBHghZhbHRlcm5hdGUubmFtZS5vbmUuY29tghBhbm90aGVyLm5hbWUuY29tghUq +LndpbGRjYXJkLmRvbWFpbi5jb22HBH8AAAEwHwYDVR0jBBgwFoAUpc3FsKH3CFsh +hQ//NGv22VnJClUwCgYIKoZIzj0EAwMDSAAwRQIhAPgcKPFVanHU84NW2ZtoNBCp +WSyKKzsJXAZG83Jx3dAMAiBdjGVwugEecBkKt7J4G1PV/0fM56MHtRIgsJc+hU4F +uw== +-----END CERTIFICATE----- diff --git a/python/tests/proton_tests/ssl_db/server-wc-private-key.pem b/python/tests/proton_tests/ssl_db/server-wc-private-key.pem index c9e000587..0caa59bbe 100644 --- a/python/tests/proton_tests/ssl_db/server-wc-private-key.pem +++ b/python/tests/proton_tests/ssl_db/server-wc-private-key.pem @@ -1,10 +1,11 @@ Bag Attributes friendlyName: server-wc-certificate - localKeyID: 54 69 6D 65 20 31 35 35 32 35 33 38 37 35 33 31 32 30 + localKeyID: 54 69 6D 65 20 31 37 39 30 32 30 32 31 32 38 32 36 39 Key Attributes: <No Attributes> -----BEGIN ENCRYPTED PRIVATE KEY----- -MIGaME4GCSqGSIb3DQEFDTBBMCkGCSqGSIb3DQEFDDAcBAio8aayQeHaFAICCAAw -DAYIKoZIhvcNAgkFADAUBggqhkiG9w0DBwQIiuo/t9M71U0ESFOz+aKK6gyX/QY9 -2aypHy3FSj/7jJxqmCRf6B0yStOsfJElYkuqRWaElLboXuY6QCNDEldnl9RA1GmT -2i25//LzKqGngQYIAg== +MIHDMF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBDHS7cZBnBW44Hudw2P +RmsMAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQAgGt1mCROWxb0/zj +0o2vvQRg20uIJjZ0GZ9Ss0y7pDTdLQ08AcjGtWuf8SXxn2jnjdB+bumhF87fttNQ +qjLaq95M6G4ldk+VaCC6VsLnjX3JovuNOrggWzzf1llRkl/B5r1MqQrfVbcNs2+d +FtMm1l2q -----END ENCRYPTED PRIVATE KEY----- diff --git a/python/tests/proton_tests/ssl_db/server-wc-request.pem b/python/tests/proton_tests/ssl_db/server-wc-request.pem index 8eae46cc4..7ee547334 100644 --- a/python/tests/proton_tests/ssl_db/server-wc-request.pem +++ b/python/tests/proton_tests/ssl_db/server-wc-request.pem @@ -1,10 +1,11 @@ ------BEGIN NEW CERTIFICATE REQUEST----- -MIIBUjCB9wIBADAwMR0wGwYDVQQDDBQqLnByZWZpeCouZG9tYWluLmNvbTEPMA0G -A1UEChMGU2VydmVyMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEH6HU/fmheSsv -QmcypcVi5ya6htApdLxyVTnQ5NjuCoXnsr6mm9cPEH19A43+JBVWsdHg9sibx1JI -nh/JU9BYK6BlMGMGCSqGSIb3DQEJDjFWMFQwMwYDVR0RBCwwKoIWYWx0ZXJuYXRl -Lm5hbWUub25lLmNvbYIQYW5vdGhlci5uYW1lLmNvbTAdBgNVHQ4EFgQUqgEQSNq/ -wnTz2TXH8tdDCLwnvWUwDAYIKoZIzj0EAwIFAANIADBFAiEA0tvlq2F+YlaR8zVS -BpgsSLl1MyoX4ap0hlNw4Ejxm+oCIGIvWIZ2Xh7j8NDBvOJV4q6fubs5+1X3vy34 -6JZIA47d ------END NEW CERTIFICATE REQUEST----- +-----BEGIN NEW CERTIFICATE REQUEST----- +MIIBrDCCATMCAQAwMDEdMBsGA1UEAwwUKi5wcmVmaXgqLmRvbWFpbi5jb20xDzAN +BgNVBAoTBlNlcnZlcjB2MBAGByqGSM49AgEGBSuBBAAiA2IABD4IZAVGHzwlw4JK +kL0I4Wh0z0oQKiOivCKp1sbQxzCNKwv1LiU/60aL74+MBc9431rwh8+govY93B6A +AnNvGhs8QzxWeY6hnEOvMF8SPNw/ipYmzgNskzS//+u7TYW5QaCBgzCBgAYJKoZI +hvcNAQkOMXMwcTAdBgNVHQ4EFgQUkUqBSJzYSVoOl5AoMl6ZZ4aIiSowUAYDVR0R +BEkwR4IWYWx0ZXJuYXRlLm5hbWUub25lLmNvbYIQYW5vdGhlci5uYW1lLmNvbYIV +Ki53aWxkY2FyZC5kb21haW4uY29thwR/AAABMAoGCCqGSM49BAMDA2cAMGQCMDwW +SWGfU0+lxtPty3Qfh55IEJZFKN9RZVUZYhvESeqMHQixHQwJIIZ9GlbIf/yuwAIw +RD4NYAutp28YRRNkGffMVUgpiskHqNtbWBtyXpyN0Bik1UEVyWgm9zt8zAwNVxdh +-----END NEW CERTIFICATE REQUEST----- diff --git a/python/tests/proton_tests/ssl_db/server-wc.pkcs12 b/python/tests/proton_tests/ssl_db/server-wc.pkcs12 index b9936ef63..f1f10cd65 100644 Binary files a/python/tests/proton_tests/ssl_db/server-wc.pkcs12 and b/python/tests/proton_tests/ssl_db/server-wc.pkcs12 differ --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
