highCVE-2026-61544Sep 15, 2026

CVE-2026-61544: libp2p-quic Remote Panic via Certificate Expiry Race

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A remote attacker can crash any application running a libp2p QUIC listener by presenting a short-lived TLS certificate and deliberately delaying the final handshake packet until the certificate…

Packagelibp2p-quic
Ecosystemrust
Affected< 0.13.1
Fixed in0.13.1
CVE-2026-61544: libp2p-quic Remote Panic via Certificate Expiry Race

The problem

In libp2p-quic < 0.13.1, the post-handshake upgrade path in transports/quic/src/connection/connecting.rs (lines 65-66) calls libp2p_tls::certificate::parse() a second time after Quinn reports handshake completion, and wraps the result in .expect() with no error handling.

libp2p_tls::certificate::parse() re-runs full certificate verification on every invocation, including a wall-clock not_after validity check. If the certificate expires in the window between the first parse (during TLS handshake) and the second parse (post-handshake upgrade), the expect panics, crashing the entire process.

No authentication or malformed packets are required.

Proof of concept

A working proof-of-concept for CVE-2026-61544 in libp2p-quic, with the exact payload below.

bash
# Attacker-side steps (no special tooling needed beyond a custom libp2p node):
#
# 1. Generate a libp2p identity keypair.
# 2. Issue a self-signed libp2p TLS certificate with a very short validity window,
#    e.g. not_before = now, not_after = now + 2s.
# 3. Open a QUIC connection to the target libp2p listener and present this certificate.
# 4. Complete the TLS 1.3 handshake up to (but not including) the final client
#    Finished fragment. Hold that packet.
# 5. Wait until the certificate's not_after timestamp has elapsed.
# 6. Send the withheld final Finished fragment.
#
# Result: the listener's TLS layer accepts the handshake (cert was valid at parse time),
# Quinn signals handshake completion, libp2p-quic re-parses the certificate in the
# post-handshake upgrade path, libp2p_tls::certificate::parse() now sees an expired cert,
# and the .expect("peer certificate is valid") call panics, crashing the process.
#
# Relevant panicking code (pre-patch, connecting.rs L65-66):
#   let peer_id = certificate::parse(&peer_cert)
#       .expect("peer certificate is valid");

The root cause is a TOCTOU (CWE-362) race on certificate validity combined with an unconditional .expect() (CWE-617). libp2p_tls::certificate::parse() is not idempotent across time because it re-evaluates the wall-clock not_after field on every call. The post-handshake call in connecting.rs assumed the result could never differ from the handshake-time parse, which is false for short-lived certificates.

The patch (PR #6525, commits 212f3774 and e8f35e12) replaces the .expect() with proper Result propagation, so an expired certificate at post-handshake parse time closes the connection gracefully instead of panicking. No change to the certificate spec or handshake flow was needed; the fix is purely about handling the error path that was already possible.

The fix

Upgrade libp2p-quic to version **0.13.1** or later. The fix is in PR #6525 (commits 212f3774 and e8f35e12): the .expect() on the post-handshake certificate re-parse is replaced with a graceful error return, so an expired certificate causes a connection close rather than a process panic.

Reporter not attributed.

References: [1][2][3][4][5]

Related research