Skip to content

Commit 89e44b4

Browse files
committed
Added public and private key claims verification. Added iam-identity extension. Deleted iam-claims extension.
1 parent c06717d commit 89e44b4

File tree

32 files changed

+755
-383
lines changed

32 files changed

+755
-383
lines changed

commons/src/main/java/org/eclipse/edc/heleade/commons/verify/claims/Claims.java

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@
2626
import java.util.Objects;
2727

2828

29+
/**
30+
* The Claims class provides methods for verifying participant claims and signatures.
31+
* These methods are intended for use cases where participants' submitted claims
32+
* need validation against trusted claims (FC) or their authenticity needs to be proven
33+
* via digital signatures.
34+
*/
2935
public class Claims {
3036

3137
/**
@@ -37,24 +43,19 @@ public class Claims {
3743
* @param participantClaims the map containing the claims provided by the participant
3844
* @return true if the signature is valid
3945
*/
40-
public static boolean verifySignature(ObjectMapper mapper, String pem, String participantSignedClaims, Map<String, Object> participantClaims) {
46+
public static boolean verifySignature(ObjectMapper mapper, String pem, String participantSignedClaims, String participantClaims) {
4147
try {
42-
pem = pem.replaceAll("-+[A-Z ]+-+", "")
43-
.replaceAll("\\s", "");
44-
45-
byte[] decoded = Base64.getDecoder().decode(pem);
46-
47-
48+
byte[] decoded = Base64.getMimeDecoder().decode(pem);
4849
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decoded);
4950
KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
5051

5152
PublicKey publicKey = keyFactory.generatePublic(keySpec);
5253

53-
String claimsString = mapper.writeValueAsString(participantClaims);
54+
5455

5556
Signature signature = Signature.getInstance("Ed25519");
5657
signature.initVerify(publicKey);
57-
signature.update(claimsString.getBytes(StandardCharsets.UTF_8));
58+
signature.update(participantClaims.getBytes(StandardCharsets.UTF_8));
5859

5960
byte[] signedBytes = Base64.getDecoder().decode(participantSignedClaims);
6061

consumers/consumer-base/resources/configuration/consumer-base-configuration.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ web.http.public.path=/public
2929

3030
# claims
3131
edc.participant.claims=deployment/assets/consumer/creds.json
32-
32+
edc.participant.private.key=deployment/assets/consumer/ed25519_private.pem
3333
# identity hub
3434
#edc.participant.id=did:web:localhost%3A7083
3535
#edc.management.context.enabled=true

consumers/consumer/build.gradle.kts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ dependencies {
3030
implementation(libs.edc.dsp)
3131
implementation(libs.edc.http)
3232
implementation(libs.edc.configuration.filesystem)
33-
//implementation(libs.edc.iam.mock)
3433
implementation(libs.edc.management.api)
3534
implementation(libs.edc.transfer.data.plane.signaling)
3635
implementation(libs.edc.validator.data.address.http.data)
@@ -55,6 +54,6 @@ dependencies {
5554
implementation(libs.edc.auth.configuration)
5655

5756
implementation(project(":providers:content-based-catalog-dispatcher"))
58-
implementation(project(":providers:iam-claims"));
57+
implementation(project(":iam-identity"));
5958
implementation(project(":commons"))
6059
}
Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
11
{
22
"location": "eu",
3-
"entity_type": "public",
4-
"legal_name": "Official Consumer",
5-
"ip_connector": "219.208.53.217",
6-
"lei_code": "0202",
7-
"num_employees": "4000",
8-
"membership": {
9-
"level": "SILVER",
10-
"branch": "operator"
11-
}
3+
"entity_type": "public"
124
}
Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
11
{
22
"location": "eu",
3-
"entity_type": "private",
4-
"legal_name": "Official Provider",
5-
"ip_connector": "219.208.53.217",
6-
"lei_code": "0202",
7-
"membership": {
8-
"level": "SILVER",
9-
"branch": "operator"
10-
}
3+
"entity_type": "public"
114
}

federated-catalog/resources/configuration/fc-configuration.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ edc.dataplane.api.public.baseurl=http://localhost:39291/public
3838
# Configuración de la base de datos MongoDB
3939
org.eclipse.edc.heleade.federated.catalog.extension.store.mongodb.uri = mongodb://localhost:27017/
4040
org.eclipse.edc.heleade.federated.catalog.extension.store.mongodb.db = federatedcatalogdb
41-
#edc.participant.claims=deployment/assets/consumer/creds.json
41+
edc.participant.claims=deployment/assets/consumer/creds.json
42+
edc.participant.private.key=deployment/assets/consumer/ed25519_private.pem

federated-catalog/src/main/java/org/eclipse/edc/heleade/federated/catalog/extension/api/verification/VerificationApiController.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@ public JsonObject verify(String body) {
9898
String id = compactedBody.getString("participantId");
9999
String participantSignedClaims = compactedBody.getString("signedClaims");
100100
Map<String, Object> participantClaims = getMapFromJsonObject(compactedBody.getJsonObject("claims"));
101+
JsonObject participantClaimsJson = compactedBody.getJsonObject("claims");
102+
String claimsString = participantClaimsJson.toString();
101103

102104
// check the signature
103105
ParticipantNode participantNode = targetNodeDirectory.getParticipantNode(id);
104-
String pem = participantNode.security().get("pem");
105-
boolean verifySignatureSuccess = verifySignature(typeManager.getMapper(), pem, participantSignedClaims, participantClaims);
106+
String pem = participantNode.security().get("https://w3id.org/edc/v0.0.1/ns/pem");
107+
boolean verifySignatureSuccess = verifySignature(typeManager.getMapper(), pem, participantSignedClaims, claimsString);
106108
JsonObjectBuilder builder = Json.createObjectBuilder();
107109
builder.add("verifySignatureSuccess", verifySignatureSuccess);
108110

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
1-
## IAM CLAIMS
1+
## I AM - IDENTITY
22

33

4-
This extension generates and verifies the participant token and includes the participant claims inside it.
4+
This extension generates and verifies the participant token and includes the participant claims && participant
5+
signed claims, these claims are signed with the participant private key.
6+
----------------
7+
## In participant onboarding, participant needs:
58

6-
When the extension is initialized, it loads participant claims.
9+
- One private key → used only to sign
10+
- One public key → shared with participant registry to verify signatures
711

8-
By default, the claims are read from a local file, but this behavior can be replaced with other sources in the future.
12+
This works in the following way:
13+
14+
Participant(Consumer) 1:
15+
16+
- Signs data with their private key
17+
Sends (claims + signature) to provider
18+
19+
Participant (Provider) 2:
20+
- Looks up participant 1’s public key in participant registry.
21+
- Verifies the signature
22+
- Verifies the claims were created by participant 1
23+
- Verifies that claims were not modified
24+
-----
25+
26+
How to generate key pair
27+
28+
29+
30+
31+
``
32+
openssl genpkey -algorithm Ed25519 -out ed25519_private.pem
33+
``
34+
35+
``
36+
openssl pkey -in ed25519_private.pem -pubout -out ed25519_public.pem
37+
``
938

1039
#### Configuration
1140

@@ -30,13 +59,14 @@ Here’s an example structure:
3059
{
3160
"location": "eu",
3261
"entityType": "public",
33-
"membership": {
34-
"level": "SILVER",
35-
"branch": "operator"
36-
}
3762
}
3863
```
3964

65+
To specify the path containing the private key.
66+
67+
``
68+
edc.participant.private.key=<path-to-pem-file>
69+
``
4070
### Reminder
4171
When creating a participant—especially a consumer:
4272

@@ -55,15 +85,11 @@ Example:
5585
"id": "search-service",
5686
"supportedProtocols": [],
5787
"claims": {
58-
"membership": {
59-
"level": "SILVER",
60-
"branch": "operator"
61-
},
62-
"location": "eu"
88+
"location": "eu",
89+
"entityType": "public"
6390
},
64-
"attributes": {
65-
"role": "consumer with claims",
66-
"description": "consumer node"
91+
"security": {
92+
"pem": "MCowBQYDK2VwAyEA/UfM1mYj4c3y7P7AXigfjl08PXISX/0ixait4gflOQU="
6793
},
6894
"@context": {
6995
"@vocab": "https://w3id.org/edc/v0.0.1/ns/"
@@ -74,13 +100,13 @@ Example:
74100

75101
#### Important:
76102

77-
Since the Identity Hub was overly complex and not functioning correctly,
78-
we decided not to use it. Instead,
79-
this extension implements the IdentityService interface,
80-
with a small modification to include claims in the generated token so
103+
Since the Identity Hub was overly complex and not functioning correctly,
104+
we decided not to use it. Instead,
105+
this extension implements the IdentityService interface,
106+
with a small modification to include claims in the generated token so
81107
they are available to the participant agent.
82108
Because the Identity Service component in EDC is not well documented,
83-
much of this understanding
84-
comes from community issues and discussions on Discord.
85-
In the future, we can—and should—expand this implementation to
109+
much of this understanding
110+
comes from community issues and discussions on Discord.
111+
In the future, we can—and should—expand this implementation to
86112
further improve identity handling.
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023 Fraunhofer Institute for Software and Systems Engineering
2+
* Copyright (c) 2025 Universidad de Alicante
33
*
44
* This program and the accompanying materials are made available under the
55
* terms of the Apache License, Version 2.0 which is available at
@@ -8,10 +8,9 @@
88
* SPDX-License-Identifier: Apache-2.0
99
*
1010
* Contributors:
11-
* Fraunhofer Institute for Software and Systems Engineering - initial API and implementation
11+
* MO - Universidad de Alicante - initial implementation
1212
*
1313
*/
14-
1514
plugins {
1615
`java-library`
1716
id("application")
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/*
2+
* Copyright (c) 2025 Universidad de Alicante
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Apache License, Version 2.0 which is available at
6+
* https://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* SPDX-License-Identifier: Apache-2.0
9+
*
10+
* Contributors:
11+
* MO - Universidad de Alicante - initial implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.identity.extension;
16+
17+
import com.fasterxml.jackson.core.type.TypeReference;
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import org.eclipse.edc.spi.monitor.Monitor;
20+
21+
import java.io.File;
22+
import java.nio.charset.StandardCharsets;
23+
import java.nio.file.Files;
24+
import java.nio.file.Path;
25+
import java.nio.file.Paths;
26+
import java.security.KeyFactory;
27+
import java.security.PrivateKey;
28+
import java.security.Signature;
29+
import java.security.spec.PKCS8EncodedKeySpec;
30+
import java.util.Base64;
31+
import java.util.Map;
32+
33+
/**
34+
* Implementation of the {@link ParticipantIdentityLoader} interface that provides
35+
* mechanisms for loading claims and private keys from files, as well as signing
36+
* claims using a private key.
37+
* This implementation primarily works with JSON-encoded claims files and PEM-encoded
38+
* private keys in PKCS #8 format. It also provides logging for diagnostics and error handling.
39+
*/
40+
public class FileParticipantIdentityLoader implements ParticipantIdentityLoader {
41+
/**
42+
* An instance of {@link ObjectMapper} used for serializing and deserializing JSON data.
43+
* It is a core dependency for converting objects to JSON and vice versa during
44+
* various operations like loading claims, signing claims, or processing file contents
45+
* related to participant identity.
46+
*/
47+
private final ObjectMapper mapper;
48+
/**
49+
* Provides a logging mechanism for the {@link FileParticipantIdentityLoader} class.
50+
* This monitor is used to log warnings, errors, and other relevant information
51+
* during operations such as file loading, private key parsing, and claims signing.
52+
* It ensures proper observability and aids in debugging any issues that arise
53+
* in the identity loader process.
54+
*/
55+
private final Monitor monitor;
56+
57+
58+
/**
59+
* Constructs a new instance of FileParticipantIdentityLoader.
60+
*
61+
* @param monitor the monitor instance used for logging and diagnostics
62+
* @param mapper the object mapper used for JSON serialization and deserialization
63+
*/
64+
public FileParticipantIdentityLoader(Monitor monitor, ObjectMapper mapper) {
65+
this.monitor = monitor;
66+
this.mapper = mapper;
67+
}
68+
69+
/**
70+
* Loads a set of claims from a file located at the specified path.
71+
* The file is expected to contain a JSON representation of the claims.
72+
* If an error occurs while reading or parsing the file, a warning is logged,
73+
* and an empty map is returned.
74+
*
75+
* @param path the file system path to the claims file
76+
* @return a map representing the loaded claims, or an empty map if an error occurs
77+
*/
78+
@Override
79+
public Map<String, Object> loadClaims(String path) {
80+
var file = new File(path);
81+
try {
82+
monitor.debug("Trying to load claims from: " + file.getAbsolutePath());
83+
return mapper.readValue(file, new TypeReference<Map<String, Object>>() {});
84+
} catch (Exception e) {
85+
monitor.warning("Error reading claims file: " + file.getAbsolutePath(), e);
86+
return Map.of();
87+
}
88+
}
89+
90+
/**
91+
* Loads a private key from a PEM-encoded file located at the specified path.
92+
* The key is expected to be in PKCS #8 format and encoded for the Ed25519 algorithm.
93+
* If an error occurs (e.g., the file is not found, the content is invalid, or an unsupported
94+
* algorithm is specified), the method logs the error and returns {@code null}.
95+
*
96+
* @param path the file system path to the PEM-encoded private key
97+
* @return the {@link PrivateKey} instance if the key is successfully loaded, or {@code null} if an error occurs
98+
*/
99+
@Override
100+
public PrivateKey loadPrivateKey(String path) {
101+
Path filePath = Paths.get(path);
102+
try {
103+
104+
String content = new String(Files.readAllBytes(filePath));
105+
String pem = content
106+
.replaceAll("-+[A-Z ]+-+", "")
107+
.replaceAll("\\s", "");
108+
109+
byte[] keyBytes = Base64.getDecoder().decode(pem);
110+
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
111+
112+
KeyFactory keyFactory = KeyFactory.getInstance("Ed25519");
113+
return keyFactory.generatePrivate(spec);
114+
} catch (Exception e) {
115+
monitor.severe(String.format("Error loading private key from %s", filePath));
116+
return null;
117+
}
118+
}
119+
120+
/**
121+
* Signs the provided claims using the provided private key and returns the signed claims as a Base64-encoded string.
122+
* If an error occurs during the signing process, a warning is logged, and the method returns {@code null}.
123+
*
124+
* @param claims the claims to be signed, represented as a map of key-value pairs
125+
* @param privateKey the private key used to sign the claims
126+
* @param monitor the monitor used for logging warnings or errors
127+
* @return the signed claims as a Base64-encoded string, or {@code null} if an error occurs during signing
128+
*/
129+
public String signClaims(Map<String, Object> claims, PrivateKey privateKey, Monitor monitor) {
130+
try {
131+
String claimsString = mapper.writeValueAsString(claims);
132+
133+
Signature signature = Signature.getInstance("Ed25519");
134+
signature.initSign(privateKey);
135+
signature.update(claimsString.getBytes(StandardCharsets.UTF_8));
136+
137+
byte[] signedBytes = signature.sign();
138+
return Base64.getEncoder().encodeToString(signedBytes);
139+
} catch (Exception e) {
140+
monitor.warning("Claims could not be signed: " + e.getMessage(), e);
141+
return null;
142+
}
143+
}
144+
145+
}

0 commit comments

Comments
 (0)