From 2393fdf6f6269325fef9466e740f662514963f4f Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger <43503240+paullatzelsperger@users.noreply.github.com> Date: Tue, 6 Feb 2024 12:18:38 +0100 Subject: [PATCH] feat: enable Multi-Tenancy in PresentationApi (#263) --- .../api/PresentationApiExtension.java | 5 +- .../identityhub/api/v1/PresentationApi.java | 4 +- .../api/v1/PresentationApiController.java | 23 +++- .../api/v1/PresentationApiControllerTest.java | 47 ++++--- .../core/CoreServicesExtension.java | 6 +- .../core/CredentialQueryResolverImpl.java | 14 ++- .../core/PresentationCreatorRegistryImpl.java | 35 ++++-- .../VerifiablePresentationServiceImpl.java | 11 +- .../verification/AccessTokenVerifierImpl.java | 13 +- .../core/CredentialQueryResolverImplTest.java | 63 ++++++---- .../PresentationCreatorRegistryImplTest.java | 118 ++++++++++++++++++ ...VerifiablePresentationServiceImplTest.java | 62 ++++----- .../AccessTokenVerifierImplComponentTest.java | 51 ++++++-- .../AccessTokenVerifierImplTest.java | 12 +- ...java => PresentationApiComponentTest.java} | 83 ++++++++---- .../PresentationCreatorRegistry.java | 21 ++-- .../VerifiablePresentationService.java | 3 +- .../spi/model/ParticipantResource.java | 7 ++ .../resolution/CredentialQueryResolver.java | 7 +- .../spi/verification/AccessTokenVerifier.java | 5 +- .../junit/testfixtures/JwtCreationUtil.java | 4 +- .../VerifiableCredentialTestUtil.java | 8 +- .../store/test/CredentialStoreTestBase.java | 29 ++++- 23 files changed, 459 insertions(+), 172 deletions(-) create mode 100644 core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java rename e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/{ResolutionApiComponentTest.java => PresentationApiComponentTest.java} (63%) diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java index d2ff98a84..257181ae7 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/PresentationApiExtension.java @@ -18,6 +18,7 @@ import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer; import org.eclipse.edc.identityhub.api.v1.PresentationApiController; import org.eclipse.edc.identityhub.api.validation.PresentationQueryValidator; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; @@ -60,6 +61,8 @@ public class PresentationApiExtension implements ServiceExtension { private JsonLd jsonLd; @Inject private TypeManager typeManager; + @Inject + private ParticipantContextService participantContextService; @Override public String name() { @@ -72,7 +75,7 @@ public void initialize(ServiceExtensionContext context) { validatorRegistry.register(PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY, new PresentationQueryValidator()); - var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor()); + var controller = new PresentationApiController(validatorRegistry, typeTransformer, credentialResolver, accessTokenVerifier, verifiablePresentationService, context.getMonitor(), participantContextService); var jsonLdMapper = typeManager.getMapper(JSON_LD); webService.registerResource(RESOLUTION_CONTEXT, new ObjectMapperProvider(jsonLdMapper)); diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApi.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApi.java index 10f3e4273..f4e0f86b0 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApi.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApi.java @@ -50,11 +50,11 @@ public interface PresentationApi { @ApiResponse(responseCode = "401", description = "No Authorization header was given.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")), @ApiResponse(responseCode = "403", description = "The given authentication token could not be validated. This can happen, when the request body " + - "calls for a broader query scope than the granted scope in the auth token", + "calls for a broader query scope than the granted scope in the auth token", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")), @ApiResponse(responseCode = "501", description = "When the request contained a presentationDefinition object, but the implementation does not support it.", content = @Content(array = @ArraySchema(schema = @Schema(implementation = ApiSchema.ApiErrorDetailSchema.class)), mediaType = "application/json")) } ) - Response queryPresentation(JsonObject query, String authHeader); + Response queryPresentation(String participantContextId, JsonObject query, String authHeader); } diff --git a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApiController.java b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApiController.java index 5b1c71d66..7f4df4307 100644 --- a/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApiController.java +++ b/core/identity-hub-api/src/main/java/org/eclipse/edc/identityhub/api/v1/PresentationApiController.java @@ -20,9 +20,12 @@ import jakarta.ws.rs.HeaderParam; import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Response; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage; @@ -43,10 +46,11 @@ import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; import static org.eclipse.edc.identitytrust.model.credentialservice.PresentationQueryMessage.PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY; +import static org.eclipse.edc.web.spi.exception.ServiceResultHandler.exceptionMapper; @Consumes(APPLICATION_JSON) @Produces(APPLICATION_JSON) -@Path("/presentation") +@Path("/participants/{participantId}/presentation") public class PresentationApiController implements PresentationApi { private final JsonObjectValidatorRegistry validatorRegistry; @@ -55,22 +59,24 @@ public class PresentationApiController implements PresentationApi { private final AccessTokenVerifier accessTokenVerifier; private final VerifiablePresentationService verifiablePresentationService; private final Monitor monitor; + private final ParticipantContextService participantContextService; public PresentationApiController(JsonObjectValidatorRegistry validatorRegistry, TypeTransformerRegistry transformerRegistry, CredentialQueryResolver queryResolver, - AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor) { + AccessTokenVerifier accessTokenVerifier, VerifiablePresentationService verifiablePresentationService, Monitor monitor, ParticipantContextService participantContextService) { this.validatorRegistry = validatorRegistry; this.transformerRegistry = transformerRegistry; this.queryResolver = queryResolver; this.accessTokenVerifier = accessTokenVerifier; this.verifiablePresentationService = verifiablePresentationService; this.monitor = monitor; + this.participantContextService = participantContextService; } @POST @Path("/query") @Override - public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION) String token) { + public Response queryPresentation(@PathParam("participantId") String participantContextId, JsonObject query, @HeaderParam(AUTHORIZATION) String token) { if (token == null) { throw new AuthenticationFailedException("Authorization header missing"); } @@ -83,15 +89,20 @@ public Response queryPresentation(JsonObject query, @HeaderParam(AUTHORIZATION) return notImplemented(); } + // verify that the participant actually exists + participantContextService.getParticipantContext(participantContextId) + .orElseThrow(exceptionMapper(ParticipantContext.class, participantContextId)); + + // verify and validate the requestor's SI token - var issuerScopes = accessTokenVerifier.verify(token).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail()))); + var issuerScopes = accessTokenVerifier.verify(token, participantContextId).orElseThrow(f -> new AuthenticationFailedException("ID token verification failed: %s".formatted(f.getFailureDetail()))); // query the database - var credentials = queryResolver.query(presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail())); + var credentials = queryResolver.query(participantContextId, presentationQuery, issuerScopes).orElseThrow(f -> new NotAuthorizedException(f.getFailureDetail())); // package the credentials in a VP and sign var audience = getAudience(token); - var presentationResponse = verifiablePresentationService.createPresentation(credentials.toList(), presentationQuery.getPresentationDefinition(), audience) + var presentationResponse = verifiablePresentationService.createPresentation(participantContextId, credentials.toList(), presentationQuery.getPresentationDefinition(), audience) .compose(presentation -> transformerRegistry.transform(presentation, JsonObject.class)) .orElseThrow(failure -> new EdcException("Error creating VerifiablePresentation: %s".formatted(failure.getFailureDetail()))); return Response.ok() diff --git a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java index 1cffa991c..4e3fdd3a5 100644 --- a/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java +++ b/core/identity-hub-api/src/test/java/org/eclipse/edc/identityservice/api/v1/PresentationApiControllerTest.java @@ -18,7 +18,9 @@ import jakarta.json.Json; import jakarta.json.JsonObject; import org.eclipse.edc.identityhub.api.v1.PresentationApiController; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.resolution.QueryResult; import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; @@ -30,6 +32,7 @@ import org.eclipse.edc.junit.annotations.ApiTest; import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.spi.result.ServiceResult; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.eclipse.edc.validator.spi.JsonObjectValidatorRegistry; import org.eclipse.edc.web.jersey.testfixtures.RestControllerTestBase; @@ -69,16 +72,20 @@ @SuppressWarnings("resource") class PresentationApiControllerTest extends RestControllerTestBase { + private static final String PARTICIPANT_ID = "participant-id"; private final JsonObjectValidatorRegistry validatorRegistryMock = mock(); private final TypeTransformerRegistry typeTransformerRegistry = mock(); private final CredentialQueryResolver queryResolver = mock(); private final AccessTokenVerifier accessTokenVerifier = mock(); private final VerifiablePresentationService generator = mock(); - + private final ParticipantContextService participantContextService = mock(a -> ServiceResult.success(ParticipantContext.Builder.newInstance() + .participantId(a.getArgument(0).toString()) + .apiTokenAlias("test-alias") + .build())); @Test void query_tokenNotPresent_shouldReturn401() { - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), null)) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), null)) .isInstanceOf(AuthenticationFailedException.class) .hasMessage("Authorization header missing"); } @@ -87,7 +94,7 @@ void query_tokenNotPresent_shouldReturn401() { void query_validationError_shouldReturn400() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(failure(violation("foo", "bar"))); - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt())) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt())) .isInstanceOf(ValidationFailureException.class) .hasMessage("foo"); } @@ -97,7 +104,7 @@ void query_transformationError_shouldReturn400() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success()); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.failure("cannot transform")); - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt())) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt())) .isInstanceOf(InvalidRequestException.class) .hasMessage("cannot transform"); verifyNoInteractions(accessTokenVerifier, queryResolver, generator); @@ -110,7 +117,7 @@ void query_withPresentationDefinition_shouldReturn501() { .presentationDefinition(PresentationDefinition.Builder.newInstance().id(UUID.randomUUID().toString()).build()); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder.build())); - var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt()); + var response = controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()); assertThat(response.getStatus()).isEqualTo(503); assertThat(response.getEntity()).extracting(o -> (ApiErrorDetail) o).satisfies(ed -> { assertThat(ed.getMessage()).isEqualTo("Not implemented."); @@ -125,9 +132,9 @@ void query_tokenVerificationFails_shouldReturn401() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success()); var presentationQueryBuilder = createPresentationQueryBuilder().build(); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder)); - when(accessTokenVerifier.verify(anyString())).thenReturn(Result.failure("test-failure")); + when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.failure("test-failure")); - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt())) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt())) .isExactlyInstanceOf(AuthenticationFailedException.class) .hasMessage("ID token verification failed: test-failure"); verifyNoInteractions(queryResolver, generator); @@ -138,10 +145,10 @@ void query_queryResolutionFails_shouldReturn403() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success()); var presentationQueryBuilder = createPresentationQueryBuilder().build(); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder)); - when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1"))); - when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure")); + when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1"))); + when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(QueryResult.unauthorized("test-failure")); - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt())) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt())) .isInstanceOf(NotAuthorizedException.class) .hasMessage("test-failure"); verifyNoInteractions(generator); @@ -152,12 +159,12 @@ void query_presentationGenerationFails_shouldReturn500() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success()); var presentationQueryBuilder = createPresentationQueryBuilder().build(); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder)); - when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1"))); - when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty())); + when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1"))); + when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty())); - when(generator.createPresentation(anyList(), any(), any())).thenReturn(Result.failure("test-failure")); + when(generator.createPresentation(anyString(), anyList(), any(), any())).thenReturn(Result.failure("test-failure")); - assertThatThrownBy(() -> controller().queryPresentation(createObjectBuilder().build(), generateJwt())) + assertThatThrownBy(() -> controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt())) .isExactlyInstanceOf(EdcException.class) .hasMessage("Error creating VerifiablePresentation: test-failure"); } @@ -167,8 +174,8 @@ void query_success() { when(validatorRegistryMock.validate(eq(PRESENTATION_QUERY_MESSAGE_TYPE_PROPERTY), any())).thenReturn(success()); var presentationQueryBuilder = createPresentationQueryBuilder().build(); when(typeTransformerRegistry.transform(isA(JsonObject.class), eq(PresentationQueryMessage.class))).thenReturn(Result.success(presentationQueryBuilder)); - when(accessTokenVerifier.verify(anyString())).thenReturn(Result.success(List.of("test-scope1"))); - when(queryResolver.query(any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty())); + when(accessTokenVerifier.verify(anyString(), anyString())).thenReturn(Result.success(List.of("test-scope1"))); + when(queryResolver.query(anyString(), any(), eq(List.of("test-scope1")))).thenReturn(success(Stream.empty())); var pres = PresentationResponseMessage.Builder.newinstance().presentation(List.of(generateJwt())) .presentationSubmission(new PresentationSubmission("id", "def-id", List.of(new InputDescriptorMapping("id", "ldp_vp", "$.verifiableCredentials[0]")))) @@ -176,9 +183,9 @@ void query_success() { var jsonResponse = Json.createObjectBuilder().build(); when(typeTransformerRegistry.transform(eq(pres), eq(JsonObject.class))).thenReturn(Result.success(jsonResponse)); - when(generator.createPresentation(anyList(), any(), any())).thenReturn(Result.success(pres)); + when(generator.createPresentation(anyString(), anyList(), any(), any())).thenReturn(Result.success(pres)); - var response = controller().queryPresentation(createObjectBuilder().build(), generateJwt()); + var response = controller().queryPresentation(PARTICIPANT_ID, createObjectBuilder().build(), generateJwt()); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(200); assertThat(response.getEntity()).isEqualTo(jsonResponse); @@ -187,11 +194,11 @@ void query_success() { @Override protected PresentationApiController controller() { - return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock()); + return new PresentationApiController(validatorRegistryMock, typeTransformerRegistry, queryResolver, accessTokenVerifier, generator, mock(), participantContextService); } private String generateJwt() { - var ecKey = generateEcKey(); + var ecKey = generateEcKey(null); var jwt = buildSignedJwt(new JWTClaimsSet.Builder().audience("test-audience") .expirationTime(Date.from(Instant.now().plusSeconds(3600))) .issuer("test-issuer") diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java index a56e1955c..546b20ffe 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java @@ -17,6 +17,7 @@ import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; import org.eclipse.edc.identityhub.core.creators.JwtPresentationGenerator; import org.eclipse.edc.identityhub.core.creators.LdpPresentationGenerator; +import org.eclipse.edc.identityhub.spi.KeyPairService; import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService; @@ -108,9 +109,10 @@ public class CoreServicesExtension implements ServiceExtension { private Vault vault; @Inject private KeyParserRegistry keyParserRegistry; - @Inject private SignatureSuiteRegistry suiteRegistry; + @Inject + private KeyPairService keyPairService; @Override public String name() { @@ -139,7 +141,7 @@ public CredentialQueryResolver createCredentialQueryResolver() { @Provider public PresentationCreatorRegistry presentationCreatorRegistry(ServiceExtensionContext context) { if (presentationCreatorRegistry == null) { - presentationCreatorRegistry = new PresentationCreatorRegistryImpl(); + presentationCreatorRegistry = new PresentationCreatorRegistryImpl(keyPairService); presentationCreatorRegistry.addCreator(new JwtPresentationGenerator(privateKeyResolver, clock, getOwnDid(context), new JwtGenerationService()), CredentialFormat.JWT); var ldpIssuer = LdpIssuer.Builder.newInstance().jsonLd(jsonLd).monitor(context.getMonitor()).build(); diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java index 5b07fd8a7..b379001eb 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImpl.java @@ -25,6 +25,7 @@ import org.eclipse.edc.spi.result.AbstractResult; import org.eclipse.edc.spi.result.Result; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -43,7 +44,7 @@ public CredentialQueryResolverImpl(CredentialStore credentialStore, ScopeToCrite } @Override - public QueryResult query(PresentationQueryMessage query, List issuerScopes) { + public QueryResult query(String participantContextId, PresentationQueryMessage query, List issuerScopes) { if (query.getPresentationDefinition() != null) { throw new UnsupportedOperationException("Querying with a DIF Presentation Exchange definition is not yet supported."); } @@ -64,7 +65,7 @@ public QueryResult query(PresentationQueryMessage query, List issuerScop } // query storage for requested credentials - var queryspec = convertToQuerySpec(proverScopeResult.getContent()); + var queryspec = convertToQuerySpec(proverScopeResult.getContent(), participantContextId); var credentialResult = credentialStore.query(queryspec); if (credentialResult.failed()) { return QueryResult.storageFailure(credentialResult.getFailureMessages()); @@ -74,7 +75,7 @@ public QueryResult query(PresentationQueryMessage query, List issuerScop var requestedCredentials = credentialResult.getContent(); // check that prover scope is not wider than issuer scope - var issuerQuery = convertToQuerySpec(issuerScopeResult.getContent()); + var issuerQuery = convertToQuerySpec(issuerScopeResult.getContent(), participantContextId); var allowedCred = credentialStore.query(issuerQuery); if (allowedCred.failed()) { return QueryResult.invalidScope(allowedCred.getFailureMessages()); @@ -108,9 +109,12 @@ private Result> parseScopes(List scopes) { return success(transformResult.stream().map(AbstractResult::getContent).toList()); } - private QuerySpec convertToQuerySpec(List criteria) { + private QuerySpec convertToQuerySpec(List criteria, String participantContextId) { + var filterByParticipant = new Criterion("participantId", "=", participantContextId); + var allCriteria = new ArrayList<>(criteria); + allCriteria.add(filterByParticipant); return QuerySpec.Builder.newInstance() - .filter(criteria) + .filter(allCriteria) .build(); } } diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java index 83f8bc88d..e360ac8c2 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java @@ -14,11 +14,16 @@ package org.eclipse.edc.identityhub.core; +import org.eclipse.edc.identityhub.spi.KeyPairService; import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairState; +import org.eclipse.edc.identityhub.spi.model.ParticipantResource; import org.eclipse.edc.identitytrust.model.CredentialFormat; import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.query.Criterion; import java.util.HashMap; import java.util.List; @@ -29,23 +34,37 @@ public class PresentationCreatorRegistryImpl implements PresentationCreatorRegistry { private final Map> creators = new HashMap<>(); - private final Map keyIds = new HashMap<>(); + private final KeyPairService keyPairService; + + public PresentationCreatorRegistryImpl(KeyPairService keyPairService) { + this.keyPairService = keyPairService; + } @Override public void addCreator(PresentationGenerator creator, CredentialFormat format) { creators.put(format, creator); } + @SuppressWarnings("unchecked") @Override - public T createPresentation(List credentials, CredentialFormat format, Map additionalData) { + public T createPresentation(String participantContextId, List credentials, CredentialFormat format, Map additionalData) { var creator = ofNullable(creators.get(format)).orElseThrow(() -> new EdcException("No PresentationCreator was found for CredentialFormat %s".formatted(format))); - var keyId = ofNullable(keyIds.get(format)).orElseThrow(() -> new EdcException("No key ID was registered for CredentialFormat %s".formatted(format))); - return (T) creator.generatePresentation(credentials, keyId, additionalData); - } + var query = ParticipantResource.queryByParticipantId(participantContextId) + .filter(new Criterion("state", "=", KeyPairState.ACTIVE.code())) + .build(); - @Override - public void addKeyId(String keyId, CredentialFormat format) { - keyIds.put(format, keyId); + var keyPairResult = keyPairService.query(query) + .orElseThrow(f -> new EdcException("Error obtaining private key for participant '%s': %s".formatted(participantContextId, f.getFailureDetail()))); + + // check if there is a default key pair + var keyPair = keyPairResult.stream().filter(KeyPairResource::isDefaultPair).findAny() + .orElseGet(() -> keyPairResult.stream().findFirst().orElse(null)); + + if (keyPair == null) { + throw new EdcException("No active key pair found for participant '%s'".formatted(participantContextId)); + } + + return (T) creator.generatePresentation(credentials, keyPair.getKeyId(), additionalData); } } diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImpl.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImpl.java index 70179acb0..c07cafb1d 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImpl.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImpl.java @@ -54,13 +54,14 @@ public VerifiablePresentationServiceImpl(CredentialFormat defaultFormatVp, Prese * all JWT-VCs in the list will be packaged in a separate JWT VP, because LDP-VPs cannot contain JWT-VCs. * Note: submitting a {@link PresentationDefinition} is not supported at the moment, and it will be ignored after logging a warning. * + * @param participantContextId The ID of the {@link org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext} for which the VP is to be generated * @param credentials The list of verifiable credentials to include in the presentation. * @param presentationDefinition The optional presentation definition. Not supported at the moment! * @param audience The Participant ID of the entity who the VP is intended for. May be null for some VP formats. * @return A Result object wrapping the PresentationResponse. */ @Override - public Result createPresentation(List credentials, @Nullable PresentationDefinition presentationDefinition, @Nullable String audience) { + public Result createPresentation(String participantContextId, List credentials, @Nullable PresentationDefinition presentationDefinition, @Nullable String audience) { if (presentationDefinition != null) { monitor.warning("A PresentationDefinition was submitted, but is currently ignored by the generator."); @@ -72,24 +73,24 @@ public Result createPresentation(List(); - Map additionalData = audience != null ? Map.of("aud", "audience") : Map.of(); + Map additionalDataJwt = audience != null ? Map.of("aud", audience) : Map.of(); if (defaultFormatVp == JSON_LD) { // LDP-VPs cannot contain JWT VCs if (!ldpVcs.isEmpty()) { // todo: once we support PresentationDefinition, the types list could be dynamic - JsonObject ldpVp = registry.createPresentation(ldpVcs, CredentialFormat.JSON_LD, Map.of("types", List.of("VerifiablePresentation"))); + JsonObject ldpVp = registry.createPresentation(participantContextId, ldpVcs, CredentialFormat.JSON_LD, Map.of("types", List.of("VerifiablePresentation"))); vpToken.add(ldpVp); } if (!jwtVcs.isEmpty()) { monitor.warning("The VP was requested in %s format, but the request yielded %s JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs".formatted(JSON_LD, jwtVcs.size())); - String jwtVp = registry.createPresentation(jwtVcs, CredentialFormat.JWT, additionalData); + String jwtVp = registry.createPresentation(participantContextId, jwtVcs, CredentialFormat.JWT, additionalDataJwt); vpToken.add(jwtVp); } } else { //defaultFormatVp == JWT - vpToken.add(registry.createPresentation(credentials, CredentialFormat.JWT, additionalData)); + vpToken.add(registry.createPresentation(participantContextId, credentials, CredentialFormat.JWT, additionalDataJwt)); } var presentationResponse = PresentationResponseMessage.Builder.newinstance().presentation(vpToken).build(); diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImpl.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImpl.java index bae2acb86..3f0cfe32f 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImpl.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImpl.java @@ -29,6 +29,7 @@ import java.util.Objects; import java.util.function.Supplier; +import static com.nimbusds.jwt.JWTClaimNames.AUDIENCE; import static com.nimbusds.jwt.JWTClaimNames.SUBJECT; import static org.eclipse.edc.identityhub.DefaultServicesExtension.ACCESS_TOKEN_CLAIM; import static org.eclipse.edc.identityhub.DefaultServicesExtension.ACCESS_TOKEN_SCOPE_CLAIM; @@ -59,7 +60,8 @@ public AccessTokenVerifierImpl(TokenValidationService tokenValidationService, Su } @Override - public Result> verify(String token) { + public Result> verify(String token, String participantId) { + Objects.requireNonNull(participantId, "Participant ID is mandatory."); var res = tokenValidationService.validate(token, publicKeyResolver, tokenValidationRulesRegistry.getRules(IATP_SELF_ISSUED_TOKEN_CONTEXT)); if (res.failed()) { return res.mapTo(); @@ -69,6 +71,14 @@ public Result> verify(String token) { var accessTokenString = claimToken.getStringClaim(ACCESS_TOKEN_CLAIM); var subClaim = claimToken.getStringClaim(SUBJECT); + TokenValidationRule audMustMatchParticipantIdRule = (at, additional) -> { + var aud = at.getListClaim(AUDIENCE); + if (aud == null || aud.isEmpty()) { + return Result.failure("Mandatory claim 'aud' on 'access_token' was null."); + } + return aud.contains(participantId) ? Result.success() : Result.failure("Participant Context ID must match 'aud' claim in 'access_token'"); + }; + TokenValidationRule subClaimsMatch = (at, additional) -> { var atSub = at.getStringClaim(SUBJECT); // correlate sub and access_token.sub @@ -82,6 +92,7 @@ public Result> verify(String token) { // verify the correctness of the 'access_token' var rules = new ArrayList<>(tokenValidationRulesRegistry.getRules(IATP_ACCESS_TOKEN_CONTEXT)); rules.add(subClaimsMatch); + rules.add(audMustMatchParticipantIdRule); var result = tokenValidationService.validate(accessTokenString, id -> Result.success(stsPublicKey.get()), rules); if (result.failed()) { return result.mapTo(); diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java index ec2d4e7ab..28a002f3b 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/CredentialQueryResolverImplTest.java @@ -46,14 +46,15 @@ class CredentialQueryResolverImplTest { + public static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant"; private final CredentialStore storeMock = mock(); private final CredentialQueryResolverImpl resolver = new CredentialQueryResolverImpl(storeMock, new EdcScopeToCriterionTransformer()); @Test void query_noResult() { when(storeMock.query(any())).thenAnswer(i -> success(List.of())); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.succeeded()).isTrue(); assertThat(res.getContent()).isEmpty(); } @@ -61,7 +62,7 @@ void query_noResult() { @Test void query_noProverScope_shouldReturnEmpty() { when(storeMock.query(any())).thenReturn(success(Collections.emptyList())); - var res = resolver.query(createPresentationQuery(), List.of("foobar")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery(), List.of("foobar")); assertThat(res.succeeded()).isFalse(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).contains("Invalid query: must contain at least one scope."); @@ -70,8 +71,8 @@ void query_noProverScope_shouldReturnEmpty() { @Test void query_proverScopeStringInvalid_shouldReturnFailure() { when(storeMock.query(any())).thenReturn(success(Collections.emptyList())); - var res = resolver.query(createPresentationQuery("invalid"), - List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("invalid"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).contains("Scope string has invalid format."); @@ -79,7 +80,7 @@ void query_proverScopeStringInvalid_shouldReturnFailure() { @Test void query_scopeStringHasWrongOperator_shouldReturnFailure() { - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:write"), List.of("ignored")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.INVALID_SCOPE); assertThat(res.getFailureDetail()).contains("Invalid scope operation: write"); @@ -89,21 +90,31 @@ void query_scopeStringHasWrongOperator_shouldReturnFailure() { void query_singleScopeString() { var credential = createCredentialResource("TestCredential"); when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:TestCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read")); assertThat(res.succeeded()).withFailMessage(res::getFailureDetail).isTrue(); assertThat(res.getContent()).containsExactly(credential.getVerifiableCredential()); } + @Test + void query_whenParticipantIdMismatch_expectEmptyResult() { + when(storeMock.query(any())).thenAnswer(i -> success(List.of())); + + var res = resolver.query("another_participant_id", + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read")); + assertThat(res.succeeded()).isTrue(); + assertThat(res.getContent()).isEmpty(); + } + @Test void query_multipleScopeStrings() { var credential1 = createCredentialResource("TestCredential"); var credential2 = createCredentialResource("AnotherCredential"); when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1, credential2))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", - "org.eclipse.edc.vc.type:AnotherCredential:read"), - List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", + "org.eclipse.edc.vc.type:AnotherCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.succeeded()).withFailMessage(res::getFailureDetail).isTrue(); assertThat(res.getContent()).containsExactlyInAnyOrder(credential1.getVerifiableCredential(), credential2.getVerifiableCredential()); } @@ -111,7 +122,7 @@ void query_multipleScopeStrings() { @Test void query_presentationDefinition_unsupported() { var q = PresentationQueryMessage.Builder.newinstance().presentationDefinition(PresentationDefinition.Builder.newInstance().id("test-pd").build()).build(); - assertThatThrownBy(() -> resolver.query(q, List.of("org.eclipse.edc.vc.type:SomeCredential:read"))) + assertThatThrownBy(() -> resolver.query(TEST_PARTICIPANT_CONTEXT_ID, q, List.of("org.eclipse.edc.vc.type:SomeCredential:read"))) .isInstanceOf(UnsupportedOperationException.class) .hasMessage("Querying with a DIF Presentation Exchange definition is not yet supported."); } @@ -123,9 +134,9 @@ void query_requestsTooManyCredentials_shouldReturnFailure() { when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1, credential2))) .thenAnswer(i -> success(List.of(credential1))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", - "org.eclipse.edc.vc.type:AnotherCredential:read"), - List.of("org.eclipse.edc.vc.type:TestCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", + "org.eclipse.edc.vc.type:AnotherCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); @@ -137,8 +148,8 @@ void query_moreCredentialsAllowed_shouldReturnOnlyRequested() { var credential1 = createCredentialResource("TestCredential"); when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.succeeded()).isTrue(); assertThat(res.getContent()).containsOnly(credential1.getVerifiableCredential()); @@ -149,8 +160,8 @@ void query_exactMatchAllowedAndRequestedCredentials() { var credential1 = createCredentialResource("TestCredential"); when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:TestCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:TestCredential:read")); assertThat(res.succeeded()).isTrue(); assertThat(res.getContent()).containsOnly(credential1.getVerifiableCredential()); @@ -163,8 +174,8 @@ void query_requestedCredentialNotAllowed() { when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1))) .thenAnswer(i -> success(List.of(credential2))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); @@ -180,8 +191,8 @@ void query_sameSizeDifferentScope() { when(storeMock.query(any())).thenAnswer(i -> success(List.of(credential1, credential2))) .thenAnswer(i -> success(List.of(credential3, credential4))); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read"), - List.of("org.eclipse.edc.vc.type:FooCredential:read", "org.eclipse.edc.vc.type:BarCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read", "org.eclipse.edc.vc.type:AnotherCredential:read"), List.of("org.eclipse.edc.vc.type:FooCredential:read", "org.eclipse.edc.vc.type:BarCredential:read")); assertThat(res.succeeded()).isFalse(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.UNAUTHORIZED_SCOPE); @@ -193,8 +204,8 @@ void query_storeReturnsFailure() { var credential1 = createCredentialResource("TestCredential"); when(storeMock.query(any())).thenReturn(StoreResult.notFound("test-failure")); - var res = resolver.query(createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), - List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); + var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID, + createPresentationQuery("org.eclipse.edc.vc.type:TestCredential:read"), List.of("org.eclipse.edc.vc.type:AnotherCredential:read")); assertThat(res.failed()).isTrue(); assertThat(res.reason()).isEqualTo(QueryFailure.Reason.STORAGE_FAILURE); @@ -217,7 +228,7 @@ private VerifiableCredentialResource createCredentialResource(String... type) { .credential(new VerifiableCredentialContainer("foobar", CredentialFormat.JSON_LD, cred)) .holderId("test-holder") .issuerId("test-issuer") - .participantId("test-participant") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) .build(); } } \ No newline at end of file diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java new file mode 100644 index 000000000..046eb00c7 --- /dev/null +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2024 Metaform Systems, Inc. + * + * 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: + * Metaform Systems, Inc. - initial API and implementation + * + */ + +package org.eclipse.edc.identityhub.core; + +import org.eclipse.edc.identityhub.spi.KeyPairService; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.KeyPairResource; +import org.eclipse.edc.identityhub.spi.model.KeyPairState; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.result.ServiceResult; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyMap; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@SuppressWarnings("unchecked") // mocking a generic type (PresentationGenerator) would raise warnings +class PresentationCreatorRegistryImplTest { + + private static final String TEST_PARTICIPANT = "test-participant"; + private final KeyPairService keyPairService = mock(); + private final PresentationCreatorRegistryImpl registry = new PresentationCreatorRegistryImpl(keyPairService); + + @Test + void createPresentation_whenSingleKey() { + var keyPair = createKeyPair(TEST_PARTICIPANT, "key-1").build(); + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of(keyPair))); + + var generator = mock(PresentationGenerator.class); + registry.addCreator(generator, CredentialFormat.JWT); + assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); + verify(generator).generatePresentation(anyList(), eq(keyPair.getKeyId()), anyMap()); + } + + @Test + void createPresentation_whenKeyPairServiceReturnsFailure() { + when(keyPairService.query(any())).thenReturn(ServiceResult.notFound("foobar")); + var generator = mock(PresentationGenerator.class); + registry.addCreator(generator, CredentialFormat.JWT); + + assertThatThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())) + .isInstanceOf(EdcException.class) + .hasMessage("Error obtaining private key for participant 'test-participant': foobar"); + verifyNoInteractions(generator); + } + + @Test + void createPresentation_whenNoDefaultKey() { + var keyPair1 = createKeyPair(TEST_PARTICIPANT, "key-1").isDefaultPair(false).build(); + var keyPair2 = createKeyPair(TEST_PARTICIPANT, "key-2").isDefaultPair(false).build(); + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of(keyPair1, keyPair2))); + + var generator = mock(PresentationGenerator.class); + registry.addCreator(generator, CredentialFormat.JWT); + assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); + verify(generator).generatePresentation(anyList(), argThat(s -> s.equals("key-1") || s.equals("key-2")), anyMap()); + } + + + @Test + void createPresentation_whenDefaultKey() { + var keyPair1 = createKeyPair(TEST_PARTICIPANT, "key-1").isDefaultPair(false).build(); + var keyPair2 = createKeyPair(TEST_PARTICIPANT, "key-2").isDefaultPair(true).build(); + var keyPair3 = createKeyPair(TEST_PARTICIPANT, "key-3").isDefaultPair(false).build(); + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of(keyPair1, keyPair2, keyPair3))); + + var generator = mock(PresentationGenerator.class); + registry.addCreator(generator, CredentialFormat.JWT); + assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); + verify(generator).generatePresentation(anyList(), eq("key-2"), anyMap()); + } + + @Test + void createPresentation_whenNoActiveKey() { + when(keyPairService.query(any())).thenReturn(ServiceResult.success(List.of())); + + var generator = mock(PresentationGenerator.class); + registry.addCreator(generator, CredentialFormat.JWT); + assertThatThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())) + .isInstanceOf(EdcException.class) + .hasMessage("No active key pair found for participant 'test-participant'"); + verifyNoInteractions(generator); + } + + private KeyPairResource.Builder createKeyPair(String participantId, String keyId) { + return KeyPairResource.Builder.newInstance() + .id(UUID.randomUUID().toString()) + .keyId(keyId) + .state(KeyPairState.ACTIVE) + .isDefaultPair(true) + .privateKeyAlias(participantId + "-alias"); + } +} \ No newline at end of file diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java index 0ea5c9bf1..f2b15869e 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java @@ -50,109 +50,109 @@ class VerifiablePresentationServiceImplTest { + private static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant"; private final Monitor monitor = mock(); private final PresentationCreatorRegistry registry = mock(); private final ObjectMapper mapper = JacksonJsonLd.createObjectMapper(); private VerifiablePresentationServiceImpl presentationGenerator; - @Test void generate_noCredentials() { - when(registry.createPresentation(anyList(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), anyList(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); presentationGenerator = new VerifiablePresentationServiceImpl(JSON_LD, registry, monitor); List ldpVcs = List.of(); - var result = presentationGenerator.createPresentation(ldpVcs, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, ldpVcs, null, null); assertThat(result).isSucceeded().matches(pr -> pr.getPresentation().isEmpty(), "VP Tokens should be empty"); } @Test void generate_defaultFormatLdp_containsOnlyLdpVc() { - when(registry.createPresentation(any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); presentationGenerator = new VerifiablePresentationServiceImpl(JSON_LD, registry, monitor); var credentials = List.of(createCredential(JSON_LD), createCredential(JSON_LD)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JSON_LD), any()); } @Test void generate_defaultFormatLdp_mixedVcs() { - when(registry.createPresentation(any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); - when(registry.createPresentation(any(), eq(JWT), any())).thenReturn(JWT_VP); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JWT), any())).thenReturn(JWT_VP); presentationGenerator = new VerifiablePresentationServiceImpl(JSON_LD, registry, monitor); var credentials = List.of(createCredential(JSON_LD), createCredential(JWT)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 1), eq(JWT), any()); - verify(registry).createPresentation(argThat(argument -> argument.size() == 1), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 1), eq(JWT), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 1), eq(JSON_LD), any()); verify(monitor).warning(eq("The VP was requested in JSON_LD format, but the request yielded 1 JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs")); } @Test void generate_defaultFormatLdp_onlyJwtVcs() { - when(registry.createPresentation(any(), eq(JWT), any())).thenReturn(JWT_VP); - when(registry.createPresentation(any(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JWT), any())).thenReturn(JWT_VP); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); presentationGenerator = new VerifiablePresentationServiceImpl(JSON_LD, registry, monitor); var credentials = List.of(createCredential(JWT), createCredential(JWT)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT), any()); - verify(registry, never()).createPresentation(any(), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JWT), any()); + verify(registry, never()).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any()); verify(monitor).warning(eq("The VP was requested in JSON_LD format, but the request yielded 2 JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs")); } @Test void generate_defaultFormatJwt_onlyJwtVcs() { - when(registry.createPresentation(any(), eq(JWT), any())).thenReturn(JWT_VP); - when(registry.createPresentation(any(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JWT), any())).thenReturn(JWT_VP); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any())).thenReturn(jsonObject(EMPTY_LDP_VP)); presentationGenerator = new VerifiablePresentationServiceImpl(JWT, registry, monitor); var credentials = List.of(createCredential(JWT), createCredential(JWT)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT), any()); - verify(registry, never()).createPresentation(any(), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JWT), any()); + verify(registry, never()).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any()); } @Test void generate_defaultFormatJwt_mixedVcs() { - when(registry.createPresentation(any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); - when(registry.createPresentation(any(), eq(JWT), any())).thenReturn(JWT_VP); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any())).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JWT), any())).thenReturn(JWT_VP); presentationGenerator = new VerifiablePresentationServiceImpl(JWT, registry, monitor); var credentials = List.of(createCredential(JSON_LD), createCredential(JWT)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT), any()); - verify(registry, never()).createPresentation(any(), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JWT), any()); + verify(registry, never()).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any()); } @Test void generate_defaultFormatJwt_onlyLdpVc() { - when(registry.createPresentation(any(), eq(JWT), any())).thenReturn(JWT_VP); + when(registry.createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JWT), any())).thenReturn(JWT_VP); presentationGenerator = new VerifiablePresentationServiceImpl(JWT, registry, monitor); var credentials = List.of(createCredential(JSON_LD), createCredential(JSON_LD)); - var result = presentationGenerator.createPresentation(credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT), any()); - verify(registry, never()).createPresentation(any(), eq(JSON_LD), any()); + verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JWT), any()); + verify(registry, never()).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any()); } @Test void generate_withPresentationDef_shouldLogWarning() { presentationGenerator = new VerifiablePresentationServiceImpl(JSON_LD, registry, monitor); - presentationGenerator.createPresentation(List.of(), PresentationDefinition.Builder.newInstance().id("test-id").build(), null); + presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, List.of(), PresentationDefinition.Builder.newInstance().id("test-id").build(), null); verify(monitor).warning(contains("A PresentationDefinition was submitted, but is currently ignored by the generator.")); } diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplComponentTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplComponentTest.java index bee227d90..4b2f345c7 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplComponentTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplComponentTest.java @@ -81,7 +81,7 @@ void selfIssuedTokenNotVerified() { var spoofedKey = generator.generateKeyPair().getPrivate(); var selfIssuedIdToken = createSignedJwt(spoofedKey, new JWTClaimsSet.Builder().claim("foo", "bar").jwtID(UUID.randomUUID().toString()).build()); - assertThat(verifier.verify(selfIssuedIdToken)).isFailed() + assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed() .detail().isEqualTo("Token verification failed"); } @@ -89,35 +89,68 @@ void selfIssuedTokenNotVerified() { @Test void selfIssuedToken_noAccessTokenClaim() { var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder()/* missing: claims("access_token", "....") */.build()); - assertThat(verifier.verify(selfIssuedIdToken)).isFailed() + assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed() .detail().isEqualTo("Required claim 'access_token' not present on token."); } + @Test + void selfIssuedToken_noAccessTokenAudienceClaim() { + var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder() + .claim("scope", "foobar") + .build()); + var selfIssuedIdToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken) + .build()); + assertThat(verifier.verify(selfIssuedIdToken, "did:web:test_participant")).isFailed() + .detail().isEqualTo("Mandatory claim 'aud' on 'access_token' was null."); + } + @Test void accessToken_notVerified() { var spoofedKey = generator.generateKeyPair().getPrivate(); var accessToken = createSignedJwt(spoofedKey, new JWTClaimsSet.Builder().claim("scope", "foobar").claim("foo", "bar").build()); var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken).build()); - assertThat(verifier.verify(siToken)).isFailed() + assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed() .detail().isEqualTo("Token verification failed"); } @Test void accessToken_noScopeClaim() { - var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()/* missing: .claim("scope", "foobar") */.claim("foo", "bar").build()); - var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken).build()); - - assertThat(verifier.verify(siToken)).isFailed() + var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder()/* missing: .claim("scope", "foobar") */ + .claim("foo", "bar") + .audience("did:web:test_participant") + .build()); + var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken) + .build()); + + assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed() .detail().isEqualTo("Required claim 'scope' not present on token."); } + @Test + void accessToken_noAudClaim() { + var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder() + .claim("scope", "foobar") + .claim("foo", "bar") + /*missin: .audience("did:web:test_participant") */ + .build()); + var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken) + .build()); + + assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed() + .detail().isEqualTo("Mandatory claim 'aud' on 'access_token' was null."); + } + @Test void assertWarning_whenSubjectClaimsMismatch() { - var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("scope", "foobar").subject("test-subject").build()); + var accessToken = createSignedJwt(stsKeyPair.getPrivate(), new JWTClaimsSet.Builder() + .claim("scope", "foobar") + .audience("did:web:test_participant") + .subject("test-subject") + .build()); var siToken = createSignedJwt(providerKeyPair.getPrivate(), new JWTClaimsSet.Builder().claim("access_token", accessToken).subject("mismatching-subject").build()); - assertThat(verifier.verify(siToken)).isSucceeded(); + assertThat(verifier.verify(siToken, "did:web:test_participant")).isSucceeded(); verify(monitor).warning(startsWith("ID token [sub] claim is not equal to [access_token.sub]")); } diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplTest.java index 1857db176..77b093151 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/token/verification/AccessTokenVerifierImplTest.java @@ -59,7 +59,7 @@ class AccessTokenVerifierImplTest { void verify_validSiToken_validAccessToken() { when(tokenValidationSerivce.validate(anyString(), any(), anyList())) .thenReturn(Result.success(idToken)); - assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID))) + assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")) .isSucceeded() .satisfies(strings -> Assertions.assertThat(strings).containsOnly(TEST_SCOPE)); verify(tokenValidationSerivce, times(2)).validate(anyString(), any(PublicKeyResolver.class), anyList()); @@ -70,7 +70,7 @@ void verify_validSiToken_validAccessToken() { void verify_siTokenValidationFails() { when(tokenValidationSerivce.validate(anyString(), any(), anyList())) .thenReturn(Result.failure("test-failure")); - assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID))).isFailed() + assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed() .detail().contains("test-failure"); } @@ -79,19 +79,19 @@ void verify_noAccessTokenClaim() { when(tokenValidationSerivce.validate(anyString(), any(PublicKeyResolver.class), anyList())) .thenReturn(Result.failure("no access token")); - assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID))).isFailed() + assertThat(verifier.verify(generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed() .detail().contains("no access token"); verify(tokenValidationSerivce).validate(anyString(), any(PublicKeyResolver.class), anyList()); } @Test void verify_accessTokenValidationFails() { - var spoofedKey = generateEcKey(); + var spoofedKey = generateEcKey("spoofed-key"); var accessToken = generateJwt(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, Map.of("scope", TEST_SCOPE), spoofedKey); var siToken = generateJwt(OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID, Map.of("client_id", OTHER_PARTICIPANT_DID, "access_token", accessToken), PROVIDER_KEY); when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.failure("test-failure")); - assertThat(verifier.verify(siToken)).isFailed() + assertThat(verifier.verify(siToken, "did:web:test_participant")).isFailed() .detail().isEqualTo("test-failure"); } @@ -108,7 +108,7 @@ void verify_accessTokenDoesNotContainScopeClaim() { when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.success(idToken)); when(tokenValidationSerivce.validate(anyString(), any(), anyList())).thenReturn(Result.failure("test-failure")); - assertThat(verifier.verify(siToken)) + assertThat(verifier.verify(siToken, "did:web:test_participant")) .isFailed() .detail().contains("test-failure"); } diff --git a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiComponentTest.java similarity index 63% rename from e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java rename to e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiComponentTest.java index 2280f3c81..430763cc9 100644 --- a/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/ResolutionApiComponentTest.java +++ b/e2e-tests/api-tests/src/test/java/org/eclipse/edc/identityhub/tests/PresentationApiComponentTest.java @@ -14,9 +14,14 @@ package org.eclipse.edc.identityhub.tests; -import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.JOSEException; import jakarta.json.JsonObject; +import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver; +import org.eclipse.edc.identityhub.junit.testfixtures.JwtCreationUtil; +import org.eclipse.edc.identityhub.spi.ParticipantContextService; import org.eclipse.edc.identityhub.spi.generator.VerifiablePresentationService; +import org.eclipse.edc.identityhub.spi.model.participant.KeyDescriptor; +import org.eclipse.edc.identityhub.spi.model.participant.ParticipantManifest; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.resolution.QueryResult; import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; @@ -27,29 +32,31 @@ import org.eclipse.edc.identitytrust.model.credentialservice.PresentationSubmission; import org.eclipse.edc.junit.annotations.ComponentTest; import org.eclipse.edc.junit.extensions.EdcRuntimeExtension; +import org.eclipse.edc.spi.result.Result; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; import org.mockito.ArgumentMatchers; import java.util.List; +import java.util.Map; import java.util.stream.Stream; import static io.restassured.http.ContentType.JSON; import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION; import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.identityhub.junit.testfixtures.JwtCreationUtil.generateSiToken; -import static org.eclipse.edc.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey; import static org.eclipse.edc.spi.result.Result.failure; import static org.eclipse.edc.spi.result.Result.success; import static org.hamcrest.Matchers.equalTo; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @ComponentTest -public class ResolutionApiComponentTest { +public class PresentationApiComponentTest { public static final String VALID_QUERY_WITH_SCOPE = """ { "@context": [ @@ -66,10 +73,12 @@ public class ResolutionApiComponentTest { .name("identity-hub") .id("identity-hub") .build(); + private static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant"; // todo: these mocks should be replaced, once their respective implementations exist! private static final CredentialQueryResolver CREDENTIAL_QUERY_RESOLVER = mock(); private static final VerifiablePresentationService PRESENTATION_GENERATOR = mock(); private static final AccessTokenVerifier ACCESS_TOKEN_VERIFIER = mock(); + private static final DidPublicKeyResolver DID_PUBLIC_KEY_RESOLVER = mock(); @RegisterExtension static EdcRuntimeExtension runtime; @@ -79,16 +88,16 @@ public class ResolutionApiComponentTest { runtime.registerServiceMock(CredentialQueryResolver.class, CREDENTIAL_QUERY_RESOLVER); runtime.registerServiceMock(VerifiablePresentationService.class, PRESENTATION_GENERATOR); runtime.registerServiceMock(AccessTokenVerifier.class, ACCESS_TOKEN_VERIFIER); + runtime.registerServiceMock(DidPublicKeyResolver.class, DID_PUBLIC_KEY_RESOLVER); } - private final ECKey consumerKey = generateEcKey(); - private final ECKey providerKey = generateEcKey(); @Test void query_tokenNotPresent_shouldReturn401() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType("application/json") - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(401) .extract().body().asString(); @@ -96,10 +105,11 @@ void query_tokenNotPresent_shouldReturn401() { @Test void query_validationError_shouldReturn400() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var query = """ { "@context": [ - "https://identity.foundation/presentation-exchange/submission/v1", + "https://identity.foundation/participants/test-participant/presentation-exchange/submission/v1", "https://w3id.org/tractusx-trust/v0.8" ], "@type": "PresentationQueryMessage" @@ -109,7 +119,7 @@ void query_validationError_shouldReturn400() { .contentType(JSON) .header(AUTHORIZATION, generateSiToken()) .body(query) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(400) .extract().body().asString(); @@ -118,6 +128,7 @@ void query_validationError_shouldReturn400() { @Test void query_withPresentationDefinition_shouldReturn503() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var query = """ { "@context": [ @@ -133,7 +144,7 @@ void query_withPresentationDefinition_shouldReturn503() { .contentType(JSON) .header(AUTHORIZATION, generateSiToken()) .body(query) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(503) .extract().body().asString(); @@ -142,13 +153,14 @@ void query_withPresentationDefinition_shouldReturn503() { @Test void query_tokenVerificationFails_shouldReturn401() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var token = generateSiToken(); - when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(failure("token not verified")); + when(ACCESS_TOKEN_VERIFIER.verify(eq(token), anyString())).thenReturn(failure("token not verified")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType(JSON) .header(AUTHORIZATION, token) .body(VALID_QUERY_WITH_SCOPE) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(401) .log().ifValidationFails() @@ -158,15 +170,16 @@ void query_tokenVerificationFails_shouldReturn401() { @Test void query_queryResolutionFails_shouldReturn403() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var token = generateSiToken(); - when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.unauthorized("scope mismatch!")); + when(ACCESS_TOKEN_VERIFIER.verify(eq(token), anyString())).thenReturn(success(List.of("test-scope1"))); + when(CREDENTIAL_QUERY_RESOLVER.query(anyString(), any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.unauthorized("scope mismatch!")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType(JSON) .header(AUTHORIZATION, token) .body(VALID_QUERY_WITH_SCOPE) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(403) .log().ifValidationFails() @@ -176,39 +189,46 @@ void query_queryResolutionFails_shouldReturn403() { @Test void query_presentationGenerationFails_shouldReturn500() { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var token = generateSiToken(); - when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); - when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null), any())).thenReturn(failure("generator test error")); + when(ACCESS_TOKEN_VERIFIER.verify(eq(token), anyString())).thenReturn(success(List.of("test-scope1"))); + when(CREDENTIAL_QUERY_RESOLVER.query(anyString(), any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); + when(PRESENTATION_GENERATOR.createPresentation(anyString(), anyList(), eq(null), any())).thenReturn(failure("generator test error")); IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType(JSON) .header(AUTHORIZATION, token) .body(VALID_QUERY_WITH_SCOPE) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(500) .log().ifValidationFails(); } @Test - void query_success() { + void query_success() throws JOSEException { + createParticipant(TEST_PARTICIPANT_CONTEXT_ID); var token = generateSiToken(); - when(ACCESS_TOKEN_VERIFIER.verify(eq(token))).thenReturn(success(List.of("test-scope1"))); - when(CREDENTIAL_QUERY_RESOLVER.query(any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); - when(PRESENTATION_GENERATOR.createPresentation(anyList(), eq(null), any())).thenReturn(success(createPresentationResponse())); + when(ACCESS_TOKEN_VERIFIER.verify(eq(token), anyString())).thenReturn(success(List.of("test-scope1"))); + when(CREDENTIAL_QUERY_RESOLVER.query(anyString(), any(), ArgumentMatchers.anyList())).thenReturn(QueryResult.success(Stream.empty())); + when(PRESENTATION_GENERATOR.createPresentation(anyString(), anyList(), eq(null), any())).thenReturn(success(createPresentationResponse())); + + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:consumer#key1"))).thenReturn(Result.success(JwtCreationUtil.CONSUMER_KEY.toPublicKey())); + when(DID_PUBLIC_KEY_RESOLVER.resolveKey(eq("did:web:provider#key1"))).thenReturn(Result.success(JwtCreationUtil.PROVIDER_KEY.toPublicKey())); var response = IDENTITY_HUB_PARTICIPANT.getResolutionEndpoint().baseRequest() .contentType(JSON) .header(AUTHORIZATION, token) .body(VALID_QUERY_WITH_SCOPE) - .post("/presentation/query") + .post("/participants/test-participant/presentation/query") .then() .statusCode(200) .log().ifValidationFails() .extract().body().as(JsonObject.class); - assertThat(response).containsKeys("presentation", "@context"); + assertThat(response) + .hasEntrySatisfying("type", jsonValue -> assertThat(jsonValue.toString()).contains("PresentationResponseMessage")) + .hasEntrySatisfying("@context", jsonValue -> assertThat(jsonValue.asJsonArray()).hasSize(2)); } @@ -220,5 +240,20 @@ private PresentationResponseMessage createPresentationResponse() { .build(); } + private void createParticipant(String participantId) { + var service = runtime.getContext().getService(ParticipantContextService.class); + var manifest = ParticipantManifest.Builder.newInstance() + .participantId(participantId) + .did("did:web:%s".formatted(participantId)) + .active(true) + .key(KeyDescriptor.Builder.newInstance() + .keyGeneratorParams(Map.of("algorithm", "EC")) + .privateKeyAlias("%s-privatekey-alias".formatted(participantId)) + .keyId("key-1") + .build()) + .build(); + service.createParticipantContext(manifest) + .orElseThrow(f -> new RuntimeException(f.getFailureDetail())); + } } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java index 7879fcfb9..8ba045970 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java @@ -35,22 +35,15 @@ public interface PresentationCreatorRegistry { * Creates a VerifiablePresentation based on a list of verifiable credentials and a credential format. How the presentation will be represented * depends on the format. JWT-VPs will be represented as {@link String}, LDP-VPs will be represented as {@link jakarta.json.JsonObject}. * - * @param The type of the presentation. Can be {@link String}, when format is {@link CredentialFormat#JWT}, or {@link jakarta.json.JsonObject}, - * when the format is {@link CredentialFormat#JSON_LD} - * @param credentials The list of verifiable credentials to include in the presentation. - * @param format The format for the presentation. - * @param additionalData Optional additional data that might be required to create the presentation, such as types, etc. + * @param The type of the presentation. Can be {@link String}, when format is {@link CredentialFormat#JWT}, or {@link jakarta.json.JsonObject}, + * when the format is {@link CredentialFormat#JSON_LD} + * @param participantContextId The ID of the {@link org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext} who creates the VP + * @param credentials The list of verifiable credentials to include in the presentation. + * @param format The format for the presentation. + * @param additionalData Optional additional data that might be required to create the presentation, such as types, etc. * @return The created presentation. * @throws IllegalArgumentException if the credential cannot be represented in the desired format. For example, LDP-VPs cannot contain JWT-VCs. * @throws org.eclipse.edc.spi.EdcException if no creator is registered for a particular format */ - T createPresentation(List credentials, CredentialFormat format, Map additionalData); - - /** - * Specify, which key ID is to be used for which {@link CredentialFormat}. It is recommended to use a separate key for every format. - * - * @param keyId the Key ID of the private key. Typically, the related public key has to be resolvable through a public method, e.g. DID:WEB - * @param format the {@link CredentialFormat} for which the key should be used. - */ - void addKeyId(String keyId, CredentialFormat format); + T createPresentation(String participantContextId, List credentials, CredentialFormat format, Map additionalData); } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/VerifiablePresentationService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/VerifiablePresentationService.java index cc13d601e..79e81041b 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/VerifiablePresentationService.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/VerifiablePresentationService.java @@ -31,10 +31,11 @@ public interface VerifiablePresentationService { /** * Creates a presentation based on a list of verifiable credentials and an optional presentation definition. * + * @param participantContextId The ID or the {@link org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext} for whom a VerifiablePresentation is to be created * @param credentials The list of verifiable credentials to include in the presentation. * @param presentationDefinition The optional presentation definition. * @param audience The Participant ID of the party who the presentation is intended for. May not be relevant for all VP formats * @return A Result object containing a PresentationResponse if the presentation creation is successful, or a failure message if it fails. */ - Result createPresentation(List credentials, @Nullable PresentationDefinition presentationDefinition, @Nullable String audience); + Result createPresentation(String participantContextId, List credentials, @Nullable PresentationDefinition presentationDefinition, @Nullable String audience); } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java index 2a9271fa1..036bd608f 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/ParticipantResource.java @@ -15,6 +15,8 @@ package org.eclipse.edc.identityhub.spi.model; import org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext; +import org.eclipse.edc.spi.query.Criterion; +import org.eclipse.edc.spi.query.QuerySpec; /** * This is the base class for all resources that are owned by a {@link ParticipantContext}. @@ -25,10 +27,15 @@ public abstract class ParticipantResource { /** * The {@link ParticipantContext} that this resource belongs to. */ + public String getParticipantId() { return participantId; } + public static QuerySpec.Builder queryByParticipantId(String participantId) { + return QuerySpec.Builder.newInstance().filter(new Criterion("participantId", "=", participantId)); + } + public abstract static class Builder> { protected final T entity; diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java index a7e7fa03b..0085a84d6 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/resolution/CredentialQueryResolver.java @@ -29,8 +29,9 @@ public interface CredentialQueryResolver { * If a failure is returned, that means that the given query does not match the given issuer scopes, which would be equivalent to an unauthorized access (c.f. HTTP 403 error). * The Result could also contain information about any errors or issues the occurred during the query execution. * - * @param query The representation of the query to be executed. - * @param issuerScopes The list of issuer scopes to be considered during the query processing. + * @param participantContextId The ID of the {@link org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext} whose credentials are to be obtained. + * @param query The representation of the query to be executed. + * @param issuerScopes The list of issuer scopes to be considered during the query processing. */ - QueryResult query(PresentationQueryMessage query, List issuerScopes); + QueryResult query(String participantContextId, PresentationQueryMessage query, List issuerScopes); } \ No newline at end of file diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java index 539e63feb..a96d1e1a3 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/verification/AccessTokenVerifier.java @@ -32,8 +32,9 @@ public interface AccessTokenVerifier { *
  • that the access_token contains >1 scope strings
  • * * - * @param token The token to be verified. Must be a JWT in base64 encoding. + * @param token The token to be verified. Must be a JWT in base64 encoding. + * @param participantId The ID of the {@link org.eclipse.edc.identityhub.spi.model.participant.ParticipantContext} who is supposed to present their credentials * @return A {@code Result} containing a {@code List} of scope strings. */ - Result> verify(String token); + Result> verify(String token, String participantId); } diff --git a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/JwtCreationUtil.java b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/JwtCreationUtil.java index 1c697048c..34bf24d61 100644 --- a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/JwtCreationUtil.java +++ b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/JwtCreationUtil.java @@ -30,8 +30,8 @@ */ public class JwtCreationUtil { public static final String TEST_SCOPE = "org.eclipse.edc.vc.type:SomeTestCredential:read"; - public static final ECKey CONSUMER_KEY = generateEcKey(); - public static final ECKey PROVIDER_KEY = generateEcKey(); + public static final ECKey CONSUMER_KEY = generateEcKey("did:web:consumer#key1"); + public static final ECKey PROVIDER_KEY = generateEcKey("did:web:provider#key1"); /** * Generates a self-issued token. diff --git a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java index 7f850539d..4cb0c67aa 100644 --- a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java +++ b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/edc/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java @@ -38,9 +38,9 @@ private VerifiableCredentialTestUtil() { } - public static ECKey generateEcKey() { + public static ECKey generateEcKey(String kid) { try { - return EC_KEY_GENERATOR.keyUse(KeyUse.SIGNATURE).generate(); + return EC_KEY_GENERATOR.keyUse(KeyUse.SIGNATURE).keyID(kid).generate(); } catch (JOSEException e) { throw new RuntimeException(e); } @@ -49,7 +49,9 @@ public static ECKey generateEcKey() { public static SignedJWT buildSignedJwt(JWTClaimsSet claims, ECKey jwk) { try { - var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256).build(); + var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256) + .keyID(jwk.getKeyID()) + .build(); var jws = new SignedJWT(jwsHeader, claims); var jwsSigner = new ECDSASigner(jwk.toECPrivateKey()); diff --git a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java index 9d6f5e4a9..e4fec8326 100644 --- a/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java +++ b/spi/identity-hub-store-spi/src/testFixtures/java/org/eclipse/edc/identityhub/store/test/CredentialStoreTestBase.java @@ -30,6 +30,7 @@ import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Map; import static java.util.stream.IntStream.range; @@ -63,6 +64,7 @@ public abstract class CredentialStoreTestBase { } } """; + public static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant"; private static final String EXAMPLE_VC = """ { "@context": [ @@ -139,6 +141,30 @@ void query_byParticipantId() { .satisfies(str -> Assertions.assertThat(str).hasSize(1)); } + @Test + void query_byParticipantIdAndType() { + var cred1 = createCredentialBuilder() + .credential(new VerifiableCredentialContainer(EXAMPLE_VC, CredentialFormat.JSON_LD, createVerifiableCredential() + .type("UniversityDegreeCredential") + .build())) + .participantId(TEST_PARTICIPANT_CONTEXT_ID).build(); + var cred2 = createCredentialBuilder().participantId("participant-context2").build(); + var cred3 = createCredentialBuilder().participantId("participant-context3").build(); + + Arrays.asList(cred1, cred2, cred3).forEach(getStore()::create); + + var query = QuerySpec.Builder.newInstance() + .filter(new Criterion("participantId", "=", TEST_PARTICIPANT_CONTEXT_ID)) + .filter(new Criterion("verifiableCredential.credential.types", "contains", "UniversityDegreeCredential")) + .build(); + + var result = getStore().query(query); + assertThat(result).isSucceeded() + .satisfies(resources -> { + Assertions.assertThat(resources).hasSize(1); + }); + } + @Test void query_byVcState() { var creds = createCredentials(); @@ -176,6 +202,7 @@ void query_likeRawVc() { .containsExactly(expectedCred)); } + @Test void query_byVcFormat() { var creds = createCredentials(); @@ -422,7 +449,7 @@ protected VerifiableCredentialResource.Builder createCredentialBuilder() { .issuerId("test-issuer") .holderId("test-holder") .state(VcState.ISSUED) - .participantId("test-participant") + .participantId(TEST_PARTICIPANT_CONTEXT_ID) .credential(new VerifiableCredentialContainer(EXAMPLE_VC, CredentialFormat.JSON_LD, createVerifiableCredential().build())) .id("test-id"); }