Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
79 changes: 73 additions & 6 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -16965,6 +16965,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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] Purpose check rejects extraneous chain certificates that are not in the validated path · Cryptographic correctness

The check is applied to every CA the peer transmits, not only to certificates in the leaf's certification path. A peer that bundles an extraneous but locally verifiable CA (timestamping or OCSP-signing sub-CA of the same root) now gets a fatal bad_certificate alert, although RFC 8446 4.4.2 permits extraneous certificates.

Related known finding #5814 (similar but distinct): Both concern certificate usage-constraint enforcement during peer processing, but #5814 omits leaf client keyUsage validation for static-RSA suites, while this applies CA extended-key-usage validation to extraneous transmitted certificates. They involve different certificate roles, operations, root causes, and fixes.

Fix: Limit enforcement to certificates on the leaf's issuer path, or set skipAddCA for an extraneous purpose-mismatched CA instead of failing the handshake.

}

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. */
Expand Down Expand Up @@ -17016,6 +17050,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;
Expand Down Expand Up @@ -18093,14 +18132,39 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx,
"not adding as CA");
}
else if (ret == 0) {
#ifdef OPENSSL_EXTRA
if (args->certIdx > args->untrustedDepth) {
args->untrustedDepth = (char)args->certIdx + 1;
#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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 [Low] Chain CA purpose check applies only to transmitted CAs, evadable by omitting the intermediate · Certificate and trust chain validation bypass

CheckChainCAExtKeyUsage() runs only inside the transmitted-chain loop. A peer that omits a purpose-constrained intermediate the verifier can already resolve from its certificate manager (operator-loaded, or cached earlier by AddCA(..., WOLFSSL_CHAIN_CA, ...)) gets the leaf accepted with no purpose check.

Related known finding #1814 (similar but distinct): Both concern extended-key-usage enforcement in ProcessPeerCerts, but #1814 suppresses peer usage checks under OPENSSL_EXTRA plus verifyNone, whereas this check is skipped when an intermediate is locally resolved rather than transmitted. The faulting operation, root cause, and required patch differ.

Fix: Enforce the purpose on the Signer resolved during leaf validation too, adding an "EKU present" sentinel to Signer.extKeyUsage as Signer.keyUsage already uses 0xFFFF.

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
#endif /* IGNORE_KEY_EXTENSIONS */
/* 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 {
Expand Down Expand Up @@ -29457,6 +29521,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";

Expand Down
2 changes: 2 additions & 0 deletions src/x509_str.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
/* <e> 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
Expand Down
2 changes: 1 addition & 1 deletion tests/api.c
Original file line number Diff line number Diff line change
Expand Up @@ -31269,7 +31269,7 @@ static int error_test(void)
{17, 15},
{19, 19},
{24, 24},
{27, 26 },
{27, 27},
{61, 30},
{63, 63},
{78, 65},
Expand Down
Loading
Loading