Skip to content
This repository has been archived by the owner on Mar 27, 2024. It is now read-only.

Commit

Permalink
feat: implement did-core JSON-LD @context representation (#3294)
Browse files Browse the repository at this point in the history
Signed-off-by: Chris Abernethy <brownoxford@gmail.com>
  • Loading branch information
brownoxford committed Jul 25, 2022
1 parent 9bdda1a commit 78317f6
Show file tree
Hide file tree
Showing 5 changed files with 497 additions and 51 deletions.
95 changes: 52 additions & 43 deletions pkg/doc/did/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,13 @@ func ParseDIDURL(didURL string) (*DIDURL, error) {
return ret, nil
}

// Context represents JSON-LD representation-specific DID-core @context, which
// must be either a string, or a list containing maps and/or strings.
type Context interface{}

// DocResolution did resolution.
type DocResolution struct {
Context []string
Context Context
DIDDocument *Doc
DocumentMetadata *DocumentMetadata
}
Expand Down Expand Up @@ -224,7 +228,7 @@ type DocumentMetadata struct {
}

type rawDocResolution struct {
Context interface{} `json:"@context"`
Context Context `json:"@context"`
DIDDocument json.RawMessage `json:"didDocument,omitempty"`
DocumentMetadata json.RawMessage `json:"didDocumentMetadata,omitempty"`
}
Expand Down Expand Up @@ -261,7 +265,7 @@ func ParseDocumentResolution(data []byte) (*DocResolution, error) {

// Doc DID Document definition.
type Doc struct {
Context []string
Context Context
ID string
AlsoKnownAs []string
VerificationMethod []VerificationMethod
Expand Down Expand Up @@ -427,7 +431,7 @@ func NewReferencedVerification(vm *VerificationMethod, r VerificationRelationshi
}

type rawDoc struct {
Context interface{} `json:"@context,omitempty"`
Context Context `json:"@context,omitempty"`
ID string `json:"id,omitempty"`
AlsoKnownAs []interface{} `json:"alsoKnownAs,omitempty"`
VerificationMethod []map[string]interface{} `json:"verificationMethod,omitempty"`
Expand Down Expand Up @@ -507,7 +511,9 @@ func ParseDocument(data []byte) (*Doc, error) {
verificationMethod = raw.VerificationMethod
}

vm, err := populateVerificationMethod(context[0], doc.ID, baseURI, verificationMethod)
schema, _ := ContextPeekString(context)

vm, err := populateVerificationMethod(schema, doc.ID, baseURI, verificationMethod)
if err != nil {
return nil, fmt.Errorf("populate verification method failed: %w", err)
}
Expand All @@ -519,7 +525,7 @@ func ParseDocument(data []byte) (*Doc, error) {
return nil, err
}

proofs, err := populateProofs(context[0], doc.ID, baseURI, raw.Proof)
proofs, err := populateProofs(schema, doc.ID, baseURI, raw.Proof)
if err != nil {
return nil, fmt.Errorf("populate proofs failed: %w", err)
}
Expand All @@ -530,18 +536,10 @@ func ParseDocument(data []byte) (*Doc, error) {
}

func requiresLegacyHandling(raw *rawDoc) bool {
context, _ := parseContext(raw.Context)

for _, ctx := range context {
if ctx == ContextV1Old {
// aca-py issue: https://github.com/hyperledger/aries-cloudagent-python/issues/1048
// old v1 context is (currently) only used by projects like aca-py that
// have not fully updated to latest did spec for aip2.0
return true
}
}

return false
// aca-py issue: https://github.com/hyperledger/aries-cloudagent-python/issues/1048
// old v1 context is (currently) only used by projects like aca-py that
// have not fully updated to latest did spec for aip2.0
return ContextContainsString(raw.Context, ContextV1Old)
}

func populateVerificationRelationships(doc *Doc, raw *rawDoc) error {
Expand Down Expand Up @@ -758,7 +756,7 @@ func getVerification(doc *Doc, rawVerification interface{},
relationship VerificationRelationship) ([]Verification, error) {
// context, docID string
vm := doc.VerificationMethod
context := doc.Context[0]
context, _ := ContextPeekString(doc.Context)

keyID, keyIDExist := rawVerification.(string)
if keyIDExist {
Expand Down Expand Up @@ -957,42 +955,45 @@ func decodeVMJwk(jwkMap map[string]interface{}, vm *VerificationMethod) error {
return nil
}

func parseContext(context interface{}) ([]string, string) {
func parseContext(context Context) (Context, string) {
context = ContextCopy(context)

switch ctx := context.(type) {
case string, []string:
return ctx, ""
case []interface{}:
var context []string
// copy slice to prevent unexpected mutation
var newContext []interface{}

var base string

for _, v := range ctx {
switch value := v.(type) {
case string:
context = append(context, value)
newContext = append(newContext, value)
case map[string]interface{}:
baseValue, ok := value["@base"].(string)
if ok {
// preserve base value if it exists and is a string
if baseValue, ok := value["@base"].(string); ok {
base = baseValue
}

delete(value, "@base")

if len(value) > 0 {
newContext = append(newContext, value)
}
}
}

return context, base
case []string:
return ctx, ""
case interface{}:
return []string{context.(string)}, ""
return ContextCleanup(newContext), base
}

return []string{""}, ""
return "", ""
}

func (r *rawDoc) schemaLoader() gojsonschema.JSONLoader {
context, _ := parseContext(r.Context)
if len(context) == 0 {
return schemaLoaderV1
}

switch context[0] {
context, _ := ContextPeekString(r.Context)
switch context {
case contextV011:
return schemaLoaderV011
case contextV12019:
Expand Down Expand Up @@ -1111,10 +1112,9 @@ func (docResolution *DocResolution) JSONBytes() ([]byte, error) {

// JSONBytes converts document to json bytes.
func (doc *Doc) JSONBytes() ([]byte, error) {
context := ContextV1

if len(doc.Context) > 0 {
context = doc.Context[0]
context, ok := ContextPeekString(doc.Context)
if !ok {
context = ContextV1
}

aka := populateRawAlsoKnownAs(doc.AlsoKnownAs)
Expand Down Expand Up @@ -1172,14 +1172,23 @@ func (doc *Doc) JSONBytes() ([]byte, error) {
return byteDoc, nil
}

func contextWithBase(doc *Doc) []interface{} {
func contextWithBase(doc *Doc) Context {
baseObject := make(map[string]interface{})
baseObject["@base"] = doc.processingMeta.baseURI

m := make([]interface{}, 0)

for _, v := range doc.Context {
m = append(m, v)
switch ctx := doc.Context.(type) {
case string:
m = append(m, ctx)
case []string:
for _, item := range ctx {
m = append(m, item)
}
case []interface{}:
if len(ctx) > 0 {
m = append(m, ctx...)
}
}

m = append(m, baseObject)
Expand Down
156 changes: 150 additions & 6 deletions pkg/doc/did/doc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ func TestValidWithDocBase(t *testing.T) {
doc, err := ParseDocument([]byte(d))
require.NoError(t, err)
require.NotNil(t, doc)
require.Contains(t, doc.Context[0], "https://www.w3.org/ns/did/v")

context, ok := doc.Context.([]string)
require.True(t, ok)
require.Contains(t, context[0], "https://www.w3.org/ns/did/v")

// test doc id
require.Equal(t, doc.ID, "did:example:123456789abcdefghi")
Expand Down Expand Up @@ -176,8 +179,7 @@ func TestDocResolution(t *testing.T) {
d, err := ParseDocumentResolution([]byte(validDocResolution))
require.NoError(t, err)

require.Equal(t, 1, len(d.Context))
require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context[0])
require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context.(string))
require.Equal(t, "did:example:21tDAKCERh95uGgKbJNHYp", d.DIDDocument.ID)
require.Equal(t, 1, len(d.DIDDocument.AlsoKnownAs))
require.Equal(t, "did:example:123", d.DIDDocument.AlsoKnownAs[0])
Expand All @@ -190,8 +192,7 @@ func TestDocResolution(t *testing.T) {
d, err = ParseDocumentResolution(bytes)
require.NoError(t, err)

require.Equal(t, 1, len(d.Context))
require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context[0])
require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context.(string))
require.Equal(t, "did:example:21tDAKCERh95uGgKbJNHYp", d.DIDDocument.ID)
require.Equal(t, 1, len(d.DIDDocument.AlsoKnownAs))
require.Equal(t, "did:example:123", d.DIDDocument.AlsoKnownAs[0])
Expand All @@ -206,13 +207,156 @@ func TestDocResolution(t *testing.T) {
})
}

func TestContextVariations(t *testing.T) {
var (
Base = "did:example:123456789abcdefghi"
Vocab = "https://www.w3.org/ns/did/#"
ContextDIDv1 = "https://www.w3.org/ns/did/v1"
ContextTraceability = "https://w3id.org/traceability/v1"
ContextBase = map[string]interface{}{"@base": Base}
ContextVocab = map[string]interface{}{"@vocab": Vocab}
ContextMixed = map[string]interface{}{"@base": Base, "@vocab": Vocab}
)

tests := map[string]struct {
input Context
context Context
base string
}{
"'string'": {
input: ContextDIDv1,
context: ContextDIDv1,
base: "",
},
"'string' empty": {
input: "",
context: "",
base: "",
},
"'[]string' empty": {
input: []string{""},
context: []string{""},
base: "",
},
"'[]string' single": {
input: []string{ContextDIDv1},
context: []string{ContextDIDv1},
base: "",
},
"'[]string' multiple": {
input: []string{ContextDIDv1, ContextTraceability},
context: []string{ContextDIDv1, ContextTraceability},
base: "",
},
"'[]interface{}' empty string": {
input: []interface{}{""},
context: []string{""},
base: "",
},
"'[]interface{}' single string": {
input: []interface{}{ContextDIDv1},
context: []string{ContextDIDv1},
base: "",
},
"'[]interface{}' multiple string": {
input: []interface{}{ContextDIDv1, ContextTraceability},
context: []string{ContextDIDv1, ContextTraceability},
base: "",
},
"'[]interface{}' string + base": {
input: []interface{}{ContextDIDv1, ContextBase},
context: []string{ContextDIDv1},
base: Base,
},
"'[]interface{}' string + vocab": {
input: []interface{}{ContextDIDv1, ContextVocab},
context: []interface{}{ContextDIDv1, ContextVocab},
base: "",
},
"'[]interface{}' string + vocab + base": {
input: []interface{}{ContextDIDv1, ContextVocab, ContextBase},
context: []interface{}{ContextDIDv1, ContextVocab},
base: Base,
},
"'[]interface{}' string + mixed": {
input: []interface{}{ContextDIDv1, ContextMixed},
context: []interface{}{ContextDIDv1, ContextVocab},
base: Base,
},
"'[]interface{}' base": {
input: []interface{}{ContextBase},
context: "",
base: Base,
},
"'[]interface{}' vocab": {
input: []interface{}{ContextVocab},
context: []interface{}{ContextVocab},
base: "",
},
"'[]interface{}' base + vocab": {
input: []interface{}{ContextBase, ContextVocab},
context: []interface{}{ContextVocab},
base: Base,
},
"'[]interface{}' mixed": {
input: []interface{}{ContextMixed},
context: []interface{}{ContextVocab},
base: Base,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
context, base := parseContext(tc.input)
require.Equal(t, tc.context, context)
require.Equal(t, tc.base, base)
})
}
}

func TestContextMutationPrevention(t *testing.T) {
t.Run("string array mutation", func(t *testing.T) {
oldContext := []string{"stringval"}
newContext, _ := parseContext(oldContext)

a0, ok := newContext.([]string)
require.True(t, ok)

a0[0] = "mutated_stringval"
require.Equal(t, []string{"stringval"}, oldContext)
})

t.Run("map element mutation (@base)", func(t *testing.T) {
oldContext := []interface{}{map[string]interface{}{"@base": "baseval"}}
_, _ = parseContext(oldContext)
require.Equal(t, []interface{}{map[string]interface{}{"@base": "baseval"}}, oldContext)
})

t.Run("map element mutation (not @base)", func(t *testing.T) {
oldContext := []interface{}{map[string]interface{}{"@key": "keyval"}}
newContext, _ := parseContext(oldContext)

a0, ok := newContext.([]interface{})
require.True(t, ok)

m0, ok := a0[0].(map[string]interface{})
require.True(t, ok)

m0["@key"] = "keyval_mutated"
require.Equal(t, []interface{}{map[string]interface{}{"@key": "keyval"}}, oldContext)
})
}

func TestValid(t *testing.T) {
docs := []string{validDoc}
for _, d := range docs {
doc, err := ParseDocument([]byte(d))
require.NoError(t, err)
require.NotNil(t, doc)
require.Contains(t, doc.Context[0], "https://www.w3.org/ns/did/v")

context, ok := doc.Context.([]string)
require.True(t, ok)
require.Contains(t, context[0], "https://www.w3.org/ns/did/v")

// test doc id
require.Equal(t, doc.ID, "did:example:21tDAKCERh95uGgKbJNHYp")
Expand Down
Loading

0 comments on commit 78317f6

Please sign in to comment.