Skip to content

Check CA ExtKeyUsage for issuers from the CertManager - #11873

Open
sameehj wants to merge 5 commits into
wolfSSL:masterfrom
sameehj:fix-ca-ext-key-usage-certmanager
Open

sameehj wants to merge 5 commits into
wolfSSL:masterfrom
sameehj:fix-ca-ext-key-usage-certmanager

Conversation

@sameehj

@sameehj sameehj commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Description

ProcessPeerCerts() checks the Extended Key Usage of CA certificates against the TLS purpose (serverAuth when verifying a server, clientAuth when verifying a client), but only for CAs the peer sends in its Certificate message. When the peer sends only its leaf and the issuing CA is already loaded in the CertManager, that CA's Extended Key Usage is not checked. The same chain is therefore rejected with EXTKEYUSE_AUTH_E when the intermediate is sent and accepted when it is omitted.

This change makes the result independent of which CAs the peer sends:

  • Signer gains an extExtKeyUsageSet bit, set by FillSigner(), so an absent extension can be told apart from one that grants none of the known purposes.
  • CheckSignerExtKeyUsage() walks from the leaf's issuer up to the self-signed trust anchor and applies the TLS purpose to every CA on the way. The trust anchor stays exempt, as in CheckChainCAExtKeyUsage(). The walk reuses the name constraints ancestor lookup, now exported as wolfssl_local_FindSignerByAkidOrName(), with the same cycle guard and WOLFSSL_MAX_CHAIN_DEPTH limit.
  • The check runs after the leaf's own key usage checks. A verify callback can override it as it can the other leaf errors. It is skipped when a chain verify callback is installed, when verification is disabled, and for a trusted peer certificate (WOLFSSL_TRUST_PEER_CERT), whose chain is not used.
  • With IGNORE_NAME_CONSTRAINTS the Signer has no issuer lookup data, so only the leaf's direct issuer is checked.
  • With Certificate Status Request v2 multi-stapling, the walk also looks up issuers in the pending signer list from TLSX_CSR2_GetPendingSigners(), the same list ProcessPeerCertParse() uses. A pending issuer does not stop the walk before a cached ancestor.
  • When the verify callback accepts EXTKEYUSE_AUTH_E for a CA that the peer sent, the handshake records that chain index in a mask in ProcPeerCertArgs. The walk skips that CA and still checks the ancestors that the peer did not send. The shared Signer is not changed.

This change does not change the persisted cert cache. #11875 rebuilds each CA from its DER, so a restored CA keeps its Extended Key Usage. Merge this change after #11875.

Testing

test_wolfSSL_chain_ca_ext_key_usage gains a leafOnly option, where the peer sends only the leaf and the CAs are loaded into the verifier. New cases:

  • depth 1: codeSigning and clientAuth CA rejected, and serverAuth, no extension and anyExtendedKeyUsage accepted, when verifying a server
  • depth 1: codeSigning and serverAuth CA rejected, and clientAuth and no extension accepted, when verifying a client
  • depth 2, with the restricted CA above an unrestricted one and neither sent, for server and client authentication
  • a verify callback overriding the result for a leaf-only chain, and one that accepts only EXTKEYUSE_AUTH_E, showing that the callback is given that error
  • a self-signed codeSigning trust anchor with a leaf-only chain, which stays accepted
  • a verify callback that accepts the purpose error only at depth 1: the handshake succeeds, and the same callback still rejects a codeSigning CA that the peer did not send

Built and run with the default configure flags plus cert generation, and with each of OPENSSL_EXTRA, OPENSSL_EXTRA + WOLFSSL_CHAIN_VERIFY_CB, IGNORE_NAME_CONSTRAINTS, WOLFSSL_ASN_ORIGINAL, WOLFSSL_ALT_CERT_CHAINS with CRL/OCSP and status request, WOLFSSL_TRUST_PEER_CERT, and NO_SKID. A trusted peer leaf whose issuer name matches a restricted CA in the CertManager still connects with the example client (-E). The new cases fail without the library change. The full --api suite passes in the default and OPENSSL_EXTRA builds.

