From 893f6ca88f66f38e3f03add972fb6e507cc898dd Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Mon, 3 Feb 2025 09:25:04 +0100 Subject: [PATCH] feat: add credential subject validation (#4776) * feat: add rule to validate credential subjects * improve fluent statement --- .../core/IdentityAndTrustExtension.java | 2 +- .../verifiable-credentials/build.gradle.kts | 1 + ...fiableCredentialValidationServiceImpl.java | 9 +- .../rules/HasValidSubjectSchema.java | 63 ++++++ ...leCredentialValidationServiceImplTest.java | 65 ++++-- .../rules/HasValidSubjectSchemaTest.java | 200 ++++++++++++++++++ .../test/resources/companyAddressSchema.json | 29 +++ .../src/test/resources/genericNameSchema.json | 16 ++ .../test/resources/personAddressSchema.json | 20 ++ .../src/test/resources/personSchema.json | 24 +++ gradle/libs.versions.toml | 1 + 11 files changed, 407 insertions(+), 23 deletions(-) create mode 100644 extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchema.java create mode 100644 extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchemaTest.java create mode 100644 extensions/common/iam/verifiable-credentials/src/test/resources/companyAddressSchema.json create mode 100644 extensions/common/iam/verifiable-credentials/src/test/resources/genericNameSchema.json create mode 100644 extensions/common/iam/verifiable-credentials/src/test/resources/personAddressSchema.json create mode 100644 extensions/common/iam/verifiable-credentials/src/test/resources/personSchema.json diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java index 1b3f945d0a..0875b58bba 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java @@ -191,7 +191,7 @@ public IdentityService createIdentityService(ServiceExtensionContext context) { var validationAction = tokenValidationAction(); var credentialValidationService = new VerifiableCredentialValidationServiceImpl(createPresentationVerifier(context), - trustedIssuerRegistry, revocationServiceRegistry, clock); + trustedIssuerRegistry, revocationServiceRegistry, clock, typeManager.getMapper()); return new IdentityAndTrustService(secureTokenService, issuerId, getCredentialServiceClient(context), validationAction, credentialServiceUrlResolver, claimTokenFunction, diff --git a/extensions/common/iam/verifiable-credentials/build.gradle.kts b/extensions/common/iam/verifiable-credentials/build.gradle.kts index 4eaebbd938..987c63ee3d 100644 --- a/extensions/common/iam/verifiable-credentials/build.gradle.kts +++ b/extensions/common/iam/verifiable-credentials/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { api(project(":spi:common:verifiable-credentials-spi")) api(project(":spi:common:http-spi")) implementation(project(":core:common:lib:util-lib")) + implementation(libs.jsonschema) testImplementation(testFixtures(project(":spi:common:verifiable-credentials-spi"))) testImplementation(libs.mockserver.netty) diff --git a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImpl.java b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImpl.java index 6a8a9ff543..38d5cc3cf4 100644 --- a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImpl.java +++ b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImpl.java @@ -14,8 +14,10 @@ package org.eclipse.edc.iam.verifiablecredentials; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.edc.iam.verifiablecredentials.rules.HasValidIssuer; import org.eclipse.edc.iam.verifiablecredentials.rules.HasValidSubjectIds; +import org.eclipse.edc.iam.verifiablecredentials.rules.HasValidSubjectSchema; import org.eclipse.edc.iam.verifiablecredentials.rules.IsInValidityPeriod; import org.eclipse.edc.iam.verifiablecredentials.rules.IsNotRevoked; import org.eclipse.edc.iam.verifiablecredentials.spi.VerifiableCredentialValidationService; @@ -40,12 +42,14 @@ public class VerifiableCredentialValidationServiceImpl implements VerifiableCred private final TrustedIssuerRegistry trustedIssuerRegistry; private final RevocationServiceRegistry revocationServiceRegistry; private final Clock clock; + private final ObjectMapper mapper; - public VerifiableCredentialValidationServiceImpl(PresentationVerifier presentationVerifier, TrustedIssuerRegistry trustedIssuerRegistry, RevocationServiceRegistry revocationServiceRegistry, Clock clock) { + public VerifiableCredentialValidationServiceImpl(PresentationVerifier presentationVerifier, TrustedIssuerRegistry trustedIssuerRegistry, RevocationServiceRegistry revocationServiceRegistry, Clock clock, ObjectMapper mapper) { this.presentationVerifier = presentationVerifier; this.trustedIssuerRegistry = trustedIssuerRegistry; this.revocationServiceRegistry = revocationServiceRegistry; this.clock = clock; + this.mapper = mapper; } @Override @@ -67,7 +71,8 @@ private Result validateVerifiableCredentials(List cr new IsInValidityPeriod(clock), new HasValidSubjectIds(presentationHolder), new IsNotRevoked(revocationServiceRegistry), - new HasValidIssuer(trustedIssuerRegistry))); + new HasValidIssuer(trustedIssuerRegistry), + new HasValidSubjectSchema(mapper))); filters.addAll(additionalRules); var results = credentials diff --git a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchema.java b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchema.java new file mode 100644 index 0000000000..7ee017bb32 --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchema.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.iam.verifiablecredentials.rules; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.eclipse.edc.iam.verifiablecredentials.spi.validation.CredentialValidationRule; +import org.eclipse.edc.spi.result.Result; + +import java.net.URI; +import java.util.Objects; + +/** + * Performs JSON Schema Validation of the credential subjects. Every credential subject must be validated against all + * credential schemas, and all validations must succeed in order for this rule to pass. + */ +public class HasValidSubjectSchema implements CredentialValidationRule { + private final ObjectMapper jsonMapper; + private final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012, builder -> builder.enableSchemaCache(true)); + + public HasValidSubjectSchema(ObjectMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + + @Override + public Result apply(VerifiableCredential verifiableCredential) { + if (verifiableCredential.getCredentialSchema() == null || verifiableCredential.getCredentialSchema().isEmpty()) { + return Result.success(); + } + return verifiableCredential.getCredentialSchema().stream().filter(Objects::nonNull).map(schema -> { + var schemaUrl = schema.id(); + // returns the schema using the JsonSchemaFactory. The factory does some caching internally, so there is no need to cache again + var jsonSchema = factory.getSchema(URI.create(schemaUrl)); + + // validate all subjects against the current schema + var validationMessages = verifiableCredential.getCredentialSubject().stream() + .map(subject -> jsonMapper.convertValue(subject, JsonNode.class)) + .flatMap(jsonNode -> jsonSchema.validate(jsonNode).stream()) + .toList(); + return validationMessages.isEmpty() + ? Result.success() + : Result.failure("Error validating CredentialSubject against schema: " + validationMessages); //ValidationMessage overwrites toString() + + }).reduce(Result::merge).orElseGet(Result::success); + } + +} diff --git a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImplTest.java b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImplTest.java index ceaabe365e..0ce26fe4bd 100644 --- a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImplTest.java +++ b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImplTest.java @@ -15,7 +15,9 @@ package org.eclipse.edc.iam.verifiablecredentials; +import com.fasterxml.jackson.databind.ObjectMapper; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; @@ -23,6 +25,7 @@ import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiablePresentationContainer; import org.eclipse.edc.iam.verifiablecredentials.spi.validation.PresentationVerifier; import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry; +import org.eclipse.edc.junit.testfixtures.TestUtils; import org.eclipse.edc.spi.result.Result; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -47,22 +50,22 @@ @SuppressWarnings("unchecked") class VerifiableCredentialValidationServiceImplTest { public static final String CONSUMER_DID = "did:web:consumer"; - private final PresentationVerifier verifierMock = mock(); - private final TrustedIssuerRegistry trustedIssuerRegistryMock = mock(); + private final PresentationVerifier verifier = mock(); + private final TrustedIssuerRegistry trustedIssuerRegistry = mock(); private final RevocationServiceRegistry revocationServiceRegistry = mock(); - private final VerifiableCredentialValidationServiceImpl service = new VerifiableCredentialValidationServiceImpl(verifierMock, trustedIssuerRegistryMock, revocationServiceRegistry, Clock.systemUTC()); + private final VerifiableCredentialValidationServiceImpl validationService = new VerifiableCredentialValidationServiceImpl(verifier, trustedIssuerRegistry, revocationServiceRegistry, Clock.systemUTC(), new ObjectMapper()); @BeforeEach void setUp() { - when(trustedIssuerRegistryMock.getSupportedTypes(TRUSTED_ISSUER)).thenReturn(Set.of(TrustedIssuerRegistry.WILDCARD)); + when(trustedIssuerRegistry.getSupportedTypes(TRUSTED_ISSUER)).thenReturn(Set.of(TrustedIssuerRegistry.WILDCARD)); when(revocationServiceRegistry.checkValidity(any())).thenReturn(Result.success()); } @Test void cryptographicError() { - when(verifierMock.verifyPresentation(any())).thenReturn(Result.failure("Cryptographic error")); + when(verifier.verifyPresentation(any())).thenReturn(Result.failure("Cryptographic error")); var presentations = List.of(createPresentationContainer()); - var result = service.validate(presentations); + var result = validationService.validate(presentations); assertThat(result).isFailed().detail().isEqualTo("Cryptographic error"); } @@ -75,9 +78,9 @@ void notYetValid() { .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); + when(verifier.verifyPresentation(any())).thenReturn(success()); var presentations = List.of(vpContainer); - var result = service.validate(presentations); + var result = validationService.validate(presentations); assertThat(result).isFailed().messages() .hasSizeGreaterThanOrEqualTo(1) .contains("Credential is not yet valid."); @@ -96,8 +99,8 @@ void oneInvalidSubjectId() { .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); - var result = service.validate(List.of(vpContainer)); + when(verifier.verifyPresentation(any())).thenReturn(success()); + var result = validationService.validate(List.of(vpContainer)); assertThat(result).isFailed().messages() .hasSizeGreaterThanOrEqualTo(1) .contains("Not all credential subject IDs match the expected subject ID '%s'. Violating subject IDs: [invalid-subject-id]".formatted(CONSUMER_DID)); @@ -113,8 +116,8 @@ void credentialHasInvalidIssuer_issuerIsUrl() { .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); - var result = service.validate(List.of(vpContainer)); + when(verifier.verifyPresentation(any())).thenReturn(success()); + var result = validationService.validate(List.of(vpContainer)); assertThat(result).isFailed().messages() .hasSizeGreaterThanOrEqualTo(1) .contains("Credential types '[test-type]' are not supported for issuer 'invalid-issuer'"); @@ -133,8 +136,8 @@ void verify_singlePresentation_singleCredential() { .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); - var result = service.validate(List.of(vpContainer)); + when(verifier.verifyPresentation(any())).thenReturn(success()); + var result = validationService.validate(List.of(vpContainer)); assertThat(result).isSucceeded(); } @@ -157,8 +160,8 @@ void verify_singlePresentation_multipleCredentials() { .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); - var result = service.validate(List.of(vpContainer)); + when(verifier.verifyPresentation(any())).thenReturn(success()); + var result = validationService.validate(List.of(vpContainer)); assertThat(result).isSucceeded(); } @@ -200,9 +203,9 @@ void verify_multiplePresentations_multipleCredentialsEach() { .build(); var vpContainer2 = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation2); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); + when(verifier.verifyPresentation(any())).thenReturn(success()); - var result = service.validate(List.of(vpContainer1, vpContainer2)); + var result = validationService.validate(List.of(vpContainer1, vpContainer2)); assertThat(result).isSucceeded(); } @@ -220,12 +223,34 @@ void verify_revocationCheckFails() { .build())) .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation); - when(verifierMock.verifyPresentation(any())).thenReturn(success()); + when(verifier.verifyPresentation(any())).thenReturn(success()); when(revocationServiceRegistry.checkValidity(any())).thenReturn(Result.failure("invalid")); - var result = service.validate(List.of(vpContainer)); + var result = validationService.validate(List.of(vpContainer)); assertThat(result).isFailed() .detail().isEqualTo("invalid"); } + @Test + void verify_subjectViolatesSchema() { + var presentation = createPresentationBuilder() + .type("VerifiablePresentation") + .holder(CONSUMER_DID) + .credentials(List.of(createCredentialBuilder() + .credentialSubjects(List.of(CredentialSubject.Builder.newInstance() + .id(CONSUMER_DID) + .claim("type", "PersonSchema") + .claim("name", "Foo Bar") + .claim("birthdate", 11237123) // violation: int instead of string + .build())) + .credentialSchema(new CredentialSchema(TestUtils.getResource("personSchema.json").toString(), "JsonSchema")) + .build())) + .build(); + var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_JWT, presentation); + when(verifier.verifyPresentation(any())).thenReturn(success()); + var result = validationService.validate(List.of(vpContainer)); + assertThat(result).isFailed().messages() + .hasSizeGreaterThanOrEqualTo(1) + .allMatch(s -> s.contains("Error validating CredentialSubject against schema")); + } } \ No newline at end of file diff --git a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchemaTest.java b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchemaTest.java new file mode 100644 index 0000000000..620cfbef56 --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/rules/HasValidSubjectSchemaTest.java @@ -0,0 +1,200 @@ +/* + * Copyright (c) 2025 Cofinity-X + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + * + * Contributors: + * Cofinity-X - initial API and implementation + * + */ + +package org.eclipse.edc.iam.verifiablecredentials.rules; + + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSchema; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer; +import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getResource; + +class HasValidSubjectSchemaTest { + + private final ObjectMapper mapper = new ObjectMapper(); + private final HasValidSubjectSchema rule = new HasValidSubjectSchema(mapper); + + @Test + void validate_oneSchema_oneSubject() { + var cred = credential() + .credentialSubject(subject().build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + assertThat(rule.apply(cred)).isSucceeded(); + } + + @Test + void validate_oneSchema_multipleSubjects_allValid() { + var cred = credential() + .credentialSubject(subject().build()) + .credentialSubject(subject() + .claim("anotherClaim", "anotherValue") + .build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + assertThat(rule.apply(cred)).isSucceeded(); + } + + @Test + void validate_oneSchema_multipleSubjects_oneValidates() { + var cred = credential() + .credentialSubject(subject().build()) + .credentialSubject(CredentialSubject.Builder.newInstance().id(UUID.randomUUID().toString()) + .claim("anotherClaim", "anotherValue") + .build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + assertThat(rule.apply(cred)).isFailed(); + } + + @Test + void validate_oneSubjectMultipleSchemas_nonIntersect() { + var cred = credential() + .credentialSubject(subject() + .claim("companyName", "QuizzQuazz Industries Inc.") + .claim("email", "info@quizzquazz.com") + .claim("street", "FooBar Street 15") + .claim("city", "BarBazTown") + .claim("postalCode", 12345) + .build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .credentialSchema(new CredentialSchema(getResource("companyAddressSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + assertThat(rule.apply(cred)).isSucceeded(); + } + + @Test + void validate_oneSubjectMultipleSchemas_intersectWithConflict() { + var cred = credential() + .credentialSubject(subject() + .claim("companyName", "QuizzQuazz Industries Inc.") + .claim("email", "info@quizzquazz.com") + .claim("street", "FooBar Street 15") + .claim("city", "BarBazTown") + .claim("postalCode", 12345) + .build()) + .credentialSchema(new CredentialSchema(getResource("companyAddressSchema.json").toString(), "JsonSchemaValidator2018")) + .credentialSchema(new CredentialSchema(getResource("personAddressSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + // personAddressSchema defines postalCode as string, companyAddressSchema defines it as int -> conflict + assertThat(rule.apply(cred)).isFailed(); + } + + @Test + void validate_multipleSchemas_oneSubject_allValid() { + var cred = credential() + .credentialSubject(subject().build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .credentialSchema(new CredentialSchema(getResource("genericNameSchema.json").toString(), "JsonSchemaValidator2019")) + .build(); + + assertThat(rule.apply(cred)).isSucceeded(); + } + + @Test + void validate_multipleSubjects_oneViolatesRestrictiveSchema() { + var cred = credential() + .credentialSubject(subject().build()) // OK + .credentialSubject(CredentialSubject.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .claim("name", "foo bar") // satisfies name schema, but violates person schema + .build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .credentialSchema(new CredentialSchema(getResource("genericNameSchema.json").toString(), "JsonSchemaValidator2019")) + .build(); + + assertThat(rule.apply(cred)).isFailed(); + } + + @Test + void validate_noSchema() { + var cred = credential() + .credentialSubject(subject().build()) + .build(); + + assertThat(rule.apply(cred)).isSucceeded(); + + var cred2 = credential() + .credentialSubject(subject().build()) + .credentialSchemas(null) + .build(); + + assertThat(rule.apply(cred2)).isSucceeded(); + + var cred3 = credential() + .credentialSubject(subject().build()) + .credentialSchema(null) + .build(); + + assertThat(rule.apply(cred3)).isSucceeded(); + } + + @Test + void validate_oneSubjectViolates() { + var cred = credential() + .credentialSubject(subject() + .claim("name", 14) + .build()) // name should be a string + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + var result = rule.apply(cred); + assertThat(result).isFailed(); + assertThat(result.getFailureMessages()).hasSize(1); + } + + @Test + void validate_claimNotCoveredBySchema_shouldSucceed() { + var cred = credential() + .credentialSubject(subject() + .claim("another-property", 14) + .build()) + .credentialSchema(new CredentialSchema(getResource("personSchema.json").toString(), "JsonSchemaValidator2018")) + .build(); + + var result = rule.apply(cred); + assertThat(result).isSucceeded(); + } + + + private VerifiableCredential.Builder credential() { + return VerifiableCredential.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .issuanceDate(Instant.now()) + .issuer(new Issuer(UUID.randomUUID().toString())) + .type("VerifiableCredential"); + } + + private CredentialSubject.Builder subject() { + return CredentialSubject.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .claim("type", "PersonSubject") + .claim("name", "Alice Smith") + .claim("birthDate", "2001-12-02T00:00:00Z"); + } +} \ No newline at end of file diff --git a/extensions/common/iam/verifiable-credentials/src/test/resources/companyAddressSchema.json b/extensions/common/iam/verifiable-credentials/src/test/resources/companyAddressSchema.json new file mode 100644 index 0000000000..6593aa8a12 --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/test/resources/companyAddressSchema.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "companyName": { + "type": "string" + }, + "email": { + "type": "string", + "forat": "email" + }, + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "postalCode": { + "type": "integer" + } + }, + "required": [ + "companyName", + "email", + "street", + "city", + "postalCode" + ] +} \ No newline at end of file diff --git a/extensions/common/iam/verifiable-credentials/src/test/resources/genericNameSchema.json b/extensions/common/iam/verifiable-credentials/src/test/resources/genericNameSchema.json new file mode 100644 index 0000000000..6ea6ce3023 --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/test/resources/genericNameSchema.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ] +} \ No newline at end of file diff --git a/extensions/common/iam/verifiable-credentials/src/test/resources/personAddressSchema.json b/extensions/common/iam/verifiable-credentials/src/test/resources/personAddressSchema.json new file mode 100644 index 0000000000..f36eb5bbb6 --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/test/resources/personAddressSchema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "street": { + "type": "string" + }, + "city": { + "type": "string" + }, + "postalCode": { + "type": "string" + } + }, + "required": [ + "street", + "city", + "postalCode" + ] +} \ No newline at end of file diff --git a/extensions/common/iam/verifiable-credentials/src/test/resources/personSchema.json b/extensions/common/iam/verifiable-credentials/src/test/resources/personSchema.json new file mode 100644 index 0000000000..13ec7160dd --- /dev/null +++ b/extensions/common/iam/verifiable-credentials/src/test/resources/personSchema.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string" + }, + "name": { + "type": "string" + }, + "birthDate": { + "type": "string" + } + }, + "required": [ + "id", + "type", + "name", + "birthDate" + ] +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa567dbdce..b4d991a950 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -102,6 +102,7 @@ atomikos-jdbc = { module = "com.atomikos:transactions-jdbc", version.ref = "atom cloudEvents = { module = "io.cloudevents:cloudevents-http-basic", version.ref = "cloudEvents" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } kafkaClients = { module = "org.apache.kafka:kafka-clients", version.ref = "kafkaClients" } +jsonschema = { module = "com.networknt:json-schema-validator", version = "1.5.5" } [bundles]