✓ProveIt
Examples / Report #3989514 Saved analysis

CVE-2026-87866: wolfSSL public-key pinning bypass

curl · 2026-09-0222.0s analysis · $0.015 in model calls

What the report claims

curl 8.22.0 built with wolfSSL can accept a CURLOPT_PINNEDPUBLICKEY hash for a key the TLS peer does not possess, bypassing public-key pinning.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-87866 wolfSSL public-key pinning bypass

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Needs human review

The available evidence does not settle the report. A maintainer should investigate.

Why this finding?
  • The provided curl source only includes the generic Curl_parseX509 function (lib/vtls/x509asn1.c:772-884). The wolfSSL backend code that performs the TLS handshake, extracts the peer certificate, calls Curl_parseX509, and compares the parsed SPKI to CURLOPT_PINNEDPUBLICKEY is not provided.
  • The report's core claim—that a malformed certificate causes wolfSSL and Curl_parseX509 to select different SPKIs from the same DER—depends on both parsers' behavior. Only curl's parser is visible; wolfSSL's parser is not in the provided code.
  • The pinned public key verification logic (likely in lib/vtls/ws.c or vtls.c) is missing, so it cannot be determined whether curl re-parses the certificate after wolfSSL validation and whether a mismatch would be detected or ignored.
  • The reproduction artifacts (zip, scripts) are not available for inspection.
  • If the described differential parsing exists, it would be a valid security issue (authentication bypass), but the provided code does not settle it.
What still needs checking
  • The wolfSSL backend source file (e.g., lib/vtls/ws.c or lib/vtls/wolfssl.c) at revision 5bbff5c2e9f7, specifically the certificate verification and pinned public key checking functions.
  • The code that calls Curl_parseX509 during pin verification and compares the result to the pinned key.
  • The CURLOPT_PINNEDPUBLICKEY handling code for the wolfSSL backend.
How this analysis ran 5 claims · 0 citations verified

Checked against curl master on 2026-09-02 (5bbff5c2e9f7). Test evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 10 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2373 tokens · $0.0002
3 · Evidence
1 code snippets
real curl source at 5bbff5c2e9f7 · 2 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
10114 tokens · 8.7s · $0.0151
5 · Receipts
0 of 0 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. To evaluate the claim, we need the wolfSSL backend code that handles certificate verification and pinned public key checking (likely lib/vtls/ws.c or similar at revision 5bbff5c2e9f7). The provided source only shows the generic Curl_parseX509 parser, not the code that calls it after wolfSSL completes the handshake and compares the parsed SPKI to CURLOPT_PINNEDPUBLICKEY. Please provide that file or point to its location in the repository. Without it, we cannot confirm whether a malformed certificate could cause wolfSSL and curl's parser to select different public keys from the same DER.

Inspect all claims and source code 5 claims · 1 code excerpts

Report claims

C1flaw not verifiable

curl accepts a pinned public key hash that does not match the peer's presented key

Read the reasoning

The claim that curl accepts a pinned public key hash that doesn't match the peer's key depends on the wolfSSL backend's certificate verification and pin comparison code. The provided source only shows Curl_parseX509 (lib/vtls/x509asn1.c), not the wolfSSL integration (likely lib/vtls/ws.c or similar) or the pinned public key verification logic that calls Curl_parseX509 and compares the result to CURLOPT_PINNEDPUBLICKEY. Without that code path, the claim cannot be evaluated.

C2location not verifiable

the flaw occurs in the wolfSSL-backed TLS handshake path of curl

Read the reasoning

The claim locates the flaw in the 'wolfSSL-backed TLS handshake path.' The provided code does not include the wolfSSL backend implementation (e.g., lib/vtls/ws.c) where the handshake completion, certificate extraction, and pin verification occur. Only the generic Curl_parseX509 parser is shown.

C3trigger not verifiable

a malformed certificate that contains a public key A but the pinned hash corresponds to public key B

Read the reasoning

The claim describes a specific malformed certificate that causes wolfSSL to use public key A while Curl_parseX509 selects embedded SPKI B due to a manipulated 'validity' length. The provided Curl_parseX509 code (lines 826-850) shows parsing of validity (notBefore/notAfter) and subjectPublicKeyInfo, but there is no evidence in the provided code that a length manipulation in the validity field can shift the subjectPublicKeyInfo parsing. Moreover, the wolfSSL-side behavior (accepting key A) is not in the provided code. The differential parser output mentioned in the report (run-differential.sh) is not reproducible from the given source alone.

