diff --git a/document/document.go b/document/document.go new file mode 100644 index 0000000..e6451f1 --- /dev/null +++ b/document/document.go @@ -0,0 +1,311 @@ +package document + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/url" + + "github.com/INFURA/go-did" + "github.com/INFURA/go-did/verifications" +) + +var _ did.Document = &Document{} + +// Document is a did.Document decoded from an arbitrary Json Document. +// It does not know anything about the DID method used to produce that document. +type Document struct { + context []string + id string + alsoKnownAs []*url.URL + controllers []string + verificationMethods map[string]did.VerificationMethod + authentication []did.VerificationMethodSignature + assertion []did.VerificationMethodSignature + keyAgreement []did.VerificationMethodKeyAgreement + capabilityInvocation []did.VerificationMethodSignature + capabilityDelegation []did.VerificationMethodSignature +} + +type aux struct { + Context []string `json:"@context"` + Id string `json:"id"` + AlsoKnownAs []string `json:"alsoKnownAs,omitempty"` + Controllers json.RawMessage `json:"controllers,omitempty"` + VerificationMethods []json.RawMessage `json:"verificationMethod,omitempty"` + Authentication []json.RawMessage `json:"authentication,omitempty"` + Assertion []json.RawMessage `json:"assertionMethod,omitempty"` + KeyAgreement []json.RawMessage `json:"keyAgreement,omitempty"` + CapabilityInvocation []json.RawMessage `json:"capabilityInvocation,omitempty"` + CapabilityDelegation []json.RawMessage `json:"capabilityDelegation,omitempty"` +} + +// FromJsonReader decodes an arbitrary Json DID Document into a usable did.Document. +func FromJsonReader(reader io.Reader) (*Document, error) { + // 1 MiB read limit to shut down abuse + reader = io.LimitReader(reader, 1<<20) + + var aux aux + err := json.NewDecoder(reader).Decode(&aux) + if err != nil { + return nil, err + } + return fromAux(&aux) +} + +// FromJsonBytes decodes an arbitrary Json DID Document into a usable did.Document. +func FromJsonBytes(data []byte) (*Document, error) { + var aux aux + err := json.Unmarshal(data, &aux) + if err != nil { + return nil, err + } + return fromAux(&aux) +} + +func fromAux(aux *aux) (*Document, error) { + var err error + res := Document{ + context: aux.Context, + id: aux.Id, + } + + // id + if !did.HasValidDIDSyntax(aux.Id) { // also enforce being required + return nil, errors.New("id has invalid DID syntax") + } + + // alsoKnownAs + res.alsoKnownAs = make([]*url.URL, len(aux.AlsoKnownAs)) + for i, u := range aux.AlsoKnownAs { + res.alsoKnownAs[i], err = url.Parse(u) + if err != nil { + return nil, fmt.Errorf("invalid alsoKnownAs: %w", err) + } + } + + // controller + var s string + var ss []string + switch { + case len(aux.Controllers) == 0: + // nothing to do + case json.Unmarshal(aux.Controllers, &s) == nil: // we have a single string + if !did.HasValidDIDSyntax(s) { + return nil, errors.New("controllers has invalid DID syntax") + } + res.controllers = []string{s} + case json.Unmarshal(aux.Controllers, &ss) == nil: // we have an array of strings + res.controllers = make([]string, len(ss)) + for i, s := range ss { + if !did.HasValidDIDSyntax(s) { + return nil, errors.New("one controllers has an invalid DID syntax") + } + res.controllers[i] = s + } + default: + return nil, fmt.Errorf("invalid controllers") + } + + // verificationMethods + res.verificationMethods = map[string]did.VerificationMethod{} + for _, m := range aux.VerificationMethods { + vm, err := verifications.UnmarshalJSON(m) + if err != nil { + return nil, fmt.Errorf("invalid verificationMethods: %w", err) + } + res.verificationMethods[vm.ID()] = vm + } + + // authentication + res.authentication, err = resolveVerificationMethods[did.VerificationMethodSignature](&res, aux.Authentication) + if err != nil { + return nil, fmt.Errorf("invalid authentication: %w", err) + } + + // assertion + res.assertion, err = resolveVerificationMethods[did.VerificationMethodSignature](&res, aux.Assertion) + if err != nil { + return nil, fmt.Errorf("invalid assertion: %w", err) + } + + // keyAgreement + res.keyAgreement, err = resolveVerificationMethods[did.VerificationMethodKeyAgreement](&res, aux.KeyAgreement) + if err != nil { + return nil, fmt.Errorf("invalid keyAgreement: %w", err) + } + + // capabilityInvocation + res.capabilityInvocation, err = resolveVerificationMethods[did.VerificationMethodSignature](&res, aux.CapabilityInvocation) + if err != nil { + return nil, fmt.Errorf("invalid capabilityInvocation: %w", err) + } + + // capabilityDelegation + res.capabilityDelegation, err = resolveVerificationMethods[did.VerificationMethodSignature](&res, aux.CapabilityDelegation) + if err != nil { + return nil, fmt.Errorf("invalid capabilityDelegation: %w", err) + } + + return &res, nil +} + +func resolveVerificationMethods[T did.VerificationMethod](doc *Document, msgs []json.RawMessage) ([]T, error) { + res := make([]T, len(msgs)) + for i, auth := range msgs { + var s string + if json.Unmarshal(auth, &s) == nil { + // We have a string, we need to resolve it. + // This can normally be an internal reference (with a fragment), but can also be a complete DID URL that + // requires an external lookup. For simplicity, we don't support that (yet?). + + vm, ok := doc.verificationMethods[s] + if !ok { + return nil, fmt.Errorf("invalid verification method reference: %s", s) + } + cast, ok := vm.(T) + if !ok { + return nil, fmt.Errorf("resolved verification method doesn't match the expected capabilities: %T instead of %T", vm, new(T)) + } + res[i] = cast + continue + } + + vm, err := verifications.UnmarshalJSON(auth) + if err == nil { + // we have a complete verification method + vms, ok := vm.(T) + if !ok { + return nil, fmt.Errorf("verification method doesn't match the expected capabilities: %T instead of %T", vm, new(T)) + } + res[i] = vms + continue + } + + return nil, fmt.Errorf("invalid verification method value: %w", err) + } + return res, nil +} + +func (d Document) MarshalJSON() ([]byte, error) { + var err error + + data := aux{Context: d.context, Id: d.id} + + // alsoKnownAs + data.AlsoKnownAs = make([]string, len(d.alsoKnownAs)) + for i, u := range d.alsoKnownAs { + data.AlsoKnownAs[i] = u.String() + } + + // controllers + switch { + case len(d.controllers) == 1: + data.Controllers, err = json.Marshal(d.controllers[0]) + case len(d.controllers) > 1: + data.Controllers, err = json.Marshal(d.controllers) + } + if err != nil { + return nil, err + } + + // verificationMethods + data.VerificationMethods = make([]json.RawMessage, len(d.verificationMethods)) + i := 0 + for _, method := range d.verificationMethods { + data.VerificationMethods[i], err = json.Marshal(method) + if err != nil { + return nil, err + } + i++ + } + + // authentication + data.Authentication, err = marshalMethods[did.VerificationMethodSignature](&d, d.authentication) + if err != nil { + return nil, err + } + + // assertion + data.Assertion, err = marshalMethods[did.VerificationMethodSignature](&d, d.assertion) + if err != nil { + return nil, err + } + + // keyAgreement + data.KeyAgreement, err = marshalMethods[did.VerificationMethodKeyAgreement](&d, d.keyAgreement) + if err != nil { + return nil, err + } + + // capabilityInvocation + data.CapabilityInvocation, err = marshalMethods[did.VerificationMethodSignature](&d, d.capabilityInvocation) + if err != nil { + return nil, err + } + + // capabilityDelegation + data.CapabilityDelegation, err = marshalMethods[did.VerificationMethodSignature](&d, d.capabilityDelegation) + if err != nil { + return nil, err + } + + return json.Marshal(data) +} + +func marshalMethods[T did.VerificationMethod](d *Document, methods []T) ([]json.RawMessage, error) { + var err error + res := make([]json.RawMessage, len(methods)) + for i, method := range methods { + if _, ok := d.verificationMethods[method.ID()]; ok { + res[i], err = json.Marshal(method.ID()) + } else { + res[i], err = json.Marshal(method) + } + if err != nil { + return nil, err + } + } + return res, nil +} + +func (d Document) Context() []string { + return d.context +} + +func (d Document) ID() string { + return d.id +} + +func (d Document) Controllers() []string { + return d.controllers +} + +func (d Document) AlsoKnownAs() []*url.URL { + return d.alsoKnownAs +} + +func (d Document) VerificationMethods() map[string]did.VerificationMethod { + return d.verificationMethods +} + +func (d Document) Authentication() []did.VerificationMethodSignature { + return d.authentication +} + +func (d Document) Assertion() []did.VerificationMethodSignature { + return d.assertion +} + +func (d Document) KeyAgreement() []did.VerificationMethodKeyAgreement { + return d.keyAgreement +} + +func (d Document) CapabilityInvocation() []did.VerificationMethodSignature { + return d.capabilityInvocation +} + +func (d Document) CapabilityDelegation() []did.VerificationMethodSignature { + return d.capabilityDelegation +} diff --git a/document/document_test.go b/document/document_test.go new file mode 100644 index 0000000..cffecfb --- /dev/null +++ b/document/document_test.go @@ -0,0 +1,63 @@ +package document + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" + + _ "github.com/INFURA/go-did/methods/did-key" + "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/x25519" +) + +func TestRoundTrip(t *testing.T) { + strDoc := ` +{ + "@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" + }] +} +` + doc, err := FromJsonBytes([]byte(strDoc)) + require.NoError(t, err) + + // basic testing + require.Equal(t, "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK", doc.ID()) + require.Equal(t, ed25519.Type, doc.Authentication()[0].Type()) + require.Equal(t, ed25519.Type, doc.Assertion()[0].Type()) + require.Equal(t, x25519.Type, doc.KeyAgreement()[0].Type()) + require.Equal(t, ed25519.Type, doc.CapabilityInvocation()[0].Type()) + require.Equal(t, ed25519.Type, doc.CapabilityDelegation()[0].Type()) + + roundtrip, err := json.Marshal(doc) + require.NoError(t, err) + require.JSONEq(t, strDoc, string(roundtrip)) +} diff --git a/interfaces.go b/interfaces.go index 65c5356..4550037 100644 --- a/interfaces.go +++ b/interfaces.go @@ -34,11 +34,11 @@ type Document interface { // Context is the set of JSON-LD context documents. Context() []string - // ID is the identifier of the Document, which is the DID itself. - ID() DID + // ID is the identifier of the Document, which is the DID itself as string. + ID() string // Controllers is the set of DID that is authorized to make changes to the Document. It's often the same as ID. - Controllers() []DID + Controllers() []string // AlsoKnownAs returns an optional set of URL describing ???TODO AlsoKnownAs() []*url.URL diff --git a/methods/did-key/document.go b/methods/did-key/document.go index 7984a6e..956b3ee 100644 --- a/methods/did-key/document.go +++ b/methods/did-key/document.go @@ -52,11 +52,11 @@ func (d document) Context() []string { ) } -func (d document) ID() did.DID { - return d.id +func (d document) ID() string { + return d.id.String() } -func (d document) Controllers() []did.DID { +func (d document) Controllers() []string { // no controller for did:key, no changes are possible return nil } diff --git a/verifications/json.go b/verifications/json.go new file mode 100644 index 0000000..6b17637 --- /dev/null +++ b/verifications/json.go @@ -0,0 +1,34 @@ +package verifications + +import ( + "encoding/json" + "fmt" + + "github.com/INFURA/go-did" + "github.com/INFURA/go-did/verifications/ed25519" + "github.com/INFURA/go-did/verifications/x25519" +) + +func UnmarshalJSON(data []byte) (did.VerificationMethod, error) { + var aux struct { + Type string + } + if err := json.Unmarshal(data, &aux); err != nil { + return nil, err + } + + var res did.VerificationMethod + switch aux.Type { + case ed25519.Type: + res = &ed25519.VerificationKey2020{} + case x25519.Type: + res = &x25519.KeyAgreementKey2020{} + default: + return nil, fmt.Errorf("unknown verification type: %s", aux.Type) + } + + if err := json.Unmarshal(data, &res); err != nil { + return nil, err + } + return res, nil +}