diff --git a/crypto/_allkeys/allkeys.go b/crypto/_allkeys/allkeys.go new file mode 100644 index 0000000..2749ac0 --- /dev/null +++ b/crypto/_allkeys/allkeys.go @@ -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) +} diff --git a/crypto/ed25519/public.go b/crypto/ed25519/public.go index 76e64ba..e0f24bc 100644 --- a/crypto/ed25519/public.go +++ b/crypto/ed25519/public.go @@ -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{} diff --git a/crypto/_helpers/multibase.go b/crypto/internal/multibase.go similarity index 100% rename from crypto/_helpers/multibase.go rename to crypto/internal/multibase.go diff --git a/crypto/p256/private.go b/crypto/p256/private.go index adf4fa3..da2bb47 100644 --- a/crypto/p256/private.go +++ b/crypto/p256/private.go @@ -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 } diff --git a/crypto/p256/public.go b/crypto/p256/public.go index 57100e8..df9804e 100644 --- a/crypto/p256/public.go +++ b/crypto/p256/public.go @@ -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) } diff --git a/crypto/p384/key.go b/crypto/p384/key.go new file mode 100644 index 0000000..733faa5 --- /dev/null +++ b/crypto/p384/key.go @@ -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" +) diff --git a/crypto/p384/key_test.go b/crypto/p384/key_test.go new file mode 100644 index 0000000..cb56ecb --- /dev/null +++ b/crypto/p384/key_test.go @@ -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) +} diff --git a/crypto/p384/private.go b/crypto/p384/private.go new file mode 100644 index 0000000..7f421fb --- /dev/null +++ b/crypto/p384/private.go @@ -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") +} diff --git a/crypto/p384/public.go b/crypto/p384/public.go new file mode 100644 index 0000000..15fca37 --- /dev/null +++ b/crypto/p384/public.go @@ -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) +} diff --git a/crypto/x25519/public.go b/crypto/x25519/public.go index 18e02c3..62da6bc 100644 --- a/crypto/x25519/public.go +++ b/crypto/x25519/public.go @@ -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) diff --git a/methods/did-key/key.go b/methods/did-key/key.go index 1091560..9910336 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -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 diff --git a/verifications/jsonwebkey/jwk.go b/verifications/jsonwebkey/jwk.go index 9d4ea4a..1a40a5d 100644 --- a/verifications/jsonwebkey/jwk.go +++ b/verifications/jsonwebkey/jwk.go @@ -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"]) diff --git a/verifications/multikey/multikey.go b/verifications/multikey/multikey.go index 3bceedf..eb9d1cb 100644 --- a/verifications/multikey/multikey.go +++ b/verifications/multikey/multikey.go @@ -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 }