Skip to content

Commit

Permalink
fix: add missing 'kid' header in JwtPresentationGenerator
Browse files Browse the repository at this point in the history
  • Loading branch information
bscholtes1A committed Feb 16, 2024
1 parent efa1bed commit 88f38d3
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 76 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@ public <T> T createPresentation(String participantContextId, List<VerifiableCred
throw new EdcException("No active key pair found for participant '%s'".formatted(participantContextId));
}

return (T) creator.generatePresentation(credentials, keyPair.getKeyId(), additionalData);
return (T) creator.generatePresentation(credentials, keyPair.getPrivateKeyAlias(), keyPair.getKeyId(), additionalData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,18 @@
import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer;
import org.eclipse.edc.identitytrust.model.credentialservice.PresentationResponseMessage;
import org.eclipse.edc.identitytrust.model.presentationdefinition.PresentationDefinition;
import org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames;
import org.eclipse.edc.spi.monitor.Monitor;
import org.eclipse.edc.spi.result.Result;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.nimbusds.jwt.JWTClaimNames.AUDIENCE;
import static java.util.Optional.ofNullable;
import static org.eclipse.edc.identitytrust.model.CredentialFormat.JSON_LD;

Expand Down Expand Up @@ -73,7 +76,10 @@ public Result<PresentationResponseMessage> createPresentation(String participant

var vpToken = new ArrayList<>();

Map<String, Object> additionalDataJwt = audience != null ? Map.of("aud", audience) : Map.of();
var additionalDataJwt = new HashMap<String, Object>();
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()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand Down Expand Up @@ -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<VerifiableCredentialContainer> credentials, String keyId) {
public String generatePresentation(List<VerifiableCredentialContainer> 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<VerifiableCredentialContainer> credentials, String privateKeyId, Map<String, Object> additionalData) {
public String generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, Map<String, Object> 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<PrivateKey> privateKeySupplier = () -> privateKeyResolver.resolvePrivateKey(privateKeyId).orElseThrow(f -> new IllegalArgumentException(f.getFailureDetail()));
Supplier<PrivateKey> 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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VerifiableCredentialContainer> credentials, String keyId) {
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String privateKeyId) {
throw new UnsupportedOperationException("Must provide additional data: 'types'");

}
Expand All @@ -87,19 +87,20 @@ public JsonObject generatePresentation(List<VerifiableCredentialContainer> 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,
* if the key ID is not in URI format,
* or if one or more VerifiableCredentials cannot be represented in the JSON-LD format.
*/
@Override
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String keyId, Map<String, Object> additionalData) {
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, Map<String, Object> additionalData) {
if (!additionalData.containsKey("types")) {
throw new IllegalArgumentException("Must provide additional data: 'types'");
}
Expand All @@ -118,7 +119,7 @@ public JsonObject generatePresentation(List<VerifiableCredentialContainer> 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");
Expand All @@ -130,7 +131,7 @@ public JsonObject generatePresentation(List<VerifiableCredentialContainer> 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
Expand All @@ -149,8 +150,8 @@ private JsonArray toJsonArray(List<VerifiableCredentialContainer> 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());
Expand All @@ -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()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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());
}


Expand All @@ -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
Expand All @@ -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));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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();
Expand All @@ -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
Expand All @@ -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"));
}
Expand Down
Loading

0 comments on commit 88f38d3

Please sign in to comment.