CVE-2026-87866: wolfSSL public-key pinning bypass
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
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.
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
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.
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.
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.
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.
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)
Source code
| 772 | int 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 | } |