CVE-2025-5025: No QUIC certificate pinning with wolfSSL
What the report claims
The report claims that curl with wolfSSL backend fails to invoke wssl_verify_pinned() for HTTP/3, causing certificate pinning to be bypassed.
Compare with curl's recorded outcome
Known vulnerability
A real vulnerability: CVE-2025-5025 No QUIC certificate pinning with wolfSSL
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?
- wssl_verify_pinned() function exists and implements certificate pinning for wolfSSL (lib/vtls/wolfssl.c:1529-1588)
- The report's core claim is that this function is not *called* for HTTP/3/QUIC connections, but the provided code does not include the QUIC connection setup, vtls callback registration, or wolfSSL QUIC integration points
- Deterministic check only confirms symbol existence, not call sites
- Reporter states pinning works for HTTP/1.1 and other TLS backends, suggesting a specific gap in HTTP/3 + wolfSSL
What still needs checking
- Call sites of wssl_verify_pinned() in the codebase (grep for wssl_verify_pinned)
- QUIC/HTTP3 connection establishment code for wolfSSL backend
- vtls filter/callback registration for wolfSSL in QUIC context
How this analysis ran 3 claims · 3 citations verified
Checked against curl master on 2025-05-19 (414ec13840fd). 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 report. The wssl_verify_pinned() function exists in wolfSSL backend (lib/vtls/wolfssl.c:1529), but we need to check whether the HTTP/3/QUIC code path invokes it. Could you confirm which curl version you tested (8.13.0 or master), and if possible, point to where the QUIC connection setup calls the TLS backend's verify_pinned callback? This will help us locate the missing hook.
Inspect all claims and source code 3 claims · 1 code excerpts
Report claims
wssl_verify_pinned() has not been implemented for HTTP/3
Read the reasoning
The provided code shows wssl_verify_pinned() is implemented (lines 1529-1588), but does not show where it is called from. The claim that it 'has not been implemented for HTTP/3' actually means it's not *invoked* for HTTP/3, which requires checking call sites in the QUIC/HTTP3 code path. Those files are not provided.
certificate pinning does not work when using HTTP/3 with wolfSSL
Read the reasoning
The pinning verification logic exists in wssl_verify_pinned(). Whether it works for HTTP/3 depends on whether the HTTP/3/QUIC code path calls this function (or an equivalent). The provided source does not include the QUIC connection setup or the vtls callback hooks for wolfSSL.
curl should invoke wssl_verify_pinned() but it has not been implemented
Read the reasoning
The claim that curl 'should invoke wssl_verify_pinned() but it has not been implemented' conflates implementation with invocation. The function is implemented; the question is whether the HTTP/3 code path invokes it. The provided code does not contain the relevant call sites.
What the report says about other projects
not addressed wolfSSL certificate manager verifyCallback is called for each cert loaded
Read the original report
Source code
| 1529 | static CURLcode wssl_verify_pinned(struct Curl_cfilter *cf, |
| C1: wssl_verify_pinned() function definition exists (lines 1529-1588), but call sites are not provided | |
| C2: wssl_verify_pinned() implements pinning verification logic, but HTTP/3 call path not shown | |
| C3: Function exists but invocation points for HTTP/3 not in provided code | |
| 1530 | struct Curl_easy *data) |
| 1531 | { |
| 1532 | struct ssl_connect_data *connssl = cf->ctx; |
| 1533 | #ifndef CURL_DISABLE_PROXY |
| 1534 | const char * const pinnedpubkey = Curl_ssl_cf_is_proxy(cf) ? |
| 1535 | data->set.str[STRING_SSL_PINNEDPUBLICKEY_PROXY] : |
| 1536 | data->set.str[STRING_SSL_PINNEDPUBLICKEY]; |
| 1537 | #else |
| 1538 | const char * const pinnedpubkey = data->set.str[STRING_SSL_PINNEDPUBLICKEY]; |
| 1539 | #endif |
| 1540 | |
| 1541 | if(pinnedpubkey) { |
| 1542 | #ifdef KEEP_PEER_CERT |
| 1543 | struct wssl_ctx *wssl = (struct wssl_ctx *)connssl->backend; |
| 1544 | WOLFSSL_X509 *x509; |
| 1545 | const char *x509_der; |
| 1546 | int x509_der_len; |
| 1547 | struct Curl_X509certificate x509_parsed; |
| 1548 | struct Curl_asn1Element *pubkey; |
| 1549 | CURLcode result; |
| 1550 | |
| 1551 | x509 = wolfSSL_get_peer_certificate(wssl->ssl); |
| 1552 | if(!x509) { |
| 1553 | failf(data, "SSL: failed retrieving server certificate"); |
| 1554 | return CURLE_SSL_PINNEDPUBKEYNOTMATCH; |
| 1555 | } |
| 1556 | |
| 1557 | x509_der = (const char *)wolfSSL_X509_get_der(x509, &x509_der_len); |
| 1558 | if(!x509_der) { |
| 1559 | failf(data, "SSL: failed retrieving ASN.1 server certificate"); |
| 1560 | return CURLE_SSL_PINNEDPUBKEYNOTMATCH; |
| 1561 | } |
| 1562 | |
| 1563 | memset(&x509_parsed, 0, sizeof(x509_parsed)); |
| 1564 | if(Curl_parseX509(&x509_parsed, x509_der, x509_der + x509_der_len)) |
| 1565 | return CURLE_SSL_PINNEDPUBKEYNOTMATCH; |
| 1566 | |
| 1567 | pubkey = &x509_parsed.subjectPublicKeyInfo; |
| 1568 | if(!pubkey->header || pubkey->end <= pubkey->header) { |
| 1569 | failf(data, "SSL: failed retrieving public key from server certificate"); |
| 1570 | return CURLE_SSL_PINNEDPUBKEYNOTMATCH; |
| 1571 | } |
| 1572 | |
| 1573 | result = Curl_pin_peer_pubkey(data, |
| 1574 | pinnedpubkey, |
| 1575 | (const unsigned char *)pubkey->header, |
| 1576 | (size_t)(pubkey->end - pubkey->header)); |
| 1577 | wolfSSL_FreeX509(x509); |
| 1578 | if(result) { |
| 1579 | failf(data, "SSL: public key does not match pinned public key"); |
| 1580 | return result; |
| 1581 | } |
| 1582 | #else |
| 1583 | failf(data, "Library lacks pinning support built-in"); |
| 1584 | return CURLE_NOT_BUILT_IN; |
| 1585 | #endif |
| 1586 | } |
| 1587 | return CURLE_OK; |
| 1588 | } |