From 8d96b49622e7eed0da9b3ac68e829190471b05f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Wed, 25 Jun 2025 16:27:24 +0200 Subject: [PATCH] add support for P-521 --- crypto/_allkeys/allkeys.go | 2 + crypto/p521/key.go | 32 +++++++ crypto/p521/key_test.go | 31 +++++++ crypto/p521/private.go | 145 ++++++++++++++++++++++++++++++++ crypto/p521/public.go | 132 +++++++++++++++++++++++++++++ methods/did-key/key.go | 3 +- verifications/jsonwebkey/jwk.go | 15 ++++ 7 files changed, 359 insertions(+), 1 deletion(-) create mode 100644 crypto/p521/key.go create mode 100644 crypto/p521/key_test.go create mode 100644 crypto/p521/private.go create mode 100644 crypto/p521/public.go diff --git a/crypto/_allkeys/allkeys.go b/crypto/_allkeys/allkeys.go index 2749ac0..3791dbf 100644 --- a/crypto/_allkeys/allkeys.go +++ b/crypto/_allkeys/allkeys.go @@ -8,6 +8,7 @@ import ( helpers "github.com/INFURA/go-did/crypto/internal" "github.com/INFURA/go-did/crypto/p256" "github.com/INFURA/go-did/crypto/p384" + "github.com/INFURA/go-did/crypto/p521" "github.com/INFURA/go-did/crypto/x25519" ) @@ -15,6 +16,7 @@ var decoders = map[uint64]func(b []byte) (crypto.PublicKey, error){ ed25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return ed25519.PublicKeyFromBytes(b) }, p256.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p256.PublicKeyFromBytes(b) }, p384.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p384.PublicKeyFromBytes(b) }, + p521.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p521.PublicKeyFromBytes(b) }, x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) }, } diff --git a/crypto/p521/key.go b/crypto/p521/key.go new file mode 100644 index 0000000..c461528 --- /dev/null +++ b/crypto/p521/key.go @@ -0,0 +1,32 @@ +package p521 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" +) + +const ( + // PublicKeyBytesSize is the size, in bytes, of public keys in raw bytes. + PublicKeyBytesSize = 67 + // PrivateKeyBytesSize is the size, in bytes, of private keys in raw bytes. + PrivateKeyBytesSize = 66 + // SignatureBytesSize is the size, in bytes, of signatures in raw bytes. + SignatureBytesSize = 132 + + MultibaseCode = uint64(0x1202) +) + +func GenerateKeyPair() (*PublicKey, *PrivateKey, error) { + priv, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader) + if err != nil { + return nil, nil, err + } + pub := priv.Public().(*ecdsa.PublicKey) + return (*PublicKey)(pub), (*PrivateKey)(priv), nil +} + +const ( + pemPubBlockType = "PUBLIC KEY" + pemPrivBlockType = "PRIVATE KEY" +) diff --git a/crypto/p521/key_test.go b/crypto/p521/key_test.go new file mode 100644 index 0000000..79a82fc --- /dev/null +++ b/crypto/p521/key_test.go @@ -0,0 +1,31 @@ +package p521 + +import ( + "testing" + + "github.com/INFURA/go-did/crypto/_testsuite" +) + +var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{ + Name: "p521", + GenerateKeyPair: GenerateKeyPair, + PublicKeyFromBytes: PublicKeyFromBytes, + PublicKeyFromPublicKeyMultibase: PublicKeyFromPublicKeyMultibase, + PublicKeyFromX509DER: PublicKeyFromX509DER, + PublicKeyFromX509PEM: PublicKeyFromX509PEM, + PrivateKeyFromBytes: PrivateKeyFromBytes, + PrivateKeyFromPKCS8DER: PrivateKeyFromPKCS8DER, + PrivateKeyFromPKCS8PEM: PrivateKeyFromPKCS8PEM, + MultibaseCode: MultibaseCode, + PublicKeyBytesSize: PublicKeyBytesSize, + PrivateKeyBytesSize: PrivateKeyBytesSize, + SignatureBytesSize: SignatureBytesSize, +} + +func TestSuite(t *testing.T) { + testsuite.TestSuite(t, harness) +} + +func BenchmarkSuite(b *testing.B) { + testsuite.BenchSuite(b, harness) +} diff --git a/crypto/p521/private.go b/crypto/p521/private.go new file mode 100644 index 0000000..71d7301 --- /dev/null +++ b/crypto/p521/private.go @@ -0,0 +1,145 @@ +package p521 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha512" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + + "github.com/INFURA/go-did/crypto" +) + +var _ crypto.SigningPrivateKey = (*PrivateKey)(nil) +var _ crypto.KeyExchangePrivateKey = (*PrivateKey)(nil) + +type PrivateKey ecdsa.PrivateKey + +// PrivateKeyFromBytes converts a serialized public key to a PrivateKey. +// This compact serialization format is the raw key material, without metadata or structure. +// It errors if the slice is not the right size. +func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) { + if len(b) != PrivateKeyBytesSize { + return nil, fmt.Errorf("invalid P-521 private key size") + } + + res := &ecdsa.PrivateKey{ + D: new(big.Int).SetBytes(b), + PublicKey: ecdsa.PublicKey{Curve: elliptic.P521()}, + } + + // recompute the public key + res.PublicKey.X, res.PublicKey.Y = res.PublicKey.Curve.ScalarBaseMult(b) + + return (*PrivateKey)(res), nil +} + +// PrivateKeyFromPKCS8DER decodes a PKCS#8 DER (binary) encoded private key. +func PrivateKeyFromPKCS8DER(bytes []byte) (*PrivateKey, error) { + priv, err := x509.ParsePKCS8PrivateKey(bytes) + if err != nil { + return nil, err + } + ecdsaPriv := priv.(*ecdsa.PrivateKey) + return (*PrivateKey)(ecdsaPriv), nil +} + +// PrivateKeyFromPKCS8PEM decodes an PKCS#8 PEM (string) encoded private key. +func PrivateKeyFromPKCS8PEM(str string) (*PrivateKey, error) { + block, _ := pem.Decode([]byte(str)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if block.Type != pemPrivBlockType { + return nil, fmt.Errorf("incorrect PEM block type") + } + return PrivateKeyFromPKCS8DER(block.Bytes) +} + +func (p *PrivateKey) Equal(other crypto.PrivateKey) bool { + if other, ok := other.(*PrivateKey); ok { + return (*ecdsa.PrivateKey)(p).Equal((*ecdsa.PrivateKey)(other)) + } + return false +} + +func (p *PrivateKey) Public() crypto.PublicKey { + ecdhPub := (*ecdsa.PrivateKey)(p).Public().(*ecdsa.PublicKey) + return (*PublicKey)(ecdhPub) +} + +func (p *PrivateKey) ToBytes() []byte { + // fixed size buffer that can get allocated on the caller's stack after inlining. + var buf [PrivateKeyBytesSize]byte + ((*ecdsa.PrivateKey)(p)).D.FillBytes(buf[:]) + return buf[:] +} + +func (p *PrivateKey) ToPKCS8DER() []byte { + res, _ := x509.MarshalPKCS8PrivateKey((*ecdsa.PrivateKey)(p)) + return res +} + +func (p *PrivateKey) ToPKCS8PEM() string { + der := p.ToPKCS8DER() + return string(pem.EncodeToMemory(&pem.Block{ + Type: pemPrivBlockType, + Bytes: der, + })) +} + +/* + Note: signatures for the crypto.SigningPrivateKey interface assumes SHA512, + 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-512 + hash := sha512.Sum512(message) + + r, s, err := ecdsa.Sign(rand.Reader, (*ecdsa.PrivateKey)(p), hash[:]) + if err != nil { + return nil, err + } + + sig := make([]byte, SignatureBytesSize) + r.FillBytes(sig[:SignatureBytesSize/2]) + s.FillBytes(sig[SignatureBytesSize/2:]) + + return sig, nil +} + +func (p *PrivateKey) SignToASN1(message []byte) ([]byte, error) { + // Hash the message with SHA-512 + hash := sha512.Sum512(message) + + return ecdsa.SignASN1(rand.Reader, (*ecdsa.PrivateKey)(p), hash[:]) +} + +func (p *PrivateKey) PublicKeyIsCompatible(remote crypto.PublicKey) bool { + if _, ok := remote.(*PublicKey); ok { + return true + } + return false +} + +func (p *PrivateKey) KeyExchange(remote crypto.PublicKey) ([]byte, error) { + if remote, ok := remote.(*PublicKey); ok { + // First, we need to convert the ECDSA (signing only) to the equivalent ECDH keys + ecdhPriv, err := (*ecdsa.PrivateKey)(p).ECDH() + if err != nil { + return nil, err + } + ecdhPub, err := (*ecdsa.PublicKey)(remote).ECDH() + if err != nil { + return nil, err + } + + return ecdhPriv.ECDH(ecdhPub) + } + return nil, fmt.Errorf("incompatible public key") +} diff --git a/crypto/p521/public.go b/crypto/p521/public.go new file mode 100644 index 0000000..5abab1e --- /dev/null +++ b/crypto/p521/public.go @@ -0,0 +1,132 @@ +package p521 + +import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/sha512" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" + + "github.com/INFURA/go-did/crypto" + helpers "github.com/INFURA/go-did/crypto/internal" +) + +var _ crypto.SigningPublicKey = (*PublicKey)(nil) + +type PublicKey ecdsa.PublicKey + +// PublicKeyFromBytes converts a serialized public key to a PublicKey. +// This compact serialization format is the raw key material, without metadata or structure. +// It errors if the slice is not the right size. +func PublicKeyFromBytes(b []byte) (*PublicKey, error) { + if len(b) != PublicKeyBytesSize { + return nil, fmt.Errorf("invalid P-521 public key size") + } + x, y := elliptic.UnmarshalCompressed(elliptic.P521(), b) + if x == nil { + return nil, fmt.Errorf("invalid P-521 public key") + } + return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}), nil +} + +// PublicKeyFromXY converts x and y coordinates into a PublicKey. +func PublicKeyFromXY(x, y *big.Int) (*PublicKey, error) { + if !elliptic.P521().IsOnCurve(x, y) { + return nil, fmt.Errorf("invalid P-521 public key") + } + return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P521(), X: x, Y: y}), nil +} + +// PublicKeyFromPublicKeyMultibase decodes the public key from its Multibase form +func PublicKeyFromPublicKeyMultibase(multibase string) (*PublicKey, error) { + code, bytes, err := helpers.PublicKeyMultibaseDecode(multibase) + if err != nil { + return nil, err + } + if code != MultibaseCode { + return nil, fmt.Errorf("invalid code") + } + return PublicKeyFromBytes(bytes) +} + +// PublicKeyFromX509DER decodes an X.509 DER (binary) encoded public key. +func PublicKeyFromX509DER(bytes []byte) (*PublicKey, error) { + pub, err := x509.ParsePKIXPublicKey(bytes) + if err != nil { + return nil, err + } + ecdsaPub := pub.(*ecdsa.PublicKey) + return (*PublicKey)(ecdsaPub), nil +} + +// PublicKeyFromX509PEM decodes an X.509 PEM (string) encoded public key. +func PublicKeyFromX509PEM(str string) (*PublicKey, error) { + block, _ := pem.Decode([]byte(str)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM block") + } + if block.Type != pemPubBlockType { + return nil, fmt.Errorf("incorrect PEM block type") + } + return PublicKeyFromX509DER(block.Bytes) +} + +func (p *PublicKey) Equal(other crypto.PublicKey) bool { + if other, ok := other.(*PublicKey); ok { + return (*ecdsa.PublicKey)(p).Equal((*ecdsa.PublicKey)(other)) + } + return false +} + +func (p *PublicKey) ToBytes() []byte { + ecdsaPub := (*ecdsa.PublicKey)(p) + return elliptic.MarshalCompressed(elliptic.P521(), ecdsaPub.X, ecdsaPub.Y) +} + +func (p *PublicKey) ToPublicKeyMultibase() string { + ecdsaPub := (*ecdsa.PublicKey)(p) + bytes := elliptic.MarshalCompressed(elliptic.P521(), ecdsaPub.X, ecdsaPub.Y) + return helpers.PublicKeyMultibaseEncode(MultibaseCode, bytes) +} + +func (p *PublicKey) ToX509DER() []byte { + res, _ := x509.MarshalPKIXPublicKey((*ecdsa.PublicKey)(p)) + return res +} + +func (p *PublicKey) ToX509PEM() string { + der := p.ToX509DER() + return string(pem.EncodeToMemory(&pem.Block{ + Type: pemPubBlockType, + Bytes: der, + })) +} + +/* + Note: signatures for the crypto.SigningPrivateKey interface assumes SHA512, + 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) != SignatureBytesSize { + return false + } + + // Hash the message with SHA-512 + hash := sha512.Sum512(message) + + r := new(big.Int).SetBytes(signature[:SignatureBytesSize/2]) + s := new(big.Int).SetBytes(signature[SignatureBytesSize/2:]) + + return ecdsa.Verify((*ecdsa.PublicKey)(p), hash[:], r, s) +} + +func (p *PublicKey) VerifyASN1(message, signature []byte) bool { + // Hash the message with SHA-512 + hash := sha512.Sum512(message) + + return ecdsa.VerifyASN1((*ecdsa.PublicKey)(p), hash[:], signature) +} diff --git a/methods/did-key/key.go b/methods/did-key/key.go index 9910336..e5423df 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -10,6 +10,7 @@ import ( "github.com/INFURA/go-did/crypto/ed25519" "github.com/INFURA/go-did/crypto/p256" "github.com/INFURA/go-did/crypto/p384" + "github.com/INFURA/go-did/crypto/p521" "github.com/INFURA/go-did/crypto/x25519" "github.com/INFURA/go-did/verifications/ed25519" "github.com/INFURA/go-did/verifications/multikey" @@ -62,7 +63,7 @@ func FromPublicKey(pub crypto.PublicKey) (did.DID, error) { xmsi := xpub.ToPublicKeyMultibase() d.keyAgreement = x25519vm.NewKeyAgreementKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, xmsi), xpub, d) return d, nil - case *p256.PublicKey, *p384.PublicKey: + case *p256.PublicKey, *p384.PublicKey, *p521.PublicKey: d := DidKey{msi: pub.ToPublicKeyMultibase()} mk := multikey.NewMultiKey(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d) d.signature = mk diff --git a/verifications/jsonwebkey/jwk.go b/verifications/jsonwebkey/jwk.go index 1a40a5d..308f786 100644 --- a/verifications/jsonwebkey/jwk.go +++ b/verifications/jsonwebkey/jwk.go @@ -57,6 +57,18 @@ func (j jwk) MarshalJSON() ([]byte, error) { X: base64.RawURLEncoding.EncodeToString(pubkey.X.Bytes()), Y: base64.RawURLEncoding.EncodeToString(pubkey.Y.Bytes()), }) + case *p521.PublicKey: + return json.Marshal(struct { + Kty string `json:"kty"` + Crv string `json:"crv"` + X string `json:"x"` + Y string `json:"y"` + }{ + Kty: "EC", + Crv: "P-521", + X: base64.RawURLEncoding.EncodeToString(pubkey.X.Bytes()), + Y: base64.RawURLEncoding.EncodeToString(pubkey.Y.Bytes()), + }) case *x25519.PublicKey: return json.Marshal(struct { Kty string `json:"kty"` @@ -105,6 +117,9 @@ func (j *jwk) UnmarshalJSON(bytes []byte) error { case "P-384": j.pubkey, err = p384.PublicKeyFromXY(x, y) return err + case "P-521": + j.pubkey, err = p521.PublicKeyFromXY(x, y) + return err default: return fmt.Errorf("unsupported Curve %s", aux["crv"])