webtransport: deterministic cert generation breaks with go 1.26 (crypto/ecdsa ignores io.Reader)
Summary
WebTransport deterministic certificate generation breaks when upgrading go.mod to go 1.26.0. The generateCert function in p2p/transport/webtransport/crypto.go relies on passing an HKDF reader to ecdsa.GenerateKey and x509.CreateCertificate to produce deterministic certificates. Go 1.26 made crypto/ecdsa functions ignore the io.Reader parameter entirely, always using the system CSPRNG instead.
This means go-libp2p cannot upgrade to go 1.26 without first fixing the deterministic cert generation.
Reproduction
# On master (go 1.25.7 in go.mod) — tests pass with Go 1.26 binary
# because GODEBUG compatibility preserves old crypto behavior
GOTOOLCHAIN=go1.26.0 go test -run "TestDeterministicSig|TestDeterministicCertHashes" \
./p2p/transport/webtransport/ -v -count=1
# PASS
# Change go.mod to go 1.26.0 — GODEBUG compat no longer applies
go mod edit -go=1.26.0
GOTOOLCHAIN=go1.26.0 go test -run "TestDeterministicSig|TestDeterministicCertHashes" \
./p2p/transport/webtransport/ -v -count=1
# FAIL — signatures differ on every runFailing tests (6 total)
| Test | File |
|---|---|
TestDeterministicSig |
p2p/transport/webtransport/crypto_test.go |
TestDeterministicCertHashes |
p2p/transport/webtransport/crypto_test.go |
TestDeterministicCertsAcrossReboots |
p2p/transport/webtransport/cert_manager_test.go |
TestDeterministicCertsAfterReboot |
p2p/test/webtransport/webtransport_test.go |
TestServerRotatesCertCorrectly |
p2p/transport/webtransport/transport_test.go |
TestServerRotatesCertCorrectlyAfterSteps |
p2p/transport/webtransport/transport_test.go |
Actual failure output
--- FAIL: TestDeterministicCertHashes (0.00s)
crypto_test.go:151:
Error: Not equal:
expected: []byte{0x30, 0x45, 0x2, 0x21, 0x0, 0x9b, 0xb6, ...}
actual : []byte{0x30, 0x45, 0x2, 0x21, 0x0, 0x9c, 0x52, ...}
--- FAIL: TestDeterministicSig (0.00s)
crypto_test.go:191:
Error: Not equal:
expected: []byte{0x30, 0x46, 0x2, 0x21, 0x0, 0x89, 0xe3, ...}
actual : []byte{0x30, 0x45, 0x2, 0x21, 0x0, 0xb3, 0xca, ...}Root Cause
Go 1.26 release notes:
The
GenerateKey,PrivateKey.Sign, andSignASN1functions now ignore theio.Readerparameter and always use the system CSPRNG.
The current generateCert implementation:
- Creates an HKDF reader from the libp2p key + start time salt
- Passes this reader to
ecdsa.GenerateKey(elliptic.P256(), hkdfReader)— ignored in Go 1.26 - Passes it to
x509.CreateCertificate(hkdfReader, ...)— internally callsecdsa.Signwhich also ignores it in Go 1.26
Result: both the private key and the certificate signature become non-deterministic, breaking WebTransport's cert hash pinning across server restarts.
Note: This works on Go 1.26 binary with go 1.25.x in go.mod because Go's GODEBUG compatibility mechanism (cryptocustomrand=1) preserves old behavior. The breakage occurs only when go.mod is updated to go 1.26.0, which disables the GODEBUG shim.
Proposed Fix
Go 1.26 provides two new APIs that make deterministic cert generation straightforward:
ecdsa.ParseRawPrivateKey(curve, scalarBytes)— Constructs an ECDSA private key from raw scalar bytes, bypassingGenerateKeyentirely. Derive 32 bytes from HKDF → deterministic P256 key.ecdsa.PrivateKey.Sign(nil, hash, opts)— Whenrandomisnil, Go 1.26 uses RFC 6979 deterministic nonces. Combined withx509.CreateCertificate(nil, ...), the entire cert becomes deterministic.
This approach:
- Requires no
GODEBUGworkarounds - Uses Go 1.26's intended APIs
- Produces deterministic certs that are stable across restarts
- Is forward-compatible (won't break in Go 1.27+)
Impact
- Blocks Go 1.26 adoption for go-libp2p
- Production impact: WebTransport relies on deterministic cert hashes for QUIC connection resumption across server restarts. Non-deterministic certs would break cert hash pinning.
- All platforms affected: linux, macOS, windows
Environment
- go-libp2p master (
7198ad34) - Go 1.26.0
- Tested on Windows/amd64 (Intel Xeon @ 2.20GHz)
Source: libp2p/go-libp2p