Skip to content

Commit

Permalink
fix: add missing kid header in JwtPresentationGenerator (#278)
Browse files Browse the repository at this point in the history
fix: add missing 'kid' header in JwtPresentationGenerator
  • Loading branch information
bscholtes1A authored Feb 18, 2024
1 parent efa1bed commit af690cd
Show file tree
Hide file tree
Showing 11 changed files with 177 additions and 91 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 @@ -26,11 +26,16 @@
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.identityhub.core.creators.LdpPresentationGenerator.TYPE_ADDITIONAL_DATA;
import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.CONTROLLER_ADDITIONAL_DATA;
import static org.eclipse.edc.identitytrust.VcConstants.VERIFIABLE_PRESENTATION_TYPE;
import static org.eclipse.edc.identitytrust.model.CredentialFormat.JSON_LD;

public class VerifiablePresentationServiceImpl implements VerifiablePresentationService {
Expand Down Expand Up @@ -73,15 +78,17 @@ 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(CONTROLLER_ADDITIONAL_DATA, participantContextId);

if (defaultFormatVp == JSON_LD) { // LDP-VPs cannot contain JWT VCs
if (!ldpVcs.isEmpty()) {

// todo: once we support PresentationDefinition, the types list could be dynamic
JsonObject ldpVp = registry.createPresentation(participantContextId, ldpVcs, CredentialFormat.JSON_LD, Map.of(
"types", List.of("VerifiablePresentation"),
"controller", participantContextId));
TYPE_ADDITIONAL_DATA, List.of(VERIFIABLE_PRESENTATION_TYPE),
CONTROLLER_ADDITIONAL_DATA, participantContextId));
vpToken.add(ldpVp);
}

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 @@ -36,10 +37,14 @@
import java.util.function.Supplier;
import java.util.stream.Stream;

import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.CONTROLLER_ADDITIONAL_DATA;
import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.VERIFIABLE_CREDENTIAL_PROPERTY;
import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.VP_TYPE_PROPERTY;
import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.IATP_CONTEXT_URL;
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 +79,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) {
throw new UnsupportedOperationException("Must provide additional data: 'aud'");
public String generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId) {
throw new UnsupportedOperationException("Must provide additional data: '%s' and '%s'".formatted(AUDIENCE, CONTROLLER_ADDITIONAL_DATA));
}

/**
* 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_ADDITIONAL_DATA)) {
throw new IllegalArgumentException("Must provide additional data: '%s'".formatted(CONTROLLER_ADDITIONAL_DATA));
}

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_ADDITIONAL_DATA) + "#" + publicKeyId));

return tokenResult.map(TokenRepresentation::getToken).orElseThrow(f -> new EdcException(f.getFailureDetail()));
}
Expand All @@ -126,8 +136,8 @@ private String createVpClaim(Stream<String> rawVcs) {

return Json.createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, stringArray(List.of(IATP_CONTEXT_URL, W3C_CREDENTIALS_URL, PRESENTATION_EXCHANGE_URL)))
.add("type", VERIFIABLE_PRESENTATION_TYPE) // todo: add more types here?
.add("verifiableCredential", vcArray.build())
.add(VP_TYPE_PROPERTY, VERIFIABLE_PRESENTATION_TYPE) // todo: add more types here?
.add(VERIFIABLE_CREDENTIAL_PROPERTY, vcArray.build())
.build()
.toString();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
import java.util.Map;
import java.util.UUID;

import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.CONTROLLER_ADDITIONAL_DATA;
import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.VERIFIABLE_CREDENTIAL_PROPERTY;
import static org.eclipse.edc.identityhub.core.creators.PresentationGeneratorConstants.VP_TYPE_PROPERTY;
import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.IATP_CONTEXT_URL;
import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.PRESENTATION_EXCHANGE_URL;
import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.W3C_CREDENTIALS_URL;
Expand All @@ -53,9 +56,9 @@
public class LdpPresentationGenerator implements PresentationGenerator<JsonObject> {

public static final String ID_PROPERTY = "id";
public static final String TYPE_PROPERTY = "type";

public static final String TYPE_ADDITIONAL_DATA = "types";
public static final String HOLDER_PROPERTY = "holder";
public static final String VERIFIABLE_CREDENTIAL_PROPERTY = "verifiableCredential";
private final PrivateKeyResolver privateKeyResolver;
private final String issuerId;
private final SignatureSuiteRegistry signatureSuiteRegistry;
Expand All @@ -75,36 +78,37 @@ 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) {
throw new UnsupportedOperationException("Must provide additional data: 'types'");
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String privateKeyId) {
throw new UnsupportedOperationException("Must provide additional data: '%s' and '%s'".formatted(TYPE_ADDITIONAL_DATA, CONTROLLER_ADDITIONAL_DATA));

}

/**
* 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) {
if (!additionalData.containsKey("types")) {
throw new IllegalArgumentException("Must provide additional data: 'types'");
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, Map<String, Object> additionalData) {
if (!additionalData.containsKey(TYPE_ADDITIONAL_DATA)) {
throw new IllegalArgumentException("Must provide additional data: '%s'".formatted(TYPE_ADDITIONAL_DATA));
}
if (!additionalData.containsKey("controller")) {
throw new IllegalArgumentException("Must provide additional data: 'controller'");
if (!additionalData.containsKey(CONTROLLER_ADDITIONAL_DATA)) {
throw new IllegalArgumentException("Must provide additional data: '%s'".formatted(CONTROLLER_ADDITIONAL_DATA));
}

var suiteIdentifier = additionalData.getOrDefault("suite", defaultSignatureSuite).toString();
Expand All @@ -118,19 +122,19 @@ 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");
var types = (List) additionalData.get(TYPE_ADDITIONAL_DATA);
var presentationObject = Json.createObjectBuilder()
.add(CONTEXT, stringArray(List.of(W3C_CREDENTIALS_URL, PRESENTATION_EXCHANGE_URL)))
.add(ID_PROPERTY, IATP_CONTEXT_URL + "/id/" + UUID.randomUUID())
.add(TYPE_PROPERTY, stringArray(types))
.add(VP_TYPE_PROPERTY, stringArray(types))
.add(HOLDER_PROPERTY, issuerId)
.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_ADDITIONAL_DATA).toString());
}

@NotNull
Expand All @@ -149,8 +153,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 +163,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
@@ -0,0 +1,28 @@
/*
* 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.identityhub.core.creators;

/**
* Contains common constants for {@link LdpPresentationGenerator} and {@link JwtPresentationGenerator}.
*/
public interface PresentationGeneratorConstants {

String CONTROLLER_ADDITIONAL_DATA = "controller";

String VP_TYPE_PROPERTY = "type";

String VERIFIABLE_CREDENTIAL_PROPERTY = "verifiableCredential";

}
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));
}
}
Loading

0 comments on commit af690cd

Please sign in to comment.