diff --git a/certs/test/gen-ext-certs.sh b/certs/test/gen-ext-certs.sh index 0d88a7048cd..d654d6f7b9a 100755 --- a/certs/test/gen-ext-certs.sh +++ b/certs/test/gen-ext-certs.sh @@ -109,6 +109,38 @@ EOF gen_cert rm -f ./certs/test/cert-ext-ncdns.cfg +# excluded;DNS: with a zero-length base, the CA/Browser Forum encoding for a +# CA that may not issue DNS names. The OpenSSL config syntax cannot express an +# empty dNSName, so the extension is given as DER. +OUT=certs/test/cert-ext-nc-empty-dns +KEYFILE=certs/test/cert-ext-nc-empty-dns-key.der +CONFIG=certs/test/cert-ext-nc-empty-dns.cfg +tee >$CONFIG <d.ia5); baseLen = wolfSSL_ASN1_STRING_length(gn->d.ia5); + if (type == WOLFSSL_GEN_DNS && baseLen == 0) { + /* RFC 5280 4.2.1.10: an empty dNSName base covers every DNS + * name. */ + return 1; + } if (baseStr == NULL || baseLen <= 0) { return 0; } @@ -6194,7 +6200,8 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc, } hasPermittedType = 1; - if (MatchNameConstraint(type, name, nameSz, gn, 1)) { + /* A name that cannot be compared is not shown to be permitted. */ + if (MatchNameConstraint(type, name, nameSz, gn, 1) == 1) { matchedPermitted = 1; break; } @@ -6221,7 +6228,8 @@ int wolfSSL_NAME_CONSTRAINTS_check_name(WOLFSSL_NAME_CONSTRAINTS* nc, continue; } - if (MatchNameConstraint(type, name, nameSz, gn, 0)) { + /* A name that cannot be compared is not shown to be outside. */ + if (MatchNameConstraint(type, name, nameSz, gn, 0) != 0) { WOLFSSL_MSG("Name in excluded subtrees"); return 0; } diff --git a/tests/api/test_asn.c b/tests/api/test_asn.c index 27d5ade2104..bef1ecf664b 100644 --- a/tests/api/test_asn.c +++ b/tests/api/test_asn.c @@ -28,6 +28,14 @@ #include #include #include + +/* A lone "." dNSName constraint base covers every DNS name, except with + * OPENSSL_COMPATIBLE_DEFAULTS where it matches no name. */ +#ifdef OPENSSL_COMPATIBLE_DEFAULTS + #define NC_DNS_DOT_BASE_MATCH 0 +#else + #define NC_DNS_DOT_BASE_MATCH 1 +#endif #ifdef HAVE_ED25519 #include #endif @@ -1048,21 +1056,52 @@ int test_wolfssl_local_MatchBaseName(void) /* user@sub.domain.com should NOT match domain.com (exact domain only) */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, "user@sub.domain.com", 19, "domain.com", 10), 0); - /* @ at start is invalid */ + /* A mailbox without a local-part or host-part cannot be compared */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "@domain.com", 11, ".domain.com", 11), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "user@", 5, ".domain.com", 11), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + /* Validated before the name-shorter-than-base return */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "u@", 2, "evil.com", 8), WC_NO_ERR_TRACE(ASN_PARSE_E)); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "x@evil.com@", 11, "evil.com", 8), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + /* A host-part starting with '.' cannot be compared */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "x@.evil.com", 11, "evil.com", 8), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + /* A local-part starting with '.' is judged by its host-part */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + ".x@evil.com", 11, "evil.com", 8), 1); ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "@domain.com", 11, ".domain.com", 11), 0); - /* @ at end is invalid */ + ".x@good.com", 11, "evil.com", 8), 0); ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "user@", 5, ".domain.com", 11), 0); - /* double @ is invalid */ + ".x@sub.evil.com", 15, ".evil.com", 9), 1); + /* An '@' outside a quoted local-part cannot be compared */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "user@@domain.com", 16, ".domain.com", 11), 0); - /* multiple @ is invalid */ + "user@@domain.com", 16, ".domain.com", 11), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "user@domain@extra.com", 21, ".domain.com", 11), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "user@domain@extra.com", 21, "extra.com", 9), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "x@evil.com@good.com", 19, "good.com", 8), + WC_NO_ERR_TRACE(ASN_PARSE_E)); + /* RFC 5322 quoted local-part containing @ */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "user@domain@extra.com", 21, ".domain.com", 11), 0); + "\"x@y\"@evil.com", 14, "evil.com", 8), 1); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "\"x@y\"@good.com", 14, "evil.com", 8), 0); /* No @ in email name */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "userdomain.com", 14, ".domain.com", 11), 0); + "userdomain.com", 14, ".domain.com", 11), + WC_NO_ERR_TRACE(ASN_PARSE_E)); /* Email domain doesn't match constraint */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, "user@other.com", 14, ".domain.com", 11), 0); @@ -1725,14 +1764,26 @@ int test_wolfssl_local_MatchBaseName(void) /* Empty/zero size */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, "", 0, "domain.com", 10), 0); + /* RFC 5280 4.2.1.10: an empty dNSName base covers every DNS name. */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, + "domain.com", 10, "", 0), 1); ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, - "domain.com", 10, "", 0), 0); + "domain.com", 10, ".", 1), NC_DNS_DOT_BASE_MATCH); + ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard( + "*.domain.com", 12, "", 0, 0), 1); + ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard( + "*.domain.com", 12, ".", 1, 1), NC_DNS_DOT_BASE_MATCH); + /* An empty rfc822Name base names no host. */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "user@domain.com", 15, "", 0), 0); /* Invalid type */ ExpectIntEQ(wolfssl_local_MatchBaseName(0xFF, "domain.com", 10, "domain.com", 10), 0); - /* Name starting with dot */ + /* Name starting with dot, whatever the base */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, ".", 1, ".", 1), 0); + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, + ".domain.com", 11, "", 0), 0); /* Name shorter than base */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, "a.com", 5, "domain.com", 10), 0); @@ -1858,13 +1909,16 @@ int test_wolfssl_local_MatchDnsConstraintWildcard(void) NULL, 3, 1), 0); ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard("*.com", 0, "com", 3, 1), 0); + /* A zero-length base covers every DNS name (RFC 5280 4.2.1.10). */ ExpectIntEQ(wolfssl_local_MatchDnsConstraintWildcard("*.com", 5, - "com", 0, 1), 0); + "com", 0, 1), 1); /* Name beginning with a dot is invalid. */ ExpectIntEQ(dnsWildPermitted(".x.com", "com"), 0); ExpectIntEQ(dnsWildExcluded(".x.com", "com"), 0); - /* Base that is only dots collapses to nothing. */ - ExpectIntEQ(dnsWildExcluded("*.example.com", "."), 0); + /* A lone dot is the empty base unless OpenSSL compatible. */ + ExpectIntEQ(dnsWildExcluded("*.example.com", "."), + NC_DNS_DOT_BASE_MATCH); + /* A base of more than one dot has an empty label. */ ExpectIntEQ(dnsWildExcluded("*.example.com", ".."), 0); /* SAN has an empty interior label ("*..com"), but only the right-most * "com" label overlaps the base "com" -- the empty label sits outside the @@ -1892,6 +1946,9 @@ int test_wolfssl_local_MatchUriNameConstraint(void) * Exact host match (no leading dot in the constraint). */ ExpectIntEQ(uriNC("https://host.com/path", "host.com"), 1); + /* A lone "." base is a leading-dot base with no labels. */ + ExpectIntEQ(uriNC("https://host.com/path", "."), + NC_DNS_DOT_BASE_MATCH); ExpectIntEQ(uriNC("https://host.com", "host.com"), 1); ExpectIntEQ(uriNC("https://host.com:8443/x", "host.com"), 1); ExpectIntEQ(uriNC("ftp://user@host.com/x", "host.com"), 1); diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 082da1ef6f1..e42595797fc 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -1644,9 +1644,15 @@ int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void) * constraint, so these names are themselves unconstrained. */ static const byte rid_dummy[] = { 0x2A, 0x03, 0x04 }; /* OID 1.2.3.4 */ static const byte ip_dummy[] = { 203, 0, 113, 7 }; + /* CNs that hostname verification can match although they hold no '.' + * or are IP literals; each is outside the permitted dNSName subtrees. */ + static const char* const hostCns[] = { + "localhost", "intranet", "2001:db8::1", "*" + }; byte *der = NULL; int derSz; + int i; byte *pt; WOLFSSL_X509 *x509 = NULL; WOLFSSL_X509 *ca = NULL; @@ -1739,6 +1745,36 @@ int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void) wolfSSL_X509_free(x509); x509 = NULL; + /* Negative cases: leaf with no SAN and a single-label, IPv6 or lone + * wildcard CN. Each stays subject to the DNS constraints. */ + for (i = 0; i < (int)(sizeof(hostCns) / sizeof(hostCns[0])); i++) { + ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert, + WOLFSSL_FILETYPE_PEM)); + ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca)); + ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), + WOLFSSL_SUCCESS); + name = NULL; + + ExpectNotNull(name = X509_NAME_new()); + ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "countryName", + MBSTRING_UTF8, (byte*)"US", 2, -1, 0), SSL_SUCCESS); + ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", + MBSTRING_UTF8, (const byte*)hostCns[i], + (int)XSTRLEN(hostCns[i]), -1, 0), SSL_SUCCESS); + ExpectIntEQ(wolfSSL_X509_set_subject_name(x509, name), + WOLFSSL_SUCCESS); + X509_NAME_free(name); + name = NULL; + + ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0); + ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz))); + ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, + WOLFSSL_FILETYPE_ASN1), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + wolfSSL_X509_free(x509); + x509 = NULL; + } + /* Positive case: leaf with an in-scope CN and no SAN. */ ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert, WOLFSSL_FILETYPE_PEM)); @@ -1844,6 +1880,98 @@ int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void) return EXPECT_RESULT(); } +int test_wolfSSL_CertManagerNameConstraint_EmptyDNS(void) +{ + EXPECT_DECLS; +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CM_VERIFY) && !defined(NO_RSA) && \ + defined(OPENSSL_EXTRA) && defined(WOLFSSL_CERT_GEN) && \ + defined(WOLFSSL_CERT_EXT) && defined(WOLFSSL_ALT_NAMES) && \ + !defined(NO_SHA256) && !defined(IGNORE_NAME_CONSTRAINTS) + /* CA (cert-ext-nc-empty-dns.der) has excluded;DNS: with a zero-length + * base, which covers every DNS name (RFC 5280 4.2.1.10). */ + WOLFSSL_CERT_MANAGER* cm = NULL; + WOLFSSL_EVP_PKEY *priv = NULL; + WOLFSSL_X509_NAME* name = NULL; + const char* ca_cert = "./certs/test/cert-ext-nc-empty-dns.der"; + const char* server_cert = "./certs/test/server-goodcn.pem"; + /* Leaf subject CN, SAN to add (NULL for none), SAN type and result. */ + static const struct { + const char* cn; + const char* san; + int sanType; + int expect; + } cases[] = { + { "John Doe", "user@wolfssl.com", ASN_RFC822_TYPE, + WOLFSSL_SUCCESS }, + { "John Doe", NULL, 0, + WOLFSSL_SUCCESS }, + { "John Doe", "www.wolfssl.com", ASN_DNS_TYPE, + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E) }, + { "www.wolfssl.com", NULL, 0, + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E) }, + { "localhost", NULL, 0, + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E) }, + { "2001:db8::1", NULL, 0, + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E) } + }; + byte *der = NULL; + int derSz; + int i; + byte *pt; + WOLFSSL_X509 *x509 = NULL; + WOLFSSL_X509 *ca = NULL; + + pt = (byte*)server_key_der_2048; + ExpectNotNull(priv = wolfSSL_d2i_PrivateKey(EVP_PKEY_RSA, NULL, + (const unsigned char**)&pt, sizeof_server_key_der_2048)); + + /* The CA itself must load in both ASN decoders. */ + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + ExpectNotNull(ca = wolfSSL_X509_load_certificate_file(ca_cert, + WOLFSSL_FILETYPE_ASN1)); + ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(ca, &derSz))); + ExpectIntEQ(wolfSSL_CertManagerLoadCABuffer(cm, der, derSz, + WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + + for (i = 0; i < (int)(sizeof(cases) / sizeof(cases[0])); i++) { + ExpectNotNull(x509 = wolfSSL_X509_load_certificate_file(server_cert, + WOLFSSL_FILETYPE_PEM)); + ExpectNotNull(name = wolfSSL_X509_get_subject_name(ca)); + ExpectIntEQ(wolfSSL_X509_set_issuer_name(x509, name), + WOLFSSL_SUCCESS); + name = NULL; + + ExpectNotNull(name = X509_NAME_new()); + ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "countryName", + MBSTRING_UTF8, (byte*)"US", 2, -1, 0), SSL_SUCCESS); + ExpectIntEQ(X509_NAME_add_entry_by_txt(name, "commonName", + MBSTRING_UTF8, (const byte*)cases[i].cn, + (int)XSTRLEN(cases[i].cn), -1, 0), SSL_SUCCESS); + ExpectIntEQ(wolfSSL_X509_set_subject_name(x509, name), + WOLFSSL_SUCCESS); + X509_NAME_free(name); + name = NULL; + + if (cases[i].san != NULL) { + ExpectIntEQ(wolfSSL_X509_add_altname(x509, cases[i].san, + cases[i].sanType), WOLFSSL_SUCCESS); + } + ExpectIntGT(wolfSSL_X509_sign(x509, priv, EVP_sha256()), 0); + ExpectNotNull((der = (byte*)wolfSSL_X509_get_der(x509, &derSz))); + ExpectIntEQ(wolfSSL_CertManagerVerifyBuffer(cm, der, derSz, + WOLFSSL_FILETYPE_ASN1), cases[i].expect); + wolfSSL_X509_free(x509); + x509 = NULL; + } + + wolfSSL_CertManagerFree(cm); + wolfSSL_X509_free(ca); + wolfSSL_EVP_PKEY_free(priv); +#endif + return EXPECT_RESULT(); +} + int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void) { EXPECT_DECLS; diff --git a/tests/api/test_certman.h b/tests/api/test_certman.h index 37b4e4fa1dd..7c8ff911821 100644 --- a/tests/api/test_certman.h +++ b/tests/api/test_certman.h @@ -36,6 +36,7 @@ int test_wolfSSL_CertManagerNameConstraint3(void); int test_wolfSSL_CertManagerNameConstraint4(void); int test_wolfSSL_CertManagerNameConstraint5(void); int test_wolfSSL_CertManagerNameConstraint_DNS_CN(void); +int test_wolfSSL_CertManagerNameConstraint_EmptyDNS(void); int test_wolfSSL_CertManagerNameConstraint_IP_SAN(void); int test_wolfSSL_CertManagerNameConstraint_RID_SAN(void); int test_wolfSSL_X509_get_ext_d2i_RID_SAN(void); @@ -82,6 +83,7 @@ int test_wolfSSL_trust_peer_cert_dates(void); TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint4), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint5), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_DNS_CN), \ + TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_EmptyDNS), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_IP_SAN), \ TEST_DECL_GROUP("certman", test_wolfSSL_CertManagerNameConstraint_RID_SAN), \ TEST_DECL_GROUP("certman", test_wolfSSL_X509_get_ext_d2i_RID_SAN), \ diff --git a/tests/api/test_ossl_x509_ext.c b/tests/api/test_ossl_x509_ext.c index af2bedc46e2..fbf6db2479f 100644 --- a/tests/api/test_ossl_x509_ext.c +++ b/tests/api/test_ossl_x509_ext.c @@ -37,6 +37,13 @@ #include #include +/* A lone "." dNSName constraint base covers every DNS name, except with + * OPENSSL_COMPATIBLE_DEFAULTS where it matches no name. */ +#ifdef OPENSSL_COMPATIBLE_DEFAULTS + #define NC_DNS_DOT_BASE_MATCH 0 +#else + #define NC_DNS_DOT_BASE_MATCH 1 +#endif int test_wolfSSL_X509_get_extension_flags(void) { @@ -2367,6 +2374,12 @@ int test_wolfSSL_NAME_CONSTRAINTS_check_name(void) /* @ at end */ ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, "user@", 5), 0); + /* An '@' outside a quoted local-part is not permitted */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + "x@evil.com@sub.wolfssl.com", 26), 0); + /* A quoted local-part may hold an '@' */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + "\"x@y\"@sub.wolfssl.com", 21), 1); } NAME_CONSTRAINTS_free(nc); @@ -2436,6 +2449,44 @@ int test_wolfSSL_NAME_CONSTRAINTS_check_name(void) "bar.example.com", 15), 1); } + if (EXPECT_SUCCESS()) { + /* An empty excluded dNSName covers every DNS name (RFC 5280 + * 4.2.1.10), as does a lone trailing dot unless OpenSSL + * compatible. */ + GENERAL_SUBTREE* subtree = NULL; + + ExpectNotNull(subtree = + wolfSSL_sk_GENERAL_SUBTREE_value(nc->excludedSubtrees, 0)); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5, + "", 0), WOLFSSL_SUCCESS); + } + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "evil.com", 8), 0); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "*.evil.com", 10), 0); + if (EXPECT_SUCCESS()) { + ExpectIntEQ(wolfSSL_ASN1_STRING_set(subtree->base->d.ia5, + ".", 1), WOLFSSL_SUCCESS); + } + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "evil.com", 8), !NC_DNS_DOT_BASE_MATCH); + } + + if (EXPECT_SUCCESS()) { + /* Moved to the permitted side, an empty dNSName permits every DNS + * name. */ + nc->permittedSubtrees = nc->excludedSubtrees; + nc->excludedSubtrees = NULL; + ExpectIntEQ(wolfSSL_ASN1_STRING_set( + wolfSSL_sk_GENERAL_SUBTREE_value(nc->permittedSubtrees, 0)-> + base->d.ia5, "", 0), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "any.example.org", 15), 1); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_DNS, + "*.example.org", 13), 1); + } + NAME_CONSTRAINTS_free(nc); nc = NULL; @@ -2677,6 +2728,19 @@ int test_wolfSSL_NAME_CONSTRAINTS_excluded(void) /* Excluded email .blocked.example.com, should be blocked */ ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, "user@www.blocked.example.com", 28), 0); + + /* A leading '.' in the local-part does not hide the host-part */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + ".x@www.example.com", 18), 1); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + ".x@www.blocked.example.com", 26), 0); + + /* A malformed mailbox is not shown to be outside the excluded + * subtree */ + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + "x@www.blocked.example.com@", 26), 0); + ExpectIntEQ(wolfSSL_NAME_CONSTRAINTS_check_name(nc, GEN_EMAIL, + "x@.www.blocked.example.com", 26), 0); } NAME_CONSTRAINTS_free(nc); diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index f1b498e6f17..751ddd1d833 100644 --- a/tests/unit-mcdc/test_asn_ext_whitebox.c +++ b/tests/unit-mcdc/test_asn_ext_whitebox.c @@ -145,6 +145,14 @@ static int wb_fail = 0; do { if (!(cond)) { printf(" [wbext][FAIL] %s\n", (msg)); wb_fail = 1; } } \ while (0) +/* A lone "." dNSName constraint base covers every DNS name, except with + * OPENSSL_COMPATIBLE_DEFAULTS where it matches no name. */ +#ifdef OPENSSL_COMPATIBLE_DEFAULTS + #define NC_DNS_DOT_BASE_MATCH 0 +#else + #define NC_DNS_DOT_BASE_MATCH 1 +#endif + /* ======================================================================== * Small stack-based pools for Base_entry / DNS_entry so name-constraint * tests never need heap bookkeeping. @@ -186,10 +194,24 @@ static DNS_entry* wb_mk_dns(const char* name, int len, int type) static void wb_match_base_name(void) { WB_NOTE("MatchBaseName(): trailing-dot trim to empty [:18555]"); - /* Both name and base are a single trailing dot: after trim both become - * length 0 -> both operands true. */ + /* A name starting with '.' is rejected before the base is looked at, + * even when the base covers every DNS name. */ WB_CHECK(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, ".", 1, ".", 1) == 0, - "both trim to empty (both true)"); + "lone dot name against lone dot base"); + WB_CHECK(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, ".x.com", 6, "", 0) + == 0, "leading-dot name against zero-length base"); + /* A zero-length base covers every DNS name (RFC 5280 4.2.1.10). */ + WB_CHECK(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, "evil.com", 8, "", 0) + == 1, "zero-length base matches all names"); + /* A one-byte base other than '.' is an ordinary base. */ + WB_CHECK(wolfssl_local_MatchBaseName(ASN_DNS_TYPE, "a.b", 3, "b", 1) + == 1, "one-byte non-dot base"); + /* A zero-length base of another type matches nothing. */ + WB_CHECK(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, "u@a.com", 7, "", + 0) == 0, "RFC822 zero-length base"); + WB_CHECK(wolfssl_local_MatchBaseName(ASN_DIR_TYPE, + "\x31\x0a\x30\x08\x06\x03\x55\x04\x0a\x0c\x01""a", 12, "", 0) + == 0, "DIR zero-length base"); /* name trims to empty, base does not -> 1st true, 2nd false (base * longer than the dot alone; nameSz(0) < baseSz so returns via the * nameSz not needed [need=0]"); WB_CHECK(PermittedListOk(&dirName, NULL, ASN_DIR_TYPE) == 1, "no restriction of this type -> ok"); @@ -819,6 +920,70 @@ static void wb_confirm_name_constraints(void) WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 1, "subjectCN==NULL (1st operand false)"); + WB_NOTE("ConfirmNameConstraints(): subjectCN fallback needs a host name"); + /* An excluded zero-length dNSName excludes every DNS name, but a CN + * that is not a host name is not a DNS name. */ + XMEMSET(&signer, 0, sizeof(signer)); + signer.excludedNames = wb_mk_base(NULL, "", 0, ASN_DNS_TYPE); + XMEMSET(&cert, 0, sizeof(cert)); + cert.subjectCN = "John Doe"; + cert.subjectCNLen = 8; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 1, + "non-hostname CN skips DNS constraints"); + cert.subjectCN = "www.example.com"; + cert.subjectCNLen = 15; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "hostname CN excluded by zero-length dNSName"); + cert.subjectCN = "*.example.com"; + cert.subjectCNLen = 13; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "wildcard CN excluded by zero-length dNSName"); + /* Single-label and IPv6 CNs can still be matched by hostname + * verification, so they stay subject to DNS constraints. */ + cert.subjectCN = "localhost"; + cert.subjectCNLen = 9; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "single-label CN excluded by zero-length dNSName"); + cert.subjectCN = "2001:db8::1"; + cert.subjectCNLen = 11; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "IPv6 CN excluded by zero-length dNSName"); + + /* Under a permitted dNSName subtree, a single-label or IPv6 CN is + * outside it and rejected. */ + XMEMSET(&signer, 0, sizeof(signer)); + signer.permittedNames = wb_mk_base(NULL, "example.com", 11, ASN_DNS_TYPE); + cert.subjectCN = "intranet"; + cert.subjectCNLen = 8; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "single-label CN outside permitted dNSName"); + cert.subjectCN = "2001:db8::1"; + cert.subjectCNLen = 11; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 0, + "IPv6 CN outside permitted dNSName"); + cert.subjectCN = "www.example.com"; + cert.subjectCNLen = 15; + WB_CHECK(ConfirmNameConstraints(&signer, &cert) == 1, + "hostname CN inside permitted dNSName"); + + WB_NOTE("CnMayBeHostName(): host name or IP literal characters"); + WB_CHECK(CnMayBeHostName("www.example.com", 15) == 1, "plain host name"); + WB_CHECK(CnMayBeHostName("a-b_c.d", 7) == 1, "'-' and '_' inside"); + WB_CHECK(CnMayBeHostName("*.example.com", 13) == 1, "wildcard label"); + WB_CHECK(CnMayBeHostName("example.com.", 12) == 1, "trailing dot"); + WB_CHECK(CnMayBeHostName("localhost", 9) == 1, "single label"); + WB_CHECK(CnMayBeHostName("*", 1) == 1, "lone wildcard"); + WB_CHECK(CnMayBeHostName("2001:db8::1", 11) == 1, "IPv6 literal"); + WB_CHECK(CnMayBeHostName("::ffff:10.0.0.1", 15) == 1, + "IPv4-mapped IPv6 literal"); + WB_CHECK(CnMayBeHostName("10.0.0.1", 8) == 1, "IPv4 literal"); + WB_CHECK(CnMayBeHostName("a..com", 6) == 1, "malformed but matchable"); + WB_CHECK(CnMayBeHostName(NULL, 3) == 0, "NULL"); + WB_CHECK(CnMayBeHostName("a.b", 0) == 0, "zero length"); + WB_CHECK(CnMayBeHostName("John Doe", 8) == 0, "space"); + WB_CHECK(CnMayBeHostName("a@b.com", 7) == 0, "'@'"); + WB_CHECK(CnMayBeHostName("a/b", 3) == 0, "'/'"); + WB_NOTE("ConfirmNameConstraints(): URI-without-DNS-host rejection [:19366-:19368]"); XMEMSET(&signer, 0, sizeof(signer)); XMEMSET(&cert, 0, sizeof(cert)); @@ -1427,6 +1592,14 @@ static void wb_decode_name_constraints(void) 0x30,0x06, 0x82,0x04, 'h','o','s','t' }; + /* excludedSubtrees holding a zero-length dNSName, the CA/Browser Forum + * encoding for a CA that may not issue DNS names. */ + static const byte ncEmptyDns[] = { + 0x30,0x06, + 0xA1,0x04, /* [1] excludedSubtrees */ + 0x30,0x02, + 0x82,0x00 + }; WB_NOTE("DecodeNameConstraints(): hasUnsupported -> extNameConstraintHasUnsupported [:21217]"); XMEMSET(&cert, 0, sizeof(cert)); @@ -1438,6 +1611,15 @@ static void wb_decode_name_constraints(void) ret = DecodeNameConstraints(ncSupported, sizeof(ncSupported), &cert); WB_CHECK(ret == 0 && cert.extNameConstraintHasUnsupported == 0, "recognized form only (2nd operand false)"); + + WB_NOTE("DecodeNameConstraints(): zero-length dNSName subtree"); + XMEMSET(&cert, 0, sizeof(cert)); + ret = DecodeNameConstraints(ncEmptyDns, sizeof(ncEmptyDns), &cert); + WB_CHECK(ret == 0 && cert.excludedNames != NULL && + cert.excludedNames->type == ASN_DNS_TYPE && + cert.excludedNames->nameSz == 0, + "zero-length dNSName stored as an empty excluded subtree"); + FreeNameSubtrees(cert.excludedNames, NULL); } #else static void wb_decode_name_constraints(void) { WB_NOTE("IGNORE_NAME_CONSTRAINTS; skipped"); } diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index a1d8bae76dc..034ebfe7d6e 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -19885,31 +19885,95 @@ static int MatchDirectoryName(const byte* name, word32 nameSz, return 1; } +/* Return 1 if a dNSName constraint base is empty once its absolute-FQDN + * trailing dot is removed. RFC 5280 4.2.1.10: any DNS name can be built by + * adding labels to the left of the empty name, so such a base covers every + * DNS name. With OPENSSL_COMPATIBLE_DEFAULTS a lone "." is not treated as + * empty and so matches no name, which is what OpenSSL decides for any name + * without a trailing dot. */ +static int DnsBaseIsEmpty(const char* base, int baseSz) +{ +#ifdef OPENSSL_COMPATIBLE_DEFAULTS + (void)base; + return (baseSz == 0); +#else + return (baseSz == 0) || (baseSz == 1 && base[0] == '.'); +#endif +} + +/* Return 1 if the local-part of a mailbox, the localSz bytes before its last + * '@', may be split there. A local-part holding another '@' must be a quoted + * string (RFC 5322 3.2.4), otherwise the host-part is ambiguous. */ +static int Rfc822LocalPartOk(const char* local, int localSz) +{ + int i; + + for (i = 0; i < localSz; i++) { + if (local[i] == '@') { + break; + } + } + if (i == localSz) { + return 1; + } + + if (localSz < 2 || local[0] != '"' || local[localSz - 1] != '"') { + return 0; + } + /* Inside the quotes a '"' or a backslash must follow a backslash. */ + for (i = 1; i < localSz - 1; i++) { + if (local[i] == '\\') { + i++; + if (i >= localSz - 1) { + return 0; /* the closing quote is escaped */ + } + } + else if (local[i] == '"') { + return 0; + } + } + + return 1; +} + /* Match a name against a name-constraint subtree of the same GeneralName * type. * - * Returns 1 on match and 0 on no match. For ASN_DIR_TYPE a negative error - * is returned when either operand is not a name that can be compared; the + * Returns 1 on match and 0 on no match. A negative error is returned when + * either operand is not a name that can be compared: for ASN_DIR_TYPE when a + * DN cannot be parsed, and for ASN_RFC822_TYPE when a mailbox has no + * local-part or host-part, or holds an '@' outside a quoted local-part. The * caller must treat that as a failed constraint check rather than as either * answer. */ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, const char* base, int baseSz) { - if (base == NULL || baseSz <= 0 || name == NULL || nameSz <= 0 || + if (base == NULL || baseSz < 0 || name == NULL || nameSz <= 0 || (type != ASN_RFC822_TYPE && type != ASN_DNS_TYPE && type != ASN_DIR_TYPE)) { return 0; } + if (type != ASN_DNS_TYPE && baseSz == 0) { + return 0; + } + if (type == ASN_DIR_TYPE) { return MatchDirectoryName((const byte*)name, (word32)nameSz, (const byte*)base, (word32)baseSz); } - if (name[0] == '.') { + /* An rfc822Name is judged by its host-part, checked below. */ + if (type == ASN_DNS_TYPE && name[0] == '.') { return 0; } + /* An empty dNSName base covers all DNS names. Report a match so that an + * excluded empty subtree excludes everything. */ + if (type == ASN_DNS_TYPE && DnsBaseIsEmpty(base, baseSz)) { + return 1; + } + if (type == ASN_DNS_TYPE) { /* MatchDomainName treats one trailing dot as the absolute-FQDN marker. * Apply the same normalization before enforcing DNS name constraints. @@ -19925,12 +19989,10 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, } } - if (nameSz < baseSz) { - return 0; - } - /* If an email type, handle special cases where the base is only - * a domain, or is an email address itself. */ + * a domain, or is an email address itself. The mailbox is validated + * before any no-match return so that a malformed name cannot pass as + * outside an excluded subtree. */ if (type == ASN_RFC822_TYPE) { const char* p = NULL; int count = 0; @@ -19955,22 +20017,29 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, } } - /* verify that name is a valid email address, store @ position */ + /* Split the mailbox at its last '@'. The host-part never holds an + * '@'; a quoted local-part may (RFC 5322). */ p = name; count = 0; while (count < nameSz) { if (*p == '@') { - if (atPos >= 0) - return 0; /* Multiple '@' in name is invalid */ atPos = count; } count++; p++; } - /* Validate '@' exists and is not at start or end */ - if (atPos < 0 || atPos == 0 || atPos == nameSz - 1) - return 0; + /* A mailbox needs a local-part and a host-part not starting with + * '.'. */ + if (atPos <= 0 || atPos == nameSz - 1 || name[atPos + 1] == '.') { + WOLFSSL_MSG("rfc822Name has no usable local-part or host-part"); + return ASN_PARSE_E; + } + + if (!Rfc822LocalPartOk(name, atPos)) { + WOLFSSL_MSG("rfc822Name has an unquoted '@' in its local-part"); + return ASN_PARSE_E; + } if (!baseIsEmail) { /* Base isn't an email address but a domain or host. @@ -19980,6 +20049,10 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, } } + if (nameSz < baseSz) { + return 0; + } + /* RFC 5280 section 4.2.1.10 * "...Any DNS name that can be constructed by simply adding zero or more * labels to the left-hand side of the name satisfies the name constraint." @@ -20325,7 +20398,7 @@ int wolfssl_local_MatchDnsConstraintWildcard(const char* name, int nameSz, int baseLead; int ni, bi; - if (name == NULL || base == NULL || nameSz <= 0 || baseSz <= 0) { + if (name == NULL || base == NULL || nameSz <= 0 || baseSz < 0) { return 0; } @@ -20335,10 +20408,20 @@ int wolfssl_local_MatchDnsConstraintWildcard(const char* name, int nameSz, if (name[nameSz - 1] == '.') { nameSz--; } + if (nameSz <= 0 || name[0] == '.') { + return 0; + } + + /* An empty base covers all DNS names: every expansion is contained in + * it and every expansion intersects it. */ + if (DnsBaseIsEmpty(base, baseSz)) { + return 1; + } + if (base[baseSz - 1] == '.') { baseSz--; } - if (nameSz <= 0 || baseSz <= 0 || name[0] == '.') { + if (baseSz <= 0) { return 0; } @@ -20696,6 +20779,32 @@ static int NameConstraintListHasType(Base_entry* list, byte nameType) return 0; } +/* Return 1 if hostname verification could match a subject CN, so the CN + * must stay subject to DNS constraints. Any CN made only of letters, digits + * and "-._*:" qualifies: that covers single-label names such as "localhost", + * wildcards, and IP literals that MatchDomainName also accepts. A CN holding + * any other byte, such as the space in "John Doe", is not a host name. */ +static int CnMayBeHostName(const char* cn, int cnLen) +{ + int i; + + if (cn == NULL || cnLen <= 0) { + return 0; + } + + for (i = 0; i < cnLen; i++) { + unsigned char c = (unsigned char)cn[i]; + + if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || + (c >= '0' && c <= '9') || c == '-' || c == '.' || + c == '_' || c == '*' || c == ':')) { + return 0; + } + } + + return 1; +} + static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) { const byte nameTypes[] = {ASN_RFC822_TYPE, ASN_DNS_TYPE, ASN_DIR_TYPE, @@ -20730,8 +20839,10 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) * hostname-in-CN fallback only when the cert presents no * dNSName SAN (RFC 6125 6.4.4). A SAN of another type * (iPAddress/registeredID/otherName/...) must NOT suppress - * this check. Skip for CA certs. */ - if (cert->subjectCN != NULL && !cert->isCA) { + * this check. Skip for CA certs and for a CN that is not a + * host name. */ + if (cert->subjectCN != NULL && !cert->isCA && + CnMayBeHostName(cert->subjectCN, cert->subjectCNLen)) { for (dnsSan = cert->altNames; dnsSan != NULL; dnsSan = dnsSan->next) { if (dnsSan->type == ASN_DNS_TYPE) diff --git a/wolfcrypt/src/asn_orig.c b/wolfcrypt/src/asn_orig.c index c8d0b820f6d..1887450af81 100644 --- a/wolfcrypt/src/asn_orig.c +++ b/wolfcrypt/src/asn_orig.c @@ -4080,6 +4080,7 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, while (idx < (word32)sz) { int seqLength, strLength; + int lenRet; word32 nameIdx; byte b, bType; @@ -4104,7 +4105,11 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, nameIdx = idx; b = input[nameIdx++]; - if (GetLength(input, &nameIdx, &strLength, sz) <= 0) { + /* A zero-length dNSName is the empty base covering every DNS name + * (RFC 5280 4.2.1.10). */ + lenRet = GetLength(input, &nameIdx, &strLength, sz); + if (lenRet < 0 || (lenRet == 0 && + b != (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE))) { WOLFSSL_MSG("\tinvalid length"); return ASN_PARSE_E; }