From 63c51774b69669d6235ccffb715a2ffd56c1ef5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Mur=C3=A9?= Date: Thu, 13 Mar 2025 11:26:39 +0100 Subject: [PATCH] marginally more working --- .gitignore | 14 ++ did-key/document.go | 30 +++- did-key/key.go | 71 +++++----- did.go | 54 ++++--- interfaces.go | 29 ++-- verifications/Ed25519/VerificationKey2020.go | 75 ---------- verifications/ed25519/VerificationKey2020.go | 132 ++++++++++++++++++ .../ed25519/VerificationKey2020_test.go | 28 ++++ 8 files changed, 292 insertions(+), 141 deletions(-) create mode 100644 .gitignore delete mode 100644 verifications/Ed25519/VerificationKey2020.go create mode 100644 verifications/ed25519/VerificationKey2020.go create mode 100644 verifications/ed25519/VerificationKey2020_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..14577db --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +.idea +*.iml +out +gen +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +go.work +go.work.sum +.env diff --git a/did-key/document.go b/did-key/document.go index 3895bb1..4b24f1b 100644 --- a/did-key/document.go +++ b/did-key/document.go @@ -1,6 +1,7 @@ package did_key import ( + "encoding/json" "net/url" "github.com/INFURA/go-did" @@ -14,17 +15,38 @@ type document struct { } func (d document) MarshalJSON() ([]byte, error) { - // TODO implement me - panic("implement me") + return json.Marshal(struct { + Context []string `json:"@context"` + ID string `json:"id"` + AlsoKnownAs []string `json:"alsoKnownAs,omitempty"` + Controller string `json:"controller,omitempty"` + VerificationMethod []did.VerificationMethod `json:"verificationMethod,omitempty"` + Authentication []string `json:"authentication,omitempty"` + AssertionMethod []string `json:"assertionMethod,omitempty"` + KeyAgreement []string `json:"keyAgreement,omitempty"` + CapabilityInvocation []string `json:"capabilityInvocation,omitempty"` + CapabilityDelegation []string `json:"capabilityDelegation,omitempty"` + }{ + Context: []string{did.JsonLdContext, d.verification.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()}, + }) } func (d document) ID() did.DID { return d.id } -func (d document) Controller() did.DID { +func (d document) Controllers() []did.DID { // no external controller possible for did:key - return d.id + return []did.DID{d.id} } func (d document) AlsoKnownAs() []url.URL { diff --git a/did-key/key.go b/did-key/key.go index c99d3a9..a904e2e 100644 --- a/did-key/key.go +++ b/did-key/key.go @@ -3,46 +3,53 @@ package did_key import ( "fmt" "net/url" + "strings" mbase "github.com/multiformats/go-multibase" - varint "github.com/multiformats/go-varint" + "github.com/multiformats/go-varint" "github.com/INFURA/go-did" + "github.com/INFURA/go-did/verifications/ed25519" ) -type multicodecCode uint64 - -// Signature algorithms from the [did:key specification] -// -// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm -const ( - X25519 multicodecCode = 0xec - Ed25519 multicodecCode = 0xed - P256 multicodecCode = 0x1200 - P384 multicodecCode = 0x1201 - P521 multicodecCode = 0x1202 - Secp256k1 multicodecCode = 0xe7 - RSA multicodecCode = 0x1205 -) +// Specification: https://w3c-ccg.github.io/did-method-key/ func Decode(identifier string) (did.DID, error) { - // baseCodec, bytes, err := mbase.Decode(identifier) - _, bytes, err := mbase.Decode(identifier) + const keyPrefix = "did:key:" + + if !strings.HasPrefix(identifier, keyPrefix) { + return nil, fmt.Errorf("must start with 'did:key'") + } + + baseCodec, bytes, err := mbase.Decode(identifier[len(keyPrefix):]) if err != nil { return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) } - // if baseCodec != mbase.Base58BTC { - // return nil, fmt.Errorf("%w: not Base58BTC encoded", did.ErrInvalidDid) - // } - code, _, err := varint.FromUvarint(bytes) + // the specification enforces that encoding + if baseCodec != mbase.Base58BTC { + return nil, fmt.Errorf("%w: not Base58BTC encoded", did.ErrInvalidDid) + } + code, read, err := varint.FromUvarint(bytes) if err != nil { return nil, fmt.Errorf("%w: %w", did.ErrInvalidDid, err) } - switch multicodecCode(code) { - case Ed25519, P256, Secp256k1, RSA: - return DidKey{bytes: string(bytes), code: multicodecCode(code)}, nil + + d := DidKey{identifier: identifier} + + switch code { + case ed25519.MultibaseCode: + d.verification, err = ed25519.NewVerificationKey2020(identifier, bytes[read:], d) + // case P256: // TODO + // case Secp256k1: // TODO + // case RSA: // TODO + default: + return nil, fmt.Errorf("%w: unsupported did:key multicodec: 0x%x", did.ErrInvalidDid, code) } - 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() { @@ -52,9 +59,8 @@ func init() { var _ did.DID = &DidKey{} type DidKey struct { - // TODO: store a verification method instead - code multicodecCode - bytes string // as string instead of []byte to allow the == operator + identifier string // cached value + verification did.VerificationMethod } func (d DidKey) Method() string { @@ -74,11 +80,12 @@ func (d DidKey) Fragment() string { } func (d DidKey) Document() (did.Document, error) { - // TODO implement me - panic("implement me") + return document{ + id: d, + verification: d.verification, + }, nil } func (d DidKey) String() string { - key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes)) - return "did:key:" + key + return d.identifier } diff --git a/did.go b/did.go index 0a6be93..b0400b6 100644 --- a/did.go +++ b/did.go @@ -6,16 +6,13 @@ import ( "sync" ) -// Decoder is a function decoding an identifier ("foo" in "did:example:foo") into a DID. +const JsonLdContext = "https://www.w3.org/ns/did/v1" + +// Decoder is a function decoding a DID string representation ("did:example:foo") into a DID. type Decoder func(identifier string) (DID, error) -var ( - decodersMu sync.RWMutex - decoders = map[string]Decoder{} -) - -// RegisterMethod registers a DID decoder for a given DID method.. -// Method must be the DID method (for example "key" in did:key). +// RegisterMethod registers a DID decoder for a given DID method. +// Method must be the DID method (for example, "key" in did:key). func RegisterMethod(method string, decoder Decoder) { decodersMu.Lock() defer decodersMu.Unlock() @@ -28,24 +25,21 @@ func RegisterMethod(method string, decoder Decoder) { decoders[method] = decoder } -// Parse returns the DID from the string representation or an error if -// the prefix and method are incorrect, if an unknown encryption algorithm -// is specified or if the method-specific-identifier's bytes don't -// represent a public key for the specified encryption algorithm. -func Parse(str string) (DID, error) { +// Parse attempts to decode a DID from its string representation. +func Parse(identifier string) (DID, error) { decodersMu.RLock() defer decodersMu.RUnlock() - if !strings.HasPrefix(str, "did:") { + if !strings.HasPrefix(identifier, "did:") { return nil, fmt.Errorf("%w: must start with \"did:\"", ErrInvalidDid) } - method, identifier, ok := strings.Cut(str[len("did:"):], ":") + method, suffix, ok := strings.Cut(identifier[len("did:"):], ":") if !ok { return nil, fmt.Errorf("%w: must have a method and an identifier", ErrInvalidDid) } - if !checkIdentifier(identifier) { + if !checkSuffix(suffix) { return nil, fmt.Errorf("%w: invalid identifier characters", ErrInvalidDid) } @@ -58,14 +52,27 @@ func Parse(str string) (DID, error) { } // MustParse is like Parse but panics instead of returning an error. -func MustParse(str string) DID { - did, err := Parse(str) +func MustParse(identifier string) DID { + did, err := Parse(identifier) if err != nil { panic(err) } return did } +// HasValidSyntax tells if the given string representation conforms to DID syntax. +// This does NOT verify that the method is supported by this library. +func HasValidSyntax(identifier string) bool { + if !strings.HasPrefix(identifier, "did:") { + return false + } + method, suffix, ok := strings.Cut(identifier[len("did:"):], ":") + if !ok { + return false + } + return checkMethod(method) && checkSuffix(suffix) +} + func checkMethod(method string) bool { if len(method) == 0 { return false @@ -80,14 +87,19 @@ func checkMethod(method string) bool { return true } -func checkIdentifier(identifier string) bool { - if len(identifier) == 0 { +func checkSuffix(suffix string) bool { + if len(suffix) == 0 { return false } // TODO - // for _, char := range identifier { + // for _, char := range suffix { // // } return true } + +var ( + decodersMu sync.RWMutex + decoders = map[string]Decoder{} +) diff --git a/interfaces.go b/interfaces.go index deac510..8a2eea4 100644 --- a/interfaces.go +++ b/interfaces.go @@ -5,7 +5,8 @@ import ( "net/url" ) -type DID interface { // --> implementation for each DID type: key, pkh .. +// DID is a decoded (i.e. from a string) Decentralized Identifiers. +type DID interface { Method() string Path() string Query() url.Values @@ -15,13 +16,15 @@ type DID interface { // --> implementation for each DID type: key, pkh .. String() string // return the full DID URL, with path, query, fragment } -type Document interface { // --> compact implementation, get serialized into json only if necessary +// Document is the interface for a DID document. It represents the "resolved" state of a DID. +type Document interface { json.Marshaler // ID is the identifier of the Document, which is the DID itself. ID() DID - // Controller is the DID that is authorized to make changes to the Document. It's often the same as ID. - Controller() DID + + // Controllers is the set of DID that is authorized to make changes to the Document. It's often the same as ID. + Controllers() []DID // AlsoKnownAs returns an optional set of URL describing ???TODO AlsoKnownAs() []url.URL @@ -55,18 +58,26 @@ type Document interface { // --> compact implementation, get serialized into jso // https://www.w3.org/TR/did-extensions-properties/#service-types } -type VerificationMethod interface { // --> implementation for each method +// VerificationMethod is a common interface for a cryptographic signature verification method. +// For example, Ed25519VerificationKey2020 implements the Ed25519 signature verification. +type VerificationMethod interface { json.Marshaler json.Unmarshaler - // ID is a string identifier for the VerificationMethod + // ID is a string identifier for the VerificationMethod. It can be referenced in a Document. ID() string + // Type is a string identifier of a verification method. // See https://www.w3.org/TR/did-extensions-properties/#verification-method-types Type() string - // ???? TODO - Controller() DID - // Verify that 'sig' is the signed hash of 'data' + // Controller is a DID able to control the VerificationMethod. + // This is not necessarily the same as for DID itself or the Document. + Controller() string + + // JsonLdContext reports the JSON-LD context definition required for this verification method. + JsonLdContext() string + + // Verify checks that 'sig' is a valid signature of 'data'. Verify(data []byte, sig []byte) bool } diff --git a/verifications/Ed25519/VerificationKey2020.go b/verifications/Ed25519/VerificationKey2020.go deleted file mode 100644 index 96e3523..0000000 --- a/verifications/Ed25519/VerificationKey2020.go +++ /dev/null @@ -1,75 +0,0 @@ -package Ed25519 - -import ( - "crypto/ed25519" - "encoding/json" - "errors" - - "github.com/INFURA/go-did" -) - -var _ did.VerificationMethod = &VerificationKey2020{} - -type VerificationKey2020 struct { - id string - pubkey ed25519.PublicKey - controller did.DID -} - -func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) { - if len(pubkey) != ed25519.PublicKeySize { - return nil, errors.New("invalid ed25519 public key size") - } - - return &VerificationKey2020{ - id: id, - pubkey: pubkey, - controller: controller, - }, nil -} - -func (v VerificationKey2020) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { - ID string `json:"id"` - Type string `json:"type"` - Controller string `json:"controller"` - PublicKeyMultibase string `json:"publicKeyMultibase"` - }{ - ID: v.ID(), - Type: v.Type(), - Controller: v.Controller().String(), - PublicKeyMultibase: v, - }) - - /* - - { - "id": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK#z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "type": "Ed25519VerificationKey2020", - "controller": "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", - "publicKeyMultibase": "z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK" - } - - */ -} - -func (v VerificationKey2020) UnmarshalJSON(bytes []byte) error { - // TODO implement me - panic("implement me") -} - -func (v VerificationKey2020) ID() string { - return v.id -} - -func (v VerificationKey2020) Type() string { - return "Ed25519VerificationKey2020" -} - -func (v VerificationKey2020) Controller() did.DID { - return v.controller -} - -func (v VerificationKey2020) Verify(data []byte, sig []byte) bool { - return ed25519.Verify(v.pubkey, data, sig) -} diff --git a/verifications/ed25519/VerificationKey2020.go b/verifications/ed25519/VerificationKey2020.go new file mode 100644 index 0000000..f75490e --- /dev/null +++ b/verifications/ed25519/VerificationKey2020.go @@ -0,0 +1,132 @@ +package ed25519 + +import ( + "crypto/ed25519" + "encoding/json" + "errors" + "fmt" + + mbase "github.com/multiformats/go-multibase" + "github.com/multiformats/go-varint" + + "github.com/INFURA/go-did" +) + +const ( + MultibaseCode = uint64(0xed) + JsonLdContext = "https://w3id.org/security/suites/ed25519-2020/v1" +) + +var _ did.VerificationMethod = &VerificationKey2020{} + +type VerificationKey2020 struct { + id string + pubkey ed25519.PublicKey + controller string +} + +func NewVerificationKey2020(id string, pubkey []byte, controller did.DID) (*VerificationKey2020, error) { + if len(pubkey) != ed25519.PublicKeySize { + return nil, errors.New("invalid ed25519 public key size") + } + + return &VerificationKey2020{ + id: id, + pubkey: pubkey, + controller: controller.String(), + }, nil +} + +func (v VerificationKey2020) MarshalJSON() ([]byte, error) { + return json.Marshal(struct { + ID string `json:"id"` + Type string `json:"type"` + Controller string `json:"controller"` + PublicKeyMultibase string `json:"publicKeyMultibase"` + }{ + ID: v.ID(), + Type: v.Type(), + Controller: v.Controller(), + PublicKeyMultibase: encodePubkey(v.pubkey), + }) +} + +func (v *VerificationKey2020) 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 != v.Type() { + return errors.New("invalid type") + } + v.id = aux.ID + if len(v.id) == 0 { + return errors.New("invalid id") + } + v.pubkey, err = decodePubkey(aux.PublicKeyMultibase) + if err != nil { + return fmt.Errorf("invalid publicKeyMultibase: %w", err) + } + v.controller = aux.Controller + if !did.HasValidSyntax(v.controller) { + return errors.New("invalid controller") + } + return nil +} + +func (v VerificationKey2020) ID() string { + return v.id +} + +func (v VerificationKey2020) Type() string { + return "Ed25519VerificationKey2020" +} + +func (v VerificationKey2020) Controller() string { + return v.controller +} + +func (v VerificationKey2020) JsonLdContext() string { + return JsonLdContext +} + +func (v VerificationKey2020) Verify(data []byte, sig []byte) bool { + return ed25519.Verify(v.pubkey, data, sig) +} + +func encodePubkey(pubkey ed25519.PublicKey) string { + // can only fail with an invalid encoding, but it's hardcoded + bytes, _ := mbase.Encode(mbase.Base58BTC, append(varint.ToUvarint(MultibaseCode), pubkey...)) + return bytes +} + +func decodePubkey(encoded string) (ed25519.PublicKey, error) { + baseCodec, bytes, err := mbase.Decode(encoded) + 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 != ed25519.PublicKeySize { + return nil, fmt.Errorf("invalid ed25519 public key size") + } + return bytes[read:], nil +} diff --git a/verifications/ed25519/VerificationKey2020_test.go b/verifications/ed25519/VerificationKey2020_test.go new file mode 100644 index 0000000..0306bfd --- /dev/null +++ b/verifications/ed25519/VerificationKey2020_test.go @@ -0,0 +1,28 @@ +package ed25519_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + _ "github.com/INFURA/go-did/did-key" + "github.com/INFURA/go-did/verifications/ed25519" +) + +func TestJson(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)) +}