diff --git a/crypto/_allkeys/allkeys.go b/crypto/_allkeys/allkeys.go index 3791dbf..1f6d31e 100644 --- a/crypto/_allkeys/allkeys.go +++ b/crypto/_allkeys/allkeys.go @@ -9,15 +9,19 @@ import ( "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/rsa" + "github.com/INFURA/go-did/crypto/secp256k1" "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) }, - p521.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return p521.PublicKeyFromBytes(b) }, - x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) }, + 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) }, + rsa.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return rsa.PublicKeyFromPKCS1DER(b) }, + secp256k1.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return secp256k1.PublicKeyFromBytes(b) }, + x25519.MultibaseCode: func(b []byte) (crypto.PublicKey, error) { return x25519.PublicKeyFromBytes(b) }, } // PublicKeyFromPublicKeyMultibase decodes the public key from its PublicKeyMultibase form diff --git a/crypto/_testsuite/testsuite.go b/crypto/_testsuite/testsuite.go index 8b857fb..0dc4bcf 100644 --- a/crypto/_testsuite/testsuite.go +++ b/crypto/_testsuite/testsuite.go @@ -29,6 +29,9 @@ type TestHarness[PubT crypto.PublicKey, PrivT crypto.PrivateKey] struct { MultibaseCode uint64 + DefaultHash crypto.Hash + OtherHashes []crypto.Hash + PublicKeyBytesSize int PrivateKeyBytesSize int SignatureBytesSize int @@ -198,10 +201,12 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har for _, tc := range []struct { name string - signer func(msg []byte) ([]byte, error) - verifier func(msg []byte, sig []byte) bool + signer func(msg []byte, opts ...crypto.SigningOption) ([]byte, error) + verifier func(msg []byte, sig []byte, opts ...crypto.SigningOption) bool expectedSize int stats *int + defaultHash crypto.Hash + otherHashes []crypto.Hash }{ { name: "Bytes signature", @@ -209,32 +214,59 @@ func TestSuite[PubT crypto.PublicKey, PrivT crypto.PrivateKey](t *testing.T, har verifier: spub.VerifyBytes, expectedSize: harness.SignatureBytesSize, stats: &stats.sigRawSize, + defaultHash: harness.DefaultHash, + otherHashes: harness.OtherHashes, }, { - name: "ASN.1 signature", - signer: spriv.SignToASN1, - verifier: spub.VerifyASN1, - stats: &stats.sigAsn1Size, + name: "ASN.1 signature", + signer: spriv.SignToASN1, + verifier: spub.VerifyASN1, + stats: &stats.sigAsn1Size, + defaultHash: harness.DefaultHash, + otherHashes: harness.OtherHashes, }, } { t.Run(tc.name, func(t *testing.T) { msg := []byte("message") - sig, err := tc.signer(msg) + sigNoParams, err := tc.signer(msg) + require.NoError(t, err) + require.NotEmpty(t, sigNoParams) + + sigDefault, err := tc.signer(msg, crypto.WithSigningHash(tc.defaultHash)) require.NoError(t, err) - require.NotEmpty(t, sig) if tc.expectedSize > 0 { - require.Equal(t, tc.expectedSize, len(sig)) + require.Equal(t, tc.expectedSize, len(sigNoParams)) } - *tc.stats = len(sig) + *tc.stats = len(sigNoParams) - valid := tc.verifier(msg, sig) + // signatures might be different (i.e. non-deterministic), but they should verify the same way + valid := tc.verifier(msg, sigNoParams) + require.True(t, valid) + valid = tc.verifier(msg, sigDefault) require.True(t, valid) - valid = tc.verifier([]byte("wrong message"), sig) + valid = tc.verifier([]byte("wrong message"), sigNoParams) + require.False(t, valid) + valid = tc.verifier([]byte("wrong message"), sigDefault) require.False(t, valid) }) + for _, hash := range tc.otherHashes { + t.Run(fmt.Sprintf("%s-%s", tc.name, hash.String()), func(t *testing.T) { + msg := []byte("message") + + sig, err := tc.signer(msg) + require.NoError(t, err) + require.NotEmpty(t, sig) + + valid := tc.verifier(msg, sig) + require.True(t, valid) + + valid = tc.verifier([]byte("wrong message"), sig) + require.False(t, valid) + }) + } } }) diff --git a/methods/did-key/document.go b/methods/did-key/document.go index 74ad91a..b592326 100644 --- a/methods/did-key/document.go +++ b/methods/did-key/document.go @@ -20,6 +20,11 @@ func (d document) MarshalJSON() ([]byte, error) { // Maybe it doesn't matter, but the spec contradicts itself. // See https://github.com/w3c-ccg/did-key-spec/issues/71 + vms := []did.VerificationMethod{d.signature} + if d.signature != did.VerificationMethod(d.keyAgreement) { + vms = append(vms, d.keyAgreement) + } + return json.Marshal(struct { Context []string `json:"@context"` ID string `json:"id"` @@ -28,17 +33,17 @@ func (d document) MarshalJSON() ([]byte, error) { VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"` Authentication []string `json:"authentication,omitempty"` AssertionMethod []string `json:"assertionMethod,omitempty"` - KeyAgreement []did.VerificationMethod `json:"keyAgreement,omitempty"` + KeyAgreement []string `json:"keyAgreement,omitempty"` CapabilityInvocation []string `json:"capabilityInvocation,omitempty"` CapabilityDelegation []string `json:"capabilityDelegation,omitempty"` }{ Context: d.Context(), ID: d.id.String(), AlsoKnownAs: nil, - VerificationMethod: []did.VerificationMethod{d.signature}, + VerificationMethod: vms, Authentication: []string{d.signature.ID()}, AssertionMethod: []string{d.signature.ID()}, - KeyAgreement: []did.VerificationMethod{d.keyAgreement}, + KeyAgreement: []string{d.keyAgreement.ID()}, CapabilityInvocation: []string{d.signature.ID()}, CapabilityDelegation: []string{d.signature.ID()}, }) @@ -66,6 +71,11 @@ func (d document) AlsoKnownAs() []*url.URL { } func (d document) VerificationMethods() map[string]did.VerificationMethod { + if d.signature == did.VerificationMethod(d.keyAgreement) { + return map[string]did.VerificationMethod{ + d.signature.ID(): d.signature, + } + } return map[string]did.VerificationMethod{ d.signature.ID(): d.signature, d.keyAgreement.ID(): d.keyAgreement, diff --git a/methods/did-key/document_test.go b/methods/did-key/document_test.go index c379018..c11ffc1 100644 --- a/methods/did-key/document_test.go +++ b/methods/did-key/document_test.go @@ -7,20 +7,19 @@ import ( "github.com/stretchr/testify/require" "github.com/INFURA/go-did" + "github.com/INFURA/go-did/methods/did-key/testvectors" ) func TestDocument(t *testing.T) { d, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") require.NoError(t, err) - doc, err := d.Document() + doc, err := d.Document(did.WithResolutionHintVerificationMethod("Ed25519VerificationKey2020")) require.NoError(t, err) bytes, err := json.MarshalIndent(doc, "", " ") require.NoError(t, err) - // TODO: https://github.com/w3c-ccg/did-key-spec/issues/71 - const expected = `{ "@context": [ "https://www.w3.org/ns/did/v1", @@ -54,8 +53,113 @@ func TestDocument(t *testing.T) { }] }` - require.JSONEq(t, expected, string(bytes)) + requireDocEqual(t, expected, string(bytes)) } -// TODO: test vectors: -// https://github.com/w3c-ccg/did-key-spec/tree/main/test-vectors +func TestVectors(t *testing.T) { + for _, filename := range testvectors.AllFiles() { + t.Run(filename, func(t *testing.T) { + vectors, err := testvectors.LoadTestVectors(filename) + require.NoError(t, err) + + for _, vector := range vectors { + t.Run(vector.DID, func(t *testing.T) { + t.Log("hint is", vector.ResolutionHint) + require.NotZero(t, vector.Document) + require.NotZero(t, vector.Pub) + require.NotZero(t, vector.Priv) + + d, err := did.Parse(vector.DID) + require.NoError(t, err) + + var opts []did.ResolutionOption + for _, hint := range vector.ResolutionHint { + opts = append(opts, did.WithResolutionHintVerificationMethod(hint)) + } + + doc, err := d.Document(opts...) + require.NoError(t, err) + bytes, err := json.MarshalIndent(doc, "", " ") + require.NoError(t, err) + requireDocEqual(t, vector.Document, string(bytes)) + }) + } + }) + } +} + +// Some variations in the DID document are legal, so we can't just require.JSONEq() to compare two of them. +// This function does its best to compare two documents, regardless of those variations. +func requireDocEqual(t *testing.T, expected, actual string) { + propsExpected := map[string]json.RawMessage{} + err := json.Unmarshal([]byte(expected), &propsExpected) + require.NoError(t, err) + + propsActual := map[string]json.RawMessage{} + err = json.Unmarshal([]byte(actual), &propsActual) + require.NoError(t, err) + + require.Equal(t, len(propsExpected), len(propsActual)) + + // if a VerificationMethod is defined inline in the properties below, we move it to vmExpected and replace it with the VM ID + var vmExpected []json.RawMessage + err = json.Unmarshal(propsExpected["verificationMethod"], &vmExpected) + require.NoError(t, err) + + for _, s := range []string{"authentication", "assertionMethod", "keyAgreement", "capabilityInvocation", "capabilityDelegation"} { + var vms []json.RawMessage + err = json.Unmarshal(propsExpected[s], &vms) + require.NoError(t, err) + for _, vmBytes := range vms { + vm := map[string]json.RawMessage{} + if err := json.Unmarshal(vmBytes, &vm); err == nil { + vmExpected = append(vmExpected, vmBytes) + propsExpected[s] = append([]byte("[ "), append(vm["id"], []byte(" ]")...)...) + } + } + } + + // Same for actual + var vmActual []json.RawMessage + err = json.Unmarshal(propsActual["verificationMethod"], &vmActual) + require.NoError(t, err) + + for _, s := range []string{"authentication", "assertionMethod", "keyAgreement", "capabilityInvocation", "capabilityDelegation"} { + var vms []json.RawMessage + err = json.Unmarshal(propsActual[s], &vms) + require.NoError(t, err) + for _, vmBytes := range vms { + vm := map[string]json.RawMessage{} + if err := json.Unmarshal(vmBytes, &vm); err == nil { + vmActual = append(vmActual, vmBytes) + propsActual[s] = append([]byte("[ "), append(vm["id"], []byte(" ]")...)...) + } + } + } + + for k, v := range propsExpected { + switch k { + case "verificationMethod": + // Convert to interface{} slices to normalize JSON formatting + expectedVMs := make([]interface{}, len(vmExpected)) + for i, vm := range vmExpected { + var normalized interface{} + err := json.Unmarshal(vm, &normalized) + require.NoError(t, err) + expectedVMs[i] = normalized + } + + actualVMs := make([]interface{}, len(vmActual)) + for i, vm := range vmActual { + var normalized interface{} + err := json.Unmarshal(vm, &normalized) + require.NoError(t, err) + actualVMs[i] = normalized + } + + require.ElementsMatch(t, expectedVMs, actualVMs, "--> on property \"%s\"", k) + default: + require.JSONEq(t, string(v), string(propsActual[k]), "--> on property \"%s\"", k) + } + } +} diff --git a/methods/did-key/key.go b/methods/did-key/key.go index e5423df..3c39fd7 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -11,9 +11,14 @@ import ( "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/rsa" + "github.com/INFURA/go-did/crypto/secp256k1" "github.com/INFURA/go-did/crypto/x25519" "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/jsonwebkey" "github.com/INFURA/go-did/verifications/multikey" + p256vm "github.com/INFURA/go-did/verifications/p256" + secp256k1vm "github.com/INFURA/go-did/verifications/secp256k1" "github.com/INFURA/go-did/verifications/x25519" ) @@ -23,12 +28,11 @@ func init() { did.RegisterMethod("key", Decode) } -var _ did.DID = &DidKey{} +var _ did.DID = DidKey{} type DidKey struct { - msi string // method-specific identifier, i.e. "12345" in "did:key:12345" - signature did.VerificationMethodSignature - keyAgreement did.VerificationMethodKeyAgreement + msi string // method-specific identifier, i.e. "12345" in "did:key:12345" + pubkey crypto.PublicKey } func Decode(identifier string) (did.DID, error) { @@ -44,38 +48,14 @@ func Decode(identifier string) (did.DID, error) { if err != nil { return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) } - d, err := FromPublicKey(pub) - if err != nil { - return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) - } - return d, nil + return DidKey{msi: msi, pubkey: pub}, nil } -func FromPublicKey(pub crypto.PublicKey) (did.DID, error) { - switch pub := pub.(type) { - case ed25519.PublicKey: - d := DidKey{msi: pub.ToPublicKeyMultibase()} - d.signature = ed25519vm.NewVerificationKey2020(fmt.Sprintf("did:key:%s#%s", d.msi, d.msi), pub, d) - xpub, err := x25519.PublicKeyFromEd25519(pub) - if err != nil { - return nil, err - } - 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, *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 - d.keyAgreement = mk - return d, nil - - default: - return nil, fmt.Errorf("unsupported public key: %T", pub) - } +func FromPublicKey(pub crypto.PublicKey) did.DID { + return DidKey{msi: pub.ToPublicKeyMultibase()} } -func FromPrivateKey(priv crypto.PrivateKey) (did.DID, error) { +func FromPrivateKey(priv crypto.PrivateKey) did.DID { return FromPublicKey(priv.Public().(crypto.PublicKey)) } @@ -83,12 +63,92 @@ func (d DidKey) Method() string { return "key" } -func (d DidKey) Document() (did.Document, error) { - return document{ - id: d, - signature: d.signature, - keyAgreement: d.keyAgreement, - }, nil +func (d DidKey) Document(opts ...did.ResolutionOption) (did.Document, error) { + params := did.CollectResolutionOpts(opts) + + doc := document{id: d} + mainVmId := fmt.Sprintf("did:key:%s#%s", d.msi, d.msi) + + switch pub := d.pubkey.(type) { + case ed25519.PublicKey: + xpub, err := x25519.PublicKeyFromEd25519(pub) + if err != nil { + return nil, err + } + xmsi := xpub.ToPublicKeyMultibase() + xVmId := fmt.Sprintf("did:key:%s#%s", d.msi, xmsi) + + switch { + case params.HasVerificationMethodHint(jsonwebkey.Type): + doc.signature = jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d) + doc.keyAgreement = jsonwebkey.NewJsonWebKey2020(xVmId, xpub, d) + case params.HasVerificationMethodHint(multikey.Type): + doc.signature = multikey.NewMultiKey(mainVmId, pub, d) + doc.keyAgreement = multikey.NewMultiKey(xVmId, xpub, d) + default: + if params.HasVerificationMethodHint(ed25519vm.Type2018) { + doc.signature = ed25519vm.NewVerificationKey2018(mainVmId, pub, d) + } + if params.HasVerificationMethodHint(x25519vm.Type2019) { + doc.keyAgreement = x25519vm.NewKeyAgreementKey2019(xVmId, xpub, d) + } + if doc.signature == nil { + doc.signature = ed25519vm.NewVerificationKey2020(mainVmId, pub, d) + } + if doc.keyAgreement == nil { + doc.keyAgreement = x25519vm.NewKeyAgreementKey2020(xVmId, xpub, d) + } + } + + case *p256.PublicKey: + switch { + case params.HasVerificationMethodHint(jsonwebkey.Type): + jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d) + doc.signature = jwk + doc.keyAgreement = jwk + case params.HasVerificationMethodHint(p256vm.Type2021): + vm := p256vm.NewKey2021(mainVmId, pub, d) + doc.signature = vm + doc.keyAgreement = vm + default: + mk := multikey.NewMultiKey(mainVmId, pub, d) + doc.signature = mk + doc.keyAgreement = mk + } + + case *secp256k1.PublicKey: + switch { + case params.HasVerificationMethodHint(jsonwebkey.Type): + jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d) + doc.signature = jwk + doc.keyAgreement = jwk + case params.HasVerificationMethodHint(secp256k1vm.Type): + vm := secp256k1vm.NewVerificationKey2019(mainVmId, pub, d) + doc.signature = vm + doc.keyAgreement = vm + default: + mk := multikey.NewMultiKey(mainVmId, pub, d) + doc.signature = mk + doc.keyAgreement = mk + } + + case *p384.PublicKey, *p521.PublicKey, *rsa.PublicKey: + switch { + case params.HasVerificationMethodHint(jsonwebkey.Type): + jwk := jsonwebkey.NewJsonWebKey2020(mainVmId, pub, d) + doc.signature = jwk + doc.keyAgreement = jwk + default: + mk := multikey.NewMultiKey(mainVmId, pub, d) + doc.signature = mk + doc.keyAgreement = mk + } + + default: + return nil, fmt.Errorf("unsupported public key: %T", pub) + } + + return doc, nil } func (d DidKey) String() string { @@ -103,5 +163,8 @@ func (d DidKey) Equal(d2 did.DID) bool { if d2, ok := d2.(DidKey); ok { return d.msi == d2.msi } + if d2, ok := d2.(*DidKey); ok { + return d.msi == d2.msi + } return false } diff --git a/methods/did-key/key_test.go b/methods/did-key/key_test.go index 0cd2f65..59ba571 100644 --- a/methods/did-key/key_test.go +++ b/methods/did-key/key_test.go @@ -20,8 +20,7 @@ func ExampleGenerateKeyPair() { fmt.Println("Private key:", base64.StdEncoding.EncodeToString(priv.ToBytes())) // Make the associated did:key - dk, err := didkey.FromPrivateKey(priv) - handleErr(err) + dk := didkey.FromPrivateKey(priv) fmt.Println("Did:", dk.String()) // Produce a signature