From 6fb23d3e42aabc7b71c63e5f79c67757a1053d17 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Tue, 11 Aug 2026 15:36:33 -0500 Subject: [PATCH 1/2] Enforce Extended Key Usage on chain-supplied intermediate CAs --- ChangeLog.md | 27 ++++ src/internal.c | 60 +++++++ src/x509_str.c | 2 + tests/api.c | 2 +- tests/api/test_ssl_cert.c | 330 ++++++++++++++++++++++++++++++++++++++ tests/api/test_ssl_cert.h | 5 +- wolfcrypt/src/asn.c | 10 +- wolfssl/ssl.h | 1 + 8 files changed, 431 insertions(+), 6 deletions(-) diff --git a/ChangeLog.md b/ChangeLog.md index 55652f4971..a222dd2b65 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -150,6 +150,33 @@ ## Fixes +* **Fix (Extended Key Usage not enforced on chain-supplied intermediate CAs)**: + the TLS peer certificate was checked for the `serverAuth` or `clientAuth` + Extended Key Usage, but the intermediate CAs sent alongside it were not. A + certificate authority restricted to another purpose by a critical EKU, a code + signing, S/MIME or timestamping subordinate CA for example, could therefore + issue a `serverAuth` leaf for any name and have wolfSSL complete the + handshake, defeating the isolation such a constrained CA exists to provide. + `ProcessPeerCerts()` now applies the same purpose check to every + chain-supplied CA it validates, whether or not the certificate manager + already holds it, and fails the handshake with `EXTKEYUSE_AUTH_E` when the CA + does not carry the purpose being validated. Per RFC 5280 4.2.1.12 an absent + extension leaves all purposes valid and `anyExtendedKeyUsage` removes the + restriction, so neither is rejected, and a self-signed certificate is exempt + because it can only take part in a path as a trust anchor the operator chose + to load. The check applies to the certificates the peer transmits; an issuer + resolved from the certificate manager because the peer did not send it is + not covered. This is stricter than before, in three cases that previously + succeeded: a chain whose intermediate asserts an Extended Key Usage without + the purpose in use, `serverAuth` only on a CA that also issues client + certificates for instance; a chain whose intermediate asserts only + KeyPurposeIds wolfSSL does not recognise, since those set no bit; and a + chain whose intermediate the operator loaded as a trusted CA, which is held + to the same rule as any other chain CA. `IGNORE_KEY_EXTENSIONS` opts out, as + it already did for the peer certificate. Adds + `WOLFSSL_X509_V_ERR_INVALID_PURPOSE`, reported through + `wolfSSL_get_verify_result()` and to verify callbacks. + * **Fix (certificate manager left pointing at a released store)**: `wolfSSL_CTX_set_cert_store()` pairs the store handed to it with the context's certificate manager, which keeps a pointer back to that store. diff --git a/src/internal.c b/src/internal.c index fd100102fa..34ab3a5654 100644 --- a/src/internal.c +++ b/src/internal.c @@ -17014,6 +17014,40 @@ static int DoCertReqCtx(WOLFSSL* ssl, ProcPeerCertArgs* args, } #endif /* WOLFSSL_TLS13 */ +/* Enforced by default (RFC 5280 4.2.1.12: when an Extended Key Usage extension + * is present the certificate may only be used for one of the indicated + * purposes). IGNORE_KEY_EXTENSIONS is a deliberate, RFC-non-conformant opt-out; + * see the macro list at the top of wolfcrypt/src/asn.c. */ +#ifndef IGNORE_KEY_EXTENSIONS +/* Check that a chain-supplied CA is authorized for the TLS purpose currently + * being validated: serverAuth when this side is authenticating a server, + * clientAuth when authenticating a client. An absent extension leaves every + * purpose valid, and anyExtendedKeyUsage removes the restriction. A + * self-signed certificate is exempt: it can only take part in a path as a + * trust anchor the operator chose to load, matching the exemption AddCA() + * makes for the Key Usage of a root. Returns 0 when the CA may be used, + * EXTKEYUSE_AUTH_E when it may not. */ +static int CheckChainCAExtKeyUsage(const WOLFSSL* ssl, const DecodedCert* cert) +{ + byte purpose; + + if (!cert->extExtKeyUsageSet || cert->selfSigned) + return 0; + + if (ssl->options.side == WOLFSSL_CLIENT_END) + purpose = EXTKEYUSE_SERVER_AUTH; + else + purpose = EXTKEYUSE_CLIENT_AUTH; + + if ((cert->extExtKeyUsage & (EXTKEYUSE_ANY | purpose)) == 0) { + WOLFSSL_MSG("Chain CA ExtKeyUse doesn't allow TLS peer authentication"); + return EXTKEYUSE_AUTH_E; + } + + return 0; +} +#endif /* IGNORE_KEY_EXTENSIONS */ + #if defined(HAVE_CERTIFICATE_STATUS_REQUEST_V2) /* Parse a chain certificate as a CA and add it to the pending signers list * for Certificate Status Request v2. */ @@ -17065,6 +17099,11 @@ static int ProcessPeerCertAddPendingCA(WOLFSSL* ssl, buffer* cert) goto exit_req_v2; } #endif + /* The Extended Key Usage purpose check is deliberately not repeated here. + * ProcessPeerCerts() applies it to this same certificate before offering it + * to the pool, and AddCA() does not apply it either, so repeating it would + * only take effect after a verify callback had already overridden the + * rejection, silently undoing that decision in CSR v2 builds alone. */ ret = AllocDer(&derBuffer, cert->length, CA_TYPE, ssl->heap); if (ret != 0 || derBuffer == NULL) { goto exit_req_v2; @@ -18142,6 +18181,24 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, "not adding as CA"); } else if (ret == 0) { + #ifndef IGNORE_KEY_EXTENSIONS + /* A CA restricted to some other purpose by its + * Extended Key Usage must not authenticate this peer, + * whether or not the certificate manager already + * holds it. */ + ret = CheckChainCAExtKeyUsage(ssl, args->dCert); + 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 = + WOLFSSL_X509_V_ERR_INVALID_PURPOSE; + } + #endif + } + #endif /* IGNORE_KEY_EXTENSIONS */ #ifdef OPENSSL_EXTRA if (args->certIdx > args->untrustedDepth) { args->untrustedDepth = (char)args->certIdx + 1; @@ -29512,6 +29569,9 @@ static const char* wolfSSL_ERR_reason_error_string_OpenSSL(unsigned long e) case WOLFSSL_X509_V_ERR_PATH_LENGTH_EXCEEDED: return "path length constraint exceeded"; + case WOLFSSL_X509_V_ERR_INVALID_PURPOSE: + return "unsupported certificate purpose"; + case WOLFSSL_X509_V_ERR_CERT_REJECTED: return "certificate rejected"; diff --git a/src/x509_str.c b/src/x509_str.c index aa67907689..3adf87386d 100644 --- a/src/x509_str.c +++ b/src/x509_str.c @@ -324,6 +324,8 @@ int GetX509Error(int e) return WOLFSSL_X509_V_ERR_CERT_REVOKED; case WC_NO_ERR_TRACE(CRL_MISSING): return WOLFSSL_X509_V_ERR_UNABLE_TO_GET_CRL; + case WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E): + return WOLFSSL_X509_V_ERR_INVALID_PURPOSE; /* is an internal wolfSSL return code, not an X509_V_* code, so 1 * here is WOLFSSL_SUCCESS - it does not collide with * WOLFSSL_X509_V_ERR_UNSPECIFIED, which shares the value but never diff --git a/tests/api.c b/tests/api.c index 25992ee13c..6d4f28629d 100644 --- a/tests/api.c +++ b/tests/api.c @@ -31442,7 +31442,7 @@ static int error_test(void) {17, 15}, {19, 19}, {24, 24}, - {27, 26 }, + {27, 27}, {61, 30}, {63, 63}, {78, 65}, diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 22cdc54803..d42ef136b1 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -32,6 +32,7 @@ #include #include +#include #include /* Tests for the certificate APIs in src/ssl_api_cert.c (moved from ssl.c). */ @@ -1496,3 +1497,332 @@ int test_wolfSSL_cert_unload(void) #endif return EXPECT_RESULT(); } + +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) + +#define TEST_EKU_CERT_BUF_SZ (2 * FOURK_BUF) + +/* Keys and scratch buffers shared by every case. */ +typedef struct test_eku_fixture { + RsaKey* caKey; + RsaKey* intKey; + RsaKey* leafKey; + WC_RNG* rng; + byte* interDer; + byte* chainDer; +} test_eku_fixture; + +/* One chain Extended Key Usage scenario. */ +typedef struct test_eku_case { + const char* extKeyUsage; /* Extended Key Usage on the issuing CA, NULL + * omits the extension */ + int selfSignedCa; /* issuing CA is self-signed and is itself the + * trust anchor */ + int clientPresents; /* client sends the chain, server verifies it */ + int pinCa; /* issuing CA is also loaded as a trusted CA */ + int expectRet; /* expected handshake result */ +} test_eku_case; + +/* Build a CA:TRUE certificate for the given key. Unless selfSign is set it is + * signed by the 2048-bit test root (ca_cert_der_2048 / ca_key_der_2048). A NULL + * extKeyUsage omits the Extended Key Usage extension entirely. Returns the DER + * length, or < 0 on failure. */ +static int test_eku_gen_ca(byte* out, int outMax, RsaKey* subjKey, + RsaKey* caKey, WC_RNG* rng, const char* extKeyUsage, int selfSign) +{ + Cert cert; + int ret = 0; + + if (wc_InitCert(&cert) != 0) + return -1; + cert.isCA = 1; + cert.sigType = CTC_SHA256wRSA; + XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.org, "wolfSSL_test", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.commonName, + selfSign ? "EKU Self Signed CA" : "EKU Intermediate", + CTC_NAME_SIZE - 1); + if (wc_SetSubjectKeyIdFromPublicKey(&cert, subjKey, NULL) != 0) + ret = -1; + if (ret == 0 && wc_SetKeyUsage(&cert, "keyCertSign,cRLSign") != 0) + ret = -1; + if (ret == 0 && extKeyUsage != NULL && + wc_SetExtKeyUsage(&cert, extKeyUsage) != 0) + ret = -1; + /* wc_InitCert() leaves selfSigned set, so naming no issuer is what makes + * the generated certificate self-signed. */ + if (ret == 0 && !selfSign) { + if (wc_SetAuthKeyIdFromCert(&cert, ca_cert_der_2048, + (int)sizeof_ca_cert_der_2048) != 0) + ret = -1; + if (ret == 0 && wc_SetIssuerBuffer(&cert, ca_cert_der_2048, + (int)sizeof_ca_cert_der_2048) != 0) + ret = -1; + } + if (ret == 0) + ret = wc_MakeCert(&cert, out, (word32)outMax, subjKey, NULL, rng); + if (ret >= 0) + ret = wc_SignCert(cert.bodySz, cert.sigType, out, (word32)outMax, + selfSign ? subjKey : caKey, NULL, rng); +#ifdef WOLFSSL_CERT_GEN_CACHE + wc_SetCert_Free(&cert); +#endif + return ret; +} + +/* Build a TLS leaf signed by the given CA. The leaf carries both serverAuth + * and clientAuth so only the issuing CA's purpose is tested. */ +static int test_eku_gen_leaf(byte* out, int outMax, RsaKey* leafKey, + const byte* issuerDer, int issuerDerSz, RsaKey* issuerKey, WC_RNG* rng) +{ + Cert cert; + int ret = 0; + + if (wc_InitCert(&cert) != 0) + return -1; + cert.isCA = 0; + cert.sigType = CTC_SHA256wRSA; + XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.org, "wolfSSL_test", CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.commonName, "EKU Leaf", CTC_NAME_SIZE - 1); + if (wc_SetSubjectKeyIdFromPublicKey(&cert, leafKey, NULL) != 0) + ret = -1; + if (ret == 0 && wc_SetAuthKeyIdFromCert(&cert, issuerDer, issuerDerSz) != 0) + ret = -1; + if (ret == 0 && + wc_SetKeyUsage(&cert, "digitalSignature,keyEncipherment") != 0) + ret = -1; + if (ret == 0 && wc_SetExtKeyUsage(&cert, "serverAuth,clientAuth") != 0) + ret = -1; + if (ret == 0 && wc_SetIssuerBuffer(&cert, issuerDer, issuerDerSz) != 0) + ret = -1; + if (ret == 0) + ret = wc_MakeCert(&cert, out, (word32)outMax, leafKey, NULL, rng); + if (ret >= 0) + ret = wc_SignCert(cert.bodySz, cert.sigType, out, (word32)outMax, + issuerKey, NULL, rng); +#ifdef WOLFSSL_CERT_GEN_CACHE + wc_SetCert_Free(&cert); +#endif + return ret; +} + +/* Run a memio handshake in which one side presents "leaf <- CA" and the peer + * verifies that chain against rootDer. When clientPresents is set the client + * sends the chain and the server verifies it, which is the client + * authentication direction. A non-NULL pinnedDer is loaded as an additional + * trusted CA on the verifying side, so the chain CA is already a known signer + * when it arrives. The handshake result and the compatibility-layer verify + * result are returned 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, + int clientPresents, int* hsRet, long* verifyRet) +{ + EXPECT_DECLS; + WOLFSSL_CTX* ctx_c = NULL; + WOLFSSL_CTX* ctx_s = NULL; + WOLFSSL_CTX* verifyCtx = NULL; + WOLFSSL* ssl_c = NULL; + WOLFSSL* ssl_s = NULL; + WOLFSSL* verifySsl = NULL; + struct test_memio_ctx test_ctx; + + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + /* The server always presents the generated chain so a single set of + * credentials covers both directions. */ + ExpectIntEQ(test_memio_setup_ex(&test_ctx, &ctx_c, &ctx_s, &ssl_c, &ssl_s, + wolfSSLv23_client_method, wolfSSLv23_server_method, + (byte*)rootDer, rootSz, (byte*)chainDer, chainSz, + (byte*)client_key_der_2048, (int)sizeof_client_key_der_2048), 0); + + if (clientPresents) { + /* Leave the server chain unverified so only the server's view of the + * client chain decides the handshake. */ + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); + ExpectIntEQ(wolfSSL_use_certificate_chain_buffer_format(ssl_c, chainDer, + (long)chainSz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_buffer(ssl_c, client_key_der_2048, + (long)sizeof_client_key_der_2048, WOLFSSL_FILETYPE_ASN1), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CTX_load_verify_buffer(ctx_s, rootDer, + (long)rootSz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER | + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + verifyCtx = ctx_s; + verifySsl = ssl_s; + } + else { + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, NULL); + verifyCtx = ctx_c; + verifySsl = ssl_c; + } + + if (pinnedDer != NULL) { + ExpectIntEQ(wolfSSL_CTX_load_verify_buffer(verifyCtx, pinnedDer, + (long)pinnedSz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); + } + + /* The harness only reports pass or fail, so read the reason from the side + * that did the verifying. */ + if (EXPECT_SUCCESS()) { + if (test_memio_do_handshake(ssl_c, ssl_s, 10, NULL) == 0) + *hsRet = 0; + else + *hsRet = wolfSSL_get_error(verifySsl, WOLFSSL_FATAL_ERROR); + #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + *verifyRet = wolfSSL_get_verify_result(verifySsl); + #endif + } + (void)verifyRet; + + wolfSSL_free(ssl_s); + wolfSSL_free(ssl_c); + wolfSSL_CTX_free(ctx_s); + wolfSSL_CTX_free(ctx_c); + + return EXPECT_RESULT(); +} + +/* Build "leaf <- CA" for one scenario, run the handshake and check both the + * handshake result and the verify result it is reported as. */ +static int test_eku_chain_case(const test_eku_fixture* f, + const test_eku_case* tc) +{ + EXPECT_DECLS; + const byte* rootDer = ca_cert_der_2048; + int rootSz = (int)sizeof_ca_cert_der_2048; + int caSz = 0; + int leafSz = 0; + int hsRet = -1; + long verifyRet = -1; + + ExpectIntGT((caSz = test_eku_gen_ca(f->interDer, TEST_EKU_CERT_BUF_SZ, + f->intKey, f->caKey, f->rng, tc->extKeyUsage, tc->selfSignedCa)), 0); + ExpectIntGT((leafSz = test_eku_gen_leaf(f->chainDer, TEST_EKU_CERT_BUF_SZ, + f->leafKey, f->interDer, caSz, f->intKey, f->rng)), 0); + /* The chain buffer holds the leaf first, then its issuer. */ + ExpectIntLE(leafSz + caSz, TEST_EKU_CERT_BUF_SZ); + + if (EXPECT_SUCCESS()) { + XMEMCPY(f->chainDer + leafSz, f->interDer, (size_t)caSz); + if (tc->selfSignedCa) { + rootDer = f->interDer; + rootSz = caSz; + } + ExpectIntEQ(test_eku_chain_handshake(f->chainDer, leafSz + caSz, + rootDer, rootSz, tc->pinCa ? f->interDer : NULL, caSz, + tc->clientPresents, &hsRet, &verifyRet), TEST_SUCCESS); + } + + ExpectIntEQ(hsRet, tc->expectRet); +#if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + ExpectIntEQ(verifyRet, (tc->expectRet == 0) ? + WOLFSSL_X509_V_OK : WOLFSSL_X509_V_ERR_INVALID_PURPOSE); +#endif + + return EXPECT_RESULT(); +} +#endif /* chain EKU test dependencies */ + +/* Test that the Extended Key Usage of a chain-supplied intermediate CA is + * enforced against the TLS purpose being validated, per RFC 5280 4.2.1.12. + * + * A CA restricted to some other purpose, code signing here, must not be able to + * authenticate a TLS peer even though the leaf below it asks for serverAuth. + * An absent extension and anyExtendedKeyUsage both leave every purpose valid + * and must still complete the handshake, and a self-signed trust anchor is + * exempt whatever its Extended Key Usage says. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_chain_ca_ext_key_usage(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ + !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ + defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ + defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) + static const test_eku_case cases[] = { + /* Server authentication: a CA restricted to code signing, or to + * client authentication, must be refused. */ + { "codeSigning", 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "clientAuth", 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + /* A CA that carries the purpose, or leaves it unrestricted, must not + * be over-rejected. */ + { "serverAuth", 0, 0, 0, 0 }, + { NULL, 0, 0, 0, 0 }, + { "any", 0, 0, 0, 0 }, + /* The rule applies to a CA the certificate manager already holds, not + * only to one seen for the first time. */ + { "codeSigning", 0, 0, 1, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "serverAuth", 0, 0, 1, 0 }, + /* A self-signed trust anchor is exempt: here it is the selfSigned + * test, not the Extended Key Usage, that decides. */ + { "codeSigning", 1, 0, 0, 0 }, +#ifndef WOLFSSL_NO_CLIENT_AUTH + /* Client authentication: the server applies the same rule with the + * clientAuth purpose. */ + { "codeSigning", 0, 1, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "serverAuth", 0, 1, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "clientAuth", 0, 1, 0, 0 }, + { NULL, 0, 1, 0, 0 }, + { "any", 0, 1, 0, 0 }, +#endif + }; + test_eku_fixture fixture; + WC_RNG rng; + RsaKey caKey; + RsaKey intKey; + RsaKey leafKey; + int rngInit = 0; + int caInit = 0; + int intInit = 0; + int leafInit = 0; + word32 idx; + size_t i; + + XMEMSET(&fixture, 0, sizeof(fixture)); + ExpectNotNull(fixture.interDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + ExpectNotNull(fixture.chainDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); + + ExpectIntEQ(wc_InitRng(&rng), 0); + if (EXPECT_SUCCESS()) rngInit = 1; + ExpectIntEQ(wc_InitRsaKey(&caKey, NULL), 0); + if (EXPECT_SUCCESS()) caInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(ca_key_der_2048, &idx, &caKey, + (word32)sizeof_ca_key_der_2048), 0); + ExpectIntEQ(wc_InitRsaKey(&intKey, NULL), 0); + if (EXPECT_SUCCESS()) intInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(server_key_der_2048, &idx, &intKey, + (word32)sizeof_server_key_der_2048), 0); + ExpectIntEQ(wc_InitRsaKey(&leafKey, NULL), 0); + if (EXPECT_SUCCESS()) leafInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(client_key_der_2048, &idx, &leafKey, + (word32)sizeof_client_key_der_2048), 0); + + fixture.caKey = &caKey; + fixture.intKey = &intKey; + fixture.leafKey = &leafKey; + fixture.rng = &rng; + + for (i = 0; i < XELEM_CNT(cases) && EXPECT_SUCCESS(); i++) { + ExpectIntEQ(test_eku_chain_case(&fixture, &cases[i]), TEST_SUCCESS); + } + + if (rngInit) wc_FreeRng(&rng); + if (caInit) wc_FreeRsaKey(&caKey); + if (intInit) wc_FreeRsaKey(&intKey); + if (leafInit) wc_FreeRsaKey(&leafKey); + XFREE(fixture.interDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(fixture.chainDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index c57cfef820..07f3bc4e75 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -47,6 +47,7 @@ int test_wolfSSL_CTX_cert_store_manager_link(void); int test_wolfSSL_cert_cb_ctx(void); int test_wolfSSL_get_certificate_api(void); int test_wolfSSL_cert_unload(void); +int test_wolfSSL_chain_ca_ext_key_usage(void); #define TEST_SSL_CERT_DECLS \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_get_verify_mode), \ @@ -75,6 +76,8 @@ int test_wolfSSL_cert_unload(void); test_wolfSSL_CTX_cert_store_manager_link), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_cb_ctx), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_get_certificate_api), \ - TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_unload) + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_cert_unload), \ + TEST_DECL_GROUP("ssl_cert", \ + test_wolfSSL_chain_ca_ext_key_usage) #endif /* TESTS_API_SSL_CERT_H */ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 934a98cb6d..86adbfcfb8 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -169,10 +169,12 @@ ASN Options: * IGNORE_KEY_EXTENSIONS: Opt-in, RFC non-conformant. Suppress all key-usage * and extended-key-usage enforcement: the TLS * keyEncipherment/digitalSignature checks and the - * serverAuth/clientAuth EKU checks (ProcessPeerCerts, - * src/internal.c), and the cRLSign requirement on a - * CRL-signing CA (VerifyCRL_Signature, below). Off by - * default; enforcement is active in a stock build. + * serverAuth/clientAuth EKU checks on both the peer + * certificate and the chain-supplied intermediate + * CAs (ProcessPeerCerts, src/internal.c), and the + * cRLSign requirement on a CRL-signing CA + * (VerifyCRL_Signature, below). Off by default; + * enforcement is active in a stock build. * IGNORE_NETSCAPE_CERT_TYPE: Ignore Netscape cert type extension * WOLFSSL_ALLOW_CRIT_AIA: Allow critical Authority Info Access * WOLFSSL_ALLOW_CRIT_AKID: Allow critical Auth Key Identifier diff --git a/wolfssl/ssl.h b/wolfssl/ssl.h index 88b28b9f28..4462ac1682 100644 --- a/wolfssl/ssl.h +++ b/wolfssl/ssl.h @@ -2827,6 +2827,7 @@ enum { WOLFSSL_X509_V_ERR_CERT_CHAIN_TOO_LONG = 22, WOLFSSL_X509_V_ERR_CERT_REVOKED = 23, WOLFSSL_X509_V_ERR_PATH_LENGTH_EXCEEDED = 25, + WOLFSSL_X509_V_ERR_INVALID_PURPOSE = 26, WOLFSSL_X509_V_ERR_CERT_REJECTED = 28, WOLFSSL_X509_V_ERR_SUBJECT_ISSUER_MISMATCH = 29, WOLFSSL_X509_V_ERR_APPLICATION_VERIFICATION = 50, From 79db942e62efcf34d0e510e682c3e669aad2cfd3 Mon Sep 17 00:00:00 2001 From: Eric Blankenhorn Date: Wed, 12 Aug 2026 08:06:09 -0500 Subject: [PATCH 2/2] Fix from review --- src/internal.c | 21 +++-- tests/api/test_ssl_cert.c | 172 ++++++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 61 deletions(-) diff --git a/src/internal.c b/src/internal.c index 34ab3a5654..d79d94a90e 100644 --- a/src/internal.c +++ b/src/internal.c @@ -18199,14 +18199,21 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, #endif } #endif /* IGNORE_KEY_EXTENSIONS */ - #ifdef OPENSSL_EXTRA - if (args->certIdx > args->untrustedDepth) { - args->untrustedDepth = (char)args->certIdx + 1; - } - #endif + /* A CA turned away above is neither part of the + * verified chain nor something to report as verified, + * so leave the depth and the log to the accepted + * case. */ + if (ret == 0) { + #ifdef OPENSSL_EXTRA + if (args->certIdx > args->untrustedDepth) { + args->untrustedDepth = (char)args->certIdx + 1; + } + #endif - if (alreadySigner) { - WOLFSSL_MSG("Verified CA from chain and already had it"); + if (alreadySigner) { + WOLFSSL_MSG("Verified CA from chain and " + "already had it"); + } } } else { diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index d42ef136b1..11a0de201b 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -1507,31 +1507,47 @@ int test_wolfSSL_cert_unload(void) /* Keys and scratch buffers shared by every case. */ typedef struct test_eku_fixture { - RsaKey* caKey; - RsaKey* intKey; + RsaKey* caKey; /* the 2048-bit test root */ + RsaKey* midKey; /* CA between the root and the CA under test */ + RsaKey* intKey; /* the CA whose Extended Key Usage is under test */ RsaKey* leafKey; WC_RNG* rng; + byte* midDer; byte* interDer; byte* chainDer; } test_eku_fixture; /* One chain Extended Key Usage scenario. */ typedef struct test_eku_case { - const char* extKeyUsage; /* Extended Key Usage on the issuing CA, NULL - * omits the extension */ - int selfSignedCa; /* issuing CA is self-signed and is itself the - * trust anchor */ + const char* extKeyUsage; /* Extended Key Usage on the CA under test, + * NULL omits the extension */ + int selfSignedCa; /* CA under test is self-signed and is itself + * the trust anchor */ int clientPresents; /* client sends the chain, server verifies it */ - int pinCa; /* issuing CA is also loaded as a trusted CA */ + int pinCa; /* CA under test is also loaded as a trusted + * CA before the handshake */ + int extraLevel; /* insert a CA between the root and the CA + * under test, putting it at depth 2 */ + int overrideCb; /* verify callback accepts what the library + * rejected */ int expectRet; /* expected handshake result */ } test_eku_case; -/* Build a CA:TRUE certificate for the given key. Unless selfSign is set it is - * signed by the 2048-bit test root (ca_cert_der_2048 / ca_key_der_2048). A NULL - * extKeyUsage omits the Extended Key Usage extension entirely. Returns the DER - * length, or < 0 on failure. */ +/* Stand in for an application that knowingly accepts a rejected certificate. */ +static int test_eku_override_cb(int preverify, WOLFSSL_X509_STORE_CTX* store) +{ + (void)preverify; + (void)store; + return 1; +} + +/* 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. Returns the + * DER length, or < 0 on failure. */ static int test_eku_gen_ca(byte* out, int outMax, RsaKey* subjKey, - RsaKey* caKey, WC_RNG* rng, const char* extKeyUsage, int selfSign) + const byte* issuerDer, int issuerDerSz, RsaKey* issuerKey, WC_RNG* rng, + const char* extKeyUsage, const char* cn) { Cert cert; int ret = 0; @@ -1542,9 +1558,7 @@ static int test_eku_gen_ca(byte* out, int outMax, RsaKey* subjKey, cert.sigType = CTC_SHA256wRSA; XSTRNCPY(cert.subject.country, "US", CTC_NAME_SIZE - 1); XSTRNCPY(cert.subject.org, "wolfSSL_test", CTC_NAME_SIZE - 1); - XSTRNCPY(cert.subject.commonName, - selfSign ? "EKU Self Signed CA" : "EKU Intermediate", - CTC_NAME_SIZE - 1); + XSTRNCPY(cert.subject.commonName, cn, CTC_NAME_SIZE - 1); if (wc_SetSubjectKeyIdFromPublicKey(&cert, subjKey, NULL) != 0) ret = -1; if (ret == 0 && wc_SetKeyUsage(&cert, "keyCertSign,cRLSign") != 0) @@ -1554,19 +1568,17 @@ static int test_eku_gen_ca(byte* out, int outMax, RsaKey* subjKey, ret = -1; /* wc_InitCert() leaves selfSigned set, so naming no issuer is what makes * the generated certificate self-signed. */ - if (ret == 0 && !selfSign) { - if (wc_SetAuthKeyIdFromCert(&cert, ca_cert_der_2048, - (int)sizeof_ca_cert_der_2048) != 0) + if (ret == 0 && issuerDer != NULL) { + if (wc_SetAuthKeyIdFromCert(&cert, issuerDer, issuerDerSz) != 0) ret = -1; - if (ret == 0 && wc_SetIssuerBuffer(&cert, ca_cert_der_2048, - (int)sizeof_ca_cert_der_2048) != 0) + if (ret == 0 && wc_SetIssuerBuffer(&cert, issuerDer, issuerDerSz) != 0) ret = -1; } if (ret == 0) ret = wc_MakeCert(&cert, out, (word32)outMax, subjKey, NULL, rng); if (ret >= 0) ret = wc_SignCert(cert.bodySz, cert.sigType, out, (word32)outMax, - selfSign ? subjKey : caKey, NULL, rng); + (issuerDer == NULL) ? subjKey : issuerKey, NULL, rng); #ifdef WOLFSSL_CERT_GEN_CACHE wc_SetCert_Free(&cert); #endif @@ -1610,16 +1622,16 @@ static int test_eku_gen_leaf(byte* out, int outMax, RsaKey* leafKey, return ret; } -/* Run a memio handshake in which one side presents "leaf <- CA" and the peer - * verifies that chain against rootDer. When clientPresents is set the client - * sends the chain and the server verifies it, which is the client - * authentication direction. A non-NULL pinnedDer is loaded as an additional - * trusted CA on the verifying side, so the chain CA is already a known signer - * when it arrives. The handshake result and the compatibility-layer verify - * result are returned through hsRet and verifyRet. */ +/* Run a memio handshake in which one side presents the given chain and the + * peer verifies it against rootDer. When clientPresents is set the client sends + * the chain and the server verifies it, which is the client authentication + * direction. A non-NULL pinnedDer is loaded as an additional trusted CA on the + * verifying side, so the chain CA is already a known signer when it arrives. + * The handshake result and the compatibility-layer verify result are returned + * 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, - int clientPresents, int* hsRet, long* verifyRet) + const test_eku_case* tc, int* hsRet, long* verifyRet) { EXPECT_DECLS; WOLFSSL_CTX* ctx_c = NULL; @@ -1628,8 +1640,12 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, WOLFSSL* ssl_c = NULL; WOLFSSL* ssl_s = NULL; WOLFSSL* verifySsl = NULL; + VerifyCallback verifyCb = NULL; struct test_memio_ctx test_ctx; + if (tc->overrideCb) + verifyCb = test_eku_override_cb; + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); /* The server always presents the generated chain so a single set of * credentials covers both directions. */ @@ -1638,7 +1654,7 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, (byte*)rootDer, rootSz, (byte*)chainDer, chainSz, (byte*)client_key_der_2048, (int)sizeof_client_key_der_2048), 0); - if (clientPresents) { + if (tc->clientPresents) { /* Leave the server chain unverified so only the server's view of the * client chain decides the handshake. */ wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_NONE, NULL); @@ -1650,12 +1666,12 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, ExpectIntEQ(wolfSSL_CTX_load_verify_buffer(ctx_s, rootDer, (long)rootSz, WOLFSSL_FILETYPE_ASN1), WOLFSSL_SUCCESS); wolfSSL_set_verify(ssl_s, WOLFSSL_VERIFY_PEER | - WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL); + WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT, verifyCb); verifyCtx = ctx_s; verifySsl = ssl_s; } else { - wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, NULL); + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, verifyCb); verifyCtx = ctx_c; verifySsl = ssl_c; } @@ -1686,41 +1702,68 @@ static int test_eku_chain_handshake(const byte* chainDer, int chainSz, return EXPECT_RESULT(); } -/* Build "leaf <- CA" for one scenario, run the handshake and check both the +/* Build the chain for one scenario, run the handshake and check both the * handshake result and the verify result it is reported as. */ static int test_eku_chain_case(const test_eku_fixture* f, const test_eku_case* tc) { EXPECT_DECLS; const byte* rootDer = ca_cert_der_2048; + const byte* issuerDer = ca_cert_der_2048; int rootSz = (int)sizeof_ca_cert_der_2048; + int issuerSz = (int)sizeof_ca_cert_der_2048; + RsaKey* issuerKey = f->caKey; + int midSz = 0; int caSz = 0; int leafSz = 0; + int chainSz = 0; int hsRet = -1; long verifyRet = -1; + /* An extra level puts the CA under test at depth 2, below a CA that does + * carry the TLS purposes. */ + if (tc->extraLevel) { + ExpectIntGT((midSz = test_eku_gen_ca(f->midDer, TEST_EKU_CERT_BUF_SZ, + f->midKey, ca_cert_der_2048, (int)sizeof_ca_cert_der_2048, + f->caKey, f->rng, "serverAuth,clientAuth", "EKU Middle CA")), 0); + issuerDer = f->midDer; + issuerSz = midSz; + issuerKey = f->midKey; + } + ExpectIntGT((caSz = test_eku_gen_ca(f->interDer, TEST_EKU_CERT_BUF_SZ, - f->intKey, f->caKey, f->rng, tc->extKeyUsage, tc->selfSignedCa)), 0); + f->intKey, tc->selfSignedCa ? NULL : issuerDer, issuerSz, issuerKey, + f->rng, tc->extKeyUsage, + tc->selfSignedCa ? "EKU Self Signed CA" : "EKU Intermediate")), 0); ExpectIntGT((leafSz = test_eku_gen_leaf(f->chainDer, TEST_EKU_CERT_BUF_SZ, f->leafKey, f->interDer, caSz, f->intKey, f->rng)), 0); - /* The chain buffer holds the leaf first, then its issuer. */ - ExpectIntLE(leafSz + caSz, TEST_EKU_CERT_BUF_SZ); + /* The chain buffer holds the leaf first, then its issuers in order. */ + ExpectIntLE(leafSz + caSz + midSz, TEST_EKU_CERT_BUF_SZ); if (EXPECT_SUCCESS()) { XMEMCPY(f->chainDer + leafSz, f->interDer, (size_t)caSz); + chainSz = leafSz + caSz; + if (tc->extraLevel) { + XMEMCPY(f->chainDer + chainSz, f->midDer, (size_t)midSz); + chainSz += midSz; + } if (tc->selfSignedCa) { rootDer = f->interDer; rootSz = caSz; } - ExpectIntEQ(test_eku_chain_handshake(f->chainDer, leafSz + caSz, - rootDer, rootSz, tc->pinCa ? f->interDer : NULL, caSz, - tc->clientPresents, &hsRet, &verifyRet), TEST_SUCCESS); + ExpectIntEQ(test_eku_chain_handshake(f->chainDer, chainSz, + rootDer, rootSz, tc->pinCa ? f->interDer : NULL, caSz, tc, + &hsRet, &verifyRet), TEST_SUCCESS); } ExpectIntEQ(hsRet, tc->expectRet); #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) - ExpectIntEQ(verifyRet, (tc->expectRet == 0) ? - WOLFSSL_X509_V_OK : WOLFSSL_X509_V_ERR_INVALID_PURPOSE); + /* An override leaves the rejection already recorded in place, so only the + * cases the library decided on its own have a predictable verify result. */ + if (!tc->overrideCb) { + ExpectIntEQ(verifyRet, (tc->expectRet == 0) ? + WOLFSSL_X509_V_OK : WOLFSSL_X509_V_ERR_INVALID_PURPOSE); + } #endif return EXPECT_RESULT(); @@ -1746,45 +1789,58 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) defined(WOLFSSL_CERT_GEN) && defined(WOLFSSL_CERT_EXT) && \ defined(USE_CERT_BUFFERS_2048) && !defined(IGNORE_KEY_EXTENSIONS) static const test_eku_case cases[] = { + /* eku, selfSigned, clientPresents, pinCa, extraLevel, overrideCb, + * expected handshake result. */ + /* Server authentication: a CA restricted to code signing, or to * client authentication, must be refused. */ - { "codeSigning", 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, - { "clientAuth", 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "codeSigning", 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "clientAuth", 0, 0, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, /* A CA that carries the purpose, or leaves it unrestricted, must not * be over-rejected. */ - { "serverAuth", 0, 0, 0, 0 }, - { NULL, 0, 0, 0, 0 }, - { "any", 0, 0, 0, 0 }, + { "serverAuth", 0, 0, 0, 0, 0, 0 }, + { NULL, 0, 0, 0, 0, 0, 0 }, + { "any", 0, 0, 0, 0, 0, 0 }, /* The rule applies to a CA the certificate manager already holds, not * only to one seen for the first time. */ - { "codeSigning", 0, 0, 1, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, - { "serverAuth", 0, 0, 1, 0 }, + { "codeSigning", 0, 0, 1, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "serverAuth", 0, 0, 1, 0, 0, 0 }, /* A self-signed trust anchor is exempt: here it is the selfSigned * test, not the Extended Key Usage, that decides. */ - { "codeSigning", 1, 0, 0, 0 }, + { "codeSigning", 1, 0, 0, 0, 0, 0 }, + /* Enforcement is not limited to the CA directly above the leaf. */ + { "codeSigning", 0, 0, 0, 1, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "serverAuth", 0, 0, 0, 1, 0, 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, 1, 0 }, #ifndef WOLFSSL_NO_CLIENT_AUTH /* Client authentication: the server applies the same rule with the * clientAuth purpose. */ - { "codeSigning", 0, 1, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, - { "serverAuth", 0, 1, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, - { "clientAuth", 0, 1, 0, 0 }, - { NULL, 0, 1, 0, 0 }, - { "any", 0, 1, 0, 0 }, + { "codeSigning", 0, 1, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "serverAuth", 0, 1, 0, 0, 0, WC_NO_ERR_TRACE(EXTKEYUSE_AUTH_E) }, + { "clientAuth", 0, 1, 0, 0, 0, 0 }, + { NULL, 0, 1, 0, 0, 0, 0 }, + { "any", 0, 1, 0, 0, 0, 0 }, #endif }; test_eku_fixture fixture; WC_RNG rng; RsaKey caKey; + RsaKey midKey; RsaKey intKey; RsaKey leafKey; int rngInit = 0; int caInit = 0; + int midInit = 0; int intInit = 0; int leafInit = 0; word32 idx; size_t i; XMEMSET(&fixture, 0, sizeof(fixture)); + ExpectNotNull(fixture.midDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, + DYNAMIC_TYPE_TMP_BUFFER)); ExpectNotNull(fixture.interDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, DYNAMIC_TYPE_TMP_BUFFER)); ExpectNotNull(fixture.chainDer = (byte*)XMALLOC(TEST_EKU_CERT_BUF_SZ, NULL, @@ -1797,6 +1853,11 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) idx = 0; ExpectIntEQ(wc_RsaPrivateKeyDecode(ca_key_der_2048, &idx, &caKey, (word32)sizeof_ca_key_der_2048), 0); + ExpectIntEQ(wc_InitRsaKey(&midKey, NULL), 0); + if (EXPECT_SUCCESS()) midInit = 1; + idx = 0; + ExpectIntEQ(wc_RsaPrivateKeyDecode(rsa_key_der_2048, &idx, &midKey, + (word32)sizeof_rsa_key_der_2048), 0); ExpectIntEQ(wc_InitRsaKey(&intKey, NULL), 0); if (EXPECT_SUCCESS()) intInit = 1; idx = 0; @@ -1809,6 +1870,7 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) (word32)sizeof_client_key_der_2048), 0); fixture.caKey = &caKey; + fixture.midKey = &midKey; fixture.intKey = &intKey; fixture.leafKey = &leafKey; fixture.rng = &rng; @@ -1819,8 +1881,10 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) if (rngInit) wc_FreeRng(&rng); if (caInit) wc_FreeRsaKey(&caKey); + if (midInit) wc_FreeRsaKey(&midKey); if (intInit) wc_FreeRsaKey(&intKey); if (leafInit) wc_FreeRsaKey(&leafKey); + XFREE(fixture.midDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); XFREE(fixture.interDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); XFREE(fixture.chainDer, NULL, DYNAMIC_TYPE_TMP_BUFFER); #endif