Skip to content

Check the dates of cached chain CAs when they are used - #11942

Open
sameehj wants to merge 1 commit into
wolfSSL:masterfrom
sameehj:fix-chain-ca-expired-signer
Open

sameehj wants to merge 1 commit into
wolfSSL:masterfrom
sameehj:fix-chain-ca-expired-signer

Conversation

@sameehj

@sameehj sameehj commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Description

After a handshake, ProcessPeerCerts() adds each verified CA from the peer's chain to the certificate manager as a WOLFSSL_CHAIN_CA. The cached Signer kept no validity dates. A later peer that sent only its leaf was then verified with the cached CA, and only the leaf's own dates were checked. The same chain was rejected with ASN_AFTER_DATE_E when the expired CA was sent, or when a new context was used, and accepted when the CA was omitted.

This change makes the result independent of whether the CA is sent:

  • Signer gains a validity pointer that holds the DER encoded notBefore and notAfter. It is allocated only for a WOLFSSL_CHAIN_CA, so CAs the application loads cost one pointer each.
  • wolfssl_local_CheckChainCADates() walks from the issuer of each peer certificate up to the self-signed trust anchor and checks the dates of every chain CA on the way. It runs in ProcessPeerCertParse() after the certificate is parsed, so the error is reported as ASN_BEFORE_DATE_E or ASN_AFTER_DATE_E on that certificate and goes through the verify callback like any other date error. wc_AsnSetSkipDateCheck() turns it off as it does the other date checks.
  • CAs the application loaded (WOLFSSL_USER_CA, WOLFSSL_USER_INTER) are trust anchors and are not checked again, as before. A chain CA retyped with wolfSSL_CertManagerLoadCABufferType() follows its new type.
  • The walk reuses the name constraints issuer lookup, now exported as wolfssl_local_FindSignerByAkidOrName(), with the same cycle guard and WOLFSSL_MAX_CHAIN_DEPTH limit. With IGNORE_NAME_CONSTRAINTS only the direct issuer is checked. Certificate Status Request v2 pending signers are searched too.
  • A CA renewed with the same key has the same subject key identifier, so AlreadySigner() keeps the old cached copy. When the peer sends a CA that is already cached as a chain CA with the same public key and it verifies, wolfssl_local_SetChainCADates() copies its dates onto the cached CA. Without this, a client whose cached CA expired would also reject a peer that sends a valid renewed CA.
  • The dates are read and written under caLock, because another handshake can update them.
  • wolfSSL_CertManagerVerifyBuffer() checks cached chain CAs the same way. wolfSSL_CertManagerLoadCABufferType() stores the dates of a CA loaded as WOLFSSL_CHAIN_CA, for example one saved through the CA cache callback.

Notes for review:

Testing

New test_wolfSSL_chain_ca_cached_dates generates a CA that expires before its leaf and moves the clock with wc_SetTimeCb(). One client context is reused across handshakes:

  • full chain at T1: accepted, and the CA is cached
  • leaf alone at T1: accepted
  • full chain at T2, after the CA expired: ASN_AFTER_DATE_E, as before
  • leaf alone at T2: ASN_AFTER_DATE_E (accepted without the library change)
  • leaf alone at T2 with a verify callback that accepts the date error: accepted
  • full chain at T2 with the CA renewed under the same key: accepted, and then the leaf alone at T2: accepted (both fail without the dates update)
  • wolfSSL_CertManagerVerifyBuffer() with the CA loaded as WOLFSSL_CHAIN_CA at T1 and T2, and as WOLFSSL_USER_CA at T2

The new test fails without the library change at the leaf-alone T2 case (0 != -151). With the dates update disabled, it fails at the renewed CA case.

Built with -Werror and ran the new test, test_wolfSSL_chain_ca_ext_key_usage, test_wolfSSL_trust_peer_cert_dates and test_various_pathlen_chains with: the default flags plus --enable-certgen --enable-certext; --enable-opensslextra with -Wall -Wextra -Wdeclaration-after-statement -Wshadow -Wpedantic; IGNORE_NAME_CONSTRAINTS; --enable-asn=original; WOLFSSL_SMALL_CERT_VERIFY; NO_ASN_TIME; NO_SKID; OCSP, stapling v1 and v2 with CRL; PERSIST_CERT_CACHE; --enable-singlethreaded; --enable-c89; and --enable-all. --enable-cryptonly builds. The full unit test passes in the default build, and the full --api suite passes with --enable-all.

With NO_SKID, wolfSSL_CertManagerLoadCABufferType() cannot find the CA to retype, so the two WOLFSSL_CHAIN_CA certificate manager checks are skipped in that build. test_wolfSSL_chain_ca_ext_key_usage already fails on master with NO_SKID, and this change does not affect it.

Tested on macOS arm64 (Apple clang).

Checklist

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

After a handshake, ProcessPeerCerts() adds each verified CA from the
peer's chain to the certificate manager as a WOLFSSL_CHAIN_CA. The Signer
kept no validity dates, so a later peer that sent only its leaf was
verified with the cached CA even after that CA had expired. Sending the
expired CA, or using a new context, failed with ASN_AFTER_DATE_E.

Keep the DER encoded notBefore and notAfter of a chain CA in its Signer,
and after parsing each peer certificate walk from its issuer up to the
self-signed trust anchor, checking the dates of every chain CA on the
way. The error is reported as ASN_BEFORE_DATE_E or ASN_AFTER_DATE_E on
that certificate, so a verify callback can accept it as before. CAs the
application loaded are trust anchors and are not checked again. The walk
uses the name constraints issuer lookup, exported as
wolfssl_local_FindSignerByAkidOrName(). With IGNORE_NAME_CONSTRAINTS only
the direct issuer is checked.

A CA renewed with the same key is not added again, so the peer sending
it now updates the dates of the cached CA. Otherwise the cached copy
would fail a chain that includes a valid renewed CA.

wolfSSL_CertManagerVerifyBuffer() checks cached chain CAs the same way,
and wolfSSL_CertManagerLoadCABufferType() stores the dates of a CA loaded
as WOLFSSL_CHAIN_CA.

Add test_wolfSSL_chain_ca_cached_dates, which reuses one client context
for a full chain, a leaf alone before and after the CA expires, a verify
callback accepting the error and a renewed CA, and checks certificate
manager verification.
Copilot AI balanced review requested due to automatic review settings October 30, 2026 16:15
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

gcc-arm-cortex-m0plus

  • FLASH: .text +4 B (+0.0%, 70,683 B / 262,144 B, total: 27% used)

gcc-arm-cortex-m3

  • FLASH: .text +644 B (+0.5%, 131,981 B / 262,144 B, total: 50% used)

gcc-arm-cortex-m4

  • FLASH: .text +640 B (+0.3%, 208,944 B / 262,144 B, total: 80% used)

gcc-arm-cortex-m4-dtls13

  • FLASH: .text +704 B (+0.4%, 195,672 B / 1,048,576 B, total: 19% used)

gcc-arm-cortex-m4-openssl-compat

  • FLASH: .rodata +32 B, .text +704 B (+0.1%, 796,860 B / 1,048,576 B, total: 76% used)

