diff --git a/did_test.go b/did_test.go index 3dc01c0..605bc33 100644 --- a/did_test.go +++ b/did_test.go @@ -1,11 +1,58 @@ -package did +package did_test import ( + "encoding/base64" + "fmt" "testing" "github.com/stretchr/testify/require" + + "github.com/INFURA/go-did" + _ "github.com/INFURA/go-did/methods/did-key" + "github.com/INFURA/go-did/verifications/x25519" ) +func ExampleSignature() { + // errors need to be handled + + // 1) Parse the DID string into a DID object + d, _ := did.Parse("did:key:z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse") + + // 2) Resolve to the DID Document + doc, _ := d.Document() + + // 3) Use the appropriate verification method (ex: verify a signature for authentication purpose) + sig, _ := base64.StdEncoding.DecodeString("nhpkr5a7juUM2eDpDRSJVdEE++0SYqaZXHtuvyafVFUx8zsOdDSrij+vHmd/ARwUOmi/ysmSD+b3K9WTBtmmBQ==") + if ok, method := did.TryAllVerify(doc.Authentication(), []byte("message"), sig); ok { + fmt.Println("Signature is valid, verified with method:", method.Type(), method.ID()) + } else { + fmt.Println("Signature is invalid") + } + + // Output: Signature is valid, verified with method: Ed25519VerificationKey2020 did:key:z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse#z6MknwcywUtTy2ADJQ8FH1GcSySKPyKDmyzT4rPEE84XREse +} + +func ExampleKeyAgreement() { + // errors need to be handled + + // 1) We have a private key for Alice + privAliceBytes, _ := base64.StdEncoding.DecodeString("fNOf3xWjFZYGYWixorM5+JR+u/2Udnc9Zw5+9rSvjqo=") + privAlice, _ := x25519.PrivateKeyFromBytes(privAliceBytes) + + // 2) We resolve the DID Document for Bob + dBob, _ := did.Parse("did:key:z6MkgRNXpJRbEE6FoXhT8KWHwJo4KyzFo1FdSEFpRLh5vuXZ") + docBob, _ := dBob.Document() + + // 3) We perform the key agreement + key, method, _ := did.FindMatchingKeyAgreement(docBob.KeyAgreement(), privAlice) + + fmt.Println("Shared key:", base64.StdEncoding.EncodeToString(key)) + fmt.Println("Verification method used:", method.Type(), method.ID()) + + // Output: Shared key: 7G1qwS/gn5W1hxBtObHc3F0jA7m2vuXkLJJ32yBuHVQ= + // Verification method used: X25519KeyAgreementKey2020 did:key:z6MkgRNXpJRbEE6FoXhT8KWHwJo4KyzFo1FdSEFpRLh5vuXZ#z6LSjeQx2VkXz8yirhrYJv8uicu9BBaeYU3Q1D9sFBovhmPF +} + func TestHasValidDIDSyntax(t *testing.T) { tests := []struct { name string @@ -38,7 +85,7 @@ func TestHasValidDIDSyntax(t *testing.T) { } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { - require.Equal(t, tt.expected, HasValidDIDSyntax(tt.input)) + require.Equal(t, tt.expected, did.HasValidDIDSyntax(tt.input)) }) } } @@ -46,7 +93,7 @@ func TestHasValidDIDSyntax(t *testing.T) { func BenchmarkHasValidDIDSyntax(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - HasValidDIDSyntax("did:example:abc:def:ghi:jkl%20mno%3Apqr%3Astuv") + did.HasValidDIDSyntax("did:example:abc:def:ghi:jkl%20mno%3Apqr%3Astuv") } } @@ -78,7 +125,7 @@ func TestHasValidDidUrlSyntax(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.expected, HasValidDidUrlSyntax(tt.input)) + require.Equal(t, tt.expected, did.HasValidDidUrlSyntax(tt.input)) }) } } @@ -86,6 +133,6 @@ func TestHasValidDidUrlSyntax(t *testing.T) { func BenchmarkHasValidDidUrlSyntax(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { - HasValidDidUrlSyntax("did:example:123456789abcdefghi/path/to/resource?key=value#section1") + did.HasValidDidUrlSyntax("did:example:123456789abcdefghi/path/to/resource?key=value#section1") } } diff --git a/go.mod b/go.mod index bd96eb4..f7a2543 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/multiformats/go-multibase v0.2.0 github.com/multiformats/go-varint v0.0.7 github.com/stretchr/testify v1.10.0 - golang.org/x/crypto v0.36.0 ) require ( diff --git a/go.sum b/go.sum index 6655010..12b76c6 100644 --- a/go.sum +++ b/go.sum @@ -14,8 +14,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/interfaces.go b/interfaces.go index 4550037..5a6e796 100644 --- a/interfaces.go +++ b/interfaces.go @@ -1,6 +1,7 @@ package did import ( + "crypto" "encoding/json" "net/url" ) @@ -108,5 +109,21 @@ type VerificationMethodSignature interface { type VerificationMethodKeyAgreement interface { VerificationMethod - // TODO: function for key agreement + // PrivateKeyIsCompatible checks that the given PrivateKey is compatible with this method. + PrivateKeyIsCompatible(local PrivateKey) bool + + // ECDH computes the shared key using the given PrivateKey. + ECDH(local PrivateKey) ([]byte, error) +} + +// Below are the interfaces for crypto.PublicKey and crypto.PrivateKey in the go standard library. +// They are not defined there for compatibility reasons, so we need to define them here. + +type PublicKey interface { + Equal(x crypto.PublicKey) bool +} + +type PrivateKey interface { + Public() crypto.PublicKey + Equal(x crypto.PrivateKey) bool } diff --git a/methods/did-key/document.go b/methods/did-key/document.go index 956b3ee..74ad91a 100644 --- a/methods/did-key/document.go +++ b/methods/did-key/document.go @@ -17,7 +17,7 @@ type document struct { func (d document) MarshalJSON() ([]byte, error) { // It's unclear where the KeyAgreement should be. - // Maybe it doesn't matter, but the spec contradict itself. + // Maybe it doesn't matter, but the spec contradicts itself. // See https://github.com/w3c-ccg/did-key-spec/issues/71 return json.Marshal(struct { diff --git a/methods/did-key/document_test.go b/methods/did-key/document_test.go index b83c1b7..c379018 100644 --- a/methods/did-key/document_test.go +++ b/methods/did-key/document_test.go @@ -2,7 +2,6 @@ package didkey import ( "encoding/json" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -20,8 +19,6 @@ func TestDocument(t *testing.T) { bytes, err := json.MarshalIndent(doc, "", " ") require.NoError(t, err) - fmt.Println(string(bytes)) - // TODO: https://github.com/w3c-ccg/did-key-spec/issues/71 const expected = `{ diff --git a/methods/did-key/key.go b/methods/did-key/key.go index 3082c3b..57128b6 100644 --- a/methods/did-key/key.go +++ b/methods/did-key/key.go @@ -1,7 +1,6 @@ package didkey import ( - "crypto" "fmt" "strings" @@ -65,7 +64,7 @@ func Decode(identifier string) (did.DID, error) { return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code) } -func FromPublicKey(pub PublicKey) (did.DID, error) { +func FromPublicKey(pub did.PublicKey) (did.DID, error) { var err error switch pub := pub.(type) { case ed25519.PublicKey: @@ -90,8 +89,8 @@ func FromPublicKey(pub PublicKey) (did.DID, error) { } } -func FromPrivateKey(priv PrivateKey) (did.DID, error) { - return FromPublicKey(priv.Public().(PublicKey)) +func FromPrivateKey(priv did.PrivateKey) (did.DID, error) { + return FromPublicKey(priv.Public().(did.PublicKey)) } func (d DidKey) Method() string { @@ -120,18 +119,3 @@ func (d DidKey) Equal(d2 did.DID) bool { } return false } - -// --------------- - -// Below are the interfaces for crypto.PublicKey and crypto.PrivateKey in the go standard library. -// They are not actually defined there for compatibility reasons. -// They are useful for did:key, it's unclear if it's useful elsewhere. - -type PublicKey interface { - Equal(x crypto.PublicKey) bool -} - -type PrivateKey interface { - Public() crypto.PublicKey - Equal(x crypto.PrivateKey) bool -} diff --git a/methods/did-key/key_test.go b/methods/did-key/key_test.go index 3458af7..391b810 100644 --- a/methods/did-key/key_test.go +++ b/methods/did-key/key_test.go @@ -1,14 +1,47 @@ package didkey_test import ( + "encoding/base64" + "fmt" "testing" "github.com/stretchr/testify/require" "github.com/INFURA/go-did" - _ "github.com/INFURA/go-did/methods/did-key" + didkey "github.com/INFURA/go-did/methods/did-key" + "github.com/INFURA/go-did/verifications/ed25519" ) +func ExampleGenerateKeyPair() { + // Generate a key pair + pub, priv, err := ed25519.GenerateKeyPair() + if err != nil { + panic(err) + } + fmt.Println("Public key:", ed25519.PublicKeyToMultibase(pub)) + fmt.Println("Private key:", base64.StdEncoding.EncodeToString(priv)) + + // Make the associated did:key + dk, err := didkey.FromPrivateKey(priv) + if err != nil { + panic(err) + } + fmt.Println("Did:", dk.String()) + + // Produce a signature + msg := []byte("message") + sig := ed25519.Sign(priv, msg) + fmt.Println("Signature:", base64.StdEncoding.EncodeToString(sig)) + + // Resolve the DID and verify a signature + doc, err := dk.Document() + if err != nil { + panic(err) + } + ok, _ := did.TryAllVerify(doc.Authentication(), msg, sig) + fmt.Println("Signature verified:", ok) +} + func TestParseDIDKey(t *testing.T) { str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z" d, err := did.Parse(str) diff --git a/utilities.go b/utilities.go new file mode 100644 index 0000000..404474c --- /dev/null +++ b/utilities.go @@ -0,0 +1,30 @@ +package did + +import ( + "fmt" +) + +// TryAllVerify tries to verify the signature with all the methods in the slice. +// It returns true if the signature is verified, and the method that verified it. +// If no method verifies the signature, it returns false and nil. +func TryAllVerify(methods []VerificationMethodSignature, data []byte, sig []byte) (bool, VerificationMethodSignature) { + for _, method := range methods { + if method.Verify(data, sig) { + return true, method + } + } + return false, nil +} + +// FindMatchingKeyAgreement tries to find a matching key agreement method for the given private key type. +// It returns the shared key as well as the selected method. +// If no matching method is found, it returns an error. +func FindMatchingKeyAgreement(methods []VerificationMethodKeyAgreement, priv PrivateKey) ([]byte, VerificationMethodKeyAgreement, error) { + for _, method := range methods { + if method.PrivateKeyIsCompatible(priv) { + key, err := method.ECDH(priv) + return key, method, err + } + } + return nil, nil, fmt.Errorf("no matching key agreement found") +} diff --git a/verifications/ed25519/key.go b/verifications/ed25519/key.go index 072c4c6..0350f2c 100644 --- a/verifications/ed25519/key.go +++ b/verifications/ed25519/key.go @@ -12,12 +12,21 @@ import ( type PublicKey = ed25519.PublicKey type PrivateKey = ed25519.PrivateKey -const PublicKeySize = ed25519.PublicKeySize +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = ed25519.PublicKeySize + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = ed25519.PrivateKeySize + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = ed25519.SignatureSize +) func GenerateKeyPair() (PublicKey, PrivateKey, error) { return ed25519.GenerateKey(rand.Reader) } +// PublicKeyFromBytes convert a serialized public key to a PublicKey. +// It errors if the slice is not the right size. func PublicKeyFromBytes(b []byte) (PublicKey, error) { if len(b) != PublicKeySize { return nil, fmt.Errorf("invalid ed25519 public key size") @@ -57,3 +66,18 @@ func PublicKeyToMultibase(pub PublicKey) string { bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...)) return bytes } + +// PrivateKeyFromBytes convert a serialized public key to a PrivateKey. +// It errors if the slice is not the right size. +func PrivateKeyFromBytes(b []byte) (PrivateKey, error) { + if len(b) != ed25519.PrivateKeySize { + return nil, fmt.Errorf("invalid ed25519 private key size") + } + return b, nil +} + +// Sign signs the message with privateKey and returns a signature. +// It will panic if len(privateKey) is not [PrivateKeySize]. +func Sign(privateKey PrivateKey, message []byte) []byte { + return ed25519.Sign(privateKey, message) +} diff --git a/verifications/x25519/KeyAgreementKey2020.go b/verifications/x25519/KeyAgreementKey2020.go index 59bedb5..ad7d594 100644 --- a/verifications/x25519/KeyAgreementKey2020.go +++ b/verifications/x25519/KeyAgreementKey2020.go @@ -96,7 +96,21 @@ func (k KeyAgreementKey2020) JsonLdContext() string { return JsonLdContext } -// TODO: make it part of did.VerificationMethodKeyAgreement in some way -func (k KeyAgreementKey2020) KeyAgreement(priv PrivateKey) ([]byte, error) { - return priv.ECDH(k.pubkey) +func (k KeyAgreementKey2020) PrivateKeyIsCompatible(local did.PrivateKey) bool { + _, ok := local.(PrivateKey) + return ok +} + +func (k KeyAgreementKey2020) ECDH(local did.PrivateKey) ([]byte, error) { + cast, ok := local.(PrivateKey) + if !ok { + return nil, errors.New("private key type doesn't match the public key type") + } + if cast == nil { + return nil, errors.New("invalid private key") + } + if k.pubkey.Curve() != cast.Curve() { + return nil, errors.New("key curves don't match") + } + return cast.ECDH(k.pubkey) } diff --git a/verifications/x25519/key.go b/verifications/x25519/key.go index 6743fd3..965daef 100644 --- a/verifications/x25519/key.go +++ b/verifications/x25519/key.go @@ -15,7 +15,14 @@ import ( type PublicKey = *ecdh.PublicKey type PrivateKey = *ecdh.PrivateKey -const PublicKeySize = 32 +const ( + // PublicKeySize is the size, in bytes, of public keys as used in this package. + PublicKeySize = 32 + // PrivateKeySize is the size, in bytes, of private keys as used in this package. + PrivateKeySize = 32 + // SignatureSize is the size, in bytes, of signatures generated and verified by this package. + SignatureSize = 32 +) func GenerateKeyPair() (PublicKey, PrivateKey, error) { priv, err := ecdh.X25519().GenerateKey(rand.Reader) @@ -25,6 +32,8 @@ func GenerateKeyPair() (PublicKey, PrivateKey, error) { return priv.Public().(PublicKey), priv, nil } +// PublicKeyFromBytes convert a serialized public key to a PublicKey. +// It errors if the slice is not the right size. func PublicKeyFromBytes(b []byte) (PublicKey, error) { return ecdh.X25519().NewPublicKey(b) } @@ -115,6 +124,12 @@ func PublicKeyToMultibase(pub PublicKey) string { return bytes } +// PrivateKeyFromBytes convert a serialized public key to a PrivateKey. +// It errors if len(privateKey) is not [PrivateKeySize]. +func PrivateKeyFromBytes(b []byte) (PrivateKey, error) { + return ecdh.X25519().NewPrivateKey(b) +} + func reverseBytes(b []byte) []byte { r := make([]byte, len(b)) for i := 0; i < len(b); i++ {