From 162aff3046b1d1971a8a4f5eb0fe046b3f75a947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Mon, 23 Jun 2025 14:13:48 +0200 Subject: [PATCH] crypto: complete p256, add more tests and refine the signatures --- crypto/ed25519/private.go | 13 ++++- crypto/ed25519/public.go | 21 +++++++- crypto/interface.go | 6 ++- crypto/internal/testSuite.go | 99 ++++++++++++++++++++++++++++++------ crypto/p256/key.go | 2 +- crypto/p256/private.go | 32 +++++++++++- crypto/p256/public.go | 30 ++++++++++- go.mod | 1 + go.sum | 2 + 9 files changed, 182 insertions(+), 24 deletions(-) diff --git a/crypto/ed25519/private.go b/crypto/ed25519/private.go index 67d4370..94c1dc8 100644 --- a/crypto/ed25519/private.go +++ b/crypto/ed25519/private.go @@ -6,6 +6,8 @@ import ( "encoding/pem" "fmt" + "golang.org/x/crypto/cryptobyte" + "github.com/INFURA/go-did/crypto" ) @@ -58,10 +60,19 @@ func (p PrivateKey) Public() crypto.PublicKey { return PublicKey{k: p.k.Public().(ed25519.PublicKey)} } -func (p PrivateKey) Sign(message []byte) ([]byte, error) { +func (p PrivateKey) SignToBytes(message []byte) ([]byte, error) { return ed25519.Sign(p.k, message), nil } +// SignToASN1 creates a signature with ASN.1 encoding. +// This ASN.1 encoding uses a BIT STRING, which would be correct for an X.509 certificate. +func (p PrivateKey) SignToASN1(message []byte) ([]byte, error) { + sig := ed25519.Sign(p.k, message) + var b cryptobyte.Builder + b.AddASN1BitString(sig) + return b.Bytes() +} + func (p PrivateKey) ToBytes() []byte { // Copy the private key to a fixed size buffer that can get allocated on the // caller's stack after inlining. diff --git a/crypto/ed25519/public.go b/crypto/ed25519/public.go index fedad65..9bd0be0 100644 --- a/crypto/ed25519/public.go +++ b/crypto/ed25519/public.go @@ -3,9 +3,12 @@ package ed25519 import ( "crypto/ed25519" "crypto/x509" + "encoding/asn1" "encoding/pem" "fmt" + "golang.org/x/crypto/cryptobyte" + "github.com/INFURA/go-did/crypto" "github.com/INFURA/go-did/crypto/internal" ) @@ -94,6 +97,22 @@ func (p PublicKey) Equal(other crypto.PublicKey) bool { return false } -func (p PublicKey) Verify(message, signature []byte) bool { +func (p PublicKey) VerifyBytes(message, signature []byte) bool { return ed25519.Verify(p.k, message, signature) } + +// VerifyASN1 verifies a signature with ASN.1 encoding. +// This ASN.1 encoding uses a BIT STRING, which would be correct for an X.509 certificate. +func (p PublicKey) VerifyASN1(message, signature []byte) bool { + var s cryptobyte.String = signature + var bitString asn1.BitString + + if !s.ReadASN1BitString(&bitString) { + return false + } + if bitString.BitLength != SignatureSize*8 { + return false + } + + return ed25519.Verify(p.k, message, bitString.Bytes) +} diff --git a/crypto/interface.go b/crypto/interface.go index 2ddc5cb..fef0e05 100644 --- a/crypto/interface.go +++ b/crypto/interface.go @@ -21,13 +21,15 @@ type PrivateKey interface { type SigningPublicKey interface { PublicKey - Verify(message, signature []byte) bool + VerifyBytes(message, signature []byte) bool + VerifyASN1(message, signature []byte) bool } type SigningPrivateKey interface { PrivateKey - Sign(message []byte) ([]byte, error) + SignToBytes(message []byte) ([]byte, error) + SignToASN1(message []byte) ([]byte, error) } type KeyExchangePublicKey interface { diff --git a/crypto/internal/testSuite.go b/crypto/internal/testSuite.go index d76068b..c69a9e2 100644 --- a/crypto/internal/testSuite.go +++ b/crypto/internal/testSuite.go @@ -44,18 +44,28 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har x509PemPubSize int pkcs8PemPrivSize int + + sigRawSize int + sigAsn1Size int }{} t.Cleanup(func() { out := strings.Builder{} - w := tabwriter.NewWriter(&out, 0, 0, 3, ' ', 0) + out.WriteString("\nKeypairs (in bytes):\n") + w := tabwriter.NewWriter(&out, 0, 0, 3, ' ', 0) _, _ = fmt.Fprintln(w, "\tPublic key\tPrivate key") _, _ = fmt.Fprintf(w, "Bytes\t%v\t%v\n", stats.bytesPubSize, stats.bytesPrivSize) _, _ = fmt.Fprintf(w, "DER (pub:x509, priv:PKCS#8)\t%v\t%v\n", stats.x509DerPubSize, stats.pkcs8DerPrivSize) _, _ = fmt.Fprintf(w, "PEM (pub:x509, priv:PKCS#8)\t%v\t%v\n", stats.x509PemPubSize, stats.pkcs8PemPrivSize) _ = w.Flush() + out.WriteString("\nSignatures (in bytes):\n") + w.Init(&out, 0, 0, 3, ' ', 0) + _, _ = fmt.Fprintln(w, "Raw bytes\tASN.1") + _, _ = fmt.Fprintf(w, "%v\t%v\n", stats.sigRawSize, stats.sigAsn1Size) + _ = w.Flush() + t.Logf("Test result for %s:\n%s\n", harness.Name, out.String()) }) @@ -172,18 +182,46 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har t.Skip("Signature is not implemented") } - msg := []byte("message") + for _, tc := range []struct { + name string + signer func(msg []byte) ([]byte, error) + verifier func(msg []byte, sig []byte) bool + expectedSize int + stats *int + }{ + { + name: "Bytes signature", + signer: spriv.SignToBytes, + verifier: spub.VerifyBytes, + expectedSize: harness.SignatureSize, + stats: &stats.sigRawSize, + }, + { + name: "ASN.1 signature", + signer: spriv.SignToASN1, + verifier: spub.VerifyASN1, + stats: &stats.sigAsn1Size, + }, + } { + t.Run(tc.name, func(t *testing.T) { + msg := []byte("message") - sig, err := spriv.Sign(msg) - require.NoError(t, err) - require.NotEmpty(t, sig) - require.Equal(t, harness.SignatureSize, len(sig)) + sig, err := tc.signer(msg) + require.NoError(t, err) + require.NotEmpty(t, sig) - valid := spub.Verify(msg, sig) - require.True(t, valid) + if tc.expectedSize > 0 { + require.Equal(t, tc.expectedSize, len(sig)) + } + *tc.stats = len(sig) - valid = spub.Verify([]byte("wrong message"), sig) - require.False(t, valid) + valid := tc.verifier(msg, sig) + require.True(t, valid) + + valid = tc.verifier([]byte("wrong message"), sig) + require.False(t, valid) + }) + } }) t.Run("KeyExchange", func(t *testing.T) { @@ -358,7 +396,7 @@ func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, ha b.Skip("Signature is not implemented") } - b.Run("Sign", func(b *testing.B) { + b.Run("Sign to Bytes signature", func(b *testing.B) { _, priv, err := harness.GenerateKeyPair() require.NoError(b, err) @@ -368,24 +406,55 @@ func BenchSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](b *testing.B, ha b.ReportAllocs() for i := 0; i < b.N; i++ { - spriv.Sign([]byte("message")) + _, _ = spriv.SignToBytes([]byte("message")) } }) - b.Run("Verify", func(b *testing.B) { + b.Run("Verify from Bytes signature", func(b *testing.B) { pub, priv, err := harness.GenerateKeyPair() require.NoError(b, err) spub := (crypto.PublicKey(pub)).(crypto.SigningPublicKey) spriv := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey) - sig, err := spriv.Sign([]byte("message")) + sig, err := spriv.SignToBytes([]byte("message")) require.NoError(b, err) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - spub.Verify([]byte("message"), sig) + spub.VerifyBytes([]byte("message"), sig) + } + }) + + b.Run("Sign to ASN.1 signature", func(b *testing.B) { + _, priv, err := harness.GenerateKeyPair() + require.NoError(b, err) + + spriv := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + _, _ = spriv.SignToASN1([]byte("message")) + } + }) + + b.Run("Verify from ASN.1 signature", func(b *testing.B) { + pub, priv, err := harness.GenerateKeyPair() + require.NoError(b, err) + + spub := (crypto.PublicKey(pub)).(crypto.SigningPublicKey) + spriv := (crypto.PrivateKey(priv)).(crypto.SigningPrivateKey) + sig, err := spriv.SignToASN1([]byte("message")) + require.NoError(b, err) + + b.ResetTimer() + b.ReportAllocs() + + for i := 0; i < b.N; i++ { + spub.VerifyASN1([]byte("message"), sig) } }) }) diff --git a/crypto/p256/key.go b/crypto/p256/key.go index f7c862f..1258a44 100644 --- a/crypto/p256/key.go +++ b/crypto/p256/key.go @@ -10,7 +10,7 @@ const ( // TODO PublicKeySize = 33 PrivateKeySize = 32 - SignatureSize = 123456 + SignatureSize = 64 MultibaseCode = uint64(0x1200) ) diff --git a/crypto/p256/private.go b/crypto/p256/private.go index bdc50a2..f996458 100644 --- a/crypto/p256/private.go +++ b/crypto/p256/private.go @@ -4,6 +4,7 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" + "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" @@ -89,6 +90,33 @@ func (p *PrivateKey) ToPKCS8PEM() string { })) } -func (p *PrivateKey) Sign(message []byte) ([]byte, error) { - return (*ecdsa.PrivateKey)(p).Sign(rand.Reader, message, nil) +/* + Note: signatures for the crypto.SigningPrivateKey interface assumes SHA256, + which should be correct almost always. If there is a need to use a different + hash function, we can add separate functions that have that flexibility. +*/ + +func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) { + // Hash the message with SHA-256 + hash := sha256.Sum256(message) + + r, s, err := ecdsa.Sign(rand.Reader, (*ecdsa.PrivateKey)(p), hash[:]) + if err != nil { + return nil, err + } + + sig := make([]byte, 64) + r.FillBytes(sig[:32]) + s.FillBytes(sig[32:]) + + return sig, nil +} + +func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) { + // Hash the message with SHA-256 + hash := sha256.Sum256(message) + + // Use ecdsa.SignASN1 for direct ASN.1 DER encoding + return ecdsa.SignASN1(rand.Reader, (*ecdsa.PrivateKey)(p), hash[:]) + } diff --git a/crypto/p256/public.go b/crypto/p256/public.go index 5b4d895..4ebbbd3 100644 --- a/crypto/p256/public.go +++ b/crypto/p256/public.go @@ -3,9 +3,11 @@ package p256 import ( "crypto/ecdsa" "crypto/elliptic" + "crypto/sha256" "crypto/x509" "encoding/pem" "fmt" + "math/big" "github.com/INFURA/go-did/crypto" helpers "github.com/INFURA/go-did/crypto/internal" @@ -94,6 +96,30 @@ func (p *PublicKey) ToX509PEM() string { })) } -func (p *PublicKey) Verify(message, signature []byte) bool { - panic("not implemented") +/* + Note: signatures for the crypto.SigningPrivateKey interface assumes SHA256, + which should be correct almost always. If there is a need to use a different + hash function, we can add separate functions that have that flexibility. +*/ + +func (p *PublicKey) VerifyBytes(message, signature []byte) bool { + if len(signature) != SignatureSize { + return false + } + + // Hash the message with SHA-256 + hash := sha256.Sum256(message) + + r := new(big.Int).SetBytes(signature[:32]) + s := new(big.Int).SetBytes(signature[32:]) + + // Use ecdsa.Verify + return ecdsa.Verify((*ecdsa.PublicKey)(p), hash[:], r, s) +} + +func (p *PublicKey) VerifyASN1(message, signature []byte) bool { + // Hash the message with SHA-256 + hash := sha256.Sum256(message) + + return ecdsa.VerifyASN1((*ecdsa.PublicKey)(p), hash[:], signature) } diff --git a/go.mod b/go.mod index f7a2543..5011e7d 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-varint v0.0.7 github.com/stretchr/testify v1.10.0 + golang.org/x/crypto v0.39.0 ) require ( diff --git a/go.sum b/go.sum index 12b76c6..efbce26 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=