diff --git a/DEPENDENCIES b/DEPENDENCIES index 8067c688b..4e09c3340 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -138,24 +138,24 @@ maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.15, Apache-2.0, a maven/mavencentral/io.swagger.core.v3/swagger-core-jakarta/2.2.18, Apache-2.0, approved, #5929 maven/mavencentral/io.swagger.core.v3/swagger-core/2.2.15, Apache-2.0, approved, #9265 maven/mavencentral/io.swagger.core.v3/swagger-core/2.2.8, Apache-2.0, approved, #9265 -maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.15, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.18, , restricted, clearlydefined +maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.15, Apache-2.0, approved, #11475 +maven/mavencentral/io.swagger.core.v3/swagger-integration-jakarta/2.2.18, Apache-2.0, approved, #11475 maven/mavencentral/io.swagger.core.v3/swagger-integration/2.2.15, Apache-2.0, approved, #10352 -maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.15, Apache-2.0, approved, clearlydefined -maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.18, , restricted, clearlydefined +maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.15, Apache-2.0, approved, #11477 +maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2-jakarta/2.2.18, Apache-2.0, approved, #11477 maven/mavencentral/io.swagger.core.v3/swagger-jaxrs2/2.2.15, Apache-2.0, approved, #9814 maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.15, Apache-2.0, approved, #5919 maven/mavencentral/io.swagger.core.v3/swagger-models-jakarta/2.2.18, Apache-2.0, approved, #5919 maven/mavencentral/io.swagger.core.v3/swagger-models/2.2.15, Apache-2.0, approved, #10353 maven/mavencentral/io.swagger.core.v3/swagger-models/2.2.8, Apache-2.0, approved, #10353 -maven/mavencentral/io.swagger.parser.v3/swagger-parser-core/2.1.10, None, restricted, #11314 +maven/mavencentral/io.swagger.parser.v3/swagger-parser-core/2.1.10, None, restricted, #11478 maven/mavencentral/io.swagger.parser.v3/swagger-parser-v2-converter/2.1.10, Apache-2.0, approved, #9330 maven/mavencentral/io.swagger.parser.v3/swagger-parser-v3/2.1.10, Apache-2.0, approved, #9323 maven/mavencentral/io.swagger.parser.v3/swagger-parser/2.1.10, None, restricted, #11316 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, #11282 +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, #11330 +maven/mavencentral/io.swagger/swagger-models/1.6.9, LicenseRef-scancode-proprietary-license, restricted, #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 diff --git a/core/identity-hub-core/build.gradle.kts b/core/identity-hub-core/build.gradle.kts index 37a91c896..819db3b41 100644 --- a/core/identity-hub-core/build.gradle.kts +++ b/core/identity-hub-core/build.gradle.kts @@ -7,10 +7,18 @@ dependencies { api(project(":spi:identity-hub-store-spi")) implementation(libs.edc.core.connector) // for the CriterionToPredicateConverterImpl implementation(libs.edc.spi.jsonld) + implementation(libs.edc.spi.jsonld) + implementation(libs.edc.ext.jsonld) // for the JSON-LD mapper implementation(libs.edc.iatp.service) // JWT validator implementation(libs.edc.core.crypto) // JWT verifier + implementation(libs.edc.jws2020) + implementation(libs.edc.vc.ldp) + implementation(libs.edc.util) implementation(libs.nimbus.jwt) testImplementation(libs.edc.junit) + testImplementation(libs.edc.ext.jsonld) testImplementation(testFixtures(project(":spi:identity-hub-spi"))) + testImplementation(testFixtures(libs.edc.vc.jwt)) // JWT generator + testImplementation(libs.edc.identity.did.crypto) // EC private key wrapper } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java index 959c5bbdd..41c122875 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/DefaultServicesExtension.java @@ -14,28 +14,39 @@ package org.eclipse.edc.identityhub; +import com.apicatalog.ld.signature.SignatureSuite; import org.eclipse.edc.identityhub.defaults.EdcScopeToCriterionTransformer; import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore; import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; -import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.IdentityHubConstants; import org.eclipse.edc.identityhub.spi.store.CredentialStore; +import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry; +import org.eclipse.edc.jsonld.util.JacksonJsonLd; import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.security.signature.jws2020.JwsSignature2020Suite; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; -@Extension("Default Services Extension") +import java.util.Collection; +import java.util.Map; + +import static org.eclipse.edc.identityhub.DefaultServicesExtension.NAME; + +@Extension(NAME) public class DefaultServicesExtension implements ServiceExtension { - @Provider(isDefault = true) - public CredentialStore createInMemStore() { - return new InMemoryCredentialStore(); + public static final String NAME = "IdentityHub Default Services Extension"; + + @Override + public String name() { + return NAME; } @Provider(isDefault = true) - public PresentationGenerator createPresentationGenerator(ServiceExtensionContext context) { - context.getMonitor().warning(" #### Creating a default NOOP PresentationGenerator, that will always return 'null'!"); - return (credentials, presentationDefinition) -> null; + public CredentialStore createInMemStore() { + return new InMemoryCredentialStore(); + } @Provider(isDefault = true) @@ -45,4 +56,25 @@ public ScopeToCriterionTransformer createScopeTransformer(ServiceExtensionContex return new EdcScopeToCriterionTransformer(); } + @Provider(isDefault = true) + public SignatureSuiteRegistry createSignatureSuiteRegistry() { + return new SignatureSuiteRegistry() { + private final Map registry = Map.of(IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, new JwsSignature2020Suite(JacksonJsonLd.createObjectMapper())); + + @Override + public void register(String w3cIdentifier, SignatureSuite suite) { + + } + + @Override + public SignatureSuite getForId(String w3cIdentifier) { + return registry.get(w3cIdentifier); + } + + @Override + public Collection getAllSuites() { + return registry.values(); + } + }; + } } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java index 900534dc4..3bebf5fe9 100644 --- a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/CoreServicesExtension.java @@ -17,58 +17,83 @@ import org.eclipse.edc.iam.did.spi.key.PublicKeyWrapper; import org.eclipse.edc.iam.did.spi.resolution.DidResolverRegistry; import org.eclipse.edc.iam.identitytrust.validation.SelfIssuedIdTokenValidator; +import org.eclipse.edc.identityhub.core.creators.JwtPresentationCreator; +import org.eclipse.edc.identityhub.core.creators.LdpPresentationCreator; import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.IdentityHubConstants; import org.eclipse.edc.identityhub.spi.resolution.CredentialQueryResolver; import org.eclipse.edc.identityhub.spi.store.CredentialStore; import org.eclipse.edc.identityhub.spi.verification.AccessTokenVerifier; import org.eclipse.edc.identityhub.token.verification.AccessTokenVerifierImpl; +import org.eclipse.edc.identitytrust.model.CredentialFormat; import org.eclipse.edc.identitytrust.validation.JwtValidator; import org.eclipse.edc.identitytrust.verification.JwtVerifier; +import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry; 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.runtime.metamodel.annotation.Provider; import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.security.PrivateKeyResolver; import org.eclipse.edc.spi.system.ServiceExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.verifiablecredentials.linkeddata.LdpIssuer; import org.eclipse.edc.verification.jwt.SelfIssuedIdTokenVerifier; import java.net.URISyntaxException; +import java.time.Clock; +import static org.eclipse.edc.identityhub.core.CoreServicesExtension.NAME; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.DID_CONTEXT_URL; import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.IATP_CONTEXT_URL; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.JWS_2020_URL; import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.PRESENTATION_EXCHANGE_URL; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.PRESENTATION_SUBMISSION_URL; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.W3C_CREDENTIALS_URL; /** - * This extension provides some core services for the IdentityHub, such as: - * + * This extension provides core services for the IdentityHub that are not intended to be user-replaceable. */ -@Extension(value = "Core Services extension") +@Extension(value = NAME) 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 = "presentation-query.v08.json"; + public static final String PRESENTATION_SUBMISSION_V1_JSON = "presentation-submission.v1.json"; + public static final String DID_JSON = "did.json"; + public static final String JWS_2020_JSON = "jws2020.json"; + public static final String CREDENTIALS_V_1_JSON = "credentials.v1.json"; + private final String defaultSuite = IdentityHubConstants.JWS_2020_SIGNATURE_SUITE; + private PresentationCreatorRegistryImpl presentationCreatorRegistry; private JwtVerifier jwtVerifier; private JwtValidator jwtValidator; + @Inject private DidResolverRegistry didResolverRegistry; - @Inject private PublicKeyWrapper identityHubPublicKey; - @Inject private JsonLd jsonLd; - @Inject private CredentialStore credentialStore; - @Inject private ScopeToCriterionTransformer transformer; + @Inject + private PrivateKeyResolver privateKeyResolver; + @Inject + private Clock clock; + @Inject + private SignatureSuiteRegistry signatureSuiteRegistry; + + @Override + public String name() { + return NAME; + } @Override public void initialize(ServiceExtensionContext context) { @@ -102,6 +127,25 @@ public CredentialQueryResolver createCredentialQueryResolver() { return new CredentialQueryResolverImpl(credentialStore, transformer); } + @Provider + public PresentationCreatorRegistry presentationCreatorRegistry(ServiceExtensionContext context) { + if (presentationCreatorRegistry == null) { + presentationCreatorRegistry = new PresentationCreatorRegistryImpl(); + presentationCreatorRegistry.addCreator(new JwtPresentationCreator(privateKeyResolver, clock, getOwnDid(context)), CredentialFormat.JWT); + + var ldpIssuer = LdpIssuer.Builder.newInstance().jsonLd(jsonLd).monitor(context.getMonitor()).build(); + presentationCreatorRegistry.addCreator(new LdpPresentationCreator(privateKeyResolver, getOwnDid(context), signatureSuiteRegistry, defaultSuite, ldpIssuer, null), + CredentialFormat.JSON_LD); + } + return presentationCreatorRegistry; + } + + @Provider + public PresentationGenerator presentationGenerator(ServiceExtensionContext context) { + return new PresentationGeneratorImpl(CredentialFormat.JSON_LD, presentationCreatorRegistry, context.getMonitor()); + } + + private String getOwnDid(ServiceExtensionContext context) { return context.getConfig().getString(OWN_DID_PROPERTY); } @@ -110,6 +154,10 @@ private void cacheContextDocuments(ClassLoader classLoader) { try { jsonLd.registerCachedDocument(PRESENTATION_EXCHANGE_URL, classLoader.getResource(PRESENTATION_EXCHANGE_V_1_JSON).toURI()); jsonLd.registerCachedDocument(IATP_CONTEXT_URL, classLoader.getResource(PRESENTATION_QUERY_V_08_JSON).toURI()); + jsonLd.registerCachedDocument(DID_CONTEXT_URL, classLoader.getResource(DID_JSON).toURI()); + jsonLd.registerCachedDocument(JWS_2020_URL, classLoader.getResource(JWS_2020_JSON).toURI()); + jsonLd.registerCachedDocument(W3C_CREDENTIALS_URL, classLoader.getResource(CREDENTIALS_V_1_JSON).toURI()); + jsonLd.registerCachedDocument(PRESENTATION_SUBMISSION_URL, classLoader.getResource(PRESENTATION_SUBMISSION_V1_JSON).toURI()); } catch (URISyntaxException e) { throw new RuntimeException(e); } diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java new file mode 100644 index 000000000..0bf070c55 --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationCreatorRegistryImpl.java @@ -0,0 +1,51 @@ +/* + * 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; + +import org.eclipse.edc.identityhub.spi.generator.PresentationCreator; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.EdcException; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static java.util.Optional.ofNullable; + +public class PresentationCreatorRegistryImpl implements PresentationCreatorRegistry { + + private final Map> creators = new HashMap<>(); + private final Map keyIds = new HashMap<>(); + + @Override + public void addCreator(PresentationCreator creator, CredentialFormat format) { + creators.put(format, creator); + } + + @Override + public T createPresentation(List credentials, CredentialFormat format) { + var creator = ofNullable(creators.get(format)).orElseThrow(() -> new EdcException("No PresentationCreator was found for CredentialFormat %s".formatted(format))); + var keyId = ofNullable(keyIds.get(format)).orElseThrow(() -> new EdcException("No key ID was registered for CredentialFormat %s".formatted(format))); + + return (T) creator.createPresentation(credentials, keyId); + } + + @Override + public void addKeyId(String keyId, CredentialFormat format) { + keyIds.put(format, keyId); + } +} diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImpl.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImpl.java new file mode 100644 index 000000000..924b8931e --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImpl.java @@ -0,0 +1,93 @@ +/* + * 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; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; +import org.eclipse.edc.identityhub.spi.generator.PresentationGenerator; +import org.eclipse.edc.identityhub.spi.model.PresentationResponse; +import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.stream.Collectors; + +import static java.util.Optional.ofNullable; +import static org.eclipse.edc.identitytrust.model.CredentialFormat.JSON_LD; + +public class PresentationGeneratorImpl implements PresentationGenerator { + private final CredentialFormat defaultFormatVp; + private final PresentationCreatorRegistry registry; + private final Monitor monitor; + + /** + * Creates a PresentationGeneratorImpl object with the specified default formats for verifiable credentials and presentations. + * + * @param defaultFormatVp The default format for verifiable presentations. + */ + public PresentationGeneratorImpl(CredentialFormat defaultFormatVp, PresentationCreatorRegistry registry, Monitor monitor) { + this.defaultFormatVp = defaultFormatVp; + this.registry = registry; + this.monitor = monitor; + } + + /** + * Creates a presentation based on the given list of verifiable credentials and optional presentation definition. If the desired format ist {@link CredentialFormat#JSON_LD}, + * all JWT-VCs in the list will be packaged in a separate JWT VP, because LDP-VPs cannot contain JWT-VCs. + * Note: submitting a {@link PresentationDefinition} is not supported at the moment, and it will be ignored after logging a warning. + * + * @param credentials The list of verifiable credentials to include in the presentation. + * @param presentationDefinition The optional presentation definition. Not supported at the moment! + * @return A Result object wrapping the PresentationResponse. + */ + @Override + public Result createPresentation(List credentials, @Nullable PresentationDefinition presentationDefinition) { + + if (presentationDefinition != null) { + monitor.warning("A PresentationDefinition was submitted, but is currently ignored by the generator."); + } + var groups = credentials.stream().collect(Collectors.groupingBy(VerifiableCredentialContainer::format)); + var jwtVcs = ofNullable(groups.get(CredentialFormat.JWT)).orElseGet(List::of); + var ldpVcs = ofNullable(groups.get(JSON_LD)).orElseGet(List::of); + + + String vpToken; + if (defaultFormatVp == JSON_LD) { // LDP-VPs cannot contain JWT VCs + var arrayBuilder = Json.createArrayBuilder(); + if (!ldpVcs.isEmpty()) { + JsonObject ldpVp = registry.createPresentation(ldpVcs, CredentialFormat.JSON_LD); + arrayBuilder.add(ldpVp); + } + + if (!jwtVcs.isEmpty()) { + monitor.warning("The VP was requested in %s format, but the request yielded %s JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs".formatted(JSON_LD, jwtVcs.size())); + String jwtVp = registry.createPresentation(jwtVcs, CredentialFormat.JWT); + arrayBuilder.add(jwtVp); + } + + vpToken = arrayBuilder.build().toString(); + } else { //defaultFormatVp == JWT + vpToken = registry.createPresentation(credentials, CredentialFormat.JWT); + } + + var presentationResponse = new PresentationResponse(vpToken, null); + return Result.success(presentationResponse); + } +} diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreator.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreator.java new file mode 100644 index 000000000..cb1d71a08 --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreator.java @@ -0,0 +1,141 @@ +/* + * 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; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWSHeader; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import jakarta.json.Json; +import jakarta.json.JsonArrayBuilder; +import org.eclipse.edc.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreator; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.jsonld.spi.JsonLdKeywords; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.security.PrivateKeyResolver; + +import java.sql.Date; +import java.time.Clock; +import java.time.Instant; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Stream; + +import static java.util.Optional.ofNullable; +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; + +/** + * JwtPresentationCreator is an implementation of the PresentationCreator interface that generates Verifiable Presentations in JWT format. + * VPs are returned as {@link String} + */ +public class JwtPresentationCreator implements PresentationCreator { + private final PrivateKeyResolver privateKeyResolver; + private final Clock clock; + private final String issuerId; + + /** + * Creates a JWT presentation based on a list of Verifiable Credential Containers. + * + * @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 JwtPresentationCreator(PrivateKeyResolver privateKeyResolver, Clock clock, String issuerId) { + this.privateKeyResolver = privateKeyResolver; + this.clock = clock; + this.issuerId = issuerId; + } + + /** + * Will always throw an {@link UnsupportedOperationException}. + * Please use {@link JwtPresentationCreator#createPresentation(List, String, Map)} instead. + */ + @Override + public String createPresentation(List credentials, String keyId) { + 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 keyId 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. + * @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 createPresentation(List credentials, String keyId, Map additionalData) { + + // check if expected data is there + if (!additionalData.containsKey("aud")) { + throw new IllegalArgumentException("Must provide additional data: 'aud'"); + } + + // check if private key can be resolved + var pk = ofNullable(privateKeyResolver.resolvePrivateKey(keyId, PrivateKeyWrapper.class)) + .orElseThrow(() -> new IllegalArgumentException("No key could be found with key ID '%s'.".formatted(keyId))); + + var rawVcs = credentials.stream().map(VerifiableCredentialContainer::rawVc); + var now = Date.from(clock.instant()); + var claimsSet = new JWTClaimsSet.Builder() + .issuer(issuerId) + .issueTime(now) + .notBeforeTime(now) + .jwtID(UUID.randomUUID().toString()) + .claim("vp", createVpClaim(rawVcs)) + .expirationTime(Date.from(Instant.now().plusSeconds(60))); + + additionalData.forEach(claimsSet::claim); + + var algo = pk.signer().supportedJWSAlgorithms().stream().findFirst() + .orElseThrow(() -> new UnsupportedOperationException("Private key with ID '%s' did not provide any supported JWS algorithms.".formatted(keyId))); + var signedJwt = new SignedJWT(new JWSHeader.Builder(algo).keyID(keyId).build(), claimsSet.build()); + + try { + signedJwt.sign(pk.signer()); + } catch (JOSEException e) { + throw new EdcException(e); + } + + return signedJwt.serialize(); + } + + private String createVpClaim(Stream rawVcs) { + var vcArray = Json.createArrayBuilder(); + rawVcs.forEach(vcArray::add); + + 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()) + .build() + .toString(); + } + + private JsonArrayBuilder stringArray(Collection values) { + var ja = Json.createArrayBuilder(); + values.forEach(s -> ja.add(s.toString())); + return ja; + } +} diff --git a/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreator.java b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreator.java new file mode 100644 index 000000000..7854aae2b --- /dev/null +++ b/core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreator.java @@ -0,0 +1,170 @@ +/* + * 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; + +import com.apicatalog.ld.signature.SignatureSuite; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.jwk.JWK; +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObject; +import org.eclipse.edc.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreator; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry; +import org.eclipse.edc.security.signature.jws2020.JwkMethod; +import org.eclipse.edc.spi.EdcException; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.util.reflection.ReflectionUtil; +import org.eclipse.edc.verifiablecredentials.linkeddata.LdpIssuer; +import org.jetbrains.annotations.NotNull; + +import java.net.URI; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static java.util.Optional.ofNullable; +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; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.CONTEXT; + +/** + * LdpPresentationCreator is a class that implements the PresentationCreator interface to generate Verifiable Presentations based on Verifiable Credential Containers. + * VPs are represented as {@link JsonObject}. + */ +public class LdpPresentationCreator implements PresentationCreator { + + public static final String ID_PROPERTY = "id"; + public static final String TYPE_PROPERTY = "type"; + 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; + private final String defaultSignatureSuite; + private final LdpIssuer ldpIssuer; + private final ObjectMapper mapper; + + public LdpPresentationCreator(PrivateKeyResolver privateKeyResolver, String ownDid, + SignatureSuiteRegistry signatureSuiteRegistry, String defaultSignatureSuite, LdpIssuer ldpIssuer, ObjectMapper mapper) { + this.privateKeyResolver = privateKeyResolver; + this.issuerId = ownDid; + this.signatureSuiteRegistry = signatureSuiteRegistry; + this.defaultSignatureSuite = defaultSignatureSuite; + this.ldpIssuer = ldpIssuer; + this.mapper = mapper; + } + + /** + * Will always throw an {@link UnsupportedOperationException}. + * Please use {@link LdpPresentationCreator#createPresentation(List, String, Map)} instead. + */ + @Override + public JsonObject createPresentation(List credentials, String keyId) { + throw new UnsupportedOperationException("Must provide additional data: 'types'"); + + } + + /** + * 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. + * @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 createPresentation(List credentials, String keyId, Map additionalData) { + if (!additionalData.containsKey("types")) { + throw new IllegalArgumentException("Must provide additional data: 'types'"); + } + + var keyIdUri = URI.create(keyId); + + var suiteIdentifier = additionalData.getOrDefault("suite", defaultSignatureSuite).toString(); + var suite = signatureSuiteRegistry.getForId(suiteIdentifier); + if (suite == null) { + throw new IllegalArgumentException("No SignatureSuite for identifier '%s' was found.".formatted(suiteIdentifier)); + } + + if (credentials.stream().anyMatch(c -> c.format() != CredentialFormat.JSON_LD)) { + throw new IllegalArgumentException("One or more VerifiableCredentials cannot be represented in the desired format " + CredentialFormat.JSON_LD); + } + + // check if private key can be resolved + var pk = ofNullable(privateKeyResolver.resolvePrivateKey(keyId, PrivateKeyWrapper.class)) + .orElseThrow(() -> new IllegalArgumentException("No key could be found with key ID '%s'.".formatted(keyId))); + + var types = (List) additionalData.get("types"); + var presentationObject = Json.createObjectBuilder() + .add(CONTEXT, stringArray(List.of(IATP_CONTEXT_URL, W3C_CREDENTIALS_URL, PRESENTATION_EXCHANGE_URL))) + .add(ID_PROPERTY, IATP_CONTEXT_URL + "/id/" + UUID.randomUUID()) + .add(TYPE_PROPERTY, stringArray(types)) + .add(HOLDER_PROPERTY, issuerId) + .add(VERIFIABLE_CREDENTIAL_PROPERTY, toJsonArray(credentials)) + .build(); + + return signPresentation(presentationObject, suite, pk, keyIdUri); + } + + @NotNull + private JsonArray toJsonArray(List credentials) { + var array = Json.createArrayBuilder(); + credentials.stream() + .map(VerifiableCredentialContainer::rawVc) + .map(str -> { + try { + return mapper.readValue(str, JsonObject.class); + } catch (JsonProcessingException e) { + throw new EdcException(e); + } + }) + .forEach(array::add); + return array.build(); + } + + private JsonObject signPresentation(JsonObject presentationObject, SignatureSuite suite, PrivateKeyWrapper pk, URI keyId) { + var type = URI.create(suite.getId().toString()); + var jwk = extractKey(pk); + var keypair = new JwkMethod(keyId, type, null, jwk); + + return ldpIssuer.signDocument(presentationObject, keypair, suite.createOptions()) + .orElseThrow(f -> new EdcException(f.getFailureDetail())); + } + + private JWK extractKey(PrivateKeyWrapper pk) { + return ReflectionUtil.getFieldValue("privateKey", pk); + } + + private JsonArrayBuilder stringArray(Collection values) { + var ja = Json.createArrayBuilder(); + values.forEach(s -> ja.add(s.toString())); + return ja; + } + +} diff --git a/core/identity-hub-core/src/main/resources/credentials.v1.json b/core/identity-hub-core/src/main/resources/credentials.v1.json new file mode 100644 index 000000000..3c21ea288 --- /dev/null +++ b/core/identity-hub-core/src/main/resources/credentials.v1.json @@ -0,0 +1,315 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": { + "@id": "cred:credentialStatus", + "@type": "@id" + }, + "credentialSubject": { + "@id": "cred:credentialSubject", + "@type": "@id" + }, + "evidence": { + "@id": "cred:evidence", + "@type": "@id" + }, + "expirationDate": { + "@id": "cred:expirationDate", + "@type": "xsd:dateTime" + }, + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "issued": { + "@id": "cred:issued", + "@type": "xsd:dateTime" + }, + "issuer": { + "@id": "cred:issuer", + "@type": "@id" + }, + "issuanceDate": { + "@id": "cred:issuanceDate", + "@type": "xsd:dateTime" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": { + "@id": "cred:termsOfUse", + "@type": "@id" + }, + "validFrom": { + "@id": "cred:validFrom", + "@type": "xsd:dateTime" + }, + "validUntil": { + "@id": "cred:validUntil", + "@type": "xsd:dateTime" + } + } + }, + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "verifiableCredential": { + "@id": "cred:verifiableCredential", + "@type": "@id", + "@container": "@graph" + } + } + }, + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/main/resources/did.json b/core/identity-hub-core/src/main/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-core/src/main/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/main/resources/jws2020.json b/core/identity-hub-core/src/main/resources/jws2020.json new file mode 100644 index 000000000..30f74b118 --- /dev/null +++ b/core/identity-hub-core/src/main/resources/jws2020.json @@ -0,0 +1,78 @@ +{ + "@context": { + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "JsonWebKey2020": { + "@id": "https://w3id.org/security#JsonWebKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + } + } + }, + "JsonWebSignature2020": { + "@id": "https://w3id.org/security#JsonWebSignature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/main/resources/presentation-submission.v1.json b/core/identity-hub-core/src/main/resources/presentation-submission.v1.json new file mode 100644 index 000000000..488925037 --- /dev/null +++ b/core/identity-hub-core/src/main/resources/presentation-submission.v1.json @@ -0,0 +1,15 @@ +{ + "@context": { + "@version": 1.1, + "PresentationSubmission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@context": { + "@version": 1.1, + "presentation_submission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@type": "@json" + } + } + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImplTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImplTest.java new file mode 100644 index 000000000..f48eb7f26 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/PresentationGeneratorImplTest.java @@ -0,0 +1,184 @@ +/* + * 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; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.json.JsonObject; +import org.eclipse.edc.identityhub.spi.generator.PresentationCreatorRegistry; +import org.eclipse.edc.identityhub.spi.model.presentationdefinition.PresentationDefinition; +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.VerifiableCredentialContainer; +import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.spi.monitor.Monitor; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +import static org.eclipse.edc.identityhub.core.creators.TestData.EMPTY_LDP_VP; +import static org.eclipse.edc.identityhub.core.creators.TestData.JWT_VP; +import static org.eclipse.edc.identityhub.core.creators.TestData.LDP_VP_WITH_PROOF; +import static org.eclipse.edc.identitytrust.model.CredentialFormat.JSON_LD; +import static org.eclipse.edc.identitytrust.model.CredentialFormat.JWT; +import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class PresentationGeneratorImplTest { + + private final Monitor monitor = mock(); + private final PresentationCreatorRegistry registry = mock(); + private final ObjectMapper mapper = JacksonJsonLd.createObjectMapper(); + private PresentationGeneratorImpl presentationGenerator; + + + @Test + void generate_noCredentials() { + when(registry.createPresentation(anyList(), eq(JSON_LD))).thenReturn(jsonObject(EMPTY_LDP_VP)); + presentationGenerator = new PresentationGeneratorImpl(JSON_LD, registry, monitor); + List ldpVcs = List.of(); + + var result = presentationGenerator.createPresentation(ldpVcs, null); + assertThat(result).isSucceeded(); + } + + @Test + void generate_defaultFormatLdp_containsOnlyLdpVc() { + when(registry.createPresentation(any(), eq(JSON_LD))).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + presentationGenerator = new PresentationGeneratorImpl(JSON_LD, registry, monitor); + + var credentials = List.of(createCredential(JSON_LD), createCredential(JSON_LD)); + var result = presentationGenerator.createPresentation(credentials, null); + + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JSON_LD)); + } + + @Test + void generate_defaultFormatLdp_mixedVcs() { + when(registry.createPresentation(any(), eq(JSON_LD))).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + when(registry.createPresentation(any(), eq(JWT))).thenReturn(JWT_VP); + presentationGenerator = new PresentationGeneratorImpl(JSON_LD, registry, monitor); + + var credentials = List.of(createCredential(JSON_LD), createCredential(JWT)); + + var result = presentationGenerator.createPresentation(credentials, null); + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 1), eq(JWT)); + verify(registry).createPresentation(argThat(argument -> argument.size() == 1), eq(JSON_LD)); + verify(monitor).warning(eq("The VP was requested in JSON_LD format, but the request yielded 1 JWT-VCs, which cannot be transported in a LDP-VP. A second VP will be returned, containing JWT-VCs")); + } + + @Test + void generate_defaultFormatLdp_onlyJwtVcs() { + when(registry.createPresentation(any(), eq(JWT))).thenReturn(JWT_VP); + when(registry.createPresentation(any(), eq(JSON_LD))).thenReturn(jsonObject(EMPTY_LDP_VP)); + presentationGenerator = new PresentationGeneratorImpl(JSON_LD, registry, monitor); + + var credentials = List.of(createCredential(JWT), createCredential(JWT)); + + var result = presentationGenerator.createPresentation(credentials, null); + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT)); + verify(registry, never()).createPresentation(any(), eq(JSON_LD)); + 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")); + } + + @Test + void generate_defaultFormatJwt_onlyJwtVcs() { + when(registry.createPresentation(any(), eq(JWT))).thenReturn(JWT_VP); + when(registry.createPresentation(any(), eq(JSON_LD))).thenReturn(jsonObject(EMPTY_LDP_VP)); + presentationGenerator = new PresentationGeneratorImpl(JWT, registry, monitor); + + var credentials = List.of(createCredential(JWT), createCredential(JWT)); + + var result = presentationGenerator.createPresentation(credentials, null); + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT)); + verify(registry, never()).createPresentation(any(), eq(JSON_LD)); + } + + @Test + void generate_defaultFormatJwt_mixedVcs() { + when(registry.createPresentation(any(), eq(JSON_LD))).thenReturn(jsonObject(LDP_VP_WITH_PROOF)); + when(registry.createPresentation(any(), eq(JWT))).thenReturn(JWT_VP); + presentationGenerator = new PresentationGeneratorImpl(JWT, registry, monitor); + + var credentials = List.of(createCredential(JSON_LD), createCredential(JWT)); + + var result = presentationGenerator.createPresentation(credentials, null); + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT)); + verify(registry, never()).createPresentation(any(), eq(JSON_LD)); + } + + @Test + void generate_defaultFormatJwt_onlyLdpVc() { + when(registry.createPresentation(any(), eq(JWT))).thenReturn(JWT_VP); + presentationGenerator = new PresentationGeneratorImpl(JWT, registry, monitor); + + var credentials = List.of(createCredential(JSON_LD), createCredential(JSON_LD)); + var result = presentationGenerator.createPresentation(credentials, null); + + assertThat(result).isSucceeded(); + verify(registry).createPresentation(argThat(argument -> argument.size() == 2), eq(JWT)); + verify(registry, never()).createPresentation(any(), eq(JSON_LD)); + } + + @Test + void generate_withPresentationDef_shouldLogWarning() { + presentationGenerator = new PresentationGeneratorImpl(JSON_LD, registry, monitor); + presentationGenerator.createPresentation(List.of(), PresentationDefinition.Builder.newInstance().id("test-id").build()); + verify(monitor).warning(contains("A PresentationDefinition was submitted, but is currently ignored by the generator.")); + + } + + protected VerifiableCredential createDummyCredential() { + return VerifiableCredential.Builder.newInstance() + .type("VerifiableCredential") + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("test-subject") + .claim("test-claim", "test-value") + .build()) + .issuer(new Issuer("test-issuer", Map.of())) + .issuanceDate(Instant.now()) + .build(); + } + + private VerifiableCredentialContainer createCredential(CredentialFormat format) { + return new VerifiableCredentialContainer("foobar", format, createDummyCredential()); + } + + private JsonObject jsonObject(String json) { + try { + return mapper.readValue(json, JsonObject.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreatorTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreatorTest.java new file mode 100644 index 000000000..0ab74a8ec --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/JwtPresentationCreatorTest.java @@ -0,0 +1,151 @@ +/* + * 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; + +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jwt.JWTClaimsSet; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.edc.iam.did.crypto.key.EcPrivateKeyWrapper; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.verifiablecredentials.jwt.JwtCreationUtils; +import org.eclipse.edc.verifiablecredentials.jwt.TestConstants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.text.ParseException; +import java.time.Clock; +import java.util.List; +import java.util.Map; + +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JwtPresentationCreatorTest extends PresentationCreatorTest { + 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 JwtPresentationCreator creator; + + @BeforeEach + void setup() { + var vpSigningKey = createKey(Curve.P_384, "vp-key"); + when(resolverMock.resolvePrivateKey(eq(KEY_ID), any())).thenReturn(new EcPrivateKeyWrapper(vpSigningKey)); + creator = new JwtPresentationCreator(resolverMock, Clock.systemUTC(), "did:web:test-issuer"); + } + + @Test + @DisplayName("Verify succesful creation of a JWT_VP") + void createPresentation_success() { + var vcSigningKey = createKey(Curve.P_256, TestConstants.CENTRAL_ISSUER_KEY_ID); + 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.createPresentation(List.of(vcc), KEY_ID, audClaim); + assertThat(vpJwt).isNotNull(); + assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); + var claims = parseJwt(vpJwt); + + REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim) + .isNotNull()); + + } + + @Test + @DisplayName("Should create a JWT_VP with VCs of different formats") + void create_whenVcsNotSameFormat() { + var vcSigningKey = createKey(Curve.P_256, TestConstants.CENTRAL_ISSUER_KEY_ID); + var jwtVc = JwtCreationUtils.createJwt(vcSigningKey, TestConstants.CENTRAL_ISSUER_DID, "degreeSub", TestConstants.VP_HOLDER_ID, Map.of("vc", TestConstants.VC_CONTENT_DEGREE_EXAMPLE)); + var ldpVc = TestData.LDP_VC_WITH_PROOF; + + var vc1 = new VerifiableCredentialContainer(jwtVc, CredentialFormat.JWT, createDummyCredential()); + var vc2 = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); + + var vpJwt = creator.createPresentation(List.of(vc1, vc2), KEY_ID, audClaim); + assertThat(vpJwt).isNotNull(); + assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); + + var claims = parseJwt(vpJwt); + + REQUIRED_CLAIMS.forEach(claim -> assertThat(claims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim) + .isNotNull()); + } + + @Test + @DisplayName("Should create a valid VP with no credential") + void create_whenVcsEmpty_shouldReturnEmptyVp() { + var vpJwt = creator.createPresentation(List.of(), KEY_ID, audClaim); + assertThat(vpJwt).isNotNull(); + assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); + + var claims = parseJwt(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() { + var vcc = new VerifiableCredentialContainer("foobar", CredentialFormat.JWT, createDummyCredential()); + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), "not-exist", audClaim)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("Should throw an exception if the required additional data is missing") + @Override + void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException() { + var vcc = new VerifiableCredentialContainer("foobar", CredentialFormat.JWT, createDummyCredential()); + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), KEY_ID)) + .describedAs("Expected exception when no additional data provided") + .isInstanceOf(UnsupportedOperationException.class); + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), KEY_ID, Map.of())) + .describedAs("Expected exception when additional data does not contain expected value ('aud')") + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("Should return an empty JWT when no credentials are passed") + void create_whenEmptyList() { + + var vpJwt = creator.createPresentation(List.of(), KEY_ID, audClaim); + assertThat(vpJwt).isNotNull(); + assertThatNoException().isThrownBy(() -> SignedJWT.parse(vpJwt)); + var claims = parseJwt(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) { + try { + return SignedJWT.parse(vpJwt).getJWTClaimsSet(); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreatorTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreatorTest.java new file mode 100644 index 000000000..2d1266a05 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/LdpPresentationCreatorTest.java @@ -0,0 +1,184 @@ +/* + * 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; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.JWEDecrypter; +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.Ed25519Signer; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.OctetKeyPair; +import com.nimbusds.jose.jwk.gen.OctetKeyPairGenerator; +import org.eclipse.edc.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.edc.identityhub.spi.model.IdentityHubConstants; +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; +import org.eclipse.edc.identitytrust.verification.SignatureSuiteRegistry; +import org.eclipse.edc.jsonld.TitaniumJsonLd; +import org.eclipse.edc.jsonld.util.JacksonJsonLd; +import org.eclipse.edc.junit.testfixtures.TestUtils; +import org.eclipse.edc.security.signature.jws2020.JwsSignature2020Suite; +import org.eclipse.edc.spi.security.PrivateKeyResolver; +import org.eclipse.edc.verifiablecredentials.jwt.JwtCreationUtils; +import org.eclipse.edc.verifiablecredentials.jwt.TestConstants; +import org.eclipse.edc.verifiablecredentials.linkeddata.LdpIssuer; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.DID_CONTEXT_URL; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.IATP_CONTEXT_URL; +import static org.eclipse.edc.identityhub.spi.model.IdentityHubConstants.JWS_2020_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; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class LdpPresentationCreatorTest extends PresentationCreatorTest { + + private final PrivateKeyResolver resolverMock = mock(); + private final Map types = Map.of("types", List.of("VerifiablePresentation", "SomeOtherPresentationType")); + private LdpPresentationCreator creator; + + @BeforeEach + void setup() throws URISyntaxException { + var vpSigningKey = createKey(KEY_ID); + when(resolverMock.resolvePrivateKey(eq(KEY_ID), any())).thenReturn(new OctetKeyPairWrapper(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 LdpPresentationCreator(resolverMock, "did:web:test-issuer", signatureSuiteRegistryMock, IdentityHubConstants.JWS_2020_SIGNATURE_SUITE, ldpIssuer, + JacksonJsonLd.createObjectMapper()); + } + + @Override + @Test + public void createPresentation_success() { + var ldpVc = TestData.LDP_VC_WITH_PROOF; + var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); + + var result = creator.createPresentation(List.of(vcc), KEY_ID, types); + assertThat(result).isNotNull(); + assertThat(result.get("https://w3id.org/security#proof")).isNotNull(); + } + + @Override + @Test + public void create_whenVcsNotSameFormat() { + var ldpVc = TestData.LDP_VC_WITH_PROOF; + var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); + + var vcSigningKey = createKey(Curve.P_256, TestConstants.CENTRAL_ISSUER_KEY_ID); + 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.createPresentation(List.of(vcc, vcc2), KEY_ID, types)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("One or more VerifiableCredentials cannot be represented in the desired format %s".formatted(CredentialFormat.JSON_LD)); + } + + @Override + @Test + public void create_whenVcsEmpty_shouldReturnEmptyVp() { + var result = creator.createPresentation(List.of(), KEY_ID, types); + assertThat(result).isNotNull(); + } + + @Override + @Test + public void create_whenKeyNotFound() { + var ldpVc = TestData.LDP_VC_WITH_PROOF; + var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), "not-exists", types)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("No key could be found with key ID 'not-exists'"); + } + + @Override + @Test + public void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException() { + var ldpVc = TestData.LDP_VC_WITH_PROOF; + var vcc = new VerifiableCredentialContainer(ldpVc, CredentialFormat.JSON_LD, createDummyCredential()); + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), KEY_ID)).isInstanceOf(UnsupportedOperationException.class) + .hasMessage("Must provide additional data: 'types'"); + + assertThatThrownBy(() -> creator.createPresentation(List.of(vcc), KEY_ID, Map.of("some-key", "some-value"))) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Must provide additional data: 'types'"); + } + + @Test + @DisplayName("Should return an empty JWT when no credentials are passed") + @Override + void create_whenEmptyList() { + + var result = creator.createPresentation(List.of(), KEY_ID, types); + assertThat(result).isNotNull(); + assertThat(result.get("https://w3id.org/security#proof")).isNotNull(); + } + + private OctetKeyPair createKey(String keyId) { + try { + return new OctetKeyPairGenerator(Curve.Ed25519) + .keyID(keyId) + .generate(); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + @NotNull + private TitaniumJsonLd initializeJsonLd() { + var jld = new TitaniumJsonLd(mock()); + jld.registerCachedDocument("https://www.w3.org/ns/odrl.jsonld", TestUtils.getResource("odrl.jsonld")); + jld.registerCachedDocument(DID_CONTEXT_URL, TestUtils.getResource("did.json")); + jld.registerCachedDocument(JWS_2020_URL, TestUtils.getResource("jws2020.json")); + jld.registerCachedDocument(W3C_CREDENTIALS_URL, TestUtils.getResource("credentials.v1.json")); + jld.registerCachedDocument(IATP_CONTEXT_URL, TestUtils.getResource("presentation-query.v08.json")); + jld.registerCachedDocument(PRESENTATION_EXCHANGE_URL, TestUtils.getResource("presentation-exchange.v1.json")); + jld.registerCachedDocument("https://www.w3.org/2018/credentials/examples/v1", TestUtils.getResource("examples.v1.json")); + return jld; + } + + private record OctetKeyPairWrapper(OctetKeyPair privateKey) implements PrivateKeyWrapper { + + @Override + public JWEDecrypter decrypter() { + return null; // not needed here + } + + @Override + public JWSSigner signer() { + try { + return new Ed25519Signer(privateKey); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationCreatorTest.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationCreatorTest.java new file mode 100644 index 000000000..ad59470fd --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/PresentationCreatorTest.java @@ -0,0 +1,79 @@ +/* + * 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; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.Curve; +import com.nimbusds.jose.jwk.ECKey; +import com.nimbusds.jose.jwk.gen.ECKeyGenerator; +import org.eclipse.edc.identitytrust.model.CredentialSubject; +import org.eclipse.edc.identitytrust.model.Issuer; +import org.eclipse.edc.identitytrust.model.VerifiableCredential; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.Map; + +abstract class PresentationCreatorTest { + + public static final String KEY_ID = "https://test.com/test-keys#key-1"; + + @Test + @DisplayName("Verify succesful creation of a JWT_VP") + abstract void createPresentation_success(); + + @Test + @DisplayName("Should create a JWT_VP with VCs of different formats") + abstract void create_whenVcsNotSameFormat(); + + @Test + @DisplayName("Should create a valid VP with no credential") + abstract void create_whenVcsEmpty_shouldReturnEmptyVp(); + + @Test + @DisplayName("Should throw an exception if no key is found for a key-id") + abstract void create_whenKeyNotFound(); + + @Test + @DisplayName("Should throw an exception if the required additional data is missing") + abstract void create_whenRequiredAdditionalDataMissing_throwsIllegalArgumentException(); + + @Test + @DisplayName("Should return an empty JWT when no credentials are passed") + abstract void create_whenEmptyList(); + + protected ECKey createKey(Curve p256, String centralIssuerKeyId) { + try { + return new ECKeyGenerator(p256) + .keyID(centralIssuerKeyId) + .generate(); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + + protected VerifiableCredential createDummyCredential() { + return VerifiableCredential.Builder.newInstance() + .type("VerifiableCredential") + .credentialSubject(CredentialSubject.Builder.newInstance() + .id("test-subject") + .claim("test-claim", "test-value") + .build()) + .issuer(new Issuer("test-issuer", Map.of())) + .issuanceDate(Instant.now()) + .build(); + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/TestData.java b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/TestData.java new file mode 100644 index 000000000..d711d3e89 --- /dev/null +++ b/core/identity-hub-core/src/test/java/org/eclipse/edc/identityhub/core/creators/TestData.java @@ -0,0 +1,149 @@ +/* + * 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; + +public interface TestData { + String LDP_VC_WITH_PROOF = """ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://www.w3.org/2018/credentials/examples/v1" + ], + "id": "http://example.gov/credentials/3732", + "type": ["VerifiableCredential", "UniversityDegreeCredential"], + "issuer": "https://example.edu", + "issuanceDate": "2010-01-01T19:23:24Z", + "credentialSubject": { + "id": "did:example:ebfeb1f712ebc6f1c276e12ec21", + "degree": { + "type": "BachelorDegree", + "name": "Bachelor of Science and Arts" + } + }, + "proof": { + "type": "Ed25519Signature2020", + "created": "2021-11-13T18:19:39Z", + "verificationMethod": "https://example.edu/issuers/14#key-1", + "proofPurpose": "assertionMethod", + "proofValue": "z58DAdFfa9SkqZMVPxAQpic7ndSayn1PzZs6ZjWp1CktyGesjuTSwRdoWhAfGFCF5bppETSTojQCrfFPP2oumHKtz" + } + } + """; + + String JWT_VC = + "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImRpZDpleGFtcGxlOmFiZmUxM2Y3MTIxMjA0MzFjMjc2ZTEyZWNhYiNrZXlzLTEifQ" + + ".eyJzdWIiOiJkaWQ6ZXhhbXBsZTplYmZlYjFmNzEyZWJjNmYxYzI3NmUxMmVjMjEiLCJqdGkiOiJodHRwOi8vZXhhbXBsZS5lZHUvY" + + "3JlZGVudGlhbHMvMzczMiIsImlzcyI6Imh0dHBzOi8vZXhhbXBsZS5jb20va2V5cy9mb28uandrIiwibmJmIjoxNTQxNDkzNzI0LCJ" + + "pYXQiOjE1NDE0OTM3MjQsImV4cCI6MTU3MzAyOTcyMywibm9uY2UiOiI2NjAhNjM0NUZTZXIiLCJ2YyI6eyJAY29udGV4dCI6WyJod" + + "HRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy92MSIsImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL2V" + + "4YW1wbGVzL3YxIl0sInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJVbml2ZXJzaXR5RGVncmVlQ3JlZGVudGlhbCJdLCJjc" + + "mVkZW50aWFsU3ViamVjdCI6eyJkZWdyZWUiOnsidHlwZSI6IkJhY2hlbG9yRGVncmVlIiwibmFtZSI6IjxzcGFuIGxhbmc9J2ZyLUNB" + + "Jz5CYWNjYWxhdXLDqWF0IGVuIG11c2lxdWVzIG51bcOpcmlxdWVzPC9zcGFuPiJ9fX19.KLJo5GAyBND3LDTn9H7FQokEsUEi8jKwXh" + + "GvoN3JtRa51xrNDgXDb0cq1UTYB-rK4Ft9YVmR1NI_ZOF8oGc_7wAp8PHbF2HaWodQIoOBxxT-4WNqAxft7ET6lkH-4S6Ux3rSGAmc" + + "zMohEEf8eCeN-jC8WekdPl6zKZQj0YPB1rx6X0-xlFBs7cl6Wt8rfBP_tZ9YgVWrQmUWypSioc0MUyiphmyEbLZagTyPlUyflGlEdqr" + + "ZAv6eSe6RtxJy6M1-lD7a5HTzanYTWBPAUHDZGyGKXdJw-W_x0IWChBzI8t3kpG253fg6V3tPgHeKXE94fz_QpYfg--7kLsyBAfQGbg"; + + String LDP_VP_WITH_PROOF = """ + { + "id": "urn:uuid:3978344f-8596-4c3a-a978-8fcaba3903c5", + "type": "VerifiablePresentation", + "verifiableCredential": { + "issuanceDate": "2023-06-12T13:13:30Z", + "credentialSubject": { + "http://schema.org/identifier": "member0123456789", + "id": "did:web:localhost:member0123456789", + "type": "https://org.eclipse.edc/linkedCredentialData#MembershipCredential" + }, + "id": "https://org.eclipse.edc/testcases/t0001", + "type": [ + "VerifiableCredential" + ], + "issuer": "did:web:localhost:member0123456789", + "expirationDate": "2024-12-31T23:00:00Z", + "proof": { + "type": "JsonWebSignature2020", + "created": "2022-12-31T23:00:00Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "https://org.eclipse.edc/verification-method", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzM4NCJ9..SwEkR4duA97jHy_WSVKIHLJqd8i2IidedmlMpUKyeV0YlPNz0pjPEKM9p7PqBb7oRIKG3-5qCxpzNhbsIEZZMzEMjWE1adckJ9SMiNr_G1wiAh3Op0cZHDgZBevIPElG" + } + }, + "proof": { + "type": "JsonWebSignature2020", + "created": "2022-12-31T23:00:00Z", + "proofPurpose": "assertionMethod", + "verificationMethod": "https://org.eclipse.edc/verification-method", + "jws": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFUzM4NCJ9..eadcijhno0JUZ2yl2QrlQBD_1rrGFS1qjYeGYV8O-XN1P-28HneLnHkvUH9IDTiTTAwnCQjdr0Tq3NEgpbz-sji0X9fT-chM86OQfqylm0Dt6_jLIj-32JHtetFU3QXS" + }, + "@context": [ + "https://www.w3.org/ns/did/v1", + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/security/suites/jws-2020/v1" + ] + } + """; + String JWT_VP = + "eyJhbGciOiJFZERTQSJ9.eyJuYmYiOjE2MDI3NjQ4MDEsImlzcyI6ImRpZDpleGFtcGxlOmViZmViMWY3MTJlYmM2ZjFjMjc2ZTEyZWMyMS" + + "IsInZwIjp7IkBjb250ZXh0IjoiaHR0cHM6Ly93d3cudzMub3JnLzIwMTgvY3JlZGVudGlhbHMvdjEiLCJ0eXBlIjoiVmVyaWZpY" + + "WJsZVByZXNlbnRhdGlvbiIsInZlcmlmaWFibGVDcmVkZW50aWFsIjoiZXlKaGJHY2lPaUpGWkVSVFFTSjkuZXlKemRXSWlPaUpr" + + "YVdRNlpYaGhiWEJzWlRwbFltWmxZakZtTnpFeVpXSmpObVl4WXpJM05tVXhNbVZqTWpFaUxDSnVZbVlpT2pFMU5qQTNNVEUwTVR" + + "rc0ltbHpjeUk2SW1ScFpEcGxlR0Z0Y0d4bE9qYzJaVEV5WldNM01USmxZbU0yWmpGak1qSXhaV0ptWldJeFppSXNJbVY0Y0NJNk" + + "1UVTJNRGM1TnpneE9Td2lkbU1pT25zaVFHTnZiblJsZUhRaU9sc2lhSFIwY0hNNkx5OTNkM2N1ZHpNdWIzSm5Mekl3TVRndlkzS" + + "mxaR1Z1ZEdsaGJITXZkakVpTENKb2RIUndjem92TDNkM2R5NTNNeTV2Y21jdk1qQXhPQzlqY21Wa1pXNTBhV0ZzY3k5bGVHRnRj" + + "R3hsY3k5Mk1TSmRMQ0owZVhCbElqcGJJbFpsY21sbWFXRmliR1ZEY21Wa1pXNTBhV0ZzSWl3aVZXNXBkbVZ5YzJsMGVVUmxaM0p" + + "sWlVOeVpXUmxiblJwWVd3aVhTd2lZM0psWkdWdWRHbGhiRk4xWW1wbFkzUWlPbnNpWTI5c2JHVm5aU0k2SWxSbGMzUWdWVzVwZG" + + "1WeWMybDBlU0lzSW1SbFozSmxaU0k2ZXlKdVlXMWxJam9pUW1GamFHVnNiM0lnYjJZZ1UyTnBaVzVqWlNCaGJtUWdRWEowY3lJc" + + "0luUjVjR1VpT2lKQ1lXTm9aV3h2Y2tSbFozSmxaU0o5Zlgwc0ltcDBhU0k2SW1oMGRIQTZMeTlsZUdGdGNHeGxMbVZrZFM5amNt" + + "VmtaVzUwYVdGc2N5OHpOek15SW4wLkdEcENPbHhpWjJpc0JRbjE1MWk1UGoyZS1rVWdrTmdfd3p4Q1BBZnhMeHRkT3o0ZnBEaW1" + + "nODFtTnczTHNuTzBHNTZBT1R2RDRTdXpTUXlqMWNQM0JnIn0sImlhdCI6MTYwMjc2NDgwMSwianRpIjoidXJuOnV1aWQ6ZWM3ND" + + "E1NTYtM2Y2ZS00ODkxLWJlNTQtNzRjMjNmZDkzNjA1In0.kv4Votk1DpFT4Irr-v85W3lorPo9r2p9qwdDrq4kH_veo7qTKtiNh" + + "C7BshUwP7zDN5_gD3GTr68OoNks2LoXDw"; + + String EMPTY_LDP_VP = """ + { + "@id": "https://w3id.org/tractusx-trust/v0.8/id/5ce0eb84-c12d-413f-9d3f-c6a9ce339490", + "@type": [ + "https://www.w3.org/2018/credentials#VerifiablePresentation", + "SomeOtherPresentationType" + ], + "https://www.w3.org/2018/credentials#holder": [ + { + "@id": "did:web:test-issuer" + } + ], + "https://www.w3.org/2018/credentials#verifiableCredential": [], + "https://w3id.org/security#proof": [ + { + "@type": [ + "https://w3id.org/security#JsonWebSignature2020" + ], + "https://w3id.org/security#jws": [ + { + "@value": "eyJiNjQiOmZhbHNlLCJjcml0IjpbImI2NCJdLCJhbGciOiJFZERTQSJ9..XDuivXfZ-NMeoYDtsQHTpX6vpWiJNYEZYiAtNX09eVGHCXznSqdudliPYARXwaI1WbgDaH8kvVho8a8Z5aZcCA" + } + ] + } + ] + } + """; + + String EMPTY_JWT_VP = "eyJraWQiOiJodHRwczovL3Rlc3QuY29tL3Rlc3Qta2V5cyNrZXktMSIsImFsZyI6IkVTMzg0In0.eyJhdWQiOiJkaWQ6d2V" + + "iOnRlc3QtYXVkaWVuY2UiLCJuYmYiOjE2OTk5Nzg2NTksImlzcyI6ImRpZDp3ZWI6dGVzdC1pc3N1ZXIiLCJ2cCI6IntcIkBjb250ZXh0XC" + + "I6W1wiaHR0cHM6Ly93M2lkLm9yZy90cmFjdHVzeC10cnVzdC92MC44XCIsXCJodHRwczovL3d3dy53My5vcmcvMjAxOC9jcmVkZW50aWFscy" + + "92MVwiLFwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uL3ByZXNlbnRhdGlvbi1leGNoYW5nZS9zdWJtaXNzaW9uL3YxXCJdLFwidHlwZ" + + "VwiOlwiVmVyaWZpYWJsZVByZXNlbnRhdGlvblwiLFwidmVyaWZpYWJsZUNyZWRlbnRpYWxcIjpbXX0iLCJleHAiOjE2OTk5Nzg3MjEsImlhd" + + "CI6MTY5OTk3ODY1OSwianRpIjoiYTEzN2JkMDUtMjAxOS00Yjg3LTlhN2UtYzdlODBjOTNlNzFjIn0.TdeDOMpCCHOCHmVZjNOg0L4e2gFc" + + "v6Pz_Adwg_SrGT0Cv94EzoGl9bl7LwcuK7mtTbbzLbOnwUpVk8xhDfiME8sVENjCMYJP9Vz1QaT32e6cGWdtAIgLHnZ7RelRd5DH"; +} diff --git a/core/identity-hub-core/src/test/resources/credentials.v1.json b/core/identity-hub-core/src/test/resources/credentials.v1.json new file mode 100644 index 000000000..3c21ea288 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/credentials.v1.json @@ -0,0 +1,315 @@ +{ + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "VerifiableCredential": { + "@id": "https://www.w3.org/2018/credentials#VerifiableCredential", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "credentialSchema": { + "@id": "cred:credentialSchema", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "JsonSchemaValidator2018": "cred:JsonSchemaValidator2018" + } + }, + "credentialStatus": { + "@id": "cred:credentialStatus", + "@type": "@id" + }, + "credentialSubject": { + "@id": "cred:credentialSubject", + "@type": "@id" + }, + "evidence": { + "@id": "cred:evidence", + "@type": "@id" + }, + "expirationDate": { + "@id": "cred:expirationDate", + "@type": "xsd:dateTime" + }, + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "issued": { + "@id": "cred:issued", + "@type": "xsd:dateTime" + }, + "issuer": { + "@id": "cred:issuer", + "@type": "@id" + }, + "issuanceDate": { + "@id": "cred:issuanceDate", + "@type": "xsd:dateTime" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "refreshService": { + "@id": "cred:refreshService", + "@type": "@id", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "ManualRefreshService2018": "cred:ManualRefreshService2018" + } + }, + "termsOfUse": { + "@id": "cred:termsOfUse", + "@type": "@id" + }, + "validFrom": { + "@id": "cred:validFrom", + "@type": "xsd:dateTime" + }, + "validUntil": { + "@id": "cred:validUntil", + "@type": "xsd:dateTime" + } + } + }, + "VerifiablePresentation": { + "@id": "https://www.w3.org/2018/credentials#VerifiablePresentation", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "cred": "https://www.w3.org/2018/credentials#", + "sec": "https://w3id.org/security#", + "holder": { + "@id": "cred:holder", + "@type": "@id" + }, + "proof": { + "@id": "sec:proof", + "@type": "@id", + "@container": "@graph" + }, + "verifiableCredential": { + "@id": "cred:verifiableCredential", + "@type": "@id", + "@container": "@graph" + } + } + }, + "EcdsaSecp256k1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256k1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "EcdsaSecp256r1Signature2019": { + "@id": "https://w3id.org/security#EcdsaSecp256r1Signature2019", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "Ed25519Signature2018": { + "@id": "https://w3id.org/security#Ed25519Signature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "RsaSignature2018": { + "@id": "https://w3id.org/security#RsaSignature2018", + "@context": { + "@version": 1.1, + "@protected": true, + "challenge": "sec:challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "xsd:dateTime" + }, + "domain": "sec:domain", + "expires": { + "@id": "sec:expiration", + "@type": "xsd:dateTime" + }, + "jws": "sec:jws", + "nonce": "sec:nonce", + "proofPurpose": { + "@id": "sec:proofPurpose", + "@type": "@vocab", + "@context": { + "@version": 1.1, + "@protected": true, + "id": "@id", + "type": "@type", + "sec": "https://w3id.org/security#", + "assertionMethod": { + "@id": "sec:assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "sec:authenticationMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "proofValue": "sec:proofValue", + "verificationMethod": { + "@id": "sec:verificationMethod", + "@type": "@id" + } + } + }, + "proof": { + "@id": "https://w3id.org/security#proof", + "@type": "@id", + "@container": "@graph" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/resources/did.json b/core/identity-hub-core/src/test/resources/did.json new file mode 100644 index 000000000..511d12ef5 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/did.json @@ -0,0 +1,57 @@ +{ + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "alsoKnownAs": { + "@id": "https://www.w3.org/ns/activitystreams#alsoKnownAs", + "@type": "@id" + }, + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "controller": { + "@id": "https://w3id.org/security#controller", + "@type": "@id" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + }, + "service": { + "@id": "https://www.w3.org/ns/did#service", + "@type": "@id", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "serviceEndpoint": { + "@id": "https://www.w3.org/ns/did#serviceEndpoint", + "@type": "@id" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/resources/examples.v1.json b/core/identity-hub-core/src/test/resources/examples.v1.json new file mode 100644 index 000000000..e872482b8 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/examples.v1.json @@ -0,0 +1,69 @@ +{ + "@context": [ + { + "@version": 1.1 + }, + "https://www.w3.org/ns/odrl.jsonld", + { + "ex": "https://example.org/examples#", + "schema": "http://schema.org/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "3rdPartyCorrelation": "ex:3rdPartyCorrelation", + "AllVerifiers": "ex:AllVerifiers", + "Archival": "ex:Archival", + "BachelorDegree": "ex:BachelorDegree", + "Child": "ex:Child", + "CLCredentialDefinition2019": "ex:CLCredentialDefinition2019", + "CLSignature2019": "ex:CLSignature2019", + "IssuerPolicy": "ex:IssuerPolicy", + "HolderPolicy": "ex:HolderPolicy", + "Mother": "ex:Mother", + "RelationshipCredential": "ex:RelationshipCredential", + "UniversityDegreeCredential": "ex:UniversityDegreeCredential", + "AlumniCredential": "ex:AlumniCredential", + "DisputeCredential": "ex:DisputeCredential", + "PrescriptionCredential": "ex:PrescriptionCredential", + "ZkpExampleSchema2018": "ex:ZkpExampleSchema2018", + "issuerData": "ex:issuerData", + "attributes": "ex:attributes", + "signature": "ex:signature", + "signatureCorrectnessProof": "ex:signatureCorrectnessProof", + "primaryProof": "ex:primaryProof", + "nonRevocationProof": "ex:nonRevocationProof", + "alumniOf": { + "@id": "schema:alumniOf", + "@type": "rdf:HTML" + }, + "child": { + "@id": "ex:child", + "@type": "@id" + }, + "degree": "ex:degree", + "degreeType": "ex:degreeType", + "degreeSchool": "ex:degreeSchool", + "college": "ex:college", + "name": { + "@id": "schema:name", + "@type": "rdf:HTML" + }, + "givenName": "schema:givenName", + "familyName": "schema:familyName", + "parent": { + "@id": "ex:parent", + "@type": "@id" + }, + "referenceId": "ex:referenceId", + "documentPresence": "ex:documentPresence", + "evidenceDocument": "ex:evidenceDocument", + "spouse": "schema:spouse", + "subjectPresence": "ex:subjectPresence", + "verifier": { + "@id": "ex:verifier", + "@type": "@id" + }, + "currentStatus": "ex:currentStatus", + "statusReason": "ex:statusReason", + "prescription": "ex:prescription" + } + ] +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/resources/jws2020.json b/core/identity-hub-core/src/test/resources/jws2020.json new file mode 100644 index 000000000..30f74b118 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/jws2020.json @@ -0,0 +1,78 @@ +{ + "@context": { + "privateKeyJwk": { + "@id": "https://w3id.org/security#privateKeyJwk", + "@type": "@json" + }, + "JsonWebKey2020": { + "@id": "https://w3id.org/security#JsonWebKey2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "publicKeyJwk": { + "@id": "https://w3id.org/security#publicKeyJwk", + "@type": "@json" + } + } + }, + "JsonWebSignature2020": { + "@id": "https://w3id.org/security#JsonWebSignature2020", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "challenge": "https://w3id.org/security#challenge", + "created": { + "@id": "http://purl.org/dc/terms/created", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "domain": "https://w3id.org/security#domain", + "expires": { + "@id": "https://w3id.org/security#expiration", + "@type": "http://www.w3.org/2001/XMLSchema#dateTime" + }, + "jws": "https://w3id.org/security#jws", + "nonce": "https://w3id.org/security#nonce", + "proofPurpose": { + "@id": "https://w3id.org/security#proofPurpose", + "@type": "@vocab", + "@context": { + "@protected": true, + "id": "@id", + "type": "@type", + "assertionMethod": { + "@id": "https://w3id.org/security#assertionMethod", + "@type": "@id", + "@container": "@set" + }, + "authentication": { + "@id": "https://w3id.org/security#authenticationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityInvocation": { + "@id": "https://w3id.org/security#capabilityInvocationMethod", + "@type": "@id", + "@container": "@set" + }, + "capabilityDelegation": { + "@id": "https://w3id.org/security#capabilityDelegationMethod", + "@type": "@id", + "@container": "@set" + }, + "keyAgreement": { + "@id": "https://w3id.org/security#keyAgreementMethod", + "@type": "@id", + "@container": "@set" + } + } + }, + "verificationMethod": { + "@id": "https://w3id.org/security#verificationMethod", + "@type": "@id" + } + } + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/resources/odrl.jsonld b/core/identity-hub-core/src/test/resources/odrl.jsonld new file mode 100644 index 000000000..e779e87f7 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/odrl.jsonld @@ -0,0 +1,200 @@ +{ + "@context": { + "odrl": "http://www.w3.org/ns/odrl/2/", + "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", + "rdfs": "http://www.w3.org/2000/01/rdf-schema#", + "owl": "http://www.w3.org/2002/07/owl#", + "skos": "http://www.w3.org/2004/02/skos/core#", + "dct": "http://purl.org/dc/terms/", + "xsd": "http://www.w3.org/2001/XMLSchema#", + "vcard": "http://www.w3.org/2006/vcard/ns#", + "foaf": "http://xmlns.com/foaf/0.1/", + "schema": "http://schema.org/", + "cc": "http://creativecommons.org/ns#", + + "uid": "@id", + "type": "@type", + + "Policy": "odrl:Policy", + "Rule": "odrl:Rule", + "profile": {"@type": "@id", "@id": "odrl:profile"}, + + "inheritFrom": {"@type": "@id", "@id": "odrl:inheritFrom"}, + + "ConflictTerm": "odrl:ConflictTerm", + "conflict": {"@type": "@vocab", "@id": "odrl:conflict"}, + "perm": "odrl:perm", + "prohibit": "odrl:prohibit", + "invalid": "odrl:invalid", + + "Agreement": "odrl:Agreement", + "Assertion": "odrl:Assertion", + "Offer": "odrl:Offer", + "Privacy": "odrl:Privacy", + "Request": "odrl:Request", + "Set": "odrl:Set", + "Ticket": "odrl:Ticket", + + "Asset": "odrl:Asset", + "AssetCollection": "odrl:AssetCollection", + "relation": {"@type": "@id", "@id": "odrl:relation"}, + "hasPolicy": {"@type": "@id", "@id": "odrl:hasPolicy"}, + + "target": {"@type": "@id", "@id": "odrl:target"}, + "output": {"@type": "@id", "@id": "odrl:output"}, + + "partOf": {"@type": "@id", "@id": "odrl:partOf"}, + "source": {"@type": "@id", "@id": "odrl:source"}, + + "Party": "odrl:Party", + "PartyCollection": "odrl:PartyCollection", + "function": {"@type": "@vocab", "@id": "odrl:function"}, + "PartyScope": "odrl:PartyScope", + + "assignee": {"@type": "@id", "@id": "odrl:assignee"}, + "assigner": {"@type": "@id", "@id": "odrl:assigner"}, + "assigneeOf": {"@type": "@id", "@id": "odrl:assigneeOf"}, + "assignerOf": {"@type": "@id", "@id": "odrl:assignerOf"}, + "attributedParty": {"@type": "@id", "@id": "odrl:attributedParty"}, + "attributingParty": {"@type": "@id", "@id": "odrl:attributingParty"}, + "compensatedParty": {"@type": "@id", "@id": "odrl:compensatedParty"}, + "compensatingParty": {"@type": "@id", "@id": "odrl:compensatingParty"}, + "consentingParty": {"@type": "@id", "@id": "odrl:consentingParty"}, + "consentedParty": {"@type": "@id", "@id": "odrl:consentedParty"}, + "informedParty": {"@type": "@id", "@id": "odrl:informedParty"}, + "informingParty": {"@type": "@id", "@id": "odrl:informingParty"}, + "trackingParty": {"@type": "@id", "@id": "odrl:trackingParty"}, + "trackedParty": {"@type": "@id", "@id": "odrl:trackedParty"}, + "contractingParty": {"@type": "@id", "@id": "odrl:contractingParty"}, + "contractedParty": {"@type": "@id", "@id": "odrl:contractedParty"}, + + "Action": "odrl:Action", + "action": {"@type": "@vocab", "@id": "odrl:action"}, + "includedIn": {"@type": "@id", "@id": "odrl:includedIn"}, + "implies": {"@type": "@id", "@id": "odrl:implies"}, + + "Permission": "odrl:Permission", + "permission": {"@type": "@id", "@id": "odrl:permission"}, + + "Prohibition": "odrl:Prohibition", + "prohibition": {"@type": "@id", "@id": "odrl:prohibition"}, + + "obligation": {"@type": "@id", "@id": "odrl:obligation"}, + + "use": "odrl:use", + "grantUse": "odrl:grantUse", + "aggregate": "odrl:aggregate", + "annotate": "odrl:annotate", + "anonymize": "odrl:anonymize", + "archive": "odrl:archive", + "concurrentUse": "odrl:concurrentUse", + "derive": "odrl:derive", + "digitize": "odrl:digitize", + "display": "odrl:display", + "distribute": "odrl:distribute", + "execute": "odrl:execute", + "extract": "odrl:extract", + "give": "odrl:give", + "index": "odrl:index", + "install": "odrl:install", + "modify": "odrl:modify", + "move": "odrl:move", + "play": "odrl:play", + "present": "odrl:present", + "print": "odrl:print", + "read": "odrl:read", + "reproduce": "odrl:reproduce", + "sell": "odrl:sell", + "stream": "odrl:stream", + "textToSpeech": "odrl:textToSpeech", + "transfer": "odrl:transfer", + "transform": "odrl:transform", + "translate": "odrl:translate", + + "Duty": "odrl:Duty", + "duty": {"@type": "@id", "@id": "odrl:duty"}, + "consequence": {"@type": "@id", "@id": "odrl:consequence"}, + "remedy": {"@type": "@id", "@id": "odrl:remedy"}, + + "acceptTracking": "odrl:acceptTracking", + "attribute": "odrl:attribute", + "compensate": "odrl:compensate", + "delete": "odrl:delete", + "ensureExclusivity": "odrl:ensureExclusivity", + "include": "odrl:include", + "inform": "odrl:inform", + "nextPolicy": "odrl:nextPolicy", + "obtainConsent": "odrl:obtainConsent", + "reviewPolicy": "odrl:reviewPolicy", + "uninstall": "odrl:uninstall", + "watermark": "odrl:watermark", + + "Constraint": "odrl:Constraint", + "LogicalConstraint": "odrl:LogicalConstraint", + "constraint": {"@type": "@id", "@id": "odrl:constraint"}, + "refinement": {"@type": "@id", "@id": "odrl:refinement"}, + "Operator": "odrl:Operator", + "operator": {"@type": "@vocab", "@id": "odrl:operator"}, + "RightOperand": "odrl:RightOperand", + "rightOperand": "odrl:rightOperand", + "rightOperandReference":{"@type": "xsd:anyURI", "@id": "odrl:rightOperandReference"}, + "LeftOperand": "odrl:LeftOperand", + "leftOperand": {"@type": "@vocab", "@id": "odrl:leftOperand"}, + "unit": "odrl:unit", + "dataType": {"@type": "xsd:anyType", "@id": "odrl:datatype"}, + "status": "odrl:status", + + "absolutePosition": "odrl:absolutePosition", + "absoluteSpatialPosition": "odrl:absoluteSpatialPosition", + "absoluteTemporalPosition":"odrl:absoluteTemporalPosition", + "absoluteSize": "odrl:absoluteSize", + "count": "odrl:count", + "dateTime": "odrl:dateTime", + "delayPeriod": "odrl:delayPeriod", + "deliveryChannel": "odrl:deliveryChannel", + "elapsedTime": "odrl:elapsedTime", + "event": "odrl:event", + "fileFormat": "odrl:fileFormat", + "industry": "odrl:industry:", + "language": "odrl:language", + "media": "odrl:media", + "meteredTime": "odrl:meteredTime", + "payAmount": "odrl:payAmount", + "percentage": "odrl:percentage", + "product": "odrl:product", + "purpose": "odrl:purpose", + "recipient": "odrl:recipient", + "relativePosition": "odrl:relativePosition", + "relativeSpatialPosition": "odrl:relativeSpatialPosition", + "relativeTemporalPosition":"odrl:relativeTemporalPosition", + "relativeSize": "odrl:relativeSize", + "resolution": "odrl:resolution", + "spatial": "odrl:spatial", + "spatialCoordinates": "odrl:spatialCoordinates", + "systemDevice": "odrl:systemDevice", + "timeInterval": "odrl:timeInterval", + "unitOfCount": "odrl:unitOfCount", + "version": "odrl:version", + "virtualLocation": "odrl:virtualLocation", + + "eq": "odrl:eq", + "gt": "odrl:gt", + "gteq": "odrl:gteq", + "lt": "odrl:lt", + "lteq": "odrl:lteq", + "neq": "odrl:neg", + "isA": "odrl:isA", + "hasPart": "odrl:hasPart", + "isPartOf": "odrl:isPartOf", + "isAllOf": "odrl:isAllOf", + "isAnyOf": "odrl:isAnyOf", + "isNoneOf": "odrl:isNoneOf", + "or": "odrl:or", + "xone": "odrl:xone", + "and": "odrl:and", + "andSequence": "odrl:andSequence", + + "policyUsage": "odrl:policyUsage" + + } +} diff --git a/core/identity-hub-core/src/test/resources/presentation-exchange.v1.json b/core/identity-hub-core/src/test/resources/presentation-exchange.v1.json new file mode 100644 index 000000000..488925037 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/presentation-exchange.v1.json @@ -0,0 +1,15 @@ +{ + "@context": { + "@version": 1.1, + "PresentationSubmission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@context": { + "@version": 1.1, + "presentation_submission": { + "@id": "https://identity.foundation/presentation-exchange/#presentation-submission", + "@type": "@json" + } + } + } + } +} \ No newline at end of file diff --git a/core/identity-hub-core/src/test/resources/presentation-query.v08.json b/core/identity-hub-core/src/test/resources/presentation-query.v08.json new file mode 100644 index 000000000..3f5e46ab2 --- /dev/null +++ b/core/identity-hub-core/src/test/resources/presentation-query.v08.json @@ -0,0 +1,116 @@ +{ + "@context": { + "@version": 1.1, + "@protected": false, + "iatp": "https://w3id.org/tractusx-trust/v0.8/", + "cred": "https://www.w3.org/2018/credentials/", + "xsd": "http://www.w3.org/2001/XMLSchema/", + "CredentialContainer": { + "@id": "iatp:CredentialContainer", + "@context": { + "payload": { + "@id": "iatp:payload", + "@type": "xsd:string" + } + } + }, + "CredentialMessage": { + "@id": "iatp:CredentialMessage", + "@context": { + "credentials": "iatp:credentials" + } + }, + "CredentialObject": { + "@id": "iatp:CredentialObject", + "@context": { + "credentialType": { + "@id": "iatp:credentialType", + "@container": "@set" + }, + "format": "iatp:format", + "offerReason": { + "@id": "iatp:offerReason", + "@type": "xsd:string" + }, + "bindingMethods": { + "@id": "iatp:bindingMethods", + "@type": "xsd:string", + "@container": "@set" + }, + "cryptographicSuites": { + "@id": "iatp:cryptographicSuites", + "@type": "xsd:string", + "@container": "@set" + }, + "issuancePolicy": "iatp:issuancePolicy" + } + }, + "CredentialOfferMessage": { + "@id": "iatp:CredentialOfferMessage", + "@context": { + "credentialIssuer": "cred:issuer", + "credentials": "iatp:credentials" + } + }, + "CredentialRequestMessage": { + "@id": "iatp:CredentialRequestMessage", + "@context": { + "format": "iatp:format", + "type": "@type" + } + }, + "CredentialService": "iatp:CredentialService", + "CredentialStatus": { + "@id": "iatp:CredentialStatus", + "@context": { + "requestId": { + "@id": "iatp:requestId", + "@type": "@id" + }, + "status": { + "@id": "iatp:status", + "@type": "xsd:string" + } + } + }, + "IssuerMetadata": { + "@id": "iatp:IssuerMetadata", + "@context": { + "credentialIssuer": "cred:issuer", + "credentialsSupported": { + "@id": "iatp:credentialsSupported", + "@container": "@set" + } + } + }, + "PresentationQueryMessage": { + "@id": "iatp:PresentationQueryMessage", + "@context": { + "presentationDefinition": "iatp:presentationDefinition", + "scope": "iatp:scope" + } + }, + "credentials": { + "@id": "iatp:credentials", + "@container": "@set" + }, + "credentialSubject": { + "@id": "iatp:credentialSubject", + "@type": "cred:credentialSubject" + }, + "format": { + "@id": "iatp:format", + "@type": "xsd:string" + }, + "presentationDefinition": { + "@id": "iatp:presentationDefinition", + "@type": "@json" + }, + "scope": { + "@id": "iatp:scope", + "@type": "xsd:string", + "@container": "@set" + }, + "type": "@type" + } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6d639cb73..bfdb1e6fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -19,6 +19,7 @@ testcontainers = "1.19.1" [libraries] edc-util = { module = "org.eclipse.edc:util", version.ref = "edc" } edc-junit = { module = "org.eclipse.edc:junit", version.ref = "edc" } +edc-jws2020 = { module = "org.eclipse.edc:jws2020", version.ref = "edc" } edc-boot = { module = "org.eclipse.edc:boot", version.ref = "edc" } edc-spi-core = { module = "org.eclipse.edc:core-spi", version.ref = "edc" } edc-spi-http = { module = "org.eclipse.edc:http-spi", version.ref = "edc" } @@ -47,6 +48,9 @@ edc-identity-did-crypto = { module = "org.eclipse.edc:identity-did-crypto", vers edc-identity-did-core = { module = "org.eclipse.edc:identity-did-core", version.ref = "edc" } edc-identity-did-web = { module = "org.eclipse.edc:identity-did-web", version.ref = "edc" } edc-iatp-service = { module = "org.eclipse.edc:identity-trust-service", version.ref = "edc" } +edc-iatp-core = { module = "org.eclipse.edc:identity-trust-core", version.ref = "edc" } +edc-vc-jwt = { module = "org.eclipse.edc:jwt-verifiable-credentials", version.ref = "edc" } +edc-vc-ldp = { module = "org.eclipse.edc:ldp-verifiable-credentials", version.ref = "edc" } edc-ext-http = { module = "org.eclipse.edc:http", version.ref = "edc" } edc-ext-jsonld = { module = "org.eclipse.edc:json-ld", version.ref = "edc" } edc-ext-observability = { module = "org.eclipse.edc:api-observability", version.ref = "edc" } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreator.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreator.java new file mode 100644 index 000000000..9f504731e --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreator.java @@ -0,0 +1,65 @@ +/* + * 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.spi.generator; + +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; + +import java.util.List; +import java.util.Map; + +/** + * Generator interface for creating VerifiablePresentation objects out of {@link VerifiableCredentialContainer}s + * + * @param the type of the presentation + */ +@FunctionalInterface +public interface PresentationCreator { + /** + * Generates a Verifiable Presentation based on a list of Verifiable Credential Containers and a key ID. Implementors must + * use the key ID to resolve the private key used for signing. Recipients of the VP must use the key ID to resolve the public + * key for verification. How the public key is made available is out-of-scope here, but a popular method is DID documents. + *

+ * Implementors must check whether all VCs can be represented in one one VP, and if not, must throw a {@link IllegalArgumentException}. + *

+ * 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. + * @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 createPresentation(List credentials, String keyId); + + /** + * Generates a Verifiable Presentation based on a list of Verifiable Credential Containers and a key ID. Implementors must + * use the key ID to resolve the private key used for signing. Recipients of the VP must use the key ID to resolve the public + * key for verification. How the public key is made available is out-of-scope here, but a popular method is DID documents. + *

+ * Implementors must check whether all VCs can be represented in one one VP, and if not, must throw a {@link IllegalArgumentException}. + *

+ * 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. + * @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 createPresentation(List credentials, String keyId, Map additionalData) { + return createPresentation(credentials, keyId); + } +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java new file mode 100644 index 000000000..015217eef --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/generator/PresentationCreatorRegistry.java @@ -0,0 +1,54 @@ +/* + * 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.spi.generator; + +import org.eclipse.edc.identitytrust.model.CredentialFormat; +import org.eclipse.edc.identitytrust.model.VerifiableCredentialContainer; + +import java.util.List; + +/** + * Registry that contains multiple {@link PresentationCreator} objects and assigns them a {@link CredentialFormat}. + * With this, it is possible to generate VerifiablePresentations in different formats. + */ +public interface PresentationCreatorRegistry { + + /** + * Registers a {@link PresentationCreator} for a particular {@link CredentialFormat} + */ + void addCreator(PresentationCreator creator, CredentialFormat format); + + /** + * Creates a VerifiablePresentation based on a list of verifiable credentials and a credential format. How the presentation will be represented + * depends on the format. JWT-VPs will be represented as {@link String}, LDP-VPs will be represented as {@link jakarta.json.JsonObject}. + * + * @param credentials The list of verifiable credentials to include in the presentation. + * @param format The format for the presentation. + * @param The type of the presentation. Can be {@link String}, when format is {@link CredentialFormat#JWT}, or {@link jakarta.json.JsonObject}, + * when the format is {@link CredentialFormat#JSON_LD} + * @return The created presentation. + * @throws IllegalArgumentException if the credential cannot be represented in the desired format. For example, LDP-VPs cannot contain JWT-VCs. + * @throws org.eclipse.edc.spi.EdcException if no creator is registered for a particular format + */ + T createPresentation(List credentials, CredentialFormat format); + + /** + * Specify, which key ID is to be used for which {@link CredentialFormat}. It is recommended to use a separate key for every format. + * + * @param keyId the Key ID of the private key. Typically, the related public key has to be resolvable through a public method, e.g. DID:WEB + * @param format the {@link CredentialFormat} for which the key should be used. + */ + void addKeyId(String keyId, CredentialFormat format); +} 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 b2cfc64d4..175b974c5 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 @@ -26,6 +26,14 @@ * Represents a Presentation Generator that creates a presentation based on a list of verifiable credentials * and an optional presentation definition. */ +@FunctionalInterface public interface PresentationGenerator { + /** + * Creates a presentation based on a list of verifiable credentials and an optional presentation definition. + * + * @param credentials The list of verifiable credentials to include in the presentation. + * @param presentationDefinition The optional presentation definition. + * @return A Result object containing a PresentationResponse if the presentation creation is successful, or a failure message if it fails. + */ Result createPresentation(List credentials, @Nullable PresentationDefinition presentationDefinition); } diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/IdentityHubConstants.java b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/IdentityHubConstants.java index e9bcb1def..bdf928f40 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/IdentityHubConstants.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/edc/identityhub/spi/model/IdentityHubConstants.java @@ -15,8 +15,14 @@ package org.eclipse.edc.identityhub.spi.model; public interface IdentityHubConstants { - String IATP_PREFIX = "https://w3id.org/tractusx-trust/v0.8/"; String IATP_CONTEXT_URL = "https://w3id.org/tractusx-trust/v0.8"; + String IATP_PREFIX = IATP_CONTEXT_URL + "/"; String PRESENTATION_EXCHANGE_URL = "https://identity.foundation/presentation-exchange/submission/v1"; - + String W3C_CREDENTIALS_URL = "https://www.w3.org/2018/credentials/v1"; + String VERIFIABLE_PRESENTATION_TYPE = "VerifiablePresentation"; + String JWS_2020_URL = "https://w3id.org/security/suites/jws-2020/v1"; + String DID_CONTEXT_URL = "https://www.w3.org/ns/did/v1"; + String PRESENTATION_SUBMISSION_URL = "https://identity.foundation/presentation-exchange/submission/v1/"; + String JWS_2020_SIGNATURE_SUITE = "JsonWebSignature2020"; + String ED25519_SIGNATURE_SUITE = "Ed25519Signature2020"; // not used right now }