diff --git a/src/x509.c b/src/x509.c index d1b66a6bcb0..1323bcb9cb5 100644 --- a/src/x509.c +++ b/src/x509.c @@ -6070,7 +6070,8 @@ static int MatchIpName(const char* name, int nameSz, WOLFSSL_GENERAL_NAME* gn) /* Helper to check if name string matches a single GENERAL_NAME constraint. * permitted selects subtree semantics for wildcard DNS names: containment * for permitted subtrees, intersection for excluded subtrees. - * Returns 1 if matches, 0 if not. */ + * Returns 1 if matches, 0 if not, and a negative error if the name cannot be + * compared, which the caller must treat as failing the constraint. */ static int MatchNameConstraint(int type, const char* name, int nameSz, WOLFSSL_GENERAL_NAME* gn, int permitted) { @@ -6093,6 +6094,11 @@ static int MatchNameConstraint(int type, const char* name, int nameSz, } baseStr = (const char*)wolfSSL_ASN1_STRING_get0_data(gn->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..f73566d1c2e 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 @@ -1054,12 +1062,24 @@ int test_wolfssl_local_MatchBaseName(void) /* @ at end is invalid */ ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, "user@", 5, ".domain.com", 11), 0); - /* double @ is invalid */ + /* An '@' outside a quoted local-part cannot be compared */ + ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, + "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.com", 16, ".domain.com", 11), 0); - /* multiple @ is invalid */ + "user@domain@extra.com", 21, "extra.com", 9), + WC_NO_ERR_TRACE(ASN_PARSE_E)); ExpectIntEQ(wolfssl_local_MatchBaseName(ASN_RFC822_TYPE, - "user@domain@extra.com", 21, ".domain.com", 11), 0); + "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, + "\"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); @@ -1725,14 +1745,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 +1890,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 +1927,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_ossl_x509_ext.c b/tests/api/test_ossl_x509_ext.c index af2bedc46e2..e52982bcff2 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; diff --git a/tests/unit-mcdc/test_asn_ext_whitebox.c b/tests/unit-mcdc/test_asn_ext_whitebox.c index f1b498e6f17..416198ab7c8 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 +907,42 @@ 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"); + + WB_NOTE("CnIsDnsName(): host name syntax"); + WB_CHECK(CnIsDnsName("www.example.com", 15) == 1, "plain host name"); + WB_CHECK(CnIsDnsName("a-b_c.d", 7) == 1, "'-' and '_' inside"); + WB_CHECK(CnIsDnsName("*.example.com", 13) == 1, "wildcard label"); + WB_CHECK(CnIsDnsName("example.com.", 12) == 1, "one trailing dot"); + WB_CHECK(CnIsDnsName(NULL, 3) == 0, "NULL"); + WB_CHECK(CnIsDnsName("a.b", 0) == 0, "zero length"); + WB_CHECK(CnIsDnsName("localhost", 9) == 0, "no dot"); + WB_CHECK(CnIsDnsName("John Doe", 8) == 0, "space"); + WB_CHECK(CnIsDnsName(".a.com", 6) == 0, "leading dot"); + WB_CHECK(CnIsDnsName("-a.com", 6) == 0, "leading hyphen"); + WB_CHECK(CnIsDnsName("a.com-", 6) == 0, "trailing hyphen"); + WB_CHECK(CnIsDnsName("a..com", 6) == 0, "empty label"); + WB_CHECK(CnIsDnsName("a-.com", 6) == 0, "hyphen before dot"); + WB_CHECK(CnIsDnsName("a.com..", 7) == 0, "two trailing dots"); + WB_CHECK(CnIsDnsName(".", 1) == 0, "lone dot"); + WB_NOTE("ConfirmNameConstraints(): URI-without-DNS-host rejection [:19366-:19368]"); XMEMSET(&signer, 0, sizeof(signer)); XMEMSET(&cert, 0, sizeof(cert)); diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index a1d8bae76dc..e81ec18ecf3 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -19885,22 +19885,78 @@ 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 - * caller must treat that as a failed constraint check rather than as either - * answer. */ + * 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 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); @@ -19910,6 +19966,12 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, 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. @@ -19955,13 +20017,12 @@ 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++; @@ -19972,6 +20033,11 @@ int wolfssl_local_MatchBaseName(int type, const char* name, int nameSz, if (atPos < 0 || atPos == 0 || atPos == nameSz - 1) return 0; + 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. * wind the name forward one character past its '@'. */ @@ -20325,7 +20391,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 +20401,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 +20772,50 @@ static int NameConstraintListHasType(Base_entry* list, byte nameType) return 0; } +/* Return 1 if a subject CN has the syntax of a DNS name: letters, digits, + * '_' and '*', with '-' and '.' only inside, no empty label, and at least one + * '.'. One trailing dot (the absolute-FQDN marker) is allowed. This follows + * OpenSSL's cn2dnsid() but keeps '*' so that a wildcard CN stays subject to + * DNS constraints. A CN such as "John Doe" is not a host name and is not + * checked against them. */ +static int CnIsDnsName(const char* cn, int cnLen) +{ + int i; + int hasDot = 0; + + if (cn == NULL || cnLen <= 0) { + return 0; + } + if (cnLen > 1 && cn[cnLen - 1] == '.') { + cnLen--; + } + + 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 == '*') { + continue; + } + /* '-' and '.' may not be first or last. */ + if (i == 0 || i == cnLen - 1) { + return 0; + } + if (c == '-') { + continue; + } + /* A '.' may not follow '-' or '.', nor precede '.'. */ + if (c == '.' && cn[i - 1] != '-' && cn[i - 1] != '.' && + cn[i + 1] != '.') { + hasDot = 1; + continue; + } + return 0; + } + + return hasDot; +} + static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) { const byte nameTypes[] = {ASN_RFC822_TYPE, ASN_DNS_TYPE, ASN_DIR_TYPE, @@ -20730,8 +20850,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 && + CnIsDnsName(cert->subjectCN, cert->subjectCNLen)) { for (dnsSan = cert->altNames; dnsSan != NULL; dnsSan = dnsSan->next) { if (dnsSan->type == ASN_DNS_TYPE)