diff --git a/pkg/doc/did/doc.go b/pkg/doc/did/doc.go index 69f7c8cd6..1b7d5b46f 100644 --- a/pkg/doc/did/doc.go +++ b/pkg/doc/did/doc.go @@ -263,6 +263,7 @@ func ParseDocumentResolution(data []byte) (*DocResolution, error) { type Doc struct { Context []string ID string + AlsoKnownAs []string VerificationMethod []VerificationMethod Service []Service Authentication []Verification @@ -428,6 +429,7 @@ func NewReferencedVerification(vm *VerificationMethod, r VerificationRelationshi type rawDoc struct { Context interface{} `json:"@context,omitempty"` ID string `json:"id,omitempty"` + AlsoKnownAs []interface{} `json:"alsoKnownAs,omitempty"` VerificationMethod []map[string]interface{} `json:"verificationMethod,omitempty"` PublicKey []map[string]interface{} `json:"publicKey,omitempty"` Service []map[string]interface{} `json:"service,omitempty"` @@ -489,9 +491,10 @@ func ParseDocument(data []byte) (*Doc, error) { } doc := &Doc{ - ID: raw.ID, - Created: raw.Created, - Updated: raw.Updated, + ID: raw.ID, + AlsoKnownAs: stringArray(raw.AlsoKnownAs), + Created: raw.Created, + Updated: raw.Updated, } context, baseURI := parseContext(raw.Context) @@ -1114,6 +1117,8 @@ func (doc *Doc) JSONBytes() ([]byte, error) { context = doc.Context[0] } + aka := populateRawAlsoKnownAs(doc.AlsoKnownAs) + vm, err := populateRawVM(context, doc.ID, doc.processingMeta.baseURI, doc.VerificationMethod) if err != nil { return nil, fmt.Errorf("JSON unmarshalling of Verification Method failed: %w", err) @@ -1148,7 +1153,7 @@ func (doc *Doc) JSONBytes() ([]byte, error) { } raw := &rawDoc{ - Context: doc.Context, ID: doc.ID, VerificationMethod: vm, + Context: doc.Context, ID: doc.ID, AlsoKnownAs: aka, VerificationMethod: vm, Authentication: auths, AssertionMethod: assertionMethods, CapabilityDelegation: capabilityDelegations, CapabilityInvocation: capabilityInvocations, KeyAgreement: keyAgreements, Service: populateRawServices(doc.Service, doc.ID, doc.processingMeta.baseURI), Created: doc.Created, @@ -1395,6 +1400,16 @@ func populateRawServices(services []Service, didID, baseURI string) []map[string return rawServices } +func populateRawAlsoKnownAs(aka []string) []interface{} { + rawAka := make([]interface{}, len(aka)) + + for i, v := range aka { + rawAka[i] = v + } + + return rawAka +} + func populateRawVM(context, didID, baseURI string, pks []VerificationMethod) ([]map[string]interface{}, error) { var rawVM []map[string]interface{} diff --git a/pkg/doc/did/doc_test.go b/pkg/doc/did/doc_test.go index 2a30f0127..e73379033 100644 --- a/pkg/doc/did/doc_test.go +++ b/pkg/doc/did/doc_test.go @@ -89,6 +89,10 @@ func TestValidWithDocBase(t *testing.T) { // test doc id require.Equal(t, doc.ID, "did:example:123456789abcdefghi") + // test alsoKnownAs + require.Equal(t, 1, len(doc.AlsoKnownAs)) + require.Equal(t, "did:example:123", doc.AlsoKnownAs[0]) + hexDecodeValue, err := hex.DecodeString("02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71") block, _ := pem.Decode([]byte(pemPK)) require.NotNil(t, block) @@ -175,6 +179,8 @@ func TestDocResolution(t *testing.T) { require.Equal(t, 1, len(d.Context)) require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context[0]) 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]) require.Equal(t, true, d.DocumentMetadata.Method.Published) require.Equal(t, "did:ex:123333", d.DocumentMetadata.CanonicalID) @@ -187,6 +193,8 @@ func TestDocResolution(t *testing.T) { require.Equal(t, 1, len(d.Context)) require.Equal(t, "https://w3id.org/did-resolution/v1", d.Context[0]) 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]) require.Equal(t, true, d.DocumentMetadata.Method.Published) require.Equal(t, "did:ex:123333", d.DocumentMetadata.CanonicalID) }) @@ -209,6 +217,10 @@ func TestValid(t *testing.T) { // test doc id require.Equal(t, doc.ID, "did:example:21tDAKCERh95uGgKbJNHYp") + // test alsoKnownAs + require.Equal(t, 1, len(doc.AlsoKnownAs)) + require.Equal(t, "did:example:123", doc.AlsoKnownAs[0]) + hexDecodeValue, err := hex.DecodeString("02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71") block, _ := pem.Decode([]byte(pemPK)) require.NotNil(t, block) @@ -561,6 +573,96 @@ func TestValidateDidDocID(t *testing.T) { }) } +func TestValidateDidDocAlsoKnownAs(t *testing.T) { + t.Run("test did doc with duplicate alsoKnownAs entries", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + for _, d := range docs { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = []interface{}{"did:example:123", "did:example:123"} + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.Error(t, err) + require.Contains(t, err.Error(), "must be unique") + } + }) + + t.Run("test did doc with non-uri alsoKnownAs string", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + for _, d := range docs { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = []interface{}{"not.a.valid.uri"} + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.Error(t, err) + require.Contains(t, err.Error(), "Does not match format 'uri'") + } + }) + + t.Run("test did doc with non-string alsoKnownAs entry", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + for _, d := range docs { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = []interface{}{0} + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.Error(t, err) + require.Contains(t, err.Error(), "Invalid type. Expected: string") + } + }) + + t.Run("test did doc with empty alsoKnownAs", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + for _, d := range docs { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = nil + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.NoError(t, err) + } + }) + + t.Run("test did doc with zero-length alsoKnownAs", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + for _, d := range docs { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = []interface{}{} + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.NoError(t, err) + } + }) + + t.Run("test did doc with valid alsoKnownAs entries", func(t *testing.T) { + docs := []string{validDoc, validDocV011} + valid := []interface{}{ + "did:example:123", + "https://social.example/username", + "urn:uuid:1231", + } + for _, d := range docs { + for _, aka := range valid { + raw := &rawDoc{} + require.NoError(t, json.Unmarshal([]byte(d), &raw)) + raw.AlsoKnownAs = []interface{}{aka} + bytes, err := json.Marshal(raw) + require.NoError(t, err) + err = validate(bytes, raw.schemaLoader()) + require.NoError(t, err) + } + } + }) +} + func TestValidateDidDocPublicKey(t *testing.T) { t.Run("test did doc with empty public key", func(t *testing.T) { docs := []string{validDoc, validDocV011} @@ -1498,20 +1600,20 @@ func TestDIDSchemas(t *testing.T) { didStr: `{ "@context": "https://w3id.org/did/v0.11", "id": "did:w123:world", - "assertionMethod": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", - "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", - "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "assertionMethod": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "did:w123:world#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"], - "authentication": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", "", - "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "authentication": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", "", + "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "did:w123:world#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"], - "capabilityDelegation": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", - "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", - "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "capabilityDelegation": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "did:w123:world#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"], - "capabilityInvocation": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", - "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", - "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", + "capabilityInvocation": ["did:w123:world#z6MksHh7qHWvybLg5QTPPdG2DgEjjduBDArV9EF9mRiRzMBN", + "did:w123:world#_Qq0UL2Fq651Q0Fjd6TvnYE-faHiOpRlPVQcY_-tA4A", + "did:w123:world#_TKzHv2jFIyvdTGF1Dsgwngfdg3SH6TpDv0Ta1aOEkw", "did:w123:world#NjQ6Y_ZMj6IUK_XkgCDwtKHlNTUTVjEYOWZtxhp1n-E"], "keyAgreement": [{ "id": "did:w123:world#zC5iai1sL93gQxn8LKh1i42fTbpfar65dVx4NYznYfG3Y5", diff --git a/pkg/doc/did/schema.go b/pkg/doc/did/schema.go index 10a6c298c..ca7b0de90 100644 --- a/pkg/doc/did/schema.go +++ b/pkg/doc/did/schema.go @@ -44,6 +44,14 @@ const ( "id": { "type": "string" }, + "alsoKnownAs": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "uniqueItems": true + }, "publicKey": { "type": "array", "items": { @@ -219,6 +227,14 @@ const ( "id": { "type": "string" }, + "alsoKnownAs": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "uniqueItems": true + }, "publicKey": { "type": "array", "items": { @@ -406,6 +422,14 @@ const ( "id": { "type": "string" }, + "alsoKnownAs": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "uniqueItems": true + }, "publicKey": { "type": "array", "items": { diff --git a/pkg/doc/did/testdata/valid_doc.jsonld b/pkg/doc/did/testdata/valid_doc.jsonld index 26ca510b0..3b35c5dae 100644 --- a/pkg/doc/did/testdata/valid_doc.jsonld +++ b/pkg/doc/did/testdata/valid_doc.jsonld @@ -1,6 +1,9 @@ { "@context": ["https://www.w3.org/ns/did/v1"], "id": "did:example:21tDAKCERh95uGgKbJNHYp", + "alsoKnownAs": [ + "did:example:123" + ], "verificationMethod": [ { "id": "did:example:123456789abcdefghi#keys-1", diff --git a/pkg/doc/did/testdata/valid_doc_resolution.jsonld b/pkg/doc/did/testdata/valid_doc_resolution.jsonld index 20a0656a1..45e28491c 100644 --- a/pkg/doc/did/testdata/valid_doc_resolution.jsonld +++ b/pkg/doc/did/testdata/valid_doc_resolution.jsonld @@ -5,6 +5,9 @@ "https://w3id.org/did/v1" ], "id": "did:example:21tDAKCERh95uGgKbJNHYp", + "alsoKnownAs": [ + "did:example:123" + ], "verificationMethod": [ { "id": "did:example:123456789abcdefghi#keys-1", diff --git a/pkg/doc/did/testdata/valid_doc_v0.11.jsonld b/pkg/doc/did/testdata/valid_doc_v0.11.jsonld index e9b46214b..e3734001e 100644 --- a/pkg/doc/did/testdata/valid_doc_v0.11.jsonld +++ b/pkg/doc/did/testdata/valid_doc_v0.11.jsonld @@ -1,6 +1,9 @@ { "@context": ["https://w3id.org/did/v0.11"], "id": "did:example:21tDAKCERh95uGgKbJNHYp", + "alsoKnownAs": [ + "did:example:123" + ], "publicKey": [ { "id": "did:example:123456789abcdefghi#keys-1", diff --git a/pkg/doc/did/testdata/valid_doc_with_base.jsonld b/pkg/doc/did/testdata/valid_doc_with_base.jsonld index a2c73b344..bf6f8662c 100644 --- a/pkg/doc/did/testdata/valid_doc_with_base.jsonld +++ b/pkg/doc/did/testdata/valid_doc_with_base.jsonld @@ -2,6 +2,9 @@ "@context": ["https://www.w3.org/ns/did/v1", { "@base": "did:example:123456789abcdefghi"}], "id": "did:example:123456789abcdefghi", + "alsoKnownAs": [ + "did:example:123" + ], "verificationMethod": [ { "id": "#keys-1",