HexBrain

curl · Report #4128701

Pinned public key check passes for a key the server does not hold with the wolfSSL backend

N/Astatus
lowseverity
8upvotes
22 Oct 2026disclosed

How it was found

Summary:

With the wolfSSL backend, --pinnedpubkey is checked against a public key that curl extracts by re-parsing the server certificate with its own ASN.1 parser (lib/vtls/x509asn1.c), while wolfSSL verifies the chain with its own parser. curl 8.23.0 added a check that the validity field of the TBSCertificate is fully consumed by its two dates, but the two fields parsed right before it get no such check: the optional version [0] element and the signature AlgorithmIdentifier, whose return value is not even NULL-checked. A CA-signed certificate whose version [0] or signature element declares a length that swallows the real issuer, validity, subject and public key A, with a tail carrying a second issuer, validity, subject and public key B inside a BIT STRING that wolfSSL skips, is chain-verified by wolfSSL 5.9.2 with key A while curl pins key B.

This breaks pinnedpubkey.md (https://curl.se/docs/manpage.html#--pinnedpubkey, source https://github.com/curl/curl/blob/curl-8_23_0/docs/cmdline-opts/pinnedpubkey.md):

A public key is extracted from this certificate and if it does not exactly match the public key provided to this option, curl aborts the connection before sending or receiving any data.

Precondition: wolfSSL 5.9.2 or older (5.9.4 rejects the certificate itself), the same precondition as the related advisory published with 8.23.0. lib/vtls/schannel.c lines 1140 to 1160 use the same parser for pinning and are likely affected on Windows; not tested here. mbedTLS, GnuTLS and OpenSSL take the key from the TLS library and are not affected.

Affected version

curl 8.23.0 and master 7d091c71fd built with wolfSSL 5.9.2, macOS. The master change for indefinite-length elements does not cover this.

Steps To Reproduce:

  1. Build a certificate signed by a test CA with real fields and key A, where the signature SEQUENCE (or version [0]) declares an oversized length, and append a fake issuer, validity, subject and public key B inside an issuerUniqueID BIT STRING. Serve it on 127.0.0.1:37000 with key A only. Pin B is the hash of a key that exists only in the test directory.

  2. Run:

curl --cacert ca.pem --pinnedpubkey "sha256//<pin B>" https://127.0.0.1:37000/
  1. Result on 8.23.0 built with wolfSSL 5.9.2:
pin B:  exit=0  http=200   public key hash matches pinned value
pin A:  exit=90 http=000   public key does not match pinned public key
server log: REQUEST GET / Host=127.0.0.1:37000 TLS=304

Control certificate (well-formed, key A): pin A exit 0, pin B exit 90. The certificate layout fixed in 8.23.0 is refused by this build (parse error, exit 35), so this is a different, unfixed path. Same certificate on wolfSSL 5.9.4, OpenSSL, mbedTLS, Apple: rejected by the TLS library (exit 35); GnuTLS pins its own key A and refuses pin B (exit 90). The generator script, server and run.sh are attached.

Root cause, lib/vtls/x509asn1.c Curl_parseX509():

  if(elem.tag == 0) {
    if(!getASN1Element(&cert->version, elem.beg, elem.end))
      return -1;
    beg = getASN1Element(&elem, beg, end);
    ...
  }
  cert->serialNumber = elem;
  /* Get signature algorithm. */
  beg = getASN1Element(&cert->signatureAlgorithm, beg, end);
  /* Get issuer. */
  beg = getASN1Element(&cert->issuer, beg, end);

Proposed fix (verified against wolfSSL 5.9.2: both crafted certificates refused, control still pins correctly, revert brings the bypass back):

--- a/lib/vtls/x509asn1.c
+++ b/lib/vtls/x509asn1.c
@@ -816,14 +816,28 @@ int Curl_parseX509(struct Curl_X509certificate *cert,
   if(elem.tag == 0) {
     if(!getASN1Element(&cert->version, elem.beg, elem.end))
       return -1;
+    if(cert->version.end != elem.end)
+      return -1;
     beg = getASN1Element(&elem, beg, end);
     if(!beg)
       return -1;
   }
   cert->serialNumber = elem;
   /* Get signature algorithm. */
   beg = getASN1Element(&cert->signatureAlgorithm, beg, end);
+  if(!beg)
+    return -1;
+  ccp = getASN1Element(&elem, cert->signatureAlgorithm.beg,
+                       cert->signatureAlgorithm.end);
+  if(!ccp || elem.tag != CURL_ASN1_OBJECT_IDENTIFIER)
+    return -1;
+  if(ccp < cert->signatureAlgorithm.end) {
+    ccp = getASN1Element(&elem, ccp, cert->signatureAlgorithm.end);
+    if(!ccp)
+      return -1;
+  }
+  if(ccp != cert->signatureAlgorithm.end)
+    return -1;
   /* Get issuer. */
   beg = getASN1Element(&cert->issuer, beg, end);
   if(!beg)

{F7036411}

Impact

An on-path attacker holding any certificate chain the client trusts, but not the key the application pinned, serves a crafted certificate; curl built with wolfSSL reports the pin as matching and sends the request. Pinning is used precisely when the CA store is not enough, so this removes the extra guarantee the user configured.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original