NO_SKID note: the existing self-issued rollover cases in this test already fail on master with NO_SKID (ASN_SIG_CONFIRM_E); this change does not affect them.

How to reproduce

Build and run the test:

./autogen.sh
./configure --enable-certgen --enable-certext
make tests/unit.test
./tests/unit.test -test_wolfSSL_chain_ca_ext_key_usage

The test reports Failed/Skipped/Passed/All: 0/0/1/1.

To see the bug, keep the new test cases and put back the library files from master (origin is the wolfSSL/wolfssl remote):

base=$(git merge-base HEAD origin/master)
git checkout "$base" -- src/internal.c wolfcrypt/src/asn.c wolfssl/internal.h wolfssl/wolfcrypt/asn.h
make tests/unit.test
./tests/unit.test -test_wolfSSL_chain_ca_ext_key_usage
git checkout HEAD -- src/internal.c wolfcrypt/src/asn.c wolfssl/internal.h wolfssl/wolfcrypt/asn.h

The test reports Failed/Skipped/Passed/All: 1/0/0/1 with:

ERROR - tests/api/test_ssl_cert.c line 2919 failed with:
    expected: hsRet == tc->expectRet
    result:   0 != -386

The leaf-only handshake with a restricted CA in the CertManager succeeds, but the test expects EXTKEYUSE_AUTH_E (-386).

Tested on macOS arm64 (Apple clang) at 390159a.

Checklist

  • added tests
  • updated/added doxygen
  • updated appropriate READMEs
  • Updated manual and documentation

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.
Copilot AI balanced review requested due to automatic review settings October 23, 2026 10:37
@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

gcc-arm-cortex-m0plus

  • FLASH: .text +120 B (+0.2%, 70,547 B / 262,144 B, total: 27% used)

gcc-arm-cortex-m3

  • FLASH: .text +352 B (+0.3%, 131,069 B / 262,144 B, total: 50% used)

gcc-arm-cortex-m4

  • FLASH: .text +384 B (-1.1%, 207,472 B / 262,144 B, total: 79% used)

gcc-arm-cortex-m4-baremetal

  • FLASH: .text +128 B (+0.2%, 73,079 B / 262,144 B, total: 28% used)

gcc-arm-cortex-m4-crypto-only

  • FLASH: .text +256 B (-1.4%, 179,859 B / 262,144 B, total: 69% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text +384 B (+0.2%, 194,584 B / 1,048,576 B, total: 19% used)

gcc-arm-cortex-m4-min-ecc

  • FLASH: .text +128 B (+0.2%, 67,737 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata -2,760 B, .text +1,728 B (-0.1%, 793,884 B / 1,048,576 B, total: 76% used)

gcc-arm-cortex-m4-pkcs7

  • FLASH: .text +192 B (-1.2%, 220,884 B / 262,144 B, total: 84% used)

gcc-arm-cortex-m4-pq

  • FLASH: .rodata -2,772 B, .text +448 B (-0.7%, 309,328 B / 1,048,576 B, total: 29% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .rodata -2,768 B, .text +384 B (-0.7%, 340,616 B / 1,048,576 B, total: 32% used)

gcc-arm-cortex-m4-sp-math

  • FLASH: .text +128 B (+0.2%, 67,737 B / 262,144 B, total: 26% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +384 B (+0.3%, 131,857 B / 262,144 B, total: 50% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +448 B (-0.9%, 246,818 B / 262,144 B, total: 94% used)

gcc-arm-cortex-m7

  • FLASH: .text +384 B (-1.1%, 207,472 B / 262,144 B, total: 79% used)

gcc-arm-cortex-m7-pq

  • FLASH: .rodata -2,772 B, .text +448 B (-0.7%, 310,224 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text +384 B (-1.0%, 246,818 B / 262,144 B, total: 94% used)

stm32-sim-stm32h753

  • FLASH: .text -2,308 B (-1.1%, 203,116 B / 2,097,152 B, total: 10% used)

zephyr-frdm_rw612

  • FLASH: rodata +100 B, text +256 B (+0.1%, 282,600 B / 67,108,864 B, total: 0% used)

Catch memory regressions in CI with MemBrowse. Free for open source.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Pending signers and restored caches can bypass issuer EKU checks, and repeated checks can invalidate certificate-specific callback overrides.

3 open issues
What changed in this PR

This PR extends TLS issuer Extended Key Usage checks to CAs loaded in the CertManager, aiming to make validation independent of which intermediates the peer sends.

Changes:

  • Records EKU extension presence on cached signers.
  • Walks issuer ancestors to enforce the TLS purpose while exempting self-signed trust anchors.
  • Adds leaf-only chain tests for server/client authentication, callback overrides and trust-anchor handling.
File Description
wolfssl/​wolfcrypt/​asn.h Adds signer EKU presence state and declares the shared issuer lookup.
wolfcrypt/​src/​asn.c Populates the new state and exposes the existing ancestor lookup.
tests/​api/​test_ssl_cert.c Extends the handshake harness and adds cached-issuer test cases.
src/​internal.c Adds issuer-chain EKU validation during peer certificate processing.

Review effort: Balanced

Comment thread src/internal.c Outdated
return EXTKEYUSE_AUTH_E;
}
#ifndef IGNORE_NAME_CONSTRAINTS
parent = wolfssl_local_FindSignerByAkidOrName(SSL_CM(ssl), NULL, ca);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 286b560. When Certificate Status Request v2 multi-stapling is in use, the issuer walk passes TLSX_CSR2_GetPendingSigners() to wolfssl_local_FindSignerByAkidOrName(). That is the same list ProcessPeerCertParse() uses. A pending issuer no longer stops the walk before a cached ancestor.

Comment thread wolfssl/wolfcrypt/asn.h
byte extKeyUsage;
word16 maxPathLen;
WC_BITFIELD selfSigned:1; /* Own key verified the signature */
WC_BITFIELD extExtKeyUsageSet:1; /* EKU extension present */

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This pull request does not change the cache layout. Pull request 11875 rebuilds each CA from its DER, and FillSigner() already copies extExtKeyUsageSet and extKeyUsage. A second cache version here would conflict with that restore. This change should land after 11875, or be rebased onto it, so a restored codeSigning CA keeps the restriction.

Comment thread src/internal.c Outdated
!args->haveTrustPeer &&
#endif
!UsingChainVerifyCb(ssl)) {
ret = CheckSignerExtKeyUsage(ssl, args->dCert->ca);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in 286b560. When the verify callback accepts EXTKEYUSE_AUTH_E for a chain CA, the handshake records that CA's subject-name hash. CheckSignerExtKeyUsage() skips that CA and still checks ancestors that were not presented. The shared Signer is not changed.

The new test accepts the purpose error only at depth 1, and the handshake succeeds. The same callback still rejects a codeSigning CA that the peer did not send.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

78810b7 replaces the subject-hash table with a chain-index mask in the existing flag word.

The table made ProcPeerCertArgs 344 bytes. ssl->async->args is 96 bytes, so the async builds failed the size assert, including WOLFSSL_STATIC_MEMORY. Passing byte[][N] to a const byte[][N] parameter also failed -Wpedantic. The mask records the same accepted chain indexes and leaves the shared Signer unchanged. In the async user_settings build, ProcPeerCertArgs is 56 bytes.

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.
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.
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.
clang-tidy readability-uppercase-literal-suffix rejected the lowercase
1u literals in RememberWaivedCaEku() and CheckSignerExtKeyUsage().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants