diff --git a/.wolfssl_known_macro_extras b/.wolfssl_known_macro_extras index c676c15524..b182ed57c0 100644 --- a/.wolfssl_known_macro_extras +++ b/.wolfssl_known_macro_extras @@ -955,6 +955,7 @@ WOLFSSL_WC_SLHDSA_RECURSIVE WOLFSSL_WC_XMSS_NO_SHA256 WOLFSSL_WC_XMSS_NO_SHAKE256 WOLFSSL_WICED_PSEUDO_UNIX_EPOCH_TIME +WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE WOLFSSL_X509_STORE_CERTS WOLFSSL_X509_TRUSTED_CERTIFICATE_CALLBACK WOLFSSL_XFREE_NO_NULLNESS_CHECK diff --git a/src/x509_str.c b/src/x509_str.c index 294a5a2eb2..e723126097 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -705,14 +705,26 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) /* We found our issuer in the non-trusted cert list, add it * to the CM and verify the current cert against it */ - #if defined(OPENSSL_ALL) || defined(WOLFSSL_QT) - /* OpenSSL doesn't allow the cert as CA if it is not CA:TRUE for - * intermediate certs. + #ifndef WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE + /* RFC 5280 6.1.3(k): a non-self-issued intermediate must have + * basicConstraints CA:TRUE to be used as a signing authority. + * Reject CA:FALSE intermediates here; the verify_cb (if any) + * may override. Define WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE + * to restore the legacy permissive behavior. */ if (!issuer->isCa) { - /* error depth is current depth + 1 */ + /* error depth is current depth + 1. The compat alias + * X509_V_ERR_INVALID_CA (= 79) lives in wolfssl/openssl/x509.h + * which is not always pulled into this translation unit + * (e.g. some linuxkm build chains). Define a local fallback + * so callers reading X509_STORE_CTX_get_error() see the + * OpenSSL-compatible value. */ + #ifndef X509_V_ERR_INVALID_CA + #define X509_V_ERR_INVALID_CA 79 + #endif SetupStoreCtxError_ex(ctx, X509_V_ERR_INVALID_CA, (ctx->chain) ? (int)(ctx->chain->num + 1) : 1); + #if defined(OPENSSL_ALL) || defined(WOLFSSL_QT) if (ctx->store->verify_cb) { ret = ctx->store->verify_cb(0, ctx); if (ret != WOLFSSL_SUCCESS) { @@ -720,7 +732,9 @@ int wolfSSL_X509_verify_cert(WOLFSSL_X509_STORE_CTX* ctx) goto exit; } } - else { + else + #endif + { ret = WOLFSSL_FAILURE; goto exit; } diff --git a/tests/api.c b/tests/api.c index 05a7688d7f..a8175791f4 100644 --- a/tests/api.c +++ b/tests/api.c @@ -22193,6 +22193,310 @@ static int test_MakeCertWith0Ser(void) return EXPECT_RESULT(); } +#if defined(WOLFSSL_ASN_TEMPLATE) && \ + defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \ + defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \ + defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \ + defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS) + +/* Build a SubjectAltName extension value (a SEQUENCE wrapping a single + * otherName GeneralName) for the Microsoft UPN OID 1.3.6.1.4.1.311.20.2.3 + * with the given 7-byte UTF8String value. */ +static word32 build_otherName_san(byte* out, word32 outSz, const char* val7) +{ + static const byte prefix[] = { + 0x30, 0x19, /* SEQUENCE, 25 */ + 0xA0, 0x17, /* [0] CONSTRUCTED, 23 */ + 0x06, 0x0A, /* OBJECT ID, 10 */ + 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, + 0x14, 0x02, 0x03, /* UPN OID */ + 0xA0, 0x09, /* [0] EXPLICIT, 9 */ + 0x0C, 0x07 /* UTF8String, 7 */ + }; + if (outSz < sizeof(prefix) + 7) + return 0; + XMEMCPY(out, prefix, sizeof(prefix)); + XMEMCPY(out + sizeof(prefix), val7, 7); + return (word32)(sizeof(prefix) + 7); +} + +/* Build a NameConstraints extension value with a single excludedSubtree + * carrying a registeredID GeneralName for OID 1.2.3.4. registeredID is a + * GeneralName form wolfSSL does not enforce, so DecodeSubtree() must + * record it as 'unsupported' and ConfirmNameConstraints() must fail + * closed when the extension is critical (RFC 5280 4.2.1.10). */ +static word32 build_registeredID_nameConstraints(byte* out, word32 outSz) +{ + static const byte ridNc[] = { + 0x30, 0x09, /* SEQUENCE, 9 */ + 0xA1, 0x07, /* [1] excluded, 7 */ + 0x30, 0x05, /* GeneralSubtree, 5 */ + 0x88, 0x03, /* [8] regId, 3 */ + 0x2A, 0x03, 0x04 /* OID 1.2.3.4 */ + }; + if (outSz < sizeof(ridNc)) + return 0; + XMEMCPY(out, ridNc, sizeof(ridNc)); + return (word32)sizeof(ridNc); +} + +/* Build a NameConstraints extension value carrying a single subtree of + * the given list type ([0] permitted or [1] excluded) for an otherName + * UPN whose UTF8 value is the given 7-byte string. */ +static word32 build_otherName_nameConstraints(byte* out, word32 outSz, + int excluded, const char* val7) +{ + static const byte common[] = { + 0x30, 0x1D, /* SEQUENCE, 29 */ + 0x00, 0x1B, /* listTag, 27 (patched) */ + 0x30, 0x19, /* GeneralSubtree, 25 */ + 0xA0, 0x17, /* [0] CONSTRUCTED, 23 */ + 0x06, 0x0A, + 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, + 0x14, 0x02, 0x03, + 0xA0, 0x09, + 0x0C, 0x07 + }; + if (outSz < sizeof(common) + 7) + return 0; + XMEMCPY(out, common, sizeof(common)); + out[2] = excluded ? 0xA1 : 0xA0; /* listTag */ + XMEMCPY(out + sizeof(common), val7, 7); + return (word32)(sizeof(common) + 7); +} + +/* Build a chain (root -> intermediate -> leaf) where the intermediate + * carries `nameConstraintsDer` as a (possibly critical) nameConstraints + * extension and the leaf carries `sanDer` as its SAN. Loads root and + * intermediate as trusted CAs into a fresh CertManager, parses the leaf + * with VERIFY, and returns the result code from wc_ParseCert(). */ +static int verify_with_otherName_chain(const byte* nameConstraintsDer, + word32 nameConstraintsDerSz, int critical, + const byte* sanDer, word32 sanDerSz) +{ + Cert cert; + DecodedCert decodedCert; + byte rootDer[FOURK_BUF]; + byte icaDer[FOURK_BUF]; + byte leafDer[FOURK_BUF]; + int rootDerSz = 0, icaDerSz = 0, leafDerSz = 0; + int parseRet = -1; + WC_RNG rng; + ecc_key rootKey, icaKey, leafKey; + WOLFSSL_CERT_MANAGER* cm = NULL; + + XMEMSET(&rng, 0, sizeof(rng)); + XMEMSET(&rootKey, 0, sizeof(rootKey)); + XMEMSET(&icaKey, 0, sizeof(icaKey)); + XMEMSET(&leafKey, 0, sizeof(leafKey)); + + if (wc_InitRng(&rng) != 0) goto done; + if (wc_ecc_init(&rootKey) != 0) goto done; + if (wc_ecc_init(&icaKey) != 0) goto done; + if (wc_ecc_init(&leafKey) != 0) goto done; + if (wc_ecc_make_key(&rng, 32, &rootKey) != 0) goto done; + if (wc_ecc_make_key(&rng, 32, &icaKey) != 0) goto done; + if (wc_ecc_make_key(&rng, 32, &leafKey) != 0) goto done; + + /* Self-signed root. */ + if (wc_InitCert(&cert) != 0) goto done; + (void)XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.org, "OtherNCRoot", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.commonName, "OtherNCRoot", CTC_NAME_SIZE); + cert.selfSigned = 1; + cert.isCA = 1; + cert.sigType = CTC_SHA256wECDSA; + cert.keyUsage = KEYUSE_KEY_CERT_SIGN | KEYUSE_CRL_SIGN; + if (wc_SetSubjectKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &rootKey) != 0) + goto done; + if (wc_MakeCert(&cert, rootDer, FOURK_BUF, NULL, &rootKey, &rng) < 0) + goto done; + rootDerSz = wc_SignCert(cert.bodySz, cert.sigType, rootDer, FOURK_BUF, + NULL, &rootKey, &rng); + if (rootDerSz < 0) goto done; + + /* Intermediate, signed by root, carrying nameConstraints. */ + if (wc_InitCert(&cert) != 0) goto done; + cert.selfSigned = 0; + cert.isCA = 1; + cert.sigType = CTC_SHA256wECDSA; + cert.keyUsage = KEYUSE_KEY_CERT_SIGN | KEYUSE_CRL_SIGN; + (void)XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.org, "OtherNCICA", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.commonName, "OtherNCICA", CTC_NAME_SIZE); + if (wc_SetIssuerBuffer(&cert, rootDer, rootDerSz) != 0) goto done; + if (wc_SetAuthKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &rootKey) != 0) + goto done; + if (wc_SetSubjectKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &icaKey) != 0) + goto done; + if (nameConstraintsDer != NULL) { + /* nameConstraints OID = 2.5.29.30 */ + if (wc_SetCustomExtension(&cert, critical ? 1 : 0, "2.5.29.30", + nameConstraintsDer, nameConstraintsDerSz) != 0) + goto done; + } + if (wc_MakeCert(&cert, icaDer, FOURK_BUF, NULL, &icaKey, &rng) < 0) + goto done; + icaDerSz = wc_SignCert(cert.bodySz, cert.sigType, icaDer, FOURK_BUF, + NULL, &rootKey, &rng); + if (icaDerSz < 0) goto done; + + /* Leaf, signed by intermediate, carrying the otherName SAN. */ + if (wc_InitCert(&cert) != 0) goto done; + cert.selfSigned = 0; + cert.isCA = 0; + cert.sigType = CTC_SHA256wECDSA; + (void)XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.org, "OtherNCLeaf", CTC_NAME_SIZE); + (void)XSTRNCPY(cert.subject.commonName, "OtherNCLeaf", CTC_NAME_SIZE); + if (wc_SetIssuerBuffer(&cert, icaDer, icaDerSz) != 0) goto done; + if (wc_SetAuthKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &icaKey) != 0) + goto done; + if (wc_SetSubjectKeyIdFromPublicKey_ex(&cert, ECC_TYPE, &leafKey) != 0) + goto done; + if (sanDer != NULL && sanDerSz > 0) { + if (sanDerSz > sizeof(cert.altNames)) goto done; + XMEMCPY(cert.altNames, sanDer, sanDerSz); + cert.altNamesSz = (int)sanDerSz; + } + if (wc_MakeCert(&cert, leafDer, FOURK_BUF, NULL, &leafKey, &rng) < 0) + goto done; + leafDerSz = wc_SignCert(cert.bodySz, cert.sigType, leafDer, FOURK_BUF, + NULL, &icaKey, &rng); + if (leafDerSz < 0) goto done; + + cm = wolfSSL_CertManagerNew(); + if (cm == NULL) goto done; + if (wolfSSL_CertManagerLoadCABuffer(cm, rootDer, rootDerSz, + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) goto done; + if (wolfSSL_CertManagerLoadCABuffer(cm, icaDer, icaDerSz, + WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS) goto done; + + wc_InitDecodedCert(&decodedCert, leafDer, (word32)leafDerSz, NULL); + parseRet = wc_ParseCert(&decodedCert, CERT_TYPE, VERIFY, cm); + wc_FreeDecodedCert(&decodedCert); + +done: + if (cm != NULL) wolfSSL_CertManagerFree(cm); + wc_ecc_free(&leafKey); + wc_ecc_free(&icaKey); + wc_ecc_free(&rootKey); + wc_FreeRng(&rng); + return parseRet; +} +#endif + +/* Verifies wolfSSL enforces an issuing CA's nameConstraints extension on a + * leaf certificate's otherName SAN (RFC 5280 4.2.1.10). The vulnerability + * was that ConfirmNameConstraints() ignored ASN_OTHER_TYPE entirely, so a + * malicious intermediate could issue leaves whose otherName SAN violated + * its own subtree. + * + * Coverage: + * 1. Critical excluded subtree, leaf SAN matches -> reject + * 2. Critical excluded subtree, leaf SAN does NOT match -> accept + * (positive control: distinguishes 'right rule fired' from + * 'broke everything with otherName') + * 3. Non-critical excluded subtree, leaf SAN matches -> reject + * (excluded is enforced regardless of criticality) + * 4. Critical permitted subtree, leaf SAN matches -> accept + * 5. Critical permitted subtree, leaf SAN does NOT match -> reject + * 6. Critical nameConstraints carrying an unsupported form + * (registeredID), leaf has no relevant SAN -> reject + * (RFC 5280 4.2.1.10 fail-closed for unprocessed forms) + * 7. Same as (6) but non-critical -> accept + */ +static int test_NameConstraints_OtherName(void) +{ + EXPECT_DECLS; +#if defined(WOLFSSL_ASN_TEMPLATE) && \ + defined(WOLFSSL_CERT_REQ) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(HAVE_ECC) && \ + defined(WOLFSSL_CERT_EXT) && !defined(NO_CERTS) && \ + defined(WOLFSSL_ALT_NAMES) && defined(WOLFSSL_CUSTOM_OID) && \ + defined(HAVE_OID_ENCODING) && !defined(IGNORE_NAME_CONSTRAINTS) + byte sanBlocked[64]; + byte sanAllowed[64]; + byte ncExcludedBlocked[64]; + byte ncPermittedAllowed[64]; + byte ncRegisteredID[16]; + word32 sanBlockedSz, sanAllowedSz; + word32 ncExcludedBlockedSz, ncPermittedAllowedSz, ncRegisteredIDSz; + + sanBlockedSz = + build_otherName_san(sanBlocked, sizeof(sanBlocked), "blocked"); + sanAllowedSz = + build_otherName_san(sanAllowed, sizeof(sanAllowed), "allowed"); + ncExcludedBlockedSz = build_otherName_nameConstraints( + ncExcludedBlocked, sizeof(ncExcludedBlocked), 1, "blocked"); + ncPermittedAllowedSz = build_otherName_nameConstraints( + ncPermittedAllowed, sizeof(ncPermittedAllowed), 0, "allowed"); + ncRegisteredIDSz = build_registeredID_nameConstraints( + ncRegisteredID, sizeof(ncRegisteredID)); + ExpectIntGT((int)sanBlockedSz, 0); + ExpectIntGT((int)sanAllowedSz, 0); + ExpectIntGT((int)ncExcludedBlockedSz, 0); + ExpectIntGT((int)ncPermittedAllowedSz, 0); + ExpectIntGT((int)ncRegisteredIDSz, 0); + + /* (1) Original bypass scenario: critical excluded otherName matches + * the leaf's otherName SAN. Must be rejected. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 1, + sanBlocked, sanBlockedSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (2) Positive control: same critical excluded subtree, but the leaf + * carries a DIFFERENT otherName value, so byte-comparison says no + * match and the chain MUST verify. This pins the rejection in (1) + * to the matching path rather than to a blanket 'reject any + * otherName under critical'. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 1, + sanAllowed, sanAllowedSz), + 0); + + /* (3) Non-critical excluded subtree, leaf SAN matches: exclusion is + * enforced regardless of criticality. */ + ExpectIntEQ(verify_with_otherName_chain( + ncExcludedBlocked, ncExcludedBlockedSz, 0, + sanBlocked, sanBlockedSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (4) Critical permitted subtree, leaf SAN inside the permitted set: + * verification succeeds. */ + ExpectIntEQ(verify_with_otherName_chain( + ncPermittedAllowed, ncPermittedAllowedSz, 1, + sanAllowed, sanAllowedSz), + 0); + + /* (5) Critical permitted subtree, leaf SAN outside the permitted set: + * verification rejects. */ + ExpectIntEQ(verify_with_otherName_chain( + ncPermittedAllowed, ncPermittedAllowedSz, 1, + sanBlocked, sanBlockedSz), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (6) Critical nameConstraints carrying a GeneralName form wolfSSL + * does not enforce (registeredID). RFC 5280 4.2.1.10 requires the + * verifier to either process the constraint or reject; we reject + * fail-closed. The leaf needs no SAN to exercise this path. */ + ExpectIntEQ(verify_with_otherName_chain( + ncRegisteredID, ncRegisteredIDSz, 1, NULL, 0), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + /* (7) Same as (6) but non-critical: RFC 5280 only mandates the + * fail-closed reject when the extension is critical, so a + * non-critical unsupported constraint form is silently ignored + * and verification succeeds. */ + ExpectIntEQ(verify_with_otherName_chain( + ncRegisteredID, ncRegisteredIDSz, 0, NULL, 0), + 0); +#endif + return EXPECT_RESULT(); +} + static int test_MakeCertWithCaFalse(void) { EXPECT_DECLS; @@ -37012,6 +37316,7 @@ TEST_CASE testCases[] = { TEST_DECL(test_PathLenSelfIssued), TEST_DECL(test_PathLenSelfIssuedAllowed), TEST_DECL(test_PathLenNoKeyUsage), + TEST_DECL(test_NameConstraints_OtherName), TEST_DECL(test_MakeCertWith0Ser), TEST_DECL(test_MakeCertWithCaFalse), #ifdef WOLFSSL_CERT_SIGN_CB diff --git a/tests/api/test_ossl_x509_str.c b/tests/api/test_ossl_x509_str.c index b0756f8bbb..30612156c7 100644 --- a/tests/api/test_ossl_x509_str.c +++ b/tests/api/test_ossl_x509_str.c @@ -1144,7 +1144,8 @@ int test_X509_STORE_untrusted(void) return EXPECT_RESULT(); } -#if defined(OPENSSL_ALL) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) +#if defined(OPENSSL_ALL) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE) static int last_errcode; static int last_errdepth; @@ -1165,7 +1166,8 @@ static int X509Callback(int ok, X509_STORE_CTX *ctx) int test_X509_STORE_InvalidCa(void) { EXPECT_DECLS; -#if defined(OPENSSL_ALL) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) +#if defined(OPENSSL_ALL) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE) const char* filename = "./certs/intermediate/ca_false_intermediate/" "test_int_not_cacert.pem"; const char* srvfile = "./certs/intermediate/ca_false_intermediate/" @@ -1221,7 +1223,8 @@ int test_X509_STORE_InvalidCa(void) int test_X509_STORE_InvalidCa_NoCallback(void) { EXPECT_DECLS; -#if defined(OPENSSL_ALL) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) +#if defined(OPENSSL_EXTRA) && !defined(NO_RSA) && !defined(NO_FILESYSTEM) && \ + !defined(WOLFSSL_X509_STORE_ALLOW_NON_CA_INTERMEDIATE) const char* filename = "./certs/intermediate/ca_false_intermediate/" "test_int_not_cacert.pem"; const char* srvfile = "./certs/intermediate/ca_false_intermediate/" diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 9a3be56616..631a3b8255 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -4375,7 +4375,8 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert); static int DecodeCrlDist(const byte* input, word32 sz, DecodedCert* cert); static int DecodeAuthInfo(const byte* input, word32 sz, DecodedCert* cert); #ifndef IGNORE_NAME_CONSTRAINTS -static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, word32 limit, void* heap); +static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, + word32 limit, byte* hasUnsupported, void* heap); static int DecodeNameConstraints(const byte* input, word32 sz, DecodedCert* cert); #endif #if defined(WOLFSSL_SEP) || defined(WOLFSSL_CERT_EXT) @@ -12138,6 +12139,8 @@ void FreeDecodedCert(DecodedCert* cert) FreeAltNames(cert->altEmailNames, cert->heap); if (cert->altDirNames) FreeAltNames(cert->altDirNames, cert->heap); + if (cert->altOtherNamesRaw) + FreeAltNames(cert->altOtherNamesRaw, cert->heap); if (cert->permittedNames) FreeNameSubtrees(cert->permittedNames, cert->heap); if (cert->excludedNames) @@ -17621,6 +17624,19 @@ int wolfssl_local_MatchIpSubnet(const byte* ip, int ipSz, return match; } +/* RFC 5280 4.2.1.10: otherName matching is byte-exact comparison of the + * full OtherName encoding (OID || [0] EXPLICIT value). Both the leaf SAN + * (cert->altOtherNamesRaw) and the constraint subtree (Base_entry from + * DecodeSubtree) store the same form, so a memcmp suffices. */ +static int MatchOtherNameConstraint(DNS_entry* name, Base_entry* current) +{ + if (name == NULL || current == NULL) + return 0; + if (name->len != current->nameSz) + return 0; + return XMEMCMP(name->name, current->name, (size_t)current->nameSz) == 0; +} + /* Search through the list to find if the name is permitted. * name The DNS name to search for * dnsList The list to search through @@ -17654,6 +17670,12 @@ static int PermittedListOk(DNS_entry* name, Base_entry* dnsList, byte nameType) break; } } + else if (nameType == ASN_OTHER_TYPE) { + if (MatchOtherNameConstraint(name, current)) { + match = 1; + break; + } + } else if (name->len >= current->nameSz && wolfssl_local_MatchBaseName(nameType, name->name, name->len, current->name, current->nameSz)) { @@ -17701,6 +17723,12 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType) break; } } + else if (nameType == ASN_OTHER_TYPE) { + if (MatchOtherNameConstraint(name, current)) { + ret = 1; + break; + } + } else if (name->len >= current->nameSz && wolfssl_local_MatchBaseName(nameType, name->name, name->len, current->name, current->nameSz)) { @@ -17718,13 +17746,14 @@ static int IsInExcludedList(DNS_entry* name, Base_entry* dnsList, byte nameType) static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) { const byte nameTypes[] = {ASN_RFC822_TYPE, ASN_DNS_TYPE, ASN_DIR_TYPE, - ASN_IP_TYPE, ASN_URI_TYPE}; + ASN_IP_TYPE, ASN_URI_TYPE, ASN_OTHER_TYPE}; int i; if (signer == NULL || cert == NULL) return 0; - if (signer->excludedNames == NULL && signer->permittedNames == NULL) + if (signer->excludedNames == NULL && signer->permittedNames == NULL && + !signer->extNameConstraintHasUnsupported) return 1; for (i=0; i < (int)sizeof(nameTypes); i++) { @@ -17789,10 +17818,15 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) case ASN_URI_TYPE: name = cert->altNames; break; + case ASN_OTHER_TYPE: + /* otherName SAN entries are stored on cert->altOtherNamesRaw + * (kept separate from altNames so the public altNames view + * is unaffected). Each entry holds the raw OtherName + * encoding (OID || [0] EXPLICIT value) and is byte-matched + * against the issuing CA's subtree. */ + name = cert->altOtherNamesRaw; + break; default: - /* Other types of names are ignored for now. - * Shouldn't it be rejected if it there is a altNamesByType[nameType] - * and signer->extNameConstraintCrit is set? */ return 0; } @@ -17833,6 +17867,19 @@ static int ConfirmNameConstraints(Signer* signer, DecodedCert* cert) } } + /* RFC 5280 4.2.1.10: "If a name constraints extension that is marked as + * critical imposes constraints on a particular name form ... the + * application MUST either process the constraint or reject the + * certificate." otherName is processed by byte-comparison above; any + * remaining unsupported forms (registeredID, x400Address, ediPartyName) + * trigger the fail-closed reject below. */ + if (signer->extNameConstraintCrit && + signer->extNameConstraintHasUnsupported) { + WOLFSSL_MSG("Critical nameConstraints contains unsupported " + "GeneralName form; rejecting"); + return 0; + } + return 1; } @@ -18136,10 +18183,36 @@ static int DecodeGeneralName(const byte* input, word32* inOutIdx, byte tag, } #endif /* WOLFSSL_RID_ALT_NAME */ #endif /* IGNORE_NAME_CONSTRAINTS */ -#if defined(WOLFSSL_SEP) || defined(WOLFSSL_FPKI) - /* GeneralName choice: otherName */ +#ifndef IGNORE_NAME_CONSTRAINTS + /* GeneralName choice: otherName. + * Store the raw OtherName encoding (OID || [0] EXPLICIT value) on a + * dedicated internal list so ConfirmNameConstraints() can byte-match + * it against the issuing CA's nameConstraints subtree (RFC 5280 + * 4.2.1.10). The raw form is kept separate from cert->altNames so + * the public altNames view (used by OpenSSL-compat APIs) reflects + * exactly what the SAN extension carries. */ + else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | ASN_OTHER_TYPE)) { + ret = SetDNSEntry(cert->heap, (const char*)(input + idx), len, + ASN_OTHER_TYPE, &cert->altOtherNamesRaw); + if (ret != 0) { + return ret; + } + #if defined(WOLFSSL_SEP) || defined(WOLFSSL_FPKI) + /* FPKI/SEP also OID-decode the otherName into a separate altNames + * entry that holds the parsed UPN/FASCN value (with oidSum != 0). + * That parsed entry is consumed by wc_GetUUIDFromCert / + * wc_GetFASCNFromCert; ConfirmNameConstraints() does not look at + * it - it iterates altOtherNamesRaw instead. */ + ret = DecodeOtherName(cert, input, &idx, len); + #else + idx += (word32)len; + #endif + } +#elif defined(WOLFSSL_SEP) || defined(WOLFSSL_FPKI) + /* No name constraints support in the build, but FPKI/SEP still need + * the parsed otherName entry for wc_GetUUIDFromCert / + * wc_GetFASCNFromCert. */ else if (tag == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | ASN_OTHER_TYPE)) { - /* TODO: test data for code path */ ret = DecodeOtherName(cert, input, &idx, len); } #endif @@ -19249,8 +19322,14 @@ static int DecodeSubtreeGeneralName(const byte* input, word32 sz, byte tag, (void)heap; - /* if constructed has leading sequence */ - if ((tag & ASN_CONSTRUCTED) == ASN_CONSTRUCTED) { + /* directoryName is encoded as [4] CONSTRUCTED { Name } where Name is a + * SEQUENCE - strip the inner SEQUENCE header. + * otherName is encoded as [0] CONSTRUCTED { OID, [0] EXPLICIT value } + * where the inner content is NOT a SEQUENCE; keep the bytes as-is so + * we can byte-match a leaf SAN otherName against the constraint. + */ + if ((tag & ASN_CONSTRUCTED) == ASN_CONSTRUCTED && + (tag & ASN_TYPE_MASK) != ASN_OTHER_TYPE) { ret = GetASN_Sequence(input, &nameIdx, &strLen, sz, 0); if (ret < 0) { ret = ASN_PARSE_E; @@ -19309,8 +19388,17 @@ static int DecodeSubtreeGeneralName(const byte* input, word32 sz, byte tag, * @return ASN_PARSE_E when SEQUENCE is not found as expected. */ #ifdef WOLFSSL_ASN_TEMPLATE +/* Decode a sub-tree of name constraints. + * + * @param [out] hasUnsupported Set to 1 when an entry with a GeneralName + * form we cannot fully enforce was + * encountered. Drives the RFC 5280 4.2.1.10 + * fail-closed requirement for critical + * nameConstraints extensions; must not be + * NULL. + */ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, - word32 limit, void* heap) + word32 limit, byte* hasUnsupported, void* heap) { DECL_ASNGETDATA(dataASN, subTreeASN_Length); word32 idx = 0; @@ -19352,13 +19440,21 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, t == (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) || t == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | ASN_DIR_TYPE) || t == (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE) || - t == (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE)) { + t == (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE) || + t == (ASN_CONTEXT_SPECIFIC | ASN_CONSTRUCTED | + ASN_OTHER_TYPE)) { /* Parse the general name and store a new entry. */ ret = DecodeSubtreeGeneralName(input + GetASNItem_DataIdx(dataASN[SUBTREEASN_IDX_BASE], input), dataASN[SUBTREEASN_IDX_BASE].length, t, head, heap); } - /* Skip entry. */ + else { + /* GeneralName form (e.g. registeredID, x400Address, + * ediPartyName) we do not enforce. Record so the caller can + * fail-closed when the nameConstraints extension is critical + * (RFC 5280 4.2.1.10). */ + *hasUnsupported = 1; + } } } @@ -19406,6 +19502,7 @@ static int DecodeNameConstraints(const byte* input, word32 sz, DECL_ASNGETDATA(dataASN, nameConstraintsASN_Length); word32 idx = 0; int ret = 0; + byte hasUnsupported = 0; CALLOC_ASNGETDATA(dataASN, nameConstraintsASN_Length, ret, cert->heap); @@ -19421,7 +19518,7 @@ static int DecodeNameConstraints(const byte* input, word32 sz, dataASN[NAMECONSTRAINTSASN_IDX_PERMIT].data.ref.data, dataASN[NAMECONSTRAINTSASN_IDX_PERMIT].data.ref.length, &cert->permittedNames, WOLFSSL_MAX_NAME_CONSTRAINTS, - cert->heap); + &hasUnsupported, cert->heap); } } if (ret == 0) { @@ -19431,10 +19528,14 @@ static int DecodeNameConstraints(const byte* input, word32 sz, dataASN[NAMECONSTRAINTSASN_IDX_EXCLUDE].data.ref.data, dataASN[NAMECONSTRAINTSASN_IDX_EXCLUDE].data.ref.length, &cert->excludedNames, WOLFSSL_MAX_NAME_CONSTRAINTS, - cert->heap); + &hasUnsupported, cert->heap); } } + if (ret == 0 && hasUnsupported) { + cert->extNameConstraintHasUnsupported = 1; + } + FREE_ASNGETDATA(dataASN, cert->heap); return ret; @@ -22878,6 +22979,9 @@ int FillSigner(Signer* signer, DecodedCert* cert, int type, DerBuffer *der) #ifndef IGNORE_NAME_CONSTRAINTS signer->permittedNames = cert->permittedNames; signer->excludedNames = cert->excludedNames; + signer->extNameConstraintCrit = cert->extNameConstraintCrit; + signer->extNameConstraintHasUnsupported = + cert->extNameConstraintHasUnsupported; #endif #ifndef NO_SKID XMEMCPY(signer->subjectKeyIdHash, cert->extSubjKeyId, diff --git a/wolfcrypt/src/asn_orig.c b/wolfcrypt/src/asn_orig.c index d6568aa5d1..505c803495 100644 --- a/wolfcrypt/src/asn_orig.c +++ b/wolfcrypt/src/asn_orig.c @@ -3577,6 +3577,36 @@ static int DecodeAltNames(const byte* input, word32 sz, DecodedCert* cert) } length -= (int)(((word32)strLen + idx - lenStartIdx)); + #ifndef IGNORE_NAME_CONSTRAINTS + /* Store the raw OtherName encoding (OID || [0] EXPLICIT value) + * on the dedicated altOtherNamesRaw list so + * ConfirmNameConstraints() can byte-match it against the + * issuing CA's subtree (RFC 5280 4.2.1.10). Kept separate from + * altNames so OpenSSL-compat APIs see exactly what the SAN + * extension carries. */ + { + DNS_entry* rawEntry = AltNameNew(cert->heap); + if (rawEntry == NULL) { + WOLFSSL_MSG("\tOut of Memory"); + return MEMORY_E; + } + rawEntry->type = ASN_OTHER_TYPE; + rawEntry->len = strLen; + rawEntry->name = (char*)XMALLOC((size_t)strLen + 1, + cert->heap, DYNAMIC_TYPE_ALTNAME); + if (rawEntry->name == NULL) { + XFREE(rawEntry, cert->heap, DYNAMIC_TYPE_ALTNAME); + return MEMORY_E; + } + rawEntry->nameStored = 1; + XMEMCPY((void*)(wc_ptr_t)rawEntry->name, &input[idx], + (size_t)strLen); + ((char*)(wc_ptr_t)rawEntry->name)[strLen] = '\0'; + rawEntry->next = cert->altOtherNamesRaw; + cert->altOtherNamesRaw = rawEntry; + } + #endif /* IGNORE_NAME_CONSTRAINTS */ + if (GetObjectId(input, &idx, &oid, oidCertAltNameType, sz) < 0) { WOLFSSL_MSG("\tbad OID"); return ASN_PARSE_E; @@ -4011,8 +4041,10 @@ int DecodeExtKeyUsage(const byte* input, word32 sz, } #ifndef IGNORE_NAME_CONSTRAINTS +/* See doc on the WOLFSSL_ASN_TEMPLATE definition in asn.c. hasUnsupported + * must not be NULL. */ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, - word32 limit, void* heap) + word32 limit, byte* hasUnsupported, void* heap) { word32 idx = 0; int ret = 0; @@ -4056,11 +4088,15 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, if (bType == ASN_DNS_TYPE || bType == ASN_RFC822_TYPE || bType == ASN_DIR_TYPE || bType == ASN_IP_TYPE || - bType == ASN_URI_TYPE) { + bType == ASN_URI_TYPE || bType == ASN_OTHER_TYPE) { Base_entry* entry; - /* if constructed has leading sequence */ - if (b & ASN_CONSTRUCTED) { + /* directoryName is encoded as [4] CONSTRUCTED { Name } where + * Name is a SEQUENCE; strip the inner SEQUENCE header. + * otherName is encoded as [0] CONSTRUCTED { OID, [0] EXPLICIT + * value }; the inner content is NOT a SEQUENCE so keep it + * as-is (matches the WOLFSSL_ASN_TEMPLATE path). */ + if ((b & ASN_CONSTRUCTED) && bType != ASN_OTHER_TYPE) { if (GetSequence(input, &nameIdx, &strLength, sz) < 0) { WOLFSSL_MSG("\tfail: constructed be a SEQUENCE"); return ASN_PARSE_E; @@ -4090,6 +4126,13 @@ static int DecodeSubtree(const byte* input, word32 sz, Base_entry** head, entry->next = *head; *head = entry; } + else { + /* GeneralName form (e.g. registeredID, x400Address, + * ediPartyName) we do not enforce. Record so the caller can + * fail-closed when the nameConstraints extension is critical + * (RFC 5280 4.2.1.10). */ + *hasUnsupported = 1; + } idx += (word32)seqLength; } @@ -4102,6 +4145,7 @@ static int DecodeNameConstraints(const byte* input, word32 sz, { word32 idx = 0; int length = 0; + byte hasUnsupported = 0; WOLFSSL_ENTER("DecodeNameConstraints"); @@ -4129,7 +4173,8 @@ static int DecodeNameConstraints(const byte* input, word32 sz, } if (DecodeSubtree(input + idx, (word32)length, subtree, - WOLFSSL_MAX_NAME_CONSTRAINTS, cert->heap) < 0) { + WOLFSSL_MAX_NAME_CONSTRAINTS, &hasUnsupported, + cert->heap) < 0) { WOLFSSL_MSG("\terror parsing subtree"); return ASN_PARSE_E; } @@ -4137,6 +4182,9 @@ static int DecodeNameConstraints(const byte* input, word32 sz, idx += (word32)length; } + if (hasUnsupported) + cert->extNameConstraintHasUnsupported = 1; + return 0; } diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index b3028c9c99..2af93bfd90 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -1760,6 +1760,13 @@ struct DecodedCert { #ifndef IGNORE_NAME_CONSTRAINTS DNS_entry* altEmailNames; /* alt names list of RFC822 entries */ DNS_entry* altDirNames; /* alt names list of DIR entries */ + /* Raw OtherName GeneralName encodings (OID || [0] EXPLICIT value) + * for any otherName SAN seen on this certificate. Used internally by + * ConfirmNameConstraints() for byte-exact matching against the + * issuing CA's nameConstraints subtrees (RFC 5280 4.2.1.10). Kept + * separate from altNames so OpenSSL-compat APIs that iterate + * altNames see exactly the entries the SAN extension carries. */ + DNS_entry* altOtherNamesRaw; Base_entry* permittedNames; /* Permitted name bases */ Base_entry* excludedNames; /* Excluded name bases */ #endif /* IGNORE_NAME_CONSTRAINTS */ @@ -2062,7 +2069,23 @@ struct DecodedCert { WC_BITFIELD extSubjAltNameCrit:1; WC_BITFIELD extAuthKeyIdCrit:1; #ifndef IGNORE_NAME_CONSTRAINTS + /*! + * \brief Set when the certificate's nameConstraints extension was + * present and marked critical. + */ WC_BITFIELD extNameConstraintCrit:1; + /*! + * \brief Set when decoding the nameConstraints extension encountered + * at least one permittedSubtrees or excludedSubtrees entry whose + * GeneralName form (e.g. registeredID, x400Address, + * ediPartyName) wolfSSL does not enforce. + * + * During verification, ConfirmNameConstraints() implements the RFC + * 5280 4.2.1.10 fail-closed requirement: when both this flag and + * extNameConstraintCrit are set, the chain is rejected rather than + * the unsupported constraint form being silently ignored. + */ + WC_BITFIELD extNameConstraintHasUnsupported:1; #endif WC_BITFIELD extSubjKeyIdCrit:1; WC_BITFIELD extKeyUsageCrit:1; @@ -2130,6 +2153,25 @@ struct Signer { byte extKeyUsage; word16 maxPathLen; WC_BITFIELD selfSigned:1; +#ifndef IGNORE_NAME_CONSTRAINTS + /*! + * \brief Mirrors DecodedCert::extNameConstraintCrit and + * DecodedCert::extNameConstraintHasUnsupported so the + * nameConstraints state survives onto the CA Signer and is + * available during chain verification. + * + * ConfirmNameConstraints() uses these flags to implement the RFC 5280 + * 4.2.1.10 fail-closed requirement: when extNameConstraintCrit is set + * and extNameConstraintHasUnsupported is also set, verification fails + * rather than the unsupported constraint form being silently ignored. + * + * Co-located with selfSigned to share its bitfield storage word and + * avoid growing sizeof(Signer), which is load-bearing for + * PERSIST_CERT_CACHE. + */ + WC_BITFIELD extNameConstraintCrit:1; + WC_BITFIELD extNameConstraintHasUnsupported:1; +#endif const byte* publicKey; int nameLen; const char*