From ac56c5a2346302c58c60557c2fdfbaff22ed707e Mon Sep 17 00:00:00 2001 From: Marc Gomez Date: Wed, 27 Jul 2022 16:10:58 +0200 Subject: [PATCH 1/8] Identity Hub CLI --- .github/workflows/verify.yaml | 21 ++- CHANGELOG.md | 1 + client-cli/README.md | 35 ++++ client-cli/build.gradle.kts | 53 ++++++ .../cli/AddVerifiableCredentialCommand.java | 79 ++++++++ .../identityhub/cli/CliException.java | 30 ++++ .../identityhub/cli/IdentityHubCli.java | 63 +++++++ .../cli/ListVerifiableCredentialsCommand.java | 62 +++++++ .../cli/VerifiableCredentialsCommand.java | 29 +++ .../identityhub/cli/TestUtils.java | 83 +++++++++ .../cli/VerifiableCredentialsCommandTest.java | 169 ++++++++++++++++++ .../src/test/resources/test-private-key.pem | 5 + .../src/test/resources/test-public-key.pem | 4 + .../FeatureDetectionReadProcessor.java | 4 +- gradle.properties | 2 + .../identity-hub-client/build.gradle.kts | 1 + .../identityhub/client/IdentityHubClient.java | 6 +- settings.gradle.kts | 3 + spi/identity-hub-spi/build.gradle.kts | 1 + .../identityhub/credentials/CryptoUtils.java | 44 +++++ .../VerifiableCredentialsJwtService.java | 21 ++- .../VerifiableCredentialsJwtServiceImpl.java | 23 ++- system-tests/README.md | 44 +++++ system-tests/launcher/Dockerfile | 22 +++ system-tests/launcher/build.gradle.kts | 38 ++++ system-tests/tests/build.gradle.kts | 51 ++++++ system-tests/tests/docker-compose.yml | 21 +++ .../resources/jwt/authority/private-key.pem | 5 + .../resources/jwt/authority/public-key.pem | 4 + .../resources/jwt/participant/private-key.pem | 5 + .../resources/jwt/participant/public-key.pem | 4 + .../tests/resources/webdid/authority/did.json | 25 +++ .../resources/webdid/participant/did.json | 29 +++ .../systemtests/IntegrationTest.java | 28 +++ .../VerifiableCredentialsIntegrationTest.java | 100 +++++++++++ 35 files changed, 1105 insertions(+), 10 deletions(-) create mode 100644 client-cli/README.md create mode 100644 client-cli/build.gradle.kts create mode 100644 client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/AddVerifiableCredentialCommand.java create mode 100644 client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/CliException.java create mode 100644 client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/IdentityHubCli.java create mode 100644 client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/ListVerifiableCredentialsCommand.java create mode 100644 client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommand.java create mode 100644 client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java create mode 100644 client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java create mode 100644 client-cli/src/test/resources/test-private-key.pem create mode 100644 client-cli/src/test/resources/test-public-key.pem create mode 100644 spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/CryptoUtils.java create mode 100644 system-tests/README.md create mode 100644 system-tests/launcher/Dockerfile create mode 100644 system-tests/launcher/build.gradle.kts create mode 100644 system-tests/tests/build.gradle.kts create mode 100644 system-tests/tests/docker-compose.yml create mode 100644 system-tests/tests/resources/jwt/authority/private-key.pem create mode 100644 system-tests/tests/resources/jwt/authority/public-key.pem create mode 100644 system-tests/tests/resources/jwt/participant/private-key.pem create mode 100644 system-tests/tests/resources/jwt/participant/public-key.pem create mode 100644 system-tests/tests/resources/webdid/authority/did.json create mode 100644 system-tests/tests/resources/webdid/participant/did.json create mode 100644 system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/IntegrationTest.java create mode 100644 system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java diff --git a/.github/workflows/verify.yaml b/.github/workflows/verify.yaml index ec6f5932c..3be589d20 100644 --- a/.github/workflows/verify.yaml +++ b/.github/workflows/verify.yaml @@ -3,7 +3,6 @@ name: Test Code (Style, Tests) on: push: pull_request: - branches: [ main ] paths-ignore: - '**.md' - 'docs/**' @@ -29,9 +28,23 @@ jobs: - uses: ./.github/actions/gradle-setup - - name: 'Tests' + - name: 'Build system tests launcher' + run: ./gradlew :system-tests:launcher:shadowJar + + - name: 'Upgrade docker-compose (for --wait option)' + run: | + sudo curl -L https://github.com/docker/compose/releases/download/v2.6.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + + - name: 'Run application in docker-compose' + run: docker-compose -f system-tests/tests/docker-compose.yml up --build --wait + timeout-minutes: 10 + + - name: 'Unit and system tests' run: ./gradlew test timeout-minutes: 10 + env: + INTEGRATION_TEST: true - name: 'Publish Test Results' uses: EnricoMi/publish-unit-test-result-action@v1 @@ -39,6 +52,10 @@ jobs: with: files: "**/test-results/**/*.xml" + - name: 'docker-compose logs' + run: docker-compose -f system-tests/tests/docker-compose.yml logs + if: always() + Checkstyle: runs-on: ubuntu-latest steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ab9c22ce..3618f7f6d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ in the detailed section referring to by linking pull requests or issues. - Identity Hub client (#4) - Maven artefact publication (#21) - CredentialsVerifier implementation (#24) +- CLI (#25) #### Changed diff --git a/client-cli/README.md b/client-cli/README.md new file mode 100644 index 000000000..0c8633b84 --- /dev/null +++ b/client-cli/README.md @@ -0,0 +1,35 @@ +# Command-line client + +The client is a Java JAR that provides access to an identity hub service via REST. + +## Running the client + +To run the command line client, and list available options and commands: + +```bash +cd IdentityHub +./gradlew build +java -jar client-cli/build/libs/identity-hub-cli.jar --help +``` + +For example, to get verifiable credentials: + +``` +java -jar client-cli/build/libs/identity-hub-cli.jar \ + -s=http://localhost:8181/api \ + vc list +``` + +The client can also be run from a local Maven repository: + +``` +cd IdentityHub +./gradlew publishToMavenLocal +``` + +``` +cd OtherDirectory +mvn dependency:copy -Dartifact=org.eclipse.dataspaceconnector.identityhub:identity-hub-cli:0.0.1-SNAPSHOT:jar:all -DoutputDirectory=. +java -jar identity-hub-cli-0.0.1-SNAPSHOT-all.jar --help +``` + diff --git a/client-cli/build.gradle.kts b/client-cli/build.gradle.kts new file mode 100644 index 000000000..2cfb36d20 --- /dev/null +++ b/client-cli/build.gradle.kts @@ -0,0 +1,53 @@ +plugins { + `java-library` + id("application") + id("com.github.johnrengelman.shadow") version "7.0.0" + `maven-publish` +} + +val edcGroup: String by project +val edcVersion: String by project +val jacksonVersion: String by project +val jupiterVersion: String by project +val assertj: String by project +val mockitoVersion: String by project +val faker: String by project +val okHttpVersion: String by project +val nimbusVersion: String by project +val bouncycastleVersion: String by project +val picoCliVersion: String by project + +dependencies { + api("info.picocli:picocli:${picoCliVersion}") + annotationProcessor("info.picocli:picocli-codegen:${picoCliVersion}") + + implementation(project(":identity-hub-core:identity-hub-client")) + implementation("${edcGroup}:identity-did-spi:${edcVersion}") + implementation("com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}") + implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") + implementation("com.nimbusds:nimbus-jose-jwt:${nimbusVersion}") + implementation("org.bouncycastle:bcpkix-jdk15on:${bouncycastleVersion}") + testImplementation("org.assertj:assertj-core:${assertj}") + testImplementation("org.mockito:mockito-core:${mockitoVersion}") + testImplementation("com.github.javafaker:javafaker:${faker}") + testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}") +} + +application { + mainClass.set("org.eclipse.dataspaceconnector.identityhub.cli.IdentityHubCli") +} + +tasks.withType { + mergeServiceFiles() + archiveFileName.set("identity-hub-cli.jar") +} + +publishing { + publications { + create("identity-hub-cli") { + artifactId = "identity-hub-cli" + from(components["java"]) + } + } +} diff --git a/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/AddVerifiableCredentialCommand.java b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/AddVerifiableCredentialCommand.java new file mode 100644 index 000000000..53f2e8bb6 --- /dev/null +++ b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/AddVerifiableCredentialCommand.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential; +import picocli.CommandLine; +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +import java.util.concurrent.Callable; + +import static org.eclipse.dataspaceconnector.identityhub.credentials.CryptoUtils.readPrivateEcKey; + + +@Command(name = "add", description = "Adds a verifiable credential to identity hub") +class AddVerifiableCredentialCommand implements Callable { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @ParentCommand + private VerifiableCredentialsCommand command; + + @CommandLine.Spec + private CommandLine.Model.CommandSpec spec; + + @CommandLine.Option(names = { "-c", "--verifiable-credential" }, required = true, description = "Verifiable Credential as JSON") + private String verifiableCredentialJson; + + @CommandLine.Option(names = { "-i", "--issuer" }, required = true, description = "DID of the Verifiable Credential issuer") + private String issuer; + + @CommandLine.Option(names = { "-b", "--subject" }, required = true, description = "DID of the Verifiable Credential subject") + private String subject; + + @CommandLine.Option(names = { "-k", "--private-key" }, required = true, description = "PEM file with EC private key for signing Verifiable Credentials") + private String privateKeyPemFile; + + @Override + public Integer call() throws Exception { + var out = spec.commandLine().getOut(); + + VerifiableCredential vc; + try { + vc = MAPPER.readValue(verifiableCredentialJson, VerifiableCredential.class); + } catch (JsonProcessingException e) { + throw new CliException("Error while processing request json."); + } + + SignedJWT signedJwt; + try { + var privateKey = readPrivateEcKey(privateKeyPemFile); + signedJwt = command.cli.verifiableCredentialsJwtService.buildSignedJwt(vc, issuer, subject, privateKey); + } catch (Exception e) { + throw new CliException("Error while signing Verifiable Credential", e); + } + + command.cli.identityHubClient.addVerifiableCredential(command.cli.hubUrl, signedJwt); + + out.println("Verifiable Credential added successfully"); + + return 0; + } + +} diff --git a/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/CliException.java b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/CliException.java new file mode 100644 index 000000000..e3d05a5e1 --- /dev/null +++ b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/CliException.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +/** + * Base exception for the CLI client. + * The client should use unchecked exceptions when appropriate (e.g., non-recoverable errors) and may extend this exception. + */ +public class CliException extends RuntimeException { + public CliException(String message) { + super(message); + } + + public CliException(String message, Throwable cause) { + super(message, cause); + } + +} diff --git a/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/IdentityHubCli.java b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/IdentityHubCli.java new file mode 100644 index 000000000..5201fd9e1 --- /dev/null +++ b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/IdentityHubCli.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.OkHttpClient; +import org.eclipse.dataspaceconnector.identityhub.client.IdentityHubClient; +import org.eclipse.dataspaceconnector.identityhub.client.IdentityHubClientImpl; +import org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService; +import org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtServiceImpl; +import org.eclipse.dataspaceconnector.spi.monitor.ConsoleMonitor; +import picocli.CommandLine; +import picocli.CommandLine.Command; + +@Command(name = "identity-hub-cli", mixinStandardHelpOptions = true, + description = "Client utility for MVD identity hub.", + subcommands = { + VerifiableCredentialsCommand.class + }) +public class IdentityHubCli { + @CommandLine.Option(names = { "-s", "--identity-hub-url" }, required = true, description = "Identity Hub URL", defaultValue = "http://localhost:8181/api/identity-hub") + String hubUrl; + + IdentityHubClient identityHubClient; + + VerifiableCredentialsJwtService verifiableCredentialsJwtService; + + public static void main(String... args) { + CommandLine commandLine = getCommandLine(); + var exitCode = commandLine.execute(args); + System.exit(exitCode); + } + + public static CommandLine getCommandLine() { + var command = new IdentityHubCli(); + return new CommandLine(command).setExecutionStrategy(command::executionStrategy); + } + + private int executionStrategy(CommandLine.ParseResult parseResult) { + init(); // custom initialization to be done before executing any command or subcommand + return new CommandLine.RunLast().execute(parseResult); + } + + private void init() { + var okHttpClient = new OkHttpClient.Builder().build(); + var objectMapper = new ObjectMapper(); + var monitor = new ConsoleMonitor(); + this.identityHubClient = new IdentityHubClientImpl(okHttpClient, objectMapper, monitor); + this.verifiableCredentialsJwtService = new VerifiableCredentialsJwtServiceImpl(objectMapper); + } +} diff --git a/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/ListVerifiableCredentialsCommand.java b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/ListVerifiableCredentialsCommand.java new file mode 100644 index 000000000..8907de458 --- /dev/null +++ b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/ListVerifiableCredentialsCommand.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.nimbusds.jwt.SignedJWT; +import picocli.CommandLine.Command; +import picocli.CommandLine.Model.CommandSpec; +import picocli.CommandLine.ParentCommand; +import picocli.CommandLine.Spec; + +import java.text.ParseException; +import java.util.Map; +import java.util.concurrent.Callable; + +import static java.util.stream.Collectors.toList; + +@Command(name = "list", description = "Lists verifiable credentials") +class ListVerifiableCredentialsCommand implements Callable { + + private static final ObjectMapper MAPPER = new ObjectMapper() + .enable(SerializationFeature.INDENT_OUTPUT); + + @ParentCommand + private VerifiableCredentialsCommand command; + + @Spec + private CommandSpec spec; + + @Override + public Integer call() throws Exception { + var out = spec.commandLine().getOut(); + var result = command.cli.identityHubClient.getVerifiableCredentials(command.cli.hubUrl); + var vcs = result.getContent().stream() + .map(this::getClaims) + .collect(toList()); + MAPPER.writeValue(out, vcs); + out.println(); + return 0; + } + + private Map getClaims(SignedJWT jwt) { + try { + return jwt.getJWTClaimsSet().getClaims(); + } catch (ParseException e) { + throw new CliException("Error while reading Verifiable Credentials claims", e); + } + } +} diff --git a/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommand.java b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommand.java new file mode 100644 index 000000000..1aa86d11b --- /dev/null +++ b/client-cli/src/main/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommand.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import picocli.CommandLine.Command; +import picocli.CommandLine.ParentCommand; + +@Command(name = "vc", mixinStandardHelpOptions = true, + description = "Manage verifiable credentials.", + subcommands = { + ListVerifiableCredentialsCommand.class, + AddVerifiableCredentialCommand.class + }) +class VerifiableCredentialsCommand { + @ParentCommand + IdentityHubCli cli; +} diff --git a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java new file mode 100644 index 000000000..ab3bd4e6e --- /dev/null +++ b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.javafaker.Faker; +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.dataspaceconnector.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.dataspaceconnector.iam.did.spi.key.PublicKeyWrapper; +import org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService; +import org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtServiceImpl; +import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential; + +import java.util.Map; + +import static org.eclipse.dataspaceconnector.identityhub.credentials.CryptoUtils.readPrivateEcKey; +import static org.eclipse.dataspaceconnector.identityhub.credentials.CryptoUtils.readPublicEcKey; + + +public class TestUtils { + static final Faker FAKER = new Faker(); + public static final String PUBLIC_KEY_PATH = "src/test/resources/test-public-key.pem"; + public static final String PRIVATE_KEY_PATH = "src/test/resources/test-private-key.pem"; + public static final PublicKeyWrapper PUBLIC_KEY; + public static final PrivateKeyWrapper PRIVATE_KEY; + private static final VerifiableCredentialsJwtService VC_JWT_SERVICE = new VerifiableCredentialsJwtServiceImpl(new ObjectMapper()); + + static { + try { + PUBLIC_KEY = readPublicEcKey(PUBLIC_KEY_PATH); + PRIVATE_KEY = readPrivateEcKey(PRIVATE_KEY_PATH); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private TestUtils() { + } + + public static VerifiableCredential createVerifiableCredential() { + return VerifiableCredential.Builder.newInstance() + .id(FAKER.internet().uuid()) + .credentialSubject(Map.of( + FAKER.internet().uuid(), FAKER.lorem().word(), + FAKER.internet().uuid(), FAKER.lorem().word())) + .build(); + } + + public static SignedJWT signVerifiableCredential(VerifiableCredential vc) { + try { + + return VC_JWT_SERVICE.buildSignedJwt( + vc, + "identity-hub-test-issuer", + "identity-hub-test-subject", + PRIVATE_KEY); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public static boolean verifyVerifiableCredentialSignature(SignedJWT jwt) { + try { + return jwt.verify(PUBLIC_KEY.verifier()); + } catch (JOSEException e) { + throw new RuntimeException(e); + } + } + +} \ No newline at end of file diff --git a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java new file mode 100644 index 000000000..ada0560f9 --- /dev/null +++ b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java @@ -0,0 +1,169 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.cli; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.javafaker.Faker; +import com.nimbusds.jwt.SignedJWT; +import org.eclipse.dataspaceconnector.identityhub.client.IdentityHubClient; +import org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtServiceImpl; +import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import picocli.CommandLine; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.PRIVATE_KEY_PATH; +import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.createVerifiableCredential; +import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.signVerifiableCredential; +import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.verifyVerifiableCredentialSignature; +import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY; +import static org.eclipse.dataspaceconnector.spi.response.StatusResult.success; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class VerifiableCredentialsCommandTest { + + static final Faker FAKER = new Faker(); + static final ObjectMapper MAPPER = new ObjectMapper(); + + static final VerifiableCredential VC1 = createVerifiableCredential(); + static final SignedJWT SIGNED_VC1 = signVerifiableCredential(VC1); + + static final VerifiableCredential VC2 = createVerifiableCredential(); + static final SignedJWT SIGNED_VC2 = signVerifiableCredential(VC2); + + String hubUrl = FAKER.internet().url(); + + IdentityHubCli app = new IdentityHubCli(); + CommandLine cmd = new CommandLine(app); + StringWriter out = new StringWriter(); + StringWriter err = new StringWriter(); + + @BeforeEach + void setUp() { + app.identityHubClient = mock(IdentityHubClient.class); + app.verifiableCredentialsJwtService = new VerifiableCredentialsJwtServiceImpl(new ObjectMapper()); + app.hubUrl = hubUrl; + cmd.setOut(new PrintWriter(out)); + cmd.setErr(new PrintWriter(err)); + } + + @Test + void list() throws Exception { + // arrange + when(app.identityHubClient.getVerifiableCredentials(app.hubUrl)).thenReturn(success(List.of(SIGNED_VC1, SIGNED_VC2))); + + // act + var exitCode = executeList(); + var outContent = out.toString(); + var errContent = err.toString(); + + // assert + assertThat(exitCode).isEqualTo(0); + assertThat(errContent).isEmpty(); + + var claims = MAPPER.readValue(outContent, new TypeReference>>() {}); + var vcs = claims.stream() + .map(c -> MAPPER.convertValue(c.get(VERIFIABLE_CREDENTIALS_KEY), VerifiableCredential.class)) + .collect(Collectors.toList()); + + assertThat(vcs) + .usingRecursiveFieldByFieldElementComparator() + .isEqualTo(List.of(VC1, VC2)); + } + + @Test + void add() throws Exception { + // arrange + var json = MAPPER.writeValueAsString(VC1); + var vcArgCaptor = ArgumentCaptor.forClass(SignedJWT.class); + doReturn(success()).when(app.identityHubClient).addVerifiableCredential(eq(app.hubUrl), vcArgCaptor.capture()); + + // act + var exitCode = executeAdd(json, PRIVATE_KEY_PATH); + var outContent = out.toString(); + var errContent = err.toString(); + + // assert + assertThat(exitCode).isEqualTo(0); + assertThat(outContent).isEqualTo("Verifiable Credential added successfully\n"); + assertThat(errContent).isEmpty(); + + verify(app.identityHubClient).addVerifiableCredential(eq(app.hubUrl), isA(SignedJWT.class)); + var signedJwt = vcArgCaptor.getValue(); + + // assert JWT signature + assertThat(verifyVerifiableCredentialSignature(signedJwt)).isTrue(); + + // verify verifiable credential claim + var vcClaim = signedJwt.getJWTClaimsSet().getJSONObjectClaim(VERIFIABLE_CREDENTIALS_KEY).toJSONString(); + var verifiableCredential = MAPPER.readValue(vcClaim, VerifiableCredential.class); + assertThat(verifiableCredential).usingRecursiveComparison().isEqualTo(VC1); + } + + @Test + void add_invalidJson_fails() { + // arrange + var json = "Invalid json"; + + // act + var exitCode = executeAdd(json, PRIVATE_KEY_PATH); + var outContent = out.toString(); + var errContent = err.toString(); + + // assert + assertThat(exitCode).isNotEqualTo(0); + assertThat(outContent).isEmpty(); + assertThat(errContent).contains("Error while processing request json"); + } + + @Test + void add_invalidPrivateKey_fails() throws JsonProcessingException { + // arrange + var json = MAPPER.writeValueAsString(VC1); + + // act + var exitCode = executeAdd(json, "non-existing-key"); + var outContent = out.toString(); + var errContent = err.toString(); + + // assert + assertThat(exitCode).isNotEqualTo(0); + assertThat(outContent).isEmpty(); + assertThat(errContent).contains("Error while signing Verifiable Credential"); + } + + private int executeList() { + return cmd.execute("-s", hubUrl, "vc", "list"); + } + + private int executeAdd(String json, String privateKey) { + return cmd.execute("-s", hubUrl, "vc", "add", "-c", json, "-i", "identity-hub-test-issuer", "-b", "identity-hub-test-subject", "-k", privateKey); + } +} \ No newline at end of file diff --git a/client-cli/src/test/resources/test-private-key.pem b/client-cli/src/test/resources/test-private-key.pem new file mode 100644 index 000000000..d042b43cb --- /dev/null +++ b/client-cli/src/test/resources/test-private-key.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEINfoZvp8OzraxNeWR5YtDOXXp9s55joSacPLpuC+g8u4oAoGCCqGSM49 +AwEHoUQDQgAEMgmADLs+uEkcS70toCTY+TDhBfmDtMtjQ2F6feomOSSVUEIrns/5 +2a1VUKoJSxzMx6SeFgxrf3w3NvDtpiVzsQ== +-----END EC PRIVATE KEY----- diff --git a/client-cli/src/test/resources/test-public-key.pem b/client-cli/src/test/resources/test-public-key.pem new file mode 100644 index 000000000..eb4fa54ca --- /dev/null +++ b/client-cli/src/test/resources/test-public-key.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMgmADLs+uEkcS70toCTY+TDhBfmD +tMtjQ2F6feomOSSVUEIrns/52a1VUKoJSxzMx6SeFgxrf3w3NvDtpiVzsQ== +-----END PUBLIC KEY----- diff --git a/extensions/identity-hub/src/main/java/org/eclipse/dataspaceconnector/identityhub/processor/FeatureDetectionReadProcessor.java b/extensions/identity-hub/src/main/java/org/eclipse/dataspaceconnector/identityhub/processor/FeatureDetectionReadProcessor.java index 31163d0e6..09c7b2c0b 100644 --- a/extensions/identity-hub/src/main/java/org/eclipse/dataspaceconnector/identityhub/processor/FeatureDetectionReadProcessor.java +++ b/extensions/identity-hub/src/main/java/org/eclipse/dataspaceconnector/identityhub/processor/FeatureDetectionReadProcessor.java @@ -39,8 +39,8 @@ public MessageResponseObject process(byte[] data) { FeatureDetection.Builder.newInstance().interfaces( WebNodeInterfaces.Builder.newInstance() .supportedCollection(COLLECTIONS_QUERY.getName()) - .supportedCollection(COLLECTIONS_WRITE.getName()) - .build() + .supportedCollection(COLLECTIONS_WRITE.getName()) + .build() ).build() )) .build(); diff --git a/gradle.properties b/gradle.properties index 1f3687645..9858ebd78 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,6 +17,8 @@ faker=1.0.2 okHttpVersion=4.9.3 jetBrainsAnnotationsVersion=15.0 nimbusVersion=8.22.1 +bouncycastleVersion=1.70 +picoCliVersion=4.6.3 # information required for publishing artifacts: edcDeveloperId=mspiekermann diff --git a/identity-hub-core/identity-hub-client/build.gradle.kts b/identity-hub-core/identity-hub-client/build.gradle.kts index b807d5ce7..7697044d1 100644 --- a/identity-hub-core/identity-hub-client/build.gradle.kts +++ b/identity-hub-core/identity-hub-client/build.gradle.kts @@ -29,6 +29,7 @@ val nimbusVersion: String by project dependencies { api(project(":spi:identity-hub-spi")) api(project(":identity-hub-core:identity-hub-model")) + api("${edcGroup}:core-spi:${edcVersion}") implementation("${edcGroup}:http:${edcVersion}") implementation("com.squareup.okhttp3:okhttp:${okHttpVersion}") implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") diff --git a/identity-hub-core/identity-hub-client/src/main/java/org/eclipse/dataspaceconnector/identityhub/client/IdentityHubClient.java b/identity-hub-core/identity-hub-client/src/main/java/org/eclipse/dataspaceconnector/identityhub/client/IdentityHubClient.java index 2db1a357a..d984ce9d2 100644 --- a/identity-hub-core/identity-hub-client/src/main/java/org/eclipse/dataspaceconnector/identityhub/client/IdentityHubClient.java +++ b/identity-hub-core/identity-hub-client/src/main/java/org/eclipse/dataspaceconnector/identityhub/client/IdentityHubClient.java @@ -33,18 +33,18 @@ public interface IdentityHubClient { * @param hubBaseUrl Base URL of the IdentityHub instance. * @return status result containing VerifiableCredentials if request successful. * @throws IOException Signaling that an I/O exception has occurred. For example during JSON serialization or when - * reaching out to the Identity Hub server. + * reaching out to the Identity Hub server. */ StatusResult> getVerifiableCredentials(String hubBaseUrl); /** * Write a VerifiableCredential. * - * @param hubBaseUrl Base URL of the IdentityHub instance. + * @param hubBaseUrl Base URL of the IdentityHub instance. * @param verifiableCredential A verifiable credential to be saved. * @return status result. * @throws IOException Signaling that an I/O exception has occurred. For example during JSON serialization or when - * reaching out to the Identity Hub server. + * reaching out to the Identity Hub server. */ StatusResult addVerifiableCredential(String hubBaseUrl, SignedJWT verifiableCredential); diff --git a/settings.gradle.kts b/settings.gradle.kts index 06d3fb1c7..6e305f9e1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,3 +6,6 @@ include(":extensions:identity-hub") include(":identity-hub-core:identity-hub-client") include(":identity-hub-core:identity-hub-model") include(":extensions:identity-hub-verifier") +include(":client-cli") +include(":system-tests:launcher") +include(":system-tests:tests") diff --git a/spi/identity-hub-spi/build.gradle.kts b/spi/identity-hub-spi/build.gradle.kts index a6b87a4aa..3536a5571 100644 --- a/spi/identity-hub-spi/build.gradle.kts +++ b/spi/identity-hub-spi/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion") implementation("com.nimbusds:nimbus-jose-jwt:${nimbusVersion}") implementation("${edcGroup}:identity-did-spi:${edcVersion}") + implementation("${edcGroup}:identity-did-crypto:${edcVersion}") testFixturesImplementation("com.nimbusds:nimbus-jose-jwt:${nimbusVersion}") testFixturesImplementation("com.github.javafaker:javafaker:${faker}") diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/CryptoUtils.java b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/CryptoUtils.java new file mode 100644 index 000000000..27175c20d --- /dev/null +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/CryptoUtils.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2022 Microsoft Corporation + * + * 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: + * Microsoft Corporation - initial API and implementation + * + */ + +package org.eclipse.dataspaceconnector.identityhub.credentials; + +import com.nimbusds.jose.JOSEException; +import com.nimbusds.jose.jwk.ECKey; +import org.eclipse.dataspaceconnector.iam.did.crypto.key.EcPrivateKeyWrapper; +import org.eclipse.dataspaceconnector.iam.did.crypto.key.EcPublicKeyWrapper; +import org.eclipse.dataspaceconnector.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.dataspaceconnector.iam.did.spi.key.PublicKeyWrapper; + +import java.io.IOException; +import java.nio.file.Path; + +import static java.nio.file.Files.readString; + +public class CryptoUtils { + + public static PublicKeyWrapper readPublicEcKey(String file) throws IOException, JOSEException { + return new EcPublicKeyWrapper(readEcKeyPemFile(file)); + } + + public static PrivateKeyWrapper readPrivateEcKey(String file) throws IOException, JOSEException { + return new EcPrivateKeyWrapper(readEcKeyPemFile(file)); + } + + private static ECKey readEcKeyPemFile(String file) throws IOException, JOSEException { + var contents = readString(Path.of(file)); + var jwk = (ECKey) ECKey.parseFromPEMEncodedObjects(contents); + return jwk; + } +} diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java index 762bcf161..bd8f9d90a 100644 --- a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java +++ b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java @@ -15,17 +15,21 @@ package org.eclipse.dataspaceconnector.identityhub.credentials; import com.nimbusds.jwt.SignedJWT; +import org.eclipse.dataspaceconnector.iam.did.spi.key.PrivateKeyWrapper; +import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential; import org.eclipse.dataspaceconnector.spi.result.Result; import java.util.Map; /** - * Service to manipulate VerifiableCredentials with JWTs. + * Service with operations for manipulation of VerifiableCredentials in JWT format. */ public interface VerifiableCredentialsJwtService { + String VERIFIABLE_CREDENTIALS_KEY = "vc"; + /** - * Extract credentials from a JWT. The credential is represented with the following format + * Extract verifiable credentials from a JWT. The credential is represented with the following format *
{@code
      * "credentialId" : {
      *     "vc": {
@@ -45,4 +49,17 @@ public interface VerifiableCredentialsJwtService {
      * @return VerifiableCredential represented as {@code Map.Entry}.
      */
     Result> extractCredential(SignedJWT jwt);
+
+    /**
+     * Builds a verifiable credential as a signed JWT
+     *
+     * @param credential The verifiable credential to sign
+     * @param issuer     The issuer of the verifiable credential
+     * @param subject    The subject of the verifiable credential
+     * @param privateKey The private key of the issuer, used for signing
+     * @return The Verifiable Credential as a JWT
+     * @throws Exception In case the credential can not be signed
+     */
+    SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, PrivateKeyWrapper privateKey) throws Exception;
+
 }
diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
index 2732728f5..c871bf4f2 100644
--- a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
+++ b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
@@ -15,16 +15,21 @@
 package org.eclipse.dataspaceconnector.identityhub.credentials;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nimbusds.jose.JOSEException;
+import com.nimbusds.jose.JWSAlgorithm;
+import com.nimbusds.jose.JWSHeader;
+import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
+import org.eclipse.dataspaceconnector.iam.did.spi.key.PrivateKeyWrapper;
 import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential;
 import org.eclipse.dataspaceconnector.spi.result.Result;
 
+import java.text.ParseException;
 import java.util.AbstractMap;
 import java.util.Map;
 import java.util.Objects;
 
 public class VerifiableCredentialsJwtServiceImpl implements VerifiableCredentialsJwtService {
-    private static final String VERIFIABLE_CREDENTIALS_KEY = "vc";
     private ObjectMapper objectMapper;
 
     public VerifiableCredentialsJwtServiceImpl(ObjectMapper objectMapper) {
@@ -46,4 +51,20 @@ public Result> extractCredential(SignedJWT jwt) {
             return Result.failure(Objects.requireNonNullElseGet(e.getMessage(), () -> e.toString()));
         }
     }
+
+    @Override
+    public SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, PrivateKeyWrapper privateKey) throws JOSEException, ParseException {
+        var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256).build();
+        var claims = new JWTClaimsSet.Builder()
+                .claim(VERIFIABLE_CREDENTIALS_KEY, credential)
+                .issuer(issuer)
+                .subject(subject)
+                .build();
+
+        var jws = new SignedJWT(jwsHeader, claims);
+
+        jws.sign(privateKey.signer());
+
+        return SignedJWT.parse(jws.serialize());
+    }
 }
