From c8abcf3201ff050688154aae515a6bff6e1ea443 Mon Sep 17 00:00:00 2001 From: reinkrul Date: Wed, 25 Oct 2023 14:59:48 +0200 Subject: [PATCH] PEX: Validate Presentation Definitions and Submissions using JSON schema (#2556) --- auth/api/iam/openid4vp_test.go | 2 +- .../test/presentation_definition_mapping.json | 7 +- go.mod | 2 + go.sum | 2 + vcr/pe/schema/README.md | 14 +- vcr/pe/schema/gen/README.md | 11 + vcr/pe/schema/{ => gen}/go.mod | 2 +- vcr/pe/schema/{ => gen}/go.sum | 0 vcr/pe/schema/{ => gen}/main.go | 2 +- vcr/pe/schema/v2/input-descriptor.json | 220 +++++++++++ vcr/pe/schema/v2/json-schema-draft-07.json | 172 +++++++++ ...-definition-claim-format-designations.json | 30 ++ .../v2/presentation-definition-envelope.json | 353 ++++++++++++++++++ vcr/pe/schema/v2/presentation-definition.json | 345 +++++++++++++++++ ...-submission-claim-format-designations.json | 11 + vcr/pe/schema/v2/presentation-submission.json | 40 ++ vcr/pe/schema/v2/schema.go | 104 ++++++ vcr/pe/schema/v2/schema_test.go | 40 ++ vcr/pe/schema/v2/submission-requirement.json | 50 +++ vcr/pe/schema/v2/submission-requirements.json | 102 +++++ vcr/pe/store.go | 26 +- vcr/pe/store_test.go | 8 + vcr/pe/submission.go | 56 +++ vcr/pe/submission_test.go | 37 ++ vcr/pe/test/definition_mapping.json | 7 +- vcr/pe/test/invalid_definition_mapping.json | 5 + vcr/pe/types.go | 17 - 27 files changed, 1627 insertions(+), 38 deletions(-) create mode 100644 vcr/pe/schema/gen/README.md rename vcr/pe/schema/{ => gen}/go.mod (60%) rename vcr/pe/schema/{ => gen}/go.sum (100%) rename vcr/pe/schema/{ => gen}/main.go (96%) create mode 100644 vcr/pe/schema/v2/input-descriptor.json create mode 100644 vcr/pe/schema/v2/json-schema-draft-07.json create mode 100644 vcr/pe/schema/v2/presentation-definition-claim-format-designations.json create mode 100644 vcr/pe/schema/v2/presentation-definition-envelope.json create mode 100644 vcr/pe/schema/v2/presentation-definition.json create mode 100644 vcr/pe/schema/v2/presentation-submission-claim-format-designations.json create mode 100644 vcr/pe/schema/v2/presentation-submission.json create mode 100644 vcr/pe/schema/v2/schema.go create mode 100644 vcr/pe/schema/v2/schema_test.go create mode 100644 vcr/pe/schema/v2/submission-requirement.json create mode 100644 vcr/pe/schema/v2/submission-requirements.json create mode 100644 vcr/pe/submission.go create mode 100644 vcr/pe/submission_test.go create mode 100644 vcr/pe/test/invalid_definition_mapping.json diff --git a/auth/api/iam/openid4vp_test.go b/auth/api/iam/openid4vp_test.go index 22920d4a92..9da7f5b192 100644 --- a/auth/api/iam/openid4vp_test.go +++ b/auth/api/iam/openid4vp_test.go @@ -93,7 +93,7 @@ func TestWrapper_handlePresentationRequest(t *testing.T) { t.Run("with scope", func(t *testing.T) { ctrl := gomock.NewController(t) peStore := &pe.DefinitionResolver{} - _ = peStore.LoadFromFile("test/presentation_definition_mapping.json") + require.NoError(t, peStore.LoadFromFile("test/presentation_definition_mapping.json")) mockVDR := vdr.NewMockVDR(ctrl) mockVCR := vcr.NewMockVCR(ctrl) mockWallet := holder.NewMockWallet(ctrl) diff --git a/auth/api/iam/test/presentation_definition_mapping.json b/auth/api/iam/test/presentation_definition_mapping.json index 4b64377e17..75e79eca18 100644 --- a/auth/api/iam/test/presentation_definition_mapping.json +++ b/auth/api/iam/test/presentation_definition_mapping.json @@ -1 +1,6 @@ -{"eOverdracht-overdrachtsbericht":{}} \ No newline at end of file +{ + "eOverdracht-overdrachtsbericht": { + "id": "eOverdracht", + "input_descriptors": [] + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index d39fb8bfba..64d8932617 100644 --- a/go.mod +++ b/go.mod @@ -51,6 +51,8 @@ require ( schneider.vip/problem v1.8.1 ) +require github.com/santhosh-tekuri/jsonschema v1.2.4 + require ( github.com/PaesslerAG/gval v1.2.2 // indirect github.com/alexandrevicenzi/go-sse v1.6.0 // indirect diff --git a/go.sum b/go.sum index 37843740c4..72a174816e 100644 --- a/go.sum +++ b/go.sum @@ -531,6 +531,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/santhosh-tekuri/jsonschema v1.2.4 h1:hNhW8e7t+H1vgY+1QeEQpveR6D4+OwKPXCfD2aieJis= +github.com/santhosh-tekuri/jsonschema v1.2.4/go.mod h1:TEAUOeZSmIxTTuHatJzrvARHiuO9LYd+cIxzgEHCQI4= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shengdoushi/base58 v1.0.0 h1:tGe4o6TmdXFJWoI31VoSWvuaKxf0Px3gqa3sUWhAxBs= github.com/shengdoushi/base58 v1.0.0/go.mod h1:m5uIILfzcKMw6238iWAhP4l3s5+uXyF3+bJKUNhAL9I= diff --git a/vcr/pe/schema/README.md b/vcr/pe/schema/README.md index fac22a7041..bf6b681e8b 100644 --- a/vcr/pe/schema/README.md +++ b/vcr/pe/schema/README.md @@ -1,11 +1,3 @@ -# generate structs from JSON schema - -From this directory, run: - -```shell -go run . -``` - -It'll generate `generated.go` within the `pe` package. -The generated code is not really useful, but it could serve as a guide for the types that are expected by the API. -The output of `generated.go` is copied to `types.go` \ No newline at end of file +Schemas files were taken from: +- https://github.com/decentralized-identity/presentation-exchange/tree/main/schemas +- https://github.com/decentralized-identity/claim-format-registry/tree/main/schemas \ No newline at end of file diff --git a/vcr/pe/schema/gen/README.md b/vcr/pe/schema/gen/README.md new file mode 100644 index 0000000000..fac22a7041 --- /dev/null +++ b/vcr/pe/schema/gen/README.md @@ -0,0 +1,11 @@ +# generate structs from JSON schema + +From this directory, run: + +```shell +go run . +``` + +It'll generate `generated.go` within the `pe` package. +The generated code is not really useful, but it could serve as a guide for the types that are expected by the API. +The output of `generated.go` is copied to `types.go` \ No newline at end of file diff --git a/vcr/pe/schema/go.mod b/vcr/pe/schema/gen/go.mod similarity index 60% rename from vcr/pe/schema/go.mod rename to vcr/pe/schema/gen/go.mod index db1192449c..b872a8ae8e 100644 --- a/vcr/pe/schema/go.mod +++ b/vcr/pe/schema/gen/go.mod @@ -1,4 +1,4 @@ -module github.com/nuts-foundation/nuts-node/vcr/pe/schema +module github.com/nuts-foundation/nuts-node/vcr/pe/gen/schema go 1.21 diff --git a/vcr/pe/schema/go.sum b/vcr/pe/schema/gen/go.sum similarity index 100% rename from vcr/pe/schema/go.sum rename to vcr/pe/schema/gen/go.sum diff --git a/vcr/pe/schema/main.go b/vcr/pe/schema/gen/main.go similarity index 96% rename from vcr/pe/schema/main.go rename to vcr/pe/schema/gen/main.go index 2eb767ecd4..7b3e4f0faf 100644 --- a/vcr/pe/schema/main.go +++ b/vcr/pe/schema/gen/main.go @@ -44,7 +44,7 @@ func main() { os.Exit(1) } - f, err := os.OpenFile("../generated.go", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) + f, err := os.OpenFile("generated.go", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) if err != nil { fmt.Fprintln(os.Stderr, "Error opening output file: ", err) diff --git a/vcr/pe/schema/v2/input-descriptor.json b/vcr/pe/schema/v2/input-descriptor.json new file mode 100644 index 0000000000..2112e900f8 --- /dev/null +++ b/vcr/pe/schema/v2/input-descriptor.json @@ -0,0 +1,220 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Input Descriptor", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id" + ] +} diff --git a/vcr/pe/schema/v2/json-schema-draft-07.json b/vcr/pe/schema/v2/json-schema-draft-07.json new file mode 100644 index 0000000000..fb92c7f756 --- /dev/null +++ b/vcr/pe/schema/v2/json-schema-draft-07.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://json-schema.org/draft-07/schema#", + "title": "Core schema meta-schema", + "definitions": { + "schemaArray": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#" } + }, + "nonNegativeInteger": { + "type": "integer", + "minimum": 0 + }, + "nonNegativeIntegerDefault0": { + "allOf": [ + { "$ref": "#/definitions/nonNegativeInteger" }, + { "default": 0 } + ] + }, + "simpleTypes": { + "enum": [ + "array", + "boolean", + "integer", + "null", + "number", + "object", + "string" + ] + }, + "stringArray": { + "type": "array", + "items": { "type": "string" }, + "uniqueItems": true, + "default": [] + } + }, + "type": ["object", "boolean"], + "properties": { + "$id": { + "type": "string", + "format": "uri-reference" + }, + "$schema": { + "type": "string", + "format": "uri" + }, + "$ref": { + "type": "string", + "format": "uri-reference" + }, + "$comment": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "default": true, + "readOnly": { + "type": "boolean", + "default": false + }, + "writeOnly": { + "type": "boolean", + "default": false + }, + "examples": { + "type": "array", + "items": true + }, + "multipleOf": { + "type": "number", + "exclusiveMinimum": 0 + }, + "maximum": { + "type": "number" + }, + "exclusiveMaximum": { + "type": "number" + }, + "minimum": { + "type": "number" + }, + "exclusiveMinimum": { + "type": "number" + }, + "maxLength": { "$ref": "#/definitions/nonNegativeInteger" }, + "minLength": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "pattern": { + "type": "string", + "format": "regex" + }, + "additionalItems": { "$ref": "#" }, + "items": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/schemaArray" } + ], + "default": true + }, + "maxItems": { "$ref": "#/definitions/nonNegativeInteger" }, + "minItems": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "uniqueItems": { + "type": "boolean", + "default": false + }, + "contains": { "$ref": "#" }, + "maxProperties": { "$ref": "#/definitions/nonNegativeInteger" }, + "minProperties": { "$ref": "#/definitions/nonNegativeIntegerDefault0" }, + "required": { "$ref": "#/definitions/stringArray" }, + "additionalProperties": { "$ref": "#" }, + "definitions": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "properties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "default": {} + }, + "patternProperties": { + "type": "object", + "additionalProperties": { "$ref": "#" }, + "propertyNames": { "format": "regex" }, + "default": {} + }, + "dependencies": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { "$ref": "#" }, + { "$ref": "#/definitions/stringArray" } + ] + } + }, + "propertyNames": { "$ref": "#" }, + "const": true, + "enum": { + "type": "array", + "items": true, + "minItems": 1, + "uniqueItems": true + }, + "type": { + "anyOf": [ + { "$ref": "#/definitions/simpleTypes" }, + { + "type": "array", + "items": { "$ref": "#/definitions/simpleTypes" }, + "minItems": 1, + "uniqueItems": true + } + ] + }, + "format": { "type": "string" }, + "contentMediaType": { "type": "string" }, + "contentEncoding": { "type": "string" }, + "if": { "$ref": "#" }, + "then": { "$ref": "#" }, + "else": { "$ref": "#" }, + "allOf": { "$ref": "#/definitions/schemaArray" }, + "anyOf": { "$ref": "#/definitions/schemaArray" }, + "oneOf": { "$ref": "#/definitions/schemaArray" }, + "not": { "$ref": "#" } + }, + "default": true +} diff --git a/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json b/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json new file mode 100644 index 0000000000..d142274c92 --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition-claim-format-designations.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition Claim Format Designations", + "type": "object", + "additionalProperties": false, + "patternProperties": { + "^jwt$|^jwt_vc$|^jwt_vp$": { + "type": "object", + "additionalProperties": false, + "properties": { + "alg": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + } + }, + "^ldp_vc$|^ldp_vp$|^ldp$": { + "type": "object", + "additionalProperties": false, + "properties": { + "proof_type": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + } + } + } + } +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-definition-envelope.json b/vcr/pe/schema/v2/presentation-definition-envelope.json new file mode 100644 index 0000000000..872a1e3966 --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition-envelope.json @@ -0,0 +1,353 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition Envelope", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "name": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + }, + "input_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id", + "constraints" + ] + }, + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + }, + "presentation_definition": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json#" + }, + "frame": { + "type": "object", + "additionalProperties": true + }, + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirement" + } + }, + "input_descriptors": { + "type": "array", + "items": { + "$ref": "#/definitions/input_descriptor" + } + } + }, + "required": [ + "id", + "input_descriptors" + ], + "additionalProperties": false + } + }, + "type": "object", + "properties": { + "presentation_definition": { + "$ref": "#/definitions/presentation_definition" + } + } +} diff --git a/vcr/pe/schema/v2/presentation-definition.json b/vcr/pe/schema/v2/presentation-definition.json new file mode 100644 index 0000000000..7ba8262b5c --- /dev/null +++ b/vcr/pe/schema/v2/presentation-definition.json @@ -0,0 +1,345 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Definition", + "definitions": { + "status_directive": { + "type": "object", + "additionalProperties": false, + "properties": { + "directive": { + "type": "string", + "enum": [ + "required", + "allowed", + "disallowed" + ] + }, + "type": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } + }, + "field": { + "type": "object", + "oneOf": [ + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "name": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + } + }, + "required": [ + "path" + ], + "additionalProperties": false + }, + { + "properties": { + "id": { + "type": "string" + }, + "optional": { + "type": "boolean" + }, + "path": { + "type": "array", + "items": { + "type": "string" + } + }, + "purpose": { + "type": "string" + }, + "intent_to_retain": { + "type": "boolean" + }, + "filter": { + "$ref": "http://json-schema.org/draft-07/schema#" + }, + "name": { + "type": "string" + }, + "predicate": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "path", + "filter", + "predicate" + ], + "additionalProperties": false + } + ] + }, + "input_descriptor": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "group": { + "type": "array", + "items": { + "type": "string" + } + }, + "constraints": { + "type": "object", + "additionalProperties": false, + "properties": { + "limit_disclosure": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "statuses": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { + "$ref": "#/definitions/status_directive" + }, + "suspended": { + "$ref": "#/definitions/status_directive" + }, + "revoked": { + "$ref": "#/definitions/status_directive" + } + } + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/definitions/field" + } + }, + "subject_is_issuer": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + }, + "is_holder": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + }, + "same_subject": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "field_id": { + "type": "array", + "items": { + "type": "string" + } + }, + "directive": { + "type": "string", + "enum": [ + "required", + "preferred" + ] + } + }, + "required": [ + "field_id", + "directive" + ] + } + } + } + } + }, + "required": [ + "id", + "constraints" + ] + }, + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" + }, + "frame": { + "type": "object", + "additionalProperties": true + }, + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirement" + } + }, + "input_descriptors": { + "type": "array", + "items": { + "$ref": "#/definitions/input_descriptor" + } + } + }, + "required": [ + "id", + "input_descriptors" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json b/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json new file mode 100644 index 0000000000..929f5360fc --- /dev/null +++ b/vcr/pe/schema/v2/presentation-submission-claim-format-designations.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission Claim Format Designations", + "type": "object", + "definitions": { + "format": { + "type": "string", + "enum": ["jwt", "jwt_vc", "jwt_vp", "ldp", "ldp_vc", "ldp_vp"] + } + } +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/presentation-submission.json b/vcr/pe/schema/v2/presentation-submission.json new file mode 100644 index 0000000000..a97275528f --- /dev/null +++ b/vcr/pe/schema/v2/presentation-submission.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission", + "type": "object", + "properties": { + "presentation_submission": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "definition_id": { "type": "string" }, + "descriptor_map": { + "type": "array", + "items": { "$ref": "#/definitions/descriptor" } + } + }, + "required": ["id", "definition_id", "descriptor_map"], + "additionalProperties": false + } + }, + "definitions": { + "descriptor": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "path": { "type": "string" }, + "path_nested": { + "type": "object", + "$ref": "#/definitions/descriptor" + }, + "format": { + "$ref": "http://identity.foundation/claim-format-registry/schemas/presentation-submission-claim-format-designations.json#/definitions/format" + } + }, + "required": ["id", "path", "format"], + "additionalProperties": false + } + }, + "required": ["presentation_submission"], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/schema.go b/vcr/pe/schema/v2/schema.go new file mode 100644 index 0000000000..44de55fdd8 --- /dev/null +++ b/vcr/pe/schema/v2/schema.go @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package v2 implements v2.0.0 of the Presentation Exchange specification +package v2 + +import ( + "bytes" + "embed" + _ "embed" + "fmt" + "github.com/santhosh-tekuri/jsonschema" + "github.com/santhosh-tekuri/jsonschema/loader" + "io" + "io/fs" + "strings" +) + +const ( + inputDescriptor = "http://identity.foundation/presentation-exchange/schemas/input-descriptor.json" + presentationDefinitionEnvelope = "http://identity.foundation/presentation-exchange/schemas/presentation-definition-envelope.json" + presentationDefinition = "http://identity.foundation/presentation-exchange/schemas/presentation-definition.json" + presentationSubmission = "http://identity.foundation/presentation-exchange/schemas/presentation-submission.json" + submissionRequirement = "http://identity.foundation/presentation-exchange/schemas/submission-requirement.json" + submissionRequirements = "http://identity.foundation/presentation-exchange/schemas/submission-requirements.json" + presentationSubmissionClaimFormatDesignations = "http://identity.foundation/claim-format-registry/schemas/presentation-submission-claim-format-designations.json" + presentationDefinitionClaimFormatDesignations = "http://identity.foundation/claim-format-registry/schemas/presentation-definition-claim-format-designations.json" +) + +//go:embed *.json +var schemaFiles embed.FS + +// PresentationDefinition is the JSON schema for a presentation definition. +var PresentationDefinition *jsonschema.Schema + +// PresentationSubmission is the JSON schema for a presentation submission. +var PresentationSubmission *jsonschema.Schema + +func init() { + // By default, it loads from filesystem, but that sounds unsafe. + // Since register our schemas, we don't need to allow loading resources. + loader.Load = func(url string) (io.ReadCloser, error) { + return nil, fmt.Errorf("refusing to load unknown schema: %s", url) + } + compiler := jsonschema.NewCompiler() + compiler.Draft = jsonschema.Draft7 + if err := loadSchemas(schemaFiles, compiler); err != nil { + panic(err) + } + PresentationDefinition = compiler.MustCompile(presentationDefinition) + PresentationSubmission = compiler.MustCompile(presentationSubmission) +} + +func loadSchemas(reader fs.ReadFileFS, compiler *jsonschema.Compiler) error { + var resources = map[string]string{ + "http://json-schema.org/draft-07/schema": "json-schema-draft-07.json", + } + schemaURLs := []string{ + inputDescriptor, + presentationDefinitionEnvelope, + presentationDefinition, + presentationSubmission, + submissionRequirement, + submissionRequirements, + presentationSubmissionClaimFormatDesignations, + presentationDefinitionClaimFormatDesignations, + } + for _, schemaURL := range schemaURLs { + // Last part of schema URL matches the embedded file's name + parts := strings.Split(schemaURL, "/") + fileName := parts[len(parts)-1] + resources[schemaURL] = fileName + } + for schemaURL, fileName := range resources { + data, err := reader.ReadFile(fileName) + if err != nil { + return fmt.Errorf("error reading schema file %s: %w", fileName, err) + } + if err := compiler.AddResource(schemaURL, bytes.NewReader(data)); err != nil { + return fmt.Errorf("error compiling schema %s: %w", schemaURL, err) + } + } + return nil +} + +// Validate validates the given data against the given schema. +func Validate(data []byte, schema *jsonschema.Schema) error { + return schema.Validate(bytes.NewReader(data)) +} diff --git a/vcr/pe/schema/v2/schema_test.go b/vcr/pe/schema/v2/schema_test.go new file mode 100644 index 0000000000..fa58d5939f --- /dev/null +++ b/vcr/pe/schema/v2/schema_test.go @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package v2 + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSchemaLoading(t *testing.T) { + assert.NotNil(t, PresentationDefinition) +} + +func TestValidate(t *testing.T) { + t.Run("ok", func(t *testing.T) { + err := Validate([]byte(`{"id":"1", "input_descriptors": []}`), PresentationDefinition) + assert.NoError(t, err) + }) + t.Run("invalid", func(t *testing.T) { + err := Validate([]byte(`{}`), PresentationDefinition) + assert.ErrorContains(t, err, "doesn't validate") + assert.ErrorContains(t, err, "missing properties: \"id\"") + }) +} diff --git a/vcr/pe/schema/v2/submission-requirement.json b/vcr/pe/schema/v2/submission-requirement.json new file mode 100644 index 0000000000..9650087ef1 --- /dev/null +++ b/vcr/pe/schema/v2/submission-requirement.json @@ -0,0 +1,50 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Presentation Submission Requirement", + "definitions": { + "submission_requirement": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from": { "type": "string" } + }, + "required": ["rule", "from"], + "additionalProperties": false + }, + { + "properties": { + "name": { "type": "string" }, + "purpose": { "type": "string" }, + "rule": { + "type": "string", + "enum": ["all", "pick"] + }, + "count": { "type": "integer", "minimum": 1 }, + "min": { "type": "integer", "minimum": 0 }, + "max": { "type": "integer", "minimum": 0 }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirement" + } + } + }, + "required": ["rule", "from_nested"], + "additionalProperties": false + } + ] + } + }, + "$ref": "#/definitions/submission_requirement" +} \ No newline at end of file diff --git a/vcr/pe/schema/v2/submission-requirements.json b/vcr/pe/schema/v2/submission-requirements.json new file mode 100644 index 0000000000..c04701eb81 --- /dev/null +++ b/vcr/pe/schema/v2/submission-requirements.json @@ -0,0 +1,102 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Submission Requirements", + "definitions": { + "submission_requirements": { + "type": "object", + "oneOf": [ + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + } + }, + "required": [ + "rule", + "from" + ], + "additionalProperties": false + }, + { + "properties": { + "name": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "rule": { + "type": "string", + "enum": [ + "all", + "pick" + ] + }, + "count": { + "type": "integer", + "minimum": 1 + }, + "min": { + "type": "integer", + "minimum": 0 + }, + "max": { + "type": "integer", + "minimum": 0 + }, + "from_nested": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/submission_requirements" + } + } + }, + "required": [ + "rule", + "from_nested" + ], + "additionalProperties": false + } + ] + } + }, + "type": "object", + "properties": { + "submission_requirements": { + "type": "array", + "items": { + "$ref": "#/definitions/submission_requirements" + } + } + }, + "required": [ + "submission_requirements" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/vcr/pe/store.go b/vcr/pe/store.go index 3c6858687e..11343cd326 100644 --- a/vcr/pe/store.go +++ b/vcr/pe/store.go @@ -20,6 +20,8 @@ package pe import ( "encoding/json" + "fmt" + v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" "io" "os" ) @@ -28,7 +30,7 @@ import ( // It loads a file with the mapping from oauth scope to presentation definition type DefinitionResolver struct { // mapping holds the oauth scope to presentation definition mapping - mapping map[string]PresentationDefinition + mapping map[string]validatingPresentationDefinition } // LoadFromFile loads the mapping from the given file @@ -45,8 +47,13 @@ func (s *DefinitionResolver) LoadFromFile(filename string) error { } // unmarshal the bytes into the mapping - s.mapping = make(map[string]PresentationDefinition) - return json.Unmarshal(bytes, &s.mapping) + result := make(map[string]validatingPresentationDefinition) + err = json.Unmarshal(bytes, &result) + if err != nil { + return fmt.Errorf("failed to unmarshal Presentation Exchange mapping file %s: %w", filename, err) + } + s.mapping = result + return nil } // ByScope returns the presentation definition for the given scope. @@ -56,5 +63,16 @@ func (s *DefinitionResolver) ByScope(scope string) *PresentationDefinition { if !ok { return nil } - return &mapping + result := PresentationDefinition(mapping) + return &result +} + +// validatingPresentationDefinition is an alias for PresentationDefinition that validates the JSON on unmarshal. +type validatingPresentationDefinition PresentationDefinition + +func (v *validatingPresentationDefinition) UnmarshalJSON(data []byte) error { + if err := v2.Validate(data, v2.PresentationDefinition); err != nil { + return err + } + return json.Unmarshal(data, (*PresentationDefinition)(v)) } diff --git a/vcr/pe/store_test.go b/vcr/pe/store_test.go index 97e77bdf3f..ebb9ae40cb 100644 --- a/vcr/pe/store_test.go +++ b/vcr/pe/store_test.go @@ -43,6 +43,14 @@ func TestStore_LoadFromFile(t *testing.T) { assert.Error(t, err) }) + + t.Run("returns an error if a presentation definition is invalid", func(t *testing.T) { + store := DefinitionResolver{} + + err := store.LoadFromFile("test/invalid_definition_mapping.json") + + assert.ErrorContains(t, err, "missing properties: \"input_descriptors\"") + }) } func TestStore_ByScope(t *testing.T) { diff --git a/vcr/pe/submission.go b/vcr/pe/submission.go new file mode 100644 index 0000000000..736c70c96c --- /dev/null +++ b/vcr/pe/submission.go @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package pe + +import ( + "encoding/json" + v2 "github.com/nuts-foundation/nuts-node/vcr/pe/schema/v2" +) + +// PresentationSubmission describes how the VCs in the VP match the input descriptors in the PD +type PresentationSubmission struct { + // Id is the id of the presentation submission, which is a UUID + Id string `json:"id"` + // DefinitionId is the id of the presentation definition that this submission is for + DefinitionId string `json:"definition_id"` + // DescriptorMap is a list of mappings from input descriptors to VCs + DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map"` +} + +// InputDescriptorMappingObject +type InputDescriptorMappingObject struct { + Id string `json:"id"` + Path string `json:"path"` + Format string `json:"format"` +} + +// ParsePresentationSubmission validates the given JSON and parses it into a PresentationSubmission. +// It returns an error if the JSON is invalid or doesn't match the JSON schema for a PresentationSubmission. +func ParsePresentationSubmission(raw []byte) (*PresentationSubmission, error) { + enveloped := `{"presentation_submission":` + string(raw) + `}` + if err := v2.Validate([]byte(enveloped), v2.PresentationSubmission); err != nil { + return nil, err + } + var result PresentationSubmission + err := json.Unmarshal(raw, &result) + if err != nil { + return nil, err + } + return &result, nil +} diff --git a/vcr/pe/submission_test.go b/vcr/pe/submission_test.go new file mode 100644 index 0000000000..1cd9aefb21 --- /dev/null +++ b/vcr/pe/submission_test.go @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2023 Nuts community + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package pe + +import ( + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestParsePresentationSubmission(t *testing.T) { + t.Run("ok", func(t *testing.T) { + submission, err := ParsePresentationSubmission([]byte(`{"id": "1", "definition_id":"1", "descriptor_map": []}`)) + require.NoError(t, err) + assert.Equal(t, "1", submission.Id) + }) + t.Run("missing id", func(t *testing.T) { + _, err := ParsePresentationSubmission([]byte(`{"definition_id":"1", "descriptor_map": []}`)) + assert.ErrorContains(t, err, `missing properties: "id"`) + }) +} diff --git a/vcr/pe/test/definition_mapping.json b/vcr/pe/test/definition_mapping.json index 284a1253e7..b543faa577 100644 --- a/vcr/pe/test/definition_mapping.json +++ b/vcr/pe/test/definition_mapping.json @@ -1,13 +1,16 @@ { "eOverdracht-overdrachtsbericht": { - "ldp_vc": { - "proof_type": ["JsonWebSignature2020"] + "format": { + "ldp_vc": { + "proof_type": ["JsonWebSignature2020"] + } }, "id": "pd_any_care_organization", "name": "Care organization", "purpose": "Finding a care organization for authorizing access to medical metadata", "input_descriptors": [ { + "id": "id_nuts_care_organization_cred", "constraints": { "fields": [ { diff --git a/vcr/pe/test/invalid_definition_mapping.json b/vcr/pe/test/invalid_definition_mapping.json new file mode 100644 index 0000000000..cfceb8bed5 --- /dev/null +++ b/vcr/pe/test/invalid_definition_mapping.json @@ -0,0 +1,5 @@ +{ + "missing input_descriptors": { + "id": "pd_any_care_organization" + } +} diff --git a/vcr/pe/types.go b/vcr/pe/types.go index 9dea0b1a96..32f4d78a2f 100644 --- a/vcr/pe/types.go +++ b/vcr/pe/types.go @@ -22,23 +22,6 @@ package pe // PresentationDefinitionClaimFormatDesignations (replaces generated one) type PresentationDefinitionClaimFormatDesignations map[string]map[string][]string -// PresentationSubmission describes how the VCs in the VP match the input descriptors in the PD -type PresentationSubmission struct { - // Id is the id of the presentation submission, which is a UUID - Id string `json:"id"` - // DefinitionId is the id of the presentation definition that this submission is for - DefinitionId string `json:"definition_id"` - // DescriptorMap is a list of mappings from input descriptors to VCs - DescriptorMap []InputDescriptorMappingObject `json:"descriptor_map"` -} - -// InputDescriptorMappingObject -type InputDescriptorMappingObject struct { - Id string `json:"id"` - Path string `json:"path"` - Format string `json:"format"` -} - // Constraints type Constraints struct { Fields []Field `json:"fields,omitempty"`