From c7c2ade0e1fc839f572fbfd760ab846f3467acca Mon Sep 17 00:00:00 2001 From: Stas Dmytryshyn Date: Tue, 13 Aug 2024 15:03:43 +0200 Subject: [PATCH] feat: refresh service (#58) * feat: refresh service * feat: drop id * feat: as url * feat: compare details * feat: check ld * feat: uset typedid * feat: with * fix: did * fix: tests * fix: refresh service --- go.mod | 2 +- go.sum | 4 +-- presexch/definition_test.go | 2 +- verifiable/common.go | 12 ++++--- verifiable/credential.go | 61 +++++++++++++++++++++++++++++++---- verifiable/credential_test.go | 14 ++++++-- 6 files changed, 77 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 00f3192..fcc77f3 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/tidwall/gjson v1.14.3 github.com/tidwall/sjson v1.1.4 github.com/trustbloc/bbs-signature-go v1.0.2 - github.com/trustbloc/did-go v1.2.1 + github.com/trustbloc/did-go v1.2.2-0.20240812150654-c7d31e666f96 github.com/trustbloc/kms-go v1.1.2 github.com/veraison/go-cose v1.1.1-0.20240126165338-2300d5c96dbd github.com/xeipuuv/gojsonschema v1.2.0 diff --git a/go.sum b/go.sum index b21312c..b87737b 100644 --- a/go.sum +++ b/go.sum @@ -132,8 +132,8 @@ github.com/tidwall/sjson v1.1.4 h1:bTSsPLdAYF5QNLSwYsKfBKKTnlGbIuhqL3CpRsjzGhg= github.com/tidwall/sjson v1.1.4/go.mod h1:wXpKXu8CtDjKAZ+3DrKY5ROCorDFahq8l0tey/Lx1fg= github.com/trustbloc/bbs-signature-go v1.0.2 h1:gepEsbLiZHv/vva9FKG5gF38mGtOIyGez7desZxiI1o= github.com/trustbloc/bbs-signature-go v1.0.2/go.mod h1:xYotcXHAbcE0TO+SteW0J6XI3geQaXq4wdnXR2k+XCU= -github.com/trustbloc/did-go v1.2.1 h1:SEOmPX+x2JlE6+jjjJp82yCCusoJ4/67zQ2ST6ytfSc= -github.com/trustbloc/did-go v1.2.1/go.mod h1:packTRoBoo8DrwOE7QKsI98xXS3Vf6ovUXYD4FUAcB4= +github.com/trustbloc/did-go v1.2.2-0.20240812150654-c7d31e666f96 h1:LpX6reFIcahgTxuDLrV9yro1gENtXQLv2NsneX5hWkc= +github.com/trustbloc/did-go v1.2.2-0.20240812150654-c7d31e666f96/go.mod h1:packTRoBoo8DrwOE7QKsI98xXS3Vf6ovUXYD4FUAcB4= github.com/trustbloc/kms-go v1.1.2 h1:nAlhDoHkSyX1eQFRz/sJsdgmJuNadyX7FJEy/9ROwys= github.com/trustbloc/kms-go v1.1.2/go.mod h1:OKOtsLbE6W5s4mpjWkvk8XEqcmt9vTgVmDNkHELpWO0= github.com/veraison/go-cose v1.1.1-0.20240126165338-2300d5c96dbd h1:QhdCHSW1/oosJbzBTEYLU6xcKxXbQzzqFnhCtW2UWbA= diff --git a/presexch/definition_test.go b/presexch/definition_test.go index 910dd22..fb7acf3 100644 --- a/presexch/definition_test.go +++ b/presexch/definition_test.go @@ -2613,7 +2613,7 @@ type credentialProto struct { Schemas []verifiable.TypedID Evidence verifiable.Evidence TermsOfUse []verifiable.TypedID - RefreshService []verifiable.TypedID + RefreshService *verifiable.TypedID SDJWTHashAlg *crypto.Hash CustomFields verifiable.CustomFields diff --git a/verifiable/common.go b/verifiable/common.go index a6c5ee1..dba9ab4 100644 --- a/verifiable/common.go +++ b/verifiable/common.go @@ -117,9 +117,10 @@ func (ja JWSAlgorithm) Name() (string, error) { } type jsonldCredentialOpts struct { - jsonldDocumentLoader ld.DocumentLoader - externalContext []string - jsonldOnlyValidRDF bool + jsonldDocumentLoader ld.DocumentLoader + externalContext []string + jsonldOnlyValidRDF bool + jsonldIncludeDetailedStructureDiffOnError bool } // Proof defines embedded proof of Verifiable Credential. @@ -130,8 +131,9 @@ type Proof map[string]interface{} type CustomFields map[string]interface{} const ( - jsonFldTypedIDID = "id" - jsonFldTypedIDType = "type" + jsonFldTypedIDID = "id" + jsonFldTypedIDType = "type" + jsonFldTypedURLType = "url" ) // TypedID defines a flexible structure with id and name fields and arbitrary extra fields diff --git a/verifiable/credential.go b/verifiable/credential.go index 839e86a..3677fba 100644 --- a/verifiable/credential.go +++ b/verifiable/credential.go @@ -497,7 +497,7 @@ type CredentialContents struct { Schemas []TypedID Evidence Evidence TermsOfUse []TypedID - RefreshService []TypedID + RefreshService *TypedID SDJWTHashAlg *crypto.Hash } @@ -859,6 +859,13 @@ func WithJSONLDOnlyValidRDF() CredentialOpt { } } +// WithJSONLDIncludeDetailedStructureDiffOnError indicates the need to include detailed structure diff. +func WithJSONLDIncludeDetailedStructureDiffOnError() CredentialOpt { + return func(opts *credentialOpts) { + opts.jsonldIncludeDetailedStructureDiffOnError = true + } +} + // parseIssuer parses raw issuer. // // Issuer can be defined by: @@ -1021,7 +1028,7 @@ func ParseCredential(vcData []byte, opts ...CredentialOpt) (*Credential, error) vc, err = parser.Parse(vcData, vcOpts) if err != nil { - if err.Error() == jsonLDStructureErrStr { + if strings.HasPrefix(err.Error(), jsonLDStructureErrStr) { return nil, err } @@ -1153,11 +1160,21 @@ func validateBaseContextWithExtendedValidation(vcJSON JSONObject, vcc *Credentia func validateJSONLD(vcJSON JSONObject, vcOpts *credentialOpts) error { // TODO: docjsonld.ValidateJSONLDMap has bug that it modify contexts of input vcJSON. Fix in did-go - return docjsonld.ValidateJSONLDMap(jsonutil.ShallowCopyObj(vcJSON), + validateOpts := []docjsonld.ValidateOpts{ docjsonld.WithDocumentLoader(vcOpts.jsonldCredentialOpts.jsonldDocumentLoader), docjsonld.WithExternalContext(vcOpts.jsonldCredentialOpts.externalContext), docjsonld.WithStrictValidation(vcOpts.strictValidation), docjsonld.WithStrictContextURIPosition(baseContext), + } + + if vcOpts.jsonldIncludeDetailedStructureDiffOnError { + validateOpts = append(validateOpts, + docjsonld.WithJSONLDIncludeDetailedStructureDiffOnError(), + ) + } + + return docjsonld.ValidateJSONLDMap(jsonutil.ShallowCopyObj(vcJSON), + validateOpts..., ) } @@ -1197,7 +1214,7 @@ func parseCredentialContents(raw JSONObject, isSDJWT bool) (*CredentialContents, return nil, fmt.Errorf("fill credential terms of use from raw: %w", err) } - refreshService, err := parseTypedID(raw[jsonFldRefreshService]) + refreshService, err := parseRefreshService(raw[jsonFldRefreshService]) if err != nil { return nil, fmt.Errorf("fill credential refresh service from raw: %w", err) } @@ -1250,6 +1267,19 @@ func parseCredentialContents(raw JSONObject, isSDJWT bool) (*CredentialContents, }, nil } +func parseRefreshService(typeIDRaw interface{}) (*TypedID, error) { + typed, err := parseTypedID(typeIDRaw) + if err != nil { + return nil, fmt.Errorf("parse refresh service: %w", err) + } + + if len(typed) == 0 { + return nil, nil + } + + return &typed[0], nil +} + func parseTypedID(typeIDRaw interface{}) ([]TypedID, error) { if typeIDRaw == nil { return nil, nil @@ -1853,8 +1883,8 @@ func serializeCredentialContents(vcc *CredentialContents, proofs []Proof) (JSONO vcJSON[jsonFldEvidence] = vcc.Evidence } - if len(vcc.RefreshService) > 0 { - vcJSON[jsonFldRefreshService] = typedIDsToRaw(vcc.RefreshService) + if vcc.RefreshService != nil { + vcJSON[jsonFldRefreshService] = serializeTypedIDObj(*vcc.RefreshService) } if len(vcc.TermsOfUse) > 0 { @@ -2072,6 +2102,25 @@ func (vc *Credential) WithModifiedStatus(status *TypedID) *Credential { } } +// WithModifiedRefreshService creates new credential with modified status and without proofs as they become invalid. +func (vc *Credential) WithModifiedRefreshService(refreshService *TypedID) *Credential { + newCredJSON := copyCredentialJSONWithoutProofs(vc.credentialJSON) + newContents := vc.Contents() + + newContents.RefreshService = refreshService + + if refreshService != nil { + newCredJSON[jsonFldRefreshService] = serializeTypedIDObj(*refreshService) + } else { + delete(newCredJSON, jsonFldRefreshService) + } + + return &Credential{ + credentialJSON: newCredJSON, + credentialContents: newContents, + } +} + // WithModifiedIssuer creates new credential with modified issuer and without proofs as they become invalid. func (vc *Credential) WithModifiedIssuer(issuer *Issuer) *Credential { newCredJSON := copyCredentialJSONWithoutProofs(vc.credentialJSON) diff --git a/verifiable/credential_test.go b/verifiable/credential_test.go index 334a54b..4e8585b 100644 --- a/verifiable/credential_test.go +++ b/verifiable/credential_test.go @@ -136,8 +136,8 @@ func TestParseCredential(t *testing.T) { // check refresh service require.NotNil(t, vcc.RefreshService) - require.Equal(t, "https://example.edu/refresh/3732", vcc.RefreshService[0].ID) - require.Equal(t, "ManualRefreshService2018", vcc.RefreshService[0].Type) + require.Equal(t, "https://example.edu/refresh/3732", vcc.RefreshService.ID) + require.Equal(t, "ManualRefreshService2018", vcc.RefreshService.Type) require.NotNil(t, vcc.Evidence) @@ -2296,6 +2296,10 @@ func TestCredential_WithModified(t *testing.T) { ID: "newID", Type: "newType", }). + WithModifiedRefreshService(&TypedID{ + ID: "12345", + Type: "SomeRefreshService", + }). WithModifiedExpired(afgotime.NewTime(expired)). WithModifiedIssued(afgotime.NewTime(now)) @@ -2307,13 +2311,16 @@ func TestCredential_WithModified(t *testing.T) { require.Equal(t, "newType", cred.Contents().Status.Type) require.EqualValues(t, now, cred.Contents().Issued.Time) require.EqualValues(t, expired, cred.Contents().Expired.Time) + require.EqualValues(t, "12345", cred.Contents().RefreshService.ID) + require.EqualValues(t, "SomeRefreshService", cred.Contents().RefreshService.Type) cred = cred. WithModifiedID(""). WithModifiedSubject(nil). WithModifiedIssuer(nil). WithModifiedContext(nil). - WithModifiedStatus(nil) + WithModifiedStatus(nil). + WithModifiedRefreshService(nil) require.Empty(t, cred.Contents().ID) require.Empty(t, cred.Contents().Subject) @@ -2328,4 +2335,5 @@ func TestCredential_WithModified(t *testing.T) { require.NotContains(t, raw, jsonFldIssuer) require.NotContains(t, raw, jsonFldContext) require.NotContains(t, raw, jsonFldStatus) + require.NotContains(t, raw, jsonFldRefreshService) }