Repository navigation
Prevent a CA from the peer's chain from being used to satisfy the NameConstraints check. - #12054
kareem-wolfssl wants to merge 2 commits into
Conversation
…eConstraints check. Thanks to Bhabani Sankar Das (https://github.com/007bsd) for the report.
|
@claude review |
|
There was a problem hiding this comment.
🟡 Changes recommended
CA ancestor traversal can still bypass name constraints through missing parents or temporary-CA self-loops and incorrectly reject valid leaves beneath self-issued key-rollover CAs.
3 open issues
What changed in this PR
This PR hardens certificate name-constraint validation so a CA cached from a peer’s chain cannot be treated as a trust anchor.
Changes:
- Distinguishes application-loaded trust anchors from peer-chain CAs during ancestor traversal.
- Adds regression tests for self-issued CAs and self-looping issuer lookups.
- Adds certificate fixtures, generation steps, and distribution entries.
| File | Description |
|---|---|
wolfcrypt/src/asn.c |
Adds trust-anchor classification and peer-chain cycle rejection. |
tests/api/test_certman.h |
Declares and registers the new regression tests. |
tests/api/test_certman.c |
Tests self-issued and self-loop CA handling. |
certs/test/nc-ancestor/gen-nc-ancestor.sh |
Generates the new CA and leaf fixtures. |
certs/test/nc-ancestor/03-selfloop-leaf-cert.pem |
Constraint-violating leaf for the self-loop case. |
certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem |
Permitted leaf for the self-issued case. |
certs/test/nc-ancestor/03-selfissued-leaf-cert.pem |
Constraint-violating leaf for the self-issued case. |
certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem |
Test key for the self-loop CA. |
certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem |
CA fixture with an ambiguous name-based issuer lookup. |
certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem |
Self-issued CA fixture with a distinct key identifier. |
certs/test/include.am |
Includes the new fixtures in source distributions. |
Review effort: Balanced
| static int SignerFromPeerChain(const Signer* signer) | ||
| { | ||
| #ifndef WOLFCRYPT_ONLY | ||
| return signer->type == WOLFSSL_CHAIN_CA; |
| static int SignerFromPeerChain(const Signer* signer) | ||
| { | ||
| #ifndef WOLFCRYPT_ONLY | ||
| return signer->type == WOLFSSL_CHAIN_CA; |
There was a problem hiding this comment.
🟡 (optional) Applications that load a self-signed root with the public type WOLFSSL_CHAIN_CA now fail every chain under it with ASN_NAME_INVALID_E, even when no CA has name constraints. SignerFromPeerChain treats the public Signer->type value as proof that a CA came from a peer. wolfSSL_CertManagerLoadCABufferType() lets callers set that value on any CA, roots included. Fix: mark peer-chain CAs with an internal flag set only by ProcessPeerCerts()/ProcessPeerCertAddPendingCA(), or refuse WOLFSSL_CHAIN_CA for self-signed certs in wolfSSL_CertManagerLoadCABufferType(), so that roots the application loads still end the walk.
Why this was flagged
One plausible reason to call wolfSSL_CertManagerLoadCABufferType(cm, root, ..., WOLFSSL_CHAIN_CA) is so wolfSSL_CertManagerUnloadIntermediateCerts() can remove the root later. That function accepts the type (ssl_certman.c:633-665), and SetCAType() allows any value from WOLFSSL_USER_CA to WOLFSSL_USER_INTER (ssl_certman.c:3556). The root then has selfSigned=1 and type=WOLFSSL_CHAIN_CA. In the walk at asn.c:26883, SignerIsTrustAnchor() now returns 0 for it. FindSignerByAkidOrName() then finds the root itself, either by its AKID or by name when it has no AKID. ncParent == ncSigner and SignerFromPeerChain() is true, so asn.c:26888-26892 returns ASN_NAME_INVALID_E. This hits every intermediate or leaf verified under that root, through wolfSSL_CertManagerVerifyBuffer and TLS handshakes alike. On the base branch the walk stopped at any selfSigned signer and these chains verified. A TLS peer cannot trigger this, because AddCA() skips a self-signed copy whose SKID is already present.
Verification: It happens when an application loads a self-signed root through the public wolfSSL_CertManagerLoadCABufferType() with type WOLFSSL_CHAIN_CA. Every leaf or chain under that root then fails with ASN_NAME_INVALID_E at asn.c:26888-26893, even when no CA has name constraints. On the base commit these chains verified.
| } | ||
| /* Stop on missing parent, self-loop, or A->B->A cycle. */ | ||
| if (ncParent == NULL || ncParent == ncSigner || | ||
| ncParent == ncPrev) |
There was a problem hiding this comment.
🟣 pre-existing, not blocking: pre-existing, security-relevant: a peer-chain CA whose parent the walk cannot resolve still ends the walk silently, so its issuer's name constraints are skipped. The new rejection covers ncParent == ncSigner and ncParent == ncPrev but not ncParent == NULL. Fix: also return ASN_NAME_INVALID_E when ncParent == NULL and SignerFromPeerChain(ncSigner). Alternatively, make FindSignerByAkidOrName accept every issuer the cert->ca lookup at asn.c:26496-26541 accepts.
A small fix can ride a push you are already making; otherwise a short reply is enough.
Why this was flagged
The cert->ca lookup that admits a CA into the chain (asn.c:26496-26541) is looser than FindSignerByAkidOrName (asn.c:25835-25885). Under WOLFSSL_ALLOW_AKID_SKID_MATCH it keeps a CA whose subjectNameHash differs from the issuer hash. Under WOLFSSL_AKID_NAME it resolves by AKID issuer+serial. Under HAVE_CERTIFICATE_STATUS_REQUEST_V2 it matches extraCAList by name alone. FindSignerByAkidOrName requires subjectNameHash == issuerNameHash and an AKID->SKID match, so it can return NULL for a WOLFSSL_CHAIN_CA that was legitimately added under its constrained issuer. The walk then hits ncParent == NULL at asn.c:26894 and breaks. The leaf is accepted without the issuer's constraints, exactly as on the base branch. The new check at asn.c:26888 excludes NULL, so the PR's stated goal (a peer-chain CA must not end the walk) is only partly met.
Verification: Verification: pre-existing (security-relevant). The new rejection at asn.c:26888 only fires on (ncParent == ncSigner || ncParent == ncPrev) && SignerFromPeerChain(ncSigner). The next check at asn.c:26895, if (ncParent == NULL || ...) break;, still ends the walk silently. The issuer's name constraints are then never checked against the leaf's names. The base code also broke on ncParent == NULL.
Fix the case where ncParent == NULL by using the already implemented check.


Description
Fixes zd#22763, F-17181
Testing
Built in + added tests, provided reproducer
Checklist