Skip to content

Commit 14385c3

Browse files
authored
feat: additional JSON-LD context on issuance (#941)
1 parent b0ea192 commit 14385c3

11 files changed

Lines changed: 139 additions & 15 deletions

File tree

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

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333

3434
import java.time.Clock;
3535
import java.time.Instant;
36+
import java.util.ArrayList;
3637
import java.util.Arrays;
3738
import java.util.Date;
3839
import java.util.HashMap;
40+
import java.util.LinkedHashSet;
3941
import java.util.List;
4042
import java.util.Map;
4143
import java.util.UUID;
@@ -72,7 +74,7 @@ public Result<VerifiableCredentialContainer> generateCredential(String participa
7274

7375
var statusResult = createCredentialStatus(claims);
7476

75-
var credentialBuilder = generateVerifiableCredential(definition.getCredentialType(), definition.getValidity(), issuerId, holderDid, subjectResult.getContent());
77+
var credentialBuilder = generateVerifiableCredential(definition.getAdditionalContext(), definition.getCredentialType(), definition.getValidity(), issuerId, holderDid, subjectResult.getContent());
7678

7779
statusResult.onSuccess(credentialBuilder::credentialStatus);
7880

@@ -114,9 +116,13 @@ private Result<String> signCredentialInternal(String participantContextId, Verif
114116
}
115117

116118
@SuppressWarnings({"unchecked", "rawtypes"})
117-
private VerifiableCredential.Builder generateVerifiableCredential(String type, long validity, String issuer, String holderId, Map<String, Object> credentialSubject) {
119+
private VerifiableCredential.Builder generateVerifiableCredential(List<String> additionalContext, String type, long validity, String issuer, String holderId, Map<String, Object> credentialSubject) {
120+
var ctx = new LinkedHashSet<>();
121+
ctx.add(VcConstants.W3C_CREDENTIALS_URL);
122+
ctx.addAll(additionalContext);
118123
return VerifiableCredential.Builder.newInstance()
119124
.id(UUID.randomUUID().toString())
125+
.contexts(new ArrayList<>(ctx))
120126
.issuer(new Issuer(issuer))
121127
.dataModelVersion(DataModelVersion.V_1_1)
122128
.issuanceDate(Instant.now(clock))
@@ -149,8 +155,11 @@ private Result<CredentialStatus> createCredentialStatus(Map<String, Object> clai
149155
}
150156

151157
private Map<String, Object> createVcClaim(VerifiableCredential verifiableCredential, String... type) {
158+
var ctx = new LinkedHashSet<>();
159+
ctx.add(VcConstants.W3C_CREDENTIALS_URL);
160+
ctx.addAll(verifiableCredential.getContext());
152161
var claims = new HashMap<>(
153-
Map.of(JsonLdKeywords.CONTEXT, List.of(VcConstants.W3C_CREDENTIALS_URL),
162+
Map.of(JsonLdKeywords.CONTEXT, ctx,
154163
TYPE_PROPERTY, Arrays.asList(type),
155164
"id", verifiableCredential.getId(),
156165
"issuanceDate", verifiableCredential.getIssuanceDate().toString(),

core/issuerservice/issuerservice-issuance/src/test/java/org/eclipse/edc/issuerservice/issuance/generator/JwtCredentialGeneratorTest.java

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ void generateCredential() {
9292
assertThat(container.rawVc()).isNotNull();
9393
assertThat(container.format()).isEqualTo(VC1_0_JWT);
9494
assertThat(container.credential()).satisfies(verifiableCredential -> {
95+
assertThat(verifiableCredential.getContext()).contains("https://www.w3.org/2018/credentials/v1");
9596
assertThat(verifiableCredential.getType()).contains("MembershipCredential");
9697
assertThat(verifiableCredential.getIssuer().id()).isEqualTo("did:example:issuer");
9798
assertThat(verifiableCredential.getCredentialSubject()).hasSize(1);
@@ -115,6 +116,48 @@ void generateCredential() {
115116
});
116117
}
117118

119+
@SuppressWarnings({"unchecked", "rawtypes"})
120+
@Test
121+
void generateCredential_additionalContext() {
122+
123+
var subjectClaims = Map.of("name", "Foo Bar");
124+
Map<String, Object> claims = Map.of("credentialSubject", subjectClaims);
125+
126+
var definition = createCredentialDefinitionBuilder()
127+
.additionalContext(List.of("https://www.w3.org/2018/credentials/examples/v1"))
128+
.build();
129+
130+
var result = jwtCredentialGenerator.generateCredential(TEST_PARTICIPANT, definition, PRIVATE_KEY_ALIAS, PUBLIC_KEY_ID, "did:example:issuer", "did:example:participant", claims);
131+
132+
assertThat(result).isSucceeded();
133+
134+
var container = result.getContent();
135+
assertThat(container.rawVc()).isNotNull();
136+
assertThat(container.format()).isEqualTo(VC1_0_JWT);
137+
assertThat(container.credential()).satisfies(verifiableCredential -> {
138+
assertThat(verifiableCredential.getContext()).contains("https://www.w3.org/2018/credentials/v1", "https://www.w3.org/2018/credentials/examples/v1");
139+
assertThat(verifiableCredential.getType()).contains("MembershipCredential");
140+
assertThat(verifiableCredential.getIssuer().id()).isEqualTo("did:example:issuer");
141+
assertThat(verifiableCredential.getCredentialSubject()).hasSize(1);
142+
143+
var subject = verifiableCredential.getCredentialSubject().get(0);
144+
145+
assertThat(subject.getId()).isEqualTo("did:example:participant");
146+
assertThat(subject.getClaims()).isEqualTo(subjectClaims);
147+
148+
});
149+
150+
151+
var extractedClaims = extractJwtClaims(container.rawVc());
152+
153+
REQUIRED_CLAIMS.forEach(claim -> assertThat(extractedClaims.getClaim(claim)).describedAs("Claim '%s' cannot be null", claim).isNotNull());
154+
assertThat(extractJwtHeader(container.rawVc()).getKeyID()).isEqualTo("did:example:issuer#%s".formatted(PUBLIC_KEY_ID));
155+
assertThat(extractedClaims.getClaim(VERIFIABLE_CREDENTIAL_CLAIM)).isInstanceOfSatisfying(Map.class, vcClaim -> {
156+
assertThat((List) vcClaim.get("type")).contains("MembershipCredential");
157+
assertThat((Map) vcClaim.get("credentialSubject")).containsAllEntriesOf(subjectClaims);
158+
});
159+
}
160+
118161
@SuppressWarnings({"unchecked", "rawtypes"})
119162
@Test
120163
void generateCredential_whenNoStatus() {
@@ -256,13 +299,17 @@ protected ECKey createKey(Curve curve, String centralIssuerKeyId) {
256299
}
257300

258301
private CredentialDefinition createCredentialDefinition() {
302+
return createCredentialDefinitionBuilder()
303+
.build();
304+
}
305+
306+
private CredentialDefinition.Builder createCredentialDefinitionBuilder() {
259307
return CredentialDefinition.Builder.newInstance()
260308
.credentialType("MembershipCredential")
261309
.mapping(new MappingDefinition("input", "outut", true))
262310
.jsonSchema("{}")
263311
.participantContextId(UUID.randomUUID().toString())
264-
.formatFrom(VC1_0_JWT)
265-
.build();
312+
.formatFrom(VC1_0_JWT);
266313
}
267314

268315
private JWTClaimsSet extractJwtClaims(String vpJwt) {

e2e-tests/dcp-issuance-tests/src/test/java/org/eclipse/edc/identityhub/tests/dcp/flow/DcpIssuanceFlowEndToEndTest.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.junit.jupiter.api.extension.RegisterExtension;
5151

5252
import java.time.Duration;
53+
import java.util.List;
5354
import java.util.Map;
5455

5556
import static io.restassured.RestAssured.given;
@@ -155,10 +156,11 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
155156
assertThat(vc.getStateAsEnum()).isEqualTo(VcStatus.ISSUED);
156157
assertThat(vc.getIssuerId()).isEqualTo(issuerDid);
157158
assertThat(vc.getHolderId()).isEqualTo(participantDid);
158-
assertThat(vc.getVerifiableCredential().credential().getCredentialStatus()).isNotEmpty()
159-
.anySatisfy(t -> {
160-
assertThat(t.getProperty("", "statusPurpose").toString()).isEqualTo("revocation");
161-
});
159+
assertThat(vc.getVerifiableCredential().credential()).satisfies(v -> {
160+
assertThat(v.getCredentialStatus()).isNotEmpty()
161+
.anySatisfy(t -> assertThat(t.getProperty("", "statusPurpose").toString()).isEqualTo("revocation"));
162+
assertThat(v.getContext()).contains("https://example.com/credentials/membership/v1");
163+
});
162164
});
163165

164166
// checks that the credential was issued on the issuer side
@@ -168,8 +170,11 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
168170
assertThat(vc.getStateAsEnum()).isEqualTo(VcStatus.ISSUED);
169171
assertThat(vc.getIssuerId()).isEqualTo(issuerDid);
170172
assertThat(vc.getHolderId()).isEqualTo(participantDid);
171-
assertThat(vc.getVerifiableCredential().credential().getCredentialStatus()).hasSize(1)
172-
.allSatisfy(t -> assertThat(t.type()).isEqualTo("BitstringStatusListEntry"));
173+
assertThat(vc.getVerifiableCredential().credential()).satisfies(v -> {
174+
assertThat(v.getCredentialStatus()).hasSize(1)
175+
.allSatisfy(t -> assertThat(t.type()).isEqualTo("BitstringStatusListEntry"));
176+
assertThat(v.getContext()).contains("https://example.com/credentials/membership/v1");
177+
});
173178
});
174179

175180
// verify that the status credential on the issuer side is accessible
@@ -213,6 +218,7 @@ void issuanceFlow(IssuerService issuer, IdentityHub identityHub) {
213218
var credentialDefinition = CredentialDefinition.Builder.newInstance()
214219
.id("membershipCredential-id")
215220
.credentialType("MembershipCredential")
221+
.additionalContext(List.of("https://example.com/credentials/membership/v1"))
216222
.jsonSchemaUrl("https://example.com/schema")
217223
.jsonSchema("{}")
218224
.attestation(attestationDefinition.getId())

extensions/api/issuer-admin-api/issuer-admin-api-configuration/src/main/resources/issuer-admin-api-version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
{
33
"version": "1.0.0-alpha",
44
"urlPath": "/v1alpha",
5-
"lastUpdated": "2026-03-04T15:00:00Z",
5+
"lastUpdated": "2026-03-10T15:00:00Z",
66
"maturity": null
77
}
88
]

extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/BaseSqlDialectStatements.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public String getInsertTemplate() {
3939
.jsonColumn(getRulesColumn())
4040
.jsonColumn(getMappingsColumn())
4141
.jsonColumn(getJsonSchemaColumn())
42+
.jsonColumn(getAdditionalContextColumn())
4243
.column(getJsonSchemaUrlColumn())
4344
.column(getValidityColumn())
4445
.column(getFormatsColumn())
@@ -55,6 +56,7 @@ public String getUpdateTemplate() {
5556
.jsonColumn(getRulesColumn())
5657
.jsonColumn(getMappingsColumn())
5758
.jsonColumn(getJsonSchemaColumn())
59+
.jsonColumn(getAdditionalContextColumn())
5860
.column(getJsonSchemaUrlColumn())
5961
.column(getValidityColumn())
6062
.column(getFormatsColumn())

extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/CredentialDefinitionStoreStatements.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ default String getJsonSchemaUrlColumn() {
6060
return "json_schema_url";
6161
}
6262

63+
default String getAdditionalContextColumn() {
64+
return "additional_context";
65+
}
66+
6367
default String getValidityColumn() {
6468
return "validity";
6569
}

extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/SqlCredentialDefinitionStore.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
*/
4848
public class SqlCredentialDefinitionStore extends AbstractSqlStore implements CredentialDefinitionStore {
4949

50-
private static final TypeReference<List<String>> ATTESTATIONS_LIST_REF = new TypeReference<>() {
50+
private static final TypeReference<List<String>> STRING_LIST_REF = new TypeReference<>() {
5151
};
5252

5353
private static final TypeReference<List<CredentialRuleDefinition>> RULES_LIST_REF = new TypeReference<>() {
@@ -109,6 +109,7 @@ public StoreResult<Void> create(CredentialDefinition credentialDefinition) {
109109
toJson(credentialDefinition.getRules()),
110110
toJson(credentialDefinition.getMappings()),
111111
toJson(credentialDefinition.getJsonSchema()),
112+
toJson(credentialDefinition.getAdditionalContext()),
112113
credentialDefinition.getJsonSchemaUrl(),
113114
credentialDefinition.getValidity(),
114115
credentialDefinition.getFormat(),
@@ -145,6 +146,7 @@ public StoreResult<Void> update(CredentialDefinition credentialDefinition) {
145146
toJson(credentialDefinition.getRules()),
146147
toJson(credentialDefinition.getMappings()),
147148
toJson(credentialDefinition.getJsonSchema()),
149+
toJson(credentialDefinition.getAdditionalContext()),
148150
credentialDefinition.getJsonSchemaUrl(),
149151
credentialDefinition.getValidity(),
150152
credentialDefinition.getFormat(),
@@ -209,9 +211,10 @@ private CredentialDefinition mapResultSet(ResultSet resultSet) throws Exception
209211
.id(resultSet.getString(statements.getIdColumn()))
210212
.participantContextId(resultSet.getString(statements.getParticipantContextIdColumn()))
211213
.credentialType(resultSet.getString(statements.getCredentialTypeColumn()))
212-
.attestations(fromJson(resultSet.getString(statements.getAttestationsColumn()), ATTESTATIONS_LIST_REF))
214+
.attestations(fromJson(resultSet.getString(statements.getAttestationsColumn()), STRING_LIST_REF))
213215
.rules(fromJson(resultSet.getString(statements.getRulesColumn()), RULES_LIST_REF))
214216
.mappings(fromJson(resultSet.getString(statements.getMappingsColumn()), MAPPINGS_LIST_REF))
217+
.additionalContext(fromJson(resultSet.getString(statements.getAdditionalContextColumn()), STRING_LIST_REF))
215218
.jsonSchema(resultSet.getString(statements.getJsonSchemaColumn()))
216219
.jsonSchemaUrl(resultSet.getString(statements.getJsonSchemaUrlColumn()))
217220
.validity(resultSet.getLong(statements.getValidityColumn()))

extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/java/org/eclipse/edc/issuerservice/store/sql/credentialdefinition/schema/postgres/CredentialDefinitionMapping.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public class CredentialDefinitionMapping extends TranslationMapping {
3838
public static final String FIELD_VALIDITY = "validity";
3939
public static final String FIELD_FORMAT = "format";
4040
public static final String FIELD_ATTESTATIONS = "attestations";
41+
public static final String FIELD_ADDITIONAL_CONTEXT = "additionalContext";
4142
public static final String FIELD_RULES = "rules";
4243
public static final String FIELD_MAPPINGS = "mappings";
4344

@@ -55,5 +56,7 @@ public CredentialDefinitionMapping(CredentialDefinitionStoreStatements statement
5556
add(FIELD_ATTESTATIONS, new JsonArrayTranslator(statements.getAttestationsColumn()));
5657
add(FIELD_RULES, new JsonFieldTranslator(RULES_ALIAS));
5758
add(FIELD_MAPPINGS, new JsonFieldTranslator(MAPPING_ALIAS));
59+
add(FIELD_ADDITIONAL_CONTEXT, new JsonArrayTranslator(statements.getAdditionalContextColumn()));
60+
5861
}
5962
}

extensions/store/sql/issuerservice-credential-definition-store-sql/src/main/resources/credential-definition-schema.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ CREATE TABLE IF NOT EXISTS credential_definitions
2121
attestations JSON NOT NULL DEFAULT '[]',
2222
rules JSON NOT NULL DEFAULT '[]',
2323
mappings JSON NOT NULL DEFAULT '[]',
24+
additional_context JSON NOT NULL DEFAULT '[]',
2425
json_schema JSON,
2526
json_schema_url VARCHAR,
2627
validity BIGINT NOT NULL,

spi/issuerservice/issuerservice-issuance-spi/src/main/java/org/eclipse/edc/issuerservice/spi/issuance/model/CredentialDefinition.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
public class CredentialDefinition extends AbstractParticipantResource {
3838

3939
private final List<String> attestations = new ArrayList<>();
40+
private final List<String> additionalContext = new ArrayList<>();
4041
private final List<CredentialRuleDefinition> rules = new ArrayList<>();
4142
private final List<MappingDefinition> mappings = new ArrayList<>();
4243
private String format;
@@ -47,7 +48,7 @@ public class CredentialDefinition extends AbstractParticipantResource {
4748

4849
private CredentialDefinition() {
4950
}
50-
51+
5152
public String getCredentialType() {
5253
return credentialType;
5354
}
@@ -77,6 +78,10 @@ public List<String> getAttestations() {
7778
return attestations;
7879
}
7980

81+
public List<String> getAdditionalContext() {
82+
return additionalContext;
83+
}
84+
8085
public List<CredentialRuleDefinition> getRules() {
8186
return rules;
8287
}
@@ -140,6 +145,11 @@ public Builder attestation(String attestation) {
140145
return this;
141146
}
142147

148+
public Builder additionalContext(List<String> additionalContext) {
149+
this.entity.additionalContext.addAll(additionalContext);
150+
return this;
151+
}
152+
143153
public Builder rules(Collection<CredentialRuleDefinition> rules) {
144154
this.entity.rules.addAll(rules);
145155
return this;

0 commit comments

Comments
 (0)