diff --git a/Readme.md b/Readme.md index 1adbd82..99816f0 100644 --- a/Readme.md +++ b/Readme.md @@ -22,7 +22,8 @@ This is an implementation of Decentralized Identifiers (DIDs) in go. It differs from the alternatives in the following ways: - **simple**: made of shared reusable components and clear interfaces -- **fast**: while it supports DID Documents as JSON files, it's not unnecessary in the way (see below) +- **fast**: while it supports DID Documents as JSON files, it's not unnecessary in the way (see below) +- **battery included**: the corresponding cryptographic handling is implemented - **support producing and using DIDs**: unlike some others, this all-in-one implementation is meant to create, manipulate and handle DIDs - **extensible**: you can easily register your custom DID method diff --git a/did-key/document.go b/did-key/document.go index 4b24f1b..6aac9c5 100644 --- a/did-key/document.go +++ b/did-key/document.go @@ -1,4 +1,4 @@ -package did_key +package didkey import ( "encoding/json" @@ -11,7 +11,8 @@ var _ did.Document = &document{} type document struct { id did.DID - verification did.VerificationMethod + signature did.VerificationMethodSignature + keyAgreement did.VerificationMethodKeyAgreement } func (d document) MarshalJSON() ([]byte, error) { @@ -23,20 +24,24 @@ func (d document) MarshalJSON() ([]byte, error) { VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"` Authentication []string `json:"authentication,omitempty"` AssertionMethod []string `json:"assertionMethod,omitempty"` - KeyAgreement []string `json:"keyAgreement,omitempty"` + KeyAgreement []did.VerificationMethod `json:"keyAgreement,omitempty"` CapabilityInvocation []string `json:"capabilityInvocation,omitempty"` CapabilityDelegation []string `json:"capabilityDelegation,omitempty"` }{ - Context: []string{did.JsonLdContext, d.verification.JsonLdContext()}, + Context: stringSet( + did.JsonLdContext, + d.signature.JsonLdContext(), + d.keyAgreement.JsonLdContext(), + ), ID: d.id.String(), AlsoKnownAs: nil, Controller: d.id.String(), - VerificationMethod: []did.VerificationMethod{d.verification}, - Authentication: []string{d.verification.ID()}, - AssertionMethod: []string{d.verification.ID()}, - KeyAgreement: []string{d.verification.ID()}, - CapabilityInvocation: []string{d.verification.ID()}, - CapabilityDelegation: []string{d.verification.ID()}, + VerificationMethod: []did.VerificationMethod{d.signature, d.keyAgreement}, + Authentication: []string{d.signature.ID()}, + AssertionMethod: []string{d.signature.ID()}, + KeyAgreement: []did.VerificationMethod{d.keyAgreement}, + CapabilityInvocation: []string{d.signature.ID()}, + CapabilityDelegation: []string{d.signature.ID()}, }) } @@ -55,26 +60,41 @@ func (d document) AlsoKnownAs() []url.URL { func (d document) VerificationMethods() map[string]did.VerificationMethod { return map[string]did.VerificationMethod{ - d.verification.ID(): d.verification, + d.signature.ID(): d.signature, + d.keyAgreement.ID(): d.keyAgreement, } } -func (d document) Authentication() []did.VerificationMethod { - return []did.VerificationMethod{d.verification} +func (d document) Authentication() []did.VerificationMethodSignature { + return []did.VerificationMethodSignature{d.signature} } -func (d document) Assertion() []did.VerificationMethod { - return []did.VerificationMethod{d.verification} +func (d document) Assertion() []did.VerificationMethodSignature { + return []did.VerificationMethodSignature{d.signature} } -func (d document) KeyAgreement() []did.VerificationMethod { - return []did.VerificationMethod{d.verification} +func (d document) KeyAgreement() []did.VerificationMethodKeyAgreement { + return []did.VerificationMethodKeyAgreement{d.keyAgreement} } -func (d document) CapabilityInvocation() []did.VerificationMethod { - return []did.VerificationMethod{d.verification} +func (d document) CapabilityInvocation() []did.VerificationMethodSignature { + return []did.VerificationMethodSignature{d.signature} } -func (d document) CapabilityDelegation() []did.VerificationMethod { - return []did.VerificationMethod{d.verification} +func (d document) CapabilityDelegation() []did.VerificationMethodSignature { + return []did.VerificationMethodSignature{d.signature} +} + +func stringSet(values ...string) []string { + res := make([]string, 0, len(values)) +loop: + for _, str := range values { + for _, item := range res { + if str == item { + continue loop + } + } + res = append(res, str) + } + return res } diff --git a/did-key/document_test.go b/did-key/document_test.go new file mode 100644 index 0000000..404447d --- /dev/null +++ b/did-key/document_test.go @@ -0,0 +1,77 @@ +package didkey + +import ( + "encoding/json" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/INFURA/go-did" + "github.com/INFURA/go-did/verifications/ed25519" +) + +func TestDocument(t *testing.T) { + d, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") + require.NoError(t, err) + + doc, err := d.Document() + require.NoError(t, err) + + bytes, err := json.MarshalIndent(doc, "", " ") + require.NoError(t, err) + + fmt.Println(string(bytes)) + + const expected = `{ + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://w3id.org/security/suites/ed25519-2020/v1", + "https://w3id.org/security/suites/x25519-2020/v1" + ], + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "verificationMethod": [{ + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }], + "authentication": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "assertionMethod": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "capabilityDelegation": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "capabilityInvocation": [ + "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + ], + "keyAgreement": [{ + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p", + "type": "X25519KeyAgreementKey2020", + "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "publicKeyMultibase": "z6LSj72tK8brWgZja8NLRwPigth2T9QRiG1uH9oKZuKjdh9p" + }] +}` + + require.JSONEq(t, expected, string(bytes)) +} + +func TestJsonRoundTrip(t *testing.T) { + data := `{ + "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "type": "Ed25519VerificationKey2020", + "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", + "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" + }` + + var vm ed25519.VerificationKey2020 + err := json.Unmarshal([]byte(data), &vm) + require.NoError(t, err) + + bytes, err := json.Marshal(vm) + require.NoError(t, err) + require.JSONEq(t, data, string(bytes)) +} diff --git a/did-key/key.go b/did-key/key.go index a904e2e..a50e0d6 100644 --- a/did-key/key.go +++ b/did-key/key.go @@ -1,6 +1,7 @@ -package did_key +package didkey import ( + "crypto" "fmt" "net/url" "strings" @@ -10,10 +11,23 @@ import ( "github.com/INFURA/go-did" "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/x25519" ) // Specification: https://w3c-ccg.github.io/did-method-key/ +func init() { + did.RegisterMethod("key", Decode) +} + +var _ did.DID = &DidKey{} + +type DidKey struct { + identifier string // cached value + signature did.VerificationMethodSignature + keyAgreement did.VerificationMethodKeyAgreement +} + func Decode(identifier string) (did.DID, error) { const keyPrefix = "did:key:" @@ -38,29 +52,49 @@ func Decode(identifier string) (did.DID, error) { switch code { case ed25519.MultibaseCode: - d.verification, err = ed25519.NewVerificationKey2020(identifier, bytes[read:], d) + d.signature, err = ed25519.NewVerificationKey2020(d.identifier, bytes[read:], d) + if err != nil { + return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) + } + xpub, err := x25519.PublicKeyFromEd25519(bytes[read:]) + if err != nil { + return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) + } + d.keyAgreement, err = x25519.NewKeyAgreementKey2020(d.identifier, xpub, d) + if err != nil { + return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) + } + // case P256: // TODO // case Secp256k1: // TODO // case RSA: // TODO default: return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code) } - if err != nil { - return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) - } return d, nil } -func init() { - did.RegisterMethod("key", Decode) +func FromPublicKey(pub PublicKey) (did.DID, error) { + var err error + switch pub := pub.(type) { + case ed25519.PublicKey: + d := DidKey{ + identifier: ed25519.PublicKeyToMultibase(pub), + } + d.signature, err = ed25519.NewVerificationKey2020(d.identifier, pub, d) + if err != nil { + return nil, err + } + return d, nil + + default: + return nil, fmt.Errorf("unsupported public key: %T", pub) + } } -var _ did.DID = &DidKey{} - -type DidKey struct { - identifier string // cached value - verification did.VerificationMethod +func FromPrivateKey(priv PrivateKey) (did.DID, error) { + return FromPublicKey(priv.Public().(PublicKey)) } func (d DidKey) Method() string { @@ -82,10 +116,29 @@ func (d DidKey) Fragment() string { func (d DidKey) Document() (did.Document, error) { return document{ id: d, - verification: d.verification, + signature: d.signature, + keyAgreement: d.keyAgreement, }, nil } func (d DidKey) String() string { return d.identifier } + +func (d DidKey) Equal(d2 did.DID) bool { + if d2, ok := d2.(DidKey); ok { + return d.identifier == d2.identifier + } + return false +} + +// --------------- + +type PublicKey interface { + Equal(x crypto.PublicKey) bool +} + +type PrivateKey interface { + Public() crypto.PublicKey + Equal(x crypto.PrivateKey) bool +} diff --git a/did-key/key_test.go b/did-key/key_test.go index bdf06a8..4c1146a 100644 --- a/did-key/key_test.go +++ b/did-key/key_test.go @@ -1,4 +1,4 @@ -package did_key_test +package didkey_test import ( "testing" @@ -36,6 +36,6 @@ func TestEquivalence(t *testing.T) { did1, err := did.Parse("did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK") require.NoError(t, err) - require.True(t, did0A == did0B) - require.False(t, did0A == did1) + require.True(t, did0A.Equal(did0B)) + require.False(t, did0A.Equal(did1)) } diff --git a/go.mod b/go.mod index b5d9181..bd96eb4 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,14 @@ module github.com/INFURA/go-did -go 1.23 +go 1.23.0 + +toolchain go1.23.1 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 12b76c6..6655010 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ 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 8a2eea4..9cc4418 100644 --- a/interfaces.go +++ b/interfaces.go @@ -14,6 +14,8 @@ type DID interface { Document() (Document, error) String() string // return the full DID URL, with path, query, fragment + + Equal(DID) bool } // Document is the interface for a DID document. It represents the "resolved" state of a DID. @@ -34,25 +36,25 @@ type Document interface { // Authentication defines how the DID is able to authenticate, for purposes such as logging into a website // or engaging in any sort of challenge-response protocol. - Authentication() []VerificationMethod + Authentication() []VerificationMethodSignature // Assertion specifies how the DID subject is expected to express claims, such as for the purposes of issuing // a Verifiable Credential. // See https://www.w3.org/TR/vc-data-model/ - Assertion() []VerificationMethod + Assertion() []VerificationMethodSignature // KeyAgreement specifies how an entity can generate encryption material in order to transmit confidential // information intended for the DID subject, such as for the purposes of establishing a secure communication channel // with the recipient. - KeyAgreement() []VerificationMethod + KeyAgreement() []VerificationMethodKeyAgreement // CapabilityInvocation specifies a verification method that might be used by the DID subject to invoke a // cryptographic capability, such as the authorization to update the DID Document. - CapabilityInvocation() []VerificationMethod + CapabilityInvocation() []VerificationMethodSignature // CapabilityDelegation specifies a mechanism that might be used by the DID subject to delegate a cryptographic // capability to another party, such as delegating the authority to access a specific HTTP API to a subordinate. - CapabilityDelegation() []VerificationMethod + CapabilityDelegation() []VerificationMethodSignature // TODO: Service // https://www.w3.org/TR/did-extensions-properties/#service-types @@ -77,7 +79,22 @@ type VerificationMethod interface { // JsonLdContext reports the JSON-LD context definition required for this verification method. JsonLdContext() string +} + +// VerificationMethodSignature is a VerificationMethod implementing signature verification. +// It can be used for Authentication, Assertion, CapabilityInvocation, CapabilityDelegation +// in a Document. +type VerificationMethodSignature interface { + VerificationMethod // Verify checks that 'sig' is a valid signature of 'data'. Verify(data []byte, sig []byte) bool } + +// VerificationMethodKeyAgreement is a VerificationMethod implementing a shared key agreement. +// It can be used for KeyAgreement in a Document. +type VerificationMethodKeyAgreement interface { + VerificationMethod + + // TODO: function for key agreement +} diff --git a/verifications/ed25519/VerificationKey2020.go b/verifications/ed25519/VerificationKey2020.go index 0f861f8..0328694 100644 --- a/verifications/ed25519/VerificationKey2020.go +++ b/verifications/ed25519/VerificationKey2020.go @@ -2,6 +2,7 @@ package ed25519 import ( "crypto/ed25519" + "crypto/rand" "encoding/json" "errors" "fmt" @@ -12,20 +13,22 @@ import ( "github.com/INFURA/go-did" ) +// Specification: https://w3c.github.io/cg-reports/credentials/CG-FINAL-di-eddsa-2020-20220724/ + const ( MultibaseCode = uint64(0xed) JsonLdContext = "https://w3id.org/security/suites/ed25519-2020/v1" ) -var _ did.VerificationMethod = &VerificationKey2020{} +var _ did.VerificationMethodSignature = &VerificationKey2020{} type VerificationKey2020 struct { id string - pubkey ed25519.PublicKey + pubkey PublicKey controller string } -func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) { +func NewVerificationKey2020(id string, pubkey PublicKey, controller did.DID) (*VerificationKey2020, error) { if len(pubkey) != ed25519.PublicKeySize { return nil, errors.New("invalid ed25519 public key size") } @@ -47,7 +50,7 @@ func (v VerificationKey2020) MarshalJSON() ([]byte, error) { ID: v.ID(), Type: v.Type(), Controller: v.Controller(), - PublicKeyMultibase: encodePubkey(v.pubkey), + PublicKeyMultibase: PublicKeyToMultibase(v.pubkey), }) } @@ -69,7 +72,7 @@ func (v *VerificationKey2020) UnmarshalJSON(bytes []byte) error { if len(v.id) == 0 { return errors.New("invalid id") } - v.pubkey, err = decodePubkey(aux.PublicKeyMultibase) + v.pubkey, err = MultibaseToPublicKey(aux.PublicKeyMultibase) if err != nil { return fmt.Errorf("invalid publicKeyMultibase: %w", err) } @@ -100,16 +103,16 @@ func (v VerificationKey2020) Verify(data []byte, sig []byte) bool { return ed25519.Verify(v.pubkey, data, sig) } -// encodePubkey encodes the public key in a suitable way for publicKeyMultibase -func encodePubkey(pubkey ed25519.PublicKey) string { +// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase +func PublicKeyToMultibase(pub PublicKey) string { // can only fail with an invalid encoding, but it's hardcoded - bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pubkey...)) + bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...)) return bytes } -// decodePubkey decodes the public key from its publicKeyMultibase form -func decodePubkey(encoded string) (ed25519.PublicKey, error) { - baseCodec, bytes, err := mbase.Decode(encoded) +// MultibaseToPublicKey decodes the public key from its publicKeyMultibase form +func MultibaseToPublicKey(multibase string) (PublicKey, error) { + baseCodec, bytes, err := mbase.Decode(multibase) if err != nil { return nil, err } @@ -132,3 +135,12 @@ func decodePubkey(encoded string) (ed25519.PublicKey, error) { } return bytes[read:], nil } + +// ------------ + +type PublicKey = ed25519.PublicKey +type PrivateKey = ed25519.PrivateKey + +func GenerateKeyPair() (PublicKey, PrivateKey, error) { + return ed25519.GenerateKey(rand.Reader) +} diff --git a/verifications/ed25519/VerificationKey2020_test.go b/verifications/ed25519/VerificationKey2020_test.go index 0306bfd..b98ec32 100644 --- a/verifications/ed25519/VerificationKey2020_test.go +++ b/verifications/ed25519/VerificationKey2020_test.go @@ -10,7 +10,7 @@ import ( "github.com/INFURA/go-did/verifications/ed25519" ) -func TestJson(t *testing.T) { +func TestJsonRoundTrip(t *testing.T) { data := `{ "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", "type": "Ed25519VerificationKey2020", @@ -26,3 +26,19 @@ func TestJson(t *testing.T) { require.NoError(t, err) require.JSONEq(t, data, string(bytes)) } + +// func TestSignature(t *testing.T) { +// d, err := didkey.Decode("did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2") +// require.NoError(t, err) +// doc, err := d.Document() +// require.NoError(t, err) +// method := doc.Authentication()[0] +// require.IsType(t, &ed25519.VerificationKey2020{}, method) +// +// require.True(t, method.Verify( +// []byte("node key test"), +// []byte("Tuhz8eG2jqYG4jUbxt14iMd3r2v2eNLftPTfrZfaaFYn5ta7wP3oYfC1rnDVJsLvHAK7j5CmVoXtGoYGL7Lnb5e"), +// )) +// +// // ed25519.NewVerificationKey2020(did, ) +// } diff --git a/verifications/x25519/KeyAgreementKey2020.go b/verifications/x25519/KeyAgreementKey2020.go new file mode 100644 index 0000000..5dc9231 --- /dev/null +++ b/verifications/x25519/KeyAgreementKey2020.go @@ -0,0 +1,131 @@ +package x25519 + +import ( + "encoding/json" + "errors" + "fmt" + + mbase "github.com/multiformats/go-multibase" + "github.com/multiformats/go-varint" + + "github.com/INFURA/go-did" +) + +// Specification: https://w3c-ccg.github.io/did-method-key/#ed25519-x25519 + +const ( + MultibaseCode = uint64(0xec) + JsonLdContext = "https://w3id.org/security/suites/x25519-2020/v1" +) + +var _ did.VerificationMethodKeyAgreement = &KeyAgreementKey2020{} + +type KeyAgreementKey2020 struct { + id string + pubkey PublicKey + controller string +} + +func NewKeyAgreementKey2020(id string, pubkey PublicKey, controller did.DID) (*KeyAgreementKey2020, error) { + if len(pubkey) != PublicKeySize { + return nil, errors.New("invalid x25519 public key size") + } + + return &KeyAgreementKey2020{ + id: id, + pubkey: pubkey, + controller: controller.String(), + }, nil +} + +func (k KeyAgreementKey2020) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyMultibase string `json:"publicKeyMultibase"` + }{ + ID: k.ID(), + Type: k.Type(), + Controller: k.Controller(), + PublicKeyMultibase: PublicKeyToMultibase(k.pubkey), + }) +} + +func (k *KeyAgreementKey2020) UnmarshalJSON(bytes []byte) error { + aux := struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyMultibase string `json:"publicKeyMultibase"` + }{} + err := json.Unmarshal(bytes, &aux) + if err != nil { + return err + } + if aux.Type != k.Type() { + return errors.New("invalid type") + } + k.id = aux.ID + if len(k.id) == 0 { + return errors.New("invalid id") + } + k.pubkey, err = MultibaseToPublicKey(aux.PublicKeyMultibase) + if err != nil { + return fmt.Errorf("invalid publicKeyMultibase: %w", err) + } + k.controller = aux.Controller + if !did.HasValidSyntax(k.controller) { + return errors.New("invalid controller") + } + return nil +} + +func (k KeyAgreementKey2020) ID() string { + return k.id +} + +func (k KeyAgreementKey2020) Type() string { + return "X25519KeyAgreementKey2020" +} + +func (k KeyAgreementKey2020) Controller() string { + return k.controller +} + +func (k KeyAgreementKey2020) JsonLdContext() string { + return JsonLdContext +} + +// PublicKeyToMultibase encodes the public key in a suitable way for publicKeyMultibase +func PublicKeyToMultibase(pub PublicKey) string { + // can only fail with an invalid encoding, but it's hardcoded + bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pub...)) + return bytes +} + +// MultibaseToPublicKey decodes the public key from its publicKeyMultibase form +func MultibaseToPublicKey(multibase string) (PublicKey, error) { + baseCodec, bytes, err := mbase.Decode(multibase) + if err != nil { + return nil, err + } + // the specification enforces that encoding + if baseCodec != mbase.Base58BTC { + return nil, fmt.Errorf("not Base58BTC encoded") + } + code, read, err := varint.FromUvarint(bytes) + if err != nil { + return nil, err + } + if code != MultibaseCode { + return nil, fmt.Errorf("invalid code") + } + if read != 2 { + return nil, fmt.Errorf("unexpected multibase") + } + if len(bytes)-read != PublicKeySize { + return nil, fmt.Errorf("invalid ed25519 public key size") + } + return bytes[read:], nil +} diff --git a/verifications/x25519/KeyAgreementKey2020_test.go b/verifications/x25519/KeyAgreementKey2020_test.go new file mode 100644 index 0000000..3e4cef4 --- /dev/null +++ b/verifications/x25519/KeyAgreementKey2020_test.go @@ -0,0 +1,27 @@ +package x25519_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/INFURA/go-did/verifications/x25519" +) + +func TestJsonRoundTrip(t *testing.T) { + data := `{ + "id": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp#z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW", + "type": "X25519KeyAgreementKey2020", + "controller": "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp", + "publicKeyMultibase": "z6LShs9GGnqk85isEBzzshkuVWrVKsRp24GnDuHk8QWkARMW" + }` + + var vm x25519.KeyAgreementKey2020 + err := json.Unmarshal([]byte(data), &vm) + require.NoError(t, err) + + bytes, err := json.Marshal(vm) + require.NoError(t, err) + require.JSONEq(t, data, string(bytes)) +} diff --git a/verifications/x25519/key.go b/verifications/x25519/key.go new file mode 100644 index 0000000..e13c1ca --- /dev/null +++ b/verifications/x25519/key.go @@ -0,0 +1,116 @@ +package x25519 + +import ( + "bytes" + "crypto" + "crypto/rand" + "fmt" + "io" + + "golang.org/x/crypto/curve25519" + + "github.com/INFURA/go-did/verifications/ed25519" +) + +// This mirrors ed25519's structure for private/public "keys". jwx +// requires dedicated types for these as they drive +// serialization/deserialization logic, as well as encryption types. +// +// Note that with the x25519 scheme, the private key is a sequence of +// 32 bytes, while the public key is the result of X25519(private, +// basepoint). +// +// Portions of this file are from Go's ed25519.go, which is +// Copyright 2016 The Go Authors. All rights reserved. + +// Originally taken from github.com/lestrrat-go/jwx/v2/x25519. + +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 = 64 + // SeedSize is the size, in bytes, of private key seeds. These are the private key representations used by RFC 8032. + SeedSize = 32 +) + +// PublicKey is the type of X25519 public keys +type PublicKey []byte + +// Any methods implemented on PublicKey might need to also be implemented on +// PrivateKey, as the latter embeds the former and will expose its methods. + +// Equal reports whether pub and x have the same value. +func (pub PublicKey) Equal(x crypto.PublicKey) bool { + xx, ok := x.(PublicKey) + if !ok { + return false + } + return bytes.Equal(pub, xx) +} + +// PrivateKey is the type of X25519 private key +type PrivateKey []byte + +// Public returns the PublicKey corresponding to priv. +func (priv PrivateKey) Public() crypto.PublicKey { + publicKey := make([]byte, PublicKeySize) + copy(publicKey, priv[SeedSize:]) + return PublicKey(publicKey) +} + +// Equal reports whether priv and x have the same value. +func (priv PrivateKey) Equal(x crypto.PrivateKey) bool { + xx, ok := x.(PrivateKey) + if !ok { + return false + } + return bytes.Equal(priv, xx) +} + +// NewKeyFromSeed calculates a private key from a seed. It will return +// an error if len(seed) is not SeedSize. This function is provided +// for interoperability with RFC 7748. RFC 7748's private keys +// correspond to seeds in this package. +func NewKeyFromSeed(seed []byte) (PrivateKey, error) { + privateKey := make([]byte, PrivateKeySize) + if len(seed) != SeedSize { + return nil, fmt.Errorf("unexpected seed size: %d", len(seed)) + } + copy(privateKey, seed) + public, err := curve25519.X25519(seed, curve25519.Basepoint) + if err != nil { + return nil, fmt.Errorf(`failed to compute public key: %w`, err) + } + copy(privateKey[SeedSize:], public) + + return privateKey, nil +} + +// GenerateKey generates a public/private key pair using entropy from rand. +// If rand is nil, crypto/rand.Reader will be used. +func GenerateKey() (PublicKey, PrivateKey, error) { + seed := make([]byte, SeedSize) + if _, err := io.ReadFull(rand.Reader, seed); err != nil { + return nil, nil, err + } + + privateKey, err := NewKeyFromSeed(seed) + if err != nil { + return nil, nil, err + } + publicKey := make([]byte, PublicKeySize) + copy(publicKey, privateKey[SeedSize:]) + + return publicKey, privateKey, nil +} + +func PublicKeyFromEd25519(pub ed25519.PublicKey) (PublicKey, error) { + // x, _ := curve25519.X25519(pub, curve25519.Basepoint) + publicKey := make([]byte, PublicKeySize) + copy(publicKey, pub) + publicKey[31] &= 0x7F + publicKey[31] |= 0x40 + publicKey[0] &= 0xF8 + return publicKey, nil +} diff --git a/verifications/x25519/key_test.go b/verifications/x25519/key_test.go new file mode 100644 index 0000000..16bdc88 --- /dev/null +++ b/verifications/x25519/key_test.go @@ -0,0 +1,51 @@ +package x25519_test + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/INFURA/go-did/verifications/x25519" +) + +func TestGenerateKey(t *testing.T) { + t.Run("x25519.GenerateKey()", func(t *testing.T) { + _, _, err := x25519.GenerateKey() + require.NoError(t, err, `x25519.GenerateKey should work`) + }) + t.Run("x25519.NewKeyFromSeed(wrongSeedLength)", func(t *testing.T) { + dummy := make([]byte, x25519.SeedSize-1) + _, err := x25519.NewKeyFromSeed(dummy) + require.Error(t, err, `wrong seed size should result in error`) + }) +} + +func TestNewKeyFromSeed(t *testing.T) { + // These test vectors are from RFC7748 Section 6.1 + const alicePrivHex = `77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a` + const alicePubHex = `8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a` + const bobPrivHex = `5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb` + const bobPubHex = `de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f` + + alicePrivSeed, err := hex.DecodeString(alicePrivHex) + require.NoError(t, err, `alice seed decoded`) + alicePriv, err := x25519.NewKeyFromSeed(alicePrivSeed) + require.NoError(t, err, `alice private key`) + + alicePub := alicePriv.Public().(x25519.PublicKey) + require.Equal(t, hex.EncodeToString(alicePub), alicePubHex, `alice public key`) + + bobPrivSeed, err := hex.DecodeString(bobPrivHex) + require.NoError(t, err, `bob seed decoded`) + bobPriv, err := x25519.NewKeyFromSeed(bobPrivSeed) + require.NoError(t, err, `bob private key`) + + bobPub := bobPriv.Public().(x25519.PublicKey) + require.Equal(t, hex.EncodeToString(bobPub), bobPubHex, `bob public key`) + + require.True(t, bobPriv.Equal(bobPriv), `bobPriv should equal bobPriv`) + require.True(t, bobPub.Equal(bobPub), `bobPub should equal bobPub`) + require.False(t, bobPriv.Equal(bobPub), `bobPriv should NOT equal bobPub`) + require.False(t, bobPub.Equal(bobPriv), `bobPub should NOT equal bobPriv`) +}