Skip to content

Commit a5f2b7c

Browse files
authored
feat: virtual transfer e2e using DCP protocol (#5699)
* feat: virtual transfer e2e using DCP protocol * pr suggestions
1 parent 232850e commit a5f2b7c

15 files changed

Lines changed: 1065 additions & 29 deletions

File tree

dist/bom/controlplane-dcp-bom/build.gradle.kts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,5 @@ plugins {
1919
dependencies {
2020
// SPI dependencies
2121
api(project(":dist:bom:controlplane-base-bom"))
22-
23-
// DCP dependencies, JWT and LDP
24-
api(project(":spi:common:jwt-spi"))
25-
26-
api(project(":extensions:common:crypto:ldp-verifiable-credentials"))
27-
api(project(":extensions:common:crypto:jwt-verifiable-credentials"))
28-
api(project(":extensions:common:crypto:lib:jws2020-lib"))
29-
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-core"))
30-
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-issuers-configuration"))
31-
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-service"))
32-
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-transform"))
33-
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-sts:decentralized-claims-sts-remote-client"))
34-
api(project(":extensions:common:iam:verifiable-credentials"))
35-
36-
api(project(":extensions:common:iam:decentralized-identity"))
37-
api(project(":extensions:common:iam:oauth2:oauth2-client"))
22+
api(project(":dist:bom:controlplane-feature-dcp-bom"))
3823
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2026 Metaform Systems, Inc.
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+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
plugins {
16+
`java-library`
17+
}
18+
19+
dependencies {
20+
// DCP dependencies, JWT and LDP
21+
api(project(":spi:common:jwt-spi"))
22+
23+
api(project(":extensions:common:crypto:ldp-verifiable-credentials"))
24+
api(project(":extensions:common:crypto:jwt-verifiable-credentials"))
25+
api(project(":extensions:common:crypto:lib:jws2020-lib"))
26+
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-core"))
27+
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-issuers-configuration"))
28+
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-service"))
29+
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-transform"))
30+
api(project(":extensions:common:iam:decentralized-claims:decentralized-claims-sts:decentralized-claims-sts-remote-client"))
31+
api(project(":extensions:common:iam:verifiable-credentials"))
32+
33+
api(project(":extensions:common:iam:decentralized-identity"))
34+
api(project(":extensions:common:iam:oauth2:oauth2-client"))
35+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,7 @@ include(":system-tests:e2e-federatedcatalog-tests:end2end-test:e2e-junit-runner"
364364
include(":dist:bom:controlplane-base-bom")
365365
include(":dist:bom:controlplane-dcp-bom")
366366
include(":dist:bom:controlplane-feature-sql-bom")
367+
include(":dist:bom:controlplane-feature-dcp-bom")
367368
include(":dist:bom:federatedcatalog-base-bom")
368369
include(":dist:bom:federatedcatalog-dcp-bom")
369370
include(":dist:bom:federatedcatalog-feature-sql-bom")

spi/common/decentralized-claims-spi/build.gradle.kts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,13 @@ dependencies {
3535
testImplementation(testFixtures(project(":spi:common:verifiable-credentials-spi")))
3636

3737
testFixturesImplementation(libs.nimbus.jwt)
38+
testFixturesImplementation(libs.wiremock) {
39+
exclude("com.networknt", "json-schema-validator")
40+
}
41+
42+
testFixturesImplementation(project(":spi:common:identity-did-spi"))
43+
testFixturesImplementation(project(":core:common:junit"))
44+
3845
}
3946

4047

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
* Copyright (c) 2026 Metaform Systems, Inc.
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+
* Metaform Systems, Inc. - initial API and implementation
12+
*
13+
*/
14+
15+
package org.eclipse.edc.iam.decentralizedclaims.spi.credentialservice;
16+
17+
import com.nimbusds.jose.JOSEException;
18+
import com.nimbusds.jose.JWSAlgorithm;
19+
import com.nimbusds.jose.JWSHeader;
20+
import com.nimbusds.jose.crypto.ECDSASigner;
21+
import com.nimbusds.jose.jwk.Curve;
22+
import com.nimbusds.jose.jwk.ECKey;
23+
import com.nimbusds.jose.jwk.gen.ECKeyGenerator;
24+
import com.nimbusds.jwt.JWTClaimsSet;
25+
import com.nimbusds.jwt.SignedJWT;
26+
import org.eclipse.edc.iam.did.spi.document.DidDocument;
27+
import org.eclipse.edc.iam.did.spi.document.Service;
28+
import org.eclipse.edc.iam.did.spi.document.VerificationMethod;
29+
import org.jspecify.annotations.NonNull;
30+
31+
import java.time.Instant;
32+
import java.util.ArrayList;
33+
import java.util.Date;
34+
import java.util.HashMap;
35+
import java.util.List;
36+
import java.util.Map;
37+
import java.util.UUID;
38+
39+
/**
40+
* Test fixture that simulates a multi-participant Credential Service with resolvable {@code did:web}
41+
* DIDs and mock presentation query endpoints.
42+
* <p>
43+
* Each participant context has its own EC key pair, DID, DID document endpoint, credential storage,
44+
* and presentation query endpoint. Participants are registered via {@link #addParticipant(String)}.
45+
*/
46+
public class CredentialService {
47+
48+
private final Map<String, ParticipantContext> participants = new HashMap<>();
49+
private final Integer port;
50+
51+
public CredentialService(Integer port) {
52+
this.port = port;
53+
}
54+
55+
private static ECKey generateKey(String participantContextId, String did) {
56+
try {
57+
return new ECKeyGenerator(Curve.P_256)
58+
.keyID(did + "#key1")
59+
.generate();
60+
} catch (JOSEException e) {
61+
throw new RuntimeException("Failed to generate EC key for participant: " + participantContextId, e);
62+
}
63+
}
64+
65+
/**
66+
* Registers a new participant context. Generates an EC key pair, creates a DID document,
67+
* and stubs the DID resolution and presentation query endpoints for this participant.
68+
*
69+
* @param participantContextId the participant context identifier
70+
*/
71+
public void addParticipant(String participantContextId) {
72+
if (participants.containsKey(participantContextId)) {
73+
throw new IllegalArgumentException("Participant already registered: " + participantContextId);
74+
}
75+
76+
var did = didFor(participantContextId);
77+
var credentialServiceUrl = "http://localhost:" + port + "/credentials/" + participantContextId;
78+
79+
var ecKey = generateKey(participantContextId, did);
80+
81+
var didDocument = DidDocument.Builder.newInstance()
82+
.id(did)
83+
.verificationMethod(List.of(
84+
VerificationMethod.Builder.newInstance()
85+
.id(ecKey.getKeyID())
86+
.type("JsonWebKey2020")
87+
.controller(did)
88+
.publicKeyJwk(ecKey.toPublicJWK().toJSONObject())
89+
.build()
90+
))
91+
.service(List.of(new Service(
92+
UUID.randomUUID().toString(),
93+
"CredentialService",
94+
credentialServiceUrl)))
95+
.build();
96+
97+
var ctx = new ParticipantContext(didDocument, ecKey);
98+
participants.put(participantContextId, ctx);
99+
}
100+
101+
public @NonNull String didFor(String participantContextId) {
102+
return "did:web:localhost%3A" + port + ":" + participantContextId;
103+
}
104+
105+
public DidDocument getDidDocument(String participantContextId) {
106+
return getParticipant(participantContextId).document;
107+
}
108+
109+
public String createStsToken(String participantContextId, String audience, String bearerAccessScope, String bearerAccessToken) {
110+
var ctx = getParticipant(participantContextId);
111+
return createStsToken(ctx, audience, bearerAccessScope, bearerAccessToken);
112+
}
113+
114+
/**
115+
* Stores a verifiable credential JWT for the given participant and updates the presentation endpoint response.
116+
*
117+
* @param participantContextId the participant context identifier
118+
* @param vcJwt the serialized JWT string of the verifiable credential
119+
*/
120+
public void storeCredential(String participantContextId, String vcJwt) {
121+
var ctx = getParticipant(participantContextId);
122+
ctx.storedCredentials.add(vcJwt);
123+
}
124+
125+
private ParticipantContext getParticipant(String participantContextId) {
126+
var ctx = participants.get(participantContextId);
127+
if (ctx == null) {
128+
throw new IllegalArgumentException("Unknown participant: " + participantContextId);
129+
}
130+
return ctx;
131+
}
132+
133+
private String createStsToken(ParticipantContext ctx, String audience, String bearerAccessScope, String bearerAccessToken) {
134+
try {
135+
var now = Date.from(Instant.now());
136+
var claimsSet = new JWTClaimsSet.Builder()
137+
.issuer(ctx.document.getId())
138+
.subject(ctx.document.getId())
139+
.jwtID(UUID.randomUUID().toString())
140+
.issueTime(now)
141+
.notBeforeTime(now)
142+
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
143+
.audience(audience);
144+
145+
if (bearerAccessScope != null) {
146+
claimsSet.claim("token", bearerAccessScope);
147+
}
148+
if (bearerAccessToken != null) {
149+
claimsSet.claim("token", bearerAccessToken);
150+
}
151+
152+
var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
153+
.keyID(ctx.ecKey.getKeyID())
154+
.build();
155+
156+
var signedJwt = new SignedJWT(header, claimsSet.build());
157+
signedJwt.sign(new ECDSASigner(ctx.ecKey));
158+
159+
return signedJwt.serialize();
160+
} catch (JOSEException e) {
161+
throw new RuntimeException("Failed to sign VP JWT", e);
162+
}
163+
}
164+
165+
public String createVpJwt(String participantContextId, String audience) {
166+
var ctx = getParticipant(participantContextId);
167+
return createVpJwt(ctx, audience);
168+
}
169+
170+
private String createVpJwt(ParticipantContext ctx, String audience) {
171+
try {
172+
var vpClaims = Map.of(
173+
"@context", List.of("https://www.w3.org/2018/credentials/v1"),
174+
"type", List.of("VerifiablePresentation"),
175+
"verifiableCredential", List.copyOf(ctx.storedCredentials)
176+
);
177+
178+
var now = Date.from(Instant.now());
179+
var claimsSet = new JWTClaimsSet.Builder()
180+
.issuer(ctx.document.getId())
181+
.subject(ctx.document.getId())
182+
.jwtID(UUID.randomUUID().toString())
183+
.issueTime(now)
184+
.notBeforeTime(now)
185+
.expirationTime(Date.from(Instant.now().plusSeconds(300)))
186+
.claim("vp", vpClaims)
187+
.audience(audience)
188+
.build();
189+
190+
var header = new JWSHeader.Builder(JWSAlgorithm.ES256)
191+
.keyID(ctx.ecKey.getKeyID())
192+
.build();
193+
194+
var signedJwt = new SignedJWT(header, claimsSet);
195+
signedJwt.sign(new ECDSASigner(ctx.ecKey));
196+
197+
return signedJwt.serialize();
198+
} catch (JOSEException e) {
199+
throw new RuntimeException("Failed to sign VP JWT", e);
200+
}
201+
}
202+
203+
private static class ParticipantContext {
204+
private final DidDocument document;
205+
private final ECKey ecKey;
206+
private final List<String> storedCredentials = new ArrayList<>();
207+
208+
ParticipantContext(DidDocument document, ECKey ecKey) {
209+
this.document = document;
210+
this.ecKey = ecKey;
211+
}
212+
}
213+
}

0 commit comments

Comments
 (0)