From 98ddb4226dc1e4242518a6e47ac09640dac8c912 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Wed, 15 Nov 2023 17:55:13 +0100 Subject: [PATCH 1/7] feat: make VP request to CredentialService --- .../identity-trust-core/build.gradle.kts | 3 +- .../core/IdentityAndTrustExtension.java | 7 +- .../DefaultCredentialServiceClient.java | 152 ++++++++++++++++ .../DefaultCredentialServiceClientTest.java | 168 ++++++++++++++++++ .../test/resources/multiple_vp-token_jwt.json | 30 ++++ .../test/resources/multiple_vp-token_ldp.json | 124 +++++++++++++ .../resources/multiple_vp-token_mixed.json | 77 ++++++++ .../src/test/resources/single_jwt-vp.json | 18 ++ .../src/test/resources/single_ldp-vp.json | 65 +++++++ .../IdentityAndTrustService.java | 2 +- .../service/IdentityAndTrustServiceTest.java | 8 +- .../IdentityTrustTransformExtension.java | 10 +- ...bjectFromPresentationQueryTransformer.java | 34 ++++ .../to/JsonObjectToIssuerTransformer.java | 2 +- .../CredentialServiceClient.java | 7 +- .../model/VerifiablePresentation.java | 2 +- .../PresentationResponse.java | 6 +- .../model/VerifiablePresentationTest.java | 2 +- 18 files changed, 702 insertions(+), 15 deletions(-) create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_jwt.json create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_ldp.json create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_mixed.json create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_jwt-vp.json create mode 100644 extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_ldp-vp.json create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/from/JsonObjectFromPresentationQueryTransformer.java diff --git a/extensions/common/iam/identity-trust/identity-trust-core/build.gradle.kts b/extensions/common/iam/identity-trust/identity-trust-core/build.gradle.kts index 0f07f56a076..e3f31b1fb4d 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/build.gradle.kts +++ b/extensions/common/iam/identity-trust/identity-trust-core/build.gradle.kts @@ -17,8 +17,9 @@ dependencies { implementation(project(":extensions:common:iam:identity-trust:identity-trust-sts:identity-trust-sts-embedded")) implementation(libs.nimbus.jwt) - testImplementation(testFixtures(project(":spi:common:identity-trust-spi"))) testImplementation(project(":core:common:junit")) + testImplementation(testFixtures(project(":spi:common:identity-trust-spi"))) testImplementation(libs.nimbus.jwt) + testImplementation(project(":extensions:common:json-ld")) } diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java index 65f5b252139..50b4a60e64d 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java @@ -14,8 +14,10 @@ package org.eclipse.edc.iam.identitytrust.core; +import jakarta.json.Json; import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry; import org.eclipse.edc.iam.identitytrust.IdentityAndTrustService; +import org.eclipse.edc.iam.identitytrust.core.defaults.DefaultCredentialServiceClient; import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator; import org.eclipse.edc.iam.identitytrust.verification.MultiFormatPresentationVerifier; import org.eclipse.edc.identitytrust.CredentialServiceClient; @@ -41,6 +43,7 @@ import org.eclipse.edc.verification.jwt.SelfIssuedIdTokenVerifier; import java.time.Clock; +import java.util.Map; import static org.eclipse.edc.spi.CoreConstants.JSON_LD; @@ -126,8 +129,8 @@ public JwtVerifier getJwtVerifier() { @Provider public CredentialServiceClient createClient(ServiceExtensionContext context) { - context.getMonitor().warning("Using a dummy CredentialServiceClient, that'll return null always. Don't use this in production use cases!"); - return (csUrl, siTokenJwt, scopes) -> null; + return new DefaultCredentialServiceClient(httpClient, Json.createBuilderFactory(Map.of()), + typeManager.getMapper(JSON_LD), typeTransformerRegistry, jsonLd, context.getMonitor()); } private String getOwnDid(ServiceExtensionContext context) { diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java new file mode 100644 index 00000000000..2b2d2468df5 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.core.defaults; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.SignedJWT; +import jakarta.json.JsonBuilderFactory; +import jakarta.json.JsonObject; +import okhttp3.MediaType; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.eclipse.edc.identitytrust.CredentialServiceClient; +import org.eclipse.edc.identitytrust.VcConstants; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiablePresentation; +import org.eclipse.edc.identitytrust.model.VerifiablePresentationContainer; +import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery; +import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponse; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.AbstractResult; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; + +import java.io.IOException; +import java.text.ParseException; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.eclipse.edc.spi.result.Result.failure; +import static org.eclipse.edc.spi.result.Result.success; + +public class DefaultCredentialServiceClient implements CredentialServiceClient { + private final EdcHttpClient httpClient; + private final JsonBuilderFactory jsonFactory; + private final ObjectMapper objectMapper; + private final TypeTransformerRegistry transformerRegistry; + private final JsonLd jsonLd; + private final Monitor monitor; + + public DefaultCredentialServiceClient(EdcHttpClient httpClient, JsonBuilderFactory jsonFactory, ObjectMapper jsonLdMapper, TypeTransformerRegistry transformerRegistry, JsonLd jsonLd, Monitor monitor) { + this.httpClient = httpClient; + this.jsonFactory = jsonFactory; + this.objectMapper = jsonLdMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY); + this.transformerRegistry = transformerRegistry; + this.jsonLd = jsonLd; + this.monitor = monitor; + } + + @Override + public Result> requestPresentation(String credentialServiceUrl, String selfIssuedTokenJwt, List scopes) { + var query = createPresentationQuery(scopes); + + try { + var requestJson = objectMapper.writeValueAsString(query); + var request = new Request.Builder() + .post(RequestBody.create(requestJson, MediaType.parse("application/json"))) + .url(credentialServiceUrl) + .addHeader("Authorization", "%s".formatted(selfIssuedTokenJwt)) + .build(); + + var response = httpClient.execute(request); + + var body = ""; + if (response.body() != null) { + body = response.body().string(); + } + + if (response.isSuccessful() && response.body() != null) { + var presentationResponse = objectMapper.readValue(body, PresentationResponse.class); + return parseResponse(presentationResponse); + } + return failure("Presentation Query failed: HTTP %s, message: %s".formatted(response.code(), body)); + + } catch (IOException e) { + monitor.warning("Error requesting VP", e); + return failure("Error requesting VP: %s".formatted(e.getMessage())); + } + + } + + private Result> parseResponse(PresentationResponse presentationResponse) throws IOException { + var vpResults = Stream.of(presentationResponse.vpToken()) + .map(this::parseVpToken) + .toList(); + + if (vpResults.stream().anyMatch(AbstractResult::failed)) { + return failure("One or more VP tokens could not be parsed. Details: %s".formatted(vpResults.stream().filter(Result::failed).map(AbstractResult::getFailureDetail).collect(Collectors.joining(",")))); + } + + return success(vpResults.stream().map(AbstractResult::getContent).toList()); + } + + private Result parseVpToken(Object vpObj) { + if (vpObj instanceof String) { // JWT VP + return parseJwtVp(vpObj.toString()); + } else if (vpObj instanceof Map) { // LDP VP + return parseLdpVp(vpObj); + } else { + return failure("Unknown VP format: " + vpObj.getClass()); + } + } + + private Result parseLdpVp(Object vpObj) { + var jsonObj = objectMapper.convertValue(vpObj, JsonObject.class); + var rawStr = jsonObj.toString(); + + return jsonLd.expand(jsonObj) + .compose(expanded -> transformerRegistry.transform(expanded, VerifiablePresentation.class)) + .map(vp -> new VerifiablePresentationContainer(rawStr, CredentialFormat.JSON_LD, vp)); + } + + private Result parseJwtVp(String rawJwt) { + try { + var jwt = SignedJWT.parse(rawJwt); + //todo: parse JWT VP + return success(new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, null)); + } catch (ParseException e) { + monitor.warning("Failed to parse JWT VP", e); + return failure("Failed to parse JWT VP: %s".formatted(e.getMessage())); + } + } + + private JsonObject createPresentationQuery(List scopes) { + var scopeArray = jsonFactory.createArrayBuilder(); + scopes.forEach(scopeArray::add); + return jsonFactory.createObjectBuilder() + .add(JsonLdKeywords.CONTEXT, jsonFactory.createArrayBuilder() + .add(VcConstants.PRESENTATION_EXCHANGE_URL) + .add(VcConstants.IATP_CONTEXT_URL)) + .add(JsonLdKeywords.TYPE, PresentationQuery.PRESENTATION_QUERY_TYPE_PROPERTY) + .add("scope", scopeArray.build()) + .build(); + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java new file mode 100644 index 00000000000..3bfd8435c3c --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.core.defaults; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import okhttp3.MediaType; +import okhttp3.Protocol; +import okhttp3.Response; +import okhttp3.ResponseBody; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.identitytrust.model.VerifiablePresentation; +import org.eclipse.edc.jsonld.spi.JsonLd; +import org.eclipse.edc.spi.http.EdcHttpClient; +import org.eclipse.edc.transform.spi.TypeTransformerRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.io.IOException; +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jsonld.util.JacksonJsonLd.createObjectMapper; +import static org.eclipse.edc.junit.testfixtures.TestUtils.getResourceFileContentAsString; +import static org.eclipse.edc.spi.result.Result.success; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class DefaultCredentialServiceClientTest { + + private static final String CS_URL = "http://test.com/cs"; + private final EdcHttpClient httpClientMock = mock(); + private DefaultCredentialServiceClient client; + + @BeforeEach + void setup() { + var registry = mock(TypeTransformerRegistry.class); + when(registry.transform(isA(JsonObject.class), eq(VerifiablePresentation.class))) + .thenReturn(success(createPresentation())); + var jsonLdMock = mock(JsonLd.class); + when(jsonLdMock.expand(any())).thenAnswer(a -> success(a.getArgument(0))); + client = new DefaultCredentialServiceClient(httpClientMock, Json.createBuilderFactory(Map.of()), + createObjectMapper(), registry, jsonLdMock, mock()); + } + + @Test + @DisplayName("CS returns a single LDP-VP") + void requestPresentation_singleLdpVp() throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(200, getResourceFileContentAsString("single_ldp-vp.json"))); + + var result = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(1).allMatch(vpc -> vpc.format() == CredentialFormat.JSON_LD); + } + + @Test + @DisplayName("CS returns a single JWT-VP") + void requestPresentation_singleJwtVp() throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(200, getResourceFileContentAsString("single_jwt-vp.json"))); + + var result = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(1).allMatch(vpc -> vpc.format() == CredentialFormat.JWT); + } + + @Test + @DisplayName("CS returns multiple VPs, one LDP-VP and a JWT-VP") + void requestPresentationLdp_multipleVp_mixed() throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_mixed.json"))); + + var result = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(2) + .anySatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JSON_LD)) + .anySatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JWT)); + } + + @Test + @DisplayName("CS returns multiple LDP-VPs") + void requestPresentation_mulipleVp_onlyLdp() throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_ldp.json"))); + + var result = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(2) + .allSatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JSON_LD)); + } + + @Test + @DisplayName("CS returns multiple JWT-VPs") + void requestPresentation_mulipleVp_onlyJwt() throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(200, getResourceFileContentAsString("multiple_vp-token_jwt.json"))); + + var result = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(result.succeeded()).isTrue(); + assertThat(result.getContent()).hasSize(2) + .allSatisfy(vp -> assertThat(vp.format()).isEqualTo(CredentialFormat.JWT)); + } + + @ParameterizedTest(name = "CS returns HTTP error code {0}") + @ValueSource(ints = { 400, 401, 403, 503, 501 }) + void requestPresentation_csReturnsError(int httpCode) throws IOException { + when(httpClientMock.execute(any())) + .thenReturn(response(httpCode, "Test failure")); + + var res = client.requestPresentation(CS_URL, "foo", List.of()); + assertThat(res.failed()).isTrue(); + assertThat(res.getFailureDetail()).isEqualTo("Presentation Query failed: HTTP %s, message: Test failure".formatted(httpCode)); + } + + private VerifiablePresentation createPresentation() { + return VerifiablePresentation.Builder.newInstance() + .type("VerifiablePresentation") + .credential(VerifiableCredential.Builder.newInstance() + .issuer(new Issuer("test-issuer", Map.of())) + .type("VerifiableCredential") + .issuanceDate(Instant.now()) + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("test-subject") + .claim("foo", "bar") + .build()) + .build()) + .build(); + } + + private Response response(int code, String body) { + return new Response.Builder() + .request(mock()) + .protocol(Protocol.HTTP_2) + .code(code) // status code + .message("") + .body(ResponseBody.create( + body, + MediaType.get("application/json; charset=utf-8") + )) + .build(); + } + + +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_jwt.json b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_jwt.json new file mode 100644 index 00000000000..c43f91cd7fc --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_jwt.json @@ -0,0 +1,30 @@ +{ + "vp_token": [ + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHBsZUdGdGNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6ZFdJaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpTENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2WTNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzSW1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2libUptSWpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5TXl3aWJtOXVZMlVpT2lJMk5qQWhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzSW5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY21WbFEzSmxaR1Z1ZEdsaGJDSmRMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFpTSTZJanh6Y0dGdUlHeGhibWM5SjJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6UEM5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhNTF4ck5EZ1hEYjBjcTFVVFlCLXJLNEZ0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQnh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ2VOLWpDOFdla2RQbDZ6S1pRajBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaYWdUeVBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKdy1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnNlYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciXX19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTvbJwdvIeuGuTqy7ipO-EYi7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58LueFfGw47NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqMBjYgP62KvhIvW8BbkGUelYMetA", + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHBsZUdGdGNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6ZFdJaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpTENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2WTNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzSW1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2libUptSWpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5TXl3aWJtOXVZMlVpT2lJMk5qQWhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzSW5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY21WbFEzSmxaR1Z1ZEdsaGJDSmRMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFpTSTZJanh6Y0dGdUlHeGhibWM5SjJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6UEM5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhNTF4ck5EZ1hEYjBjcTFVVFlCLXJLNEZ0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQnh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ2VOLWpDOFdla2RQbDZ6S1pRajBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaYWdUeVBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKdy1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnNlYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciXX19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTvbJwdvIeuGuTqy7ipO-EYi7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58LueFfGw47NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqMBjYgP62KvhIvW8BbkGUelYMetA" + ], + "presentation_submission": { + "id": "Presentation example 2", + "definition_id": "Example with multiple VPs", + "descriptor_map": [ + { + "id": "ID Card with constraints", + "format": "ldp_vp", + "path": "$[0]", + "path_nested": { + "format": "ldp_vc", + "path": "$[0].verifiableCredential[0]" + } + }, + { + "id": "Ontario Health Insurance Plan", + "format": "jwt_vp_json", + "path": "$[1]", + "path_nested": { + "format": "jwt_vc_json", + "path": "$[1].vp.verifiableCredential[0]" + } + } + ] + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_ldp.json b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_ldp.json new file mode 100644 index 00000000000..c37b14bdbed --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_ldp.json @@ -0,0 +1,124 @@ +{ + "vp_token": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "https://example.com/credentials/1872", + "type": [ + "VerifiableCredential", + "IDCardCredential" + ], + "issuer": { + "id": "did:example:issuer" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "given_name": "Fredrik", + "family_name": "Strömberg", + "birthdate": "1949-01-22" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "jws": "eyJhb...IAoDA", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:example:issuer#keys-1" + } + } + ], + "id": "ebc6f1c2", + "holder": "did:example:holder", + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "challenge": "n-0S6_WzA2Mj", + "domain": "https://client.example.org/cb", + "jws": "eyJhb...JQdBw", + "proofPurpose": "authentication", + "verificationMethod": "did:example:holder#key-1" + } + }, + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "https://example.com/credentials/1872", + "type": [ + "VerifiableCredential", + "IDCardCredential" + ], + "issuer": { + "id": "did:example:issuer" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "given_name": "Fredrik", + "family_name": "Strömberg", + "birthdate": "1949-01-22" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "jws": "eyJhb...IAoDA", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:example:issuer#keys-1" + } + } + ], + "id": "ebc6f1c2", + "holder": "did:example:holder", + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "challenge": "n-0S6_WzA2Mj", + "domain": "https://client.example.org/cb", + "jws": "eyJhb...JQdBw", + "proofPurpose": "authentication", + "verificationMethod": "did:example:holder#key-1" + } + } + ], + "presentation_submission": { + "id": "Presentation example 2", + "definition_id": "Example with multiple VPs", + "descriptor_map": [ + { + "id": "ID Card with constraints", + "format": "ldp_vp", + "path": "$[0]", + "path_nested": { + "format": "ldp_vc", + "path": "$[0].verifiableCredential[0]" + } + }, + { + "id": "Ontario Health Insurance Plan", + "format": "jwt_vp_json", + "path": "$[1]", + "path_nested": { + "format": "jwt_vc_json", + "path": "$[1].vp.verifiableCredential[0]" + } + } + ] + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_mixed.json b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_mixed.json new file mode 100644 index 00000000000..318b6348b6b --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/multiple_vp-token_mixed.json @@ -0,0 +1,77 @@ +{ + "vp_token": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "https://example.com/credentials/1872", + "type": [ + "VerifiableCredential", + "IDCardCredential" + ], + "issuer": { + "id": "did:example:issuer" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "given_name": "Fredrik", + "family_name": "Strömberg", + "birthdate": "1949-01-22" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "jws": "eyJhb...IAoDA", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:example:issuer#keys-1" + } + } + ], + "id": "ebc6f1c2", + "holder": "did:example:holder", + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "challenge": "n-0S6_WzA2Mj", + "domain": "https://client.example.org/cb", + "jws": "eyJhb...JQdBw", + "proofPurpose": "authentication", + "verificationMethod": "did:example:holder#key-1" + } + }, + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHBsZUdGdGNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6ZFdJaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpTENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2WTNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzSW1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2libUptSWpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5TXl3aWJtOXVZMlVpT2lJMk5qQWhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzSW5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY21WbFEzSmxaR1Z1ZEdsaGJDSmRMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFpTSTZJanh6Y0dGdUlHeGhibWM5SjJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6UEM5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhNTF4ck5EZ1hEYjBjcTFVVFlCLXJLNEZ0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQnh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ2VOLWpDOFdla2RQbDZ6S1pRajBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaYWdUeVBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKdy1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnNlYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciXX19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTvbJwdvIeuGuTqy7ipO-EYi7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58LueFfGw47NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqMBjYgP62KvhIvW8BbkGUelYMetA" + ], + "presentation_submission": { + "id": "Presentation example 2", + "definition_id": "Example with multiple VPs", + "descriptor_map": [ + { + "id": "ID Card with constraints", + "format": "ldp_vp", + "path": "$[0]", + "path_nested": { + "format": "ldp_vc", + "path": "$[0].verifiableCredential[0]" + } + }, + { + "id": "Ontario Health Insurance Plan", + "format": "jwt_vp_json", + "path": "$[1]", + "path_nested": { + "format": "jwt_vc_json", + "path": "$[1].vp.verifiableCredential[0]" + } + } + ] + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_jwt-vp.json b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_jwt-vp.json new file mode 100644 index 00000000000..2b8ab33f259 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_jwt-vp.json @@ -0,0 +1,18 @@ +{ + "vp_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHBsZUdGdGNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6ZFdJaU9pSmthV1E2WlhoaGJYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpTENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2WTNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzSW1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2libUptSWpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5TXl3aWJtOXVZMlVpT2lJMk5qQWhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1MGFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzSW5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY21WbFEzSmxaR1Z1ZEdsaGJDSmRMQ0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFpTSTZJanh6Y0dGdUlHeGhibWM5SjJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6UEM5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhNTF4ck5EZ1hEYjBjcTFVVFlCLXJLNEZ0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQnh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ2VOLWpDOFdla2RQbDZ6S1pRajBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaYWdUeVBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKdy1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnNlYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciXX19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTvbJwdvIeuGuTqy7ipO-EYi7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58LueFfGw47NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqMBjYgP62KvhIvW8BbkGUelYMetA", + "presentation_submission": { + "id": "Presentation example 1", + "definition_id": "Example with selective disclosure", + "descriptor_map": [ + { + "id": "ID card with constraints", + "format": "ldp_vp", + "path": "$", + "path_nested": { + "format": "ldp_vc", + "path": "$.verifiableCredential[0]" + } + } + ] + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_ldp-vp.json b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_ldp-vp.json new file mode 100644 index 00000000000..8fa2b31f652 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/resources/single_ldp-vp.json @@ -0,0 +1,65 @@ +{ + "vp_token": { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": [ + "VerifiablePresentation" + ], + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "https://example.com/credentials/1872", + "type": [ + "VerifiableCredential", + "IDCardCredential" + ], + "issuer": { + "id": "did:example:issuer" + }, + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "given_name": "Fredrik", + "family_name": "Strömberg", + "birthdate": "1949-01-22" + }, + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "jws": "eyJhb...JQdBw", + "proofPurpose": "assertionMethod", + "verificationMethod": "did:example:issuer#keys-1" + } + } + ], + "id": "ebc6f1c2", + "holder": "did:example:holder", + "proof": { + "type": "Ed25519Signature2018", + "created": "2021-03-19T15:30:15Z", + "challenge": "n-0S6_WzA2Mj", + "domain": "https://client.example.org/cb", + "jws": "eyJhbG...IAoDA", + "proofPurpose": "authentication", + "verificationMethod": "did:example:holder#key-1" + } + }, + "presentation_submission": { + "id": "Presentation example 1", + "definition_id": "Example with selective disclosure", + "descriptor_map": [ + { + "id": "ID card with constraints", + "format": "ldp_vp", + "path": "$", + "path_nested": { + "format": "ldp_vc", + "path": "$.verifiableCredential[0]" + } + } + ] + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java index 24e41e42974..49efea0eadc 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityAndTrustService.java @@ -128,7 +128,7 @@ public Result verifyJwtToken(TokenRepresentation tokenRepresentation return vpResponse.mapTo(); } - var verifiablePresentation = vpResponse.getContent(); + var verifiablePresentation = vpResponse.getContent().get(0); var credentials = verifiablePresentation.presentation().getCredentials(); // verify, that the VP and all VPs are cryptographically OK var result = presentationVerifier.verifyPresentation(verifiablePresentation) diff --git a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java index 0fe0fe02dd7..e92742a8dde 100644 --- a/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-service/src/test/java/org/eclipse/edc/iam/identitytrust/service/IdentityAndTrustServiceTest.java @@ -147,7 +147,7 @@ void presentationRequestFails() { @Test void cryptographicError() { when(mockedVerifier.verifyPresentation(any())).thenReturn(Result.failure("Cryptographic error")); - when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(createPresentationContainer())); + when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(List.of(createPresentationContainer()))); var token = createJwt(); var result = service.verifyJwtToken(token, "test-audience"); assertThat(result).isFailed().detail().isEqualTo("Cryptographic error"); @@ -163,7 +163,7 @@ void notYetValid() { .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation); when(mockedVerifier.verifyPresentation(any())).thenReturn(success()); - when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(vpContainer)); + when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(List.of(vpContainer))); var token = createJwt(CONSUMER_DID, EXPECTED_OWN_DID); var result = service.verifyJwtToken(token, "test-audience"); assertThat(result).isFailed().messages() @@ -184,7 +184,7 @@ void oneInvalidSubjectId() { .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation); when(mockedVerifier.verifyPresentation(any())).thenReturn(success()); - when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(vpContainer)); + when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(List.of(vpContainer))); var token = createJwt(CONSUMER_DID, EXPECTED_OWN_DID); var result = service.verifyJwtToken(token, "test-audience"); assertThat(result).isFailed().messages() @@ -209,7 +209,7 @@ void credentialHasInvalidIssuer_issuerIsUrl() { .build(); var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.JSON_LD, presentation); when(mockedVerifier.verifyPresentation(any())).thenReturn(success()); - when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(vpContainer)); + when(mockedClient.requestPresentation(any(), any(), any())).thenReturn(success(List.of(vpContainer))); var token = createJwt(consumerDid, EXPECTED_OWN_DID); var result = service.verifyJwtToken(token, "test-audience"); assertThat(result).isFailed().messages() diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java index a5e9b3a8a9f..96285505d31 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java @@ -14,13 +14,15 @@ package org.eclipse.edc.iam.identitytrust; -import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToCredentialStatusTransformer; +import org.eclipse.edc.iam.identitytrust.transform.from.JsonObjectFromPresentationQueryTransformer; +import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import org.jetbrains.annotations.NotNull; @@ -29,6 +31,7 @@ import java.net.URISyntaxException; import static java.lang.String.format; +import static org.eclipse.edc.spi.CoreConstants.JSON_LD; @Extension(value = IdentityTrustTransformExtension.NAME, categories = { "iam", "transform", "jsonld" }) public class IdentityTrustTransformExtension implements ServiceExtension { @@ -39,6 +42,8 @@ public class IdentityTrustTransformExtension implements ServiceExtension { @Inject private JsonLd jsonLdService; + @Inject + private TypeManager typeManager; @Override public void initialize(ServiceExtensionContext context) { @@ -50,7 +55,8 @@ public void initialize(ServiceExtensionContext context) { .onSuccess(uri -> jsonLdService.registerCachedDocument("https://www.w3.org/2018/credentials/v1", uri)) .onFailure(failure -> context.getMonitor().warning("Failed to register cached json-ld document: " + failure.getFailureDetail())); - typeTransformerRegistry.register(new JsonObjectToCredentialStatusTransformer()); + typeTransformerRegistry.register(new JsonObjectToPresentationQueryTransformer(typeManager.getMapper(JSON_LD))); + typeTransformerRegistry.register(new JsonObjectFromPresentationQueryTransformer()); } @NotNull diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/from/JsonObjectFromPresentationQueryTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/from/JsonObjectFromPresentationQueryTransformer.java new file mode 100644 index 00000000000..6ff7b2048eb --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/from/JsonObjectFromPresentationQueryTransformer.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.transform.from; + +import jakarta.json.JsonObject; +import org.eclipse.edc.identitytrust.model.credentialservice.PresentationQuery; +import org.eclipse.edc.jsonld.spi.transformer.AbstractJsonLdTransformer; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class JsonObjectFromPresentationQueryTransformer extends AbstractJsonLdTransformer { + + public JsonObjectFromPresentationQueryTransformer() { + super(PresentationQuery.class, JsonObject.class); + } + + @Override + public @Nullable JsonObject transform(@NotNull PresentationQuery presentationQuery, @NotNull TransformerContext context) { + return null; + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java index e34b02aefbd..50b15af219d 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JsonObjectToIssuerTransformer.java @@ -25,7 +25,7 @@ import java.util.HashMap; public class JsonObjectToIssuerTransformer extends AbstractJsonLdTransformer { - protected JsonObjectToIssuerTransformer() { + public JsonObjectToIssuerTransformer() { super(JsonObject.class, Issuer.class); } diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/CredentialServiceClient.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/CredentialServiceClient.java index b4b2947dd06..de4d730b9f4 100644 --- a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/CredentialServiceClient.java +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/CredentialServiceClient.java @@ -26,12 +26,17 @@ public interface CredentialServiceClient { /** * Sends a presentation request * to the specified credential service. + *

+ * The CredentialService will return 401/403 error codes if either the SI Token is not authorized, if the scopes don't match or if no scopes are provided. + *

+ * Note that sending a DIF Presentation Definition is not supported yet and will result in a 5xx error. * * @param csUrl The URL of the CredentialService, from which the presentation is to be requested. * @param siTokenJwt A Self-Issued ID token in JWT format, that contains the access_token * @param scopes A list of strings, each containing a scope definition + * @return A list of {@link VerifiablePresentationContainer} objects, or a failure if the request was unsuccessful. */ - Result requestPresentation(String csUrl, String siTokenJwt, List scopes); + Result> requestPresentation(String csUrl, String siTokenJwt, List scopes); //todo: add write api? } diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java index d870dd77de0..2c401df683a 100644 --- a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java @@ -97,7 +97,7 @@ public VerifiablePresentation build() { } // these next two aren't mandated by the spec, but there is no point in having a VP without credentials or a proof. if (instance.credentials == null || instance.credentials.isEmpty()) { - throw new IllegalArgumentException("VerifiablePresentation must have at least one presentation."); + throw new IllegalArgumentException("VerifiablePresentation must have at least one credential."); } return instance; } diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/credentialservice/PresentationResponse.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/credentialservice/PresentationResponse.java index 30530d028e3..99143d06161 100644 --- a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/credentialservice/PresentationResponse.java +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/credentialservice/PresentationResponse.java @@ -19,7 +19,11 @@ /** * A representation of a Presentation Response * that the credential service sends back to the requester. + *

+ * The {@code vp_token} param is a JSON String or JSON object that MUST contain a single Verifiable Presentation or an + * array of JSON Strings and JSON objects, each of them containing a Verifiable Presentations. Each Verifiable Presentation + * MUST be represented as a JSON string (that is a Base64url encoded value) or a JSON object, depending on the requested format. */ -public record PresentationResponse(@JsonProperty("vp_token") String vpToken, +public record PresentationResponse(@JsonProperty("vp_token") Object[] vpToken, @JsonProperty("presentation_submission") PresentationSubmission presentationSubmission) { } diff --git a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java index 295cc81ccb0..494bfaacaa0 100644 --- a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java +++ b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java @@ -50,6 +50,6 @@ void build_noCredentials() { .types(List.of("test-type")) .build()) .isInstanceOf(IllegalArgumentException.class) - .hasMessageEndingWith("must have at least one presentation."); + .hasMessageEndingWith("must have at least one credential."); } } From f3408ceb12c026ada8278744aabe8071de981a39 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 16 Nov 2023 15:06:17 +0100 Subject: [PATCH 2/7] Add transformers for JWT VC/VP --- .../DefaultCredentialServiceClient.java | 2 + .../identity-trust-transform/build.gradle.kts | 1 + .../IdentityTrustTransformExtension.java | 4 + .../JwtToVerifiableCredentialTransformer.java | 117 +++++++++++++++++ ...wtToVerifiablePresentationTransformer.java | 99 +++++++++++++++ .../iam/identitytrust/transform/TestData.java | 113 +++++++++++++++++ ...ToVerifiableCredentialTransformerTest.java | 42 +++++++ ...VerifiablePresentationTransformerTest.java | 118 ++++++++++++++++++ .../model/VerifiablePresentation.java | 4 - .../model/VerifiablePresentationTest.java | 6 +- 10 files changed, 498 insertions(+), 8 deletions(-) create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformerTest.java create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java index 2b2d2468df5..3cb104ae590 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java @@ -130,6 +130,8 @@ private Result parseLdpVp(Object vpObj) { private Result parseJwtVp(String rawJwt) { try { var jwt = SignedJWT.parse(rawJwt); + var claims = jwt.getJWTClaimsSet(); + var vp = claims.getClaim("vp"); //todo: parse JWT VP return success(new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, null)); } catch (ParseException e) { diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/build.gradle.kts b/extensions/common/iam/identity-trust/identity-trust-transform/build.gradle.kts index f7c9dfd4986..88951aa1ea5 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/build.gradle.kts +++ b/extensions/common/iam/identity-trust/identity-trust-transform/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { api(project(":spi:common:json-ld-spi")) api(project(":spi:common:transform-spi")) api(project(":spi:common:transform-spi")) + api(libs.nimbus.jwt) testImplementation(project(":extensions:common:json-ld")) testImplementation(project(":core:common:transform-core")) //for the TransformerContextImpl diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java index 96285505d31..42bdaf2863c 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java @@ -16,6 +16,8 @@ import org.eclipse.edc.iam.identitytrust.transform.from.JsonObjectFromPresentationQueryTransformer; import org.eclipse.edc.iam.identitytrust.transform.to.JsonObjectToPresentationQueryTransformer; +import org.eclipse.edc.iam.identitytrust.transform.to.JwtToVerifiableCredentialTransformer; +import org.eclipse.edc.iam.identitytrust.transform.to.JwtToVerifiablePresentationTransformer; import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; @@ -57,6 +59,8 @@ public void initialize(ServiceExtensionContext context) { typeTransformerRegistry.register(new JsonObjectToPresentationQueryTransformer(typeManager.getMapper(JSON_LD))); typeTransformerRegistry.register(new JsonObjectFromPresentationQueryTransformer()); + typeTransformerRegistry.register(new JwtToVerifiablePresentationTransformer(context.getMonitor(), typeManager.getMapper(JSON_LD))); + typeTransformerRegistry.register(new JwtToVerifiableCredentialTransformer(context.getMonitor())); } @NotNull diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java new file mode 100644 index 00000000000..22a24dbd684 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.transform.to; + +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.identitytrust.model.CredentialStatus; +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.edc.transform.spi.TypeTransformer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.ParseException; +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import static java.util.Optional.ofNullable; + +public class JwtToVerifiableCredentialTransformer implements TypeTransformer { + public static final String TYPE_CLAIM = "type"; + public static final String EXPIRATION_DATE_PROPERTY = "expirationDate"; + public static final String ISSUANCE_DATE_PROPERTY = "issuanceDate"; + private static final String VC_CLAIM = "vc"; + private static final String SUBJECT_CLAIM = "credentialSubject"; + private static final String CREDENTIAL_STATUS_PROPERTY = "credentialStatus"; + private final Monitor monitor; + + public JwtToVerifiableCredentialTransformer(Monitor monitor) { + this.monitor = monitor; + } + + @Override + public Class getInputType() { + return String.class; + } + + @Override + public Class getOutputType() { + return VerifiableCredential.class; + } + + @Override + public @Nullable VerifiableCredential transform(@NotNull String serializedJwt, @NotNull TransformerContext context) { + try { + var jwt = SignedJWT.parse(serializedJwt); + var claims = jwt.getJWTClaimsSet(); + var builder = VerifiableCredential.Builder.newInstance(); + + var vcObject = claims.getClaim(VC_CLAIM); + if (vcObject instanceof Map vc) { + builder.types((List) vc.get(TYPE_CLAIM)); + builder.credentialSubject(extractSubject((Map) vc.get(SUBJECT_CLAIM))); + + getExpirationDate(vc, builder, claims); + getIssuanceDate(vc, builder, claims); + + builder.issuer(new Issuer(claims.getIssuer(), Map.of())); + builder.credentialStatus(extractStatus((Map) vc.get(CREDENTIAL_STATUS_PROPERTY))); + builder.name(claims.getSubject()); // todo: is this correct? + return builder.build(); + } + } catch (ParseException e) { + monitor.warning("Error parsing JWT", e); + context.reportProblem("Error parsing JWT: %s".formatted(e.getMessage())); + } + return null; + } + + private void getIssuanceDate(Map vcObject, VerifiableCredential.Builder builder, JWTClaimsSet fallback) { + builder.issuanceDate(ofNullable(vcObject.get(ISSUANCE_DATE_PROPERTY)) + .map(o -> Instant.parse(o.toString())) + .orElseGet(() -> fallback.getIssueTime().toInstant())); + } + + /** + * Expiration date is entirely optional, take it either from the VC or from the JWT + */ + private void getExpirationDate(Map vcObject, VerifiableCredential.Builder builder, JWTClaimsSet fallback) { + builder.expirationDate(ofNullable(vcObject.get(EXPIRATION_DATE_PROPERTY)) + .map(o -> Instant.parse(o.toString())) + .orElseGet(() -> ofNullable(fallback.getExpirationTime()).map(Date::toInstant).orElse(null))); + } + + private CredentialStatus extractStatus(Map status) { + if (status == null || status.isEmpty()) { + return null; + } + var id = status.remove("id").toString(); + var type = status.remove("type").toString(); + + return new CredentialStatus(id, type, status); + } + + private CredentialSubject extractSubject(Map subject) { + var bldr = CredentialSubject.Builder.newInstance(); + subject.entrySet().forEach(e -> bldr.claim(e.getKey(), e.getValue())); + return bldr.build(); + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java new file mode 100644 index 00000000000..542a79560b6 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.transform.to; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.SignedJWT; +import jakarta.json.JsonObject; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.identitytrust.model.VerifiablePresentation; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.eclipse.edc.transform.spi.TypeTransformer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.text.ParseException; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@SuppressWarnings("unchecked") +public class JwtToVerifiablePresentationTransformer implements TypeTransformer { + private static final String VP_CLAIM = "vp"; + private final Monitor monitor; + private final ObjectMapper objectMapper; + + public JwtToVerifiablePresentationTransformer(Monitor monitor, ObjectMapper objectMapper) { + this.monitor = monitor; + this.objectMapper = objectMapper; + } + + @Override + public Class getInputType() { + return String.class; + } + + @Override + public Class getOutputType() { + return VerifiablePresentation.class; + } + + @Override + public @Nullable VerifiablePresentation transform(@NotNull String jsonWebToken, @NotNull TransformerContext context) { + + var builder = VerifiablePresentation.Builder.newInstance(); + try { + var signedJwt = SignedJWT.parse(jsonWebToken); + var claimsSet = signedJwt.getJWTClaimsSet(); + + var vpObject = claimsSet.getClaim(VP_CLAIM); + builder.holder(claimsSet.getIssuer()); + builder.id(claimsSet.getJWTID()); + + if (vpObject instanceof Map map) { + builder.types((List) map.get("type")); + + var credentialObject = map.get("verifiableCredential"); + builder.credentials(extractCredentials(credentialObject, context)); + return builder.build(); + } + + } catch (ParseException e) { + monitor.warning("Error parsing JWT", e); + context.reportProblem("Error parsing JWT: %s".formatted(e.getMessage())); + } + context.reportProblem("Could not parse VerifiablePresentation from JWT."); + return null; + } + + private List extractCredentials(Object credentialsObject, TransformerContext context) { + Collection list; + if (credentialsObject instanceof Collection) { + list = (Collection) credentialsObject; + } else { + list = List.of(credentialsObject); + } + + return list.stream().map(obj -> { + if (obj instanceof String) { // VC is JWT + return context.transform(obj.toString(), VerifiableCredential.class); + } else { // VC is LDP + var input = objectMapper.convertValue(obj, JsonObject.class); + return context.transform(input, VerifiableCredential.class); + } + }).toList(); + } +} diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java index 09c41d46209..78b8c7c1502 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/TestData.java @@ -122,4 +122,117 @@ public interface TestData { } } """; + String EXAMPLE_JWT_VP = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2" + + "tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi0" + + "4NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmY" + + "iOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbn" + + "RleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50a" + + "WFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlv" + + "biJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6WyJleUpoYkdjaU9pSlNVekkxTmlJc0luUjVjQ0k2SWtwWFZDSXNJbXRwWkNJNkltUnBaRHB" + + "sZUdGdGNHeGxPbUZpWm1VeE0yWTNNVEl4TWpBME16RmpNamMyWlRFeVpXTmhZaU5yWlhsekxURWlmUS5leUp6ZFdJaU9pSmthV1E2WlhoaG" + + "JYQnNaVHBsWW1abFlqRm1OekV5WldKak5tWXhZekkzTm1VeE1tVmpNakVpTENKcWRHa2lPaUpvZEhSd09pOHZaWGhoYlhCc1pTNWxaSFV2W" + + "TNKbFpHVnVkR2xoYkhNdk16Y3pNaUlzSW1semN5STZJbWgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjB2YTJWNWN5OW1iMjh1YW5kcklpd2lib" + + "UptSWpveE5UUXhORGt6TnpJMExDSnBZWFFpT2pFMU5ERTBPVE0zTWpRc0ltVjRjQ0k2TVRVM016QXlPVGN5TXl3aWJtOXVZMlVpT2lJMk5qQ" + + "WhOak0wTlVaVFpYSWlMQ0oyWXlJNmV5SkFZMjl1ZEdWNGRDSTZXeUpvZEhSd2N6b3ZMM2QzZHk1M015NXZjbWN2TWpBeE9DOWpjbVZrWlc1M" + + "GFXRnNjeTkyTVNJc0ltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OHlNREU0TDJOeVpXUmxiblJwWVd4ekwyVjRZVzF3YkdWekwzWXhJbDBzS" + + "W5SNWNHVWlPbHNpVm1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aUxDSlZibWwyWlhKemFYUjVSR1ZuY21WbFEzSmxaR1Z1ZEdsaGJDSmRM" + + "Q0pqY21Wa1pXNTBhV0ZzVTNWaWFtVmpkQ0k2ZXlKa1pXZHlaV1VpT25zaWRIbHdaU0k2SWtKaFkyaGxiRzl5UkdWbmNtVmxJaXdpYm1GdFp" + + "TSTZJanh6Y0dGdUlHeGhibWM5SjJaeUxVTkJKejVDWVdOallXeGhkWExEcVdGMElHVnVJRzExYzJseGRXVnpJRzUxYmNPcGNtbHhkV1Z6UE" + + "M5emNHRnVQaUo5ZlgxOS5LTEpvNUdBeUJORDNMRFRuOUg3RlFva0VzVUVpOGpLd1hoR3ZvTjNKdFJhNTF4ck5EZ1hEYjBjcTFVVFlCLXJLNE" + + "Z0OVlWbVIxTklfWk9GOG9HY183d0FwOFBIYkYySGFXb2RRSW9PQnh4VC00V05xQXhmdDdFVDZsa0gtNFM2VXgzclNHQW1jek1vaEVFZjhlQ" + + "2VOLWpDOFdla2RQbDZ6S1pRajBZUEIxcng2WDAteGxGQnM3Y2w2V3Q4cmZCUF90WjlZZ1ZXclFtVVd5cFNpb2MwTVV5aXBobXlFYkxaYWdUe" + + "VBsVXlmbEdsRWRxclpBdjZlU2U2UnR4Snk2TTEtbEQ3YTVIVHphbllUV0JQQVVIRFpHeUdLWGRKdy1XX3gwSVdDaEJ6STh0M2twRzI1M2ZnN" + + "lYzdFBnSGVLWEU5NGZ6X1FwWWZnLS03a0xzeUJBZlFHYmciXX19.ft_Eq4IniBrr7gtzRfrYj8Vy1aPXuFZU-6_ai0wvaKcsrzI4JkQEKTv" + + "bJwdvIeuGuTqy7ipO-EYi7V4TvonPuTRdpB7ZHOlYlbZ4wA9WJ6mSVSqDACvYRiFvrOFmie8rgm6GacWatgO4m4NqiFKFko3r58LueFfGw4" + + "7NK9RcfOkVQeHCq4btaDqksDKeoTrNysF4YS89INa-prWomrLRAhnwLOo1Etp3E4ESAxg73CR2kA5AoMbf5KtFueWnMcSbQkMRdWcGC1VssC" + + "0tB0JffVjq7ZV6OTyV4kl1-UVgiPLXUTpupFfLRhf9QpqMBjYgP62KvhIvW8BbkGUelYMetA"; + + String EXAMPLE_JWT_VP_EMPTY_CREDENTIALS_ARRAY = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplY + mZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiL + CJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5Mzcy + NCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzI + wMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZl + cmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6W11 + 9fQ.hF9sRPFLaVtYQLh3edAfsbl5oceszAlhPrnzm8eSHd_MHMgKmtUm0iu44lytLpsI6AP38SKJy1BEFfbg9NkKLR6cJe0z7Xqn84C4DaQw + c4zqaRaMTXhx8QAIH11Zj5sWhoJkCAAQcqhZTXqEVJCQWr1FLJiR4tVytOORZHh1i9AtiMIMqhzleaMSmHgxZXGKnluucFf93H_bKJLRMVIKS + mi7gJk6Jz8cjQ1Jv1P2UQNagxZ7Y-p6glsE9FxAKXoLBUVsZzITrnrkx9PK91ZeH694dJRa1QmwOIjUnBt9i2-UPs_i-fyebHEkocpV6ETOv5- + LBtS5puQCOEEkabh07w + """; + + String EXMPLE_JWT_VP_NO_VP_CLAIM = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplY + mZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLC + JhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNCw + iZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIn0.QixkENyxnqRjsQcfkqvP28tRFlNeWDmObVaVZGqVHTQhPAka7NrioDm + 2draiAU7u0OSAtSfBomcLAcRSuNflDT7WQMm-RtzKmxzmdTOjeT0qgB1dwtovmo9cedpCHKYDg0MWrKLLy_EqTlEwgQIdPF43pbTh_QHueM0 + AqguFjo7RYHix77ueBEz9A7yvCFSE0LQRMYzy8Fjc-JA_jaq1G1eg7M-IceCB74Sj52nlbEIbfLxjlgqHzpQ4M4gN-J-9rA5qrReI1uI-hj + 7EbH9P_HKksL5jVPzLNMHLE68Z907J22n3cthZFVPAgpwFgIcIMAVB7uyOVR0P8PA3JdFQg"""; + + String EXAMPLE_JWT_VP_SINGLE_VC = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpl + YmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzU + iLCJhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5Mz + cyNCwiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnL + zIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpb + IlZlcmlmaWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI + 6ImV5SmhiR2NpT2lKU1V6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW1ScFpEcGxlR0Z0Y0d4bE9tRmlabVV4TTJZM01USXhNak + EwTXpGak1qYzJaVEV5WldOaFlpTnJaWGx6TFRFaWZRLmV5SnpkV0lpT2lKa2FXUTZaWGhoYlhCc1pUcGxZbVpsWWpGbU56RXlaV0pqTm1Ze + Fl6STNObVV4TW1Wak1qRWlMQ0pxZEdraU9pSm9kSFJ3T2k4dlpYaGhiWEJzWlM1bFpIVXZZM0psWkdWdWRHbGhiSE12TXpjek1pSXNJbWx6 + Y3lJNkltaDBkSEJ6T2k4dlpYaGhiWEJzWlM1amIyMHZhMlY1Y3k5bWIyOHVhbmRySWl3aWJtSm1Jam94TlRReE5Ea3pOekkwTENKcFlYUWl + PakUxTkRFME9UTTNNalFzSW1WNGNDSTZNVFUzTXpBeU9UY3lNeXdpYm05dVkyVWlPaUkyTmpBaE5qTTBOVVpUWlhJaUxDSjJZeUk2ZXlKQV + kyOXVkR1Y0ZENJNld5Sm9kSFJ3Y3pvdkwzZDNkeTUzTXk1dmNtY3ZNakF4T0M5amNtVmtaVzUwYVdGc2N5OTJNU0lzSW1oMGRIQnpPaTh2ZD + NkM0xuY3pMbTl5Wnk4eU1ERTRMMk55WldSbGJuUnBZV3h6TDJWNFlXMXdiR1Z6TDNZeElsMHNJblI1Y0dVaU9sc2lWbVZ5YVdacFlXSnNaVU + 55WldSbGJuUnBZV3dpTENKVmJtbDJaWEp6YVhSNVJHVm5jbVZsUTNKbFpHVnVkR2xoYkNKZExDSmpjbVZrWlc1MGFXRnNVM1ZpYW1WamRDST + ZleUprWldkeVpXVWlPbnNpZEhsd1pTSTZJa0poWTJobGJHOXlSR1ZuY21WbElpd2libUZ0WlNJNklqeHpjR0Z1SUd4aGJtYzlKMlp5TFVOQk + p6NUNZV05qWVd4aGRYTERxV0YwSUdWdUlHMTFjMmx4ZFdWeklHNTFiY09wY21seGRXVnpQQzl6Y0dGdVBpSjlmWDE5LktMSm81R0F5Qk5EM0 + xEVG45SDdGUW9rRXNVRWk4akt3WGhHdm9OM0p0UmE1MXhyTkRnWERiMGNxMVVUWUItcks0RnQ5WVZtUjFOSV9aT0Y4b0djXzd3QXA4UEhiRj + JIYVdvZFFJb09CeHhULTRXTnFBeGZ0N0VUNmxrSC00UzZVeDNyU0dBbWN6TW9oRUVmOGVDZU4takM4V2VrZFBsNnpLWlFqMFlQQjFyeDZYMC + 14bEZCczdjbDZXdDhyZkJQX3RaOVlnVldyUW1VV3lwU2lvYzBNVXlpcGhteUViTFphZ1R5UGxVeWZsR2xFZHFyWkF2NmVTZTZSdHhKeTZNMS + 1sRDdhNUhUemFuWVRXQlBBVUhEWkd5R0tYZEp3LVdfeDBJV0NoQnpJOHQza3BHMjUzZmc2VjN0UGdIZUtYRTk0ZnpfUXBZZmctLTdrTHN5Qk + FmUUdiZyJ9fQ.31tK3Nd0c8F7cqeS-2wBsQesPMCNNqq1v_9WDRVYksG8RxdVizLSb28KTmfqoxoFNVpVxCOTsit_GWndEhARbZxH3ASgjkn + 3Q46N2216r7psE3a_kUIFC80vetVFDnKMF5B8mlgmiFl6S6fNGAgmJO-VDYw6_pfGvyqzApm7UBTgzALhX6K__uCH9JhQydWrSbwzfcx1E7P + uyttAjCTBgjIGDLIyVyOtuxcaa4gCFmh2sXt3EvW5J5swVSyCfQu5cJlnA28yHd2jxI2Ry8QmQy65RLFhnDVWBT-zPjxvu0A1lOjy70R8jyl + 7jR40aoI0-hQJ5LFWJjmXPbbiR4MIkg + """; + + String EXAMPLE_JWT_VP_WITH_LDP_VC = """ + eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOjB4YWJjI2tleTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTplYm + ZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJ1cm46dXVpZDozOTc4MzQ0Zi04NTk2LTRjM2EtYTk3OC04ZmNhYmEzOTAzYzUiLC + JhdWQiOiJkaWQ6ZXhhbXBsZTo0YTU3NTQ2OTczNDM2ZjZmNmM0YTRhNTc1NzMiLCJuYmYiOjE1NDE0OTM3MjQsImlhdCI6MTU0MTQ5MzcyNC + wiZXhwIjoxNTczMDI5NzIzLCJub25jZSI6IjM0M3MkRlNGRGEtIiwidnAiOnsiQGNvbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTg + vY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy9leGFtcGxlcy92MSJdLCJ0eXBlIjpbIlZlcml + maWFibGVQcmVzZW50YXRpb24iLCJDcmVkZW50aWFsTWFuYWdlclByZXNlbnRhdGlvbiJdLCJ2ZXJpZmlhYmxlQ3JlZGVudGlhbCI6W3siQGN + vbnRleHQiOlsiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW5 + 0aWFscy9leGFtcGxlcy92MSIsImh0dHBzOi8vdzNpZC5vcmcvc2VjdXJpdHkvc3VpdGVzL2VkMjU1MTktMjAyMC92MSJdLCJpZCI6Imh0dHA + 6Ly9leGFtcGxlLmVkdS9jcmVkZW50aWFscy8zNzMyIiwidHlwZSI6WyJWZXJpZmlhYmxlQ3JlZGVudGlhbCIsIlVuaXZlcnNpdHlEZWdyZWV + DcmVkZW50aWFsIl0sImlzc3VlciI6Imh0dHBzOi8vZXhhbXBsZS5lZHUvaXNzdWVycy8xNCIsImlzc3VhbmNlRGF0ZSI6IjIwMTAtMDEtMD + FUMTk6MjM6MjRaIiwiY3JlZGVudGlhbFN1YmplY3QiOnsiaWQiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLC + JkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IkJhY2hlbG9yIG9mIFNjaWVuY2UgYW5kIEFydHMifX0sImNyZWRlbn + RpYWxTdGF0dXMiOnsiaWQiOiJodHRwczovL2V4YW1wbGUuZWR1L3N0YXR1cy8yNCIsInR5cGUiOiJDcmVkZW50aWFsU3RhdHVzTGlzdDIwMT + cifSwicHJvb2YiOnsidHlwZSI6IkVkMjU1MTlTaWduYXR1cmUyMDIwIiwiY3JlYXRlZCI6IjIwMjItMDItMjVUMTQ6NTg6NDNaIiwidmVyaWZ + pY2F0aW9uTWV0aG9kIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzE0I2tleS0xIiwicHJvb2ZQdXJwb3NlIjoiYXNzZXJ0aW9uTWV0 + aG9kIiwicHJvb2ZWYWx1ZSI6InozQlhzRmZ4MXFKNU5zVGtLcVJFalEzQUdoNlJBbUN3dmd1MUhjRFN6SzNQNVFFZzJUQXc4dWZrdEpCdzhRa + 0FRUmNpTUd5QmY1VDJBSHlSZzJ3MTNVdmhwIn19XX19.GcAjYFJm6KmqZjiYUocN8vEB_UDtKJOl29thxJrWYQeA5HcSAYip_fMvqxbqY7SSN + 2gdTKkZmZhK0SYTrl-zIXIUlB011PYkIWM4WIlr956BQPAHdYA-gosr8KfFX6Jr1-k0c6xYNt-1sWhtrsXepPNqmTI9kIsGL5hpCyMrvnlak7 + rsm3sqVy7PYV_vCzElLqReF7unVTsxhdVIQurLiKfQ66JZTrIty-CZ5F-VulKe5Qzbxgz1-YQa1QDPn9uYWfA4_MxP7ukg6cIky8HgK2iIPBoz + ZKjRop7QKEM07xL3aP_2gfzXej35qptGtE8y48pWdOkURYFhcMgymCsV7Q + """; + + String EXAMPLE_JWT_VC = """ + eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ2YyI6eyJAY29udGV4dCI6WyJodHRwczovL3 + d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L + 2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YxIl0sImlkIjoiaHR0cDovL2V4YW1wbGUuZWR1L2NyZWRl + bnRpYWxzLzM3MzIiLCJ0eXBlIjpbIlZlcmlmaWFibGVDcmVkZW50aWFsIiwiVW5pdmVyc2l0eUR + lZ3JlZUNyZWRlbnRpYWwiXSwiaXNzdWVyIjoiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLz + E0IiwiaXNzdWFuY2VEYXRlIjoiMjAxMC0wMS0wMVQxOToyMzoyNFoiLCJjcmVkZW50aWFsU3Via + mVjdCI6eyJpZCI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSIsImRl + Z3JlZSI6eyJ0eXBlIjoiQmFjaGVsb3JEZWdyZWUiLCJuYW1lIjoiQmFjaGVsb3Igb2YgU2NpZW5 + jZSBhbmQgQXJ0cyJ9fSwiY3JlZGVudGlhbFN0YXR1cyI6eyJpZCI6Imh0dHBzOi8vZXhhbXBsZS + 5lZHUvc3RhdHVzLzI0IiwidHlwZSI6IkNyZWRlbnRpYWxTdGF0dXNMaXN0MjAxNyJ9fSwiaXNzI + joiaHR0cHM6Ly9leGFtcGxlLmVkdS9pc3N1ZXJzLzE0IiwibmJmIjoxMjYyMzczODA0LCJqdGki + OiJodHRwOi8vZXhhbXBsZS5lZHUvY3JlZGVudGlhbHMvMzczMiIsInN1YiI6ImRpZDpleGFtcGx + lOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMSJ9.YQKQUu_zreDs69AZ8YqpMGHLl9V_tWH4N + S9P9l67J1wWHf0QCyt5hyuA8ckM4seV-1TRbeiHwdJ3VRkDMcwFcg + """; } diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformerTest.java new file mode 100644 index 00000000000..8c27f729841 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformerTest.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.transform.to; + +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VC; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; + +class JwtToVerifiableCredentialTransformerTest { + private final Monitor monitor = mock(); + private final JwtToVerifiableCredentialTransformer transformer = new JwtToVerifiableCredentialTransformer(monitor); + private final TransformerContext context = mock(); + + @Test + void transform_success() { + var vc = transformer.transform(EXAMPLE_JWT_VC, context); + + assertThat(vc).isNotNull(); + assertThat(vc.getTypes()).doesNotContainNull().isNotEmpty(); + assertThat(vc.getCredentialStatus()).isNotNull(); + assertThat(vc.getCredentialSubject()).doesNotContainNull().isNotEmpty(); + + verifyNoInteractions(context); + } +} \ No newline at end of file diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java new file mode 100644 index 00000000000..a2262e7a812 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2023 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) + * + * 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: + * Bayerische Motoren Werke Aktiengesellschaft (BMW AG) - initial API and implementation + * + */ + +package org.eclipse.edc.iam.identitytrust.transform.to; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.JsonObject; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.transform.spi.TransformerContext; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_EMPTY_CREDENTIALS_ARRAY; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_SINGLE_VC; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_WITH_LDP_VC; +import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXMPLE_JWT_VP_NO_VP_CLAIM; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class JwtToVerifiablePresentationTransformerTest { + + private static final ObjectMapper MAPPER = JacksonJsonLd.createObjectMapper(); + private final TransformerContext context = mock(); + private final Monitor monitor = mock(); + private final JwtToVerifiablePresentationTransformer transformer = new JwtToVerifiablePresentationTransformer(monitor, MAPPER); + private final JwtToVerifiableCredentialTransformer credentialTransformer = new JwtToVerifiableCredentialTransformer(monitor); + private final JsonObjectToVerifiableCredentialTransformer credentialTransformerJson = new JsonObjectToVerifiableCredentialTransformer(); + + @BeforeEach + void setup() { + when(context.transform(isA(String.class), eq(VerifiableCredential.class))) + .thenAnswer(a -> credentialTransformer.transform(a.getArgument(0), context)); + } + + @Test + void transform_success() { + var vp = transformer.transform(EXAMPLE_JWT_VP, context); + assertThat(vp).isNotNull(); + assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation", "CredentialManagerPresentation"); + assertThat(vp.getCredentials()).hasSize(1).allSatisfy(vc -> { + assertThat(vc.getCredentialSubject()).isNotEmpty(); + assertThat(vc.getTypes()).isNotEmpty(); + }); + } + + @Test + @DisplayName("VP has a single 'verifiableCredential' (not an array)") + void transform_success_singleCredential() { + var vp = transformer.transform(EXAMPLE_JWT_VP_SINGLE_VC, context); + assertThat(vp).isNotNull(); + assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation", "CredentialManagerPresentation"); + assertThat(vp.getCredentials()).hasSize(1) + .doesNotContainNull() + .allSatisfy(vc -> { + assertThat(vc.getCredentialSubject()).isNotEmpty(); + assertThat(vc.getTypes()).isNotEmpty(); + }); + } + + @Test + @DisplayName("JWT does not contain a 'vp' claim") + void transform_noVpClaim() { + var vp = transformer.transform(EXMPLE_JWT_VP_NO_VP_CLAIM, context); + assertThat(vp).isNull(); + verify(context).reportProblem(eq("Could not parse VerifiablePresentation from JWT.")); + } + + + @Test + @DisplayName("VP claim contains an empty 'verifiableCredentials' array") + void transform_vpClaimWithoutCredentials() { + var vp = transformer.transform(EXAMPLE_JWT_VP_EMPTY_CREDENTIALS_ARRAY, context); + assertThat(vp).isNotNull(); + assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation", "CredentialManagerPresentation"); + assertThat(vp.getCredentials()).isEmpty(); + } + + @Test + @DisplayName("VP claim contains a single LDP-VC") + void transform_containsLdpVc() { + when(context.transform(isA(JsonObject.class), eq(VerifiableCredential.class))) + .thenAnswer(a -> credentialTransformerJson.transform(a.getArgument(0), context)); + var vp = transformer.transform(EXAMPLE_JWT_VP_WITH_LDP_VC, context); + assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation", "CredentialManagerPresentation"); + assertThat(vp.getCredentials()).hasSize(1) + .doesNotContainNull() + .allSatisfy(vc -> { + assertThat(vc.getCredentialSubject()).isNotEmpty(); + assertThat(vc.getTypes()).isNotEmpty(); + }); + } + + @Test + @DisplayName("String is not a valid JWT") + void transform_inputIsNotValidJwt() { + assertThat(transformer.transform("foobar", context)).isNull(); + verify(context).reportProblem("Could not parse VerifiablePresentation from JWT."); + } +} \ No newline at end of file diff --git a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java index 2c401df683a..04583f513cb 100644 --- a/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java +++ b/spi/common/identity-trust-spi/src/main/java/org/eclipse/edc/identitytrust/model/VerifiablePresentation.java @@ -95,10 +95,6 @@ public VerifiablePresentation build() { if (instance.types == null || instance.types.isEmpty()) { throw new IllegalArgumentException("VerifiablePresentation must have at least one type."); } - // these next two aren't mandated by the spec, but there is no point in having a VP without credentials or a proof. - if (instance.credentials == null || instance.credentials.isEmpty()) { - throw new IllegalArgumentException("VerifiablePresentation must have at least one credential."); - } return instance; } } diff --git a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java index 494bfaacaa0..702acb6d533 100644 --- a/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java +++ b/spi/common/identity-trust-spi/src/test/java/org/eclipse/edc/identitytrust/model/VerifiablePresentationTest.java @@ -46,10 +46,8 @@ void build_noType() { @Test void build_noCredentials() { - assertThatThrownBy(() -> VerifiablePresentation.Builder.newInstance() + assertThatNoException().isThrownBy(() -> VerifiablePresentation.Builder.newInstance() .types(List.of("test-type")) - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageEndingWith("must have at least one credential."); + .build()); } } From 2cd7a8bff51c47844745ef90ece9fc8b61b76f92 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Thu, 16 Nov 2023 15:12:34 +0100 Subject: [PATCH 3/7] DEPENDENCIES --- DEPENDENCIES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index abf0ffadfdb..3611853887b 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -160,7 +160,7 @@ maven/mavencentral/io.swagger.parser.v3/swagger-parser/2.1.10, None, restricted, maven/mavencentral/io.swagger/swagger-annotations/1.6.9, Apache-2.0, approved, #3792 maven/mavencentral/io.swagger/swagger-compat-spec-parser/1.0.64, None, restricted, #11479 maven/mavencentral/io.swagger/swagger-core/1.6.9, Apache-2.0, approved, #4358 -maven/mavencentral/io.swagger/swagger-models/1.6.9, LicenseRef-scancode-proprietary-license, restricted, #11476 +maven/mavencentral/io.swagger/swagger-models/1.6.9, Apache-2.0, approved, #11476 maven/mavencentral/io.swagger/swagger-parser/1.0.64, Apache-2.0, approved, #4359 maven/mavencentral/jakarta.activation/jakarta.activation-api/1.2.1, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf maven/mavencentral/jakarta.activation/jakarta.activation-api/2.1.0, EPL-2.0 OR BSD-3-Clause OR GPL-2.0-only with Classpath-exception-2.0, approved, ee4j.jaf From fb63e62e3aa3d17805fe1bd119c98a0baab18a0b Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Nov 2023 15:46:43 +0100 Subject: [PATCH 4/7] fix tests --- .../IdentityTrustTransformExtension.java | 2 +- ...wtToVerifiablePresentationTransformer.java | 16 +++- ...VerifiablePresentationTransformerTest.java | 32 +++++++- .../src/test/resources/expanded_vc.json | 77 +++++++++++++++++++ 4 files changed, 119 insertions(+), 8 deletions(-) create mode 100644 extensions/common/iam/identity-trust/identity-trust-transform/src/test/resources/expanded_vc.json diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java index 42bdaf2863c..47b6039b30f 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/IdentityTrustTransformExtension.java @@ -59,7 +59,7 @@ public void initialize(ServiceExtensionContext context) { typeTransformerRegistry.register(new JsonObjectToPresentationQueryTransformer(typeManager.getMapper(JSON_LD))); typeTransformerRegistry.register(new JsonObjectFromPresentationQueryTransformer()); - typeTransformerRegistry.register(new JwtToVerifiablePresentationTransformer(context.getMonitor(), typeManager.getMapper(JSON_LD))); + typeTransformerRegistry.register(new JwtToVerifiablePresentationTransformer(context.getMonitor(), typeManager.getMapper(JSON_LD), jsonLdService)); typeTransformerRegistry.register(new JwtToVerifiableCredentialTransformer(context.getMonitor())); } diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java index 542a79560b6..adb98bfa934 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformer.java @@ -19,6 +19,7 @@ import jakarta.json.JsonObject; import org.eclipse.edc.identitytrust.model.VerifiableCredential; import org.eclipse.edc.identitytrust.model.VerifiablePresentation; +import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.transform.spi.TransformerContext; import org.eclipse.edc.transform.spi.TypeTransformer; @@ -29,16 +30,19 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Objects; @SuppressWarnings("unchecked") public class JwtToVerifiablePresentationTransformer implements TypeTransformer { private static final String VP_CLAIM = "vp"; private final Monitor monitor; private final ObjectMapper objectMapper; + private final JsonLd jsonLd; - public JwtToVerifiablePresentationTransformer(Monitor monitor, ObjectMapper objectMapper) { + public JwtToVerifiablePresentationTransformer(Monitor monitor, ObjectMapper objectMapper, JsonLd jsonLd) { this.monitor = monitor; this.objectMapper = objectMapper; + this.jsonLd = jsonLd; } @Override @@ -92,8 +96,14 @@ private List extractCredentials(Object credentialsObject, return context.transform(obj.toString(), VerifiableCredential.class); } else { // VC is LDP var input = objectMapper.convertValue(obj, JsonObject.class); - return context.transform(input, VerifiableCredential.class); + var expansion = jsonLd.expand(input); + if (expansion.succeeded()) { + return context.transform(expansion.getContent(), VerifiableCredential.class); + } + context.reportProblem("Error expanding embedded VC: %s".formatted(expansion.getFailureDetail())); + return null; } - }).toList(); + + }).filter(Objects::nonNull).toList(); } } diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java index a2262e7a812..dc438dafabe 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiablePresentationTransformerTest.java @@ -14,25 +14,37 @@ package org.eclipse.edc.iam.identitytrust.transform.to; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.json.JsonObject; +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.eclipse.edc.jsonld.spi.JsonLd; import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.junit.testfixtures.TestUtils; import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; import org.eclipse.edc.transform.spi.TransformerContext; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import java.time.Instant; +import java.util.Map; + import static org.assertj.core.api.Assertions.assertThat; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_EMPTY_CREDENTIALS_ARRAY; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_SINGLE_VC; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXAMPLE_JWT_VP_WITH_LDP_VC; import static org.eclipse.edc.iam.identitytrust.transform.TestData.EXMPLE_JWT_VP_NO_VP_CLAIM; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,9 +53,9 @@ class JwtToVerifiablePresentationTransformerTest { private static final ObjectMapper MAPPER = JacksonJsonLd.createObjectMapper(); private final TransformerContext context = mock(); private final Monitor monitor = mock(); - private final JwtToVerifiablePresentationTransformer transformer = new JwtToVerifiablePresentationTransformer(monitor, MAPPER); private final JwtToVerifiableCredentialTransformer credentialTransformer = new JwtToVerifiableCredentialTransformer(monitor); - private final JsonObjectToVerifiableCredentialTransformer credentialTransformerJson = new JsonObjectToVerifiableCredentialTransformer(); + private final JsonLd jsonLd = mock(); + private final JwtToVerifiablePresentationTransformer transformer = new JwtToVerifiablePresentationTransformer(monitor, MAPPER, jsonLd); @BeforeEach void setup() { @@ -96,10 +108,21 @@ void transform_vpClaimWithoutCredentials() { @Test @DisplayName("VP claim contains a single LDP-VC") - void transform_containsLdpVc() { + void transform_containsLdpVc() throws JsonProcessingException { + when(jsonLd.expand(any())) + .thenReturn(Result.success(JacksonJsonLd.createObjectMapper() + .readValue(TestUtils.getResourceFileContentAsString("expanded_vc.json"), JsonObject.class))); + when(context.transform(isA(JsonObject.class), eq(VerifiableCredential.class))) - .thenAnswer(a -> credentialTransformerJson.transform(a.getArgument(0), context)); + .thenAnswer(a -> VerifiableCredential.Builder.newInstance().type("VerifiableCredential") + .credentialSubject(CredentialSubject.Builder.newInstance().id("test-subj").claim("key", "val").build()) + .issuer(new Issuer("test-issuer", Map.of())) + .issuanceDate(Instant.now()) + .build()); + + var vp = transformer.transform(EXAMPLE_JWT_VP_WITH_LDP_VC, context); + assertThat(vp).isNotNull(); assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation", "CredentialManagerPresentation"); assertThat(vp.getCredentials()).hasSize(1) .doesNotContainNull() @@ -107,6 +130,7 @@ void transform_containsLdpVc() { assertThat(vc.getCredentialSubject()).isNotEmpty(); assertThat(vc.getTypes()).isNotEmpty(); }); + verify(context, never()).reportProblem(anyString()); } @Test diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/test/resources/expanded_vc.json b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/resources/expanded_vc.json new file mode 100644 index 00000000000..bbfa5315762 --- /dev/null +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/test/resources/expanded_vc.json @@ -0,0 +1,77 @@ +{ + "@id": "http://example.edu/credentials/3732", + "@type": [ + "https://www.w3.org/2018/credentials#VerifiableCredential", + "https://example.org/examples#UniversityDegreeCredential" + ], + "https://www.w3.org/2018/credentials#issuer": [ + { + "@id": "https://example.edu/issuers/14" + } + ], + "https://www.w3.org/2018/credentials#issuanceDate": [ + { + "@value": "2010-01-01T19:23:24Z", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + ], + "https://www.w3.org/2018/credentials#credentialSubject": [ + { + "@id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "https://example.org/examples#degree": [ + { + "@type": [ + "https://example.org/examples#BachelorDegree" + ], + "http://schema.org/name": [ + { + "@value": "Bachelor of Science and Arts", + "@type": "http://www.w3.org/1999/02/22-rdf-syntax-ns#HTML" + } + ] + } + ] + } + ], + "https://www.w3.org/2018/credentials#credentialStatus": [ + { + "@id": "https://example.edu/status/24", + "@type": [ + "CredentialStatusList2017" + ] + } + ], + "https://w3id.org/security#proof": [ + { + "@graph": [ + { + "@type": [ + "https://w3id.org/security#Ed25519Signature2020" + ], + "http://purl.org/dc/terms/created": [ + { + "@value": "2022-02-25T14:58:43Z", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + } + ], + "https://w3id.org/security#verificationMethod": [ + { + "@id": "https://example.edu/issuers/14#key-1" + } + ], + "https://w3id.org/security#proofPurpose": [ + { + "@id": "https://w3id.org/security#assertionMethod" + } + ], + "https://w3id.org/security#proofValue": [ + { + "@value": "z3BXsFfx1qJ5NsTkKqREjQ3AGh6RAmCwvgu1HcDSzK3P5QEg2TAw8ufktJBw8QkAQRciMGyBf5T2AHyRg2w13Uvhp", + "@type": "https://w3id.org/security#multibase" + } + ] + } + ] + } + ] +} \ No newline at end of file From 78ee0a5231dfa440b0858950c63f801f1c5bb10c Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Nov 2023 18:41:48 +0100 Subject: [PATCH 5/7] pr remarks --- .../to/JwtToVerifiableCredentialTransformer.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java index 22a24dbd684..85a22cd098e 100644 --- a/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java +++ b/extensions/common/iam/identity-trust/identity-trust-transform/src/main/java/org/eclipse/edc/iam/identitytrust/transform/to/JwtToVerifiableCredentialTransformer.java @@ -35,9 +35,11 @@ import static java.util.Optional.ofNullable; public class JwtToVerifiableCredentialTransformer implements TypeTransformer { - public static final String TYPE_CLAIM = "type"; public static final String EXPIRATION_DATE_PROPERTY = "expirationDate"; public static final String ISSUANCE_DATE_PROPERTY = "issuanceDate"; + public static final String ID_PROPERTY = "id"; + public static final String TYPE_PROPERTY = "type"; + public static final String TYPE_CLAIM = TYPE_PROPERTY; private static final String VC_CLAIM = "vc"; private static final String SUBJECT_CLAIM = "credentialSubject"; private static final String CREDENTIAL_STATUS_PROPERTY = "credentialStatus"; @@ -103,8 +105,8 @@ private CredentialStatus extractStatus(Map status) { if (status == null || status.isEmpty()) { return null; } - var id = status.remove("id").toString(); - var type = status.remove("type").toString(); + var id = status.remove(ID_PROPERTY).toString(); + var type = status.remove(TYPE_PROPERTY).toString(); return new CredentialStatus(id, type, status); } From 9416bcdcbfc12fb62956e61c489fc78a9f2be6c1 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Fri, 17 Nov 2023 19:20:03 +0100 Subject: [PATCH 6/7] use transformer --- .../core/defaults/DefaultCredentialServiceClient.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java index 3cb104ae590..afee99d55b2 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java @@ -132,8 +132,10 @@ private Result parseJwtVp(String rawJwt) { var jwt = SignedJWT.parse(rawJwt); var claims = jwt.getJWTClaimsSet(); var vp = claims.getClaim("vp"); - //todo: parse JWT VP - return success(new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, null)); + + return transformerRegistry.transform(rawJwt, VerifiablePresentation.class) + .map(pres -> new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, null)); + } catch (ParseException e) { monitor.warning("Failed to parse JWT VP", e); return failure("Failed to parse JWT VP: %s".formatted(e.getMessage())); From 964f08802cf6f13cd8b8482e064cf4981ff1dc93 Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Sat, 18 Nov 2023 09:25:16 +0100 Subject: [PATCH 7/7] fix tests --- .../defaults/DefaultCredentialServiceClient.java | 15 ++------------- .../DefaultCredentialServiceClientTest.java | 4 +--- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java index afee99d55b2..28327e4fb78 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClient.java @@ -16,7 +16,6 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; -import com.nimbusds.jwt.SignedJWT; import jakarta.json.JsonBuilderFactory; import jakarta.json.JsonObject; import okhttp3.MediaType; @@ -38,7 +37,6 @@ import org.eclipse.edc.transform.spi.TypeTransformerRegistry; import java.io.IOException; -import java.text.ParseException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -128,18 +126,9 @@ private Result parseLdpVp(Object vpObj) { } private Result parseJwtVp(String rawJwt) { - try { - var jwt = SignedJWT.parse(rawJwt); - var claims = jwt.getJWTClaimsSet(); - var vp = claims.getClaim("vp"); - - return transformerRegistry.transform(rawJwt, VerifiablePresentation.class) - .map(pres -> new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, null)); + return transformerRegistry.transform(rawJwt, VerifiablePresentation.class) + .map(pres -> new VerifiablePresentationContainer(rawJwt, CredentialFormat.JWT, pres)); - } catch (ParseException e) { - monitor.warning("Failed to parse JWT VP", e); - return failure("Failed to parse JWT VP: %s".formatted(e.getMessage())); - } } private JsonObject createPresentationQuery(List scopes) { diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java index 3bfd8435c3c..bc1f453b709 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/defaults/DefaultCredentialServiceClientTest.java @@ -15,7 +15,6 @@ package org.eclipse.edc.iam.identitytrust.core.defaults; import jakarta.json.Json; -import jakarta.json.JsonObject; import okhttp3.MediaType; import okhttp3.Protocol; import okhttp3.Response; @@ -45,7 +44,6 @@ import static org.eclipse.edc.spi.result.Result.success; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -58,7 +56,7 @@ class DefaultCredentialServiceClientTest { @BeforeEach void setup() { var registry = mock(TypeTransformerRegistry.class); - when(registry.transform(isA(JsonObject.class), eq(VerifiablePresentation.class))) + when(registry.transform(any(), eq(VerifiablePresentation.class))) .thenReturn(success(createPresentation())); var jsonLdMock = mock(JsonLd.class); when(jsonLdMock.expand(any())).thenAnswer(a -> success(a.getArgument(0)));