Skip to content

Commit

Permalink
Support ION DID Reconstruction & Long Form DID resolution (#389)
Browse files Browse the repository at this point in the history
* support ion did reconstruction

* name conflicts

* badrename

* Switch to custom unmarshaller

* pr comments

* Update did/ion/did.go

Co-authored-by: Andres Uribe <[email protected]>

* Update did/ion/did.go

Co-authored-by: Andres Uribe <[email protected]>

* pr comments

---------

Co-authored-by: Andres Uribe <[email protected]>
  • Loading branch information
decentralgabe and andresuribe87 authored May 24, 2023
1 parent 3c2456d commit bb5e0d7
Show file tree
Hide file tree
Showing 20 changed files with 728 additions and 230 deletions.
173 changes: 173 additions & 0 deletions did/ion/did.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package ion

import (
"fmt"
"strings"

"github.com/TBD54566975/ssi-sdk/crypto/jwx"
"github.com/TBD54566975/ssi-sdk/cryptosuite"
"github.com/goccy/go-json"

"github.com/TBD54566975/ssi-sdk/did"
Expand All @@ -17,6 +19,21 @@ type InitialState struct {
Delta Delta `json:"delta,omitempty"`
}

func (is InitialState) ToDIDStrings() (shortFormDID string, longFormDID string, err error) {
shortFormDID, err = CreateShortFormDID(is.SuffixData)
if err != nil {
return shortFormDID, longFormDID, err
}
initialStateBytesCanonical, err := CanonicalizeAny(is)
if err != nil {
err = errors.Wrap(err, "canonicalizing long form DID suffix data")
return shortFormDID, longFormDID, err
}
encoded := Encode(initialStateBytesCanonical)
longFormDID = strings.Join([]string{shortFormDID, encoded}, ":")
return shortFormDID, longFormDID, nil
}

// CreateLongFormDID generates a long form DID URI representation from a document, recovery, and update keys,
// intended to be the initial state of a DID Document. The method follows the guidelines in the spec:
// https://identity.foundation/sidetree/spec/#long-form-did-uris
Expand All @@ -41,6 +58,11 @@ func CreateLongFormDID(recoveryKey, updateKey jwx.PublicKeyJWK, document Documen
return strings.Join([]string{shortFormDID, encoded}, ":"), nil
}

// IsLongFormDID checks if a string is a long form DID URI
func IsLongFormDID(maybeLongFormDID string) bool {
return strings.Count(maybeLongFormDID, ":") == 3
}

// DecodeLongFormDID decodes a long form DID into a short form DID and
// its create operation suffix data
func DecodeLongFormDID(longFormDID string) (string, *InitialState, error) {
Expand Down Expand Up @@ -85,3 +107,154 @@ func LongToShortFormDID(longFormDID string) (string, error) {
}
return shortFormDID, nil
}

// PatchesToDIDDocument applies a list of sidetree state patches in order resulting in a DID Document.
func PatchesToDIDDocument(shortFormDID, longFormDID string, patches []Patch) (*did.Document, error) {
if len(patches) == 0 {
return nil, errors.New("no patches to apply")
}
if shortFormDID == "" {
return nil, errors.New("short form DID is required")
}
doc := did.Document{
Context: []string{"https://www.w3.org/ns/did/v1"},
ID: shortFormDID,
AlsoKnownAs: longFormDID,
}
for _, patch := range patches {
switch patch.GetAction() {
case AddServices:
addServicePatch := patch.(AddServicesAction)
doc.Services = append(doc.Services, addServicePatch.Services...)
case RemoveServices:
removeServicePatch := patch.(RemoveServicesAction)
for _, id := range removeServicePatch.IDs {
for i, service := range doc.Services {
if service.ID == id {
doc.Services = append(doc.Services[:i], doc.Services[i+1:]...)
}
}
}
case AddPublicKeys:
addKeyPatch := patch.(AddPublicKeysAction)
gotDoc, err := addPublicKeysPatch(doc, addKeyPatch)
if err != nil {
return nil, err
}
doc = *gotDoc
case RemovePublicKeys:
removeKeyPatch := patch.(RemovePublicKeysAction)
gotDoc, err := removePublicKeysPatch(doc, removeKeyPatch)
if err != nil {
return nil, err
}
doc = *gotDoc
case Replace:
replacePatch := patch.(ReplaceAction)
gotDoc, err := replaceActionPatch(doc, replacePatch)
if err != nil {
return nil, err
}
doc = *gotDoc
default:
return nil, fmt.Errorf("unknown patch type: %T", patch)
}
}
return &doc, nil
}

func replaceActionPatch(doc did.Document, patch ReplaceAction) (*did.Document, error) {
// first zero out all public keys and services
doc.VerificationMethod = nil
doc.Authentication = nil
doc.AssertionMethod = nil
doc.KeyAgreement = nil
doc.CapabilityInvocation = nil
doc.CapabilityDelegation = nil
doc.Services = nil

// now add back what the patch includes
gotDoc, err := addPublicKeysPatch(doc, AddPublicKeysAction{PublicKeys: patch.Document.PublicKeys})
if err != nil {
return nil, err
}
doc = *gotDoc
for _, service := range patch.Document.Services {
doc.Services = append(doc.Services, service)
}
return &doc, nil
}

func addPublicKeysPatch(doc did.Document, patch AddPublicKeysAction) (*did.Document, error) {
for _, key := range patch.PublicKeys {
currKey := key
doc.VerificationMethod = append(doc.VerificationMethod, did.VerificationMethod{
ID: currKey.ID,
Type: cryptosuite.LDKeyType(currKey.Type),
Controller: doc.ID,
PublicKeyJWK: &currKey.PublicKeyJWK,
})
for _, purpose := range currKey.Purposes {
switch purpose {
case Authentication:
doc.Authentication = append(doc.Authentication, currKey.ID)
case AssertionMethod:
doc.AssertionMethod = append(doc.AssertionMethod, currKey.ID)
case KeyAgreement:
doc.KeyAgreement = append(doc.KeyAgreement, currKey.ID)
case CapabilityInvocation:
doc.CapabilityInvocation = append(doc.CapabilityInvocation, currKey.ID)
case CapabilityDelegation:
doc.CapabilityDelegation = append(doc.CapabilityDelegation, currKey.ID)
default:
return nil, fmt.Errorf("unknown key purpose: %s:%s", currKey.ID, purpose)
}
}
}
return &doc, nil
}

func removePublicKeysPatch(doc did.Document, patch RemovePublicKeysAction) (*did.Document, error) {
for _, id := range patch.IDs {
removed := false
for i, key := range doc.VerificationMethod {
if key.ID != id {
continue
}
doc.VerificationMethod = append(doc.VerificationMethod[:i], doc.VerificationMethod[i+1:]...)
removed = true

// TODO(gabe): in the future handle the case where the value is not a simple ID
// remove from all other key lists
for j, a := range doc.Authentication {
if a == id {
doc.Authentication = append(doc.Authentication[:j], doc.Authentication[j+1:]...)
}
}
for j, am := range doc.AssertionMethod {
if am == id {
doc.AssertionMethod = append(doc.AssertionMethod[:j], doc.AssertionMethod[j+1:]...)
}
}
for j, ka := range doc.KeyAgreement {
if ka == id {
doc.KeyAgreement = append(doc.KeyAgreement[:j], doc.KeyAgreement[j+1:]...)
}
}
for j, ci := range doc.CapabilityInvocation {
if ci == id {
doc.CapabilityInvocation = append(doc.CapabilityInvocation[:j], doc.CapabilityInvocation[j+1:]...)
}
}
for j, cd := range doc.CapabilityDelegation {
if cd == id {
doc.CapabilityDelegation = append(doc.CapabilityDelegation[:j], doc.CapabilityDelegation[j+1:]...)
}
}
}
if !removed {
return nil, fmt.Errorf("could not find key with id %s", id)
}
}
return &doc, nil
}
Loading

0 comments on commit bb5e0d7

Please sign in to comment.