add support for P-521

This commit is contained in:
Michael Muré
2025-06-25 16:27:24 +02:00
parent 5230212c86
commit 8d96b49622
7 changed files with 359 additions and 1 deletions

View File

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

32
crypto/p521/key.go Normal file
View File

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

31
crypto/p521/key_test.go Normal file
View File

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

145
crypto/p521/private.go Normal file
View File

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

132
crypto/p521/public.go Normal file
View File

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

View File

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

View File

@@ -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"])