diff --git a/crypto/keys.go b/crypto/keys.go new file mode 100644 index 00000000..6c00d393 --- /dev/null +++ b/crypto/keys.go @@ -0,0 +1,138 @@ +package crypto + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "fmt" + + "github.com/pkg/errors" + + secp "github.com/decred/dcrd/dcrec/secp256k1/v4" + + "github.com/lestrrat-go/jwx/x25519" +) + +type KeyType string + +const ( + Ed25519 KeyType = "Ed25519" + X25519 KeyType = "X25519" + Secp256k1 KeyType = "secp256k1" + P224 KeyType = "P-224" + P256 KeyType = "P-256" + P384 KeyType = "P-384" + P521 KeyType = "P-521" + RSA KeyType = "RSA" + + RSAKeySize int = 2048 +) + +// GenerateKeyByKeyType creates a brand-new key, returning the public and private key for the given key type +func GenerateKeyByKeyType(kt KeyType) (crypto.PublicKey, crypto.PrivateKey, error) { + switch kt { + case Ed25519: + return GenerateEd25519Key() + case X25519: + return GenerateX25519Key() + case Secp256k1: + return GenerateSecp256k1Key() + case P224: + return GenerateP224Key() + case P256: + return GenerateP256Key() + case P384: + return GenerateP384Key() + case P521: + return GenerateP521Key() + case RSA: + return GenerateRSA2048Key() + } + return nil, nil, fmt.Errorf("unsupported key type: %s", kt) +} + +func PubKeyToBytes(key crypto.PublicKey) ([]byte, error) { + ed25519Key, ok := key.(ed25519.PublicKey) + if ok { + return ed25519Key, nil + } + + x25519Key, ok := key.(x25519.PublicKey) + if ok { + return x25519Key, nil + } + + secp256k1Key, ok := key.(secp.PublicKey) + if ok { + return secp256k1Key.SerializeCompressed(), nil + } + + ecdsaKey, ok := key.(ecdsa.PublicKey) + if ok { + return elliptic.Marshal(ecdsaKey.Curve, ecdsaKey.X, ecdsaKey.Y), nil + } + + rsaKey, ok := key.(rsa.PublicKey) + if ok { + return x509.MarshalPKCS1PublicKey(&rsaKey), nil + } + + return nil, errors.New("unknown public key type; could not convert to bytes") +} + +func GenerateEd25519Key() (ed25519.PublicKey, ed25519.PrivateKey, error) { + return ed25519.GenerateKey(rand.Reader) +} + +func GenerateX25519Key() (x25519.PublicKey, x25519.PrivateKey, error) { + return x25519.GenerateKey(rand.Reader) +} + +func GenerateSecp256k1Key() (secp.PublicKey, secp.PrivateKey, error) { + privKey, err := secp.GeneratePrivateKey() + if err != nil { + return secp.PublicKey{}, secp.PrivateKey{}, err + } + pubKey := privKey.PubKey() + return *pubKey, *privKey, nil +} + +func GenerateP224Key() (ecdsa.PublicKey, ecdsa.PrivateKey, error) { + return generateECDSAKey(elliptic.P224()) +} + +func GenerateP256Key() (ecdsa.PublicKey, ecdsa.PrivateKey, error) { + return generateECDSAKey(elliptic.P256()) +} + +func GenerateP384Key() (ecdsa.PublicKey, ecdsa.PrivateKey, error) { + return generateECDSAKey(elliptic.P384()) +} + +func GenerateP521Key() (ecdsa.PublicKey, ecdsa.PrivateKey, error) { + return generateECDSAKey(elliptic.P521()) +} + +func generateECDSAKey(curve elliptic.Curve) (ecdsa.PublicKey, ecdsa.PrivateKey, error) { + privKey, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return ecdsa.PublicKey{}, ecdsa.PrivateKey{}, err + } + return privKey.PublicKey, *privKey, nil +} + +func GenerateRSA2048Key() (rsa.PublicKey, rsa.PrivateKey, error) { + privKey, err := rsa.GenerateKey(rand.Reader, RSAKeySize) + if err != nil { + return rsa.PublicKey{}, rsa.PrivateKey{}, err + } + return privKey.PublicKey, *privKey, nil +} + +func GetSupportedKeyTypes() []KeyType { + return []KeyType{Ed25519, X25519, Secp256k1, P224, P256, P384, P521, RSA} +} diff --git a/cryptosuite/cryptosuite.go b/cryptosuite/cryptosuite.go index 8fb513cb..3aa487c2 100644 --- a/cryptosuite/cryptosuite.go +++ b/cryptosuite/cryptosuite.go @@ -38,7 +38,7 @@ type CryptoSuite interface { type CryptoSuiteInfo interface { ID() string - Type() string + Type() LDKeyType CanonicalizationAlgorithm() string MessageDigestAlgorithm() crypto.Hash SignatureAlgorithm() SignatureType diff --git a/cryptosuite/jsonwebkey2020.go b/cryptosuite/jsonwebkey2020.go index 7ed031a0..0cf3de46 100644 --- a/cryptosuite/jsonwebkey2020.go +++ b/cryptosuite/jsonwebkey2020.go @@ -3,36 +3,29 @@ package cryptosuite import ( - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "crypto/rsa" "encoding/base64" "encoding/json" "fmt" - "github.com/lestrrat-go/jwx/x25519" + "github.com/TBD54566975/did-sdk/crypto" "github.com/lestrrat-go/jwx/jwa" "github.com/lestrrat-go/jwx/jws" "github.com/lestrrat-go/jwx/jwk" - "github.com/TBD54566975/did-sdk/util" - - secp "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/pkg/errors" ) type ( - KTY string - CRV string - ALG string + KTY string + CRV string + ALG string + LDKeyType string ) const ( - JsonWebKey2020 string = "JsonWebKey2020" + JsonWebKey2020 LDKeyType = "JsonWebKey2020" // Supported key types @@ -44,20 +37,16 @@ const ( Ed25519 CRV = "Ed25519" X25519 CRV = "X25519" - SECP256k1 CRV = "secp256k1" + Secp256k1 CRV = "secp256k1" P256 CRV = "P-256" P384 CRV = "P-384" - - // Known key sizes - - RSAKeySize int = 2048 ) // JSONWebKey2020 complies with https://w3c-ccg.github.io/lds-jws2020/#json-web-key-2020 type JSONWebKey2020 struct { - ID string `json:"id,omitempty"` - Type string `json:"type,omitempty"` - Controller string `json:"controller,omitempty"` + ID string `json:"id,omitempty"` + Type LDKeyType `json:"type,omitempty"` + Controller string `json:"controller,omitempty"` PrivateKeyJWK `json:"privateKeyJwk,omitempty"` PublicKeyJWK `json:"publicKeyJwk,omitempty"` } @@ -96,11 +85,26 @@ type PublicKeyJWK struct { KID string `json:"kid,omitempty"` } +func ToPublicKeyJWK(key jwk.Key) (*PublicKeyJWK, error) { + keyBytes, err := json.Marshal(key) + if err != nil { + return nil, err + } + var pubKeyJWK PublicKeyJWK + if err := json.Unmarshal(keyBytes, &pubKeyJWK); err != nil { + return nil, err + } + return &pubKeyJWK, nil +} + // GenerateJSONWebKey2020 The JSONWebKey2020 type specifies a number of key type and curve pairs to enable JOSE conformance // these pairs are supported in this library and generated via the function below // https://w3c-ccg.github.io/lds-jws2020/#dfn-jsonwebkey2020 func GenerateJSONWebKey2020(kty KTY, crv *CRV) (*JSONWebKey2020, error) { if kty == RSA { + if crv != nil { + return nil, fmt.Errorf("RSA key type cannot have curve specified: %s", *crv) + } return GenerateRSAJSONWebKey2020() } if crv == nil { @@ -120,7 +124,7 @@ func GenerateJSONWebKey2020(kty KTY, crv *CRV) (*JSONWebKey2020, error) { } if kty == EC { switch curve { - case SECP256k1: + case Secp256k1: return GenerateSECP256k1JSONWebKey2020() case P256: return GenerateP256JSONWebKey2020() @@ -134,12 +138,12 @@ func GenerateJSONWebKey2020(kty KTY, crv *CRV) (*JSONWebKey2020, error) { } func GenerateRSAJSONWebKey2020() (*JSONWebKey2020, error) { - privateKey, err := rsa.GenerateKey(rand.Reader, RSAKeySize) + _, privKey, err := crypto.GenerateRSA2048Key() if err != nil { return nil, err } rsaJWK := jwk.NewRSAPrivateKey() - if err := rsaJWK.FromRaw(privateKey); err != nil { + if err := rsaJWK.FromRaw(&privKey); err != nil { return nil, errors.Wrap(err, "failed to generate rsa jwk") } kty := rsaJWK.KeyType().String() @@ -167,7 +171,7 @@ func GenerateRSAJSONWebKey2020() (*JSONWebKey2020, error) { } func GenerateEd25519JSONWebKey2020() (*JSONWebKey2020, error) { - _, privKey, err := util.GenerateEd25519Key() + _, privKey, err := crypto.GenerateEd25519Key() if err != nil { return nil, err } @@ -196,7 +200,7 @@ func GenerateEd25519JSONWebKey2020() (*JSONWebKey2020, error) { } func GenerateX25519JSONWebKey2020() (*JSONWebKey2020, error) { - _, privKey, err := x25519.GenerateKey(rand.Reader) + _, privKey, err := crypto.GenerateX25519Key() if err != nil { return nil, err } @@ -228,7 +232,7 @@ func GenerateSECP256k1JSONWebKey2020() (*JSONWebKey2020, error) { // We use the secp256k1 implementation from Decred https://github.com/decred/dcrd // which is utilized in the widely accepted go bitcoin node implementation from the btcsuite project // https://github.com/btcsuite/btcd/blob/master/btcec/btcec.go#L23 - privKey, err := secp.GeneratePrivateKey() + _, privKey, err := crypto.GenerateSecp256k1Key() if err != nil { return nil, err } @@ -260,12 +264,12 @@ func GenerateSECP256k1JSONWebKey2020() (*JSONWebKey2020, error) { } func GenerateP256JSONWebKey2020() (*JSONWebKey2020, error) { - privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + _, privKey, err := crypto.GenerateP256Key() if err != nil { return nil, err } p256JWK := jwk.NewECDSAPrivateKey() - if err := p256JWK.FromRaw(privKey); err != nil { + if err := p256JWK.FromRaw(&privKey); err != nil { return nil, errors.Wrap(err, "failed to generate p-256 jwk") } kty := p256JWK.KeyType().String() @@ -291,12 +295,12 @@ func GenerateP256JSONWebKey2020() (*JSONWebKey2020, error) { } func GenerateP384JSONWebKey2020() (*JSONWebKey2020, error) { - privKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + _, privKey, err := crypto.GenerateP384Key() if err != nil { return nil, err } p384JWK := jwk.NewECDSAPrivateKey() - if err := p384JWK.FromRaw(privKey); err != nil { + if err := p384JWK.FromRaw(&privKey); err != nil { return nil, errors.Wrap(err, "failed to generate p-384 jwk") } kty := p384JWK.KeyType().String() @@ -439,7 +443,7 @@ func AlgFromKeyAndCurve(kty jwa.KeyType, crv jwa.EllipticCurveAlgorithm) (jwa.Si if kty == jwa.EC { switch curve { - case jwa.EllipticCurveAlgorithm(SECP256k1): + case jwa.EllipticCurveAlgorithm(Secp256k1): return jwa.ES256K, nil case jwa.P256: return jwa.ES256, nil @@ -453,6 +457,9 @@ func AlgFromKeyAndCurve(kty jwa.KeyType, crv jwa.EllipticCurveAlgorithm) (jwa.Si } func crvPtr(crv CRV) *CRV { + if crv == "" { + return nil + } return &crv } diff --git a/cryptosuite/jsonwebkey2020_test.go b/cryptosuite/jsonwebkey2020_test.go index 8fd72128..9e66288b 100644 --- a/cryptosuite/jsonwebkey2020_test.go +++ b/cryptosuite/jsonwebkey2020_test.go @@ -24,7 +24,7 @@ func TestJSONWebKey2020SignerVerifier(t *testing.T) { { name: "secpk256k1", kty: EC, - crv: crvPtr(SECP256k1), + crv: crvPtr(Secp256k1), }, { name: "P-256", diff --git a/cryptosuite/jwssignaturesuite.go b/cryptosuite/jwssignaturesuite.go index 78ecbd3b..9e5a7dc7 100644 --- a/cryptosuite/jwssignaturesuite.go +++ b/cryptosuite/jwssignaturesuite.go @@ -1,3 +1,5 @@ +//go:build jwx_es256k + package cryptosuite import ( @@ -18,7 +20,7 @@ const ( JSONWebSignature2020Context string = "https://w3id.org/security/suites/jws-2020/v1" JSONWebSignature2020 SignatureType = "JsonWebSignature2020" JWSSignatureSuiteID string = "https://w3c-ccg.github.io/security-vocab/#JsonWebSignature2020" - JWSSignatureSuiteType = JsonWebKey2020 + JWSSignatureSuiteType LDKeyType = JsonWebKey2020 JWSSignatureSuiteCanonicalizationAlgorithm string = "https://w3id.org/security#URDNA2015" // JWSSignatureSuiteDigestAlgorithm uses https://www.rfc-editor.org/rfc/rfc4634 JWSSignatureSuiteDigestAlgorithm crypto.Hash = crypto.SHA256 @@ -34,7 +36,7 @@ func (j JWSSignatureSuite) ID() string { return JWSSignatureSuiteID } -func (j JWSSignatureSuite) Type() string { +func (j JWSSignatureSuite) Type() LDKeyType { return JWSSignatureSuiteType } diff --git a/cryptosuite/jwssignaturesuite_test.go b/cryptosuite/jwssignaturesuite_test.go index 4b382a96..5a61b59f 100644 --- a/cryptosuite/jwssignaturesuite_test.go +++ b/cryptosuite/jwssignaturesuite_test.go @@ -3,7 +3,6 @@ package cryptosuite import ( - "encoding/json" "testing" "github.com/stretchr/testify/assert" @@ -54,32 +53,103 @@ func TestJSONWebKey2020ToJWK(t *testing.T) { assert.NoError(t, err) } -// tests data from https://github.com/decentralized-identity/JWS-Test-Suite/tree/main/data/credentials -func TestJSONWebSignature2020Suite(t *testing.T) { - tc := TestCredential{ +func TestJsonWebSignature2020AllKeyTypes(t *testing.T) { + tests := []struct { + name string + kty KTY + crv CRV + expectErr bool + }{ + { + name: "RSA", + kty: RSA, + expectErr: false, + }, + { + name: "RSA with CRV", + kty: RSA, + crv: Ed25519, + expectErr: true, + }, + { + name: "Ed25519", + kty: OKP, + crv: Ed25519, + expectErr: false, + }, + { + name: "Ed25519 with EC", + kty: EC, + crv: Ed25519, + expectErr: true, + }, + { + name: "P-256", + kty: EC, + crv: P256, + expectErr: false, + }, + { + name: "P-384", + kty: EC, + crv: P384, + expectErr: false, + }, + { + name: "secp256k1", + kty: EC, + crv: Secp256k1, + expectErr: false, + }, + { + name: "secp256k1 as OKP", + kty: OKP, + crv: Secp256k1, + expectErr: true, + }, + { + name: "unsupported curve", + kty: EC, + crv: "P512", + expectErr: true, + }, + } + + suite := JWSSignatureSuite{} + testCred := TestCredential{ Context: []string{"https://www.w3.org/2018/credentials/v1", "https://w3id.org/security/suites/jws-2020/v1"}, Type: []string{"VerifiableCredential"}, Issuer: "did:example:123", IssuanceDate: "2021-01-01T19:23:24Z", + CredentialSubject: map[string]interface{}{ + "id": "did:example:abcd", + "firstName": "Satoshi", + "lastName": "Nakamoto", + }, } - jwk, err := GenerateEd25519JSONWebKey2020() - assert.NoError(t, err) - assert.NotEmpty(t, jwk) - - suite := JWSSignatureSuite{} - signer, err := NewJSONWebKeySigner(jwk.PrivateKeyJWK) - assert.NoError(t, err) - - p, err := suite.Sign(signer, &tc) - assert.NoError(t, err) - assert.NotEmpty(t, p) - - verifier, err := NewJSONWebKeyVerifier(jwk.PublicKeyJWK) - assert.NoError(t, err) - err = suite.Verify(verifier, *p) - assert.NoError(t, err) + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + jwk, err := GenerateJSONWebKey2020(test.kty, crvPtr(test.crv)) + + if !test.expectErr { + signer, err := NewJSONWebKeySigner(jwk.PrivateKeyJWK) + assert.NoError(t, err) + + p, err := suite.Sign(signer, &testCred) + assert.NoError(t, err) + assert.NotEmpty(t, p) + + verifier, err := NewJSONWebKeyVerifier(jwk.PublicKeyJWK) + assert.NoError(t, err) + err = suite.Verify(verifier, *p) + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } } // https://github.com/decentralized-identity/JWS-Test-Suite @@ -115,9 +185,6 @@ func TestJsonWebSignature2020TestVectors(t *testing.T) { p, err := suite.Sign(signer, &knownCred) assert.NoError(t, err) - b, _ := json.Marshal(p) - println(string(b)) - verifier, err := NewJSONWebKeyVerifier(knownJWK.PublicKeyJWK) assert.NoError(t, err) diff --git a/did/key.go b/did/key.go index 16b989d5..99a8354f 100644 --- a/did/key.go +++ b/did/key.go @@ -1,8 +1,19 @@ package did import ( - "crypto/ed25519" + gocrypto "crypto" "fmt" + "strings" + + "github.com/lestrrat-go/jwx/jwk" + + "github.com/mr-tron/base58" + + "github.com/TBD54566975/did-sdk/cryptosuite" + + "github.com/pkg/errors" + + "github.com/TBD54566975/did-sdk/crypto" "github.com/multiformats/go-multibase" "github.com/multiformats/go-multicodec" @@ -15,18 +26,64 @@ const ( // Base58BTCMultiBase Base58BTC https://github.com/multiformats/go-multibase/blob/master/multibase.go Base58BTCMultiBase = multibase.Base58BTC - // Ed25519MultiCodec ed25519-pub https://github.com/multiformats/multicodec/blob/master/table.csv - Ed25519MultiCodec = multicodec.Ed25519Pub + // Multicodec reference https://github.com/multiformats/multicodec/blob/master/table.csv + + Ed25519MultiCodec = multicodec.Ed25519Pub + X25519MultiCodec = multicodec.X25519Pub + Secp256k1MultiCodec = multicodec.Secp256k1Pub + P256MultiCodec = multicodec.P256Pub + P384MultiCodec = multicodec.P384Pub + P521MultiCodec = multicodec.P521Pub + RSAMultiCodec = multicodec.RsaPub // DIDKeyPrefix did:key prefix DIDKeyPrefix = "did:key" + + // DID Key Types + + X25519KeyAgreementKey2019 cryptosuite.LDKeyType = "X25519KeyAgreementKey2019" + Ed25519VerificationKey2018 cryptosuite.LDKeyType = "Ed25519VerificationKey2018" + EcdsaSecp256k1VerificationKey2019 cryptosuite.LDKeyType = "EcdsaSecp256k1VerificationKey2019" ) -func CreateDIDKey(key ed25519.PublicKey) (*DIDKey, error) { +func GenerateDIDKey(kt crypto.KeyType) (gocrypto.PrivateKey, *DIDKey, error) { + if !isSupportedKeyType(kt) { + return nil, nil, fmt.Errorf("unsupported did:key type: %s", kt) + } + + pubKey, privKey, err := crypto.GenerateKeyByKeyType(kt) + if err != nil { + return nil, nil, errors.Wrap(err, "could not generate key for did:key") + } + + pubKeyBytes, err := crypto.PubKeyToBytes(pubKey) + if err != nil { + return nil, nil, err + } + + didKey, err := CreateDIDKey(kt, pubKeyBytes) + if err != nil { + return nil, nil, err + } + return privKey, didKey, err +} + +// CreateDIDKey constructs a did:key from a specific key type and its corresponding public key +// This method does not attempt to validate that the provided public key is of the specified key type. +// A safer method is `GenerateDIDKey` which handles key generation based on the provided key type. +func CreateDIDKey(kt crypto.KeyType, publicKey []byte) (*DIDKey, error) { + if !isSupportedKeyType(kt) { + return nil, fmt.Errorf("unsupported did:key type: %s", kt) + } + // did:key: - prefix := varint.ToUvarint(uint64(Ed25519MultiCodec)) - multiCodec := append(prefix, key...) - encoded, err := multibase.Encode(Base58BTCMultiBase, multiCodec) + multiCodec, err := keyTypeToMultiCodec(kt) + if err != nil { + return nil, err + } + prefix := varint.ToUvarint(uint64(multiCodec)) + codec := append(prefix, publicKey...) + encoded, err := multibase.Encode(Base58BTCMultiBase, codec) if err != nil { return nil, err } @@ -34,6 +91,140 @@ func CreateDIDKey(key ed25519.PublicKey) (*DIDKey, error) { return &did, nil } -func (d DIDKey) Expand() string { - return "" +// Decode takes a did:key and returns the underlying public key value as bytes, the LD key type, and a possible error +func (d DIDKey) Decode() ([]byte, cryptosuite.LDKeyType, error) { + parsed := d.Parse() + if parsed == "" { + return nil, "", fmt.Errorf("could not decode did:key value: %s", string(d)) + } + + encoding, decoded, err := multibase.Decode(parsed) + if err != nil { + return nil, "", err + } + if encoding != Base58BTCMultiBase { + return nil, "", fmt.Errorf("expected %d encoding but found %d", Base58BTCMultiBase, encoding) + } + + // n = # bytes for the int, which we expect to be two from our multicodec + multiCodec, n, err := varint.FromUvarint(decoded) + if err != nil { + return nil, "", err + } + if n != 2 { + return nil, "", fmt.Errorf("error parsing did:key varint") + } + + pubKeyBytes := decoded[n:] + multiCodecValue := multicodec.Code(multiCodec) + switch multiCodecValue { + case Ed25519MultiCodec: + return pubKeyBytes, Ed25519VerificationKey2018, nil + case X25519MultiCodec: + return pubKeyBytes, X25519KeyAgreementKey2019, nil + case Secp256k1MultiCodec: + return pubKeyBytes, EcdsaSecp256k1VerificationKey2019, nil + case P256MultiCodec, P384MultiCodec, P521MultiCodec, RSAMultiCodec: + return pubKeyBytes, cryptosuite.JsonWebKey2020, nil + default: + return nil, "", fmt.Errorf("unknown multicodec for did:key: %d", multiCodecValue) + } +} + +// Expand turns the DID key into a complaint DID Document +func (d DIDKey) Expand() (*DIDDocument, error) { + keyReference := "#" + d.Parse() + id := string(d) + + pubKey, keyType, err := d.Decode() + if err != nil { + return nil, err + } + + verificationMethod, err := constructVerificationMethod(id, keyReference, pubKey, keyType) + if err != nil { + return nil, err + } + + verificationMethodSet := []VerificationMethodSet{ + []string{keyReference}, + } + + return &DIDDocument{ + Context: KnownDIDContext, + ID: id, + VerificationMethod: []VerificationMethod{*verificationMethod}, + Authentication: verificationMethodSet, + AssertionMethod: verificationMethodSet, + KeyAgreement: verificationMethodSet, + CapabilityDelegation: verificationMethodSet, + }, nil +} + +func constructVerificationMethod(id, keyReference string, pubKey []byte, keyType cryptosuite.LDKeyType) (*VerificationMethod, error) { + if keyType != cryptosuite.JsonWebKey2020 { + return &VerificationMethod{ + ID: keyReference, + Type: keyType, + Controller: id, + PublicKeyBase58: base58.Encode(pubKey), + }, nil + } + standardJWK, err := jwk.New(pubKey) + if err != nil { + return nil, errors.Wrap(err, "could not expand key of type JsonWebKey2020") + } + pubKeyJWK, err := cryptosuite.ToPublicKeyJWK(standardJWK) + if err != nil { + return nil, errors.Wrap(err, "could convert did:key to PublicKeyJWK") + } + return &VerificationMethod{ + ID: keyReference, + Type: keyType, + Controller: id, + PublicKeyJWK: pubKeyJWK, + }, nil +} + +// Parse returns the value without the `did:key` prefix +func (d DIDKey) Parse() string { + split := strings.Split(string(d), DIDKeyPrefix+":") + if len(split) != 2 { + return "" + } + return split[1] +} + +func keyTypeToMultiCodec(kt crypto.KeyType) (multicodec.Code, error) { + switch kt { + case crypto.Ed25519: + return Ed25519MultiCodec, nil + case crypto.X25519: + return X25519MultiCodec, nil + case crypto.Secp256k1: + return Secp256k1MultiCodec, nil + case crypto.P256: + return P256MultiCodec, nil + case crypto.P384: + return P384MultiCodec, nil + case crypto.P521: + return P521MultiCodec, nil + case crypto.RSA: + return RSAMultiCodec, nil + } + return 0, fmt.Errorf("unknown multicodec for key type: %s", kt) +} + +func isSupportedKeyType(kt crypto.KeyType) bool { + keyTypes := GetSupportedDIDKeyTypes() + for _, t := range keyTypes { + if t == kt { + return true + } + } + return false +} + +func GetSupportedDIDKeyTypes() []crypto.KeyType { + return []crypto.KeyType{crypto.Ed25519, crypto.X25519, crypto.Secp256k1, crypto.P256, crypto.P384, crypto.P521, crypto.RSA} } diff --git a/did/key_test.go b/did/key_test.go index 98a3cc5e..f5d9f571 100644 --- a/did/key_test.go +++ b/did/key_test.go @@ -1,20 +1,240 @@ +//go:build jwx_es256k + package did import ( + "strings" "testing" - "github.com/TBD54566975/did-sdk/util" + "github.com/TBD54566975/did-sdk/cryptosuite" + + "github.com/multiformats/go-multicodec" + + "github.com/multiformats/go-multibase" + "github.com/multiformats/go-varint" + + "github.com/TBD54566975/did-sdk/crypto" "github.com/stretchr/testify/assert" ) func TestCreateDIDKey(t *testing.T) { - pk, sk, err := util.GenerateEd25519Key() + pk, sk, err := crypto.GenerateEd25519Key() assert.NoError(t, err) assert.NotEmpty(t, pk) assert.NotEmpty(t, sk) - didKey, err := CreateDIDKey(pk) + didKey, err := CreateDIDKey(crypto.Ed25519, pk) assert.NoError(t, err) assert.NotEmpty(t, didKey) + + didDoc, err := didKey.Expand() + assert.NoError(t, err) + assert.NotEmpty(t, didDoc) + assert.Equal(t, string(*didKey), didDoc.ID) +} + +func TestGenerateDIDKey(t *testing.T) { + tests := []struct { + name string + keyType crypto.KeyType + }{ + { + name: "Ed25519", + keyType: crypto.Ed25519, + }, + { + name: "x25519", + keyType: crypto.X25519, + }, + { + name: "Secp256k1", + keyType: crypto.Secp256k1, + }, + { + name: "P256", + keyType: crypto.P256, + }, + { + name: "P384", + keyType: crypto.P384, + }, + { + name: "P521", + keyType: crypto.P521, + }, + { + name: "RSA", + keyType: crypto.RSA, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + privKey, didKey, err := GenerateDIDKey(test.keyType) + assert.NoError(t, err) + assert.NotNil(t, didKey) + assert.NotEmpty(t, privKey) + + assert.True(t, strings.Contains(string(*didKey), "did:key")) + + codec, err := keyTypeToMultiCodec(test.keyType) + assert.NoError(t, err) + + encoding, decoded, err := multibase.Decode(didKey.Parse()) + assert.NoError(t, err) + assert.True(t, encoding == Base58BTCMultiBase) + + multiCodec, n, err := varint.FromUvarint(decoded) + assert.NoError(t, err) + assert.Equal(t, 2, n) + assert.Equal(t, codec, multicodec.Code(multiCodec)) + }) + } +} + +// From https://w3c-ccg.github.io/did-method-key/#test-vectors +func TestKnownTestVectors(t *testing.T) { + + t.Run("Ed25519 / X25519", func(tt *testing.T) { + did1 := "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, Ed25519VerificationKey2018, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:z6MkjchhfUsD6mmvni8mCdXHw216Xrm9bQe2mBH1P5RDjVJG" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, Ed25519VerificationKey2018, didDoc2.VerificationMethod[0].Type) + + did3 := "did:key:z6MknGc3ocHs3zdPiJbnaaqDi58NGb4pk1Sp9WxWufuXSdxf" + didKey3 := DIDKey(did3) + didDoc3, err := didKey3.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did3, didDoc3.ID) + assert.Equal(tt, 1, len(didDoc3.VerificationMethod)) + assert.Equal(tt, Ed25519VerificationKey2018, didDoc3.VerificationMethod[0].Type) + }) + + t.Run("X25519", func(tt *testing.T) { + did1 := "did:key:z6LSeu9HkTHSfLLeUs2nnzUSNedgDUevfNQgQjQC23ZCit6F" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, X25519KeyAgreementKey2019, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:z6LStiZsmxiK4odS4Sb6JmdRFuJ6e1SYP157gtiCyJKfrYha" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, X25519KeyAgreementKey2019, didDoc2.VerificationMethod[0].Type) + + did3 := "did:key:z6LSoMdmJz2Djah2P4L9taDmtqeJ6wwd2HhKZvNToBmvaczQ" + didKey3 := DIDKey(did3) + didDoc3, err := didKey3.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did3, didDoc3.ID) + assert.Equal(tt, 1, len(didDoc3.VerificationMethod)) + assert.Equal(tt, X25519KeyAgreementKey2019, didDoc3.VerificationMethod[0].Type) + }) + + t.Run("Secp256k1", func(tt *testing.T) { + did1 := "did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, EcdsaSecp256k1VerificationKey2019, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:zQ3shtxV1FrJfhqE1dvxYRcCknWNjHc3c5X1y3ZSoPDi2aur2" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, EcdsaSecp256k1VerificationKey2019, didDoc2.VerificationMethod[0].Type) + + did3 := "did:key:zQ3shZc2QzApp2oymGvQbzP8eKheVshBHbU4ZYjeXqwSKEn6N" + didKey3 := DIDKey(did3) + didDoc3, err := didKey3.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did3, didDoc3.ID) + assert.Equal(tt, 1, len(didDoc3.VerificationMethod)) + assert.Equal(tt, EcdsaSecp256k1VerificationKey2019, didDoc3.VerificationMethod[0].Type) + }) + + t.Run("P-256", func(tt *testing.T) { + did1 := "did:key:zDnaerDaTF5BXEavCrfRZEk316dpbLsfPDZ3WJ5hRTPFU2169" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:zDnaerx9CtbPJ1q36T5Ln5wYt3MQYeGRG5ehnPAmxcf5mDZpv" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc2.VerificationMethod[0].Type) + }) + + t.Run("P-384", func(tt *testing.T) { + did1 := "did:key:z82Lm1MpAkeJcix9K8TMiLd5NMAhnwkjjCBeWHXyu3U4oT2MVJJKXkcVBgjGhnLBn2Kaau9" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:z82LkvCwHNreneWpsgPEbV3gu1C6NFJEBg4srfJ5gdxEsMGRJUz2sG9FE42shbn2xkZJh54" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc2.VerificationMethod[0].Type) + }) + + t.Run("P-521", func(tt *testing.T) { + did1 := "did:key:z2J9gaYxrKVpdoG9A4gRnmpnRCcxU6agDtFVVBVdn1JedouoZN7SzcyREXXzWgt3gGiwpoHq7K68X4m32D8HgzG8wv3sY5j7" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc1.VerificationMethod[0].Type) + + did2 := "did:key:z2J9gcGdb2nEyMDmzQYv2QZQcM1vXktvy1Pw4MduSWxGabLZ9XESSWLQgbuPhwnXN7zP7HpTzWqrMTzaY5zWe6hpzJ2jnw4f" + didKey2 := DIDKey(did2) + didDoc2, err := didKey2.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did2, didDoc2.ID) + assert.Equal(tt, 1, len(didDoc2.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc2.VerificationMethod[0].Type) + }) + + t.Run("RSA 4096", func(tt *testing.T) { + did1 := "did:key:zgghBUVkqmWS8e1ioRVp2WN9Vw6x4NvnE9PGAyQsPqM3fnfPf8EdauiRVfBTcVDyzhqM5FFC7ekAvuV1cJHawtfgB9wDcru1hPDobk3hqyedijhgWmsYfJCmodkiiFnjNWATE7PvqTyoCjcmrc8yMRXmFPnoASyT5beUd4YZxTE9VfgmavcPy3BSouNmASMQ8xUXeiRwjb7xBaVTiDRjkmyPD7NYZdXuS93gFhyDFr5b3XLg7Rfj9nHEqtHDa7NmAX7iwDAbMUFEfiDEf9hrqZmpAYJracAjTTR8Cvn6mnDXMLwayNG8dcsXFodxok2qksYF4D8ffUxMRmyyQVQhhhmdSi4YaMPqTnC1J6HTG9Yfb98yGSVaWi4TApUhLXFow2ZvB6vqckCNhjCRL2R4MDUSk71qzxWHgezKyDeyThJgdxydrn1osqH94oSeA346eipkJvKqYREXBKwgB5VL6WF4qAK6sVZxJp2dQBfCPVZ4EbsBQaJXaVK7cNcWG8tZBFWZ79gG9Cu6C4u8yjBS8Ux6dCcJPUTLtixQu4z2n5dCsVSNdnP1EEs8ZerZo5pBgc68w4Yuf9KL3xVxPnAB1nRCBfs9cMU6oL1EdyHbqrTfnjE8HpY164akBqe92LFVsk8RusaGsVPrMekT8emTq5y8v8CabuZg5rDs3f9NPEtogjyx49wiub1FecM5B7QqEcZSYiKHgF4mfkteT2" + didKey1 := DIDKey(did1) + didDoc1, err := didKey1.Expand() + assert.NoError(tt, err) + assert.Equal(tt, did1, didDoc1.ID) + assert.Equal(tt, 1, len(didDoc1.VerificationMethod)) + assert.Equal(tt, cryptosuite.JsonWebKey2020, didDoc1.VerificationMethod[0].Type) + }) } diff --git a/did/model.go b/did/model.go index 12287ff5..580007bd 100644 --- a/did/model.go +++ b/did/model.go @@ -1,3 +1,5 @@ +//go:build jwx_es256k + package did import ( @@ -8,8 +10,8 @@ import ( "github.com/TBD54566975/did-sdk/util" ) -var ( - emptyDID = &DIDDocument{} +const ( + KnownDIDContext string = "https://www.w3.org/ns/did/v1" ) // DIDDocument is a representation of the did core specification https://www.w3.org/TR/did-core @@ -31,10 +33,10 @@ type DIDDocument struct { } type VerificationMethod struct { - ID string `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` - Controller string `json:"controller" validate:"required"` - PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` + ID string `json:"id" validate:"required"` + Type cryptosuite.LDKeyType `json:"type" validate:"required"` + Controller string `json:"controller" validate:"required"` + PublicKeyBase58 string `json:"publicKeyBase58,omitempty"` // must conform to https://datatracker.ietf.org/doc/html/rfc7517 PublicKeyJWK *cryptosuite.PublicKeyJWK `json:"publicKeyJwk,omitempty" validate:"omitempty,dive"` // https://datatracker.ietf.org/doc/html/draft-multiformats-multibase-03 @@ -60,11 +62,11 @@ func (d *DIDDocument) IsEmpty() bool { if d == nil { return true } - return reflect.DeepEqual(d, emptyDID) + return reflect.DeepEqual(d, &DIDDocument{}) } func (d *DIDDocument) IsValid() error { - return util.GetValidator().Struct(d) + return util.NewValidator().Struct(d) } // TODO(gabe) DID Resolution Metadata diff --git a/did/model_test.go b/did/model_test.go index e03e33ee..4dceed88 100644 --- a/did/model_test.go +++ b/did/model_test.go @@ -1,3 +1,5 @@ +//go:build jwx_es256k + package did import ( diff --git a/util/helpers.go b/util/helpers.go index 9a33890d..545ff264 100644 --- a/util/helpers.go +++ b/util/helpers.go @@ -1,7 +1,6 @@ package util import ( - "crypto/ed25519" "encoding/json" "errors" "fmt" @@ -19,36 +18,36 @@ const ( ISO8601Template string = "2006-01-02T15:04:05-0700" ) -var ( - v *validator.Validate - - proc *ld.JsonLdProcessor - options *ld.JsonLdOptions -) +type LDProcessor struct { + *ld.JsonLdProcessor + *ld.JsonLdOptions +} -func init() { - // golang validator - v = validator.New() +func NewValidator() *validator.Validate { + return validator.New() +} +func NewLDProcessor() LDProcessor { // JSON LD processing - proc = ld.NewJsonLdProcessor() - options = ld.NewJsonLdOptions("") + proc := ld.NewJsonLdProcessor() + options := ld.NewJsonLdOptions("") options.Format = "application/n-quads" options.Algorithm = "URDNA2015" options.ProcessingMode = ld.JsonLd_1_1 options.ProduceGeneralizedRdf = true + return LDProcessor{ + JsonLdProcessor: proc, + JsonLdOptions: options, + } } -func GetValidator() *validator.Validate { - return v -} - -func GetLDProcessor() *ld.JsonLdProcessor { - return proc +func (l LDProcessor) GetOptions() *ld.JsonLdOptions { + return l.JsonLdOptions } func LDNormalize(document interface{}) (interface{}, error) { - return GetLDProcessor().Normalize(document, options) + processor := NewLDProcessor() + return processor.Normalize(document, processor.GetOptions()) } func GetISO8601Timestamp() string { @@ -59,10 +58,6 @@ func AsISO8601Timestamp(t time.Time) string { return t.UTC().Format(ISO8601Template) } -func GenerateEd25519Key() (ed25519.PublicKey, ed25519.PrivateKey, error) { - return ed25519.GenerateKey(nil) -} - // Copy makes a 1:1 copy of src into dst. func Copy(src interface{}, dst interface{}) error { if err := validateCopy(src, dst); err != nil { diff --git a/vc/model.go b/vc/model.go index cadc2a01..883fdad6 100644 --- a/vc/model.go +++ b/vc/model.go @@ -12,11 +12,6 @@ const ( VerifiableCredentialsLinkedDataContext string = "https://www.w3.org/2018/credentials/v1" ) -var ( - emptyCredential = &VerifiableCredential{} - emptyPresentation = &VerifiablePresentation{} -) - type CredentialSubject map[string]interface{} // VerifiableCredential is the known_schemas model outlined in the @@ -88,11 +83,11 @@ func (v *VerifiableCredential) IsEmpty() bool { if v == nil { return true } - return reflect.DeepEqual(v, emptyCredential) + return reflect.DeepEqual(v, &VerifiableCredential{}) } func (v *VerifiableCredential) IsValid() error { - return util.GetValidator().Struct(v) + return util.NewValidator().Struct(v) } // VerifiablePresentation https://www.w3.org/TR/2021/REC-vc-data-model-20211109/#presentations-0 @@ -109,9 +104,9 @@ func (v *VerifiablePresentation) IsEmpty() bool { if v == nil { return true } - return reflect.DeepEqual(v, emptyPresentation) + return reflect.DeepEqual(v, &VerifiablePresentation{}) } func (v *VerifiablePresentation) IsValid() error { - return util.GetValidator().Struct(v) + return util.NewValidator().Struct(v) }