diff --git a/charts/tractusx-connector-azure-vault/templates/deployment-controlplane.yaml b/charts/tractusx-connector-azure-vault/templates/deployment-controlplane.yaml index dae3f4f2b..226fb820f 100644 --- a/charts/tractusx-connector-azure-vault/templates/deployment-controlplane.yaml +++ b/charts/tractusx-connector-azure-vault/templates/deployment-controlplane.yaml @@ -119,15 +119,15 @@ spec: # SSI / MIW CONFIGURATION ########################## - name: "TX_SSI_MIW_URL" - value: {{ .Values.controlplane.ssi.miw.url }} + value: {{ .Values.controlplane.ssi.miw.url | quote }} - name: "TX_SSI_MIW_AUTHORITY_ID" - value: {{ .Values.controlplane.ssi.miw.authorityId }} + value: {{ .Values.controlplane.ssi.miw.authorityId | quote }} - name: "TX_SSI_OAUTH_TOKEN_URL" - value: {{ .Values.controlplane.ssi.oauth.tokenurl }} + value: {{ .Values.controlplane.ssi.oauth.tokenurl | quote }} - name: "TX_SSI_OAUTH_CLIENT_ID" - value: {{ .Values.controlplane.ssi.oauth.client.id }} + value: {{ .Values.controlplane.ssi.oauth.client.id | quote }} - name: "TX_SSI_OAUTH_CLIENT_SECRET_ALIAS" - value: {{ .Values.controlplane.ssi.oauth.client.secretAlias }} + value: {{ .Values.controlplane.ssi.oauth.client.secretAlias | quote }} - name: "TX_SSI_ENDPOINT_AUDIENCE" value: {{ printf "%s%s" (include "txdc.controlplane.url.protocol" .) .Values.controlplane.endpoints.protocol.path | quote }} diff --git a/charts/tractusx-connector-memory/templates/deployment-runtime.yaml b/charts/tractusx-connector-memory/templates/deployment-runtime.yaml index 2f012f22c..054ad5959 100644 --- a/charts/tractusx-connector-memory/templates/deployment-runtime.yaml +++ b/charts/tractusx-connector-memory/templates/deployment-runtime.yaml @@ -119,15 +119,15 @@ spec: # SSI / MIW CONFIGURATION ########################## - name: "TX_SSI_MIW_URL" - value: {{ .Values.runtime.ssi.miw.url }} + value: {{ .Values.runtime.ssi.miw.url | quote }} - name: "TX_SSI_MIW_AUTHORITY_ID" - value: {{ .Values.runtime.ssi.miw.authorityId }} + value: {{ .Values.runtime.ssi.miw.authorityId | quote }} - name: "TX_SSI_OAUTH_TOKEN_URL" - value: {{ .Values.runtime.ssi.oauth.tokenurl }} + value: {{ .Values.runtime.ssi.oauth.tokenurl | quote }} - name: "TX_SSI_OAUTH_CLIENT_ID" - value: {{ .Values.runtime.ssi.oauth.client.id }} + value: {{ .Values.runtime.ssi.oauth.client.id | quote }} - name: "TX_SSI_OAUTH_CLIENT_SECRET_ALIAS" - value: {{ .Values.runtime.ssi.oauth.client.secretAlias }} + value: {{ .Values.runtime.ssi.oauth.client.secretAlias | quote }} - name: "TX_SSI_ENDPOINT_AUDIENCE" value: {{ printf "%s%s" (include "txdc.runtime.url.protocol" .) .Values.runtime.endpoints.protocol.path | quote }} diff --git a/charts/tractusx-connector/templates/deployment-controlplane.yaml b/charts/tractusx-connector/templates/deployment-controlplane.yaml index 1faca6b1c..1c4185297 100644 --- a/charts/tractusx-connector/templates/deployment-controlplane.yaml +++ b/charts/tractusx-connector/templates/deployment-controlplane.yaml @@ -119,15 +119,15 @@ spec: # SSI / MIW CONFIGURATION ########################## - name: "TX_SSI_MIW_URL" - value: {{ .Values.controlplane.ssi.miw.url }} + value: {{ .Values.controlplane.ssi.miw.url | quote }} - name: "TX_SSI_MIW_AUTHORITY_ID" - value: {{ .Values.controlplane.ssi.miw.authorityId }} + value: {{ .Values.controlplane.ssi.miw.authorityId | quote }} - name: "TX_SSI_OAUTH_TOKEN_URL" - value: {{ .Values.controlplane.ssi.oauth.tokenurl }} + value: {{ .Values.controlplane.ssi.oauth.tokenurl | quote }} - name: "TX_SSI_OAUTH_CLIENT_ID" - value: {{ .Values.controlplane.ssi.oauth.client.id }} + value: {{ .Values.controlplane.ssi.oauth.client.id | quote }} - name: "TX_SSI_OAUTH_CLIENT_SECRET_ALIAS" - value: {{ .Values.controlplane.ssi.oauth.client.secretAlias }} + value: {{ .Values.controlplane.ssi.oauth.client.secretAlias | quote }} - name: "TX_SSI_ENDPOINT_AUDIENCE" value: {{ printf "%s%s" (include "txdc.controlplane.url.protocol" .) .Values.controlplane.endpoints.protocol.path | quote }} diff --git a/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/CredentialIdentityExtractorTest.java b/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/CredentialIdentityExtractorTest.java index fcbb900ae..3fb8aa154 100644 --- a/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/CredentialIdentityExtractorTest.java +++ b/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/CredentialIdentityExtractorTest.java @@ -26,14 +26,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.eclipse.edc.spi.agent.ParticipantAgent.PARTICIPANT_IDENTITY; -import static org.eclipse.tractusx.edc.iam.ssi.identity.extractor.fixtures.Credentials.SIMPLE_VP; -import static org.eclipse.tractusx.edc.iam.ssi.identity.extractor.fixtures.Credentials.SUMMARY_VP_NO_HOLDER; -import static org.eclipse.tractusx.edc.iam.ssi.identity.extractor.fixtures.Credentials.SUMMARY_VP_NO_SUBJECT; import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CX_SUMMARY_NS_V1; import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.VP_PROPERTY; import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.createObjectMapper; import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.expand; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SIMPLE_VP; import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SUMMARY_VP; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SUMMARY_VP_NO_HOLDER; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SUMMARY_VP_NO_SUBJECT; public class CredentialIdentityExtractorTest { diff --git a/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/fixtures/Credentials.java b/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/fixtures/Credentials.java deleted file mode 100644 index fd14612eb..000000000 --- a/edc-extensions/ssi/ssi-identity-extractor/src/test/java/org/eclipse/tractusx/edc/iam/ssi/identity/extractor/fixtures/Credentials.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.tractusx.edc.iam.ssi.identity.extractor.fixtures; - -public interface Credentials { - - String SIMPLE_VP = """ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": "VerifiablePresentation", - "verifiableCredential": [ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", - "type": [ - "VerifiableCredential" - ], - "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", - "issuanceDate": "2023-06-02T12:00:00Z", - "expirationDate": "2022-06-16T18:56:59Z", - "credentialSubject": { - "id": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000" - } - } - ] - } - """; - - String SUMMARY_VP_NO_HOLDER = """ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": "VerifiablePresentation", - "verifiableCredential": [ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/2023/catenax/credentials/summary/v1" - ], - "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", - "type": [ - "VerifiableCredential", - "SummaryCredential" - ], - "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", - "issuanceDate": "2023-06-02T12:00:00Z", - "expirationDate": "2022-06-16T18:56:59Z", - "credentialSubject": { - "id": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000" - } - } - ] - } - """; - - String SUMMARY_VP_NO_SUBJECT = """ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1" - ], - "type": "VerifiablePresentation", - "verifiableCredential": [ - { - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/2023/catenax/credentials/summary/v1" - ], - "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", - "type": [ - "VerifiableCredential", - "SummaryCredential" - ], - "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", - "issuanceDate": "2023-06-02T12:00:00Z", - "expirationDate": "2022-06-16T18:56:59Z" - } - ] - } - """; -} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/README.md b/edc-extensions/ssi/ssi-miw-credential-client/README.md index 80bb2d27d..beef1f43d 100644 --- a/edc-extensions/ssi/ssi-miw-credential-client/README.md +++ b/edc-extensions/ssi/ssi-miw-credential-client/README.md @@ -1,7 +1,7 @@ # MIW Client Credential Module This module contains an implementation of the `SsiCredentialClient` interface for SSI. -It basically narrow down to two operations: +It basically narrows down to two operations: - obtaining a token for protocol communication - validating the token @@ -13,10 +13,13 @@ For obtaining a `JWT` token also it reaches the MIW, that will create a token wi ## Configuration -| Key | Required | Example | Description | -|-----------------------------------------|----------|----------------|-----------------------------------| -| tx.ssi.miw.url | X | | MIW URL | -| tx.ssi.miw.authority.id | X | | BPN number of the authority | -| tx.ssi.oauth.token.url | X | | Token URL (Keycloak) | -| tx.ssi.oauth.client.id | X | | Client id | -| tx.ssi.oauth.client.secret.alias | X | | Vault alias for the client secret | +| Key | Required | Example | Description | +|----------------------------------|----------|----------------|-----------------------------------| +| tx.ssi.miw.url | X | | MIW URL | +| tx.ssi.miw.authority.id | X | | BPN number of the authority | +| tx.ssi.miw.authority.issuer | | | The id of the issuer (DID) | +| tx.ssi.oauth.token.url | X | | Token URL (Keycloak) | +| tx.ssi.oauth.client.id | X | | Client id | +| tx.ssi.oauth.client.secret.alias | X | | Vault alias for the client secret | + +By default, the `tx.ssi.miw.authority.issuer` is composed with `did:web:: diff --git a/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts b/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts index e06e947e6..37455f538 100644 --- a/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts +++ b/edc-extensions/ssi/ssi-miw-credential-client/build.gradle.kts @@ -27,5 +27,6 @@ dependencies { implementation(libs.jakartaJson) implementation(libs.nimbus.jwt) + testImplementation(testFixtures(project(":spi:ssi-spi"))) testImplementation(testFixtures(libs.edc.junit)) } diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java index 9108d9c05..1f3286ae2 100644 --- a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtension.java @@ -17,7 +17,6 @@ import org.eclipse.edc.runtime.metamodel.annotation.Extension; import org.eclipse.edc.runtime.metamodel.annotation.Inject; import org.eclipse.edc.runtime.metamodel.annotation.Provider; -import org.eclipse.edc.runtime.metamodel.annotation.Setting; import org.eclipse.edc.spi.http.EdcHttpClient; import org.eclipse.edc.spi.monitor.Monitor; import org.eclipse.edc.spi.system.ServiceExtension; @@ -25,6 +24,7 @@ import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClientImpl; +import org.eclipse.tractusx.edc.iam.ssi.miw.config.SsiMiwConfiguration; import org.eclipse.tractusx.edc.iam.ssi.miw.oauth2.MiwOauth2Client; @@ -33,12 +33,6 @@ public class SsiMiwApiClientExtension implements ServiceExtension { public static final String EXTENSION_NAME = "SSI MIW Api Client"; - @Setting(value = "MIW API base url") - public static final String MIW_BASE_URL = "tx.ssi.miw.url"; - - @Setting(value = "MIW Authority ID") - public static final String MIW_AUTHORITY_ID = "tx.ssi.miw.authority.id"; - @Inject private MiwOauth2Client oauth2Client; @@ -51,6 +45,9 @@ public class SsiMiwApiClientExtension implements ServiceExtension { @Inject private Monitor monitor; + @Inject + private SsiMiwConfiguration miwConfiguration; + @Override public String name() { return EXTENSION_NAME; @@ -58,11 +55,7 @@ public String name() { @Provider public MiwApiClient apiClient(ServiceExtensionContext context) { - var baseUrl = context.getConfig().getString(MIW_BASE_URL); - var authorityId = context.getConfig().getString(MIW_AUTHORITY_ID); - - - return new MiwApiClientImpl(httpClient, baseUrl, oauth2Client, context.getParticipantId(), authorityId, typeManager.getMapper(), monitor); + return new MiwApiClientImpl(httpClient, miwConfiguration.getUrl(), oauth2Client, context.getParticipantId(), miwConfiguration.getAuthorityId(), typeManager.getMapper(), monitor); } } diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtension.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtension.java new file mode 100644 index 000000000..2ce9b0c8c --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtension.java @@ -0,0 +1,63 @@ +/* + * 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.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Provider; +import org.eclipse.edc.runtime.metamodel.annotation.Setting; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.tractusx.edc.iam.ssi.miw.config.SsiMiwConfiguration; + +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import static java.lang.String.format; + + +@Extension(SsiMiwConfigurationExtension.EXTENSION_NAME) +public class SsiMiwConfigurationExtension implements ServiceExtension { + + + @Setting(value = "MIW API base url") + public static final String MIW_BASE_URL = "tx.ssi.miw.url"; + @Setting(value = "MIW Authority ID") + public static final String MIW_AUTHORITY_ID = "tx.ssi.miw.authority.id"; + @Setting(value = "MIW Authority Issuer") + public static final String MIW_AUTHORITY_ISSUER = "tx.ssi.miw.authority.issuer"; + public static final String AUTHORITY_ID_TEMPLATE = "did:web:%s:%s"; + protected static final String EXTENSION_NAME = "SSI Miw configuration extension"; + + @Provider + public SsiMiwConfiguration miwConfiguration(ServiceExtensionContext context) { + var baseUrl = context.getConfig().getString(MIW_BASE_URL); + var authorityId = context.getConfig().getString(MIW_AUTHORITY_ID); + var authorityIssuer = authorityIssuer(context, baseUrl, authorityId); + + return SsiMiwConfiguration.Builder.newInstance() + .url(baseUrl) + .authorityId(authorityId) + .authorityIssuer(authorityIssuer) + .build(); + } + + + private String authorityIssuer(ServiceExtensionContext context, String baseUrl, String authorityId) { + var uri = URI.create(baseUrl); + var defaultAuthorityIssuer = format(AUTHORITY_ID_TEMPLATE, URLEncoder.encode(uri.getAuthority(), StandardCharsets.UTF_8), authorityId); + return context.getConfig().getString(MIW_AUTHORITY_ISSUER, defaultAuthorityIssuer); + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtension.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtension.java new file mode 100644 index 000000000..44c58955d --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtension.java @@ -0,0 +1,50 @@ +/* + * 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.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.runtime.metamodel.annotation.Extension; +import org.eclipse.edc.runtime.metamodel.annotation.Inject; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.system.ServiceExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.tractusx.edc.iam.ssi.miw.config.SsiMiwConfiguration; +import org.eclipse.tractusx.edc.iam.ssi.miw.rule.SsiCredentialIssuerValidationRule; +import org.eclipse.tractusx.edc.iam.ssi.miw.rule.SsiCredentialSubjectIdValidationRule; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry; + +@Extension(SsiMiwValidationRuleExtension.EXTENSION_NAME) +public class SsiMiwValidationRuleExtension implements ServiceExtension { + + protected static final String EXTENSION_NAME = "SSI MIW validation rules extension"; + @Inject + private SsiValidationRuleRegistry registry; + + @Inject + private Monitor monitor; + + @Inject + private SsiMiwConfiguration miwConfiguration; + + @Override + public String name() { + return EXTENSION_NAME; + } + + @Override + public void initialize(ServiceExtensionContext context) { + registry.addRule(new SsiCredentialSubjectIdValidationRule(monitor)); + registry.addRule(new SsiCredentialIssuerValidationRule(miwConfiguration.getAuthorityIssuer(), monitor)); + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/config/SsiMiwConfiguration.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/config/SsiMiwConfiguration.java new file mode 100644 index 000000000..eb35f9e7e --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/config/SsiMiwConfiguration.java @@ -0,0 +1,70 @@ +/* + * 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.tractusx.edc.iam.ssi.miw.config; + +import java.util.Objects; + +public class SsiMiwConfiguration { + + protected String url; + protected String authorityId; + protected String authorityIssuer; + + public String getAuthorityId() { + return authorityId; + } + + public String getUrl() { + return url; + } + + public String getAuthorityIssuer() { + return authorityIssuer; + } + + public static class Builder { + private final SsiMiwConfiguration config; + + private Builder() { + config = new SsiMiwConfiguration(); + } + + public static Builder newInstance() { + return new Builder(); + } + + public Builder url(String url) { + config.url = url; + return this; + } + + public Builder authorityId(String authorityId) { + config.authorityId = authorityId; + return this; + } + + public Builder authorityIssuer(String authorityIssuer) { + config.authorityIssuer = authorityIssuer; + return this; + } + + public SsiMiwConfiguration build() { + Objects.requireNonNull(config.url); + Objects.requireNonNull(config.authorityIssuer); + Objects.requireNonNull(config.authorityId); + return config; + } + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRule.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRule.java new file mode 100644 index 000000000..418fd44ad --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRule.java @@ -0,0 +1,99 @@ +/* + * 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.tractusx.edc.iam.ssi.miw.rule; + +import jakarta.json.JsonObject; +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdFieldExtractor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CREDENTIAL_ISSUER; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.SUMMARY_CREDENTIAL_TYPE; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.VP_PROPERTY; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTypeFunctions.extractObjectsOfType; + +/** + * {@link TokenValidationRule} that compares the issuer in the Verifiable Credential (Summary) with the one provided + * by configuration. + */ +public class SsiCredentialIssuerValidationRule implements TokenValidationRule { + + private static final String SUBJECT_ISSUER_EXTRACTOR_PREFIX = "Credential issuer extractor:"; + + private static final String SUBJECT_ISSUER_FIELD_ALIAS = "issuer"; + + private final String credentialIssuer; + + private final Monitor monitor; + + private final JsonLdFieldExtractor credentialIssuerExtractor = JsonLdFieldExtractor.Builder.newInstance() + .field(CREDENTIAL_ISSUER) + .fieldAlias(SUBJECT_ISSUER_FIELD_ALIAS) + .errorPrefix(SUBJECT_ISSUER_EXTRACTOR_PREFIX) + .build(); + + public SsiCredentialIssuerValidationRule(String credentialIssuer, Monitor monitor) { + this.credentialIssuer = credentialIssuer; + this.monitor = monitor; + } + + @Override + public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { + + var vp = (JsonObject) toVerify.getClaim(VP_PROPERTY); + + return Optional.ofNullable(vp) + .map(v -> extractObjectsOfType(SUMMARY_CREDENTIAL_TYPE, v)) + .orElse(Stream.empty()) + .map(this::extractIssuer) + .findFirst() + .orElseGet(() -> Result.failure("Failed to extract credential subject from the membership credential")) + .compose(this::validateCredentialIssuer) + .onFailure(failure -> monitor.severe(failure.getFailureDetail())); + + } + + private Result validateCredentialIssuer(String extractedCredentialIssuer) { + if (credentialIssuer.equals(extractedCredentialIssuer)) { + return Result.success(); + } else { + return Result.failure(format("Invalid credential issuer: expected %s, found %s", credentialIssuer, extractedCredentialIssuer)); + } + } + + private Result extractIssuer(JsonObject credential) { + return this.credentialIssuerExtractor.extract(credential) + .compose(this::extractIssuerValue); + } + + private Result extractIssuerValue(JsonObject issuer) { + var issuerValue = issuer.getString(ID); + if (issuerValue == null) { + return Result.failure("Failed to find the issuer"); + } else { + return Result.success(issuerValue); + } + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRule.java b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRule.java new file mode 100644 index 000000000..dcd37ec15 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRule.java @@ -0,0 +1,100 @@ +/* + * 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.tractusx.edc.iam.ssi.miw.rule; + +import jakarta.json.JsonObject; +import org.eclipse.edc.jwt.spi.TokenValidationRule; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.edc.spi.result.Result; +import org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdFieldExtractor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Stream; + +import static java.lang.String.format; +import static org.eclipse.edc.jsonld.spi.JsonLdKeywords.ID; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CREDENTIAL_SUBJECT; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.SUMMARY_CREDENTIAL_TYPE; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.VP_PROPERTY; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTypeFunctions.extractObjectsOfType; + +/** + * {@link TokenValidationRule} that compares the issuer of the VP (JWT format) with the credential subject id of + * the Verifiable Credential (Summary) + */ +public class SsiCredentialSubjectIdValidationRule implements TokenValidationRule { + + private static final String CREDENTIAL_SUBJECT_EXTRACTOR_PREFIX = "Credential subject extractor:"; + private static final String CREDENTIAL_SUBJECT_FIELD_ALIAS = "credentialSubject"; + + private final Monitor monitor; + + private final JsonLdFieldExtractor credentialSubjectExtractor = JsonLdFieldExtractor.Builder.newInstance() + .field(CREDENTIAL_SUBJECT) + .fieldAlias(CREDENTIAL_SUBJECT_FIELD_ALIAS) + .errorPrefix(CREDENTIAL_SUBJECT_EXTRACTOR_PREFIX) + .build(); + + public SsiCredentialSubjectIdValidationRule(Monitor monitor) { + this.monitor = monitor; + } + + @Override + public Result checkRule(@NotNull ClaimToken toVerify, @Nullable Map additional) { + var issuer = toVerify.getStringClaim(ISSUER); + + if (issuer == null) { + return Result.failure("Required issuer (iss) claim is missing in token"); + } + var vp = (JsonObject) toVerify.getClaim(VP_PROPERTY); + + return Optional.ofNullable(vp) + .map(v -> extractObjectsOfType(SUMMARY_CREDENTIAL_TYPE, v)) + .orElse(Stream.empty()) + .map(this::extractSubjectId) + .findFirst() + .orElseGet(() -> Result.failure("Failed to extract credential subject from the membership credential")) + .compose(credentialSubjectId -> validateCredentialSubjectId(credentialSubjectId, issuer)) + .onFailure((failure -> monitor.severe(failure.getFailureDetail()))); + + } + + private Result validateCredentialSubjectId(String credentialSubjectId, String issuer) { + if (issuer.equals(credentialSubjectId)) { + return Result.success(); + } else { + return Result.failure(format("Issuer %s and credential subject id %s don't match", issuer, credentialSubjectId)); + } + } + + private Result extractSubjectId(JsonObject credential) { + return this.credentialSubjectExtractor.extract(credential) + .compose(this::extractId); + } + + private Result extractId(JsonObject credentialSubject) { + var id = credentialSubject.getString(ID); + if (id == null) { + return Result.failure("Failed to find the id in credential subject"); + } else { + return Result.success(id); + } + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension b/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension index 36fdd24f4..251e96278 100644 --- a/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/main/resources/META-INF/services/org.eclipse.edc.spi.system.ServiceExtension @@ -14,4 +14,6 @@ org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwCredentialClientExtension org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwApiClientExtension -org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwOauth2ClientExtension \ No newline at end of file +org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwOauth2ClientExtension +org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwValidationRuleExtension +org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwConfigurationExtension diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java index dbc11d343..a7080b33a 100644 --- a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwApiClientExtensionTest.java @@ -16,48 +16,43 @@ import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; import org.eclipse.edc.spi.system.ServiceExtensionContext; -import org.eclipse.edc.spi.system.configuration.Config; import org.eclipse.edc.spi.system.injection.ObjectFactory; import org.eclipse.edc.spi.types.TypeManager; import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClient; import org.eclipse.tractusx.edc.iam.ssi.miw.api.MiwApiClientImpl; +import org.eclipse.tractusx.edc.iam.ssi.miw.config.SsiMiwConfiguration; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; -import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwApiClientExtension.MIW_AUTHORITY_ID; -import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwApiClientExtension.MIW_BASE_URL; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @ExtendWith(DependencyInjectionExtension.class) public class SsiMiwApiClientExtensionTest { - SsiMiwApiClientExtension extension; - - ServiceExtensionContext context; + private final SsiMiwConfiguration cfg = mock(SsiMiwConfiguration.class); + private SsiMiwApiClientExtension extension; @BeforeEach void setup(ObjectFactory factory, ServiceExtensionContext context) { - this.context = spy(context); + context.registerService(SsiMiwConfiguration.class, cfg); context.registerService(MiwApiClient.class, mock(MiwApiClient.class)); context.registerService(TypeManager.class, new TypeManager()); extension = factory.constructInstance(SsiMiwApiClientExtension.class); } @Test - void initialize() { - var config = mock(Config.class); - when(context.getConfig()).thenReturn(config); - when(config.getString(MIW_BASE_URL)).thenReturn("url"); - when(config.getString(MIW_AUTHORITY_ID)).thenReturn("authorityId"); - + void initialize(ServiceExtensionContext context) { + when(cfg.getUrl()).thenReturn("http://localhost"); + when(cfg.getAuthorityId()).thenReturn("id"); assertThat(extension.apiClient(context)).isInstanceOf(MiwApiClientImpl.class); - verify(config).getString(MIW_BASE_URL); - verify(config).getString(MIW_AUTHORITY_ID); + + verify(cfg).getUrl(); + verify(cfg).getAuthorityId(); + } } diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtensionTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtensionTest.java new file mode 100644 index 000000000..24f521f3a --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwConfigurationExtensionTest.java @@ -0,0 +1,100 @@ +/* + * 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.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.configuration.Config; +import org.eclipse.edc.spi.system.injection.ObjectFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static java.lang.String.format; +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwConfigurationExtension.AUTHORITY_ID_TEMPLATE; +import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwConfigurationExtension.MIW_AUTHORITY_ID; +import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwConfigurationExtension.MIW_AUTHORITY_ISSUER; +import static org.eclipse.tractusx.edc.iam.ssi.miw.SsiMiwConfigurationExtension.MIW_BASE_URL; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class SsiMiwConfigurationExtensionTest { + + private SsiMiwConfigurationExtension extension; + + private ServiceExtensionContext context; + + @BeforeEach + void setup(ObjectFactory factory, ServiceExtensionContext context) { + this.context = spy(context); + extension = factory.constructInstance(SsiMiwConfigurationExtension.class); + } + + @Test + void initialize() { + var url = "http://localhost:8080"; + var authorityId = "id"; + var authorityIssuer = "issuer"; + + var cfg = mock(Config.class); + when(context.getConfig()).thenReturn(cfg); + + when(cfg.getString(MIW_BASE_URL)).thenReturn(url); + when(cfg.getString(MIW_AUTHORITY_ID)).thenReturn(authorityId); + when(cfg.getString(eq(MIW_AUTHORITY_ISSUER), anyString())).thenReturn(authorityIssuer); + + var miwConfig = extension.miwConfiguration(context); + + verify(cfg).getString(MIW_BASE_URL); + verify(cfg).getString(MIW_AUTHORITY_ID); + verify(cfg).getString(eq(MIW_AUTHORITY_ISSUER), anyString()); + + assertThat(miwConfig.getUrl()).isEqualTo(url); + assertThat(miwConfig.getAuthorityId()).isEqualTo(authorityId); + assertThat(miwConfig.getAuthorityIssuer()).isEqualTo(authorityIssuer); + + } + + @Test + void initialize_withDefaultIssuer() { + var url = "http://localhost:8080"; + var authorityId = "id"; + + var cfg = mock(Config.class); + when(context.getConfig()).thenReturn(cfg); + + when(cfg.getString(MIW_BASE_URL)).thenReturn(url); + when(cfg.getString(MIW_AUTHORITY_ID)).thenReturn(authorityId); + when(cfg.getString(eq(MIW_AUTHORITY_ISSUER), anyString())).thenAnswer(answer -> answer.getArgument(1)); + + var miwConfig = extension.miwConfiguration(context); + + verify(cfg).getString(MIW_BASE_URL); + verify(cfg).getString(MIW_AUTHORITY_ID); + verify(cfg).getString(eq(MIW_AUTHORITY_ISSUER), anyString()); + + assertThat(miwConfig.getUrl()).isEqualTo(url); + assertThat(miwConfig.getAuthorityId()).isEqualTo(authorityId); + assertThat(miwConfig.getAuthorityIssuer()).isEqualTo(format(AUTHORITY_ID_TEMPLATE, "localhost%3A8080", authorityId)); + + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtensionTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtensionTest.java new file mode 100644 index 000000000..89439aab1 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/SsiMiwValidationRuleExtensionTest.java @@ -0,0 +1,58 @@ +/* + * 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.tractusx.edc.iam.ssi.miw; + +import org.eclipse.edc.junit.extensions.DependencyInjectionExtension; +import org.eclipse.edc.spi.system.ServiceExtensionContext; +import org.eclipse.edc.spi.system.injection.ObjectFactory; +import org.eclipse.tractusx.edc.iam.ssi.miw.config.SsiMiwConfiguration; +import org.eclipse.tractusx.edc.iam.ssi.miw.rule.SsiCredentialIssuerValidationRule; +import org.eclipse.tractusx.edc.iam.ssi.miw.rule.SsiCredentialSubjectIdValidationRule; +import org.eclipse.tractusx.edc.iam.ssi.spi.SsiValidationRuleRegistry; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.mockito.ArgumentMatchers.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(DependencyInjectionExtension.class) +public class SsiMiwValidationRuleExtensionTest { + + private final SsiValidationRuleRegistry registry = mock(SsiValidationRuleRegistry.class); + private final SsiMiwConfiguration cfg = mock(SsiMiwConfiguration.class); + private SsiMiwValidationRuleExtension extension; + + @BeforeEach + void setup(ObjectFactory factory, ServiceExtensionContext context) { + context.registerService(SsiMiwConfiguration.class, cfg); + context.registerService(SsiValidationRuleRegistry.class, registry); + extension = factory.constructInstance(SsiMiwValidationRuleExtension.class); + } + + @Test + void initialize(ServiceExtensionContext context) { + when(cfg.getAuthorityIssuer()).thenReturn("issuer"); + + extension.initialize(context); + verify(registry).addRule(isA(SsiCredentialSubjectIdValidationRule.class)); + verify(registry).addRule(isA(SsiCredentialIssuerValidationRule.class)); + + verify(cfg).getAuthorityIssuer(); + } + +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRuleTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRuleTest.java new file mode 100644 index 000000000..34afa8c57 --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialIssuerValidationRuleTest.java @@ -0,0 +1,60 @@ +/* + * 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.tractusx.edc.iam.ssi.miw.rule; + +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryContext; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CX_SUMMARY_NS_V1; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.VP_PROPERTY; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.createObjectMapper; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.expand; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SUMMARY_VP; +import static org.mockito.Mockito.mock; + +public class SsiCredentialIssuerValidationRuleTest { + + static final Map CONTEXT_CACHE = Map.of(CX_SUMMARY_NS_V1, SummaryContext.SUMMARY_CONTEXT); + + SsiCredentialIssuerValidationRule validationRule; + + @Test + void checkRule() throws JsonProcessingException { + validationRule = new SsiCredentialIssuerValidationRule("did:web:issuer-a016-203-129-213-99.ngrok-free.app:BPNL000000000000", mock(Monitor.class)); + var vp = expand(createObjectMapper().readValue(SUMMARY_VP, JsonObject.class), CONTEXT_CACHE); + var claimToken = ClaimToken.Builder.newInstance().claim(VP_PROPERTY, vp).build(); + var result = validationRule.checkRule(claimToken, Map.of()); + + assertThat(result.succeeded()).isTrue(); + } + + + @Test + void checkRule_shouldFail_whenIssuerIsWrong() throws JsonProcessingException { + validationRule = new SsiCredentialIssuerValidationRule("issuer", mock(Monitor.class)); + var vp = expand(createObjectMapper().readValue(SUMMARY_VP, JsonObject.class), CONTEXT_CACHE); + var claimToken = ClaimToken.Builder.newInstance().claim(VP_PROPERTY, vp).build(); + var result = validationRule.checkRule(claimToken, Map.of()); + + assertThat(result.succeeded()).isFalse(); + } +} diff --git a/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRuleTest.java b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRuleTest.java new file mode 100644 index 000000000..7fefd00fb --- /dev/null +++ b/edc-extensions/ssi/ssi-miw-credential-client/src/test/java/org/eclipse/tractusx/edc/iam/ssi/miw/rule/SsiCredentialSubjectIdValidationRuleTest.java @@ -0,0 +1,77 @@ +/* + * 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.tractusx.edc.iam.ssi.miw.rule; + +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.json.JsonObject; +import org.eclipse.edc.spi.iam.ClaimToken; +import org.eclipse.edc.spi.monitor.Monitor; +import org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryContext; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.CX_SUMMARY_NS_V1; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.CredentialsNamespaces.VP_PROPERTY; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.createObjectMapper; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.JsonLdTextFixtures.expand; +import static org.eclipse.tractusx.edc.iam.ssi.spi.jsonld.SummaryCredential.SUMMARY_VP; +import static org.mockito.Mockito.mock; + +public class SsiCredentialSubjectIdValidationRuleTest { + + static final Map CONTEXT_CACHE = Map.of(CX_SUMMARY_NS_V1, SummaryContext.SUMMARY_CONTEXT); + + private final SsiCredentialSubjectIdValidationRule validationRule = new SsiCredentialSubjectIdValidationRule(mock(Monitor.class)); + + @Test + void checkRule() throws JsonProcessingException { + var vp = expand(createObjectMapper().readValue(SUMMARY_VP, JsonObject.class), CONTEXT_CACHE); + var claimToken = ClaimToken.Builder.newInstance() + .claim(VP_PROPERTY, vp) + .claim(ISSUER, "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000").build(); + + var result = validationRule.checkRule(claimToken, Map.of()); + + assertThat(result.succeeded()).isTrue(); + } + + @Test + void checkRule_shouldFail_whenIssuerMissingInClaims() throws JsonProcessingException { + var vp = expand(createObjectMapper().readValue(SUMMARY_VP, JsonObject.class), CONTEXT_CACHE); + var claimToken = ClaimToken.Builder.newInstance() + .claim(VP_PROPERTY, vp) + .build(); + + var result = validationRule.checkRule(claimToken, Map.of()); + + assertThat(result.succeeded()).isFalse(); + } + + @Test + void checkRule_shouldFail_whenWrongIssuerInClaims() throws JsonProcessingException { + var vp = expand(createObjectMapper().readValue(SUMMARY_VP, JsonObject.class), CONTEXT_CACHE); + var claimToken = ClaimToken.Builder.newInstance() + .claim(VP_PROPERTY, vp) + .claim(ISSUER, "wrong").build(); + + var result = validationRule.checkRule(claimToken, Map.of()); + + assertThat(result.succeeded()).isFalse(); + } + +} diff --git a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-azure-vault-test.yaml b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-azure-vault-test.yaml index fe6821871..9f4768383 100644 --- a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-azure-vault-test.yaml +++ b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-azure-vault-test.yaml @@ -49,6 +49,10 @@ controlplane: pullPolicy: Never tag: "latest" repository: "edc-controlplane-postgresql-azure-vault" + ssi: + miw: + url: "http://localhost:8080" + authorityId: "authorityId" securityContext: # avoids some errors in the log: cannot write temp files of large multipart requests when R/O readOnlyRootFilesystem: false diff --git a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-memory-test.yaml b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-memory-test.yaml index bf44b46f1..f80687cda 100644 --- a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-memory-test.yaml +++ b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-memory-test.yaml @@ -40,6 +40,10 @@ runtime: endpoints: management: authKey: password + ssi: + miw: + url: "http://localhost:8080" + authorityId: "authorityId" image: pullPolicy: Never tag: "latest" diff --git a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-test.yaml b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-test.yaml index e622036d9..6dd81aa29 100644 --- a/edc-tests/deployment/src/main/resources/helm/tractusx-connector-test.yaml +++ b/edc-tests/deployment/src/main/resources/helm/tractusx-connector-test.yaml @@ -37,6 +37,9 @@ controlplane: # avoids some errors in the log: cannot write temp files of large multipart requests when R/O readOnlyRootFilesystem: false ssi: + miw: + url: "http://localhost:8080" + authorityId: "authorityId" oauth: client: secretAlias: "client-secret" diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java index 4fc4f4f36..022b79ada 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/lifecycle/TestRuntimeConfiguration.java @@ -119,6 +119,7 @@ public static Map sokratesSsiConfiguration() { put("tx.ssi.oauth.client.id", "client_id"); put("tx.ssi.oauth.client.secret.alias", "client_secret_alias"); put("tx.ssi.miw.authority.id", "authorityId"); + put("tx.ssi.miw.authority.issuer", "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000"); put("tx.vault.seed.secrets", "client_secret_alias:client_secret"); put("tx.ssi.endpoint.audience", SOKRATES_DSP_CALLBACK); } @@ -200,6 +201,7 @@ public static Map platoSsiConfiguration() { put("tx.ssi.oauth.client.id", "client_id"); put("tx.ssi.oauth.client.secret.alias", "client_secret_alias"); put("tx.ssi.miw.authority.id", "authorityId"); + put("tx.ssi.miw.authority.issuer", "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000"); put("tx.vault.seed.secrets", "client_secret_alias:client_secret"); put("tx.ssi.endpoint.audience", PLATO_DSP_CALLBACK); } diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/MiwSsiCatalogTest.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/MiwSsiCatalogTest.java index f7cbf0ec3..15f42ee22 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/MiwSsiCatalogTest.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/catalog/MiwSsiCatalogTest.java @@ -56,6 +56,7 @@ public static Map sokratesSsiMiwConfiguration() { put("tx.ssi.oauth.client.id", "miw_private_client"); put("tx.ssi.oauth.client.secret.alias", "client_secret_alias"); put("tx.ssi.miw.authority.id", "BPNL000000000000"); + put("tx.ssi.miw.authority.issuer", "did:web:localhost%3A8080:BPNL000000000000"); put("tx.vault.seed.secrets", "client_secret_alias:miw_private_client"); put("tx.ssi.endpoint.audience", SOKRATES_DSP_CALLBACK); } diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/negotiation/SsiContractNegotiationInMemoryTest.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/negotiation/SsiContractNegotiationInMemoryTest.java index 7429a1b71..6523b2358 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/negotiation/SsiContractNegotiationInMemoryTest.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/negotiation/SsiContractNegotiationInMemoryTest.java @@ -67,11 +67,13 @@ void setup() throws IOException { miwPlatoServer = new MockWebServer(); oauthServer = new MockWebServer(); + var credentialSubjectId = "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000"; + miwSokratesServer.start(MIW_SOKRATES_PORT); - miwSokratesServer.setDispatcher(new MiwDispatcher(SOKRATES_BPN, SUMMARY_VC_TEMPLATE, PLATO_DSP_CALLBACK)); + miwSokratesServer.setDispatcher(new MiwDispatcher(SOKRATES_BPN, SUMMARY_VC_TEMPLATE, credentialSubjectId, PLATO_DSP_CALLBACK)); miwPlatoServer.start(MIW_PLATO_PORT); - miwPlatoServer.setDispatcher(new MiwDispatcher(PLATO_BPN, SUMMARY_VC_TEMPLATE, SOKRATES_DSP_CALLBACK)); + miwPlatoServer.setDispatcher(new MiwDispatcher(PLATO_BPN, SUMMARY_VC_TEMPLATE, credentialSubjectId, SOKRATES_DSP_CALLBACK)); oauthServer.start(OAUTH_PORT); oauthServer.setDispatcher(new KeycloakDispatcher()); diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/transfer/SsiHttpConsumerPullWithProxyInMemoryTest.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/transfer/SsiHttpConsumerPullWithProxyInMemoryTest.java index d026d47e4..38991a864 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/transfer/SsiHttpConsumerPullWithProxyInMemoryTest.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/tests/transfer/SsiHttpConsumerPullWithProxyInMemoryTest.java @@ -70,11 +70,13 @@ void setup() throws IOException { miwPlatoServer = new MockWebServer(); oauthServer = new MockWebServer(); + var credentialSubjectId = "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000"; + miwSokratesServer.start(MIW_SOKRATES_PORT); - miwSokratesServer.setDispatcher(new MiwDispatcher(SOKRATES_BPN, SUMMARY_VC_TEMPLATE, PLATO_DSP_CALLBACK)); + miwSokratesServer.setDispatcher(new MiwDispatcher(SOKRATES_BPN, SUMMARY_VC_TEMPLATE, credentialSubjectId, PLATO_DSP_CALLBACK)); miwPlatoServer.start(MIW_PLATO_PORT); - miwPlatoServer.setDispatcher(new MiwDispatcher(PLATO_BPN, SUMMARY_VC_TEMPLATE, SOKRATES_DSP_CALLBACK)); + miwPlatoServer.setDispatcher(new MiwDispatcher(PLATO_BPN, SUMMARY_VC_TEMPLATE, credentialSubjectId, SOKRATES_DSP_CALLBACK)); oauthServer.start(OAUTH_PORT); oauthServer.setDispatcher(new KeycloakDispatcher()); diff --git a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/token/MiwDispatcher.java b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/token/MiwDispatcher.java index c0c70f9b7..15ae67e4a 100644 --- a/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/token/MiwDispatcher.java +++ b/edc-tests/e2e-tests/src/test/java/org/eclipse/tractusx/edc/token/MiwDispatcher.java @@ -45,13 +45,16 @@ public class MiwDispatcher extends Dispatcher { private static final TypeManager MAPPER = new TypeManager(); - + private final String audience; + private final String credentialSubjectId; + private final Map summaryVc; - public MiwDispatcher(String bpn, String vcFile, String audience) { + public MiwDispatcher(String bpn, String vcFile, String credentialSubjectId, String audience) { this.audience = audience; + this.credentialSubjectId = credentialSubjectId; var json = format(readVcContent(vcFile), bpn); summaryVc = MAPPER.readValue(json, new TypeReference<>() { }); @@ -107,6 +110,7 @@ private MockResponse presentationValidationResponse() { private JWTClaimsSet createClaims(Instant exp, Map presentation) { return new JWTClaimsSet.Builder() + .issuer(credentialSubjectId) .claim("vp", presentation) .audience(audience) .expirationTime(Date.from(exp)) diff --git a/edc-tests/e2e-tests/src/test/resources/docker-compose.yml b/edc-tests/e2e-tests/src/test/resources/docker-compose.yml index 706c4aa72..25f0ae1ba 100644 --- a/edc-tests/e2e-tests/src/test/resources/docker-compose.yml +++ b/edc-tests/e2e-tests/src/test/resources/docker-compose.yml @@ -38,7 +38,7 @@ services: wallet: platform: linux/amd64 container_name: managed-identity-wallet - image: ghcr.io/catenax-ng/tx-managed-identity-wallets_miw_service:0.0.1-snapshot.2994d69 + image: ghcr.io/catenax-ng/tx-managed-identity-wallets_miw_service:latest-java-did-web ports: - "8080:8080" environment: diff --git a/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/CredentialsNamespaces.java b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/CredentialsNamespaces.java index 164511ef9..ecac8fc7a 100644 --- a/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/CredentialsNamespaces.java +++ b/spi/ssi-spi/src/main/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/CredentialsNamespaces.java @@ -31,4 +31,6 @@ public interface CredentialsNamespaces { String CX_USE_CASE_NS_V1 = CX_USE_CASE_NS + "/v1"; String CX_SUMMARY_CREDENTIAL = "SummaryCredential"; String CREDENTIAL_SUBJECT = W3C_VC_PREFIX + "#credentialSubject"; + String CREDENTIAL_ISSUER = W3C_VC_PREFIX + "#issuer"; + } diff --git a/spi/ssi-spi/src/testFixtures/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/SummaryCredential.java b/spi/ssi-spi/src/testFixtures/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/SummaryCredential.java index 7f190449b..207194ded 100644 --- a/spi/ssi-spi/src/testFixtures/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/SummaryCredential.java +++ b/spi/ssi-spi/src/testFixtures/java/org/eclipse/tractusx/edc/iam/ssi/spi/jsonld/SummaryCredential.java @@ -35,7 +35,7 @@ public interface SummaryCredential { "VerifiableCredential", "SummaryCredential" ], - "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", + "issuer": "did:web:issuer-a016-203-129-213-99.ngrok-free.app:BPNL000000000000", "issuanceDate": "2023-06-02T12:00:00Z", "expirationDate": "2022-06-16T18:56:59Z", "credentialSubject": { @@ -66,4 +66,83 @@ public interface SummaryCredential { ] } """; + + String SIMPLE_VP = """ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", + "type": [ + "VerifiableCredential" + ], + "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", + "issuanceDate": "2023-06-02T12:00:00Z", + "expirationDate": "2022-06-16T18:56:59Z", + "credentialSubject": { + "id": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000" + } + } + ] + } + """; + + String SUMMARY_VP_NO_HOLDER = """ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/2023/catenax/credentials/summary/v1" + ], + "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", + "type": [ + "VerifiableCredential", + "SummaryCredential" + ], + "issuer": "did:web:no-holder.ngrok-free.app:BPNL000000000000", + "issuanceDate": "2023-06-02T12:00:00Z", + "expirationDate": "2022-06-16T18:56:59Z", + "credentialSubject": { + "id": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000" + } + } + ] + } + """; + + String SUMMARY_VP_NO_SUBJECT = """ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1" + ], + "type": "VerifiablePresentation", + "verifiableCredential": [ + { + "@context": [ + "https://www.w3.org/2018/credentials/v1", + "https://w3id.org/2023/catenax/credentials/summary/v1" + ], + "id": "urn:uuid:12345678-1234-1234-1234-123456789abc", + "type": [ + "VerifiableCredential", + "SummaryCredential" + ], + "issuer": "did:web:a016-203-129-213-99.ngrok-free.app:BPNL000000000000", + "issuanceDate": "2023-06-02T12:00:00Z", + "expirationDate": "2022-06-16T18:56:59Z" + } + ] + } + """; }