diff --git a/go.mod b/go.mod index 407d92af4f..2b4f56ecfc 100644 --- a/go.mod +++ b/go.mod @@ -124,6 +124,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/mr-tron/base58 v1.1.3 // indirect + github.com/multiformats/go-multicodec v0.9.0 // indirect github.com/multiformats/go-multihash v0.0.11 // indirect github.com/nats-io/jwt/v2 v2.5.2 // indirect github.com/nats-io/nkeys v0.4.5 // indirect diff --git a/go.sum b/go.sum index d9ab41d7a7..09628337b4 100644 --- a/go.sum +++ b/go.sum @@ -416,6 +416,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc= github.com/mr-tron/base58 v1.1.3/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= +github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= +github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= github.com/multiformats/go-multihash v0.0.11 h1:yEyBxwoR/7vBM5NfLVXRnpQNVLrMhpS6MRb7Z/1pnzc= github.com/multiformats/go-multihash v0.0.11/go.mod h1:LXRDJcYYY+9BjlsFe6i5LV7uekf0OoEJdnRmitUshxk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= diff --git a/vdr/didkey/resolver.go b/vdr/didkey/resolver.go new file mode 100644 index 0000000000..70a2e89404 --- /dev/null +++ b/vdr/didkey/resolver.go @@ -0,0 +1,109 @@ +package didkey + +import ( + "bytes" + "crypto" + "crypto/ed25519" + "crypto/x509" + "encoding/binary" + "errors" + "fmt" + "github.com/lestrrat-go/jwx/x25519" + "github.com/multiformats/go-multicodec" + ssi "github.com/nuts-foundation/go-did" + "github.com/nuts-foundation/go-did/did" + "github.com/nuts-foundation/nuts-node/vdr/resolver" + "github.com/shengdoushi/base58" +) + +// MethodName is the name of this DID method. +const MethodName = "key" + +var _ resolver.DIDResolver = &Resolver{} + +var errInvalidPublicKeyLength = errors.New("invalid did:key: invalid public key length") + +type Resolver struct { +} + +func (r Resolver) Resolve(id did.DID, metadata *resolver.ResolveMetadata) (*did.Document, *resolver.DocumentMetadata, error) { + if id.Method != MethodName { + return nil, nil, fmt.Errorf("unsupported DID method: %s", id.Method) + } + encodedKey := id.ID + if len(encodedKey) == 0 || encodedKey[0] != 'z' { + return nil, nil, errors.New("did:key does not start with 'z'") + } + mcBytes, err := base58.Decode(encodedKey[1:], base58.BitcoinAlphabet) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid base58btc: %w", err) + } + reader := bytes.NewReader(mcBytes) + keyType, err := binary.ReadUvarint(reader) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid base58btc: %w", err) + } + // See https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm + var key crypto.PublicKey + keyLength := reader.Len() + switch multicodec.Code(keyType) { + case multicodec.Secp256k1Pub: + if keyLength != 33 { + return nil, nil, errInvalidPublicKeyLength + } + return nil, nil, errors.New("TODO: support secp256k1 public key") + case multicodec.X25519Pub: + if keyLength != 32 { + return nil, nil, errInvalidPublicKeyLength + } + key = x25519.PublicKey(mcBytes[1:]) + case multicodec.Ed25519Pub: + if keyLength != 32 { + return nil, nil, errInvalidPublicKeyLength + } + key = ed25519.PublicKey(mcBytes[1:]) + case multicodec.P256Pub: + if keyLength != 33 { + return nil, nil, errInvalidPublicKeyLength + } + return nil, nil, errors.New("TODO: find out P256 pub key encoding") + case multicodec.P384Pub: + if keyLength != 49 { + return nil, nil, errInvalidPublicKeyLength + } + return nil, nil, errors.New("TODO: find out P384 pub key encoding") + case multicodec.P521Pub: + return nil, nil, errors.New("TODO: find out P521 pub key encoding") + case multicodec.RsaPub: + key, err = x509.ParsePKCS1PublicKey(mcBytes[1:]) + if err != nil { + return nil, nil, fmt.Errorf("did:key: invalid PKCS#1 encoded RSA public key: %w", err) + } + default: + return nil, nil, fmt.Errorf("did:key: unsupported public key type: %d", keyType) + } + + document := did.Document{ + Context: nil, + ID: id, + AlsoKnownAs: nil, + Authentication: nil, + AssertionMethod: nil, + KeyAgreement: nil, + CapabilityInvocation: nil, + CapabilityDelegation: nil, + Service: nil, + } + keyID := id + keyID.Fragment = id.ID + vm, err := did.NewVerificationMethod(keyID, ssi.JsonWebKey2020, id, key) + if err != nil { + return nil, nil, err + } + document.AddAssertionMethod(vm) + document.AddAuthenticationMethod(vm) + document.AddKeyAgreement(vm) + document.AddCapabilityDelegation(vm) + document.AddCapabilityInvocation(vm) + return nil, nil, nil +} diff --git a/vdr/didkey/resolver_test.go b/vdr/didkey/resolver_test.go new file mode 100644 index 0000000000..7ce5c0731b --- /dev/null +++ b/vdr/didkey/resolver_test.go @@ -0,0 +1,35 @@ +package didkey + +import ( + "github.com/nuts-foundation/go-did/did" + "github.com/stretchr/testify/require" + "testing" +) + +func TestResolver_Resolve(t *testing.T) { + tests := []struct { + name string + id string + err error + }{ + { + name: "Ed25519", + id: "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Resolver{} + got, got1, err := r.Resolve(did.MustParseDID(tt.id), nil) + if tt.err != nil { + require.Error(t, err) + require.Nil(t, got) + require.Nil(t, got1) + return + } + require.NoError(t, err) + require.NotNil(t, got) + require.NotNil(t, got1) + }) + } +}