Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion")
implementation("org.eclipse.tractusx.edc:control-plane-migration:$txVersion")
implementation("org.eclipse.tractusx.edc:tx-dcp:$txVersion")
implementation(project(":edc-extensions:dynamic-issuers"))
}

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation("org.eclipse.tractusx.edc:retirement-evaluation-store-sql:$txVersion")
implementation("org.eclipse.tractusx.edc:control-plane-migration:$txVersion")
implementation("org.eclipse.tractusx.edc:tx-dcp:$txVersion")
implementation(project(":edc-extensions:dynamic-issuers"))
}

tasks.withType<com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar> {
Expand Down
64 changes: 64 additions & 0 deletions edc-extensions/dynamic-issuers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
## Dynamic trusted issuers extension

This extension enables you to let your controlplane fetch information about trusted issuers. In order for this to work,
your dataspace organisation needs to have set up a trust server. In essence, this extension provides an alternative
implementation of a TrustedIssuerRegistry, which replaces the DefaultTrustedIssuerRegistry.

You can provide the trust server's URL via an environment property like in this example:

```
edc.iam.trustserver.url=https://my-trustserver.com/trusted-issuers
```

You can omit this property entirely. In that case, no attempts to fetch updates will be made and this extension will
simply behave like the DefaultTrustedIssuerRegistry from the upstream EDC.

Also note, that this extension does in general not interfere with the TrustedIssuerConfigurationExtension. I.e. you can
still define trusted issuers purely via your env-properties. Conflicts can only arise, when an issuer-did:web-id, which
you provided via property, is also known to the remote trusted issuer server. In that case, the information from the
remote server will take precedence and override anything conflicting that may have been given in your properties.

The rationale is, that the remote server (which is administered by the dataspace governance organization) is expected to have more recent information.

### The information payload

The response body from the trust server is expected to be in this format:

```json
{
"interval": 30,
"issuerdata": [
{
"id": "did:web:local-issuer-wallet:con-x-issuer",
"supportedTypes": [
"https://my-domain.com/credentials/essential/MembershipCredential",
"https://my-domain.com/credentials/other/FooCredential"
]
}
]
}
```

At first, the body may contain an "interval" field. This allows the remote server to tell this extension, after how many
seconds a new update request should be made. The idea is, that the central trust server may have load-balancing-related
reasons to lower the intensity of incoming requests from his multiple clients. This field is however optional. If the trust
server chooses not to use this field, then the default interval (usually 2 hours) will be applied.

Beyond that, the response body must always have an "issuerdata" field, that supplies an array of objects. Each of these
objects must have an "id" field, that states the did:web-id of one particular issuer. And it needs to have a "supportedTypes" field with an array value attached. This array must contain strings of Credential-Types, for which this issuer is authorized to create credentials.

Note that an asterisk symbol ("*") in this array would be interpreted as a wildcard. I.e. an issuer-id that is equipped with
such a wild card is assumed to be allowed to create ANY CredentialType, so caution is advised here.

### Configuring the default update interval

You can override the above-mentioned default interval using this property:

```
edc.iam.trustserver.default.interval=3600
```
The given value sets the amount of seconds, i.e. 3600 seconds would equal one hour. Please note that this value will only
ever be used if the remote server chooses not to use to "interval" field in his response (see above).

Aside from that, a special initialization interval of 30 seconds will be used on bootup, as long as your controlplane has not
managed to establish a connection to the trust server at least once.
36 changes: 36 additions & 0 deletions edc-extensions/dynamic-issuers/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST)
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

plugins {
id("java")
id("application")
}

repositories { mavenCentral() }

val edcVersion = "0.15.1"

dependencies {
implementation("org.eclipse.edc:core-spi:${edcVersion}")
implementation("org.eclipse.edc:verifiable-credential-spi:${edcVersion}")

testImplementation("org.eclipse.edc:junit:${edcVersion}") {
exclude(group = "org.junit.jupiter")
exclude(group = "org.junit.platform")
exclude(group = "org.junit")
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST)
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package de.fraunhofer.isst.edc.extension.dynamic_issuers;

import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry;
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 java.net.URI;

@Extension("Dynamic Issuers Registry Extension")
public class DynamicIssuersExtension implements ServiceExtension {

public static final long DEFAULT_UPDATE_INTERVAL = 7200L; // 2 hours

@Setting(description = "The trust server URL to be used", required = false, key = "edc.iam.trustserver.url")
private String trustServerUrl;

@Setting(description = "The default interval between calls to the trust server", required = false, key = "edc.iam.trustserver.default.interval")
private String trustServerDefaultInterval;

@Provider
public TrustedIssuerRegistry provideDynamicTrustedIssuerRegistry(ServiceExtensionContext context) {
var localMonitor = context.getMonitor().withPrefix(this.getClass().getSimpleName());
URI serverUri;
try {
serverUri = URI.create(trustServerUrl);
} catch (Exception e) {
localMonitor.warning("Could not parse value of edc.iam.trustserver.url: " + trustServerUrl);
serverUri = null;
}
localMonitor.info("Using Trust Server: " + trustServerUrl);

long defaultInterval;
try {
defaultInterval = Long.parseLong(trustServerDefaultInterval);
} catch (NumberFormatException e) {
defaultInterval = DEFAULT_UPDATE_INTERVAL;
}
localMonitor.info("Using Default Interval: " + defaultInterval + " seconds");

return new DynamicTrustedIssuerRegistry(serverUri, context.getMonitor(), defaultInterval);
}

@Override
public String name() {
return "Dynamic Issuers Registry Extension";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/*
* Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST)
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* 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.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/

package de.fraunhofer.isst.edc.extension.dynamic_issuers;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry;
import org.eclipse.edc.spi.monitor.Monitor;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class DynamicTrustedIssuerRegistry implements TrustedIssuerRegistry {

private static final ObjectMapper MAPPER = new ObjectMapper();
private static final TypeReference<Set<String>> STRING_SET_TYPEREFERENCE = new TypeReference<>() {
};
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
private static final ScheduledExecutorService SCHEDULER =
Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "dynamic-trusted-issuer-worker");
t.setDaemon(true);
return t;
});

private final Map<String, Set<String>> store = new HashMap<>();
private final Set<String> externalIssuers = new HashSet<>();
private final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();

private final Monitor monitor;
private final URI trustedIssuerServer;
private final long defaultInterval;
private boolean completedInitialCall = false;

public DynamicTrustedIssuerRegistry(URI trustedIssuerServer, Monitor monitor, long defaultInterval) {
this.defaultInterval = defaultInterval;
this.monitor = monitor.withPrefix(this.getClass().getSimpleName());
this.trustedIssuerServer = trustedIssuerServer;
if (trustedIssuerServer != null) {
SCHEDULER.schedule(this::fetchUpdateFromServer, 0, TimeUnit.SECONDS);
} else {
monitor.warning("Trust Server URL is null, no updates will be fetched.");
}

}

@Override
public void register(Issuer issuer, String credentialType) {
try {
LOCK.writeLock().lock();
monitor.debug("Registering " + issuer.id());
store.computeIfAbsent(issuer.id(), k -> new HashSet<>()).add(credentialType);
} finally {
LOCK.writeLock().unlock();
}
}

@Override
public Set<String> getSupportedTypes(Issuer issuer) {
try {
LOCK.readLock().lock();
monitor.debug("Providing supported types request for " + issuer.id());
return store.getOrDefault(issuer.id(), Set.of());
} finally {
LOCK.readLock().unlock();
}
}

private void fetchUpdateFromServer() {
long newInterval = completedInitialCall ? defaultInterval : 30;
try {
HttpRequest request = HttpRequest.newBuilder().uri(trustedIssuerServer).GET().build();
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
JsonNode responseJson = MAPPER.readTree(response.body());
monitor.debug("Got response from trusted issuer server: \n" + responseJson.toPrettyString());
try {
newInterval = responseJson.get("interval").asLong();
} catch (Exception e) {
}
JsonNode issuerData = responseJson.get("issuerdata");
handleUpdate(issuerData);
completedInitialCall = true;
} else {
monitor.warning("Unexpected response status from trusted issuer server " + response.statusCode());
}

} catch (java.net.ConnectException ce) {
monitor.warning("Failed to reach trusted issuer server " + trustedIssuerServer);
} catch (Exception e) {
monitor.warning("Unexpected error while trying to reach trusted issuer server", e);
} finally {
SCHEDULER.schedule(this::fetchUpdateFromServer, newInterval, TimeUnit.SECONDS);
monitor.debug("Next update scheduled in " + newInterval + " seconds");
}
}

private void handleUpdate(JsonNode update) {
try {
LOCK.writeLock().lock();
Set<String> foundIssuers = new HashSet<>();
if (update.isArray()) {
for (var item : update) {
try {
String id = item.get("id").asText();
Set<String> types = MAPPER.convertValue(item.get("supportedTypes"), STRING_SET_TYPEREFERENCE);
foundIssuers.add(id);
externalIssuers.add(id);
store.put(id, types);

} catch (Exception e) {
monitor.warning("Failure while handling array item: " + item.toPrettyString(), e);
}
}
Set<String> delta = new HashSet<>(externalIssuers);
delta.removeAll(foundIssuers);
delta.forEach(id -> {
monitor.warning("Removing trusted issuer " + id);
store.remove(id);
});
externalIssuers.removeAll(delta);
} else {
monitor.warning("Payload from trusted issuer server is not an array!");
}
} finally {
LOCK.writeLock().unlock();
}
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#################################################################################
# Copyright (c) 2026 Fraunhofer-Gesellschaft zur Foerderung der angewandten Forschung e.V. (represented by Fraunhofer ISST)
#
# See the NOTICE file(s) distributed with this work for additional
# information regarding copyright ownership.
#
# 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.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
# SPDX-License-Identifier: Apache-2.0
#################################################################################

de.fraunhofer.isst.edc.extension.dynamic_issuers.DynamicIssuersExtension
2 changes: 2 additions & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ include(":edc-extensions:agreements:retirement-evaluation-api")
include(":edc-extensions:agreements:retirement-evaluation-spi")
include(":edc-extensions:agreements:retirement-evaluation-store-sql")

include(":edc-extensions:dynamic-issuers")

// extensions - data plane
include(":edc-extensions:dataplane:dataplane-proxy:edc-dataplane-proxy-consumer-api")
include(":edc-extensions:dataplane:dataplane-util")
Expand Down