diff --git a/system-tests/README.md b/system-tests/README.md
new file mode 100644
index 000000000..ded407b60
--- /dev/null
+++ b/system-tests/README.md
@@ -0,0 +1,44 @@
+## System tests
+
+System tests run a sample EDC connector with the Identity Hub extension and a DID server using docker. The DID server provides sample DID documents for the EDC connector and an external authority.
+
+The test checks that verifiable credentials can be added to the Identity Hub of the EDC connector using the CLI. In addition, an instance of `CredentialsVerifier` is injected (hence the `@ExtendWith(EdcExtension.class)` annotation) to verify that another EDC connector is able to retrieve and verify the signature of these verifiable credentials.
+
+#### Local test resources
+
+The following test resources are used to run system tests:
+
+- A set of private and public keys for both the external authority and the EDC connector (identity hub owner) at `system-tests/resources/jwt/authority`. These keys were generated with the following commands and are commited into the git repository:
+
+    ```bash
+    # generate a private key
+    openssl ecparam -name prime256v1 -genkey -noout -out private-key.pem
+    # generate corresponding public key
+    openssl ec -in private-key.pem -pubout -out public-key.pem
+    ```
+  
+- Web DIDs are available under `system-tests/resources/webdid` folder. The `publicKeyJwk` section of each `did.json` was generated by converting the corresponding public key to JWK format, for example the authority public key was converted to JWK using following command:
+
+    ```bash
+    docker run -i danedmunds/pem-to-jwk:1.2.1 --public --pretty < system-tests/tests/resources/jwt/participant/public-key.pem
+    ```
+
+## Running tests locally 
+
+Build launcher for system tests
+
+```bash
+./gradlew :system-tests:launcher:build
+```
+
+Run test components with:
+
+```bash
+docker-compose -f system-tests/tests/docker-compose.yml up --build
+```
+
+Run test with:
+
+```bash
+INTEGRATION_TEST=true ./gradlew :system-tests:tests:test
+```
\ No newline at end of file
diff --git a/system-tests/launcher/Dockerfile b/system-tests/launcher/Dockerfile
new file mode 100644
index 000000000..7774bd7ab
--- /dev/null
+++ b/system-tests/launcher/Dockerfile
@@ -0,0 +1,22 @@
+FROM openjdk:17-slim-buster
+
+# Optional JVM arguments, such as memory settings
+ARG JVM_ARGS=""
+
+WORKDIR /app
+
+COPY ./build/libs/app.jar /app
+
+EXPOSE 8181
+EXPOSE 9191
+EXPOSE 8282
+
+ENV WEB_HTTP_PORT="8181"
+ENV WEB_HTTP_PATH="/api"
+ENV WEB_HTTP_DATA_PORT="9191"
+ENV WEB_HTTP_DATA_PATH="/api/v1/data"
+ENV WEB_HTTP_IDS_PORT="8282"
+ENV WEB_HTTP_IDS_PATH="/api/v1/ids"
+
+ENV JVM_ARGS=$JVM_ARGS
+ENTRYPOINT [ "sh", "-c", "java $JVM_ARGS -jar app.jar"]
diff --git a/system-tests/launcher/build.gradle.kts b/system-tests/launcher/build.gradle.kts
new file mode 100644
index 000000000..f3bcab5e4
--- /dev/null
+++ b/system-tests/launcher/build.gradle.kts
@@ -0,0 +1,38 @@
+/*
+ *  Copyright (c) 2022 Microsoft Corporation
+ *
+ *  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:
+ *       Microsoft Corporation - initial API and implementation
+ *
+ */
+
+plugins {
+    `java-library`
+    id("application")
+    id("com.github.johnrengelman.shadow") version "7.0.0"
+}
+
+val edcVersion: String by project
+val edcGroup: String by project
+val identityHubVersion: String by project
+val identityHubGroup: String by project
+
+dependencies {
+    implementation("${edcGroup}:core:${edcVersion}")
+    implementation(project(":extensions:identity-hub"))
+}
+
+application {
+    mainClass.set("org.eclipse.dataspaceconnector.boot.system.runtime.BaseRuntime")
+}
+
+tasks.withType {
+    mergeServiceFiles()
+    archiveFileName.set("app.jar")
+}
diff --git a/system-tests/tests/build.gradle.kts b/system-tests/tests/build.gradle.kts
new file mode 100644
index 000000000..870a3f356
--- /dev/null
+++ b/system-tests/tests/build.gradle.kts
@@ -0,0 +1,51 @@
+/*
+ *  Copyright (c) 2022 Microsoft Corporation
+ *
+ *  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:
+ *       Microsoft Corporation - initial API and implementation
+ *
+ */
+
+plugins {
+    `java-library`
+}
+
+val edcVersion: String by project
+val edcGroup: String by project
+val jacksonVersion: String by project
+val jupiterVersion: String by project
+val assertj: String by project
+val mockitoVersion: String by project
+val faker: String by project
+val okHttpVersion: String by project
+val nimbusVersion: String by project
+val bouncycastleVersion: String by project
+val picoCliVersion: String by project
+
+dependencies {
+    testImplementation(project(":system-tests:launcher"))
+    testImplementation(project(":spi:identity-hub-spi"))
+    testImplementation(project(":extensions:identity-hub-verifier"))
+    testImplementation(project(":client-cli"))
+    testImplementation("${edcGroup}:identity-did-core:${edcVersion}")
+    testImplementation("${edcGroup}:identity-did-web:${edcVersion}")
+    testImplementation("${edcGroup}:junit:${edcVersion}")
+    testImplementation("info.picocli:picocli:${picoCliVersion}")
+    testImplementation("info.picocli:picocli-codegen:${picoCliVersion}")
+    testImplementation("com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}")
+    testImplementation("com.squareup.okhttp3:okhttp:${okHttpVersion}")
+    testImplementation("com.nimbusds:nimbus-jose-jwt:${nimbusVersion}")
+    testImplementation("org.bouncycastle:bcpkix-jdk15on:${bouncycastleVersion}")
+    testImplementation("org.assertj:assertj-core:${assertj}")
+    testImplementation("org.mockito:mockito-core:${mockitoVersion}")
+    testImplementation("com.github.javafaker:javafaker:${faker}")
+    testImplementation("org.junit.jupiter:junit-jupiter-api:${jupiterVersion}")
+    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${jupiterVersion}")
+}
+
diff --git a/system-tests/tests/docker-compose.yml b/system-tests/tests/docker-compose.yml
new file mode 100644
index 000000000..5c59f3c7e
--- /dev/null
+++ b/system-tests/tests/docker-compose.yml
@@ -0,0 +1,21 @@
+services:
+
+  # A nginx based HTTP server to serve DIDs.
+  did-server:
+    container_name: did-server
+    image: nginx
+    volumes:
+      - ./resources/webdid:/usr/share/nginx/html
+    ports:
+      - "8080:80"
+
+  # Dataspace participant with identity-hub
+  participant:
+    container_name: participant
+    build:
+      context: ../launcher
+      args:
+        JVM_ARGS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5007"
+    ports:
+      - "8182:8181"
+      - "5007:5007"
diff --git a/system-tests/tests/resources/jwt/authority/private-key.pem b/system-tests/tests/resources/jwt/authority/private-key.pem
new file mode 100644
index 000000000..fe36be582
--- /dev/null
+++ b/system-tests/tests/resources/jwt/authority/private-key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIC8DhiolgcPjFIGyqHoP6Cu+jBsKl05bnZVBop9G36CRoAoGCCqGSM49
+AwEHoUQDQgAE25DvKuU5+gvMdKkyiDDIsx3tcuPXjgVyAjs1JcfFtvi9I0Femuqy
+mDTu3WWdYmdaJQMJJx3qwEJGTVTxcKGtEg==
+-----END EC PRIVATE KEY-----
diff --git a/system-tests/tests/resources/jwt/authority/public-key.pem b/system-tests/tests/resources/jwt/authority/public-key.pem
new file mode 100644
index 000000000..40e8cacd7
--- /dev/null
+++ b/system-tests/tests/resources/jwt/authority/public-key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE25DvKuU5+gvMdKkyiDDIsx3tcuPX
+jgVyAjs1JcfFtvi9I0FemuqymDTu3WWdYmdaJQMJJx3qwEJGTVTxcKGtEg==
+-----END PUBLIC KEY-----
diff --git a/system-tests/tests/resources/jwt/participant/private-key.pem b/system-tests/tests/resources/jwt/participant/private-key.pem
new file mode 100644
index 000000000..18683140f
--- /dev/null
+++ b/system-tests/tests/resources/jwt/participant/private-key.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIMH5nIIP0d/AI1D1L6DpIscPL3EX5x5XHxrBnt2+TduzoAoGCCqGSM49
+AwEHoUQDQgAET6VaLBE16dj2WcqcCaF6M1ORlaQhrT5bEhY8JUiDwRCFxVvg+cSY
+fZMxhs+T5y3AfHejXkk0g6Ehg8HNHeJh/g==
+-----END EC PRIVATE KEY-----
diff --git a/system-tests/tests/resources/jwt/participant/public-key.pem b/system-tests/tests/resources/jwt/participant/public-key.pem
new file mode 100644
index 000000000..8085f6095
--- /dev/null
+++ b/system-tests/tests/resources/jwt/participant/public-key.pem
@@ -0,0 +1,4 @@
+-----BEGIN PUBLIC KEY-----
+MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAET6VaLBE16dj2WcqcCaF6M1ORlaQh
+rT5bEhY8JUiDwRCFxVvg+cSYfZMxhs+T5y3AfHejXkk0g6Ehg8HNHeJh/g==
+-----END PUBLIC KEY-----
diff --git a/system-tests/tests/resources/webdid/authority/did.json b/system-tests/tests/resources/webdid/authority/did.json
new file mode 100644
index 000000000..649b1a687
--- /dev/null
+++ b/system-tests/tests/resources/webdid/authority/did.json
@@ -0,0 +1,25 @@
+{
+	"id": "did:web:localhost%3A8080:authority",
+	"@context": [
+		"https://www.w3.org/ns/did/v1",
+		{
+			"@base": "did:web:localhost%3A8080:authority"
+		}
+	],
+	"service": [],
+	"verificationMethod": [{
+		"id": "#my-key-1",
+		"controller": "",
+		"type": "EcdsaSecp256k1VerificationKey2019",
+		"publicKeyJwk": {
+			"kty": "EC",
+			"kid": "czbdfcccY3TEaeeQj9J-PtzEH-DDzMKgRYHKohvImbU",
+			"crv": "P-256",
+			"x": "25DvKuU5-gvMdKkyiDDIsx3tcuPXjgVyAjs1JcfFtvg",
+			"y": "vSNBXprqspg07t1lnWJnWiUDCScd6sBCRk1U8XChrRI"
+		}
+	}],
+	"authentication": [
+		"#my-key-1"
+	]
+}
\ No newline at end of file
diff --git a/system-tests/tests/resources/webdid/participant/did.json b/system-tests/tests/resources/webdid/participant/did.json
new file mode 100644
index 000000000..29d78db38
--- /dev/null
+++ b/system-tests/tests/resources/webdid/participant/did.json
@@ -0,0 +1,29 @@
+{
+	"id": "did:web:localhost%3A8080:participant",
+	"@context": [
+		"https://www.w3.org/ns/did/v1",
+		{
+			"@base": "did:web:localhost%3A8080:participant"
+		}
+	],
+	"service": [{
+		"id": "#identity-hub-url",
+		"type": "IdentityHub",
+		"serviceEndpoint": "http://localhost:8182/api/identity-hub"
+	}],
+	"verificationMethod": [{
+		"id": "#my-key-1",
+		"controller": "",
+		"type": "EcdsaSecp256k1VerificationKey2019",
+		"publicKeyJwk": {
+			"kty": "EC",
+			"kid": "uIowOgviyWueDwC2SjwEPnqsBVQkhIN3m6B0Ajs6rlU",
+			"crv": "P-256",
+			"x": "T6VaLBE16dj2WcqcCaF6M1ORlaQhrT5bEhY8JUiDwRA",
+			"y": "hcVb4PnEmH2TMYbPk-ctwHx3o15JNIOhIYPBzR3iYf4"
+		}
+	}],
+	"authentication": [
+		"#my-key-1"
+	]
+}
\ No newline at end of file
diff --git a/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/IntegrationTest.java b/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/IntegrationTest.java
new file mode 100644
index 000000000..c81825337
--- /dev/null
+++ b/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/IntegrationTest.java
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) 2022 Microsoft Corporation
+ *
+ *  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:
+ *       Microsoft Corporation - initial API and implementation
+ *
+ */
+
+package org.eclipse.dataspaceconnector.identityhub.systemtests;
+
+import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target({ ElementType.TYPE, ElementType.METHOD })
+@Retention(RetentionPolicy.RUNTIME)
+@EnabledIfEnvironmentVariable(named = "INTEGRATION_TEST", matches = "true")
+public @interface IntegrationTest {
+}
diff --git a/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java b/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java
new file mode 100644
index 000000000..d4f86af6e
--- /dev/null
+++ b/system-tests/tests/src/test/java/org/eclipse/dataspaceconnector/identityhub/systemtests/VerifiableCredentialsIntegrationTest.java
@@ -0,0 +1,100 @@
+/*
+ *  Copyright (c) 2022 Microsoft Corporation
+ *
+ *  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:
+ *       Microsoft Corporation - initial API and implementation
+ *
+ */
+
+package org.eclipse.dataspaceconnector.identityhub.systemtests;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.javafaker.Faker;
+import net.minidev.json.JSONObject;
+import org.eclipse.dataspaceconnector.iam.did.spi.credentials.CredentialsVerifier;
+import org.eclipse.dataspaceconnector.iam.did.spi.resolution.DidResolverRegistry;
+import org.eclipse.dataspaceconnector.identityhub.cli.IdentityHubCli;
+import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential;
+import org.eclipse.dataspaceconnector.junit.extensions.EdcExtension;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import picocli.CommandLine;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.map;
+import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY;
+
+@IntegrationTest
+@ExtendWith(EdcExtension.class)
+class VerifiableCredentialsIntegrationTest {
+
+    static final Faker FAKER = new Faker();
+    static final String HUB_URL = "http://localhost:8182/api/identity-hub";
+    static final String AUTHORITY_DID = "did:web:localhost%3A8080:authority";
+    static final String PARTICIPANT_DID = "did:web:localhost%3A8080:participant";
+    static final String AUTHORITY_PRIVATE_KEY_PATH = "resources/jwt/authority/private-key.pem";
+    static final ObjectMapper MAPPER = new ObjectMapper();
+    static final VerifiableCredential VC1 = VerifiableCredential.Builder.newInstance()
+            .id(FAKER.internet().uuid())
+            .credentialSubject(Map.of(
+                    FAKER.internet().uuid(), FAKER.lorem().word(),
+                    FAKER.internet().uuid(), FAKER.lorem().word()))
+            .build();
+
+    CommandLine cmd = IdentityHubCli.getCommandLine();
+    StringWriter out = new StringWriter();
+    StringWriter err = new StringWriter();
+
+    @BeforeEach
+    void setUp(EdcExtension extension) {
+        cmd.setOut(new PrintWriter(out));
+        cmd.setErr(new PrintWriter(err));
+
+        extension.setConfiguration(Map.of(
+                "edc.identity.hub.url", HUB_URL,
+                "edc.iam.did.web.use.https", "false"));
+    }
+
+    @Test
+    void push_and_get_verifiable_credentials(CredentialsVerifier verifier, DidResolverRegistry resolverRegistry) throws Exception {
+        addVerifiableCredentialWithCli();
+        assertGetVerifiedCredentials(verifier, resolverRegistry);
+    }
+
+    private void addVerifiableCredentialWithCli() throws JsonProcessingException {
+        var json = MAPPER.writeValueAsString(VC1);
+        int result = cmd.execute("-s", HUB_URL, "vc", "add", "-c", json, "-i", AUTHORITY_DID, "-b", PARTICIPANT_DID, "-k", AUTHORITY_PRIVATE_KEY_PATH);
+        assertThat(result).isEqualTo(0);
+    }
+
+    private void assertGetVerifiedCredentials(CredentialsVerifier verifier, DidResolverRegistry resolverRegistry) {
+        var didResult = resolverRegistry.resolve(PARTICIPANT_DID);
+        assertThat(didResult.succeeded()).isTrue();
+
+        var verifiedCredentials = verifier.getVerifiedCredentials(didResult.getContent());
+        assertThat(verifiedCredentials.succeeded()).isTrue();
+
+        var vcs = verifiedCredentials.getContent();
+        assertThat(vcs)
+                .extractingByKey(VC1.getId())
+                .asInstanceOf(map(String.class, JSONObject.class))
+                .extractingByKey(VERIFIABLE_CREDENTIALS_KEY)
+                .satisfies(c -> {
+                    assertThat(MAPPER.convertValue(c, VerifiableCredential.class))
+                            .usingRecursiveComparison()
+                            .isEqualTo(VC1);
+                });
+    }
+}
\ No newline at end of file

From 946a728f9a09e23715d3dd0c41d8736fe9ca2e12 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 09:43:11 +0200
Subject: [PATCH 2/8] Extend changelog

---
 CHANGELOG.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3618f7f6d..7838f2a7b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,7 +17,7 @@ in the detailed section referring to by linking pull requests or issues.
 - Identity Hub client (#4)
 - Maven artefact publication (#21) 
 - CredentialsVerifier implementation (#24)
-- CLI (#25)
+- Identity Hub Command Line Interface (#25)
 
 #### Changed
 

From e2c6d782d46e231f19ed75738b713f081aa7c6b2 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 11:34:39 +0200
Subject: [PATCH 3/8] Add test for buildSignedJwt

---
 .../VerifiableCredentialsJwtService.java      | 24 +++----
 .../VerifiableCredentialsJwtServiceImpl.java  | 32 ++++-----
 .../VerifiableCredentialsJwtServiceTest.java  | 67 +++++++++++++++----
 3 files changed, 83 insertions(+), 40 deletions(-)

diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java
index bd8f9d90a..3114678ad 100644
--- a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java
+++ b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtService.java
@@ -28,6 +28,18 @@ public interface VerifiableCredentialsJwtService {
 
     String VERIFIABLE_CREDENTIALS_KEY = "vc";
 
+    /**
+     * Builds a verifiable credential as a signed JWT
+     *
+     * @param credential The verifiable credential to sign
+     * @param issuer     The issuer of the verifiable credential
+     * @param subject    The subject of the verifiable credential
+     * @param privateKey The private key of the issuer, used for signing
+     * @return The Verifiable Credential as a JWT
+     * @throws Exception In case the credential can not be signed
+     */
+    SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, PrivateKeyWrapper privateKey) throws Exception;
+
     /**
      * Extract verifiable credentials from a JWT. The credential is represented with the following format
      * 
{@code
@@ -50,16 +62,4 @@ public interface VerifiableCredentialsJwtService {
      */
     Result> extractCredential(SignedJWT jwt);
 
-    /**
-     * Builds a verifiable credential as a signed JWT
-     *
-     * @param credential The verifiable credential to sign
-     * @param issuer     The issuer of the verifiable credential
-     * @param subject    The subject of the verifiable credential
-     * @param privateKey The private key of the issuer, used for signing
-     * @return The Verifiable Credential as a JWT
-     * @throws Exception In case the credential can not be signed
-     */
-    SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, PrivateKeyWrapper privateKey) throws Exception;
-
 }
diff --git a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
index c871bf4f2..41bbdec8f 100644
--- a/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
+++ b/spi/identity-hub-spi/src/main/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceImpl.java
@@ -36,22 +36,6 @@ public VerifiableCredentialsJwtServiceImpl(ObjectMapper objectMapper) {
         this.objectMapper = objectMapper;
     }
 
-    @Override
-    public Result> extractCredential(SignedJWT jwt) {
-        try {
-            var payload = jwt.getPayload().toJSONObject();
-            var vcObject = payload.get(VERIFIABLE_CREDENTIALS_KEY);
-            if (vcObject == null) {
-                return Result.failure(String.format("No %s field found", VERIFIABLE_CREDENTIALS_KEY));
-            }
-            var verifiableCredential = objectMapper.convertValue(vcObject, VerifiableCredential.class);
-
-            return Result.success(new AbstractMap.SimpleEntry<>(verifiableCredential.getId(), payload));
-        } catch (RuntimeException e) {
-            return Result.failure(Objects.requireNonNullElseGet(e.getMessage(), () -> e.toString()));
-        }
-    }
-
     @Override
     public SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, PrivateKeyWrapper privateKey) throws JOSEException, ParseException {
         var jwsHeader = new JWSHeader.Builder(JWSAlgorithm.ES256).build();
@@ -67,4 +51,20 @@ public SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer,
 
         return SignedJWT.parse(jws.serialize());
     }
+
+    @Override
+    public Result> extractCredential(SignedJWT jwt) {
+        try {
+            var payload = jwt.getPayload().toJSONObject();
+            var vcObject = payload.get(VERIFIABLE_CREDENTIALS_KEY);
+            if (vcObject == null) {
+                return Result.failure(String.format("No %s field found", VERIFIABLE_CREDENTIALS_KEY));
+            }
+            var verifiableCredential = objectMapper.convertValue(vcObject, VerifiableCredential.class);
+
+            return Result.success(new AbstractMap.SimpleEntry<>(verifiableCredential.getId(), payload));
+        } catch (RuntimeException e) {
+            return Result.failure(Objects.requireNonNullElseGet(e.getMessage(), () -> e.toString()));
+        }
+    }
 }
diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
index 14408ae90..773dd062c 100644
--- a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
+++ b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
@@ -20,39 +20,81 @@
 import com.nimbusds.jose.JWSHeader;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
+import org.eclipse.dataspaceconnector.iam.did.crypto.key.EcPrivateKeyWrapper;
+import org.eclipse.dataspaceconnector.iam.did.crypto.key.EcPublicKeyWrapper;
+import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
+import java.util.Map;
+
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.buildSignedJwt;
+import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY;
 import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
-import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateVerifiableCredential;
 import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.toMap;
 
 public class VerifiableCredentialsJwtServiceTest {
 
-    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-    static final VerifiableCredentialsJwtService VERIFIABLE_CREDENTIALS_JWT_SERVICE = new VerifiableCredentialsJwtServiceImpl(OBJECT_MAPPER);
     static final Faker FAKER = new Faker();
-    static final String VERIFIABLE_CREDENTIALS_KEY = "vc";
+    static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    static final VerifiableCredential VERIFIABLE_CREDENTIAL = VerifiableCredential.Builder.newInstance()
+            .id(FAKER.internet().uuid())
+            .credentialSubject(Map.of(
+                    FAKER.internet().uuid(), FAKER.lorem().word(),
+                    FAKER.internet().uuid(), FAKER.lorem().word()))
+            .build();
     static final JWSHeader JWS_HEADER = new JWSHeader.Builder(JWSAlgorithm.ES256).build();
+    EcPrivateKeyWrapper privateKey;
+    EcPublicKeyWrapper publicKey;
+    VerifiableCredentialsJwtService service;
+
+    @BeforeEach
+    public void setUp() {
+        var key = generateEcKey();
+        privateKey = new EcPrivateKeyWrapper(key);
+        publicKey = new EcPublicKeyWrapper(key);
+        service = new VerifiableCredentialsJwtServiceImpl(OBJECT_MAPPER);
+    }
 
     @Test
-    public void extractCredential_OnJwtWithValidCredential() throws Exception {
+    public void buildSignedJwt_success() throws Exception {
+        // Arrange
+        var issuer = FAKER.lorem().word();
+        var subject = FAKER.lorem().word();
 
+        // Act
+        var signedJWT = service.buildSignedJwt(VERIFIABLE_CREDENTIAL, issuer, subject, privateKey);
+
+        // Assert
+        boolean result = signedJWT.verify(publicKey.verifier());
+        assertThat(result).isTrue();
+
+        assertThat(signedJWT.getPayload().toJSONObject())
+                .containsEntry("iss", issuer)
+                .containsEntry("sub", subject)
+                .extractingByKey(VERIFIABLE_CREDENTIALS_KEY)
+                .satisfies(c -> {
+                    assertThat(OBJECT_MAPPER.convertValue(c, VerifiableCredential.class))
+                            .usingRecursiveComparison()
+                            .isEqualTo(VERIFIABLE_CREDENTIAL);
+                });
+    }
+
+    @Test
+    public void extractCredential_OnJwtWithValidCredential() throws Exception {
         // Arrange
-        var verifiableCredential = generateVerifiableCredential();
         var issuer = FAKER.lorem().word();
         var subject = FAKER.lorem().word();
-        var jwt = buildSignedJwt(verifiableCredential, issuer, subject, generateEcKey());
+        var jwt = service.buildSignedJwt(VERIFIABLE_CREDENTIAL, issuer, subject, privateKey);
 
         // Act
-        var result = VERIFIABLE_CREDENTIALS_JWT_SERVICE.extractCredential(jwt);
+        var result = service.extractCredential(jwt);
 
         // Assert
         assertThat(result.succeeded()).isTrue();
         assertThat(result.getContent())
                 .usingRecursiveComparison()
-                .isEqualTo(toMap(verifiableCredential, issuer, subject).entrySet().stream().findFirst().get());
+                .isEqualTo(toMap(VERIFIABLE_CREDENTIAL, issuer, subject).entrySet().stream().findFirst().get());
     }
 
     @Test
@@ -63,7 +105,7 @@ public void extractCredential_OnJwtWithMissingVcField() {
         var jws = new SignedJWT(JWS_HEADER, claims);
 
         // Act
-        var result = VERIFIABLE_CREDENTIALS_JWT_SERVICE.extractCredential(jws);
+        var result = service.extractCredential(jws);
 
         // Assert
         assertThat(result.failed()).isTrue();
@@ -77,9 +119,10 @@ public void extractCredential_OnjJwtWithWrongFormat() {
         var jws = new SignedJWT(JWS_HEADER, claims);
 
         // Act
-        var result = VERIFIABLE_CREDENTIALS_JWT_SERVICE.extractCredential(jws);
+        var result = service.extractCredential(jws);
 
         // Assert
         assertThat(result.failed()).isTrue();
     }
+
 }

From c8435d30c2d6f0681da2ebdcfe6ff3c2b9e64d55 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 11:35:32 +0200
Subject: [PATCH 4/8] Rename test util class for CLI

---
 .../identityhub/cli/{TestUtils.java => CliTestUtils.java} | 4 ++--
 .../identityhub/cli/VerifiableCredentialsCommandTest.java | 8 ++++----
 2 files changed, 6 insertions(+), 6 deletions(-)
 rename client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/{TestUtils.java => CliTestUtils.java} (98%)

diff --git a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/CliTestUtils.java
similarity index 98%
rename from client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java
rename to client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/CliTestUtils.java
index ab3bd4e6e..0a6c75381 100644
--- a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/TestUtils.java
+++ b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/CliTestUtils.java
@@ -30,7 +30,7 @@
 import static org.eclipse.dataspaceconnector.identityhub.credentials.CryptoUtils.readPublicEcKey;
 
 
-public class TestUtils {
+public class CliTestUtils {
     static final Faker FAKER = new Faker();
     public static final String PUBLIC_KEY_PATH = "src/test/resources/test-public-key.pem";
     public static final String PRIVATE_KEY_PATH = "src/test/resources/test-private-key.pem";
@@ -47,7 +47,7 @@ public class TestUtils {
         }
     }
 
-    private TestUtils() {
+    private CliTestUtils() {
     }
 
     public static VerifiableCredential createVerifiableCredential() {
diff --git a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java
index ada0560f9..fab4765ea 100644
--- a/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java
+++ b/client-cli/src/test/java/org/eclipse/dataspaceconnector/identityhub/cli/VerifiableCredentialsCommandTest.java
@@ -34,10 +34,10 @@
 import java.util.stream.Collectors;
 
 import static org.assertj.core.api.Assertions.assertThat;
-import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.PRIVATE_KEY_PATH;
-import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.createVerifiableCredential;
-import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.signVerifiableCredential;
-import static org.eclipse.dataspaceconnector.identityhub.cli.TestUtils.verifyVerifiableCredentialSignature;
+import static org.eclipse.dataspaceconnector.identityhub.cli.CliTestUtils.PRIVATE_KEY_PATH;
+import static org.eclipse.dataspaceconnector.identityhub.cli.CliTestUtils.createVerifiableCredential;
+import static org.eclipse.dataspaceconnector.identityhub.cli.CliTestUtils.signVerifiableCredential;
+import static org.eclipse.dataspaceconnector.identityhub.cli.CliTestUtils.verifyVerifiableCredentialSignature;
 import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY;
 import static org.eclipse.dataspaceconnector.spi.response.StatusResult.success;
 import static org.mockito.ArgumentMatchers.eq;

From e1bc734a08bdda6b7bea58307c28af0500ce8b95 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 11:46:22 +0200
Subject: [PATCH 5/8] Consolidate test

---
 .../VerifiableCredentialsJwtServiceTest.java  | 27 ++++++++++---------
 1 file changed, 15 insertions(+), 12 deletions(-)

diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
index 773dd062c..76bf5be62 100644
--- a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
+++ b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
@@ -29,20 +29,16 @@
 import java.util.Map;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.map;
 import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY;
 import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateEcKey;
-import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.toMap;
+import static org.eclipse.dataspaceconnector.identityhub.junit.testfixtures.VerifiableCredentialTestUtil.generateVerifiableCredential;
 
 public class VerifiableCredentialsJwtServiceTest {
 
     static final Faker FAKER = new Faker();
     static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-    static final VerifiableCredential VERIFIABLE_CREDENTIAL = VerifiableCredential.Builder.newInstance()
-            .id(FAKER.internet().uuid())
-            .credentialSubject(Map.of(
-                    FAKER.internet().uuid(), FAKER.lorem().word(),
-                    FAKER.internet().uuid(), FAKER.lorem().word()))
-            .build();
+    static final VerifiableCredential VERIFIABLE_CREDENTIAL = generateVerifiableCredential();
     static final JWSHeader JWS_HEADER = new JWSHeader.Builder(JWSAlgorithm.ES256).build();
     EcPrivateKeyWrapper privateKey;
     EcPublicKeyWrapper publicKey;
@@ -92,14 +88,21 @@ public void extractCredential_OnJwtWithValidCredential() throws Exception {
 
         // Assert
         assertThat(result.succeeded()).isTrue();
-        assertThat(result.getContent())
-                .usingRecursiveComparison()
-                .isEqualTo(toMap(VERIFIABLE_CREDENTIAL, issuer, subject).entrySet().stream().findFirst().get());
+        assertThat(result.getContent().getKey()).isEqualTo(VERIFIABLE_CREDENTIAL.getId());
+        assertThat(result.getContent().getValue())
+                .asInstanceOf(map(String.class, Object.class))
+                .containsEntry("iss", issuer)
+                .containsEntry("sub", subject)
+                .extractingByKey(VERIFIABLE_CREDENTIALS_KEY)
+                .satisfies(c -> {
+                    assertThat(OBJECT_MAPPER.convertValue(c, VerifiableCredential.class))
+                            .usingRecursiveComparison()
+                            .isEqualTo(VERIFIABLE_CREDENTIAL);
+                });
     }
 
     @Test
     public void extractCredential_OnJwtWithMissingVcField() {
-
         // Arrange
         var claims = new JWTClaimsSet.Builder().claim(FAKER.lorem().word(), FAKER.lorem().word()).build();
         var jws = new SignedJWT(JWS_HEADER, claims);
@@ -113,7 +116,7 @@ public void extractCredential_OnJwtWithMissingVcField() {
     }
 
     @Test
-    public void extractCredential_OnjJwtWithWrongFormat() {
+    public void extractCredential_OnJwtWithWrongFormat() {
         // Arrange
         var claims = new JWTClaimsSet.Builder().claim(VERIFIABLE_CREDENTIALS_KEY, FAKER.lorem().word()).build();
         var jws = new SignedJWT(JWS_HEADER, claims);

From 1d95d130a71fb16ae908c29e667b28e7525ea39c Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 13:21:06 +0200
Subject: [PATCH 6/8] Remove unused import

---
 .../credentials/VerifiableCredentialsJwtServiceTest.java        | 2 --
 1 file changed, 2 deletions(-)

diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
index 76bf5be62..8ba35e5e6 100644
--- a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
+++ b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
@@ -26,8 +26,6 @@
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
-import java.util.Map;
-
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.InstanceOfAssertFactories.map;
 import static org.eclipse.dataspaceconnector.identityhub.credentials.VerifiableCredentialsJwtService.VERIFIABLE_CREDENTIALS_KEY;

From 7c5c72e723c98a2bae29ee4f1675e7afea71b477 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 13:21:24 +0200
Subject: [PATCH 7/8] Simplify method

---
 .../junit/testfixtures/VerifiableCredentialTestUtil.java    | 6 ++----
 1 file changed, 2 insertions(+), 4 deletions(-)

diff --git a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/dataspaceconnector/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/dataspaceconnector/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java
index 6c00f7cb7..9d836612f 100644
--- a/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/dataspaceconnector/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java
+++ b/spi/identity-hub-spi/src/testFixtures/java/org/eclipse/dataspaceconnector/identityhub/junit/testfixtures/VerifiableCredentialTestUtil.java
@@ -25,8 +25,7 @@
 import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
 import com.nimbusds.jwt.JWTClaimsSet;
 import com.nimbusds.jwt.SignedJWT;
-import org.eclipse.dataspaceconnector.iam.did.crypto.key.KeyConverter;
-import org.eclipse.dataspaceconnector.iam.did.spi.document.EllipticCurvePublicKey;
+import org.eclipse.dataspaceconnector.iam.did.crypto.key.EcPublicKeyWrapper;
 import org.eclipse.dataspaceconnector.iam.did.spi.key.PublicKeyWrapper;
 import org.eclipse.dataspaceconnector.identityhub.credentials.model.VerifiableCredential;
 
@@ -57,8 +56,7 @@ public static ECKey generateEcKey() {
     }
 
     public static PublicKeyWrapper toPublicKeyWrapper(ECKey jwk) {
-        var publicKey = new EllipticCurvePublicKey(jwk.getCurve().getName(), jwk.getKeyType().getValue(), jwk.getX().toString(), jwk.getY().toString());
-        return KeyConverter.toPublicKeyWrapper(publicKey, "ec");
+        return new EcPublicKeyWrapper(jwk);
     }
 
     public static SignedJWT buildSignedJwt(VerifiableCredential credential, String issuer, String subject, ECKey jwk) {

From 3bbc511ac8f45082d7deb5f57b3f9133025f1ea3 Mon Sep 17 00:00:00 2001
From: Marc Gomez 
Date: Thu, 28 Jul 2022 13:22:04 +0200
Subject: [PATCH 8/8] Checkstyle

---
 .../credentials/VerifiableCredentialsJwtServiceTest.java    | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
index 8ba35e5e6..714f83e1b 100644
--- a/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
+++ b/spi/identity-hub-spi/src/test/java/org/eclipse/dataspaceconnector/identityhub/credentials/VerifiableCredentialsJwtServiceTest.java
@@ -57,13 +57,13 @@ public void buildSignedJwt_success() throws Exception {
         var subject = FAKER.lorem().word();
 
         // Act
-        var signedJWT = service.buildSignedJwt(VERIFIABLE_CREDENTIAL, issuer, subject, privateKey);
+        var signedJwt = service.buildSignedJwt(VERIFIABLE_CREDENTIAL, issuer, subject, privateKey);
 
         // Assert
-        boolean result = signedJWT.verify(publicKey.verifier());
+        boolean result = signedJwt.verify(publicKey.verifier());
         assertThat(result).isTrue();
 
-        assertThat(signedJWT.getPayload().toJSONObject())
+        assertThat(signedJwt.getPayload().toJSONObject())
                 .containsEntry("iss", issuer)
                 .containsEntry("sub", subject)
                 .extractingByKey(VERIFIABLE_CREDENTIALS_KEY)