From da1310b78a7d0eda024ae21b2fca16ad09a97293 Mon Sep 17 00:00:00 2001 From: Steve Moyer Date: Thu, 17 Oct 2024 07:42:40 -0400 Subject: [PATCH] feat(did): strengthens crypto for public key handliing --- did/crypto.go | 118 ++++++++++++++++++++++++++++++++++++++++++--- did/crypto_test.go | 32 ++++++++++++ did/did.go | 20 ++++++-- go.mod | 4 +- 4 files changed, 161 insertions(+), 13 deletions(-) diff --git a/did/crypto.go b/did/crypto.go index c79d5ef..d723f5e 100644 --- a/did/crypto.go +++ b/did/crypto.go @@ -1,9 +1,13 @@ package did import ( + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "errors" + "fmt" + "github.com/decred/dcrd/dcrec/secp256k1/v4" crypto "github.com/libp2p/go-libp2p/core/crypto" "github.com/libp2p/go-libp2p/core/crypto/pb" "github.com/multiformats/go-multicodec" @@ -46,21 +50,73 @@ func GenerateRSA() (crypto.PrivKey, DID, error) { return priv, did, err } +// GenerateECDSA generates an ECDSA private key and the matching DID +// for the default P256 curve. +func GenerateECDSA() (crypto.PrivKey, DID, error) { + return GenerateECDSAWithCurve(P256) +} + +// GenerateECDSAWithCurve generates an ECDSA private key and matching +// DID for the user-supplied curve +func GenerateECDSAWithCurve(code multicodec.Code) (crypto.PrivKey, DID, error) { + var curve elliptic.Curve + + switch code { + case P256: + curve = elliptic.P256() + case P384: + curve = elliptic.P384() + case P521: + curve = elliptic.P521() + default: + return nil, Undef, errors.New("unsupported ECDSA curve") + } + + priv, pub, err := crypto.GenerateECDSAKeyPairWithCurve(curve, rand.Reader) + if err != nil { + return nil, Undef, err + } + + _ = priv + _ = pub + + return nil, Undef, nil // TODO + +} + func FromPrivKey(privKey crypto.PrivKey) (DID, error) { return FromPubKey(privKey.GetPublic()) } +// FromPubKey returns a did:key constructed from the provided public key. func FromPubKey(pubKey crypto.PubKey) (DID, error) { - code, ok := map[pb.KeyType]multicodec.Code{ - pb.KeyType_Ed25519: multicodec.Ed25519Pub, - pb.KeyType_RSA: multicodec.RsaPub, - pb.KeyType_Secp256k1: multicodec.Secp256k1Pub, - pb.KeyType_ECDSA: multicodec.Es256, - }[pubKey.Type()] - if !ok { + var code multicodec.Code + + switch pubKey.Type() { + case pb.KeyType_Ed25519: + code = multicodec.Ed25519Pub + case pb.KeyType_RSA: + code = RSA + case pb.KeyType_Secp256k1: + code = Secp256k1 + case pb.KeyType_ECDSA: + var err error + if code, err = codeForCurve(pubKey); err != nil { + return Undef, err + } + default: return Undef, errors.New("unsupported key type") } + if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 { + var err error + + pubKey, err = coerceECDSAToSecp256k1(pubKey) + if err != nil { + return Undef, err + } + } + pubBytes, err := pubKey.Raw() if err != nil { return Undef, err @@ -80,3 +136,51 @@ func ToPubKey(s string) (crypto.PubKey, error) { return id.PubKey() } + +func codeForCurve(pubKey crypto.PubKey) (multicodec.Code, error) { + stdPub, err := crypto.PubKeyToStdKey(pubKey) + if err != nil { + return multicodec.Identity, err + } + + ecdsaPub, ok := stdPub.(*ecdsa.PublicKey) + if !ok { + return multicodec.Identity, errors.New("failed to assert type for code to curve") + } + + switch ecdsaPub.Curve { + case elliptic.P256(): + return P256, nil + case elliptic.P384(): + return P384, nil + case elliptic.P521(): + return P521, nil + case secp256k1.S256(): + return Secp256k1, nil + default: + return multicodec.Identity, fmt.Errorf("unsupported ECDSA curve: %s", ecdsaPub.Curve.Params().Name) + } +} + +func coerceECDSAToSecp256k1(pubKey crypto.PubKey) (crypto.PubKey, error) { + stdPub, err := crypto.PubKeyToStdKey(pubKey) + if err != nil { + return nil, err + } + + ecdsaPub, ok := stdPub.(*ecdsa.PublicKey) + if !ok { + return nil, errors.New("failed to assert type for secp256k1 coersion") + } + + ecdsaPubBytes := append([]byte{0x04}, append(ecdsaPub.X.Bytes(), ecdsaPub.Y.Bytes()...)...) + + secp256k1Pub, err := secp256k1.ParsePubKey(ecdsaPubBytes) + if err != nil { + return nil, err + } + + cryptoPub := crypto.Secp256k1PublicKey(*secp256k1Pub) + + return &cryptoPub, nil +} diff --git a/did/crypto_test.go b/did/crypto_test.go index 6ee6fcb..6395811 100644 --- a/did/crypto_test.go +++ b/did/crypto_test.go @@ -1,9 +1,13 @@ package did_test import ( + "crypto/elliptic" + "crypto/rand" "testing" "github.com/libp2p/go-libp2p/core/crypto" + "github.com/multiformats/go-multicodec" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/ucan-wg/go-ucan/did" @@ -17,6 +21,34 @@ const ( func TestFromPubKey(t *testing.T) { t.Parallel() + _, ecdsaP256, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P256(), rand.Reader) + require.NoError(t, err) + _, ecdsaP384, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P384(), rand.Reader) + require.NoError(t, err) + _, ecdsaP521, err := crypto.GenerateECDSAKeyPairWithCurve(elliptic.P521(), rand.Reader) + require.NoError(t, err) + _, secp256k1PubKey1, err := crypto.GenerateSecp256k1Key(rand.Reader) + require.NoError(t, err) + + test := func(pub crypto.PubKey, code multicodec.Code) func(t *testing.T) { + t.Helper() + + return func(t *testing.T) { + t.Parallel() + + id, err := did.FromPubKey(pub) + require.NoError(t, err) + p, err := id.PubKey() + require.NoError(t, err) + assert.Equal(t, pub, p) + } + } + + t.Run("ECDSA with P256 curve", test(ecdsaP256, did.P256)) + t.Run("ECDSA with P384 curve", test(ecdsaP384, did.P384)) + t.Run("ECDSA with P521 curve", test(ecdsaP521, did.P521)) + t.Run("With secp256k1 (secp256k1)", test(secp256k1PubKey1, did.Secp256k1)) + id, err := did.FromPubKey(examplePubKey(t)) require.NoError(t, err) require.Equal(t, exampleDID(t), id) diff --git a/did/did.go b/did/did.go index 9711178..111f210 100644 --- a/did/did.go +++ b/did/did.go @@ -10,10 +10,18 @@ import ( varint "github.com/multiformats/go-varint" ) -const Ed25519 = multicodec.Ed25519Pub // recommended -const P256 = multicodec.P256Pub -const Secp256k1 = multicodec.Secp256k1Pub -const RSA = multicodec.RsaPub +// Signature algorithms from the [did:key specification] +// +// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm +const ( + X25519 = multicodec.X25519Pub + Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended + P256 = multicodec.P256Pub // UCAN required + P384 = multicodec.P384Pub + P521 = multicodec.P521Pub + Secp256k1 = multicodec.Secp256k1Pub // UCAN required + RSA = multicodec.RsaPub +) // Undef can be used to represent a nil or undefined DID, using DID{} // directly is also acceptable. @@ -65,10 +73,14 @@ func (d DID) Defined() bool { return d.code == 0 || len(d.bytes) > 0 } +// PubKey returns the public key encapsulated in the did:key. func (d DID) PubKey() (crypto.PubKey, error) { unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{ + X25519: crypto.UnmarshalEd25519PublicKey, Ed25519: crypto.UnmarshalEd25519PublicKey, P256: crypto.UnmarshalECDSAPublicKey, + P384: crypto.UnmarshalECDSAPublicKey, + P521: crypto.UnmarshalECDSAPublicKey, Secp256k1: crypto.UnmarshalSecp256k1PublicKey, RSA: crypto.UnmarshalRsaPublicKey, }[d.code] diff --git a/go.mod b/go.mod index 28cd9c6..d5ad0f9 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/ucan-wg/go-ucan go 1.23 require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/ipfs/go-cid v0.4.1 github.com/ipld/go-ipld-prime v0.21.0 github.com/libp2p/go-libp2p v0.36.3 + github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-multicodec v0.9.0 github.com/multiformats/go-multihash v0.2.3 @@ -16,11 +18,9 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/minio/sha256-simd v1.0.1 // indirect - github.com/mr-tron/base58 v1.2.0 // indirect github.com/multiformats/go-base32 v0.1.0 // indirect github.com/multiformats/go-base36 v0.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect