From d971d14dfd3ab6a93726d27572233bdbf0d79a53 Mon Sep 17 00:00:00 2001 From: Sameeh Date: Fri, 23 Oct 2026 08:17:17 +0300 Subject: [PATCH 1/5] Check CA ExtKeyUsage for issuers from the CertManager The CA Extended Key Usage check in ProcessPeerCerts() ran only for CA certificates in the peer's Certificate message. When the peer sent just its leaf, the issuing CA came from the CertManager and its Extended Key Usage was not looked at, so the result depended on whether the peer included the intermediate. Record in the Signer whether the certificate had an Extended Key Usage extension, and after the leaf checks walk every CA from the leaf's issuer up to the self-signed trust anchor, applying the TLS purpose to each. The walk uses the same AKID-or-name lookup as the name constraints check, which is exported as wolfssl_local_FindSignerByAkidOrName(). Trusted peer certificates are not checked, as their chain is not used. Extend test_wolfSSL_chain_ca_ext_key_usage with leaf-only cases at depth one and two for server and client authentication, verify callback handling and a self-signed trust anchor. --- src/internal.c | 62 +++++++++++++++++++++++ tests/api/test_ssl_cert.c | 104 +++++++++++++++++++++++++++----------- wolfcrypt/src/asn.c | 8 +-- wolfssl/wolfcrypt/asn.h | 5 ++ 4 files changed, 147 insertions(+), 32 deletions(-) diff --git a/src/internal.c b/src/internal.c index 644cb33ca9e..dc2a38f1f96 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18134,6 +18134,45 @@ static int CheckChainCAExtKeyUsage(const WOLFSSL* ssl, const DecodedCert* cert) return 0; } + +/* Check the Extended Key Usage of each CA from ca up to the self-signed trust + * anchor against the TLS purpose. With IGNORE_NAME_CONSTRAINTS there is no + * issuer lookup, so only ca is checked. + * Returns 0, EXTKEYUSE_AUTH_E or MAX_CHAIN_ERROR. */ +static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca) +{ + byte purpose; + Signer* prev = NULL; + Signer* parent; + int depth = 0; + + if (ssl->options.side == WOLFSSL_CLIENT_END) + purpose = EXTKEYUSE_SERVER_AUTH; + else + purpose = EXTKEYUSE_CLIENT_AUTH; + + while (ca != NULL && !ca->selfSigned) { + if (ca->extExtKeyUsageSet && + (ca->extKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0) { + WOLFSSL_MSG("Issuer CA ExtKeyUse doesn't allow TLS peer auth"); + return EXTKEYUSE_AUTH_E; + } + #ifndef IGNORE_NAME_CONSTRAINTS + parent = wolfssl_local_FindSignerByAkidOrName(SSL_CM(ssl), NULL, ca); + #else + parent = NULL; + #endif + /* Stop on missing parent, self-loop, or A->B->A cycle. */ + if (parent == NULL || parent == ca || parent == prev) + break; + if (++depth >= WOLFSSL_MAX_CHAIN_DEPTH) + return MAX_CHAIN_ERROR; + prev = ca; + ca = parent; + } + + return 0; +} #endif /* IGNORE_KEY_EXTENSIONS */ #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) @@ -20430,6 +20469,29 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } } } + + /* CAs the peer did not send were not covered by + * CheckChainCAExtKeyUsage(). */ + if (ret == 0 && !ssl->options.verifyNone && + #ifdef WOLFSSL_TRUST_PEER_CERT + !args->haveTrustPeer && + #endif + !UsingChainVerifyCb(ssl)) { + ret = CheckSignerExtKeyUsage(ssl, args->dCert->ca); + if (ret != 0) { + WOLFSSL_ERROR_VERBOSE(ret); + #if defined(OPENSSL_EXTRA) || \ + defined(OPENSSL_EXTRA_X509_SMALL) + /* Return first cert error here */ + if (ssl->peerVerifyRet == 0) { + ssl->peerVerifyRet = + (ret == WC_NO_ERR_TRACE(MAX_CHAIN_ERROR)) ? + WOLFSSL_X509_V_ERR_CERT_CHAIN_TOO_LONG : + WOLFSSL_X509_V_ERR_INVALID_PURPOSE; + } + #endif + } + } #endif /* IGNORE_KEY_EXTENSIONS */ if (args->fatal) { diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index e07fa99e356..25f1d75d3a6 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2623,6 +2623,8 @@ typedef struct test_eku_case { int expectRet; /* expected handshake result */ const char* keyUsage; /* Key Usage on the CA under test, NULL uses * keyCertSign,cRLSign */ + int leafOnly; /* peer sends only the leaf; with extraLevel + * the middle CA is also loaded to verify */ } test_eku_case; /* Stand in for an application that knowingly accepts a rejected certificate. */ @@ -2756,7 +2758,8 @@ static int test_eku_gen_leaf(byte* out, int outMax, RsaKey* leafKey, * through hsRet and verifyRet. */ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, const byte* rootDer, int rootSz, const byte* pinnedDer, int pinnedSz, - const test_eku_case* tc, int* hsRet, long* verifyRet) + const byte* pinned2Der, int pinned2Sz, const test_eku_case* tc, + int* hsRet, long* verifyRet) { EXPECT_DECLS; WOLFSSL_CTX* ctx_c = NULL; @@ -2812,6 +2815,10 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, ExpectIntEQ(wolfSSL_CTX_load_verify_buffer(verifyCtx, pinnedDer, (long)pinnedSz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); } + if (pinned2Der != NULL) { + ExpectIntEQ(wolfSSL_CTX_load_verify_buffer(verifyCtx, pinned2Der, + (long)pinned2Sz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + } /* The harness only reports pass or fail, so read the reason from the side * that did the verifying. */ @@ -2882,18 +2889,21 @@ static int test_eku_chain_case(const test_eku_fixture* f, if (EXPECT_SUCCESS()) { chainSz = leafSz; - if (tc->extraLevel) { + if (tc->extraLevel && !tc->leafOnly) { XMEMCPY(f->chainDer + chainSz, f->midDer, (size_t)midSz); chainSz += midSz; } - XMEMCPY(f->chainDer + chainSz, f->interDer, (size_t)caSz); - chainSz += caSz; + if (!tc->leafOnly) { + XMEMCPY(f->chainDer + chainSz, f->interDer, (size_t)caSz); + chainSz += caSz; + } if (tc->selfSignedCa) { rootDer = f->interDer; rootSz = caSz; } ExpectIntEQ(test_eku_chain_handshake(f->chainDer, chainSz, - rootDer, rootSz, tc->pinCa ? f->interDer : NULL, caSz, tc, + rootDer, rootSz, tc->pinCa ? f->interDer : NULL, caSz, + (tc->leafOnly && tc->extraLevel) ? f->midDer : NULL, midSz, tc, &hsRet, &verifyRet), TEST_SUCCESS); } @@ -2934,54 +2944,76 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) static const test_eku_case cases[] = { /* eku, selfSigned, selfIssued, clientPresents, pinCa, extraLevel, - * overrideCb, verifyDepth, expected handshake result, keyUsage. */ + * overrideCb, verifyDepth, expected handshake result, keyUsage, + * leafOnly. */ /* Server authentication: a CA restricted to code signing, or to * client authentication, must be refused. */ { "codeSigning", 0, 0, 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, + NULL, 0 }, { "clientAuth", 0, 0, 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, + NULL, 0 }, /* A CA that carries the purpose, or leaves it unrestricted, must not * be over-rejected. */ - { "serverAuth", 0, 0, 0, 0, 0, 0, 0, 0, NULL }, - { NULL, 0, 0, 0, 0, 0, 0, 0, 0, NULL }, - { "any", 0, 0, 0, 0, 0, 0, 0, 0, NULL }, + { "serverAuth", 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0 }, + { NULL, 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0 }, + { "any", 0, 0, 0, 0, 0, 0, 0, 0, NULL, 0 }, /* The rule applies to a CA the certificate manager already holds, not * only to one seen for the first time. */ { "codeSigning", 0, 0, 0, 1, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, - { "serverAuth", 0, 0, 0, 1, 0, 0, 0, 0, NULL }, + NULL, 0 }, + { "serverAuth", 0, 0, 0, 1, 0, 0, 0, 0, NULL, 0 }, + /* The peer sends only the leaf, so the CA comes from the certificate + * manager. The result must match the case where the CA is sent. */ + { "codeSigning", 0, 0, 0, 1, 0, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "clientAuth", 0, 0, 0, 1, 0, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "serverAuth", 0, 0, 0, 1, 0, 0, 0, 0, NULL, 1 }, + { NULL, 0, 0, 0, 1, 0, 0, 0, 0, NULL, 1 }, + { "any", 0, 0, 0, 1, 0, 0, 0, 0, NULL, 1 }, + { "codeSigning", 0, 0, 0, 1, 0, 1, 0, 0, NULL, 1 }, + { "codeSigning", 0, 0, 0, 1, 0, 2, 0, 0, NULL, 1 }, + { "codeSigning", 1, 0, 0, 0, 0, 0, 0, 0, NULL, 1 }, +#ifndef IGNORE_NAME_CONSTRAINTS + /* The CA under test is above the middle CA and neither is sent. */ + { "codeSigning", 0, 0, 0, 1, 1, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "serverAuth", 0, 0, 0, 1, 1, 0, 0, 0, NULL, 1 }, +#endif /* A self-signed trust anchor is exempt: here it is the anchor test, * not the Extended Key Usage, that decides. */ - { "codeSigning", 1, 0, 0, 0, 0, 0, 0, 0, NULL }, + { "codeSigning", 1, 0, 0, 0, 0, 0, 0, 0, NULL, 0 }, /* A self-issued CA key rollover certificate is not an anchor. It * carries the anchor's issuer and subject names, so DecodedCert's * selfIssued flag is set, but it holds a different key and is signed * by the anchor. The Extended Key Usage must still be enforced. */ { "codeSigning", 0, 1, 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, - { "serverAuth", 0, 1, 0, 0, 0, 0, 0, 0, NULL }, + NULL, 0 }, + { "serverAuth", 0, 1, 0, 0, 0, 0, 0, 0, NULL, 0 }, /* Key Usage, not Extended Key Usage: the same rollover certificate * cannot sign the leaf when its Key Usage omits keyCertSign. */ #ifndef ALLOW_INVALID_CERTSIGN { "serverAuth", 0, 1, 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(NOT_CA_ERROR), - "digitalSignature" }, + "digitalSignature", 0 }, #endif /* Enforcement is not limited to the CA directly above the leaf: here * the CA under test issues "EKU Middle CA", which issues the leaf. */ { "codeSigning", 0, 0, 0, 0, 1, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, - { "serverAuth", 0, 0, 0, 0, 1, 0, 0, 0, NULL }, + NULL, 0 }, + { "serverAuth", 0, 0, 0, 0, 1, 0, 0, 0, NULL, 0 }, /* An application that installs a verify callback keeps the last word, * as it does for every other chain error. */ - { "codeSigning", 0, 0, 0, 0, 0, 1, 0, 0, NULL }, + { "codeSigning", 0, 0, 0, 0, 0, 1, 0, 0, NULL, 0 }, #ifdef OPENSSL_EXTRA /* The purpose check must not stand in for the checks that run before * it. With the depth limit pinned below the chain, a callback that @@ -2989,29 +3021,43 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) * rejection, whether or not the CA under test carries the purpose. */ { "serverAuth", 0, 0, 0, 0, 1, 2, 1, WC_NO_ERR_TRACE(MAX_CHAIN_ERROR), - NULL }, + NULL, 0 }, { "codeSigning", 0, 0, 0, 0, 1, 2, 1, WC_NO_ERR_TRACE(MAX_CHAIN_ERROR), - NULL }, + NULL, 0 }, #endif #ifndef WOLFSSL_NO_CLIENT_AUTH /* Client authentication: the server applies the same rule with the * clientAuth purpose. */ { "codeSigning", 0, 0, 1, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, + NULL, 0 }, { "serverAuth", 0, 0, 1, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, - { "clientAuth", 0, 0, 1, 0, 0, 0, 0, 0, NULL }, - { NULL, 0, 0, 1, 0, 0, 0, 0, 0, NULL }, - { "any", 0, 0, 1, 0, 0, 0, 0, 0, NULL }, + NULL, 0 }, + { "clientAuth", 0, 0, 1, 0, 0, 0, 0, 0, NULL, 0 }, + { NULL, 0, 0, 1, 0, 0, 0, 0, 0, NULL, 0 }, + { "any", 0, 0, 1, 0, 0, 0, 0, 0, NULL, 0 }, + { "codeSigning", 0, 0, 1, 1, 0, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "serverAuth", 0, 0, 1, 1, 0, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "clientAuth", 0, 0, 1, 1, 0, 0, 0, 0, NULL, 1 }, + { NULL, 0, 0, 1, 1, 0, 0, 0, 0, NULL, 1 }, +#ifndef IGNORE_NAME_CONSTRAINTS + { "codeSigning", 0, 0, 1, 1, 1, 0, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, + { "clientAuth", 0, 0, 1, 1, 1, 0, 0, 0, NULL, 1 }, +#endif /* The rollover shape is rejected in the client authentication * direction too. */ { "serverAuth", 0, 1, 1, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), - NULL }, - { "clientAuth", 0, 1, 1, 0, 0, 0, 0, 0, NULL }, + NULL, 0 }, + { "clientAuth", 0, 1, 1, 0, 0, 0, 0, 0, NULL, 0 }, #endif }; test_eku_fixture fixture; diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index a1d8bae76dc..1c437e4c3aa 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -25676,8 +25676,8 @@ Signer* findSignerByName(Signer *list, byte *hash) /* Find a signer for cert in cm and extraCAList. Prefers AKID->SKID * with name-hash validation. Fall back to name-only when AKID is * absent. */ -static Signer* FindSignerByAkidOrName(WOLFSSL_CERT_MANAGER* cm, - Signer* extraCAList, Signer* cert) +Signer* wolfssl_local_FindSignerByAkidOrName(WOLFSSL_CERT_MANAGER* cm, + Signer* extraCAList, Signer* cert) { Signer* signer = NULL; #ifdef HAVE_CERTIFICATE_STATUS_REQUEST_V2 @@ -26704,7 +26704,8 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, /* Stop at trust anchor (self-signed). */ if (ncSigner->selfSigned) break; - ncParent = FindSignerByAkidOrName(cm, extraCAList, ncSigner); + ncParent = wolfssl_local_FindSignerByAkidOrName(cm, extraCAList, + ncSigner); /* Stop on missing parent, self-loop, or A->B->A cycle. */ if (ncParent == NULL || ncParent == ncSigner || ncParent == ncPrev) @@ -26788,6 +26789,7 @@ int FillSigner(Signer* signer, DecodedCert* cert, int type, DerBuffer *der) } signer->maxPathLen = cert->maxPathLen; signer->selfSigned = cert->selfSigned; + signer->extExtKeyUsageSet = cert->extExtKeyUsageSet; #ifndef IGNORE_NAME_CONSTRAINTS signer->permittedNames = cert->permittedNames; signer->excludedNames = cert->excludedNames; diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 6bb3427a208..5827ebc7384 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -2364,6 +2364,7 @@ struct Signer { byte extKeyUsage; word16 maxPathLen; WC_BITFIELD selfSigned:1; /* Own key verified the signature */ + WC_BITFIELD extExtKeyUsageSet:1; /* EKU extension present */ #ifndef IGNORE_NAME_CONSTRAINTS /*! * \brief Mirrors DecodedCert::extNameConstraintCrit and @@ -2666,6 +2667,10 @@ WOLFSSL_LOCAL int wc_GetPubX509(DecodedCert* cert, int verify, int* badDate); WOLFSSL_LOCAL const byte* OidFromId(word32 id, word32 type, word32* oidSz); WOLFSSL_LOCAL Signer* findSignerByKeyHash(Signer *list, byte *hash); WOLFSSL_LOCAL Signer* findSignerByName(Signer *list, byte *hash); +#ifndef IGNORE_NAME_CONSTRAINTS +WOLFSSL_LOCAL Signer* wolfssl_local_FindSignerByAkidOrName( + WOLFSSL_CERT_MANAGER* cm, Signer* extraCAList, Signer* cert); +#endif WOLFSSL_TEST_VIS int FillSigner(Signer* signer, DecodedCert* cert, int type, DerBuffer *der); WOLFSSL_TEST_VIS Signer* MakeSigner(void* heap); WOLFSSL_TEST_VIS void FreeSigner(Signer* signer, void* heap); From ff6a0633ec7a0595573ba9f94c76c2b1ad87718b Mon Sep 17 00:00:00 2001 From: Sameeh Date: Mon, 26 Oct 2026 09:34:47 +0200 Subject: [PATCH 2/5] Honor a verify callback that accepts a chain CA purpose error CheckSignerExtKeyUsage() runs after the chain callback and starts at the leaf issuer. A callback that accepts EXTKEYUSE_AUTH_E for that CA was asked again at depth 0, and a depth-specific acceptance failed the handshake. Record the subject-name hash on the handshake when the callback accepts the error. The later walk skips that CA and still checks ancestors. The shared Signer is not changed. Pass the Certificate Status Request v2 pending signer list into the issuer lookup, the same list certificate parsing uses. --- src/internal.c | 82 +++++++++++++++++++++++++++++++++++---- tests/api/test_ssl_cert.c | 32 ++++++++++++++- wolfssl/internal.h | 7 ++++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/internal.c b/src/internal.c index dc2a38f1f96..ec6c42b9102 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18135,11 +18135,49 @@ static int CheckChainCAExtKeyUsage(const WOLFSSL* ssl, const DecodedCert* cert) return 0; } +/* True when this handshake already accepted an Extended Key Usage error + * for the CA with this subject-name hash. */ +static int SignerEkuWaived(const Signer* ca, + const byte waiveName[][SIGNER_DIGEST_SIZE], int waiveCount) +{ + int i; + + for (i = 0; i < waiveCount; i++) { + if (XMEMCMP(ca->subjectNameHash, waiveName[i], SIGNER_DIGEST_SIZE) == 0) + return 1; + } + return 0; +} + +/* Record the subject-name hash of a chain CA whose Extended Key Usage error + * the verify callback accepted. */ +static void RememberWaivedCaEku(ProcPeerCertArgs* args) +{ + int i; + + if (args == NULL || args->dCert == NULL) + return; + if (args->ekuWaiveCount >= MAX_CHAIN_DEPTH) + return; + for (i = 0; i < (int)args->ekuWaiveCount; i++) { + if (XMEMCMP(args->ekuWaiveName[i], args->dCert->subjectHash, + SIGNER_DIGEST_SIZE) == 0) + return; + } + XMEMCPY(args->ekuWaiveName[args->ekuWaiveCount], args->dCert->subjectHash, + SIGNER_DIGEST_SIZE); + args->ekuWaiveCount++; +} + /* Check the Extended Key Usage of each CA from ca up to the self-signed trust - * anchor against the TLS purpose. With IGNORE_NAME_CONSTRAINTS there is no - * issuer lookup, so only ca is checked. + * anchor against the TLS purpose. A CA whose error this handshake already + * accepted is skipped, and the walk continues to ancestors the chain did not + * present. With IGNORE_NAME_CONSTRAINTS there is no issuer lookup, so only ca + * is checked. pending holds Certificate Status Request v2 signers that are + * not in the certificate manager yet. * Returns 0, EXTKEYUSE_AUTH_E or MAX_CHAIN_ERROR. */ -static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca) +static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca, + Signer* pending, const byte waiveName[][SIGNER_DIGEST_SIZE], int waiveCount) { byte purpose; Signer* prev = NULL; @@ -18153,13 +18191,15 @@ static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca) while (ca != NULL && !ca->selfSigned) { if (ca->extExtKeyUsageSet && - (ca->extKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0) { + (ca->extKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0 && + !SignerEkuWaived(ca, waiveName, waiveCount)) { WOLFSSL_MSG("Issuer CA ExtKeyUse doesn't allow TLS peer auth"); return EXTKEYUSE_AUTH_E; } #ifndef IGNORE_NAME_CONSTRAINTS - parent = wolfssl_local_FindSignerByAkidOrName(SSL_CM(ssl), NULL, ca); + parent = wolfssl_local_FindSignerByAkidOrName(SSL_CM(ssl), pending, ca); #else + (void)pending; parent = NULL; #endif /* Stop on missing parent, self-loop, or A->B->A cycle. */ @@ -19935,7 +19975,22 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, SetRevocationVerifyRet(ssl, ret); /* Do verify callback */ - ret = DoVerifyCallbackHeldCrl(ssl, args, ret, &heldCrlRet); + { + int cbErr = ret; + + ret = DoVerifyCallbackHeldCrl(ssl, args, cbErr, + &heldCrlRet); + #ifndef IGNORE_KEY_EXTENSIONS + /* The later issuer walk repeats this CA. Remember an + * accepted purpose error so that walk does not reject + * it again. Ancestors are still checked. */ + if (ret == 0 && + cbErr == WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) && + args->dCert != NULL && args->dCert->isCA) { + RememberWaivedCaEku(args); + } + #endif + } if (CrlErrWaived(ssl, ret)) ret = ssl->error = 0; #if defined(HAVE_OCSP) && defined(HAVE_CRL) && \ @@ -20471,13 +20526,24 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } /* CAs the peer did not send were not covered by - * CheckChainCAExtKeyUsage(). */ + * CheckChainCAExtKeyUsage(). Chain CAs whose purpose error + * this handshake already accepted are skipped. */ if (ret == 0 && !ssl->options.verifyNone && #ifdef WOLFSSL_TRUST_PEER_CERT !args->haveTrustPeer && #endif !UsingChainVerifyCb(ssl)) { - ret = CheckSignerExtKeyUsage(ssl, args->dCert->ca); + Signer* ekuPending = NULL; + + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) && \ + !defined(NO_TLS) + if (TLSX_CSR2_IsMulti(ssl->extensions)) { + ekuPending = TLSX_CSR2_GetPendingSigners( + ssl->extensions); + } + #endif + ret = CheckSignerExtKeyUsage(ssl, args->dCert->ca, + ekuPending, args->ekuWaiveName, args->ekuWaiveCount); if (ret != 0) { WOLFSSL_ERROR_VERBOSE(ret); #if defined(OPENSSL_EXTRA) || \ diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 25f1d75d3a6..77ee9d46f1d 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2617,7 +2617,8 @@ typedef struct test_eku_case { * depth 2 */ int overrideCb; /* verify callback installed on the verifying * side: 0 none, 1 accepts every error, - * 2 waives only the purpose error */ + * 2 waives only the purpose error, + * 3 waives the purpose error only at depth 1 */ int verifyDepth; /* verify depth pinned on the verifying side, * 0 leaves the default in place */ int expectRet; /* expected handshake result */ @@ -2651,6 +2652,25 @@ static int test_eku_purpose_only_cb(int preverify, return preverify; } +/* Accept a purpose error only for the intermediate at depth 1. A later report + * of the same error at depth 0, with the leaf as the current certificate, + * must not be required for the handshake to succeed. */ +static int test_eku_depth_purpose_cb(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + if (preverify) + return 1; + if (store->error_depth == 1 && + (store->error == WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + || store->error == WOLFSSL_X509_V_ERR_INVALID_PURPOSE +#endif + )) { + return 1; + } + return 0; +} + /* Build a CA:TRUE certificate for subjKey. A NULL issuerDer makes it * self-signed, otherwise it is signed by issuerKey in the name of issuerDer. A * NULL extKeyUsage omits the Extended Key Usage extension entirely. When @@ -2775,6 +2795,8 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, verifyCb = test_eku_override_cb; else if (tc->overrideCb == 2) verifyCb = test_eku_purpose_only_cb; + else if (tc->overrideCb == 3) + verifyCb = test_eku_depth_purpose_cb; XMEMSET(&test_ctx, 0, sizeof(test_ctx)); /* The server always presents the generated chain so a single set of @@ -3014,6 +3036,14 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) /* An application that installs a verify callback keeps the last word, * as it does for every other chain error. */ { "codeSigning", 0, 0, 0, 0, 0, 1, 0, 0, NULL, 0 }, + /* The purpose error is accepted only at depth 1, where the chain CA + * is presented. The issuer walk must not ask again at depth 0. */ + { "codeSigning", 0, 0, 0, 0, 0, 3, 0, 0, NULL, 0 }, + /* The same callback does not cover a CA the peer never sent. That + * error is reported for the leaf, at depth 0. */ + { "codeSigning", 0, 0, 0, 1, 0, 3, 0, + WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E), + NULL, 1 }, #ifdef OPENSSL_EXTRA /* The purpose check must not stand in for the checks that run before * it. With the depth limit pinned below the chain, a callback that diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 4c95a99929a..16a94cb15d5 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3037,6 +3037,13 @@ typedef struct ProcPeerCertArgs { #ifdef HAVE_CERTIFICATE_STATUS_REQUEST word16 csrAcked:1; /* survives the staple checks clearing status_request */ #endif +#ifndef IGNORE_KEY_EXTENSIONS + /* Subject-name hashes of chain CAs whose Extended Key Usage error this + * handshake already accepted. The later issuer walk skips these CAs and + * still checks ancestors. The shared Signer is left unchanged. */ + byte ekuWaiveCount; + byte ekuWaiveName[MAX_CHAIN_DEPTH][SIGNER_DIGEST_SIZE]; +#endif } ProcPeerCertArgs; WOLFSSL_LOCAL int DoVerifyCallback(WOLFSSL_CERT_MANAGER* cm, WOLFSSL* ssl, int cert_err, ProcPeerCertArgs* args); From e392bad7ba914652e01f0c90e3b3eb053423df09 Mon Sep 17 00:00:00 2001 From: Sameeh Date: Mon, 26 Oct 2026 11:56:33 +0200 Subject: [PATCH 3/5] Keep the chain CA purpose waiver inside the async args buffer The subject-hash table made ProcPeerCertArgs larger than ssl->async->args, and the array argument failed -Wpedantic. Record accepted chain indexes in the existing flag word. --- src/internal.c | 55 +++++++++++++++------------------------------- wolfssl/internal.h | 13 ++++++----- 2 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/internal.c b/src/internal.c index ec6c42b9102..46d173fbab3 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18135,54 +18135,31 @@ static int CheckChainCAExtKeyUsage(const WOLFSSL* ssl, const DecodedCert* cert) return 0; } -/* True when this handshake already accepted an Extended Key Usage error - * for the CA with this subject-name hash. */ -static int SignerEkuWaived(const Signer* ca, - const byte waiveName[][SIGNER_DIGEST_SIZE], int waiveCount) -{ - int i; - - for (i = 0; i < waiveCount; i++) { - if (XMEMCMP(ca->subjectNameHash, waiveName[i], SIGNER_DIGEST_SIZE) == 0) - return 1; - } - return 0; -} - -/* Record the subject-name hash of a chain CA whose Extended Key Usage error - * the verify callback accepted. */ +/* Record the chain index of a CA whose Extended Key Usage error the verify + * callback accepted. The leaf is index 0. Its issuer is index 1. */ static void RememberWaivedCaEku(ProcPeerCertArgs* args) { - int i; - - if (args == NULL || args->dCert == NULL) - return; - if (args->ekuWaiveCount >= MAX_CHAIN_DEPTH) + if (args == NULL || args->certIdx < 0 || args->certIdx >= MAX_CHAIN_DEPTH) return; - for (i = 0; i < (int)args->ekuWaiveCount; i++) { - if (XMEMCMP(args->ekuWaiveName[i], args->dCert->subjectHash, - SIGNER_DIGEST_SIZE) == 0) - return; - } - XMEMCPY(args->ekuWaiveName[args->ekuWaiveCount], args->dCert->subjectHash, - SIGNER_DIGEST_SIZE); - args->ekuWaiveCount++; + args->ekuWaiveMask |= (word16)(1u << args->certIdx); } /* Check the Extended Key Usage of each CA from ca up to the self-signed trust - * anchor against the TLS purpose. A CA whose error this handshake already - * accepted is skipped, and the walk continues to ancestors the chain did not - * present. With IGNORE_NAME_CONSTRAINTS there is no issuer lookup, so only ca - * is checked. pending holds Certificate Status Request v2 signers that are - * not in the certificate manager yet. + * anchor against the TLS purpose. waiveMask names chain indexes whose error + * this handshake already accepted. The first signer is the leaf issuer, chain + * index 1. A waived CA is skipped and the walk continues. With + * IGNORE_NAME_CONSTRAINTS there is no issuer lookup, so only ca is checked. + * pending holds Certificate Status Request v2 signers that are not in the + * certificate manager yet. * Returns 0, EXTKEYUSE_AUTH_E or MAX_CHAIN_ERROR. */ static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca, - Signer* pending, const byte waiveName[][SIGNER_DIGEST_SIZE], int waiveCount) + Signer* pending, unsigned waiveMask) { byte purpose; Signer* prev = NULL; Signer* parent; int depth = 0; + int certIdx = 1; if (ssl->options.side == WOLFSSL_CLIENT_END) purpose = EXTKEYUSE_SERVER_AUTH; @@ -18190,9 +18167,12 @@ static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca, purpose = EXTKEYUSE_CLIENT_AUTH; while (ca != NULL && !ca->selfSigned) { + int waived = (certIdx < MAX_CHAIN_DEPTH) && + ((waiveMask & (1u << certIdx)) != 0); + if (ca->extExtKeyUsageSet && (ca->extKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0 && - !SignerEkuWaived(ca, waiveName, waiveCount)) { + !waived) { WOLFSSL_MSG("Issuer CA ExtKeyUse doesn't allow TLS peer auth"); return EXTKEYUSE_AUTH_E; } @@ -18209,6 +18189,7 @@ static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca, return MAX_CHAIN_ERROR; prev = ca; ca = parent; + certIdx++; } return 0; @@ -20543,7 +20524,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } #endif ret = CheckSignerExtKeyUsage(ssl, args->dCert->ca, - ekuPending, args->ekuWaiveName, args->ekuWaiveCount); + ekuPending, args->ekuWaiveMask); if (ret != 0) { WOLFSSL_ERROR_VERBOSE(ret); #if defined(OPENSSL_EXTRA) || \ diff --git a/wolfssl/internal.h b/wolfssl/internal.h index 16a94cb15d5..b5214fc62e8 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3038,11 +3038,14 @@ typedef struct ProcPeerCertArgs { word16 csrAcked:1; /* survives the staple checks clearing status_request */ #endif #ifndef IGNORE_KEY_EXTENSIONS - /* Subject-name hashes of chain CAs whose Extended Key Usage error this - * handshake already accepted. The later issuer walk skips these CAs and - * still checks ancestors. The shared Signer is left unchanged. */ - byte ekuWaiveCount; - byte ekuWaiveName[MAX_CHAIN_DEPTH][SIGNER_DIGEST_SIZE]; + /* certIdx of each chain CA whose Extended Key Usage error this handshake + * accepted. Kept in this flag word so ProcPeerCertArgs still fits + * ssl->async->args. The later issuer walk skips those indexes and still + * checks ancestors. The shared Signer is left unchanged. */ +#if MAX_CHAIN_DEPTH > 11 +#error "MAX_CHAIN_DEPTH does not fit the peer-cert EKU waive mask" +#endif + word16 ekuWaiveMask:MAX_CHAIN_DEPTH; #endif } ProcPeerCertArgs; WOLFSSL_LOCAL int DoVerifyCallback(WOLFSSL_CERT_MANAGER* cm, WOLFSSL* ssl, From 6143ef9ba173a9e61c7a3c3a8d722c42b8c72fc9 Mon Sep 17 00:00:00 2001 From: Sameeh Date: Mon, 26 Oct 2026 12:57:56 +0200 Subject: [PATCH 4/5] Drop the null test on the decoded chain certificate The decoded certificate is allocated before this loop. The extra null test made -Wnull-dereference treat the later isCA use as a null dereference under -Werror. --- src/internal.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/internal.c b/src/internal.c index 46d173fbab3..7e8990d5e3c 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19967,7 +19967,7 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, * it again. Ancestors are still checked. */ if (ret == 0 && cbErr == WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) && - args->dCert != NULL && args->dCert->isCA) { + args->dCert->isCA) { RememberWaivedCaEku(args); } #endif From a1cb3149bc0b234ce3b3bc8809736953619fecbe Mon Sep 17 00:00:00 2001 From: Sameeh Date: Thu, 29 Oct 2026 09:54:56 +0200 Subject: [PATCH 5/5] Use uppercase U suffixes in the CA purpose waiver mask clang-tidy readability-uppercase-literal-suffix rejected the lowercase 1u literals in RememberWaivedCaEku() and CheckSignerExtKeyUsage(). --- src/internal.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index 7e8990d5e3c..b661bf81720 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18141,7 +18141,7 @@ static void RememberWaivedCaEku(ProcPeerCertArgs* args) { if (args == NULL || args->certIdx < 0 || args->certIdx >= MAX_CHAIN_DEPTH) return; - args->ekuWaiveMask |= (word16)(1u << args->certIdx); + args->ekuWaiveMask |= (word16)(1U << args->certIdx); } /* Check the Extended Key Usage of each CA from ca up to the self-signed trust @@ -18168,7 +18168,7 @@ static int CheckSignerExtKeyUsage(const WOLFSSL* ssl, Signer* ca, while (ca != NULL && !ca->selfSigned) { int waived = (certIdx < MAX_CHAIN_DEPTH) && - ((waiveMask & (1u << certIdx)) != 0); + ((waiveMask & (1U << certIdx)) != 0); if (ca->extExtKeyUsageSet && (ca->extKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0 &&