HexBrain

curl · Report #3973098

07: GnuTLS 0-RTT early data bypasses file-backed public-key pin verification

N/Astatus
8upvotes
2 Sept 2026disclosed

How it was found

GnuTLS 0-RTT early data bypasses file-backed public-key pin verification

Summary

When CURLSSLOPT_EARLYDATA is enabled and CURLOPT_PINNEDPUBLICKEY references a file path, replacing that file's contents between connections does not invalidate the TLS session cache. On the next connection, curl sends the HTTP request as TLS 1.3 early data to the formerly-pinned server before checking the current pin file. curl then returns CURLE_SSL_PINNEDPUBKEYNOTMATCH (90), but the server has already received the request payload.

This violates the documented CURLOPT_PINNEDPUBLICKEY guarantee: "curl aborts the connection before sending or receiving any data."

Affected versions

curl 8.11.0 through 8.21.0, plus tested 8.22.0 prerelease/development versions.

Root cause

ssl_peer_key_build() in lib/vtls/vtls_scache.c constructs the TLS session-cache identity using the literal pinned_key string — for a file-backed pin this is only the pathname, not the file contents:

if(ssl->pinned_key && ssl->pinned_key[0]) {
    result = curlx_dyn_addf(&buf, ":Pinned-%s", ssl->pinned_key);

Replacing the file at that path does not change the cache key, so an old session ticket remains eligible for resumption.

In gtls_connect_common() in lib/vtls/gtls.c, the execution order during session resumption is:

  1. gtls_send_earlydata() — sends request bytes as 0-RTT data
  2. cf_gtls_handshake() — completes the TLS handshake
  3. gtls_verifyserver() → pkp_pin_peer_pubkey() — reads the pin file and checks the key

The pin file is read after the early data has already been accepted by the server.

Reproduction

  1. Build an affected curl release, starting with 8.11.0, against GnuTLS with TLS 1.3 early-data support.
  2. Configure a TLS 1.3 HTTPS server with a hostname-valid certificate and enable session tickets, session resumption, and acceptance of 0-RTT early data.
  3. Create two unrelated public-key pins, pin A for the server's current key and pin B for a different key.
  4. Store pin A in a file and configure a libcurl easy handle with CURLOPT_PINNEDPUBLICKEY set to that file's pathname.
  5. Enable CURLSSLOPT_EARLYDATA, leave the TLS session cache enabled, and ensure normal certificate and hostname verification succeeds independently of the public-key pin.
  6. Make an initial request to the server. Confirm that pin A matches and that the server issues an early-data-capable TLS session ticket.
  7. Close the first connection while retaining the easy handle and its TLS session cache. Force the next transfer to open a fresh connection rather than reuse the existing connection.
  8. Replace the contents of the pin file with pin B without changing the file's pathname.
  9. Make a second request to the original server and include a unique marker or representative sensitive value in the request path, headers, or body.
  10. Record data received by the server and distinguish TLS 1.3 early data from application data received after the handshake.
  11. Observe the vulnerable result: the second connection resumes the cached session and the server receives the marked request as 0-RTT data before curl checks the updated pin file.
  12. Confirm that curl subsequently reports CURLE_SSL_PINNEDPUBKEYNOTMATCH even though the request has already reached the server.
  13. Repeat the same sequence with early data disabled. Confirm that session resumption may still occur and the pin check still fails, but the server receives no marked request.
  14. Confirm a fix by repeating the early-data sequence after binding the pin file's current contents to the session-cache identity, invalidating cached tickets when those contents change, or suppressing early data until the current pin has been verified. The marked request must not reach the server.

Expected result

After the pin file changes, curl must verify its current contents before sending any application data. A pin mismatch must prevent the server from receiving the request.

Vulnerable result

curl identifies the cached TLS session using the unchanged pin pathname, sends the new request as 0-RTT data, and only afterward discovers that the server key does not match the updated pin.

Suggested fix

Incorporate the file-backed pin material (a hash of the file contents) into the TLS session-cache identity instead of only the pathname, so that an in-place pin rotation invalidates any cached ticket. Alternatively, suppress early data when the pin source is a file whose current contents have not been bound into the session identity.

Impact

Summary:

A formerly trusted endpoint possessing the required TLS resumption state can receive and process a new 0-RTT request after its public-key pin has been revoked. This can disclose credentials, authorization headers, request bodies, or other sensitive information and may trigger unintended state-changing operations before curl reports the pin mismatch.

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