From 57c63ad1a5d9fce7a43f91bec8e5e0519583de6f Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Wed, 4 Nov 2026 16:59:13 -0700 Subject: [PATCH 1/2] Certificate Processesing Error with ```c ./configure --enable-opensslextra --enable-altcertchains ``` If the user loads a verifiy callback that delays peer auth and the server sends a chain that looks like this: - Attacker-ca -> has all data for us to find a signer in our trust store but is sign by attacker controlled key. - Intermediate_ca -> selfsigned - leaf -> produced by attacker ca We accept the connection and trust the identity of the leaf cert. This is becuase we incorrectly wipe the error produced by the bad root CA when we come across the intermediate with the error we rightfuly don't care about. this mean we get to the point where we have forgotten out root is bad and check the leaf against it which returns OK due to the leaf being produced from the attacker root. --- src/internal.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/internal.c b/src/internal.c index 1c5196874d2..b2ab041cead 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19873,6 +19873,12 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, && !args->haveTrustPeer #endif /* WOLFSSL_TRUST_PEER_CERT */ ) { + #ifdef WOLFSSL_ALT_CERT_CHAINS + word16 verifyErr = args->verifyErr; + #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + unsigned long peerVerifyRet = ssl->peerVerifyRet; + #endif + #endif int skipAddCA = 0; #if defined(HAVE_OCSP) && defined(HAVE_CRL) int ocspNoUrl = 0; @@ -20171,10 +20177,12 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } ret = 0; /* clear errors and continue */ + /* restore previous errors so chain is rejected + * if it should be */ #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) - ssl->peerVerifyRet = 0; + ssl->peerVerifyRet = peerVerifyRet; #endif - args->verifyErr = 0; + args->verifyErr = verifyErr; /* do not add to certificate manager */ skipAddCA = 1; From 19868a563f9d8c4a40c7059b9072b57f5269b879 Mon Sep 17 00:00:00 2001 From: Aidan Keefe Date: Thu, 5 Nov 2026 15:10:36 -0700 Subject: [PATCH 2/2] reproduction test --- .../gen-alt-chain-forged-ca.sh | 83 +++++++++++ certs/test/alt-chain-forged-ca/leaf-key.pem | 28 ++++ .../alt-chain-forged-ca/realRoot-cert.pem | 18 +++ .../test/alt-chain-forged-ca/server-chain.pem | 58 ++++++++ certs/test/include.am | 6 + src/internal.c | 22 +-- tests/api/test_ssl_cert.c | 140 ++++++++++++++++++ tests/api/test_ssl_cert.h | 2 + 8 files changed, 348 insertions(+), 9 deletions(-) create mode 100755 certs/test/alt-chain-forged-ca/gen-alt-chain-forged-ca.sh create mode 100644 certs/test/alt-chain-forged-ca/leaf-key.pem create mode 100644 certs/test/alt-chain-forged-ca/realRoot-cert.pem create mode 100644 certs/test/alt-chain-forged-ca/server-chain.pem diff --git a/certs/test/alt-chain-forged-ca/gen-alt-chain-forged-ca.sh b/certs/test/alt-chain-forged-ca/gen-alt-chain-forged-ca.sh new file mode 100755 index 00000000000..af22d5156e8 --- /dev/null +++ b/certs/test/alt-chain-forged-ca/gen-alt-chain-forged-ca.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# +# gen-alt-chain-forged-ca.sh +# Generate the forged-CA alternate chain test certs. +# +# realRoot trusted anchor, CN=Root, fixed SKID DEADBEEF... +# evilRoot attacker key with the same name and SKID as realRoot. Only +# used to sign attackerCA; never sent or trusted. +# attackerCA CA issued by evilRoot. Its issuer name and AKID point at +# realRoot, so the peer finds realRoot as signer but the +# signature does not verify. +# intermediate unrelated self-signed CA, gives ASN_SELF_SIGNED_E which the +# alternate cert chain code bypasses. +# leaf issued by attackerCA. +# +# server-chain.pem is sent as: leaf, intermediate, attackerCA. Only it, +# realRoot-cert.pem and leaf-key.pem (the server's key) are kept. All other +# keys and certs are made fresh in a temp dir and removed once the chain is +# made. + +set -e + +DIR="$(cd "$(dirname "$0")" && pwd)" +WORK="$(mktemp -d)" +trap 'rm -f "$WORK"/*; rmdir "$WORK"' EXIT + +cat > "$WORK/ext.cnf" <<'CNF' +[req] +distinguished_name = dn +prompt = no +[dn] +[root] +basicConstraints = critical, CA:TRUE +keyUsage = critical, digitalSignature, keyCertSign, cRLSign +subjectKeyIdentifier = DE:AD:BE:EF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF +[ca] +basicConstraints = critical, CA:TRUE +keyUsage = critical, digitalSignature, keyCertSign, cRLSign +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid +[leaf] +basicConstraints = critical, CA:FALSE +keyUsage = critical, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = DNS:leaf.example.com +subjectKeyIdentifier = hash +authorityKeyIdentifier = keyid +CNF + +# $1 key-path $2 out +root(){ + openssl req -new -x509 -key "$1" -subj "/O=Test/CN=Root" \ + -days 7300 -sha256 -set_serial 1 -config "$WORK/ext.cnf" \ + -extensions root -out "$2" +} + +# $1 key-path $2 CN $3 issuer-cert $4 issuer-key-path $5 ext $6 out $7 serial +child(){ + openssl req -new -key "$1" -subj "/O=Test/CN=$2" \ + -config "$WORK/ext.cnf" -out "$WORK/c.csr" + openssl x509 -req -in "$WORK/c.csr" -CA "$3" -CAkey "$4" \ + -set_serial "$7" -days 7300 -sha256 -extfile "$WORK/ext.cnf" \ + -extensions "$5" -out "$6" +} + +for k in realRoot evilRoot attackerCA intermediate; do + openssl genrsa -out "$WORK/$k-key.pem" 2048 +done +openssl genrsa -out "$DIR/leaf-key.pem" 2048 + +root "$WORK/realRoot-key.pem" "$DIR/realRoot-cert.pem" +root "$WORK/evilRoot-key.pem" "$WORK/evilRoot-cert.pem" +child "$WORK/attackerCA-key.pem" Attacker "$WORK/evilRoot-cert.pem" \ + "$WORK/evilRoot-key.pem" ca "$WORK/attackerCA-cert.pem" 2 +openssl req -new -x509 -key "$WORK/intermediate-key.pem" \ + -subj "/O=Test/CN=Intermediate" -days 7300 -sha256 -set_serial 3 \ + -config "$WORK/ext.cnf" -extensions ca -out "$WORK/intermediate-cert.pem" +child "$DIR/leaf-key.pem" leaf.example.com "$WORK/attackerCA-cert.pem" \ + "$WORK/attackerCA-key.pem" leaf "$WORK/leaf-cert.pem" 4 + +cat "$WORK/leaf-cert.pem" "$WORK/intermediate-cert.pem" \ + "$WORK/attackerCA-cert.pem" > "$DIR/server-chain.pem" +echo "Done" diff --git a/certs/test/alt-chain-forged-ca/leaf-key.pem b/certs/test/alt-chain-forged-ca/leaf-key.pem new file mode 100644 index 00000000000..ceee4245ab0 --- /dev/null +++ b/certs/test/alt-chain-forged-ca/leaf-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDHqNwFqKxe/AEZ +VWZaHNniBMakNXNx7mtE1sV2+GrdkJQE5MLzpXAvjdPJtS6NuQPgBWvm8dAyYC7o +gBXSPFdvy2XwiJan1hEycgivINNbLYNmDS1PZK4XfNNqYupySA+hcIhmujkXKfut +WdivXlaOx0YUpw3XZpphd9Ry9aJ1Tn49jMb4zUXoGtZ8CvudLCdlcu5pXFwQqBfu +hX5K/f0y1v3ho1F4DJaTOEhXyH00vEZpJL/kbGW0lgymTkltZmm0LX9Judt+O3Zi +pT/RSQ+4fqHuvLH3ndGfJifZ3rodoerDbWlR/s+CWGO14zSugV39QLRh6xHgmbRU +i5mguhUzAgMBAAECggEAAzghFnarrNTLk1dLd3yxzXfXpUv270MLKyJHyQBkmYT1 +9gXWpN7c6hwtA+2uPfrznPGWPBq+PImzex+eYNqE0f8jRvEU2/wfukLMPaXXjC4c +L8TBOPkxG2mkvDlMrKIMnat2vF1LtvVmb8GcNLVEy2soIWrq7+p65vZrkDaPGezh +e2WI30saRLK0mFsJYOJ7IpD0WOcZWA/9QKBPxXpBkeGf8MJbujiHXAtLk6BXn19o +A2CfuXcYkamkXh48/R8ENu75i2PXqoRqX+TldcQcNXwQA5/el50So8ur2Bg1Jhfb +/Iig8X4rVKhrlpWgE+iSu0BooOjvUmEj9chjflXVTQKBgQDla4HuSbwYCCGoXiR9 +mTfl5V+yz/QYHsq+H8Lw2fUJicgRRVO1TMmoGNP+C0BeVp9JKnoPK1ZdlDvYuu7n +01XIJ9u/VBv13/b0s+QaCuAoN4AnO0a5jWKMRPp+65YBPhIUiHJBpQX2870+u6nM +vYnyWlc3FSRrlOLaYFY+XFUHfQKBgQDeyqxraolshiwtpEa8P3COevESA1BRhVjB +nScwfNOYBvYDp1vGYFoe9iza8wjxzY9Et20bP7sy5VvFKod/mpjAFeEUDsRUohmO +IfU4m6hPjrBZfWQ7jOI54Gf5UnjG2glJ3VF6pVA/uwZb8idRbUJ2zHmLUoz7sLLq +yR9Ej68ObwKBgG15jKnRo6DwGGeIbO9GMudQOgdQtlDgOKza6SIXT5d6mO6s31Hv +T3cYSei7I78gpKe8Tjx42KCVAK3eHeyf6xo08uZs017TvbttMBbBQ8/NFCkkXw5i +CjmXHNV9w7naDbSgm6z5e0wpaQ/Lay5Y77+nHmEZhKSVil7cg6sEUkrdAoGAKkvb +9ACj2mCWnoujijWZDtO0VLEgXHmuYgON3xyzpxcbOZJaUA0Wzu35kg+10QIvguI3 +6oZs6bq1c3chhk45Va5Rrtcm2tJze5Ji1b5nc8MpI7Q0hUG36Tj7RFAt8cKPJODJ +iTk9RPwitu4AHOtbROaI5LRlY5smPIC2WPPWTi0CgYAyMSUMrKoHp9HKthk6pErV ++u7t8qOjq7+l+/dX25GKQxoSu8SGNSI1uQSzHjD7Fs6yMqolerIHZo8/+PGXWIYh +j/b6PIneLhlRtYbatYbA8e38Peu4eqUSZG7SixjcJLZSoIxFhgtPpR+PfTTGlgQa +CneNQ6VLSbsPw2jpwNLf4Q== +-----END PRIVATE KEY----- diff --git a/certs/test/alt-chain-forged-ca/realRoot-cert.pem b/certs/test/alt-chain-forged-ca/realRoot-cert.pem new file mode 100644 index 00000000000..2f149ce9ade --- /dev/null +++ b/certs/test/alt-chain-forged-ca/realRoot-cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+TCCAeGgAwIBAgIBATANBgkqhkiG9w0BAQsFADAeMQ0wCwYDVQQKDARUZXN0 +MQ0wCwYDVQQDDARSb290MB4XDTI2MTEwNTIyMDQzNloXDTQ2MTAzMTIyMDQzNlow +HjENMAsGA1UECgwEVGVzdDENMAsGA1UEAwwEUm9vdDCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBAJcCodfd6jQD404Bp2L6Oi5SWRUQVvmGCN08P9FdoBT+ +CPoW9txRzXQ4jJcNUbD9+gcQm1NhNn1IS/rHE4ITWt5Gf2YaQnQkXUz6YdAZlSeO +TSV8abDp3fF4qLXlMyRtbDXShy3U4RAsNWd0Eu19/b9zWnGGRmA4v2ElFUmsYw4R +l1L48X+pX8XTQAg8RFGoryGgf3SuINR4qFkODFD5T5+GtkRPFbz8Tiztm3I7unY4 +l28RsWMQ++uQ+aPGa0ZxOmf3Mv4daBnZzWt6nYKyOp6Sx1V7ssDI5+MvFbtEC785 +3eRm/dYDBWLm5jPQMMNibrUHvAbT/jNL/T6lbM7TNvECAwEAAaNCMEAwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFN6tvu8AESIzRFVm +d4iZqrvM3e7/MA0GCSqGSIb3DQEBCwUAA4IBAQAQgpJ4KYMDe0sVloDcQKxTnuXX ++B48VP1YodWwpPZiRsokl9wxJ/bzaCTNPragH40PSouLwwxrsLVEC/PYzHlnW5lO +XGHXxnB35l0KcNpAef2oBb5z18WkkpgW1r0WGP2ns0ZG4yL6uN2Lb5qKtNnfPqHH +wpF08n2Emxjk5WRWI7jwZDA25MrGSMge5WnG+e+EdSED652zglTXmLtdwJESSEqg +XfsswK+cbMQeNh0hGKQhzfoUHTwB6g2/eFesgNKHQR4yAolbFGJ8LqMIDVOzBFIj +WQcIEPloNI5k7YBMOjVf3+eDoGUVVus4to+lUGwkr/tOrfFpiT3DRGYQEuD5 +-----END CERTIFICATE----- diff --git a/certs/test/alt-chain-forged-ca/server-chain.pem b/certs/test/alt-chain-forged-ca/server-chain.pem new file mode 100644 index 00000000000..ad119690182 --- /dev/null +++ b/certs/test/alt-chain-forged-ca/server-chain.pem @@ -0,0 +1,58 @@ +-----BEGIN CERTIFICATE----- +MIIDWzCCAkOgAwIBAgIBBDANBgkqhkiG9w0BAQsFADAiMQ0wCwYDVQQKDARUZXN0 +MREwDwYDVQQDDAhBdHRhY2tlcjAeFw0yNjExMDUyMjA0MzZaFw00NjEwMzEyMjA0 +MzZaMCoxDTALBgNVBAoMBFRlc3QxGTAXBgNVBAMMEGxlYWYuZXhhbXBsZS5jb20w +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDHqNwFqKxe/AEZVWZaHNni +BMakNXNx7mtE1sV2+GrdkJQE5MLzpXAvjdPJtS6NuQPgBWvm8dAyYC7ogBXSPFdv +y2XwiJan1hEycgivINNbLYNmDS1PZK4XfNNqYupySA+hcIhmujkXKfutWdivXlaO +x0YUpw3XZpphd9Ry9aJ1Tn49jMb4zUXoGtZ8CvudLCdlcu5pXFwQqBfuhX5K/f0y +1v3ho1F4DJaTOEhXyH00vEZpJL/kbGW0lgymTkltZmm0LX9Judt+O3ZipT/RSQ+4 +fqHuvLH3ndGfJifZ3rodoerDbWlR/s+CWGO14zSugV39QLRh6xHgmbRUi5mguhUz +AgMBAAGjgZMwgZAwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0l +BAwwCgYIKwYBBQUHAwEwGwYDVR0RBBQwEoIQbGVhZi5leGFtcGxlLmNvbTAdBgNV +HQ4EFgQUZWEHg66piqeBE79Yh++du4czvuEwHwYDVR0jBBgwFoAUIQq1riRwrOOl +Lf8TAmqaOC3UasYwDQYJKoZIhvcNAQELBQADggEBAGlQh2YNCTasmEy+B+Lre8FT +JhYUXFB7vYj6/0bkkiivf+DH++m3My13DDTgBWU6MaZWos7EJkDTsk1zeEkagU0W +bDyj+Rbv2wbYWRD+ePWXJ/Mlum+gQ9Pb6Gk+p71DWLTHxqTxe+GUuz72liLyHOOE +Wf/9aXd2pEchbsoAE/MLgjkR9ZXoUIGn1xz73LgX0OkfU6G27+0LILngmdgG1vFS ++niQ9uoJreqqRA6Ux8fdwt/dDrbR5jj6BameFXG/aPwXX7rVvxPaiRDUcuS7yWfj +hCWm3K6XzB0zw1QWvWdXjo1vBg2zD8qxcc1iKpplO57X6n7byEhbsD07voAcqnA= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIBAzANBgkqhkiG9w0BAQsFADAmMQ0wCwYDVQQKDARUZXN0 +MRUwEwYDVQQDDAxJbnRlcm1lZGlhdGUwHhcNMjYxMTA1MjIwNDM2WhcNNDYxMDMx +MjIwNDM2WjAmMQ0wCwYDVQQKDARUZXN0MRUwEwYDVQQDDAxJbnRlcm1lZGlhdGUw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+2lHDnIc9TZztNLvfHt0J +6F5n+l4djINJlO7qm0ionlAUxXGQ/yBD7tE4u7isVQdpTu8GVgMvFlzhRXGywtMW +WzQfmDhb5L44ImGvvzpBHg3PUoS6PfjCPetAQr3y2ffzZZb23inN/BkXKXyHxu/n +FkOZhC7XQPF2a0/Asux+ML8Y7bXZlkFGW7PHvWLgb2yIiR3l0/YN296jNtqv8k4z +sZGwscuQbcO2OYx64w7LuHb2t39K3y/l0AW2RizEOVme0RS1L9tqL6etPjRclw8w +rnI+Rvbm4vYNnT8Rr6oHmCbMbyaVVdgi9bAomDqBdECb8W95XQns0NjOOLfKkj99 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1Ud +DgQWBBQi1spsbHe3+fvuV6UCgLLcbyMwljANBgkqhkiG9w0BAQsFAAOCAQEAHWnP +efm8loo+GdhBAYqkx9WzI6GdTSL+NqA5n5gvFN/IHvWq0lKO8/NKhGLYAiwxgT9g +89ou9s8P6fQfakhKIv9SKh94pPNzGwAMMY5hERf56ia6lbUDm19XzOf/R20tx6tg +SStOqdbOML2/Pl1psUTWTzKr/KwmE/5kDvuinuXPccQ/Plq8n7BofuZiZA6xn9B+ +VETMynOl4zaEEN09cKSaOB76oq02chsFNwMjNy7WUy9w2XiuewSOCX1+l9q/HXSB +b8cD22aeScGDMFVc31BAvn5jG0G25aLBF3G3IbCm/VstR1S6PHIhya+29NMbEfG/ +JGwx+TKcPMbOHSZ5vA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIDHjCCAgagAwIBAgIBAjANBgkqhkiG9w0BAQsFADAeMQ0wCwYDVQQKDARUZXN0 +MQ0wCwYDVQQDDARSb290MB4XDTI2MTEwNTIyMDQzNloXDTQ2MTAzMTIyMDQzNlow +IjENMAsGA1UECgwEVGVzdDERMA8GA1UEAwwIQXR0YWNrZXIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQClhVsnbALuXcBp9QsI6Fx+gaaugUtmh6KrvYfL +Ydip3MoifFI/UcYk70CHBYzF7hPUzkn10dRZqma4mOkBQ3etL2evEvUZUhsXTVLI +ogRFton4u5XBsdn6fBK0j9ytjdPZcPuPQNtyxj6IXLEPZpAfT0iOOHHzunWPDEpG +T3+DVjUkTt+rWNgPur8FdbOU8iDT11L3eSdoQRQUbavCM5+fiQGSw22iF3iZ7Y6j +breCyRSKCV6fvajHvulU6UgxnOm4hGAc6atHUuKc0NqCPdziIyz7qlIr5CZxecOg +MeaQqWkqhcJUtt5Mmu0Ph5KYH0ydbDbzRQW3L0Vw5BDJwqjrAgMBAAGjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBQhCrWuJHCs +46Ut/xMCapo4LdRqxjAfBgNVHSMEGDAWgBTerb7vABEiM0RVZneImaq7zN3u/zAN +BgkqhkiG9w0BAQsFAAOCAQEAs9WndeHFpWEXSHYYdWJUU4fewSvGQLfAU0ZYF6Vf +bb7g2OkZc8yW3bQwV0oZUrHHfJUpMimabV21YXAjbbHDYs4iGmGGAxKHst5JSxPK +MYwn4QNr2lzsqcO6qYqsf8jJMR86B+pBvkPLU/bIY15MMzks+9mGqSyoj56UBO6n +dlu9EC+6aMX1sH51Z8sNWM+Z0OcfEv6/1NdXJdgapbfLmZT8dzmUsZ1/sO4KoToD +F54+AIllAUHC2qgAn9KjWba4xtQ0BYaHiDMBRdCZfFZoieyme93SoSYsADIbwZN+ +Rd6BEXGa83TNqC/PWmJGcl3F1G+e01ssvsavBHwD9Ty9MA== +-----END CERTIFICATE----- diff --git a/certs/test/include.am b/certs/test/include.am index 0857c3b5d34..a407eec1955 100644 --- a/certs/test/include.am +++ b/certs/test/include.am @@ -127,3 +127,9 @@ EXTRA_DIST += \ certs/test/nc-ancestor/03-leaf-chain.pem \ certs/test/nc-ancestor/03-valid-leaf-cert.pem \ certs/test/nc-ancestor/03-valid-leaf-key.pem + +EXTRA_DIST += \ + certs/test/alt-chain-forged-ca/gen-alt-chain-forged-ca.sh \ + certs/test/alt-chain-forged-ca/realRoot-cert.pem \ + certs/test/alt-chain-forged-ca/leaf-key.pem \ + certs/test/alt-chain-forged-ca/server-chain.pem diff --git a/src/internal.c b/src/internal.c index b2ab041cead..870bd7beae4 100644 --- a/src/internal.c +++ b/src/internal.c @@ -19873,11 +19873,12 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, && !args->haveTrustPeer #endif /* WOLFSSL_TRUST_PEER_CERT */ ) { - #ifdef WOLFSSL_ALT_CERT_CHAINS - word16 verifyErr = args->verifyErr; - #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) + #if (defined(WOLFSSL_ALT_CERT_CHAINS) || \ + (defined(__APPLE__) && defined(WOLFSSL_SYS_CA_CERTS))) && \ + (defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL)) + /* Error recorded by earlier certs, restored when this + * cert's CA error below is bypassed instead of wiped. */ unsigned long peerVerifyRet = ssl->peerVerifyRet; - #endif #endif int skipAddCA = 0; #if defined(HAVE_OCSP) && defined(HAVE_CRL) @@ -20177,12 +20178,13 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, } ret = 0; /* clear errors and continue */ - /* restore previous errors so chain is rejected - * if it should be */ + /* Only this cert's error is waived. Keep the + * errors from earlier certs, including + * args->verifyErr, so the chain is still + * rejected if it should be. */ #if defined(OPENSSL_EXTRA) || defined(OPENSSL_EXTRA_X509_SMALL) ssl->peerVerifyRet = peerVerifyRet; #endif - args->verifyErr = verifyErr; /* do not add to certificate manager */ skipAddCA = 1; @@ -20217,10 +20219,12 @@ int ProcessPeerCerts(WOLFSSL* ssl, byte* input, word32* inOutIdx, WOLFSSL_MSG("Bypassing errors to allow for Apple native" " CA validation"); ret = 0; /* clear errors and continue */ - args->verifyErr = 0; + /* Only this cert's error is waived. Keep the errors + * from earlier certs, including args->verifyErr, so + * the chain is still rejected if it should be. */ #if defined(OPENSSL_EXTRA) \ || defined(OPENSSL_EXTRA_X509_SMALL) - ssl->peerVerifyRet = 0; + ssl->peerVerifyRet = peerVerifyRet; #endif /* do not add to certificate manager */ skipAddCA = 1; diff --git a/tests/api/test_ssl_cert.c b/tests/api/test_ssl_cert.c index 64c29841f44..857db379407 100644 --- a/tests/api/test_ssl_cert.c +++ b/tests/api/test_ssl_cert.c @@ -3978,6 +3978,146 @@ int test_wolfSSL_chain_ca_ext_key_usage(void) return EXPECT_RESULT(); } +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_ALT_CERT_CHAINS) && defined(OPENSSL_EXTRA) && \ + defined(WOLFSSL_PEM_TO_DER) && !defined(NO_RSA) && !defined(NO_TLS) && \ + !defined(NO_SHA256) && !defined(NO_FILESYSTEM) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) +static int test_alt_chain_forged_ca_cb_calls; + +/* Accept every certificate and leave the decision to + * wolfSSL_get_verify_result() once the handshake is done. */ +static int test_alt_chain_forged_ca_cb(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + (void)preverify; + (void)store; + test_alt_chain_forged_ca_cb_calls++; + return 1; +} + +#ifndef NO_WOLFSSL_CM_VERIFY +static int test_alt_chain_forged_ca_cm_calls; + +/* Certificate manager callback that rejects every certificate that failed. */ +static int test_alt_chain_forged_ca_cm_cb(int preverify, + WOLFSSL_X509_STORE_CTX* store) +{ + (void)store; + test_alt_chain_forged_ca_cm_calls++; + return preverify; +} +#endif +#endif + +/* Test that bypassing a CA error for the alternate certificate chain does not + * also erase an error recorded by an earlier certificate in the chain. + * + * The server sends leaf, intermediate, attackerCA. attackerCA names the + * trusted root as its issuer, with a matching AKID, but is signed by another + * key, so its signature check fails. The intermediate is an unrelated + * self-signed CA, so it fails with ASN_SELF_SIGNED_E, which the alternate + * chain code bypasses. The leaf is issued by attackerCA. + * + * Cases: + * 0. No callback: the forged chain is refused. + * 1. A verify callback accepts every certificate and then checks + * wolfSSL_get_verify_result(). That must report the forged CA's + * WOLFSSL_X509_V_ERR_INVALID_CA. Before the fix, the intermediate's + * bypass reset peerVerifyRet to 0, so the leaf was reported as verified. + * 2. A certificate manager callback rejects the forged CA but the verify + * callback overrides the error, so the chain carries on with + * args->verifyErr set. The handshake must still fail at the leaf. Before + * the fix, the intermediate's bypass cleared args->verifyErr and the + * handshake completed. + * + * @return TEST_SUCCESS on success. + */ +int test_wolfSSL_alt_chain_forged_ca(void) +{ + EXPECT_DECLS; +#if defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && \ + defined(WOLFSSL_ALT_CERT_CHAINS) && defined(OPENSSL_EXTRA) && \ + defined(WOLFSSL_PEM_TO_DER) && !defined(NO_RSA) && !defined(NO_TLS) && \ + !defined(NO_SHA256) && !defined(NO_FILESYSTEM) && \ + !defined(NO_WOLFSSL_CLIENT) && !defined(NO_WOLFSSL_SERVER) + const char* rootFile = + "./certs/test/alt-chain-forged-ca/realRoot-cert.pem"; + const char* chainFile = + "./certs/test/alt-chain-forged-ca/server-chain.pem"; + const char* keyFile = + "./certs/test/alt-chain-forged-ca/leaf-key.pem"; + WOLFSSL_CTX *ctx_c = NULL, *ctx_s = NULL; + WOLFSSL *ssl_c = NULL, *ssl_s = NULL; + struct test_memio_ctx test_ctx; + int mode; +#ifndef NO_WOLFSSL_CM_VERIFY + const int lastMode = 2; +#else + const int lastMode = 1; +#endif + + for (mode = 0; mode <= lastMode && EXPECT_SUCCESS(); mode++) { + XMEMSET(&test_ctx, 0, sizeof(test_ctx)); + ExpectIntEQ(test_memio_setup(&test_ctx, &ctx_c, &ctx_s, &ssl_c, + &ssl_s, wolfSSLv23_client_method, wolfSSLv23_server_method), 0); + + ExpectIntEQ(wolfSSL_CTX_load_verify_locations(ctx_c, rootFile, NULL), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_certificate_chain_file(ssl_s, chainFile), + WOLFSSL_SUCCESS); + ExpectIntEQ(wolfSSL_use_PrivateKey_file(ssl_s, keyFile, + WOLFSSL_FILETYPE_PEM), WOLFSSL_SUCCESS); + + if (mode == 0) { + /* Without a callback the forged chain must be refused. */ + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, NULL); + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntNE(wolfSSL_get_verify_result(ssl_c), WOLFSSL_X509_V_OK); + } + else if (mode == 1) { + test_alt_chain_forged_ca_cb_calls = 0; + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, + test_alt_chain_forged_ca_cb); + /* The callback waives every error, so the handshake completes, + * but the forged CA's error must still be reported. */ + ExpectIntEQ(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntGT(test_alt_chain_forged_ca_cb_calls, 0); + ExpectIntEQ(wolfSSL_get_verify_result(ssl_c), + WOLFSSL_X509_V_ERR_INVALID_CA); + } +#ifndef NO_WOLFSSL_CM_VERIFY + else { + test_alt_chain_forged_ca_cb_calls = 0; + test_alt_chain_forged_ca_cm_calls = 0; + if (ctx_c != NULL) { + wolfSSL_CertManagerSetVerify( + wolfSSL_CTX_GetCertManager(ctx_c), + test_alt_chain_forged_ca_cm_cb); + } + wolfSSL_set_verify(ssl_c, WOLFSSL_VERIFY_PEER, + test_alt_chain_forged_ca_cb); + /* The certificate manager's rejection of the forged CA must + * survive the intermediate's bypass and fail the leaf. */ + ExpectIntNE(test_memio_do_handshake(ssl_c, ssl_s, 10, NULL), 0); + ExpectIntGT(test_alt_chain_forged_ca_cm_calls, 0); + ExpectIntGT(test_alt_chain_forged_ca_cb_calls, 0); + } +#endif + + wolfSSL_free(ssl_c); + ssl_c = NULL; + wolfSSL_free(ssl_s); + ssl_s = NULL; + wolfSSL_CTX_free(ctx_c); + ctx_c = NULL; + wolfSSL_CTX_free(ctx_s); + ctx_s = NULL; + } +#endif + return EXPECT_RESULT(); +} + #if defined(WOLFSSL_SMALL_CERT_VERIFY) && \ defined(HAVE_MANUAL_MEMIO_TESTS_DEPENDENCIES) && !defined(NO_RSA) && \ !defined(NO_TLS) && !defined(NO_SHA256) && !defined(NO_ASN_TIME) && \ diff --git a/tests/api/test_ssl_cert.h b/tests/api/test_ssl_cert.h index 1f802590194..e6bdb22e5b3 100644 --- a/tests/api/test_ssl_cert.h +++ b/tests/api/test_ssl_cert.h @@ -73,6 +73,7 @@ int test_wolfSSL_verify_tls13_failnocert_only(void); int test_wolfSSL_verify_empty_server_cert(void); int test_wolfSSL_verify_post_handshake_defers(void); int test_wolfSSL_chain_ca_ext_key_usage(void); +int test_wolfSSL_alt_chain_forged_ca(void); int test_wolfSSL_small_cert_verify_sig_error(void); int test_wolfSSL_session_chain_oversized_cert(void); int test_wolfSSL_session_ticket_oversized_peer_cert(void); @@ -130,6 +131,7 @@ int test_wolfSSL_SESSION_get0_peer_full_chain(void); TEST_DECL_GROUP("ssl_cert", \ test_wolfSSL_verify_post_handshake_defers), \ TEST_DECL_GROUP("ssl_cert", test_wolfSSL_chain_ca_ext_key_usage), \ + TEST_DECL_GROUP("ssl_cert", test_wolfSSL_alt_chain_forged_ca), \ TEST_DECL_GROUP("ssl_cert", \ test_wolfSSL_small_cert_verify_sig_error), \ TEST_DECL_GROUP("ssl_cert", \