diff --git a/credential/model.go b/credential/model.go index 6b5dba16..f2cb350a 100644 --- a/credential/model.go +++ b/credential/model.go @@ -68,8 +68,9 @@ func (cs CredentialSubject) GetJSONSchema() map[string]any { } type CredentialSchema struct { - ID string `json:"id" validate:"required"` - Type string `json:"type" validate:"required"` + ID string `json:"id" validate:"required"` + Type string `json:"type" validate:"required"` + DigestSRI string `json:"digestSRI,omitempty"` } type RefreshService struct { diff --git a/credential/schema/model.go b/credential/schema/model.go index fb64557a..5886a590 100644 --- a/credential/schema/model.go +++ b/credential/schema/model.go @@ -13,6 +13,11 @@ const ( JSONSchemaType VCJSONSchemaType = "JsonSchema" TypeProperty string = "type" + // Known CredentialSchema property values for JsonSchemaCredential + + JSONSchemaCredentialSchemaID = "https://www.w3.org/2022/credentials/v2/json-schema-credential-schema.json" + JSONSchemaCredentialDigestSRI = "sha384-S57yQDg1MTzF56Oi9DbSQ14u7jBy0RDdx0YbeV7shwhCS88G8SCXeFq82PafhCrW" + Draft202012 JSONSchemaVersion = "https://json-schema.org/draft/2020-12/schema" Draft201909 JSONSchemaVersion = "https://json-schema.org/draft/2019-09/schema" Draft7 JSONSchemaVersion = "https://json-schema.org/draft-07/schema#" diff --git a/credential/schema/testdata/jsonschemacredential-schema-1.json b/credential/schema/testdata/jsonschemacredential-schema-1.json index e4fe4cff..6d4f447d 100644 --- a/credential/schema/testdata/jsonschemacredential-schema-1.json +++ b/credential/schema/testdata/jsonschemacredential-schema-1.json @@ -7,6 +7,11 @@ "type": ["VerifiableCredential", "JsonSchemaCredential"], "issuer": "https://example.com/issuers/14", "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSchema": { + "id": "https://www.w3.org/2022/credentials/v2/json-schema-credential-schema.json", + "type": "JsonSchema", + "digestSRI": "sha384-S57yQDg1MTzF56Oi9DbSQ14u7jBy0RDdx0YbeV7shwhCS88G8SCXeFq82PafhCrW" + }, "credentialSubject": { "id": "https://example.com/schemas/email-credential-schema.json", "type": "JsonSchema", diff --git a/credential/schema/vcjsonschema.go b/credential/schema/vcjsonschema.go index d3cbc0bc..614754ce 100644 --- a/credential/schema/vcjsonschema.go +++ b/credential/schema/vcjsonschema.go @@ -48,23 +48,11 @@ func IsCredentialValidForJSONSchema(cred credential.VerifiableCredential, vcs VC s = JSONSchema(vcs) schemaID = s.ID() case JSONSchemaCredentialType: - var vc credential.VerifiableCredential - schemaString := vcs.String() - if err := json.Unmarshal([]byte(schemaString), &vc); err != nil { - return errors.Wrap(err, "unmarshalling schema") + var err error + s, schemaID, err = parseJSONSchemaCredential(vcs) + if err != nil { + return errors.Wrap(err, "parsing credential schema") } - schemaType, ok := vc.CredentialSubject[TypeProperty] - if !ok { - return errors.New("credential schema's credential subject does not contain a `type`") - } - if schemaType != JSONSchemaType.String() { - return fmt.Errorf("credential schema's credential subject type<%s> does not match schema type<%s>", schemaType, JSONSchemaType) - } - s = vc.CredentialSubject.GetJSONSchema() - if len(s) == 0 { - return errors.New("credential schema's credential subject does not contain a valid `jsonSchema`") - } - schemaID = vc.ID } // check the ID is a valid URI @@ -100,6 +88,42 @@ func IsCredentialValidForJSONSchema(cred credential.VerifiableCredential, vcs VC return nil } +// JsonSchemaCredential helper for IsCredentialValidForJSONSchema +func parseJSONSchemaCredential(vcs VCJSONSchema) (JSONSchema, string, error) { + var vc credential.VerifiableCredential + schemaString := vcs.String() + if err := json.Unmarshal([]byte(schemaString), &vc); err != nil { + return nil, "", errors.Wrap(err, "unmarshalling schema") + } + schemaType, ok := vc.CredentialSubject[TypeProperty] + if !ok { + return nil, "", errors.New("credential schema's credential subject does not contain a `type`") + } + if schemaType != JSONSchemaType.String() { + return nil, "", fmt.Errorf("credential schema's credential subject type<%s> does not match schema type<%s>", schemaType, JSONSchemaType) + } + if vc.CredentialSchema == nil { + return nil, "", errors.New("credential schema's credential subject does not contain a `credentialSchema`") + } + credSchema := vc.CredentialSchema + if credSchema.ID != JSONSchemaCredentialSchemaID { + return nil, "", fmt.Errorf("credential schema's credential schema id<%s> does not match known id<%s>", credSchema.ID, JSONSchemaCredentialSchemaID) + } + if credSchema.Type != JSONSchemaType.String() { + return nil, "", fmt.Errorf("credential schema's credential schema type<%s> does not match known type<%s>", credSchema.Type, JSONSchemaType) + } + if credSchema.DigestSRI != JSONSchemaCredentialDigestSRI { + return nil, "", fmt.Errorf("credential schema's credential schema digest sri<%s> does not match known sri<%s>", credSchema.DigestSRI, JSONSchemaCredentialDigestSRI) + } + + s := vc.CredentialSubject.GetJSONSchema() + if len(s) == 0 { + return nil, "", errors.New("credential schema's credential subject does not contain a valid `jsonSchema`") + } + schemaID := vc.ID + return s, schemaID, nil +} + // GetCredentialSchemaFromCredential returns the credential schema and type for a given credential given // a credential schema access, which is used to retrieve the schema func GetCredentialSchemaFromCredential(access VCJSONSchemaAccess, cred credential.VerifiableCredential) (VCJSONSchema, VCJSONSchemaType, error) { diff --git a/credential/schema/vcjsonschema_test.go b/credential/schema/vcjsonschema_test.go index 34a7551a..2061436d 100644 --- a/credential/schema/vcjsonschema_test.go +++ b/credential/schema/vcjsonschema_test.go @@ -254,6 +254,66 @@ func TestIsCredentialValidForJSONSchema_JsonSchemaCredential(t *testing.T) { }) }) + t.Run("The value of the credentialSchema property MUST always be set to [known json schema]", func(t *testing.T) { + t.Run("valid credentialSchema", func(t *testing.T) { + cred := getTestVCJSONSchemaCredential() + schema := getTestVCJSONSchemaSchema() + err := IsCredentialValidForJSONSchema(cred, schema, JSONSchemaCredentialType) + assert.NoError(t, err) + }) + + t.Run("credentialSchema wrong id", func(t *testing.T) { + cred := getTestVCJSONSchemaCredential() + schema := getTestVCJSONSchemaSchema() + schema["credentialSchema"] = map[string]any{ + "id": "bad", + "type": JSONSchemaType, + "digestSRI": JSONSchemaCredentialDigestSRI, + } + err := IsCredentialValidForJSONSchema(cred, schema, JSONSchemaCredentialType) + assert.Error(t, err) + assert.ErrorContains(t, err, "credential schema's credential schema id does not match known id") + }) + + t.Run("credentialSchema wrong type", func(t *testing.T) { + cred := getTestVCJSONSchemaCredential() + schema := getTestVCJSONSchemaSchema() + schema["credentialSchema"] = map[string]any{ + "id": JSONSchemaCredentialSchemaID, + "type": "NotJsonSchema", + "digestSRI": JSONSchemaCredentialDigestSRI, + } + err := IsCredentialValidForJSONSchema(cred, schema, JSONSchemaCredentialType) + assert.Error(t, err) + assert.ErrorContains(t, err, "credential schema's credential schema type does not match known type") + }) + + t.Run("credentialSchema wrong digestSRI", func(t *testing.T) { + cred := getTestVCJSONSchemaCredential() + schema := getTestVCJSONSchemaSchema() + schema["credentialSchema"] = map[string]any{ + "id": JSONSchemaCredentialSchemaID, + "type": JSONSchemaType, + "digestSRI": "bad", + } + err := IsCredentialValidForJSONSchema(cred, schema, JSONSchemaCredentialType) + assert.Error(t, err) + assert.ErrorContains(t, err, "credential schema's credential schema digest sri does not match known sri") + }) + + t.Run("credentialSchema missing digestSRI", func(t *testing.T) { + cred := getTestVCJSONSchemaCredential() + schema := getTestVCJSONSchemaSchema() + schema["credentialSchema"] = map[string]any{ + "id": JSONSchemaCredentialSchemaID, + "type": JSONSchemaType, + } + err := IsCredentialValidForJSONSchema(cred, schema, JSONSchemaCredentialType) + assert.Error(t, err) + assert.ErrorContains(t, err, "credential schema's credential schema digest sri<> does not match known sri") + }) + }) + t.Run("the $id property MUST be present and its value MUST represent a valid URI", func(t *testing.T) { t.Run("$id is a valid URI", func(t *testing.T) { cred := getTestVCJSONSchemaCredential() @@ -385,6 +445,11 @@ func getTestVCJSONSchemaSchema() VCJSONSchema { "required": []string{"credentialSubject"}, }, }, + "credentialSchema": map[string]any{ + "id": JSONSchemaCredentialSchemaID, + "type": "JsonSchema", + "digestSRI": JSONSchemaCredentialDigestSRI, + }, } }