Description
When the Presentation API returns a Verifiable Presentation containing credentials with custom @context definitions (e.g., credential-schema-specific contexts), the JSON-LD compaction step drops all custom claims that are not defined in the protocol scope. This results in presentations missing critical credential fields.
Important: This issue specifically affects credentials that include JSON-LD @context arrays with credential-schema-specific contexts (beyond the standard W3C Credentials context). The bug manifests when these schema contexts define custom terms that are not present in the DCP protocol context used during compaction.
Impact
- Severity: Critical
- Affected Component: LDP Verifiable Presentation generation
- User Impact: Custom claims in credentials are silently dropped during VP generation, breaking interoperability with systems expecting these fields (e.g., EDC catalog requests, Catena-X dataspace integration)
Root Cause
The bug occurs in three locations during VP generation:
1. LdpPresentationGenerator.generatePresentation() (Line 134-142)
The VP is built with only hardcoded contexts, not including contexts from embedded credentials:
var presentationObject = Json.createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, stringArray(
List.of(VcConstants.W3C_CREDENTIALS_URL,
VcConstants.PRESENTATION_EXCHANGE_URL)))
.add(ID_PROPERTY, DcpConstants.DCP_CONTEXT_URL + "/id/" + UUID.randomUUID())
.add(VP_TYPE_PROPERTY, stringArray(types))
.add(HOLDER_PROPERTY, issuerId)
.add(VERIFIABLE_CREDENTIAL_PROPERTY, toJsonArray(credentials))
.build();
Problem: The VP @context array is ["https://www.w3.org/2018/credentials/v1", "https://identity.foundation/presentation-exchange/submission/v1"] but embedded credentials may have additional credential-schema contexts that define custom credential terms. These contexts are not included in the VP's context array, causing compaction to drop any terms not defined in the base contexts.
2. PresentationApiController.queryPresentation() (Line 140)
The compaction step only uses the protocol scope, ignoring credential contexts:
.compose(json -> jsonLd.compact(json, protocol.scope()))
Where protocol.scope() is just "org.eclipse.tractusx.vc.type" (or similar), which defines only protocol-level messaging structures, not credential-schema-specific fields.
3. TitaniumJsonLd.compact()
Creates the compaction context only from the protocol scope string, not from the VP's embedded contexts:
public Result<JsonObject> compact(JsonObject input, String scope) {
JsonObject contextDoc = factory.createObjectBuilder()
.add("@context", createContext(scope)) // Only protocol scope!
.build();
return JsonLd.compact(document, contextDocument).get();
}
Expected Behavior
When generating a Verifiable Presentation:
- Extract
@context from all embedded credentials
- Merge these contexts with the VP's base contexts
- Use the merged context array for JSON-LD compaction
- Preserve all custom claims defined in credential contexts
Example
Before (Current - Incorrect):
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://identity.foundation/presentation-exchange/submission/v1"
],
"holder": "did:web:test-holder",
"verifiableCredential": [{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://example.org/credentials/membership/v1" // Custom credential schema context
],
"type": ["VerifiableCredential", "MembershipCredential"],
"credentialSubject": {
"id": "did:web:holder",
"membershipLevel": "premium",
"membershipId": "MEMBER-12345" // ❌ LOST during compaction (defined in membership schema)
}
}]
}
After Compaction → membershipId is dropped because the credential's schema context (https://example.org/credentials/membership/v1) is not in the VP's context array!
Expected (Correct):
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://identity.foundation/presentation-exchange/submission/v1",
"https://example.org/credentials/membership/v1" // ✅ Merged from credential schema
],
"holder": "did:web:test-holder",
"verifiableCredential": [{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://example.org/credentials/membership/v1"
],
"type": ["VerifiableCredential", "MembershipCredential"],
"credentialSubject": {
"id": "did:web:holder",
"membershipLevel": "premium",
"membershipId": "MEMBER-12345" // ✅ Preserved
}
}]
}
Steps to Reproduce
-
Create a credential with custom credential schema @context:
INSERT INTO credential_resource (id, verifiable_credential, ...)
VALUES (
'vc-test',
'{"@context": ["https://www.w3.org/2018/credentials/v1", "https://example.org/credentials/membership/v1"],
"type": ["VerifiableCredential", "MembershipCredential"],
"credentialSubject": {"id": "did:web:holder", "membershipId": "MEMBER-12345"}}',
...
);
-
Query the Presentation API:
curl -X POST http://localhost:18184/api/identity-hub/v1/participants/test/presentations/query \
-H "Authorization: Bearer <token>" \
-d '{"@context": [...], "scope": ["org.eclipse.tractusx.vc.type"]}'
-
Observe that the returned VP's credentialSubject is missing custom fields like membershipId that are defined in the credential's schema context
Proposed Solution
Option 1: Context Merging (Recommended)
Modify LdpPresentationGenerator.generatePresentation() to extract and merge contexts:
@Override
public JsonObject generatePresentation(..., List<VerifiableCredentialContainer> credentials, ...) {
// Extract contexts from all credentials
Set<String> credentialContexts = credentials.stream()
.flatMap(vc -> extractContexts(vc.credential()))
.collect(Collectors.toSet());
// Build VP with merged contexts
List<String> vpContexts = new ArrayList<>(
List.of(VcConstants.W3C_CREDENTIALS_URL,
VcConstants.PRESENTATION_EXCHANGE_URL));
vpContexts.addAll(credentialContexts); // ✅ Include credential contexts
var presentationObject = Json.createObjectBuilder()
.add(JsonLdKeywords.CONTEXT, stringArray(vpContexts))
.add(VERIFIABLE_CREDENTIAL_PROPERTY, toJsonArray(credentials))
.build();
return signPresentation(presentationObject, ...);
}
private Stream<String> extractContexts(VerifiableCredential credential) {
// Extract @context array from credential JSON
return credential.getJsonArray("@context").stream()
.map(ctx -> ((JsonString) ctx).getString());
}
Option 2: Skip Compaction for LDP VPs
Modify PresentationApiController.queryPresentation() to skip compaction for LDP presentations:
var presentationResponse = verifiablePresentationService.createPresentation(...)
.compose(presentation -> protocolRegistry.transform(presentation, JsonObject.class))
.compose(json -> {
// Skip compaction for LDP VPs - they already have proper contexts
if (isLdpPresentation(json)) {
return Result.success(json);
}
return jsonLd.compact(json, protocol.scope());
})
.orElseThrow(...);
Option 3: Enhanced Compaction Context
Pass both the protocol scope AND credential contexts to compaction:
// Extract contexts from VP before compaction
List<String> allContexts = extractAllContexts(vpJsonObject);
allContexts.add(protocol.scope());
return jsonLd.compact(vpJsonObject, allContexts);
Environment
- IdentityHub Version: 0.15.1 (EDC BOM) / main branch
- Affected Files:
core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/services/verifiablepresentation/generators/LdpPresentationGenerator.java (Line 134-140)
protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java (Line 140)
org/eclipse/edc/jsonld/TitaniumJsonLd.java (part of EDC Connector framework)
- Verified in: Upstream repository cloned and inspected on January 8, 2026
Additional Context
Important Context Distinction:
- Protocol contexts (e.g.,
https://w3id.org/tractusx-trust/v0.8, which redirects to DCP context) define message envelope structures for presentation exchange protocol
- Credential schema contexts (e.g.,
https://example.org/credentials/membership/v1) define credential-specific terms and claims
- This bug affects credentials that include credential schema contexts in their
@context arrays
This issue affects any deployment using custom credential schemas with additional @context definitions beyond the standard W3C Credentials context. It's particularly critical for:
- Domain-specific credential schemas (MembershipCredentials, LicenseCredentials, etc.)
- Industry-specific dataspaces with custom credential types
- Custom dataspace implementations where credentials define specialized claim structures
The bug has been verified through:
- Upstream source code inspection: Cloned eclipse-edc/IdentityHub repository and verified exact code at specified line numbers
- Complete execution flow tracing through the VP generation pipeline
- Git history analysis showing no recent fixes for this issue
- Testing with TractuX deployment showing custom claims are lost
References
Testing
After implementing the fix, the following should be verified:
- Custom claims preserved: VP contains all credential fields including custom claims
- Multiple contexts: VPs with credentials from different schemas work correctly
- Backward compatibility: Standard W3C VCs without custom contexts still work
- Compaction correctness: JSON-LD compaction produces valid, semantically equivalent output
- EDC integration: Consumer EDC can successfully parse the VP and extract claims for policy evaluation
Note: A fix for this issue has already been implemented and is available at:
The fix includes:
- Context extraction from all credentials (handles both string and array formats)
- Context merging with base VP contexts while avoiding duplicates
- New test cases for custom contexts, single/multiple credentials, and different context formats
- All tests passing
I'm ready to collaborate on getting this merged into the main repository.
Description
When the Presentation API returns a Verifiable Presentation containing credentials with custom
@contextdefinitions (e.g., credential-schema-specific contexts), the JSON-LD compaction step drops all custom claims that are not defined in the protocol scope. This results in presentations missing critical credential fields.Important: This issue specifically affects credentials that include JSON-LD
@contextarrays with credential-schema-specific contexts (beyond the standard W3C Credentials context). The bug manifests when these schema contexts define custom terms that are not present in the DCP protocol context used during compaction.Impact
Root Cause
The bug occurs in three locations during VP generation:
1.
LdpPresentationGenerator.generatePresentation()(Line 134-142)The VP is built with only hardcoded contexts, not including contexts from embedded credentials:
Problem: The VP
@contextarray is["https://www.w3.org/2018/credentials/v1", "https://identity.foundation/presentation-exchange/submission/v1"]but embedded credentials may have additional credential-schema contexts that define custom credential terms. These contexts are not included in the VP's context array, causing compaction to drop any terms not defined in the base contexts.2.
PresentationApiController.queryPresentation()(Line 140)The compaction step only uses the protocol scope, ignoring credential contexts:
Where
protocol.scope()is just"org.eclipse.tractusx.vc.type"(or similar), which defines only protocol-level messaging structures, not credential-schema-specific fields.3.
TitaniumJsonLd.compact()Creates the compaction context only from the protocol scope string, not from the VP's embedded contexts:
Expected Behavior
When generating a Verifiable Presentation:
@contextfrom all embedded credentialsExample
Before (Current - Incorrect):
{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://identity.foundation/presentation-exchange/submission/v1" ], "holder": "did:web:test-holder", "verifiableCredential": [{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://example.org/credentials/membership/v1" // Custom credential schema context ], "type": ["VerifiableCredential", "MembershipCredential"], "credentialSubject": { "id": "did:web:holder", "membershipLevel": "premium", "membershipId": "MEMBER-12345" // ❌ LOST during compaction (defined in membership schema) } }] }After Compaction →
membershipIdis dropped because the credential's schema context (https://example.org/credentials/membership/v1) is not in the VP's context array!Expected (Correct):
{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://identity.foundation/presentation-exchange/submission/v1", "https://example.org/credentials/membership/v1" // ✅ Merged from credential schema ], "holder": "did:web:test-holder", "verifiableCredential": [{ "@context": [ "https://www.w3.org/2018/credentials/v1", "https://example.org/credentials/membership/v1" ], "type": ["VerifiableCredential", "MembershipCredential"], "credentialSubject": { "id": "did:web:holder", "membershipLevel": "premium", "membershipId": "MEMBER-12345" // ✅ Preserved } }] }Steps to Reproduce
Create a credential with custom credential schema
@context:Query the Presentation API:
Observe that the returned VP's
credentialSubjectis missing custom fields likemembershipIdthat are defined in the credential's schema contextProposed Solution
Option 1: Context Merging (Recommended)
Modify
LdpPresentationGenerator.generatePresentation()to extract and merge contexts:Option 2: Skip Compaction for LDP VPs
Modify
PresentationApiController.queryPresentation()to skip compaction for LDP presentations:Option 3: Enhanced Compaction Context
Pass both the protocol scope AND credential contexts to compaction:
Environment
core/identity-hub-core/src/main/java/org/eclipse/edc/identityhub/core/services/verifiablepresentation/generators/LdpPresentationGenerator.java(Line 134-140)protocols/dcp/dcp-identityhub/presentation-api/src/main/java/org/eclipse/edc/identityhub/api/verifiablecredential/PresentationApiController.java(Line 140)org/eclipse/edc/jsonld/TitaniumJsonLd.java(part of EDC Connector framework)Additional Context
Important Context Distinction:
https://w3id.org/tractusx-trust/v0.8, which redirects to DCP context) define message envelope structures for presentation exchange protocolhttps://example.org/credentials/membership/v1) define credential-specific terms and claims@contextarraysThis issue affects any deployment using custom credential schemas with additional
@contextdefinitions beyond the standard W3C Credentials context. It's particularly critical for:The bug has been verified through:
References
Testing
After implementing the fix, the following should be verified:
Note: A fix for this issue has already been implemented and is available at:
fix/893-merge-credential-contexts-vpThe fix includes:
I'm ready to collaborate on getting this merged into the main repository.