From fc05adb38cda1fe540addc0e8277d9a075fa68f5 Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 6 Nov 2026 15:39:52 -0700 Subject: [PATCH 1/2] Prevent a CA from the peer's chain from being used to satisfy the NameConstraints check. Thanks to Bhabani Sankar Das (https://github.com/007bsd) for the report. --- certs/test/include.am | 6 + .../01-uri-permit-ca-selfissued-cert.pem | 12 ++ .../01-uri-permit-ca-selfloop-cert.pem | 13 ++ .../01-uri-permit-ca-selfloop-key.pem | 5 + .../nc-ancestor/03-selfissued-leaf-cert.pem | 13 ++ .../03-selfissued-valid-leaf-cert.pem | 13 ++ .../nc-ancestor/03-selfloop-leaf-cert.pem | 13 ++ certs/test/nc-ancestor/gen-nc-ancestor.sh | 61 ++++++++++ tests/api/test_certman.c | 113 ++++++++++++++++++ tests/api/test_certman.h | 6 + wolfcrypt/src/asn.c | 40 ++++++- 11 files changed, 293 insertions(+), 2 deletions(-) create mode 100644 certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem create mode 100644 certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem create mode 100644 certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem create mode 100644 certs/test/nc-ancestor/03-selfissued-leaf-cert.pem create mode 100644 certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem create mode 100644 certs/test/nc-ancestor/03-selfloop-leaf-cert.pem diff --git a/certs/test/include.am b/certs/test/include.am index 0857c3b5d34..1e4bf367c00 100644 --- a/certs/test/include.am +++ b/certs/test/include.am @@ -121,9 +121,15 @@ EXTRA_DIST += \ certs/test/nc-ancestor/00-uri-permit-ca-permissive-key.pem \ certs/test/nc-ancestor/01-uri-permit-ca-cert.pem \ certs/test/nc-ancestor/01-uri-permit-ca-key.pem \ + certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem \ + certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem \ + certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem \ certs/test/nc-ancestor/02-benign-sub-ca-cert.pem \ certs/test/nc-ancestor/02-benign-sub-ca-key.pem \ certs/test/nc-ancestor/03-leaf-attacker-key.pem \ certs/test/nc-ancestor/03-leaf-chain.pem \ + certs/test/nc-ancestor/03-selfissued-leaf-cert.pem \ + certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem \ + certs/test/nc-ancestor/03-selfloop-leaf-cert.pem \ certs/test/nc-ancestor/03-valid-leaf-cert.pem \ certs/test/nc-ancestor/03-valid-leaf-key.pem diff --git a/certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem b/certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem new file mode 100644 index 00000000000..615515d935e --- /dev/null +++ b/certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem @@ -0,0 +1,12 @@ +-----BEGIN CERTIFICATE----- +MIIBwjCCAWigAwIBAgIBEzAKBggqhkjOPQQDAjA4MQswCQYDVQQGEwJVUzERMA8G +A1UECgwITkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwHhcNMjYxMTA2 +MTkyNzIxWhcNNDYxMTAxMTkyNzIxWjA4MQswCQYDVQQGEwJVUzERMA8GA1UECgwI +TkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAARa5TKzBn7IAmACK5uk8Y+l1/PV4o9pwnsbQR5N3291A4Z52JZ2 +OdLsIZzhg27smqDlnXesUDov4S6BVBEUgKwzo2MwYTAPBgNVHRMBAf8EBTADAQH/ +MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUEREREREREREREREREREREREREREw +HwYDVR0jBBgwFoAU17wuBSyDXCKK/YsCoH6YeRZlzXQwCgYIKoZIzj0EAwIDSAAw +RQIhAOyosfbmkUV2O6qOxujPO0GLcy/8P+UUfqR77/BIb45pAiBMyI3WOdMeIkoh +pt0D5QYfmux4NZrKvyW+7YUQ79gpyQ== +-----END CERTIFICATE----- diff --git a/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem b/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem new file mode 100644 index 00000000000..f45d97d6931 --- /dev/null +++ b/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB7jCCAZSgAwIBAgIBFDAKBggqhkjOPQQDAjA4MQswCQYDVQQGEwJVUzERMA8G +A1UECgwITkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwHhcNMjYxMTA2 +MjA1NzIxWhcNNDYxMTAxMjA1NzIxWjA4MQswCQYDVQQGEwJVUzERMA8GA1UECgwI +TkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAASd89ZOqUGRiGZ6EJPe5Ixql3SJDWwGPoak7hIJUbG1gpq1yPE9 +qRrrjttx/Wdd7F1J2V0Tz06w5U+knZTFg/dvo4GOMIGLMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBQiIiIAIiIiIiIiIiIiIiIiIiIi +IjBJBgNVHSMEQjBAoTukOTA3MQswCQYDVQQGEwJVUzERMA8GA1UECgwITkMgVGVz +dHMxFTATBgNVBAMMDE5DIFRlc3QgUm9vdIIBETAKBggqhkjOPQQDAgNIADBFAiEA +gfxWUW18vk1M0fZz/ATHRqOvgnl6vWzbIfGqIsaXVcoCIHhgbIbWQLRDxzynrd7v +Ub3/LpirS785DU66GItJ9T39 +-----END CERTIFICATE----- diff --git a/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem b/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem new file mode 100644 index 00000000000..e7e1b095b8a --- /dev/null +++ b/certs/test/nc-ancestor/01-uri-permit-ca-selfloop-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIJiHP3Uaox1ooRULgObGYNCm+lWFtYKA21kCLG7gVJzHoAoGCCqGSM49 +AwEHoUQDQgAEnfPWTqlBkYhmehCT3uSMapd0iQ1sBj6GpO4SCVGxtYKatcjxPaka +647bcf1nXexdSdldE89OsOVPpJ2UxYP3bw== +-----END EC PRIVATE KEY----- diff --git a/certs/test/nc-ancestor/03-selfissued-leaf-cert.pem b/certs/test/nc-ancestor/03-selfissued-leaf-cert.pem new file mode 100644 index 00000000000..002bf431401 --- /dev/null +++ b/certs/test/nc-ancestor/03-selfissued-leaf-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICBjCCAa2gAwIBAgIBMjAKBggqhkjOPQQDAjA4MQswCQYDVQQGEwJVUzERMA8G +A1UECgwITkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwHhcNMjYxMTA2 +MTkyNzIxWhcNNDYxMTAxMTkyNzIxWjBAMQswCQYDVQQGEwJVUzERMA8GA1UECgwI +TkMgVGVzdHMxHjAcBgNVBAMMFU5DIFRlc3QgQXR0YWNrZXIgTGVhZjBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABNAzKoFsCE7NysRkRxmHdI61HECHaMhInraZ3s1v +GrgyYRXvmesAjzZIfCpm58kjRKdJpcKJFL74uaJeM/Gzsl+jgZ8wgZwwDAYDVR0T +AQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYD +VR0OBBYEFO6GdfO6MvjzgDGAs71HPqOXaCtBMB8GA1UdIwQYMBaAFBERERERERER +ERERERERERERERERMCcGA1UdEQEB/wQdMBuGGWh0dHBzOi8vYXR0YWNrZXIuY29t +L2xlYWYwCgYIKoZIzj0EAwIDRwAwRAIgeV2lLWd0ex2wmlUGBL6EKe3qt473LI4P +q4N9Fb559I0CIB2jHrsRNz+eTJnYGSr1ulYn97ebQD6ZIuDn8lQhvYum +-----END CERTIFICATE----- diff --git a/certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem b/certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem new file mode 100644 index 00000000000..dd1874dc6b4 --- /dev/null +++ b/certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICBTCCAaygAwIBAgIBMzAKBggqhkjOPQQDAjA4MQswCQYDVQQGEwJVUzERMA8G +A1UECgwITkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwHhcNMjYxMTA2 +MTkyNzIxWhcNNDYxMTAxMTkyNzIxWjA9MQswCQYDVQQGEwJVUzERMA8GA1UECgwI +TkMgVGVzdHMxGzAZBgNVBAMMEk5DIFRlc3QgVmFsaWQgTGVhZjBZMBMGByqGSM49 +AgEGCCqGSM49AwEHA0IABNcVnczEUQaQMcxlrButtTd8HqQEoMLGBl8XLPdI/eZs +42jj3B4l6txsi/zPG+9klJad5sPkp2Uqf9PiHowizW+jgaEwgZ4wDAYDVR0TAQH/ +BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYDVR0O +BBYEFLG1JSabNebv3xs8ZTuMVceg4FmrMB8GA1UdIwQYMBaAFBERERERERERERER +ERERERERERERMCkGA1UdEQEB/wQfMB2GG2h0dHBzOi8vYmVuaWduLmV4YW1wbGUu +Y29tLzAKBggqhkjOPQQDAgNHADBEAiBDedccc8Do8rV+ZtfBUI3XkuS7Qm78aDBb +/IYa86h3/QIgdw85qtNx5jnhNzG+HpHKKysIF38d6EWcuZQjctnvqyA= +-----END CERTIFICATE----- diff --git a/certs/test/nc-ancestor/03-selfloop-leaf-cert.pem b/certs/test/nc-ancestor/03-selfloop-leaf-cert.pem new file mode 100644 index 00000000000..b0bfdfba835 --- /dev/null +++ b/certs/test/nc-ancestor/03-selfloop-leaf-cert.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICCDCCAa2gAwIBAgIBNDAKBggqhkjOPQQDAjA4MQswCQYDVQQGEwJVUzERMA8G +A1UECgwITkMgVGVzdHMxFjAUBgNVBAMMDVVSSSBQZXJtaXQgQ0EwHhcNMjYxMTA2 +MjA1NzIxWhcNNDYxMTAxMjA1NzIxWjBAMQswCQYDVQQGEwJVUzERMA8GA1UECgwI +TkMgVGVzdHMxHjAcBgNVBAMMFU5DIFRlc3QgQXR0YWNrZXIgTGVhZjBZMBMGByqG +SM49AgEGCCqGSM49AwEHA0IABNAzKoFsCE7NysRkRxmHdI61HECHaMhInraZ3s1v +GrgyYRXvmesAjzZIfCpm58kjRKdJpcKJFL74uaJeM/Gzsl+jgZ8wgZwwDAYDVR0T +AQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHQYD +VR0OBBYEFO6GdfO6MvjzgDGAs71HPqOXaCtBMB8GA1UdIwQYMBaAFCIiIgAiIiIi +IiIiIiIiIiIiIiIiMCcGA1UdEQEB/wQdMBuGGWh0dHBzOi8vYXR0YWNrZXIuY29t +L2xlYWYwCgYIKoZIzj0EAwIDSQAwRgIhAMC16OUiGJfGeir9UR9Rx6TVC61HgtTF +K/jbYr+Eoc9mAiEAg0ICIKY4VUOmwko22FfF5MVa5ya2HeU0PEI/gBXoemE= +-----END CERTIFICATE----- diff --git a/certs/test/nc-ancestor/gen-nc-ancestor.sh b/certs/test/nc-ancestor/gen-nc-ancestor.sh index e645bc0ae0e..1ef78ddf588 100755 --- a/certs/test/nc-ancestor/gen-nc-ancestor.sh +++ b/certs/test/nc-ancestor/gen-nc-ancestor.sh @@ -73,6 +73,29 @@ subjectKeyIdentifier = hash authorityKeyIdentifier = keyid EOF +cat > "$WORK/selfissued-ca.cnf" <<'EOF' +[req] +distinguished_name = dn +prompt = no +[dn] +[v3_selfissued_ca] +basicConstraints = critical, CA:TRUE +keyUsage = critical, digitalSignature, keyCertSign, cRLSign +subjectKeyIdentifier = 11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11 +authorityKeyIdentifier = keyid +EOF + +cat > "$WORK/selfloop-ca.cnf" <<'EOF' +[req] +distinguished_name = dn +prompt = no +[v3_selfloop_ca] +basicConstraints = critical, CA:TRUE +keyUsage = critical, digitalSignature, keyCertSign, cRLSign +subjectKeyIdentifier = 22:22:22:00:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22:22 +authorityKeyIdentifier = issuer:always +EOF + cat > "$WORK/leaf-attacker.cnf" <<'EOF' [req] distinguished_name = dn @@ -149,6 +172,44 @@ mkchild "$DIR/03-valid-leaf-key.pem" "NC Test Valid Leaf" \ "$DIR/03-valid-leaf-cert.pem" "$WORK/leaf-valid.cnf" \ v3_leaf_valid 0x31 +# ---- 01 uri-permit-ca-selfissued: subject == issuer == uri-permit-ca's DN +# and uri-permit-ca's key, no NC, distinct SKID. Issued by uri-permit-ca, +# so it reaches a peer in the chain below it. ---- + +issuer_cert="$DIR/01-uri-permit-ca-cert.pem" +issuer_key="$DIR/01-uri-permit-ca-key.pem" +mkchild "$DIR/01-uri-permit-ca-key.pem" "URI Permit CA" \ + "$DIR/01-uri-permit-ca-selfissued-cert.pem" "$WORK/selfissued-ca.cnf" \ + v3_selfissued_ca 0x13 + +# ---- 03 leaves issued by the self-issued CA ---- + +issuer_cert="$DIR/01-uri-permit-ca-selfissued-cert.pem" +issuer_key="$DIR/01-uri-permit-ca-key.pem" +mkchild "$DIR/03-leaf-attacker-key.pem" "NC Test Attacker Leaf" \ + "$DIR/03-selfissued-leaf-cert.pem" "$WORK/leaf-attacker.cnf" \ + v3_leaf_attacker 0x32 +mkchild "$DIR/03-valid-leaf-key.pem" "NC Test Valid Leaf" \ + "$DIR/03-selfissued-valid-leaf-cert.pem" "$WORK/leaf-valid.cnf" \ + v3_leaf_valid 0x33 + +# ---- 01 uri-permit-ca-selfloop: same DN as uri-permit-ca, own key, and an +# AKID with no keyIdentifier. Its SKID is pinned to uri-permit-ca's CA-table +# row (first 4 bytes big-endian, modulo 11) so a name lookup reaches it +# first. ---- + +issuer_cert="$DIR/01-uri-permit-ca-cert.pem" +issuer_key="$DIR/01-uri-permit-ca-key.pem" +mkchild "$DIR/01-uri-permit-ca-selfloop-key.pem" "URI Permit CA" \ + "$DIR/01-uri-permit-ca-selfloop-cert.pem" "$WORK/selfloop-ca.cnf" \ + v3_selfloop_ca 0x14 + +issuer_cert="$DIR/01-uri-permit-ca-selfloop-cert.pem" +issuer_key="$DIR/01-uri-permit-ca-selfloop-key.pem" +mkchild "$DIR/03-leaf-attacker-key.pem" "NC Test Attacker Leaf" \ + "$DIR/03-selfloop-leaf-cert.pem" "$WORK/leaf-attacker.cnf" \ + v3_leaf_attacker 0x34 + # ---- Concatenated bundle: attacker leaf + benign-sub-ca + uri-permit-ca ---- cat "$WORK/03-leaf-cert.pem" \ diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 81c8b3573f7..9e141f09bd2 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -5303,6 +5303,119 @@ int test_wolfSSL_CertManagerNameConstraint_skid_disambiguates(void) return EXPECT_RESULT(); } +/* A self-issued CA delivered in the peer's chain is not a trust anchor, so + * the walk continues to the constrained CA that signed it. */ +int test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca(void) +{ + EXPECT_DECLS; +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CM_VERIFY) && defined(HAVE_ECC) && \ + !defined(IGNORE_NAME_CONSTRAINTS) && !defined(NO_SHA256) && \ + !defined(NO_SKID) && defined(WOLFSSL_ALT_NAMES) && \ + (defined(WOLFSSL_PEM_TO_DER) || defined(OPENSSL_EXTRA)) + WOLFSSL_CERT_MANAGER* cm = NULL; + const char* root_cert = + "./certs/test/nc-ancestor/00-root-cert.pem"; + const char* strict_cert = + "./certs/test/nc-ancestor/01-uri-permit-ca-cert.pem"; + const char* selfissued_cert = + "./certs/test/nc-ancestor/01-uri-permit-ca-selfissued-cert.pem"; + const char* valid_leaf_cert = + "./certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem"; + const char* attacker_leaf_cert = + "./certs/test/nc-ancestor/03-selfissued-leaf-cert.pem"; + byte* selfissued_buf = NULL; + size_t selfissued_sz = 0; + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, root_cert, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, strict_cert, NULL), + WOLFSSL_SUCCESS); + + /* Cached from a peer's chain rather than loaded by the application. */ + ExpectIntEQ(load_file(selfissued_cert, &selfissued_buf, &selfissued_sz), 0); + ExpectIntEQ(wolfSSL_CertManagerLoadCABufferType(cm, selfissued_buf, + (sword32)selfissued_sz, WOLFSSL_FILETYPE_PEM, 0, + WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, WOLFSSL_CHAIN_CA), + WOLFSSL_SUCCESS); + + /* Positive: leaf satisfies the constrained CA's permit. */ + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, valid_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + /* Negative: leaf violates the constrained CA's permit. */ + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, attacker_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + wolfSSL_CertManagerFree(cm); + XFREE(selfissued_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} + +/* A peer-supplied CA carrying the constrained CA's name, with no AKID key + * identifier, resolves to itself by name. The walk must not end there. */ +int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) +{ + EXPECT_DECLS; +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CM_VERIFY) && defined(HAVE_ECC) && \ + !defined(IGNORE_NAME_CONSTRAINTS) && !defined(NO_SHA256) && \ + !defined(NO_SKID) && defined(WOLFSSL_ALT_NAMES) && \ + (defined(WOLFSSL_PEM_TO_DER) || defined(OPENSSL_EXTRA)) + WOLFSSL_CERT_MANAGER* cm = NULL; + const char* root_cert = + "./certs/test/nc-ancestor/00-root-cert.pem"; + const char* strict_cert = + "./certs/test/nc-ancestor/01-uri-permit-ca-cert.pem"; + const char* benign_sub_ca_cert = + "./certs/test/nc-ancestor/02-benign-sub-ca-cert.pem"; + const char* selfloop_cert = + "./certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem"; + const char* valid_leaf_cert = + "./certs/test/nc-ancestor/03-valid-leaf-cert.pem"; + const char* attacker_leaf_cert = + "./certs/test/nc-ancestor/03-selfloop-leaf-cert.pem"; + byte* selfloop_buf = NULL; + size_t selfloop_sz = 0; + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, root_cert, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, strict_cert, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, benign_sub_ca_cert, NULL), + WOLFSSL_SUCCESS); + + /* Shares the strict CA's name and table row, so a name lookup finds + * it first. */ + ExpectIntEQ(load_file(selfloop_cert, &selfloop_buf, &selfloop_sz), 0); + ExpectIntEQ(wolfSSL_CertManagerLoadCABufferType(cm, selfloop_buf, + (sword32)selfloop_sz, WOLFSSL_FILETYPE_PEM, 0, + WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, WOLFSSL_CHAIN_CA), + WOLFSSL_SUCCESS); + + /* Positive: an unrelated chain still verifies with it cached. */ + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, valid_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + /* Negative: leaf violates the strict CA's permit. */ + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, attacker_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WC_NO_ERR_TRACE(ASN_NAME_INVALID_E)); + + wolfSSL_CertManagerFree(cm); + XFREE(selfloop_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} + #if defined(WOLFSSL_TRUST_PEER_CERT) && !defined(NO_ASN_TIME_CHECK) && \ defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) /* Before the validity period of certs/test/expired/expired-cert.pem */ diff --git a/tests/api/test_certman.h b/tests/api/test_certman.h index feb9cb687fa..cd629823eca 100644 --- a/tests/api/test_certman.h +++ b/tests/api/test_certman.h @@ -68,6 +68,8 @@ int test_wolfSSL_CertManagerRejectRPK(void); int test_wolfSSL_X509_V_ERR_strings(void); int test_wolfSSL_CertManagerNameConstraint_valid_chain(void); int test_wolfSSL_CertManagerNameConstraint_skid_disambiguates(void); +int test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca(void); +int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void); int test_wolfSSL_trust_peer_cert_dates(void); #define TEST_CERTMAN_DECLS \ @@ -120,6 +122,10 @@ int test_wolfSSL_trust_peer_cert_dates(void); test_wolfSSL_CertManagerNameConstraint_valid_chain), \ TEST_DECL_GROUP("certman", \ test_wolfSSL_CertManagerNameConstraint_skid_disambiguates), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca), \ TEST_DECL_GROUP("certman", test_wolfSSL_trust_peer_cert_dates), \ TEST_DECL_GROUP("certman", \ test_wolfSSL_CertManagerCheckCRL_out_of_date), \ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 46090474893..2ae8fb2f1cc 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -25885,6 +25885,34 @@ static Signer* FindSignerByAkidOrName(WOLFSSL_CERT_MANAGER* cm, return signer; } + +/* A CA cached from a peer's chain. Always 0 without a certificate + * manager, which only keeps application-loaded CAs. */ +static int SignerFromPeerChain(const Signer* signer) +{ +#ifndef WOLFCRYPT_ONLY + return signer->type == WOLFSSL_CHAIN_CA; +#else + (void)signer; + return 0; +#endif +} + +/* A self-signed CA ends the ancestor walk only when the application loaded + * it. One cached from a peer's chain is not a trust anchor. */ +static int SignerIsTrustAnchor(const Signer* signer) +{ + if (!signer->selfSigned) { + return 0; + } +#ifndef WOLFCRYPT_ONLY + /* X509_verify_cert() holds its own intermediates as WOLFSSL_TEMP_CA. */ + if (SignerFromPeerChain(signer) || signer->type == WOLFSSL_TEMP_CA) { + return 0; + } +#endif + return 1; +} #endif /* !IGNORE_NAME_CONSTRAINTS */ #ifdef WOLFSSL_DUAL_ALG_CERTS @@ -26851,10 +26879,18 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, WOLFSSL_ERROR_VERBOSE(ASN_NAME_INVALID_E); return ASN_NAME_INVALID_E; } - /* Stop at trust anchor (self-signed). */ - if (ncSigner->selfSigned) + /* Stop at trust anchor. */ + if (SignerIsTrustAnchor(ncSigner)) break; ncParent = FindSignerByAkidOrName(cm, extraCAList, ncSigner); + /* A CA from the peer's chain cannot end the walk: the + * constraints of whatever issued it would go unapplied. */ + if ((ncParent == ncSigner || ncParent == ncPrev) && + SignerFromPeerChain(ncSigner)) { + WOLFSSL_MSG("NC ancestor walk ended below a trust anchor"); + WOLFSSL_ERROR_VERBOSE(ASN_NAME_INVALID_E); + return ASN_NAME_INVALID_E; + } /* Stop on missing parent, self-loop, or A->B->A cycle. */ if (ncParent == NULL || ncParent == ncSigner || ncParent == ncPrev) From d9a20f7817b4ff9e99d733104cfb700261d8470d Mon Sep 17 00:00:00 2001 From: Kareem Date: Fri, 6 Nov 2026 17:57:55 -0700 Subject: [PATCH 2/2] Add GetCAByNameSkip to detect a self-issued CA. Fix the case where ncParent == NULL by using the already implemented check. --- certs/test/include.am | 1 + certs/test/nc-ancestor/gen-nc-ancestor.sh | 3 + linuxkm/linuxkm_wc_port.h | 5 ++ linuxkm/module_hooks.c | 1 + src/ssl_certman.c | 9 ++- tests/api/test_certman.c | 71 ++++++++++++++++++++--- tests/api/test_certman.h | 3 + wolfcrypt/src/asn.c | 34 +++++++---- wolfssl/internal.h | 4 ++ 9 files changed, 113 insertions(+), 18 deletions(-) diff --git a/certs/test/include.am b/certs/test/include.am index 1e4bf367c00..06f45461597 100644 --- a/certs/test/include.am +++ b/certs/test/include.am @@ -131,5 +131,6 @@ EXTRA_DIST += \ certs/test/nc-ancestor/03-selfissued-leaf-cert.pem \ certs/test/nc-ancestor/03-selfissued-valid-leaf-cert.pem \ certs/test/nc-ancestor/03-selfloop-leaf-cert.pem \ + certs/test/nc-ancestor/03-selfloop-valid-leaf-cert.pem \ certs/test/nc-ancestor/03-valid-leaf-cert.pem \ certs/test/nc-ancestor/03-valid-leaf-key.pem diff --git a/certs/test/nc-ancestor/gen-nc-ancestor.sh b/certs/test/nc-ancestor/gen-nc-ancestor.sh index 1ef78ddf588..4766719bf94 100755 --- a/certs/test/nc-ancestor/gen-nc-ancestor.sh +++ b/certs/test/nc-ancestor/gen-nc-ancestor.sh @@ -209,6 +209,9 @@ issuer_key="$DIR/01-uri-permit-ca-selfloop-key.pem" mkchild "$DIR/03-leaf-attacker-key.pem" "NC Test Attacker Leaf" \ "$DIR/03-selfloop-leaf-cert.pem" "$WORK/leaf-attacker.cnf" \ v3_leaf_attacker 0x34 +mkchild "$DIR/03-valid-leaf-key.pem" "NC Test Valid Leaf" \ + "$DIR/03-selfloop-valid-leaf-cert.pem" "$WORK/leaf-valid.cnf" \ + v3_leaf_valid 0x35 # ---- Concatenated bundle: attacker leaf + benign-sub-ca + uri-permit-ca ---- diff --git a/linuxkm/linuxkm_wc_port.h b/linuxkm/linuxkm_wc_port.h index cd69dd18d2b..32d86b03e35 100644 --- a/linuxkm/linuxkm_wc_port.h +++ b/linuxkm/linuxkm_wc_port.h @@ -1171,6 +1171,9 @@ #ifndef NO_SKID struct Signer *GetCAByName(struct WOLFSSL_CERT_MANAGER *cm, unsigned char *hash); + struct Signer *GetCAByNameSkip(struct WOLFSSL_CERT_MANAGER *cm, + unsigned char *hash, + const struct Signer *skip); #ifdef HAVE_OCSP struct Signer* GetCAByKeyHash(struct WOLFSSL_CERT_MANAGER *cm, const unsigned char* keyHash); @@ -1546,6 +1549,7 @@ typeof(GetCA) *GetCA; #ifndef NO_SKID typeof(GetCAByName) *GetCAByName; + typeof(GetCAByNameSkip) *GetCAByNameSkip; #ifdef HAVE_OCSP typeof(GetCAByKeyHash) *GetCAByKeyHash; #endif /* HAVE_OCSP */ @@ -1842,6 +1846,7 @@ #define GetCA WC_PIE_INDIRECT_SYM(GetCA) #ifndef NO_SKID #define GetCAByName WC_PIE_INDIRECT_SYM(GetCAByName) + #define GetCAByNameSkip WC_PIE_INDIRECT_SYM(GetCAByNameSkip) #ifdef HAVE_OCSP #define GetCAByKeyHash WC_PIE_INDIRECT_SYM(GetCAByKeyHash) #endif /* HAVE_OCSP */ diff --git a/linuxkm/module_hooks.c b/linuxkm/module_hooks.c index 04f89e25b00..81f4de35374 100644 --- a/linuxkm/module_hooks.c +++ b/linuxkm/module_hooks.c @@ -2402,6 +2402,7 @@ static int set_up_wolfssl_linuxkm_pie_redirect_table(void) { wolfssl_linuxkm_pie_redirect_table.GetCA = GetCA; #ifndef NO_SKID wolfssl_linuxkm_pie_redirect_table.GetCAByName = GetCAByName; + wolfssl_linuxkm_pie_redirect_table.GetCAByNameSkip = GetCAByNameSkip; #ifdef HAVE_OCSP wolfssl_linuxkm_pie_redirect_table.GetCAByKeyHash = GetCAByKeyHash; #endif /* HAVE_OCSP */ diff --git a/src/ssl_certman.c b/src/ssl_certman.c index 0946251320b..a4b00e45457 100644 --- a/src/ssl_certman.c +++ b/src/ssl_certman.c @@ -3069,6 +3069,13 @@ Signer* GetCAByAKID(WOLFSSL_CERT_MANAGER* cm, const byte* issuer, #ifndef NO_SKID /* return CA if found, otherwise NULL. Walk through hash table. */ Signer* GetCAByName(WOLFSSL_CERT_MANAGER* cm, byte* hash) +{ + return GetCAByNameSkip(cm, hash, NULL); +} + +/* As GetCAByName() but never returns skip. */ +Signer* GetCAByNameSkip(WOLFSSL_CERT_MANAGER* cm, byte* hash, + const Signer* skip) { Signer* ret = NULL; Signer* signers; @@ -3083,7 +3090,7 @@ Signer* GetCAByName(WOLFSSL_CERT_MANAGER* cm, byte* hash) for (row = 0; row < CA_TABLE_SIZE && ret == NULL; row++) { signers = cm->caTable[row]; while (signers && ret == NULL) { - if (XMEMCMP(hash, signers->subjectNameHash, + if (signers != skip && XMEMCMP(hash, signers->subjectNameHash, SIGNER_DIGEST_SIZE) == 0) { ret = signers; } diff --git a/tests/api/test_certman.c b/tests/api/test_certman.c index 9e141f09bd2..4a67ea8c936 100644 --- a/tests/api/test_certman.c +++ b/tests/api/test_certman.c @@ -5358,7 +5358,8 @@ int test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca(void) } /* A peer-supplied CA carrying the constrained CA's name, with no AKID key - * identifier, resolves to itself by name. The walk must not end there. */ + * identifier, resolves to itself by name. The walk must go on to the CA + * that issued it. */ int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) { EXPECT_DECLS; @@ -5372,12 +5373,10 @@ int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) "./certs/test/nc-ancestor/00-root-cert.pem"; const char* strict_cert = "./certs/test/nc-ancestor/01-uri-permit-ca-cert.pem"; - const char* benign_sub_ca_cert = - "./certs/test/nc-ancestor/02-benign-sub-ca-cert.pem"; const char* selfloop_cert = "./certs/test/nc-ancestor/01-uri-permit-ca-selfloop-cert.pem"; const char* valid_leaf_cert = - "./certs/test/nc-ancestor/03-valid-leaf-cert.pem"; + "./certs/test/nc-ancestor/03-selfloop-valid-leaf-cert.pem"; const char* attacker_leaf_cert = "./certs/test/nc-ancestor/03-selfloop-leaf-cert.pem"; byte* selfloop_buf = NULL; @@ -5389,8 +5388,6 @@ int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) WOLFSSL_SUCCESS); ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, strict_cert, NULL), WOLFSSL_SUCCESS); - ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, benign_sub_ca_cert, NULL), - WOLFSSL_SUCCESS); /* Shares the strict CA's name and table row, so a name lookup finds * it first. */ @@ -5400,7 +5397,7 @@ int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, WOLFSSL_CHAIN_CA), WOLFSSL_SUCCESS); - /* Positive: an unrelated chain still verifies with it cached. */ + /* Positive: leaf within the strict CA's permit. */ ExpectIntEQ(wolfSSL_CertManagerVerify(cm, valid_leaf_cert, WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); @@ -5416,6 +5413,66 @@ int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void) return EXPECT_RESULT(); } +/* Peer-chain CAs left cached after their root is unloaded. */ +int test_wolfSSL_CertManagerNameConstraint_orphan_chain_ca(void) +{ + EXPECT_DECLS; +#if !defined(NO_FILESYSTEM) && !defined(NO_CERTS) && \ + !defined(NO_WOLFSSL_CM_VERIFY) && defined(HAVE_ECC) && \ + !defined(IGNORE_NAME_CONSTRAINTS) && !defined(NO_SHA256) && \ + !defined(NO_SKID) && defined(WOLFSSL_ALT_NAMES) && \ + (defined(WOLFSSL_PEM_TO_DER) || defined(OPENSSL_EXTRA)) + WOLFSSL_CERT_MANAGER* cm = NULL; + const char* root_cert = + "./certs/test/nc-ancestor/00-root-cert.pem"; + const char* strict_cert = + "./certs/test/nc-ancestor/01-uri-permit-ca-cert.pem"; + const char* benign_sub_ca_cert = + "./certs/test/nc-ancestor/02-benign-sub-ca-cert.pem"; + const char* valid_leaf_cert = + "./certs/test/nc-ancestor/03-valid-leaf-cert.pem"; + byte* strict_buf = NULL; + size_t strict_sz = 0; + byte* sub_ca_buf = NULL; + size_t sub_ca_sz = 0; + + ExpectNotNull(cm = wolfSSL_CertManagerNew()); + + ExpectIntEQ(wolfSSL_CertManagerLoadCA(cm, root_cert, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(load_file(strict_cert, &strict_buf, &strict_sz), 0); + ExpectIntEQ(wolfSSL_CertManagerLoadCABufferType(cm, strict_buf, + (sword32)strict_sz, WOLFSSL_FILETYPE_PEM, 0, + WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, WOLFSSL_CHAIN_CA), + WOLFSSL_SUCCESS); + ExpectIntEQ(load_file(benign_sub_ca_cert, &sub_ca_buf, &sub_ca_sz), 0); + ExpectIntEQ(wolfSSL_CertManagerLoadCABufferType(cm, sub_ca_buf, + (sword32)sub_ca_sz, WOLFSSL_FILETYPE_PEM, 0, + WOLFSSL_LOAD_VERIFY_DEFAULT_FLAGS, WOLFSSL_CHAIN_CA), + WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, valid_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WOLFSSL_SUCCESS); + + ExpectIntEQ(wolfSSL_CertManagerUnloadTypeCerts(cm, WOLFSSL_USER_CA), + WOLFSSL_SUCCESS); + + /* Issuer missing one level up, then two. */ + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, benign_sub_ca_cert, + WOLFSSL_FILETYPE_PEM), + WC_NO_ERR_TRACE(ASN_NO_SIGNER_E)); + ExpectIntEQ(wolfSSL_CertManagerVerify(cm, valid_leaf_cert, + WOLFSSL_FILETYPE_PEM), + WC_NO_ERR_TRACE(ASN_NO_SIGNER_E)); + + wolfSSL_CertManagerFree(cm); + XFREE(strict_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); + XFREE(sub_ca_buf, NULL, DYNAMIC_TYPE_TMP_BUFFER); +#endif + return EXPECT_RESULT(); +} + #if defined(WOLFSSL_TRUST_PEER_CERT) && !defined(NO_ASN_TIME_CHECK) && \ defined(HAVE_SSL_MEMIO_TESTS_DEPENDENCIES) /* Before the validity period of certs/test/expired/expired-cert.pem */ diff --git a/tests/api/test_certman.h b/tests/api/test_certman.h index cd629823eca..ff1b0af96b4 100644 --- a/tests/api/test_certman.h +++ b/tests/api/test_certman.h @@ -70,6 +70,7 @@ int test_wolfSSL_CertManagerNameConstraint_valid_chain(void); int test_wolfSSL_CertManagerNameConstraint_skid_disambiguates(void); int test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca(void); int test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca(void); +int test_wolfSSL_CertManagerNameConstraint_orphan_chain_ca(void); int test_wolfSSL_trust_peer_cert_dates(void); #define TEST_CERTMAN_DECLS \ @@ -126,6 +127,8 @@ int test_wolfSSL_trust_peer_cert_dates(void); test_wolfSSL_CertManagerNameConstraint_selfissued_chain_ca), \ TEST_DECL_GROUP("certman", \ test_wolfSSL_CertManagerNameConstraint_selfloop_chain_ca), \ + TEST_DECL_GROUP("certman", \ + test_wolfSSL_CertManagerNameConstraint_orphan_chain_ca), \ TEST_DECL_GROUP("certman", test_wolfSSL_trust_peer_cert_dates), \ TEST_DECL_GROUP("certman", \ test_wolfSSL_CertManagerCheckCRL_out_of_date), \ diff --git a/wolfcrypt/src/asn.c b/wolfcrypt/src/asn.c index 2ae8fb2f1cc..47c9741b2df 100644 --- a/wolfcrypt/src/asn.c +++ b/wolfcrypt/src/asn.c @@ -25223,6 +25223,18 @@ Signer* GetCAByName(WOLFSSL_CERT_MANAGER* cm, byte* hash) return NULL; } + +Signer* GetCAByNameSkip(WOLFSSL_CERT_MANAGER* cm, byte* hash, + const Signer* skip); +Signer* GetCAByNameSkip(WOLFSSL_CERT_MANAGER* cm, byte* hash, + const Signer* skip) +{ + (void)cm; + (void)hash; + (void)skip; + + return NULL; +} #endif /* NO_SKID */ #ifdef WOLFSSL_AKID_NAME @@ -25850,7 +25862,8 @@ static Signer* FindSignerByAkidOrName(WOLFSSL_CERT_MANAGER* cm, * is set (could substitute a same-DN sibling). */ } else { - signer = GetCAByName(cm, cert->issuerNameHash); + /* A self-issued CA shares its issuer's name. */ + signer = GetCAByNameSkip(cm, cert->issuerNameHash, cert); } #else signer = GetCA(cm, cert->issuerNameHash); @@ -26883,18 +26896,19 @@ int ParseCertRelative(DecodedCert* cert, int type, int verify, if (SignerIsTrustAnchor(ncSigner)) break; ncParent = FindSignerByAkidOrName(cm, extraCAList, ncSigner); - /* A CA from the peer's chain cannot end the walk: the - * constraints of whatever issued it would go unapplied. */ - if ((ncParent == ncSigner || ncParent == ncPrev) && - SignerFromPeerChain(ncSigner)) { - WOLFSSL_MSG("NC ancestor walk ended below a trust anchor"); - WOLFSSL_ERROR_VERBOSE(ASN_NAME_INVALID_E); - return ASN_NAME_INVALID_E; - } /* Stop on missing parent, self-loop, or A->B->A cycle. */ if (ncParent == NULL || ncParent == ncSigner || - ncParent == ncPrev) + ncParent == ncPrev) { + /* A CA from the peer's chain cannot end the walk: the + * constraints of whatever issued it would go unapplied. */ + if (SignerFromPeerChain(ncSigner)) { + WOLFSSL_MSG("NC ancestor walk found no issuer for a " + "chain CA"); + WOLFSSL_ERROR_VERBOSE(ASN_NO_SIGNER_E); + return ASN_NO_SIGNER_E; + } break; + } if (++ncDepth >= WOLFSSL_MAX_CHAIN_DEPTH) { WOLFSSL_MSG("NC ancestor walk exceeded WOLFSSL_MAX_CHAIN_DEPTH"); WOLFSSL_ERROR_VERBOSE(ASN_PATHLEN_SIZE_E); diff --git a/wolfssl/internal.h b/wolfssl/internal.h index d0aa5473165..85e62024b37 100644 --- a/wolfssl/internal.h +++ b/wolfssl/internal.h @@ -8053,6 +8053,10 @@ WOLFSSL_LOCAL WC_RNG* WOLFSSL_RSA_GetRNG(WOLFSSL_RSA *rsa, WC_RNG **tmpRNG, WOLFSSL_LOCAL Signer* GetCAByName(WOLFSSL_CERT_MANAGER* cm, byte* hash); #endif + #if !defined(NO_SKID) && !defined(GetCAByNameSkip) + WOLFSSL_LOCAL Signer* GetCAByNameSkip(WOLFSSL_CERT_MANAGER* cm, + byte* hash, const Signer* skip); + #endif #endif /* !NO_CERTS */ WOLFSSL_LOCAL int BuildTlsHandshakeHash(WOLFSSL* ssl, byte* hash, word32* hashLen);