gcc-arm-cortex-m4-pq

  • FLASH: .text +640 B (+0.2%, 312,336 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m4-rsa-only

  • FLASH: .text +640 B (+0.2%, 343,048 B / 1,048,576 B, total: 33% used)

gcc-arm-cortex-m4-tls12

  • FLASH: .text +640 B (+0.5%, 132,753 B / 262,144 B, total: 51% used)

gcc-arm-cortex-m4-tls13

  • FLASH: .text +640 B (+0.3%, 249,314 B / 262,144 B, total: 95% used)

gcc-arm-cortex-m7

  • FLASH: .text +640 B (+0.3%, 209,008 B / 262,144 B, total: 80% used)

gcc-arm-cortex-m7-pq

  • FLASH: .text +576 B (+0.2%, 313,168 B / 1,048,576 B, total: 30% used)

gcc-arm-cortex-m7-tls13

  • FLASH: .text +640 B (+0.3%, 249,314 B / 262,144 B, total: 95% used)

zephyr-frdm_rw612

  • FLASH: log_const_area -8 B, rodata -8 B, text +1,584 B (+0.6%, 284,032 B / 67,108,864 B, total: 0% used)

No memory changes detected for:


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

Unresolved critical flaws allow unauthorized or failed-handshake updates to extend cached CA validity, bypass issuer expiry checks, and introduce a data race in shared CA state.

7 open issues
What changed in this PR

This PR adds validity checks for cached chain CAs in TLS and certificate-manager verification, aiming to make verification consistent whether peers send their intermediates or omit them.

Changes:

  • Store validity dates on cached chain signers and check them along the issuer path.
  • Refresh cached dates when a verified CA renewal uses the same key.
  • Add regression tests for expiration, callback overrides, renewal, and application-loaded trust anchors.
File Description
wolfssl/​wolfcrypt/​asn.h Defines signer validity storage and shared helper declarations.
wolfssl/​internal.h Declares cached-CA date-check and update helpers.
wolfcrypt/​src/​asn.c Stores and frees signer dates; exposes issuer lookup.
tests/​api/​test_ssl_cert.h Registers the cached-CA validity test.
tests/​api/​test_ssl_cert.c Tests cached expiration, renewal, and verification behavior.
src/​ssl_certman.c Checks cached issuer dates and updates stored validity.
src/​internal.c Integrates date checks and renewal updates into handshakes.

Review effort: Balanced

Comment thread src/internal.c
Comment on lines +17558 to +17560
if (ret == 0 && (verify == VERIFY || verify == VERIFY_NAME)) {
ret = wolfssl_local_CheckChainCADates(SSL_CM(ssl), args->dCert->ca,
extraSigners);
Comment thread src/internal.c
Comment on lines +19674 to +19675
ret = wolfssl_local_SetChainCADates(SSL_CM(ssl),
subjectHash, args->dCert);
Comment thread src/ssl_certman.c
Comment on lines +2862 to +2873
isChainCA = 0;
if (ca->validity != NULL) {
/* Another handshake can change the dates, and the application
* the type. */
if (wc_LockMutex(&cm->caLock) != 0) {
ret = BAD_MUTEX_E;
break;
}
isChainCA = (ca->type == WOLFSSL_CHAIN_CA);
XMEMCPY(&validity, ca->validity, sizeof(validity));
wc_UnLockMutex(&cm->caLock);
}
Comment thread src/ssl_certman.c
Comment on lines +2956 to +2960
if (signer != NULL && signer->type == WOLFSSL_CHAIN_CA &&
cert->publicKey != NULL && signer->publicKey != NULL &&
signer->pubKeySize == cert->pubKeySize &&
XMEMCMP(signer->publicKey, cert->publicKey,
cert->pubKeySize) == 0) {
Comment thread src/internal.c
Comment on lines +17558 to +17560
if (ret == 0 && (verify == VERIFY || verify == VERIFY_NAME)) {
ret = wolfssl_local_CheckChainCADates(SSL_CM(ssl), args->dCert->ca,
extraSigners);
Comment thread src/internal.c
Comment on lines +19670 to +19674
else if (caVerifyRet == 0 && subjectHash != NULL) {
/* A CA renewed with the same key is not added
* again. Take its dates so the cached CA does
* not expire with the old certificate. */
ret = wolfssl_local_SetChainCADates(SSL_CM(ssl),
Comment thread src/ssl_certman.c
Comment on lines +2890 to +2893
if (ca->selfSigned)
break;
#ifndef IGNORE_NAME_CONSTRAINTS
parent = wolfssl_local_FindSignerByAkidOrName(cm, pending, ca);
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