From 418cf8ff40bf3c027ada462ea51bf9800f04dab7 Mon Sep 17 00:00:00 2001 From: Marco Munizaga Date: Fri, 20 Jun 2025 08:34:52 -0700 Subject: [PATCH] refactor(webtransport): Use keygen package for deterministic ecdsa key generation. --- go.mod | 2 + go.sum | 8 ++++ p2p/transport/webtransport/crypto.go | 53 +++++++++++++---------- p2p/transport/webtransport/crypto_test.go | 38 ---------------- test-plans/go.mod | 2 + test-plans/go.sum | 8 ++++ 6 files changed, 49 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index 726d375b5..709e0f012 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ retract v0.26.1 // Tag was applied incorrectly due to a bug in the release workf retract v0.36.0 // Accidentally modified the tag. require ( + filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b github.com/benbjohnson/clock v1.3.5 github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 @@ -66,6 +67,7 @@ require ( ) require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 39e9b093f..5556718e9 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,13 @@ +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -224,6 +230,8 @@ golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/p2p/transport/webtransport/crypto.go b/p2p/transport/webtransport/crypto.go index 90504ead0..a2daae6cd 100644 --- a/p2p/transport/webtransport/crypto.go +++ b/p2p/transport/webtransport/crypto.go @@ -2,6 +2,7 @@ package libp2pwebtransport import ( "bytes" + "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/sha256" @@ -17,6 +18,8 @@ import ( "golang.org/x/crypto/hkdf" + "filippo.io/keygen" + ic "github.com/libp2p/go-libp2p/core/crypto" "github.com/multiformats/go-multihash" @@ -71,11 +74,16 @@ func generateCert(key ic.PrivKey, start, end time.Time) (*x509.Certificate, *ecd BasicConstraintsValid: true, } - caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader) + ecdsaSeed := make([]byte, 192/8) // 192 bits of entropy required for P256 + if _, err := io.ReadFull(deterministicHKDFReader, ecdsaSeed); err != nil { + return nil, nil, err + } + + caPrivateKey, err := keygen.ECDSA(elliptic.P256(), ecdsaSeed) if err != nil { return nil, nil, err } - caBytes, err := x509.CreateCertificate(deterministicHKDFReader, certTempl, certTempl, caPrivateKey.Public(), caPrivateKey) + caBytes, err := x509.CreateCertificate(deterministicHKDFReader, certTempl, certTempl, caPrivateKey.Public(), deterministicSigner{caPrivateKey}) if err != nil { return nil, nil, err } @@ -136,29 +144,26 @@ func verifyRawCerts(rawCerts [][]byte, certHashes []multihash.DecodedMultihash) return nil } -// deterministicReader is a hack. It counter-acts the Go library's attempt at -// making ECDSA signatures non-deterministic. Go adds non-determinism by -// randomly dropping a singly byte from the reader stream. This counteracts this -// by detecting when a read is a single byte and using a different reader -// instead. -type deterministicReader struct { - reader io.Reader - singleByteReader io.Reader -} - func newDeterministicReader(seed []byte, salt []byte, info string) io.Reader { - reader := hkdf.New(sha256.New, seed, salt, []byte(info)) - singleByteReader := hkdf.New(sha256.New, seed, salt, []byte(info+" single byte")) - - return &deterministicReader{ - reader: reader, - singleByteReader: singleByteReader, - } + return hkdf.New(sha256.New, seed, salt, []byte(info)) } -func (r *deterministicReader) Read(p []byte) (n int, err error) { - if len(p) == 1 { - return r.singleByteReader.Read(p) - } - return r.reader.Read(p) +// deterministicSigner wraps an ecdsa.PrivateKey and exposes a `Sign` method +// that will produce deterministic signatures by ignoring the rand reader. +// Go 1.24 produces deterministic ecdsa signatures when passed a nil random source. +// See: https://go.dev/doc/go1.24#cryptoecdsapkgcryptoecdsa +type deterministicSigner struct { + priv *ecdsa.PrivateKey +} + +var _ crypto.Signer = deterministicSigner{} + +func (ds deterministicSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + // Ignore the rand reader to produce deterministic signatures. + _ = rand + return ds.priv.Sign(nil, digest, opts) +} + +func (ds deterministicSigner) Public() crypto.PublicKey { + return ds.priv.Public() } diff --git a/p2p/transport/webtransport/crypto_test.go b/p2p/transport/webtransport/crypto_test.go index b4f806ca4..8c9cd6b2a 100644 --- a/p2p/transport/webtransport/crypto_test.go +++ b/p2p/transport/webtransport/crypto_test.go @@ -11,7 +11,6 @@ import ( "crypto/x509" "crypto/x509/pkix" "fmt" - "io" "math/big" mrand "math/rand" "testing" @@ -155,40 +154,3 @@ func TestDeterministicCertHashes(t *testing.T) { require.Equal(t, keyBytes, keyBytes2) } } - -// TestDeterministicSig tests that our hack around making ECDSA signatures -// deterministic works. If this fails, this means we need to try another -// strategy to make deterministic signatures or try something else entirely. -// See deterministicReader for more context. -func TestDeterministicSig(t *testing.T) { - // Run this test 1000 times since we want to make sure the signatures are deterministic - runs := 1000 - for range runs { - zeroSeed := [32]byte{} - deterministicHKDFReader := newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo) - b := [1024]byte{} - io.ReadFull(deterministicHKDFReader, b[:]) - caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader) - require.NoError(t, err) - - sig, err := caPrivateKey.Sign(deterministicHKDFReader, b[:], crypto.SHA256) - require.NoError(t, err) - - deterministicHKDFReader = newDeterministicReader(zeroSeed[:], nil, deterministicCertInfo) - b2 := [1024]byte{} - io.ReadFull(deterministicHKDFReader, b2[:]) - caPrivateKey2, err := ecdsa.GenerateKey(elliptic.P256(), deterministicHKDFReader) - require.NoError(t, err) - - sig2, err := caPrivateKey2.Sign(deterministicHKDFReader, b2[:], crypto.SHA256) - require.NoError(t, err) - - keyBytes, err := x509.MarshalECPrivateKey(caPrivateKey) - require.NoError(t, err) - keyBytes2, err := x509.MarshalECPrivateKey(caPrivateKey2) - require.NoError(t, err) - - require.Equal(t, sig, sig2) - require.Equal(t, keyBytes, keyBytes2) - } -} diff --git a/test-plans/go.mod b/test-plans/go.mod index d9db143bd..888ab4ddf 100644 --- a/test-plans/go.mod +++ b/test-plans/go.mod @@ -9,6 +9,8 @@ require ( ) require ( + filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 // indirect + filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect diff --git a/test-plans/go.sum b/test-plans/go.sum index 21d586420..01f9f93e5 100644 --- a/test-plans/go.sum +++ b/test-plans/go.sum @@ -1,7 +1,13 @@ +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5 h1:JA0fFr+kxpqTdxR9LOBiTWpGNchqmkcsgmdeJZRclZ0= +filippo.io/bigmod v0.1.1-0.20260103110540-f8a47775ebe5/go.mod h1:OjOXDNlClLblvXdwgFFOQFJEocLhhtai8vGLy0JCZlI= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b h1:REI1FbdW71yO56Are4XAxD+OS/e+BQsB3gE4mZRQEXY= +filippo.io/keygen v0.0.0-20260114151900-8e2790ea4c5b/go.mod h1:9nnw1SlYHYuPSo/3wjQzNjSbeHlq2NsKo5iEtfJPWP0= github.com/benbjohnson/clock v1.3.5 h1:VvXlSJBzZpA/zum6Sj74hxwYI2DIxRWuNIoXAzHZz5o= github.com/benbjohnson/clock v1.3.5/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3 h1:oe6fCvaEpkhyW3qAicT0TnGtyht/UrgvOwMcEgLb7Aw= +github.com/canonical/go-sp800.90a-drbg v0.0.0-20210314144037-6eeb1040d6c3/go.mod h1:qdP0gaj0QtgX2RUZhnlVrceJ+Qln8aSlDyJwelLLFeM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -220,6 +226,8 @@ golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=