Skip to content

Commit bc10787

Browse files
feat: implement VCDM2.0 JOSE issuance (#947)
* feat: implement VCDM2.0 JOSE issuance * add e2e test # Conflicts: # core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/process/IssuanceProcessManagerImpl.java * fix tests * simplify test -> parameterized * checkstyle * use flattened claims * cosmetics * cosmetics v2 * fix minor issues * fix test assertion * PR remarks
1 parent 9637b03 commit bc10787

7 files changed

Lines changed: 597 additions & 51 deletions

File tree

core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/IssuanceServicesExtension.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.eclipse.edc.issuerservice.issuance.attestation.AttestationPipelineImpl;
2323
import org.eclipse.edc.issuerservice.issuance.credentialdefinition.CredentialDefinitionServiceImpl;
2424
import org.eclipse.edc.issuerservice.issuance.generator.CredentialGeneratorRegistryImpl;
25+
import org.eclipse.edc.issuerservice.issuance.generator.JoseVcdm20CredentialGenerator;
2526
import org.eclipse.edc.issuerservice.issuance.generator.JwtCredentialGenerator;
2627
import org.eclipse.edc.issuerservice.issuance.mapping.IssuanceClaimsMapperImpl;
2728
import org.eclipse.edc.issuerservice.issuance.rule.CredentialRuleDefinitionEvaluatorImpl;
@@ -146,6 +147,7 @@ public CredentialGeneratorRegistry createCredentialGeneratorRegistry() {
146147

147148
var jwtGenerationService = new JwtGenerationService(jwsSignerProvider);
148149
generator.addGenerator(CredentialFormat.VC1_0_JWT, new JwtCredentialGenerator(jwtGenerationService, clock));
150+
generator.addGenerator(CredentialFormat.VC2_0_JOSE, new JoseVcdm20CredentialGenerator(jwtGenerationService, clock));
149151
return generator;
150152
}
151153

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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.issuerservice.issuance.generator;
16+
17+
public interface Constants {
18+
String VERIFIABLE_CREDENTIAL_CLAIM = "vc";
19+
String CREDENTIAL_SUBJECT = "credentialSubject";
20+
String CREDENTIAL_STATUS = "credentialStatus";
21+
String VERIFIABLE_CREDENTIAL = "VerifiableCredential";
22+
String TYPE = "type";
23+
String VALID_FROM = "validFrom";
24+
String ID = "id";
25+
String ISSUER = "issuer";
26+
String W3C_CREDENTIALS_URL_V2 = "https://www.w3.org/ns/credentials/v2";
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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.issuerservice.issuance.generator;
16+
17+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
18+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
19+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject;
20+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.DataModelVersion;
21+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
22+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
23+
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredentialContainer;
24+
import org.eclipse.edc.issuerservice.spi.issuance.generator.CredentialGenerator;
25+
import org.eclipse.edc.issuerservice.spi.issuance.model.CredentialDefinition;
26+
import org.eclipse.edc.jsonld.spi.JsonLdKeywords;
27+
import org.eclipse.edc.spi.iam.TokenRepresentation;
28+
import org.eclipse.edc.spi.result.Result;
29+
import org.eclipse.edc.token.spi.KeyIdDecorator;
30+
import org.eclipse.edc.token.spi.TokenDecorator;
31+
import org.eclipse.edc.token.spi.TokenGenerationService;
32+
33+
import java.time.Clock;
34+
import java.time.Instant;
35+
import java.util.ArrayList;
36+
import java.util.Arrays;
37+
import java.util.Date;
38+
import java.util.HashMap;
39+
import java.util.List;
40+
import java.util.Map;
41+
import java.util.UUID;
42+
43+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.CREDENTIAL_STATUS;
44+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.CREDENTIAL_SUBJECT;
45+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.ID;
46+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.ISSUER;
47+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.TYPE;
48+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.VALID_FROM;
49+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.VERIFIABLE_CREDENTIAL;
50+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.W3C_CREDENTIALS_URL_V2;
51+
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
52+
53+
public class JoseVcdm20CredentialGenerator implements CredentialGenerator {
54+
55+
private final TokenGenerationService tokenGenerationService;
56+
private final Clock clock;
57+
58+
public JoseVcdm20CredentialGenerator(TokenGenerationService tokenGenerationService, Clock clock) {
59+
this.tokenGenerationService = tokenGenerationService;
60+
this.clock = clock;
61+
}
62+
63+
@Override
64+
public Result<VerifiableCredentialContainer> generateCredential(String participantContextId, CredentialDefinition definition,
65+
String privateKeyAlias, String publicKeyId, String issuerId,
66+
String holderDid, Map<String, Object> claims) {
67+
68+
var subjectResult = extractCredentialSubject(claims);
69+
if (subjectResult.failed()) {
70+
return subjectResult.mapFailure();
71+
}
72+
73+
var statusResult = createCredentialStatus(claims);
74+
75+
var ctx = new ArrayList<>();
76+
ctx.add(W3C_CREDENTIALS_URL_V2);
77+
ctx.addAll(definition.getAdditionalContext());
78+
//noinspection unchecked
79+
var builder = VerifiableCredential.Builder.newInstance()
80+
.id(UUID.randomUUID().toString())
81+
.issuer(new Issuer(issuerId))
82+
.contexts(ctx)
83+
.dataModelVersion(DataModelVersion.V_2_0)
84+
.issuanceDate(Instant.now(clock))
85+
.expirationDate(Instant.now(clock).plusSeconds(definition.getValidity()))
86+
.types(List.of(VERIFIABLE_CREDENTIAL, definition.getCredentialType()))
87+
.credentialSubject(CredentialSubject.Builder.newInstance()
88+
.id(holderDid)
89+
.claims(subjectResult.getContent())
90+
.build());
91+
92+
statusResult.onSuccess(builder::credentialStatus);
93+
94+
var credential = builder.build();
95+
96+
return signCredentialInternal(participantContextId, credential, privateKeyAlias, publicKeyId, issuerId, VERIFIABLE_CREDENTIAL, definition.getCredentialType())
97+
.map(token -> new VerifiableCredentialContainer(token, CredentialFormat.VC2_0_JOSE, credential));
98+
}
99+
100+
@Override
101+
public Result<String> signCredential(String participantContextId, VerifiableCredential credential, String privateKeyAlias,
102+
String publicKeyId) {
103+
104+
var issuerId = credential.getIssuer().id();
105+
var type = credential.getType().toArray(new String[0]);
106+
107+
return signCredentialInternal(participantContextId, credential, privateKeyAlias, publicKeyId, issuerId, type);
108+
}
109+
110+
private Result<String> signCredentialInternal(String participantContextId, VerifiableCredential credential, String privateKeyAlias, String publicKeyId, String issuerId, String... types) {
111+
var composedKeyId = publicKeyId.startsWith(issuerId)
112+
? publicKeyId
113+
: issuerId + "#" + publicKeyId;
114+
115+
TokenDecorator decorator = tokenBuilder -> {
116+
var vcClaim = createVcClaim(credential, types);
117+
vcClaim.forEach(tokenBuilder::claims);
118+
119+
if (credential.getExpirationDate() != null) {
120+
tokenBuilder.claims(EXPIRATION_TIME, Date.from(credential.getExpirationDate()));
121+
}
122+
123+
return tokenBuilder;
124+
};
125+
126+
return tokenGenerationService
127+
.generate(participantContextId, privateKeyAlias, decorator, new KeyIdDecorator(composedKeyId))
128+
.map(TokenRepresentation::getToken);
129+
}
130+
131+
private Map<String, Object> createVcClaim(VerifiableCredential credential, String... types) {
132+
133+
var claims = new HashMap<>(
134+
Map.of(JsonLdKeywords.CONTEXT, List.of("https://www.w3.org/ns/credentials/v2"),
135+
TYPE, Arrays.asList(types),
136+
ID, credential.getId(),
137+
VALID_FROM, credential.getIssuanceDate().toString(),
138+
ISSUER, credential.getIssuer().id(),
139+
CREDENTIAL_SUBJECT, credentialSubjectClaims(credential)
140+
));
141+
if (credential.getExpirationDate() != null) {
142+
claims.put("validUntil", credential.getExpirationDate().toString());
143+
}
144+
if (credential.getDescription() != null) {
145+
claims.put("description", credential.getDescription());
146+
}
147+
if (credential.getName() != null) {
148+
claims.put("name", credential.getName());
149+
}
150+
151+
var status = credentialStatusClaims(credential);
152+
if (!status.isEmpty()) {
153+
claims.put(CREDENTIAL_STATUS, status);
154+
}
155+
return claims;
156+
}
157+
158+
private List<Map<String, Object>> credentialSubjectClaims(VerifiableCredential verifiableCredential) {
159+
return verifiableCredential.getCredentialSubject().stream().map(CredentialSubject::getClaims).toList();
160+
}
161+
162+
@SuppressWarnings("unchecked")
163+
private Result<Map<String, Object>> extractCredentialSubject(Map<String, Object> claims) {
164+
if (!claims.containsKey(CREDENTIAL_SUBJECT)) {
165+
return Result.failure("Missing credentialSubject in claims");
166+
}
167+
return Result.success((Map<String, Object>) claims.get(CREDENTIAL_SUBJECT));
168+
}
169+
170+
private Map<String, Object> credentialStatusClaims(VerifiableCredential verifiableCredential) {
171+
if (verifiableCredential.getCredentialStatus().isEmpty()) {
172+
return Map.of();
173+
}
174+
var status = verifiableCredential.getCredentialStatus().get(0);
175+
var statusMap = new HashMap<String, Object>(Map.of(ID, status.id(),
176+
"type", status.type()));
177+
statusMap.putAll(status.additionalProperties());
178+
return statusMap;
179+
}
180+
181+
@SuppressWarnings("unchecked")
182+
private Result<CredentialStatus> createCredentialStatus(Map<String, Object> claims) {
183+
if (!claims.containsKey(CREDENTIAL_STATUS)) {
184+
return Result.failure("no credentialStatus in claims");
185+
}
186+
var statusClaims = (Map<String, Object>) claims.get(CREDENTIAL_STATUS);
187+
188+
return Result.success(new CredentialStatus((String) statusClaims.get(ID),
189+
(String) statusClaims.get("type"),
190+
statusClaims));
191+
}
192+
}

core/issuerservice/issuerservice-issuance/src/main/java/org/eclipse/edc/issuerservice/issuance/generator/JwtCredentialGenerator.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@
4242
import java.util.Map;
4343
import java.util.UUID;
4444

45+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.CREDENTIAL_STATUS;
46+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.CREDENTIAL_SUBJECT;
47+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.TYPE;
48+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.VERIFIABLE_CREDENTIAL;
49+
import static org.eclipse.edc.issuerservice.issuance.generator.Constants.VERIFIABLE_CREDENTIAL_CLAIM;
4550
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.EXPIRATION_TIME;
4651
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUED_AT;
4752
import static org.eclipse.edc.jwt.spi.JwtRegisteredClaimNames.ISSUER;
@@ -51,11 +56,7 @@
5156

5257
public class JwtCredentialGenerator implements CredentialGenerator {
5358

54-
public static final String VERIFIABLE_CREDENTIAL_CLAIM = "vc";
55-
public static final String CREDENTIAL_SUBJECT = "credentialSubject";
56-
public static final String CREDENTIAL_STATUS = "credentialStatus";
57-
public static final String VERIFIABLE_CREDENTIAL = "VerifiableCredential";
58-
public static final String TYPE_PROPERTY = "type";
59+
5960
private final TokenGenerationService tokenGenerationService;
6061
private final Clock clock;
6162

@@ -115,7 +116,7 @@ private Result<String> signCredentialInternal(String participantContextId, Verif
115116
.map(TokenRepresentation::getToken);
116117
}
117118

118-
@SuppressWarnings({"unchecked", "rawtypes"})
119+
@SuppressWarnings({ "unchecked", "rawtypes" })
119120
private VerifiableCredential.Builder generateVerifiableCredential(List<String> additionalContext, String type, long validity, String issuer, String holderId, Map<String, Object> credentialSubject) {
120121
var ctx = new LinkedHashSet<>();
121122
ctx.add(VcConstants.W3C_CREDENTIALS_URL);
@@ -160,7 +161,7 @@ private Map<String, Object> createVcClaim(VerifiableCredential verifiableCredent
160161
ctx.addAll(verifiableCredential.getContext());
161162
var claims = new HashMap<>(
162163
Map.of(JsonLdKeywords.CONTEXT, ctx,
163-
TYPE_PROPERTY, Arrays.asList(type),
164+
TYPE, Arrays.asList(type),
164165
"id", verifiableCredential.getId(),
165166
"issuanceDate", verifiableCredential.getIssuanceDate().toString(),
166167
"issuer", verifiableCredential.getIssuer().id(),

0 commit comments

Comments
 (0)