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 run
Failing 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, and SignASN1 functions now ignore the io.Reader parameter 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 calls ecdsa.Sign which 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, bypassing GenerateKey entirely. Derive 32 bytes from HKDF → deterministic P256 key.
-
ecdsa.PrivateKey.Sign(nil, hash, opts) — When random is nil, Go 1.26 uses RFC 6979 deterministic nonces. Combined with x509.CreateCertificate(nil, ...), the entire cert becomes deterministic.
This approach:
- Requires no
GODEBUG workarounds
- 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)
Summary
WebTransport deterministic certificate generation breaks when upgrading
go.modtogo 1.26.0. ThegenerateCertfunction inp2p/transport/webtransport/crypto.gorelies on passing an HKDF reader toecdsa.GenerateKeyandx509.CreateCertificateto produce deterministic certificates. Go 1.26 madecrypto/ecdsafunctions ignore theio.Readerparameter entirely, always using the system CSPRNG instead.This means go-libp2p cannot upgrade to
go 1.26without first fixing the deterministic cert generation.Reproduction
Failing tests (6 total)
TestDeterministicSigp2p/transport/webtransport/crypto_test.goTestDeterministicCertHashesp2p/transport/webtransport/crypto_test.goTestDeterministicCertsAcrossRebootsp2p/transport/webtransport/cert_manager_test.goTestDeterministicCertsAfterRebootp2p/test/webtransport/webtransport_test.goTestServerRotatesCertCorrectlyp2p/transport/webtransport/transport_test.goTestServerRotatesCertCorrectlyAfterStepsp2p/transport/webtransport/transport_test.goActual failure output
Root Cause
Go 1.26 release notes:
The current
generateCertimplementation:ecdsa.GenerateKey(elliptic.P256(), hkdfReader)— ignored in Go 1.26x509.CreateCertificate(hkdfReader, ...)— internally callsecdsa.Signwhich also ignores it in Go 1.26Result: 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.xin go.mod because Go's GODEBUG compatibility mechanism (cryptocustomrand=1) preserves old behavior. The breakage occurs only whengo.modis updated togo 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:
GODEBUGworkaroundsImpact
Environment
7198ad34)