diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java index e360ac8c2..5044a824c 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java @@ -65,6 +65,6 @@ public T createPresentation(String participantContextId, List createPresentation(String participant var vpToken = new ArrayList<>(); - Map additionalDataJwt = audience != null ? Map.of("aud", audience) : Map.of(); + var additionalDataJwt = new HashMap(); + ofNullable(audience).ifPresent(aud -> additionalDataJwt.put(AUDIENCE, audience)); + additionalDataJwt.put(JwtRegisteredClaimNames.AUDIENCE, audience); + additionalDataJwt.put("controller", participantContextId); if (defaultFormatVp == JSON_LD) { // LDP-VPs cannot contain JWT VCs if (!ldpVcs.isEmpty()) { diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGenerator.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGenerator.java index 43ea65adf..9c101eee7 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGenerator.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGenerator.java @@ -22,6 +22,7 @@ import org.eclipse.edc.spi.EdcException; import org.eclipse.edc.spi.iam.TokenRepresentation; import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.token.spi.KeyIdDecorator; import org.eclipse.edc.token.spi.TokenDecorator; import org.eclipse.edc.token.spi.TokenGenerationService; @@ -40,6 +41,7 @@ import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.PRESENTATION_EXCHANGE_URL; import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.VERIFIABLE_PRESENTATION_TYPE; import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.W3C_CREDENTIALS_URL; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.AUDIENCE; import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME; import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT; import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER; @@ -74,38 +76,43 @@ public JwtPresentationGenerator(PrivateKeyResolver privateKeyResolver, Clock clo /** * Will always throw an {@link UnsupportedOperationException}. - * Please use {@link JwtPresentationGenerator#generatePresentation(List, String, Map)} instead. + * Please use {@link JwtPresentationGenerator#generatePresentation(List, String, String, Map)} instead. */ @Override - public String generatePresentation(List credentials, String keyId) { + public String generatePresentation(List credentials, String privateKeyAlias, String publicKeyId) { throw new UnsupportedOperationException("Must provide additional data: 'aud'"); } /** * Creates a presentation using the given Verifiable Credential Containers and additional data. * - * @param credentials The list of Verifiable Credential Containers to include in the presentation. - * @param privateKeyId The key ID of the private key to be used for generating the presentation. - * @param additionalData Additional data to include in the presentation. Must contain an entry 'aud'. Every entry in the map is added as a claim to the token. + * @param credentials The list of Verifiable Credential Containers to include in the presentation. + * @param privateKeyAlias The alias of the private key to be used for generating the presentation. + * @param publicKeyId The ID used by the counterparty to resolve the public key for verifying the VP. + * @param additionalData Additional data to include in the presentation. Must contain an entry 'aud'. Every entry in the map is added as a claim to the token. * @return The serialized JWT presentation. * @throws IllegalArgumentException If the additional data does not contain the required 'aud' value or if no private key could be resolved for the key ID. * @throws UnsupportedOperationException If the private key does not provide any supported JWS algorithms. * @throws EdcException If signing the JWT fails. */ @Override - public String generatePresentation(List credentials, String privateKeyId, Map additionalData) { + public String generatePresentation(List credentials, String privateKeyAlias, String publicKeyId, Map additionalData) { // check if expected data is there - if (!additionalData.containsKey("aud")) { - throw new IllegalArgumentException("Must provide additional data: 'aud'"); + if (!additionalData.containsKey(AUDIENCE)) { + throw new IllegalArgumentException("Must provide additional data: '%s'".formatted(AUDIENCE)); + } + + if (!additionalData.containsKey("controller")) { + throw new IllegalArgumentException("Must provide additional data: 'controller'"); } var rawVcs = credentials.stream().map(VerifiableCredentialContainer::rawVc); - Supplier privateKeySupplier = () -> privateKeyResolver.resolvePrivateKey(privateKeyId).orElseThrow(f -> new IllegalArgumentException(f.getFailureDetail())); + Supplier privateKeySupplier = () -> privateKeyResolver.resolvePrivateKey(privateKeyAlias).orElseThrow(f -> new IllegalArgumentException(f.getFailureDetail())); var tokenResult = tokenGenerationService.generate(privateKeySupplier, vpDecorator(rawVcs), tp -> { additionalData.forEach(tp::claims); return tp; - }); + }, new KeyIdDecorator(additionalData.get("controller") + "#" + publicKeyId)); return tokenResult.map(TokenRepresentation::getToken).orElseThrow(f -> new EdcException(f.getFailureDetail())); } diff --git a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGenerator.java b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGenerator.java index 586a8a5ba..f52e48c43 100644 --- a/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGenerator.java +++ b/core/identity-hub-credentials/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGenerator.java @@ -75,10 +75,10 @@ public LdpPresentationGenerator(PrivateKeyResolver privateKeyResolver, String ow /** * Will always throw an {@link UnsupportedOperationException}. - * Please use {@link LdpPresentationGenerator#generatePresentation(List, String, Map)} instead. + * Please use {@link LdpPresentationGenerator#generatePresentation(List, String, String, Map)} instead. */ @Override - public JsonObject generatePresentation(List credentials, String keyId) { + public JsonObject generatePresentation(List credentials, String privateKeyAlias, String privateKeyId) { throw new UnsupportedOperationException("Must provide additional data: 'types'"); } @@ -87,11 +87,12 @@ public JsonObject generatePresentation(List crede * Creates a presentation with the given credentials, key ID, and additional data. Note that JWT-VCs cannot be represented in LDP-VPs - while the spec would allow that * the JSON schema does not. * - * @param credentials The list of Verifiable Credential Containers to include in the presentation. - * @param keyId The key ID of the private key to be used for generating the presentation. Must be a URI. - * @param additionalData The additional data to be included in the presentation. - * It must contain a "types" field and optionally, a "suite" field to indicate the desired signature suite. - * If the "suite" parameter is specified, it must be a W3C identifier for signature suites. + * @param credentials The list of Verifiable Credential Containers to include in the presentation. + * @param privateKeyAlias The alias of the private key to be used for generating the presentation. + * @param publicKeyId The ID used by the counterparty to resolve the public key for verifying the VP. + * @param additionalData The additional data to be included in the presentation. + * It must contain a "types" field and optionally, a "suite" field to indicate the desired signature suite. + * If the "suite" parameter is specified, it must be a W3C identifier for signature suites. * @return The created presentation as a JsonObject. * @throws IllegalArgumentException If the additional data does not contain "types", * if no {@link SignatureSuite} is found for the provided suite identifier, @@ -99,7 +100,7 @@ public JsonObject generatePresentation(List crede * or if one or more VerifiableCredentials cannot be represented in the JSON-LD format. */ @Override - public JsonObject generatePresentation(List credentials, String keyId, Map additionalData) { + public JsonObject generatePresentation(List credentials, String privateKeyAlias, String publicKeyId, Map additionalData) { if (!additionalData.containsKey("types")) { throw new IllegalArgumentException("Must provide additional data: 'types'"); } @@ -118,7 +119,7 @@ public JsonObject generatePresentation(List crede } // check if private key can be resolved - var pk = privateKeyResolver.resolvePrivateKey(keyId) + var pk = privateKeyResolver.resolvePrivateKey(privateKeyAlias) .orElseThrow(f -> new IllegalArgumentException(f.getFailureDetail())); var types = (List) additionalData.get("types"); @@ -130,7 +131,7 @@ public JsonObject generatePresentation(List crede .add(VERIFIABLE_CREDENTIAL_PROPERTY, toJsonArray(credentials)) .build(); - return signPresentation(presentationObject, suite, pk, keyId, additionalData.get("controller").toString()); + return signPresentation(presentationObject, suite, pk, publicKeyId, additionalData.get("controller").toString()); } @NotNull @@ -149,8 +150,8 @@ private JsonArray toJsonArray(List credentials) { return array.build(); } - private JsonObject signPresentation(JsonObject presentationObject, SignatureSuite suite, PrivateKey pk, String keyId, String controller) { - var keyIdUri = URI.create(keyId); + private JsonObject signPresentation(JsonObject presentationObject, SignatureSuite suite, PrivateKey pk, String publicKeyId, String controller) { + var keyIdUri = URI.create(publicKeyId); var controllerUri = URI.create(controller); var type = URI.create(suite.getId().toString()); @@ -159,7 +160,7 @@ private JsonObject signPresentation(JsonObject presentationObject, SignatureSuit var options = (DataIntegrityProofOptions) suite.createOptions(); options.purpose(URI.create("https://w3id.org/security#assertionMethod")); - options.verificationMethod(new JwkMethod(URI.create(controller + "#" + keyId), null, controllerUri, null)); + options.verificationMethod(new JwkMethod(URI.create(controller + "#" + publicKeyId), null, controllerUri, null)); return ldpIssuer.signDocument(presentationObject, keypair, options) .orElseThrow(f -> new EdcException(f.getFailureDetail())); } diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java index 046eb00c7..f8d0f8416 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImplTest.java @@ -54,7 +54,7 @@ void createPresentation_whenSingleKey() { var generator = mock(PresentationGenerator.class); registry.addCreator(generator, CredentialFormat.JWT); assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); - verify(generator).generatePresentation(anyList(), eq(keyPair.getKeyId()), anyMap()); + verify(generator).generatePresentation(anyList(), eq(keyPair.getPrivateKeyAlias()), eq(keyPair.getKeyId()), anyMap()); } @Test @@ -78,7 +78,10 @@ void createPresentation_whenNoDefaultKey() { var generator = mock(PresentationGenerator.class); registry.addCreator(generator, CredentialFormat.JWT); assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); - verify(generator).generatePresentation(anyList(), argThat(s -> s.equals("key-1") || s.equals("key-2")), anyMap()); + verify(generator).generatePresentation(anyList(), + argThat(s -> s.equals(keyPair1.getPrivateKeyAlias()) || s.equals(keyPair2.getPrivateKeyAlias())), + argThat(s -> s.equals(keyPair1.getKeyId()) || s.equals(keyPair2.getKeyId())), + anyMap()); } @@ -92,7 +95,7 @@ void createPresentation_whenDefaultKey() { var generator = mock(PresentationGenerator.class); registry.addCreator(generator, CredentialFormat.JWT); assertThatNoException().isThrownBy(() -> registry.createPresentation(TEST_PARTICIPANT, List.of(), CredentialFormat.JWT, Map.of())); - verify(generator).generatePresentation(anyList(), eq("key-2"), anyMap()); + verify(generator).generatePresentation(anyList(), eq(keyPair2.getPrivateKeyAlias()), eq(keyPair2.getKeyId()), anyMap()); } @Test @@ -113,6 +116,6 @@ private KeyPairResource.Builder createKeyPair(String participantId, String keyId .keyId(keyId) .state(KeyPairState.ACTIVE) .isDefaultPair(true) - .privateKeyAlias(participantId + "-alias"); + .privateKeyAlias("%s-%s-alias".formatted(participantId, keyId)); } } \ No newline at end of file diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java index f2b15869e..a8e2fdcaf 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/VerifiablePresentationServiceImplTest.java @@ -25,6 +25,7 @@ import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; import org.eclipse.edc.identitytrust.model.presentationdefinition.PresentationDefinition; import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames; import org.eclipse.edc.spi.monitor.Monitor; import org.junit.jupiter.api.Test; @@ -50,6 +51,7 @@ class VerifiablePresentationServiceImplTest { + private static final String TEST_AUDIENCE = "did:web:audience.com"; private static final String TEST_PARTICIPANT_CONTEXT_ID = "test-participant"; private final Monitor monitor = mock(); private final PresentationCreatorRegistry registry = mock(); @@ -75,7 +77,11 @@ void generate_defaultFormatLdp_containsOnlyLdpVc() { var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); assertThat(result).isSucceeded(); - verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JSON_LD), any()); + verify(registry).createPresentation( + eq(TEST_PARTICIPANT_CONTEXT_ID), + argThat(argument -> argument.size() == 2), + eq(JSON_LD), + argThat(additional -> TEST_PARTICIPANT_CONTEXT_ID.equals(additional.get("controller")))); } @Test @@ -101,9 +107,15 @@ void generate_defaultFormatLdp_onlyJwtVcs() { var credentials = List.of(createCredential(JWT), createCredential(JWT)); - var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, null); + var result = presentationGenerator.createPresentation(TEST_PARTICIPANT_CONTEXT_ID, credentials, null, TEST_AUDIENCE); assertThat(result).isSucceeded(); - verify(registry).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), argThat(argument -> argument.size() == 2), eq(JWT), any()); + verify(registry).createPresentation( + eq(TEST_PARTICIPANT_CONTEXT_ID), + argThat(argument -> argument.size() == 2), + eq(JWT), + argThat(additional -> TEST_PARTICIPANT_CONTEXT_ID.equals(additional.get("controller")) && + TEST_AUDIENCE.equals(additional.get(JwtRegisteredClaimNames.AUDIENCE))) + ); verify(registry, never()).createPresentation(eq(TEST_PARTICIPANT_CONTEXT_ID), any(), eq(JSON_LD), any()); verify(monitor).warning(eq("The VP was requested in JSON_LD format, but the request yielded 2 JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs")); } diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGeneratorTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGeneratorTest.java index f5957fecb..ab52c4e06 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGeneratorTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationGeneratorTest.java @@ -15,6 +15,7 @@ package org.eclipse.edc.identityhub.core.creators; import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.jwk.Curve; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; @@ -46,17 +47,20 @@ class JwtPresentationGeneratorTest extends PresentationGeneratorTest { public static final List REQUIRED_CLAIMS = asList("aud", "exp", "iat", "vp"); - private final Map audClaim = Map.of("aud", "did:web:test-audience"); - private final PrivateKeyResolver resolverMock = mock(); + private static final Map ADDITIONAL_DATA = Map.of( + "aud", "did:web:test-audience", + "controller", "did:web:test" + ); + private final PrivateKeyResolver privateKeyResolver = mock(); private final TokenGenerationService tokenGenerationService = new JwtGenerationService(); private JwtPresentationGenerator creator; @BeforeEach void setup() throws JOSEException { var vpSigningKey = createKey(Curve.P_384, "vp-key"); - when(resolverMock.resolvePrivateKey(any())).thenReturn(Result.failure("not found")); - when(resolverMock.resolvePrivateKey(eq(KEY_ID))).thenReturn(Result.success(vpSigningKey.toPrivateKey())); - creator = new JwtPresentationGenerator(resolverMock, Clock.systemUTC(), "did:web:test-issuer", tokenGenerationService); + when(privateKeyResolver.resolvePrivateKey(any())).thenReturn(Result.failure("not found")); + when(privateKeyResolver.resolvePrivateKey(eq(PRIVATE_KEY_ALIAS))).thenReturn(Result.success(vpSigningKey.toPrivateKey())); + creator = new JwtPresentationGenerator(privateKeyResolver, Clock.systemUTC(), "did:web:test-issuer", tokenGenerationService); } @Test @@ -66,14 +70,14 @@ void createPresentation_success() { var jwtVc = JwtCreationUtils.createJwt(vcSigningKey, TestConstants.CENTRAL_ISSUER_DID, "degreeSub", TestConstants.VP_HOLDER_ID, Map.of("vc", TestConstants.VC_CONTENT_DEGREE_EXAMPLE)); var vcc = new VerifiableCredentialContainer(jwtVc, CredentialFormat.JWT, createDummyCredential()); - var vpJwt = creator.generatePresentation(List.of(vcc), KEY_ID, audClaim); + var vpJwt = creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(vpJwt).isNotNull(); assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); - var claims = parseJwt(vpJwt); + var claims = extractJwtClaims(vpJwt); REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim) .isNotNull()); - + assertThat(extractJwtHeader(vpJwt).getKeyID()).isEqualTo("did:web:test#%s".formatted(PUBLIC_KEY_ID)); } @Test @@ -86,11 +90,11 @@ void create_whenVcsNotSameFormat() { var vc1 = new VerifiableCredentialContainer(jwtVc, CredentialFormat.JWT, createDummyCredential()); var vc2 = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); - var vpJwt = creator.generatePresentation(List.of(vc1, vc2), KEY_ID, audClaim); + var vpJwt = creator.generatePresentation(List.of(vc1, vc2), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(vpJwt).isNotNull(); assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); - var claims = parseJwt(vpJwt); + var claims = extractJwtClaims(vpJwt); REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim).isNotNull()); } @@ -98,20 +102,20 @@ void create_whenVcsNotSameFormat() { @Test @DisplayName("Should create a valid VP with no credential") void create_whenVcsEmpty_shouldReturnEmptyVp() { - var vpJwt = creator.generatePresentation(List.of(), KEY_ID, audClaim); + var vpJwt = creator.generatePresentation(List.of(), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(vpJwt).isNotNull(); assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); - var claims = parseJwt(vpJwt); + var claims = extractJwtClaims(vpJwt); REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim).isNotNull()); } @Test - @DisplayName("Should throw an exception if no key is found for a key-id") - void create_whenKeyNotFound() { + @DisplayName("Should throw an exception if no private key is found for a key-id") + void create_whenPrivateKeyNotFound() { var vcc = new VerifiableCredentialContainer("foobar", CredentialFormat.JWT, createDummyCredential()); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), "not-exist", audClaim)).isInstanceOf(IllegalArgumentException.class); + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), "not-exist", PUBLIC_KEY_ID, ADDITIONAL_DATA)).isInstanceOf(IllegalArgumentException.class); } @Test @@ -119,10 +123,10 @@ void create_whenKeyNotFound() { @Override void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException() { var vcc = new VerifiableCredentialContainer("foobar", CredentialFormat.JWT, createDummyCredential()); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), KEY_ID)) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID)) .describedAs("Expected exception when no additional data provided") .isInstanceOf(UnsupportedOperationException.class); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), KEY_ID, Map.of())) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, Map.of())) .describedAs("Expected exception when additional data does not contain expected value ('aud')") .isInstanceOf(IllegalArgumentException.class); } @@ -131,16 +135,16 @@ void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException() { @DisplayName("Should return an empty JWT when no credentials are passed") void create_whenEmptyList() { - var vpJwt = creator.generatePresentation(List.of(), KEY_ID, audClaim); + var vpJwt = creator.generatePresentation(List.of(), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(vpJwt).isNotNull(); assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); - var claims = parseJwt(vpJwt); + var claims = extractJwtClaims(vpJwt); REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim).isNotNull()); assertThat(claims.getClaim("vp")).isNotNull(); } - private JWTClaimsSet parseJwt(String vpJwt) { + private JWTClaimsSet extractJwtClaims(String vpJwt) { try { return SignedJWT.parse(vpJwt).getJWTClaimsSet(); } catch (ParseException e) { @@ -148,5 +152,11 @@ private JWTClaimsSet parseJwt(String vpJwt) { } } - + private JWSHeader extractJwtHeader(String vpJwt) { + try { + return SignedJWT.parse(vpJwt).getHeader(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } } \ No newline at end of file diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGeneratorTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGeneratorTest.java index 7c9b82612..f621ecaa8 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGeneratorTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationGeneratorTest.java @@ -52,10 +52,13 @@ import static org.mockito.Mockito.when; class LdpPresentationGeneratorTest extends PresentationGeneratorTest { + private static final Map ADDITIONAL_DATA = Map.of( + "types", List.of("VerifiablePresentation", "SomeOtherPresentationType"), + "controller", "did:web:test" + ); + + private final PrivateKeyResolver privateKeyResolver = mock(); - private final PrivateKeyResolver resolverMock = mock(); - private final Map additionalArgs = Map.of("types", List.of("VerifiablePresentation", "SomeOtherPresentationType"), - "controller", "did:web:test"); private LdpPresentationGenerator creator; @BeforeEach @@ -64,15 +67,15 @@ void setup() throws NoSuchAlgorithmException { .generateKeyPair() .getPrivate(); - when(resolverMock.resolvePrivateKey(any())).thenReturn(Result.failure("no key found")); - when(resolverMock.resolvePrivateKey(eq(KEY_ID))).thenReturn(Result.success(vpSigningKey)); + when(privateKeyResolver.resolvePrivateKey(any())).thenReturn(Result.failure("no key found")); + when(privateKeyResolver.resolvePrivateKey(eq(PRIVATE_KEY_ALIAS))).thenReturn(Result.success(vpSigningKey)); var signatureSuiteRegistryMock = mock(SignatureSuiteRegistry.class); when(signatureSuiteRegistryMock.getForId(IdentityHubConstants.JWS_2020_SIGNATURE_SUITE)).thenReturn(new JwsSignature2020Suite(new ObjectMapper())); var ldpIssuer = LdpIssuer.Builder.newInstance() .jsonLd(initializeJsonLd()) .monitor(mock()) .build(); - creator = new LdpPresentationGenerator(resolverMock, "did:web:test-issuer", signatureSuiteRegistryMock, IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, ldpIssuer, + creator = new LdpPresentationGenerator(privateKeyResolver, "did:web:test-issuer", signatureSuiteRegistryMock, IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, ldpIssuer, JacksonJsonLd.createObjectMapper()); } @@ -82,7 +85,7 @@ public void createPresentation_success() { var ldpVc = TestData.LDP_VC_WITH_PROOF; var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); - var result = creator.generatePresentation(List.of(vcc), KEY_ID, additionalArgs); + var result = creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(result).isNotNull(); assertThat(result.get("https://w3id.org/security#proof")).isNotNull(); } @@ -97,7 +100,7 @@ public void create_whenVcsNotSameFormat() { var jwtVc = JwtCreationUtils.createJwt(vcSigningKey, TestConstants.CENTRAL_ISSUER_DID, "degreeSub", TestConstants.VP_HOLDER_ID, Map.of("vc", TestConstants.VC_CONTENT_DEGREE_EXAMPLE)); var vcc2 = new VerifiableCredentialContainer(jwtVc, CredentialFormat.JWT, createDummyCredential()); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc, vcc2), KEY_ID, additionalArgs)) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc, vcc2), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA)) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("One or more VerifiableCredentials cannot be represented in the desired format %s".formatted(CredentialFormat.JSON_LD)); } @@ -105,17 +108,17 @@ public void create_whenVcsNotSameFormat() { @Override @Test public void create_whenVcsEmpty_shouldReturnEmptyVp() { - var result = creator.generatePresentation(List.of(), KEY_ID, additionalArgs); + var result = creator.generatePresentation(List.of(), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(result).isNotNull(); } @Override @Test - public void create_whenKeyNotFound() { + public void create_whenPrivateKeyNotFound() { var ldpVc = TestData.LDP_VC_WITH_PROOF; var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), "not-exists", additionalArgs)) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), "not-exists", PUBLIC_KEY_ID, ADDITIONAL_DATA)) .isInstanceOf(IllegalArgumentException.class); } @@ -124,10 +127,10 @@ public void create_whenKeyNotFound() { public void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException() { var ldpVc = TestData.LDP_VC_WITH_PROOF; var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), KEY_ID)).isInstanceOf(UnsupportedOperationException.class) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID)).isInstanceOf(UnsupportedOperationException.class) .hasMessage("Must provide additional data: 'types'"); - assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), KEY_ID, Map.of("some-key", "some-value"))) + assertThatThrownBy(() -> creator.generatePresentation(List.of(vcc), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, Map.of("some-key", "some-value"))) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Must provide additional data: 'types'"); } @@ -137,7 +140,7 @@ public void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentExcept @Override void create_whenEmptyList() { - var result = creator.generatePresentation(List.of(), KEY_ID, additionalArgs); + var result = creator.generatePresentation(List.of(), PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, ADDITIONAL_DATA); assertThat(result).isNotNull(); assertThat(result.get("https://w3id.org/security#proof")).isNotNull(); } diff --git a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationGeneratorTest.java b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationGeneratorTest.java index d7440dca9..9bc87aeec 100644 --- a/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationGeneratorTest.java +++ b/core/identity-hub-credentials/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationGeneratorTest.java @@ -29,7 +29,8 @@ abstract class PresentationGeneratorTest { - public static final String KEY_ID = "key-1"; + public static final String PRIVATE_KEY_ALIAS = "private-key"; + public static final String PUBLIC_KEY_ID = "key-1"; @Test @DisplayName("Verify succesful creation of a JWT_VP") @@ -45,7 +46,7 @@ abstract class PresentationGeneratorTest { @Test @DisplayName("Should throw an exception if no key is found for a key-id") - abstract void create_whenKeyNotFound(); + abstract void create_whenPrivateKeyNotFound(); @Test @DisplayName("Should throw an exception if the required additional data is missing") diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java index 3654445e0..75047cc23 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationGenerator.java @@ -36,13 +36,14 @@ public interface PresentationGenerator { * The concrete return type of the VP depends on the implementation, for example JWT VPs are represented as String, LDP VPs are represented * as {@link jakarta.json.JsonObject}. * - * @param credentials The list of Verifiable Credential Containers to include in the presentation. - * @param keyId The key ID of the private key to be used for generating the presentation. + * @param credentials The list of Verifiable Credential Containers to include in the presentation. + * @param privateKeyAlias The alias of the private key to be used for generating the presentation. + * @param publicKeyId The ID used by the counterparty to resolve the public key for verifying the VP. * @return The generated Verifiable Presentation. The concrete return type depends on the implementation. * @throws IllegalArgumentException If not all VCs can be represented in one VP. * @throws UnsupportedOperationException If additional data is required by the implementation, or if specified key is not suitable for signing. */ - T generatePresentation(List credentials, String keyId); + T generatePresentation(List credentials, String privateKeyAlias, String publicKeyId); /** * Generates a Verifiable Presentation based on a list of Verifiable Credential Containers and a key ID. Implementors must @@ -54,12 +55,13 @@ public interface PresentationGenerator { * The concrete return type of the VP depends on the implementation, for example JWT VPs are represented as String, LDP VPs are represented * as {@link jakarta.json.JsonObject}. * - * @param credentials The list of Verifiable Credential Containers to include in the presentation. - * @param keyId The key ID of the private key to be used for generating the presentation. + * @param credentials The list of Verifiable Credential Containers to include in the presentation. + * @param privateKeyAlias The alias of the private key to be used for generating the presentation. + * @param publicKeyId The ID used by the counterparty to resolve the public key for verifying the VP. * @return The generated Verifiable Presentation. The concrete return type depends on the implementation. * @throws IllegalArgumentException If not all VCs can be represented in one VP, mandatory additional information was not given, or the specified key is not suitable for signing. */ - default T generatePresentation(List credentials, String keyId, Map additionalData) { - return generatePresentation(credentials, keyId); + default T generatePresentation(List credentials, String privateKeyAlias, String publicKeyId, Map additionalData) { + return generatePresentation(credentials, privateKeyAlias, publicKeyId); } }