C4impact not verifiable

the application request is sent after the TLS handshake is accepted despite the pin mismatch

Read the reasoning

This claim follows from C1-C3. Since the code path that sends the application request after pin verification (the wolfSSL verify_pinned or equivalent function) is not provided, it cannot be confirmed that a mismatched pin would still allow the request to be sent.

C5reproduction not verifiable

run the provided reproduce.sh script which builds wolfSSL and curl and demonstrates the bypass

Read the reasoning

The reproduction steps reference a zip attachment and scripts (reproduce.sh, run-differential.sh) that are not included in the provided materials. The deterministic checks only confirm symbol existence, not the PoC bundle.

What the report says about other projects

not addressed the public-key pin must still be verified independently

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary curl 8.22.0 built with wolfSSL can accept a `CURLOPT_PINNEDPUBLICKEY` hash for a key the TLS peer does not possess. wolfSSL completes the TLS handshake using public key **A** from a malformed certificate. curl then reparses the same DER using `Curl_parseX509()` and, because of a manipulated `validity` length, selects embedded SPKI **B**. The server has only private key **A**, but pin **B** is accepted and curl sends the HTTP request. This is not a CA-validation claim. The PoC uses curl's documented pinning-only mode (`-k` with `--pinnedpubkey`), where the public-key pin must still be verified independently. ## Steps To Reproduce Prerequisites: `git`, `curl`, `unzip`, `openssl`, `clang`/`clang++`, `make`, Autotools, `pkg-config`, and zlib development headers. 1. Extract the attached bundle and prepare isolated paths: ```sh unzip SUBMISSION-CURL-WOLFSSL-X509-PIN-BYPASS-20260902-H1-FINAL.zip POC_DIR="$(pwd)/wolfssl-x509-pin-bypass" REPRO_ROOT="$(mktemp -d)" WOLFSSL_PREFIX="$REPRO_ROOT/wolfssl-install" ``` 2. Build wolfSSL 5.9.2: ```sh git clone --depth 1 --branch v5.9.2-stable \ https://github.com/wolfSSL/wolfssl.git "$REPRO_ROOT/wolfssl" cd "$REPRO_ROOT/wolfssl" ./autogen.sh CC=clang \ CFLAGS='-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined' \ LDFLAGS='-fsanitize=address,undefined' \ ./configure --prefix="$WOLFSSL_PREFIX" \ --enable-tls13 --enable-harden --enable-ed25519 \ --enable-sessioncerts --enable-opensslextra \ --disable-benchmark --disable-crypttests make -j"$(nproc)" make install ``` 3. Download, verify, and build curl 8.22.0: ```sh cd "$REPRO_ROOT" curl -LO https://curl.se/download/curl-8.22.0.tar.xz printf '%s %s\n' \ f7ef3ae8a22e521f289803fe93543eb64c329b58aa73a9e224dfd915a2a5f4f7 \ curl-8.22.0.tar.xz | sha256sum -c - tar -xf curl-8.22.0.tar.xz cd curl-8.22.0 CC=clang \ CFLAGS='-O1 -g -fno-omit-frame-pointer -fsanitize=address,undefined' \ CPPFLAGS="-I$WOLFSSL_PREFIX/include" \ LDFLAGS="-L$WOLFSSL_PREFIX/lib -Wl,-rpath,$WOLFSSL_PREFIX/lib -fsanitize=address,undefined" \ ./configure --with-wolfssl="$WOLFSSL_PREFIX" \ --without-libpsl --without-brotli --without-zstd --without-nghttp2 \ --enable-debug --disable-shared make -j"$(nproc)" ``` 4. Run the end-to-end attack and controls: ```sh CURL_BIN="$REPRO_ROOT/curl-8.22.0/src/curl" \ WOLFSSL_SRC="$REPRO_ROOT/wolfssl" \ "$POC_DIR/poc/reprodu …

Source code

