libp2p / libp2p/go-libp2p

webtransport: deterministic cert generation breaks with go 1.26 (crypto/ecdsa ignores io.Reader)

Open
#3,473 1 comment 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Go
Stars
6.9k
Forks
1.3k
Avg merge
13d 21h
Merged PRs (30d)
1

Description

## 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

```bash
# 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](https://go.dev/doc/go1.26#crypto/ecdsa):

> The `GenerateKey`, `PrivateKey.Sign`, and `SignASN1` functions now ignore the `io.Reader` parameter and always use the system CSPRNG.

The current `generateCert` implementation:
1. Creates an HKDF reader from the libp2p key + start time salt
2. Passes this reader to `ecdsa.GenerateKey(elliptic.P256(), hkdfReader)` — **ignored in Go 1.26**
3. 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:

1. **`ecdsa.ParseRawPrivateKey(curve, scalarBytes)`** — Constructs an ECDSA private key from raw scalar bytes, bypassing `GenerateKey` entirely. Derive 32 bytes from HKDF → deterministic P256 key.

2. **`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)

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in p2p/transport/webtransport/crypto.go at generateCert, then read the deterministic tests in p2p/transport/webtransport/crypto_test.go and the listed cert-manager, WebTransport, and transport tests. Check the Go 1.26 ecdsa APIs described in the issue and verify that certificate generation remains deterministic across runs and reboots. Run the provided Go 1.26 test command and confirm all six failing tests pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
cryptography, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.