add support for P-384

This commit is contained in:
Michael Muré
2025-06-25 15:53:29 +02:00
parent 2809127a08
commit 5230212c86
13 changed files with 406 additions and 49 deletions

View File

@@ -0,0 +1,32 @@
package allkeys
import (
"fmt"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/ed25519"
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/x25519"
)
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) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}
// PublicKeyFromPublicKeyMultibase decodes the public key from its PublicKeyMultibase form
func PublicKeyFromPublicKeyMultibase(multibase string) (crypto.PublicKey, error) {
code, pubBytes, err := helpers.PublicKeyMultibaseDecode(multibase)
if err != nil {
return nil, fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
decoder, ok := decoders[code]
if !ok {
return nil, fmt.Errorf("unsupported publicKeyMultibase code: %d", code)
}
return decoder(pubBytes)
}

View File

@@ -10,7 +10,7 @@ import (
"golang.org/x/crypto/cryptobyte"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.SigningPublicKey = &PublicKey{}

View File

@@ -106,9 +106,9 @@ func (p *PrivateKey) SignToBytes(message []byte) ([]byte, error) {
return nil, err
}
sig := make([]byte, 64)
r.FillBytes(sig[:32])
s.FillBytes(sig[32:])
sig := make([]byte, SignatureBytesSize)
r.FillBytes(sig[:SignatureBytesSize/2])
s.FillBytes(sig[SignatureBytesSize/2:])
return sig, nil
}

View File

@@ -10,7 +10,7 @@ import (
"math/big"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/_helpers"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.SigningPublicKey = (*PublicKey)(nil)
@@ -118,8 +118,8 @@ func (p *PublicKey) VerifyBytes(message, signature []byte) bool {
// Hash the message with SHA-256
hash := sha256.Sum256(message)
r := new(big.Int).SetBytes(signature[:32])
s := new(big.Int).SetBytes(signature[32:])
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)
}

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

@@ -0,0 +1,32 @@
package p384
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
)
const (
// PublicKeyBytesSize is the size, in bytes, of public keys in raw bytes.
PublicKeyBytesSize = 49
// PrivateKeyBytesSize is the size, in bytes, of private keys in raw bytes.
PrivateKeyBytesSize = 48
// SignatureBytesSize is the size, in bytes, of signatures in raw bytes.
SignatureBytesSize = 96
MultibaseCode = uint64(0x1201)
)
func GenerateKeyPair() (*PublicKey, *PrivateKey, error) {
priv, err := ecdsa.GenerateKey(elliptic.P384(), 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/p384/key_test.go Normal file
View File

@@ -0,0 +1,31 @@
package p384
import (
"testing"
"github.com/INFURA/go-did/crypto/_testsuite"
)
var harness = testsuite.TestHarness[*PublicKey, *PrivateKey]{
Name: "p384",
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/p384/private.go Normal file
View File

@@ -0,0 +1,145 @@
package p384
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-384 private key size")
}
res := &ecdsa.PrivateKey{
D: new(big.Int).SetBytes(b),
PublicKey: ecdsa.PublicKey{Curve: elliptic.P384()},
}
// 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 SHA384,
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-384
hash := sha512.Sum384(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-384
hash := sha512.Sum384(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/p384/public.go Normal file
View File

@@ -0,0 +1,132 @@
package p384
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-384 public key size")
}
x, y := elliptic.UnmarshalCompressed(elliptic.P384(), b)
if x == nil {
return nil, fmt.Errorf("invalid P-384 public key")
}
return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P384(), X: x, Y: y}), nil
}
// PublicKeyFromXY converts x and y coordinates into a PublicKey.
func PublicKeyFromXY(x, y *big.Int) (*PublicKey, error) {
if !elliptic.P384().IsOnCurve(x, y) {
return nil, fmt.Errorf("invalid P-384 public key")
}
return (*PublicKey)(&ecdsa.PublicKey{Curve: elliptic.P384(), 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.P384(), ecdsaPub.X, ecdsaPub.Y)
}
func (p *PublicKey) ToPublicKeyMultibase() string {
ecdsaPub := (*ecdsa.PublicKey)(p)
bytes := elliptic.MarshalCompressed(elliptic.P384(), 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 SHA384,
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-384
hash := sha512.Sum384(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-384
hash := sha512.Sum384(message)
return ecdsa.VerifyASN1((*ecdsa.PublicKey)(p), hash[:], signature)
}

View File

@@ -8,8 +8,8 @@ import (
"math/big"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/ed25519"
helpers "github.com/INFURA/go-did/crypto/internal"
)
var _ crypto.PublicKey = (*PublicKey)(nil)

View File

@@ -6,9 +6,10 @@ import (
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
"github.com/INFURA/go-did/crypto/_helpers"
allkeys "github.com/INFURA/go-did/crypto/_allkeys"
"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/x25519"
"github.com/INFURA/go-did/verifications/ed25519"
"github.com/INFURA/go-did/verifications/multikey"
@@ -38,16 +39,7 @@ func Decode(identifier string) (did.DID, error) {
msi := identifier[len(keyPrefix):]
code, bytes, err := helpers.PublicKeyMultibaseDecode(msi)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
decoder, ok := decoders[code]
if !ok {
return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code)
}
pub, err := decoder(bytes)
pub, err := allkeys.PublicKeyFromPublicKeyMultibase(msi)
if err != nil {
return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err)
}
@@ -58,12 +50,6 @@ func Decode(identifier string) (did.DID, error) {
return d, nil
}
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) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}
func FromPublicKey(pub crypto.PublicKey) (did.DID, error) {
switch pub := pub.(type) {
case ed25519.PublicKey:
@@ -76,7 +62,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:
case *p256.PublicKey, *p384.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

@@ -9,6 +9,7 @@ import (
"github.com/INFURA/go-did/crypto"
"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/x25519"
)
@@ -22,6 +23,16 @@ type jwk struct {
func (j jwk) MarshalJSON() ([]byte, error) {
switch pubkey := j.pubkey.(type) {
case ed25519.PublicKey:
return json.Marshal(struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
X string `json:"x"`
}{
Kty: "OKP",
Crv: "Ed25519",
X: base64.RawURLEncoding.EncodeToString(pubkey.ToBytes()),
})
case *p256.PublicKey:
return json.Marshal(struct {
Kty string `json:"kty"`
@@ -34,15 +45,17 @@ func (j jwk) MarshalJSON() ([]byte, error) {
X: base64.RawURLEncoding.EncodeToString(pubkey.X.Bytes()),
Y: base64.RawURLEncoding.EncodeToString(pubkey.Y.Bytes()),
})
case ed25519.PublicKey:
case *p384.PublicKey:
return json.Marshal(struct {
Kty string `json:"kty"`
Crv string `json:"crv"`
X string `json:"x"`
Y string `json:"y"`
}{
Kty: "OKP",
Crv: "Ed25519",
X: base64.RawURLEncoding.EncodeToString(pubkey.ToBytes()),
Kty: "EC",
Crv: "P-384",
X: base64.RawURLEncoding.EncodeToString(pubkey.X.Bytes()),
Y: base64.RawURLEncoding.EncodeToString(pubkey.Y.Bytes()),
})
case *x25519.PublicKey:
return json.Marshal(struct {
@@ -89,6 +102,9 @@ func (j *jwk) UnmarshalJSON(bytes []byte) error {
case "P-256":
j.pubkey, err = p256.PublicKeyFromXY(x, y)
return err
case "P-384":
j.pubkey, err = p384.PublicKeyFromXY(x, y)
return err
default:
return fmt.Errorf("unsupported Curve %s", aux["crv"])

View File

@@ -7,10 +7,7 @@ import (
"github.com/INFURA/go-did"
"github.com/INFURA/go-did/crypto"
helpers "github.com/INFURA/go-did/crypto/_helpers"
"github.com/INFURA/go-did/crypto/ed25519"
"github.com/INFURA/go-did/crypto/p256"
"github.com/INFURA/go-did/crypto/x25519"
allkeys "github.com/INFURA/go-did/crypto/_allkeys"
)
// Specification: https://www.w3.org/TR/cid-1.0/#Multikey
@@ -74,15 +71,7 @@ func (m *MultiKey) UnmarshalJSON(bytes []byte) error {
return errors.New("invalid controller")
}
code, pubBytes, err := helpers.PublicKeyMultibaseDecode(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
decoder, ok := decoders[code]
if !ok {
return fmt.Errorf("unsupported publicKeyMultibase code: %d", code)
}
m.pubkey, err = decoder(pubBytes)
m.pubkey, err = allkeys.PublicKeyFromPublicKeyMultibase(aux.PublicKeyMultibase)
if err != nil {
return fmt.Errorf("invalid publicKeyMultibase: %w", err)
}
@@ -90,12 +79,6 @@ func (m *MultiKey) UnmarshalJSON(bytes []byte) error {
return nil
}
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) },
x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) },
}
func (m MultiKey) ID() string {
return m.id
}