Skip to content

JSON-LD Compaction Drops Custom Claims in Verifiable Presentations #893

@wahidulazam

Description

@wahidulazam

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:

  1. Extract @context from all embedded credentials
  2. Merge these contexts with the VP's base contexts
  3. Use the merged context array for JSON-LD compaction
  4. 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 CompactionmembershipId 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

  1. 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"}}',
      ...
    );
  2. 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"]}'
  3. 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:

  1. Upstream source code inspection: Cloned eclipse-edc/IdentityHub repository and verified exact code at specified line numbers
  2. Complete execution flow tracing through the VP generation pipeline
  3. Git history analysis showing no recent fixes for this issue
  4. Testing with TractuX deployment showing custom claims are lost

References

Testing

After implementing the fix, the following should be verified:

  1. Custom claims preserved: VP contains all credential fields including custom claims
  2. Multiple contexts: VPs with credentials from different schemas work correctly
  3. Backward compatibility: Standard W3C VCs without custom contexts still work
  4. Compaction correctness: JSON-LD compaction produces valid, semantically equivalent output
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    triageall new issues awaiting classification

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions