diff --git a/src/internal.c b/src/internal.c index 95b71ddbfd1..1cda525c750 100644 --- a/src/internal.c +++ b/src/internal.c @@ -17941,6 +17941,66 @@ static int CheckChainCAExtKeyUsage(const WOLFSSL* ssl, const DecodedCert* cert) return 0; } + +/* 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) +{ + if (args == NULL || args->certIdx < 0 || args->certIdx >= MAX_CHAIN_DEPTH) + return; + 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. 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, 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; + else + 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 && + !waived) { + 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), pending, ca); + #else + (void)pending; + 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; + certIdx++; + } + + return 0; +} #endif /* IGNORE_KEY_EXTENSIONS */ #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) @@ -19570,7 +19630,21 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif /* defined(__APPLE__) && defined(WOLFSSL_SYS_CA_CERTS) */ /* Do verify callback */ - ret = DoVerifyCallback(SSL_CM(ssl), ssl, ret, args); + { + int cbErr = ret; + + ret = DoVerifyCallback(SSL_CM(ssl), ssl, cbErr, args); + #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->isCA) { + RememberWaivedCaEku(args); + } + #endif + } if (ssl->options.verifyNone && (ret == WC_NO_ERR_TRACE(CRL_MISSING) || ret == WC_NO_ERR_TRACE(CRL_CERT_REVOKED) || @@ -20096,6 +20170,40 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } } } + + /* CAs the peer did not send were not covered by + * 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)) { + 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->ekuWaiveMask); + 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 68dcedca73a..4d01112c2cb 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -2604,12 +2604,15 @@ 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 */ 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. */ @@ -2636,6 +2639,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 @@ -2743,7 +2765,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; @@ -2759,6 +2782,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 @@ -2799,6 +2824,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. */ @@ -2869,18 +2898,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); } @@ -2921,54 +2953,84 @@ 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 }, + /* 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 @@ -2976,29 +3038,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 0d3c85a5ece..960c09100fa 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -25351,8 +25351,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 @@ -26372,7 +26372,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) @@ -26456,6 +26457,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/internal.h b/wolfssl/internal.h index 6f9c8cddb6e..72200bcf3b4 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -3019,6 +3019,16 @@ typedef struct ProcPeerCertArgs { #ifdef WOLFSSL_TRUST_PEER_CERT word16 haveTrustPeer:1; /* was cert verified by loaded trusted peer cert */ #endif +#ifndef IGNORE_KEY_EXTENSIONS + /* 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, int cert_err, ProcPeerCertArgs* args); diff --git a/wolfssl/wolfcrypt/asn.h b/wolfssl/wolfcrypt/asn.h index 879dfbdf668..817cad56008 100644 --- a/wolfssl/wolfcrypt/asn.h +++ b/wolfssl/wolfcrypt/asn.h @@ -2351,6 +2351,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 @@ -2642,6 +2643,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);