crypto: complete p256, add more tests and refine the signatures

This commit is contained in:
Michael Muré
2025-06-23 14:13:48 +02:00
parent 5be4ab8175
commit 162aff3046
9 changed files with 182 additions and 24 deletions

View File

@@ -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.

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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)
}
})
})

View File

@@ -10,7 +10,7 @@ const (
// TODO
PublicKeySize = 33
PrivateKeySize = 32
SignatureSize = 123456
SignatureSize = 64
MultibaseCode = uint64(0x1200)
)

View File

@@ -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[:])
}

View File

@@ -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)
}

1
go.mod
View File

@@ -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 (

2
go.sum
View File

@@ -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=