diff --git a/README.rst b/README.rst index 2a0cd90d11..4cb460f377 100644 --- a/README.rst +++ b/README.rst @@ -176,74 +176,74 @@ The following options can be configured on the server: :widths: 20 30 50 :class: options-table - ================================= ============================================================================================================================================================================================================================================================================================================================================================================= ======================================================================================================================================================================================================================================== - Key Default Description - ================================= ============================================================================================================================================================================================================================================================================================================================================================================= ======================================================================================================================================================================================================================================== - configfile nuts.yaml Nuts config file - cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. - datadir ./data Directory where the node stores its files. - internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. - loggerformat text Log format (text, json) - strictmode false When set, insecure settings are forbidden. - verbosity info Log level (trace, debug, info, warn, error) - tls.certfile PEM file containing the certificate for the server (also used as client certificate). - tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. - tls.certkeyfile PEM file containing the private key of the server certificate. - tls.crl.maxvaliditydays 0 The number of days a CRL can be outdated, after that it will hard-fail. - tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. - tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. + ================================= ==================================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================== + Key Default Description + ================================= ==================================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================== + configfile nuts.yaml Nuts config file + cpuprofile When set, a CPU profile is written to the given path. Ignored when strictmode is set. + datadir ./data Directory where the node stores its files. + internalratelimiter true When set, expensive internal calls are rate-limited to protect the network. Always enabled in strict mode. + loggerformat text Log format (text, json) + strictmode false When set, insecure settings are forbidden. + verbosity info Log level (trace, debug, info, warn, error) + tls.certfile PEM file containing the certificate for the server (also used as client certificate). + tls.certheader Name of the HTTP header that will contain the client certificate when TLS is offloaded. + tls.certkeyfile PEM file containing the private key of the server certificate. + tls.offload Whether to enable TLS offloading for incoming connections. Enable by setting it to 'incoming'. If enabled 'tls.certheader' must be configured as well. + tls.truststorefile truststore.pem PEM file containing the trusted CA certificates for authenticating remote servers. + tls.crl.maxvaliditydays 0 The number of days a CRL can be outdated, after that it will hard-fail. **Auth** - auth.clockskew 5000 Allowed JWT Clock skew in milliseconds - auth.contractvalidators [irma,uzi,dummy] sets the different contract validators to use - auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client - auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. - auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. - auth.publicurl public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy. + auth.clockskew 5000 Allowed JWT Clock skew in milliseconds + auth.contractvalidators [irma,uzi,dummy] sets the different contract validators to use + auth.publicurl public URL which can be reached by a users IRMA client, this should include the scheme and domain: https://example.com. Additional paths should only be added if some sort of url-rewriting is done in a reverse-proxy. + auth.http.timeout 30 HTTP timeout (in seconds) used by the Auth API HTTP client + auth.irma.autoupdateschemas true set if you want automatically update the IRMA schemas every 60 minutes. + auth.irma.schememanager pbdf IRMA schemeManager to use for attributes. Can be either 'pbdf' or 'irma-demo'. **Crypto** - crypto.storage fs Storage to use, 'fs' for file system, vaultkv for Vault KV store, default: fs. - crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. - crypto.vault.pathprefix kv The Vault path prefix. default: kv. - crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 5s). - crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. + crypto.storage fs Storage to use, 'fs' for file system, vaultkv for Vault KV store, default: fs. + crypto.vault.address The Vault address. If set it overwrites the VAULT_ADDR env var. + crypto.vault.pathprefix kv The Vault path prefix. default: kv. + crypto.vault.timeout 5s Timeout of client calls to Vault, in Golang time.Duration string format (e.g. 5s). + crypto.vault.token The Vault token. If set it overwrites the VAULT_TOKEN env var. **Events** - events.nats.hostname localhost Hostname for the NATS server - events.nats.port 4222 Port where the NATS server listens on - events.nats.storagedir Directory where file-backed streams are stored in the NATS server - events.nats.timeout 30 Timeout for NATS server operations + events.nats.hostname localhost Hostname for the NATS server + events.nats.port 4222 Port where the NATS server listens on + events.nats.storagedir Directory where file-backed streams are stored in the NATS server + events.nats.timeout 30 Timeout for NATS server operations **HTTP** - http.default.address \:1323 Address and port the server will be listening to - http.default.auth.type Whether to enable authentication for the default interface, specify 'token' for bearer token authentication. - http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. - http.default.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). - http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + http.default.address \:1323 Address and port the server will be listening to + http.default.log metadata What to log about HTTP requests. Options are 'nothing', 'metadata' (log request method, URI, IP and response code), and 'metadata-and-body' (log the request and response body, in addition to the metadata). + http.default.tls Whether to enable TLS for the default interface, options are 'disabled', 'server', 'server-client'. Leaving it empty is synonymous to 'disabled', + http.default.auth.type Whether to enable authentication for the default interface, specify 'token' for bearer token authentication. + http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://nuts.nl/credentials/v2=assets/contexts/nuts-v2.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. - jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. + jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts-v1_1.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** - network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. - network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). - network.disablenodeauthentication false Disable node DID authentication using client certificate, causing all node DIDs to be accepted. Unsafe option, only intended for workshops/demo purposes so it's not allowed in strict-mode. Automatically enabled when TLS is disabled. - network.enablediscovery true Whether to enable automatic connecting to other nodes. - network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. - network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). - network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). - network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. - network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. - network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). - network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. + network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. + network.connectiontimeout 5000 Timeout before an outbound connection attempt times out (in milliseconds). + network.disablenodeauthentication false Disable node DID authentication using client certificate, causing all node DIDs to be accepted. Unsafe option, only intended for workshops/demo purposes so it's not allowed in strict-mode. Automatically enabled when TLS is disabled. + network.enablediscovery true Whether to enable automatic connecting to other nodes. + network.enabletls true Whether to enable TLS for gRPC connections, which can be disabled for demo/development purposes. It is NOT meant for TLS offloading (see 'tls.offload'). Disabling TLS is not allowed in strict-mode. + network.grpcaddr \:5555 Local address for gRPC to listen on. If empty the gRPC server won't be started and other nodes will not be able to connect to this node (outbound connections can still be made). + network.maxbackoff 24h0m0s Maximum between outbound connections attempts to unresponsive nodes (in Golang duration format, e.g. '1h', '30m'). + network.nodedid Specifies the DID of the organization that operates this node, typically a vendor for EPD software. It is used to identify the node on the network. If the DID document does not exist of is deactivated, the node will not start. + network.protocols [] Specifies the list of network protocols to enable on the server. They are specified by version (1, 2). If not set, all protocols are enabled. + network.v2.diagnosticsinterval 5000 Interval (in milliseconds) that specifies how often the node should broadcast its diagnostic information to other nodes (specify 0 to disable). + network.v2.gossipinterval 5000 Interval (in milliseconds) that specifies how often the node should gossip its new hashes to other nodes. **Storage** - storage.bbolt.backup.directory Target directory for BBolt database backups. - storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. - storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. - storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. - storage.redis.password Redis database password. If set, it overrides the username in the connection URL. - storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. - storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. - storage.redis.sentinel.password Password for authenticating to Redis Sentinels. - storage.redis.sentinel.username Username for authenticating to Redis Sentinels. - storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). - storage.redis.username Redis database username. If set, it overrides the username in the connection URL. - ================================= ============================================================================================================================================================================================================================================================================================================================================================================= ======================================================================================================================================================================================================================================== + storage.bbolt.backup.directory Target directory for BBolt database backups. + storage.bbolt.backup.interval 0s Interval, formatted as Golang duration (e.g. 10m, 1h) at which BBolt database backups will be performed. + storage.redis.address Redis database server address. This can be a simple 'host:port' or a Redis connection URL with scheme, auth and other options. + storage.redis.database Redis database name, which is used as prefix every key. Can be used to have multiple instances use the same Redis instance. + storage.redis.password Redis database password. If set, it overrides the username in the connection URL. + storage.redis.username Redis database username. If set, it overrides the username in the connection URL. + storage.redis.sentinel.master Name of the Redis Sentinel master. Setting this property enables Redis Sentinel. + storage.redis.sentinel.nodes [] Addresses of the Redis Sentinels to connect to initially. Setting this property enables Redis Sentinel. + storage.redis.sentinel.password Password for authenticating to Redis Sentinels. + storage.redis.sentinel.username Username for authenticating to Redis Sentinels. + storage.redis.tls.truststorefile PEM file containing the trusted CA certificate(s) for authenticating remote Redis servers. Can only be used when connecting over TLS (use 'rediss://' as scheme in address). + ================================= ==================================================================================================================================================================================================================================================================================================================== ======================================================================================================================================================================================================================================== This table is automatically generated using the configuration flags in the core and engines. When they're changed the options table must be regenerated using the Makefile: diff --git a/docs/pages/deployment/server_options.rst b/docs/pages/deployment/server_options.rst index 295123891a..e5a0981087 100755 --- a/docs/pages/deployment/server_options.rst +++ b/docs/pages/deployment/server_options.rst @@ -43,7 +43,7 @@ http.default.auth.type Whether to enable authentication for the default interface, specify 'token' for bearer token authentication. http.default.cors.origin [] When set, enables CORS from the specified origins on the default HTTP interface. **JSONLD** - jsonld.contexts.localmapping [https://schema.org=assets/contexts/schema-org-v13.ldjson,https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. + jsonld.contexts.localmapping [https://nuts.nl/credentials/v1=assets/contexts/nuts.ldjson,https://www.w3.org/2018/credentials/v1=assets/contexts/w3c-credentials-v1.ldjson,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json=assets/contexts/lds-jws2020-v1.ldjson,https://schema.org=assets/contexts/schema-org-v13.ldjson] This setting allows mapping external URLs to local files for e.g. preventing external dependencies. These mappings have precedence over those in remoteallowlist. jsonld.contexts.remoteallowlist [https://schema.org,https://www.w3.org/2018/credentials/v1,https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json] In strict mode, fetching external JSON-LD contexts is not allowed except for context-URLs listed here. **Network** network.bootstrapnodes [] List of bootstrap nodes (':') which the node initially connect to. diff --git a/docs/pages/technology/jsonld.rst b/docs/pages/technology/jsonld.rst index 815acdb4aa..ce821b0b76 100644 --- a/docs/pages/technology/jsonld.rst +++ b/docs/pages/technology/jsonld.rst @@ -12,4 +12,4 @@ Within Verifiable Credentials, JSON-LD is used to convert it to a normalized doc Nuts Context V1 *************** -https://raw.githubusercontent.com/nuts-foundation/nuts-node/master/vcr/assets/assets/contexts/nuts.ldjson \ No newline at end of file +https://nuts.nl/credentials/v1 \ No newline at end of file diff --git a/go.mod b/go.mod index ef4425620f..8e908294aa 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( github.com/nuts-foundation/go-did v0.3.0 github.com/nuts-foundation/go-leia/v3 v3.3.0 github.com/nuts-foundation/go-stoabs v1.4.0 - github.com/piprate/json-gold v0.4.2 + github.com/piprate/json-gold v0.5.1-0.20221121142341-01873264bae4 github.com/privacybydesign/irmago v0.10.0 github.com/prometheus/client_golang v1.13.1 github.com/prometheus/client_model v0.3.0 diff --git a/go.sum b/go.sum index e1f64d1080..23ab523388 100644 --- a/go.sum +++ b/go.sum @@ -642,8 +642,8 @@ github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCko github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pierrec/lz4 v2.5.2+incompatible h1:WCjObylUIOlKy/+7Abdn34TLIkXiA4UWUMhxq9m9ZXI= github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/piprate/json-gold v0.4.2 h1:Rq8V+637HOFcj20KdTqW/g/llCwX2qtau0g5d1pD79o= -github.com/piprate/json-gold v0.4.2/go.mod h1:OK1z7UgtBZk06n2cDE2OSq1kffmjFFp5/2yhLLCz9UM= +github.com/piprate/json-gold v0.5.1-0.20221121142341-01873264bae4 h1:fnJ3Tf9WZpggydknuvrB9rHs9uHQGi6J4EbbGifSz/g= +github.com/piprate/json-gold v0.5.1-0.20221121142341-01873264bae4/go.mod h1:WZ501QQMbZZ+3pXFPhQKzNwS1+jls0oqov3uQ2WasLs= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/jsonld/testsuite/compat_test.go b/jsonld/testsuite/compat_test.go new file mode 100644 index 0000000000..9868a28cf8 --- /dev/null +++ b/jsonld/testsuite/compat_test.go @@ -0,0 +1,140 @@ +package testsuite + +import ( + "crypto/x509" + "encoding/json" + "encoding/pem" + "github.com/nuts-foundation/nuts-node/crypto" + "github.com/nuts-foundation/nuts-node/jsonld" + "github.com/nuts-foundation/nuts-node/vcr/signature" + "github.com/nuts-foundation/nuts-node/vcr/signature/proof" + "github.com/piprate/json-gold/ld" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "strings" + "testing" + "time" +) + +type testCase struct { + name string + file string +} + +var testCases = []testCase{ + // Note: there is no test for an NutsAuthorizationCredential with localParameters, + // because localParameters in v1.1 aren't compatible with v1.0 since its type changed from @graph to @json. + // This is not a problem, because nobody actually used it in v1.0. + { + name: "NutsAuthorizationCredential", + file: "authcred_001.ldjson", + }, + { + name: "NutsOrganizationCredential", + file: "orgcred_001.ldjson", + }, +} + +// TestCompatibility tests backwards compatibility of the Nuts JSON-LD context. +// It uses the test cases found in ./fixtures and checks the signature against every Nuts JSON-LD context version. +func TestCompatibility(t *testing.T) { + key := readSigningKey(t) + type context struct { + version string + loader ld.DocumentLoader + } + contexts := []context{ + { + version: "1.0", + loader: jsonld.NewMappedDocumentLoader(map[string]string{ + "https://nuts.nl/credentials/v1": "../../vcr/assets/assets/contexts/nuts.ldjson", + jsonld.W3cVcContext: "../../vcr/assets/assets/contexts/w3c-credentials-v1.ldjson", + jsonld.Jws2020Context: "../../vcr/assets/assets/contexts/lds-jws2020-v1.ldjson", + }, ld.NewDefaultDocumentLoader(nil)), + }, + } + + for _, ctx := range contexts { + t.Run(ctx.version, func(t *testing.T) { + for _, tc := range testCases { + t.Run(tc.file, func(t *testing.T) { + data, err := os.ReadFile("./fixtures/" + tc.file) + require.NoError(t, err) + var document proof.SignedDocument + err = json.Unmarshal(data, &document) + require.NoError(t, err) + + ldProof := proof.LDProof{} + err = document.UnmarshalProofValue(&ldProof) + require.NoError(t, err) + err = ldProof.Verify(document.DocumentWithoutProof(), signature.JSONWebSignature2020{ContextLoader: ctx.loader}, key.Public()) + assert.NoError(t, err) + }) + } + }) + } +} + +// TestGenerateSignedFixtures is used to generate signed test fixtures of the unsigned test cases. +// It's only there as runnable unit test to assert it keeps working. +func TestGenerateSignedFixtures(t *testing.T) { + const saveSigned = false + + loader := jsonld.NewMappedDocumentLoader(map[string]string{ + "https://nuts.nl/credentials/v1": "../../vcr/assets/assets/contexts/nuts.ldjson", + jsonld.W3cVcContext: "../../vcr/assets/assets/contexts/w3c-credentials-v1.ldjson", + jsonld.Jws2020Context: "../../vcr/assets/assets/contexts/lds-jws2020-v1.ldjson", + }, ld.NewDefaultDocumentLoader(nil)) + + privateKey := readSigningKey(t) + + for _, testCase := range testCases { + t.Run(testCase.file, func(t *testing.T) { + unsignedFile := "./fixtures/" + strings.ReplaceAll(testCase.file, ".ldjson", "_unsigned.ldjson") + data, err := os.ReadFile(unsignedFile) + require.NoError(t, err) + + var tbs proof.Document + err = json.Unmarshal(data, &tbs) + require.NoError(t, err) + + signed, err := proof.NewLDProof(proof.ProofOptions{ + Created: time.Now(), + }).Sign(tbs, signature.JSONWebSignature2020{ContextLoader: loader}, privateKey) + require.NoError(t, err) + + var targetFile = "./fixtures/" + testCase.file + // If not saving, still save it (although to temp dir) to it keeps working + if !saveSigned { + tempFile, err := os.CreateTemp("", testCase.file) + defer func() { + _ = os.Remove(tempFile.Name()) + }() + require.NoError(t, err) + _ = tempFile.Close() + targetFile = tempFile.Name() + } + signedBytes, err := json.MarshalIndent(signed, "", " ") + require.NoError(t, err) + // Copy file mode from unsigned file + fileInfo, err := os.Stat(unsignedFile) + require.NoError(t, err) + err = os.WriteFile(targetFile, signedBytes, fileInfo.Mode()) + require.NoError(t, err) + println("Written to", targetFile) + }) + } +} + +func readSigningKey(t *testing.T) crypto.Key { + pkPEMBytes, err := os.ReadFile("private_key.pem") + require.NoError(t, err) + pkDerBytes, _ := pem.Decode(pkPEMBytes) + privateKey, err := x509.ParseECPrivateKey(pkDerBytes.Bytes) + require.NoError(t, err) + return crypto.TestKey{ + PrivateKey: privateKey, + Kid: "key-id", + } +} diff --git a/jsonld/testsuite/fixtures/authcred_001.ldjson b/jsonld/testsuite/fixtures/authcred_001.ldjson new file mode 100644 index 0000000000..73dd570ee9 --- /dev/null +++ b/jsonld/testsuite/fixtures/authcred_001.ldjson @@ -0,0 +1,34 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://nuts.nl/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" + ], + "credentialSubject": { + "id": "did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY", + "purposeOfUse": "eTransfer", + "resources": [ + { + "operations": [ + "read" + ], + "path": "/composition/1", + "userContext": true + } + ] + }, + "id": "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#1", + "issuanceDate": "2022-11-25T09:44:16.972576+01:00", + "issuer": "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW", + "proof": { + "created": "2022-12-05T13:47:24.714488+01:00", + "jws": "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..8fD_JGQiYLvryd3EPUR7ft41piyqm0rs8_3jYVLZjgld4q9YIjPus-nAE1f4473oo4xh9PW_khbRJaiU-twBMw", + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "key-id" + }, + "type": [ + "NutsAuthorizationCredential", + "VerifiableCredential" + ] +} \ No newline at end of file diff --git a/jsonld/testsuite/fixtures/authcred_001_unsigned.ldjson b/jsonld/testsuite/fixtures/authcred_001_unsigned.ldjson new file mode 100644 index 0000000000..fa8de5d159 --- /dev/null +++ b/jsonld/testsuite/fixtures/authcred_001_unsigned.ldjson @@ -0,0 +1,27 @@ +{ + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://nuts.nl/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" + ], + "credentialSubject": { + "id": "did:nuts:B8PUHs2AUHbFF1xLLK4eZjgErEcMXHxs68FteY7NDtCY", + "purposeOfUse": "eTransfer", + "resources": [ + { + "operations": [ + "read" + ], + "path": "/composition/1", + "userContext": true + } + ] + }, + "id": "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW#1", + "issuanceDate": "2022-11-25T09:44:16.972576+01:00", + "issuer": "did:nuts:GvkzxsezHvEc8nGhgz6Xo3jbqkHwswLmWw3CYtCm7hAW", + "type": [ + "NutsAuthorizationCredential", + "VerifiableCredential" + ] +} \ No newline at end of file diff --git a/jsonld/testsuite/fixtures/orgcred_001.ldjson b/jsonld/testsuite/fixtures/orgcred_001.ldjson new file mode 100644 index 0000000000..b47ff33fec --- /dev/null +++ b/jsonld/testsuite/fixtures/orgcred_001.ldjson @@ -0,0 +1,28 @@ +{ + "@context": [ + "https://nuts.nl/credentials/v1", + "https://www.w3.org/2018/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" + ], + "credentialSubject": { + "id": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", + "organization": { + "city": "IJbergen", + "name": "Because we care B.V." + } + }, + "id": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey#ec8af8cf-67d4-4b54-9bd6-8a861e729e11", + "issuanceDate": "2022-06-01T15:34:40.65319+02:00", + "issuer": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", + "proof": { + "created": "2022-12-05T13:47:24.717922+01:00", + "jws": "eyJhbGciOiJFUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..WyQ-9rd323hXQLgceVAA4eRf1PHwK58dMu_Ugsl63i4WPCj6gNsZQ173qneetZYzNl5IMZWgtuZMGkB5tg-lag", + "proofPurpose": "assertionMethod", + "type": "JsonWebSignature2020", + "verificationMethod": "key-id" + }, + "type": [ + "NutsOrganizationCredential", + "VerifiableCredential" + ] +} \ No newline at end of file diff --git a/jsonld/testsuite/fixtures/orgcred_001_unsigned.ldjson b/jsonld/testsuite/fixtures/orgcred_001_unsigned.ldjson new file mode 100644 index 0000000000..dc321cf495 --- /dev/null +++ b/jsonld/testsuite/fixtures/orgcred_001_unsigned.ldjson @@ -0,0 +1,21 @@ +{ + "@context": [ + "https://nuts.nl/credentials/v1", + "https://www.w3.org/2018/credentials/v1", + "https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json" + ], + "credentialSubject": { + "id": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", + "organization": { + "city": "IJbergen", + "name": "Because we care B.V." + } + }, + "id": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey#ec8af8cf-67d4-4b54-9bd6-8a861e729e11", + "issuanceDate": "2022-06-01T15:34:40.65319+02:00", + "issuer": "did:nuts:CuE3qeFGGLhEAS3gKzhMCeqd1dGa9at5JCbmCfyMU2Ey", + "type": [ + "NutsOrganizationCredential", + "VerifiableCredential" + ] +} \ No newline at end of file diff --git a/jsonld/testsuite/private_key.pem b/jsonld/testsuite/private_key.pem new file mode 100644 index 0000000000..10a906cad4 --- /dev/null +++ b/jsonld/testsuite/private_key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEICWw5A5U8aPEiu/QJyIMP4mUqFwx62CZgjr1xdxgcoZYoAoGCCqGSM49 +AwEHoUQDQgAEZoS/Grh0lkKnW3ZO/NOEzq3kfIP6TYzsq3ldvJPEoK3mipaGUiYd +tSsNzlnEd4g8ecj06XlVpRGSZXOz6fLFpQ== +-----END EC PRIVATE KEY----- \ No newline at end of file diff --git a/vcr/assets/assets/contexts/nuts.ldjson b/vcr/assets/assets/contexts/nuts.ldjson index 22556d0237..565f06d60a 100644 --- a/vcr/assets/assets/contexts/nuts.ldjson +++ b/vcr/assets/assets/contexts/nuts.ldjson @@ -3,7 +3,6 @@ "@version": 1.1, "@protected": true, "@base": "https://nuts.nl/credentials/v1", - "@vocab": "#", "id": "@id", "type": "@type", @@ -74,7 +73,7 @@ "userContext": "nuts:userContext" } }, - "localParameters": {"@id": "nuts:localParameters", "@container": "@graph"} + "localParameters": {"@id": "nuts:localParameters", "@type": "@json"} } } } diff --git a/vcr/credential/validator.go b/vcr/credential/validator.go index db5f115691..ffdd6e4360 100644 --- a/vcr/credential/validator.go +++ b/vcr/credential/validator.go @@ -20,13 +20,11 @@ package credential import ( - "encoding/json" + "bytes" "errors" "fmt" ssi "github.com/nuts-foundation/go-did" - "github.com/nuts-foundation/nuts-node/vcr/log" "github.com/piprate/json-gold/ld" - "reflect" "strings" "github.com/nuts-foundation/go-did/vc" @@ -68,28 +66,20 @@ type AllFieldsDefinedValidator struct { // Validate implements Validator.Validate. func (d AllFieldsDefinedValidator) Validate(input vc.VerifiableCredential) error { - // First expand, then compact and marshal to JSON, then compare + // Expand with safe mode enabled, which asserts that all properties are defined in the JSON-LD context. inputAsJSON, _ := input.MarshalJSON() - inputAsMap := make(map[string]interface{}) - _ = json.Unmarshal(inputAsJSON, &inputAsMap) - normalizeJSONLDVC(inputAsMap) - expectedAsJSON, _ := json.Marshal(inputAsMap) + document, err := ld.DocumentFromReader(bytes.NewReader(inputAsJSON)) + if err != nil { + return err + } processor := ld.NewJsonLdProcessor() options := ld.NewJsonLdOptions("") options.DocumentLoader = d.DocumentLoader - compactedAsMap, err := processor.Compact(inputAsMap, inputAsMap, options) - if err != nil { - return failure("unable to compact JSON-LD VC: %s", err) - } - normalizeJSONLDVC(compactedAsMap) - compactedAsJSON, _ := json.Marshal(compactedAsMap) - - if string(expectedAsJSON) != string(compactedAsJSON) { - log.Logger().Debug("VC validation failed, not all fields are defined by JSON-LD context") - log.Logger().Debugf(" Given VC: %s", string(expectedAsJSON)) - log.Logger().Debugf(" Cleaned up VC: %s", string(compactedAsJSON)) - return failure("not all fields are defined by JSON-LD context") + options.SafeMode = true + + if _, err = processor.Expand(document, options); err != nil { + return &validationError{msg: err.Error()} } return nil } @@ -251,66 +241,3 @@ func validateNutsCredentialID(credential vc.VerifiableCredential) error { } return nil } - -// normalizeJSONLDVC takes a JSON-LD Verifiable Credential unmarshaled into a map and normalizes it, to structure it the same JSON-LD compaction would do. -// This is used for validating whether JSON-LD stays the same after compaction (for checking whether all fields are defined in the context). -// The following changes are made by normalizing: -// - Slices with 1 entry are "unsliced", so it becomes a scalar value -// - Empty map entries are removed -func normalizeJSONLDVC(input map[string]interface{}) { - delete(input, "proof") - normalizeJSONMap(input) -} - -// normalizeJSONMap see normalizeJSONLDVC -func normalizeJSONMap(input map[string]interface{}) { - for key, v := range input { - if v == nil { - // Remove empty properties - delete(input, key) - continue - } - normalizeJSONProperty(v, func(newValue interface{}) { - input[key] = newValue - }) - } -} - -// normalizeJSONProperty see normalizeJSONLDVC -func normalizeJSONProperty(input interface{}, setter func(newValue interface{})) { - value := reflect.ValueOf(input) - // If it's a slice with a single value, unslice it - if value.Kind() == reflect.Slice { - switch value.Len() { - case 0: - // Empty slice, do nothing - case 1: - // Slice with 1 entry, unslice it - input = value.Index(0).Interface() - setter(input) - default: - // Slice with zero or more entries, iterate - normalizeJSONSlice(value) - } - } - - asMap, isMap := input.(map[string]interface{}) - if isMap { - if idValue, hasID := asMap["id"]; hasID && len(asMap) == 1 { - setter(idValue) - } else { - normalizeJSONMap(asMap) - } - } -} - -// normalizeJSONSlice see normalizeJSONLDVC -func normalizeJSONSlice(input reflect.Value) { - length := input.Len() - for i := 0; i < length; i++ { - current := input.Index(i) - normalizeJSONProperty(current, func(newValue interface{}) { - current.Set(reflect.ValueOf(newValue)) - }) - } -} diff --git a/vcr/credential/validator_test.go b/vcr/credential/validator_test.go index ddefa42693..69ed5e3e6e 100644 --- a/vcr/credential/validator_test.go +++ b/vcr/credential/validator_test.go @@ -20,7 +20,6 @@ package credential import ( - "encoding/json" ssi "github.com/nuts-foundation/go-did" "github.com/nuts-foundation/go-did/vc" "github.com/nuts-foundation/nuts-node/jsonld" @@ -347,11 +346,7 @@ func TestNutsAuthorizationCredentialValidator_Validate(t *testing.T) { func TestAllFieldsDefinedValidator(t *testing.T) { validator := AllFieldsDefinedValidator{jsonld.NewTestJSONLDManager(t).DocumentLoader()} t.Run("ok", func(t *testing.T) { - var invalidCredentialSubject = make(map[string]interface{}) - invalidCredentialSubject["id"] = vdr.TestDIDB.String() - inputVC := *validNutsOrganizationCredential() - inputVC.CredentialSubject[0] = invalidCredentialSubject err := validator.Validate(inputVC) @@ -370,7 +365,7 @@ func TestAllFieldsDefinedValidator(t *testing.T) { err := validator.Validate(inputVC) - assert.EqualError(t, err, "validation failed: not all fields are defined by JSON-LD context") + assert.EqualError(t, err, "validation failed: invalid property: Dropping property that did not expand into an absolute IRI or keyword.") }) } @@ -433,17 +428,3 @@ func TestDefaultCredentialValidator(t *testing.T) { assert.EqualError(t, err, "validation failed: type 'VerifiableCredential' is required") }) } - -func TestNormalizeJSONMap(t *testing.T) { - t.Run("slice in slice", func(t *testing.T) { - // Test can't be covered by one of the VC types, so a simple unit test suffices - input := `{"outer": [["first"]]}` - inputAsMap := make(map[string]interface{}) - _ = json.Unmarshal([]byte(input), &inputAsMap) - - normalizeJSONMap(inputAsMap) - - actual, _ := json.Marshal(inputAsMap) - assert.JSONEq(t, `{"outer":["first"]}`, string(actual)) - }) -} diff --git a/vcr/issuer/issuer_test.go b/vcr/issuer/issuer_test.go index 437f75e08e..41381bc2b0 100644 --- a/vcr/issuer/issuer_test.go +++ b/vcr/issuer/issuer_test.go @@ -278,7 +278,7 @@ func Test_issuer_Issue(t *testing.T) { } result, err := sut.Issue(invalidCred, true, true) - assert.EqualError(t, err, "validation failed: not all fields are defined by JSON-LD context") + assert.EqualError(t, err, "validation failed: invalid property: Dropping property that did not expand into an absolute IRI or keyword.") assert.Nil(t, result) })