feat(webrtc): stable /certhash across restarts#3512
Conversation
Derive the webrtc-direct DTLS cert deterministically from the host private key (HKDF + Go 1.24 deterministic ECDSA + hardcoded validity window) so the /certhash multiaddr component depends only on the host key. The advertised address no longer churns on every restart, and cached peerstore/DHT entries stay valid. cert.go documents why this is safe under the libp2p webrtc-direct spec and current browser DTLS behavior.
| priv, err := keygen.ECDSA(elliptic.P256(), ecdsaSeed) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("derive ECDSA key: %w", err) | ||
| } |
There was a problem hiding this comment.
Can you use webrtc.GenerateCertificate(priv) for the certificate creation?
There was a problem hiding this comment.
Sadly no, it's non-deterministic: random serial, NotBefore/NotAfter from time.Now(), and it signs with rand.Reader. The DER bytes change every run, so /certhash would shift on each restart, which is the behavior (bug?) this PR fixes. NewCertificate has the same problem since it also signs with rand.Reader, so Go 1.24's deterministic-ECDSA path never kicks in. That's why the cert is built by hand with a nil-rand signer and wrapped via webrtc.CertificateFromX509.
|
@MarcoPolo This looks fine to me, but I'd like your review on this. |
Switch the deterministic cert to the stdlib crypto/hkdf (Go 1.24+), derive the serial unsigned so it is always positive, and pin the fingerprint for a fixed key with a golden vector. The golden vector turns any drift in the derivation (filippo.io/keygen, the x509 encoder, or this code) into a CI failure instead of a silent, network-visible /certhash change for every node. A note marks the serial and signer logic as intentionally local to this transport rather than shared with webtransport, since the two are independent on the wire and in browsers and may diverge.
The serial was read as int64 and negated when negative, but math.MinInt64 negates back to itself, so that one HKDF-derived value stayed negative. x509.CreateCertificate rejects a negative serial (RFC 5280 4.1.2.2 requires a positive one), so cert generation would fail for that key. Found while fuzzing the deterministic-cert serial against the RFC. The bug predates this branch, but it is fixed here, alongside the webrtc cert changes, rather than in a separate PR: both transports share the same serial-derivation pattern and splitting the fix would conflict with the webrtc cert work in this branch. Read the serial unsigned and clamp to at least 1.
|
Pushed two small follow-ups here, both belong in this PR (nothing that needs a separate change). The cert serial derivation wasn't RFC 5280 compliant: it read the bytes as a signed int64 and flipped the sign when negative, but math.MinInt64 flips back to itself, so that one value stayed negative and x509.CreateCertificate refuses a negative serial. I ran into it while fuzzing the serial against the RFC. webtransport had the same pattern, so I fixed both rather than leave one half-done. I also added a golden-vector test that pins the certhash for a fixed key, so the derivation can't drift without CI catching it. No behavior change otherwise. The webrtc side also moves to the stdlib crypto/hkdf, which produces identical bytes. |
Problem
webrtc-direct minted a fresh random ECDSA cert on every process start. The cert's SHA-256 is the
/certhashin the listen multiaddr, so the advertised address changed on every restart and invalidated anything that had pinned it: peerstore TTLs, DHT records, external address books, dialer caches.Fix
Derive the DTLS cert deterministically from the libp2p host key, so the
/certhashdepends only on that key and stays put across restarts:crypto/hkdf) over the host key yields the serial and the ECDSA P-256 key; signing with a nil rand source is deterministic on Go 1.24+.2020-01-01to2120-01-01window keeps the DER byte-stable.math.MinInt64negative, whichx509.CreateCertificaterejects; I caught it fuzzing the serial against the RFC. webtransport shared the bug, so it's fixed here too./certhashfor a fixed key, so any drift in the derivation fails CI instead of silently changing every node's address.The DTLS cert isn't the identity credential (Noise XX inside DTLS handles that), so a stable cert is safe. Full rationale and spec links live in
p2p/transport/webrtc/cert.go.