lib/vtls/x509asn1.clines 772–884 · definition of Curl_parseX509
772int Curl_parseX509(struct Curl_X509certificate *cert,
773 const char *beg, const char *end)
774{
775 struct Curl_asn1Element elem;
776 struct Curl_asn1Element tbsCertificate;
777 const char *ccp;
778 static const char defaultVersion = 0; /* v1. */
779
780 cert->certificate.header = NULL;
781 cert->certificate.beg = beg;
782 cert->certificate.end = end;
783
784 /* Get the sequence content. */
785 if(!getASN1Element(&elem, beg, end))
786 return -1; /* Invalid bounds/size. */
787 beg = elem.beg;
788 end = elem.end;
789
790 /* Get tbsCertificate. */
791 beg = getASN1Element(&tbsCertificate, beg, end);
792 if(!beg)
793 return -1;
794 /* Skip the signatureAlgorithm. */
795 beg = getASN1Element(&cert->signatureAlgorithm, beg, end);
796 if(!beg)
797 return -1;
798 /* Get the signatureValue. */
799 if(!getASN1Element(&cert->signature, beg, end))
800 return -1;
801
802 /* Parse TBSCertificate. */
803 beg = tbsCertificate.beg;
804 end = tbsCertificate.end;
805 /* Get optional version, get serialNumber. */
806 cert->version.header = NULL;
807 cert->version.beg = &defaultVersion;
808 cert->version.end = &defaultVersion + sizeof(defaultVersion);
809 beg = getASN1Element(&elem, beg, end);
810 if(!beg)
811 return -1;
812 if(elem.tag == 0) {
813 if(!getASN1Element(&cert->version, elem.beg, elem.end))
814 return -1;
815 beg = getASN1Element(&elem, beg, end);
816 if(!beg)
817 return -1;
818 }
819 cert->serialNumber = elem;
820 /* Get signature algorithm. */
821 beg = getASN1Element(&cert->signatureAlgorithm, beg, end);
822 /* Get issuer. */
823 beg = getASN1Element(&cert->issuer, beg, end);
824 if(!beg)
825 return -1;
826 /* Get notBefore and notAfter. */
827 beg = getASN1Element(&elem, beg, end);
828 if(!beg)
829 return -1;
830 ccp = getASN1Element(&cert->notBefore, elem.beg, elem.end);
831 if(!ccp)
832 return -1;
833 if(!getASN1Element(&cert->notAfter, ccp, elem.end))
834 return -1;
835 /* Get subject. */
836 beg = getASN1Element(&cert->subject, beg, end);
837 if(!beg)
838 return -1;
839 /* Get subjectPublicKeyAlgorithm and subjectPublicKey. */
840 beg = getASN1Element(&cert->subjectPublicKeyInfo, beg, end);
841 if(!beg)
842 return -1;
843 ccp = getASN1Element(&cert->subjectPublicKeyAlgorithm,
844 cert->subjectPublicKeyInfo.beg,
845 cert->subjectPublicKeyInfo.end);
846 if(!ccp)
847 return -1;
848 if(!getASN1Element(&cert->subjectPublicKey, ccp,
849 cert->subjectPublicKeyInfo.end))
850 return -1;
851 /* Get optional issuerUniqueID, subjectUniqueID and extensions. */
852 cert->issuerUniqueID.tag = cert->subjectUniqueID.tag = 0;
853 cert->extensions.tag = elem.tag = 0;
854 cert->issuerUniqueID.header = cert->subjectUniqueID.header = NULL;
855 cert->issuerUniqueID.beg = cert->issuerUniqueID.end = "";
856 cert->subjectUniqueID.beg = cert->subjectUniqueID.end = "";
857 cert->extensions.header = NULL;
858 cert->extensions.beg = cert->extensions.end = "";
859 if(beg < end) {
860 beg = getASN1Element(&elem, beg, end);
861 if(!beg)
862 return -1;
863 }
864 if(elem.tag == 1) {
865 cert->issuerUniqueID = elem;
866 if(beg < end) {
867 beg = getASN1Element(&elem, beg, end);
868 if(!beg)
869 return -1;
870 }
871 }
872 if(elem.tag == 2) {
873 cert->subjectUniqueID = elem;
874 if(beg < end) {
875 beg = getASN1Element(&elem, beg, end);
876 if(!beg)
877 return -1;
878 }
879 }
880 if(elem.tag == 3)
881 if(!getASN1Element(&cert->extensions, elem.beg, elem.end))
882 return -1;
883 return 0;
884}