Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow multiple VP isuers #372

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
maven/mavencentral/com.apicatalog/carbon-did/0.3.0, Apache-2.0, approved, clearlydefined

Check warning on line 1 in DEPENDENCIES

View workflow job for this annotation

GitHub Actions / check / Dash-Verify-Licenses

Restricted Dependencies found

Some dependencies are marked 'restricted' - please review them
maven/mavencentral/com.apicatalog/copper-multibase/0.5.0, Apache-2.0, approved, #14501
maven/mavencentral/com.apicatalog/copper-multicodec/0.1.1, Apache-2.0, approved, #14500
maven/mavencentral/com.apicatalog/iron-verifiable-credentials/0.14.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -76,6 +76,7 @@
maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.39.3, Apache-2.0, approved, #14830
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.40, , restricted, clearlydefined
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.17.0, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #15077
maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause, approved, clearlydefined
maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.security.signature.jws2020.Jws2020SignatureSuite;
import org.eclipse.edc.spi.security.Vault;
import org.eclipse.edc.spi.system.ServiceExtension;
Expand Down Expand Up @@ -75,8 +74,6 @@
public class CoreServicesExtension implements ServiceExtension {

public static final String NAME = "IdentityHub Core Services Extension";
@Setting(value = "Configure this IdentityHub's DID", required = true)
public static final String OWN_DID_PROPERTY = "edc.ih.iam.id";

public static final String PRESENTATION_EXCHANGE_V_1_JSON = "presentation-exchange.v1.json";
public static final String PRESENTATION_QUERY_V_08_JSON = "iatp.v08.json";
Expand Down Expand Up @@ -150,11 +147,11 @@ public CredentialQueryResolver createCredentialQueryResolver(ServiceExtensionCon
@Provider
public PresentationCreatorRegistry presentationCreatorRegistry(ServiceExtensionContext context) {
if (presentationCreatorRegistry == null) {
presentationCreatorRegistry = new PresentationCreatorRegistryImpl(keyPairService);
presentationCreatorRegistry.addCreator(new JwtPresentationGenerator(privateKeyResolver, clock, getOwnDid(context), new JwtGenerationService()), CredentialFormat.JWT);
presentationCreatorRegistry = new PresentationCreatorRegistryImpl(keyPairService, participantContextService);
presentationCreatorRegistry.addCreator(new JwtPresentationGenerator(privateKeyResolver, clock, new JwtGenerationService()), CredentialFormat.JWT);

var ldpIssuer = LdpIssuer.Builder.newInstance().jsonLd(jsonLd).monitor(context.getMonitor()).build();
presentationCreatorRegistry.addCreator(new LdpPresentationGenerator(privateKeyResolver, getOwnDid(context), signatureSuiteRegistry, IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, ldpIssuer, typeManager.getMapper(JSON_LD)),
presentationCreatorRegistry.addCreator(new LdpPresentationGenerator(privateKeyResolver, signatureSuiteRegistry, IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, ldpIssuer, typeManager.getMapper(JSON_LD)),
CredentialFormat.JSON_LD);
}
return presentationCreatorRegistry;
Expand All @@ -172,10 +169,6 @@ public CredentialStatusCheckService createStatusCheckService() {
return new CredentialStatusCheckServiceImpl(revocationService, clock);
}

private String getOwnDid(ServiceExtensionContext context) {
return context.getConfig().getString(OWN_DID_PROPERTY);
}

private void cacheContextDocuments(ClassLoader classLoader) {
try {
jsonLd.registerCachedDocument(PRESENTATION_EXCHANGE_URL, classLoader.getResource(PRESENTATION_EXCHANGE_V_1_JSON).toURI());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantResource;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.generator.PresentationCreatorRegistry;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.generator.PresentationGenerator;
Expand All @@ -35,9 +37,11 @@ public class PresentationCreatorRegistryImpl implements PresentationCreatorRegis

private final Map<CredentialFormat, PresentationGenerator<?>> creators = new HashMap<>();
private final KeyPairService keyPairService;
private final ParticipantContextService participantContextService;

public PresentationCreatorRegistryImpl(KeyPairService keyPairService) {
public PresentationCreatorRegistryImpl(KeyPairService keyPairService, ParticipantContextService participantContextService) {
this.keyPairService = keyPairService;
this.participantContextService = participantContextService;
}

@Override
Expand Down Expand Up @@ -65,6 +69,11 @@ 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.getPrivateKeyAlias(), keyPair.getKeyId(), additionalData);
var did = participantContextService.getParticipantContext(participantContextId)
.map(ParticipantContext::getDid)
.orElseThrow(f -> new EdcException(f.getFailureDetail()));


return (T) creator.generatePresentation(credentials, keyPair.getPrivateKeyAlias(), keyPair.getKeyId(), did, additionalData);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ public class JwtPresentationGenerator implements PresentationGenerator<String> {
public static final String VERIFIABLE_PRESENTATION_CLAIM = "vp";
private final PrivateKeyResolver privateKeyResolver;
private final Clock clock;
private final String issuerId;

private final TokenGenerationService tokenGenerationService;

Expand All @@ -58,18 +57,16 @@ public class JwtPresentationGenerator implements PresentationGenerator<String> {
*
* @param privateKeyResolver The resolver for private keys used for signing the presentation.
* @param clock The clock used for generating timestamps.
* @param issuerId The ID of the issuer for the presentation. Could be a DID.
*/
public JwtPresentationGenerator(PrivateKeyResolver privateKeyResolver, Clock clock, String issuerId, TokenGenerationService tokenGenerationService) {
public JwtPresentationGenerator(PrivateKeyResolver privateKeyResolver, Clock clock, TokenGenerationService tokenGenerationService) {
this.privateKeyResolver = privateKeyResolver;
this.clock = clock;
this.issuerId = issuerId;
this.tokenGenerationService = tokenGenerationService;
}

/**
* Will always throw an {@link UnsupportedOperationException}.
* Please use {@link JwtPresentationGenerator#generatePresentation(List, String, String, Map)} instead.
* Please use {@link PresentationGenerator#generatePresentation(List, String, String, String, Map)} instead.
*/
@Override
public String generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId) {
Expand All @@ -82,14 +79,15 @@ public String generatePresentation(List<VerifiableCredentialContainer> credentia
* @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 issuerId The ID of this issuer. Usually a DID.
* @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 privateKeyAlias, String publicKeyId, Map<String, Object> additionalData) {
public String generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, String issuerId, Map<String, Object> additionalData) {

// check if expected data is there
if (!additionalData.containsKey(JwtRegisteredClaimNames.AUDIENCE)) {
Expand All @@ -104,15 +102,15 @@ public String generatePresentation(List<VerifiableCredentialContainer> credentia
.map(VerifiableCredentialContainer::rawVc)
.collect(Collectors.toList());
Supplier<PrivateKey> privateKeySupplier = () -> privateKeyResolver.resolvePrivateKey(privateKeyAlias).orElseThrow(f -> new IllegalArgumentException(f.getFailureDetail()));
var tokenResult = tokenGenerationService.generate(privateKeySupplier, vpDecorator(rawVcs), tp -> {
var tokenResult = tokenGenerationService.generate(privateKeySupplier, vpDecorator(rawVcs, issuerId), 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()));
}

private TokenDecorator vpDecorator(List<String> rawVcs) {
private TokenDecorator vpDecorator(List<String> rawVcs, String issuerId) {
var now = Date.from(clock.instant());
return tp -> tp.claims(JwtRegisteredClaimNames.ISSUER, issuerId)
.claims(JwtRegisteredClaimNames.ISSUED_AT, now)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,14 @@ public class LdpPresentationGenerator implements PresentationGenerator<JsonObjec
public static final String HOLDER_PROPERTY = "holder";
public static final URI ASSERTION_METHOD = URI.create("https://w3id.org/security#assertionMethod");
private final PrivateKeyResolver privateKeyResolver;
private final String issuerId;
private final SignatureSuiteRegistry signatureSuiteRegistry;
private final String defaultSignatureSuite;
private final LdpIssuer ldpIssuer;
private final ObjectMapper mapper;

public LdpPresentationGenerator(PrivateKeyResolver privateKeyResolver, String ownDid,
public LdpPresentationGenerator(PrivateKeyResolver privateKeyResolver,
SignatureSuiteRegistry signatureSuiteRegistry, String defaultSignatureSuite, LdpIssuer ldpIssuer, ObjectMapper mapper) {
this.privateKeyResolver = privateKeyResolver;
this.issuerId = ownDid;
this.signatureSuiteRegistry = signatureSuiteRegistry;
this.defaultSignatureSuite = defaultSignatureSuite;
this.ldpIssuer = ldpIssuer;
Expand All @@ -79,7 +77,7 @@ public LdpPresentationGenerator(PrivateKeyResolver privateKeyResolver, String ow

/**
* Will always throw an {@link UnsupportedOperationException}.
* Please use {@link LdpPresentationGenerator#generatePresentation(List, String, String, Map)} instead.
* Please use {@link PresentationGenerator#generatePresentation(List, String, String, String, Map)} instead.
*/
@Override
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String privateKeyId) {
Expand All @@ -94,6 +92,7 @@ public JsonObject generatePresentation(List<VerifiableCredentialContainer> crede
* @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 issuerId The ID of this issuer. Usually a DID.
* @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.
Expand All @@ -104,7 +103,7 @@ public JsonObject generatePresentation(List<VerifiableCredentialContainer> crede
* or if one or more VerifiableCredentials cannot be represented in the JSON-LD format.
*/
@Override
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, Map<String, Object> additionalData) {
public JsonObject generatePresentation(List<VerifiableCredentialContainer> credentials, String privateKeyAlias, String publicKeyId, String issuerId, Map<String, Object> additionalData) {
if (!additionalData.containsKey(TYPE_ADDITIONAL_DATA)) {
throw new IllegalArgumentException("Must provide additional data: '%s'".formatted(TYPE_ADDITIONAL_DATA));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@
import org.eclipse.edc.identityhub.spi.keypair.KeyPairService;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairResource;
import org.eclipse.edc.identityhub.spi.keypair.model.KeyPairState;
import org.eclipse.edc.identityhub.spi.participantcontext.ParticipantContextService;
import org.eclipse.edc.identityhub.spi.participantcontext.model.ParticipantContext;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.generator.PresentationGenerator;
import org.eclipse.edc.spi.EdcException;
import org.eclipse.edc.spi.result.ServiceResult;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.util.List;
Expand All @@ -32,6 +35,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
Expand All @@ -42,9 +46,20 @@
@SuppressWarnings("unchecked") // mocking a generic type (PresentationGenerator) would raise warnings
class PresentationCreatorRegistryImplTest {

public static final String ISSUER_ID = "did:web:test";
private static final String TEST_PARTICIPANT = "test-participant";
private final KeyPairService keyPairService = mock();
private final PresentationCreatorRegistryImpl registry = new PresentationCreatorRegistryImpl(keyPairService);
private final ParticipantContextService participantContextService = mock();
private final PresentationCreatorRegistryImpl registry = new PresentationCreatorRegistryImpl(keyPairService, participantContextService);

@BeforeEach
void setup() {
when(participantContextService.getParticipantContext(anyString()))
.thenReturn(ServiceResult.success(ParticipantContext.Builder.newInstance()
.participantId("test-participant")
.apiTokenAlias("test-token")
.did(ISSUER_ID).build()));
}

@Test
void createPresentation_whenSingleKey() {
Expand All @@ -54,7 +69,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.getPrivateKeyAlias()), eq(keyPair.getKeyId()), anyMap());
verify(generator).generatePresentation(anyList(), eq(keyPair.getPrivateKeyAlias()), eq(keyPair.getKeyId()), eq(ISSUER_ID), anyMap());
}

@Test
Expand All @@ -81,7 +96,7 @@ void createPresentation_whenNoDefaultKey() {
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());
eq(ISSUER_ID), anyMap());
}


Expand All @@ -95,7 +110,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(keyPair2.getPrivateKeyAlias()), eq(keyPair2.getKeyId()), anyMap());
verify(generator).generatePresentation(anyList(), eq(keyPair2.getPrivateKeyAlias()), eq(keyPair2.getKeyId()), eq(ISSUER_ID), anyMap());
}

@Test
Expand Down
Loading
Loading