Skip to content

Commit c7717a6

Browse files
fix: make VP holder property optional (#5697)
* fix(decentralized-claims): make VP holder property optional - JwtToVerifiablePresentationTransformer now reads an explicit `holder` claim from the VP JWT and falls back to the `iss` (issuer) claim when absent, instead of always using the issuer - VerifiableCredentialValidationServiceImpl skips HasValidSubjectIds validation when the presentation holder is null, allowing VPs that carry no holder to pass validation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * checkstyle --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b0946c5 commit c7717a6

6 files changed

Lines changed: 114 additions & 5 deletions

File tree

extensions/common/api/management-api-configuration/src/main/resources/management-api-version.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
{
1515
"version": "5.0.0-beta",
1616
"urlPath": "/v5beta",
17-
"lastUpdated": "2026-03-27T09:00:00Z",
17+
"lastUpdated": "2026-04-23T09:00:00Z",
1818
"maturity": "beta"
1919
}
2020
]

extensions/common/iam/decentralized-claims/decentralized-claims-transform/src/main/java/org/eclipse/edc/iam/decentralizedclaims/transform/to/JwtToVerifiablePresentationTransformer.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.text.ParseException;
3333
import java.util.Map;
3434

35+
3536
@SuppressWarnings("unchecked")
3637
public class JwtToVerifiablePresentationTransformer extends AbstractJwtTransformer<VerifiablePresentation> {
3738

@@ -104,7 +105,7 @@ public JwtToVerifiablePresentationTransformer(Monitor monitor, TypeManager typeM
104105
var vpToken = idClaim.substring(DATA_URL_VP_JWT.length());
105106
// credentialObject should contain EnvelopedVerifiableCredentials
106107
var vpClaims = SignedJWT.parse(vpToken).getJWTClaimsSet();
107-
builder.holder(vpClaims.getIssuer());
108+
builder.holder(vpClaims.getStringClaim("holder"));
108109
builder.id(vpClaims.getJWTID());
109110
// verifiable credentials as EnvelopedVerifiableCredentials
110111
listOrReturn(vpClaims.getClaim(VERIFIABLE_CREDENTIAL_PROPERTY), o -> extractEnvelopedCredential(o, context)).forEach(builder::credential);

extensions/common/iam/decentralized-claims/decentralized-claims-transform/src/test/java/org/eclipse/edc/iam/decentralizedclaims/transform/TestData.java

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,73 @@ public interface TestData {
485485
L07clQVAlvy-drJJ5dkmV5VTvThC0azqNfcmhQ1VRSgi0Z4gdYaM92IrW3OiRH3kIR-h7pLR3505m8CQ
486486
""";
487487

488+
@NotNull String EXAMPLE_ENVELOPED_PRESENTATION_WITH_HOLDER = """
489+
eyJhbGciOiJFUzI1NiJ9.eyJpZCI6ImRhdGE6YXBwbGljYXRpb24vdnArand0LGV5SmhiR2NpT2lK
490+
RlV6STFOaUo5LmV5Sm9iMnhrWlhJaU9pSmthV1E2ZDJWaU9tVjRZVzF3YkdVNmFHOXNaR1Z5SWl3a
491+
WRIbHdaU0k2SWxabGNtbG1hV0ZpYkdWUWNtVnpaVzUwWVhScGIyNGlMQ0pBWTI5dWRHVjRkQ0k2V3
492+
lKb2RIUndjem92TDNkM2R5NTNNeTV2Y21jdmJuTXZZM0psWkdWdWRHbGhiSE12ZGpJaUxDSm9kSFJ
493+
3Y3pvdkwzZDNkeTUzTXk1dmNtY3Zibk12WTNKbFpHVnVkR2xoYkhNdlpYaGhiWEJzWlhNdmRqSWlY
494+
U3dpZG1WeWFXWnBZV0pzWlVOeVpXUmxiblJwWVd3aU9sdDdJa0JqYjI1MFpYaDBJam9pYUhSMGNIT
495+
TZMeTkzZDNjdWR6TXViM0puTDI1ekwyTnlaV1JsYm5ScFlXeHpMM1l5SWl3aWFXUWlPaUprWVhSaE
496+
9tRndjR3hwWTJGMGFXOXVMM1pqSzJwM2RDeGxlVXB5WVZkUmFVOXBTakpaZVRGNllWZGtkVWxwZDJ
497+
sWlYzaHVTV3B2YVZKV1RYbE9WRmxwWmxFdVpYbEthbU50Vm10YVZ6VXdZVmRHYzFVeVRtOWFWekZv
498+
U1dwd1ltVjVTbkJhUTBrMlNXMW9NR1JJUW5wUGFUaDJXbGhvYUdKWVFuTmFVelYyWTIxamRscFlhR
499+
2hpV0VKeldsaE5kbHBIVm01amJWWnNURzF3ZW1JeU5HbE1RMG93WlZoQ2JFbHFiMmxUYms1MllteE
500+
9hbUZIVm5SWlUwbzVURWh6YVdGWFVXbFBhVXB2WkVoU2QyTjZiM1pNTWxZMFdWY3hkMkpIVlhWaU0
501+
wcHVUREpXTkZsWE1YZGlSMVo2VERKR2MyUlhNWFZoVXpWeFl6STVkVWxwZDJsa1NHeDNXbE5KTmts
502+
cmNIcGlNalZVV1RKb2JHSlhSV2xtVmpCelNXMU9lVnBYVW14aWJsSndXVmQ0VkdSWFNuRmFWMDR3U
503+
1dwd04wbHRiR3RKYW05cFdrZHNhMDl0VmpSWlZ6RjNZa2RWTmxwWFNtMWFWMGw0V21wamVFMXRWbW
504+
xaZWxwdFRWZE5lVTU2V214TlZFcHNXWHBKZUVscGQybGFSMVp1WTIxV2JFbHFjRGRKYmxJMVkwZFZ
505+
hVTlwU2tabFIwWjBZMGQ0YkZGdFJtcGhSMVp6WWpOS1JWcFhaSGxhVjFWcFRFTktkVmxYTVd4SmFt
506+
OXBVVzFHYW1GSFZuTmlNMGxuWWpKWloxVXlUbkJhVnpWcVdsTkNhR0p0VVdkUldFb3dZM2xLT1V4R
507+
FNtaGlTRlowWW0xc1VGcHBTVFpsZVVwMVdWY3hiRWxxYjJsU1dHaG9ZbGhDYzFwVFFsWmliV3d5V2
508+
xoS2VtRllValZKYmpFNVRFTktjRnBEU1RaSmJXZ3daRWhCTmt4NU9URmliV3d5V2xoS2VtRllValZ
509+
NYlZZMFdWY3hkMkpIVlhaWk0wcHNXa2RXZFdSSGJHaGlTRTEyVFhwamVrMXBTWE5KYmxwb1lrZHNh
510+
MUp1U25aaVUwazJTV3BKZDAxVVFYUk5SRVYwVFVSR1ZVMVVhelpOYWswMlRXcFNZVWxwZDJsa1NHe
511+
DNXbE5KTmxkNVNsZGFXRXB3V20xc2FGbHRlR3hSTTBwc1drZFdkV1JIYkdoaVEwbHpTV3RXTkZsWE
512+
1YZGlSMVpGV2xka2VWcFhWa1JqYlZacldsYzFNR0ZYUm5OSmFYZHBVbGhvYUdKWVFuTmFWa0pzWTI
513+
1T2RtSnJUbmxhVjFKc1ltNVNjRmxYZDJsWVUzZHBVVWRPZG1KdVVteGxTRkZwVDJ4emFXRklVakJq
514+
U0UwMlRIazVNMlF6WTNWa2VrMTFZak5LYmt3eU5YcE1NazU1V2xkU2JHSnVVbkJaVjNoNlRETlplV
515+
WxwZDJsaFNGSXdZMGhOTmt4NU9UTmtNMk4xWkhwTmRXSXpTbTVNTWpWNlRESk9lVnBYVW14aWJsSn
516+
dXVmQ0ZWt3eVZqUlpWekYzWWtkV2Vrd3pXWGxKYkRCelNXMXNlbU16Vm14amFVazJTVzFvTUdSSVF
517+
ucFBhVGgyWkZjMWNHUnRWbmxqTW13d1pWTTFiR1ZIUm5SalIzaHNUREpzZW1NelZteGpiazEyVFZS
518+
UmFXWlJMbWRUY2xWT05XMVBNR3RzTVVKaFUzZGFlRVJpYjE5UVNFNUhNWGR0WmpGdGRYUXlkRlJJU
519+
Ws1WlNWcGxWMUJpVGw5NlgwZ3RPV1JyVHpKa1ptTlFPVE5YU25kNVJUVmxiMG8xYTNGZmFreDZVMk
520+
p2Vm5wbklpd2lkSGx3WlNJNklrVnVkbVZzYjNCbFpGWmxjbWxtYVdGaWJHVkRjbVZrWlc1MGFXRnN
521+
JbjBzZXlKQVkyOXVkR1Y0ZENJNkltaDBkSEJ6T2k4dmQzZDNMbmN6TG05eVp5OXVjeTlqY21Wa1pX
522+
NTBhV0ZzY3k5Mk1pSXNJbWxrSWpvaVpHRjBZVHBoY0hCc2FXTmhkR2x2Ymk5Mll5dHFkM1FzWlhsS
523+
2NtRlhVV2xQYVVveVdYa3hlbUZYWkhWSmFYZHBXVmQ0YmtscWIybFNWazE1VGxSWmFXWlJMbVY1U2
524+
1wamJWWnJXbGMxTUdGWFJuTlZNazV2V2xjeGFFbHFjR0psZVVwd1drTkpOa2x0YURCa1NFSjZUMms
525+
0ZGxwWWFHaGlXRUp6V2xNMWRtTnRZM1phV0dob1lsaENjMXBZVFhaYVIxWnVZMjFXYkV4dGNIcGlN
526+
alJwVEVOS01HVllRbXhKYW05cFUyNU9kbUpzVG1waFIxWjBXVk5LT1V4SWMybGhWMUZwVDJsS2IyU
527+
klVbmRqZW05MlRESldORmxYTVhkaVIxVjFZak5LYmt3eVZqUlpWekYzWWtkV2Vrd3lSbk5rVnpGMV
528+
lWTTFjV015T1hWSmFYZHBaRWhzZDFwVFNUWkphM0I2WWpJMVZGa3lhR3hpVjBWcFpsWXdjMGx0VG5
529+
sYVYxSnNZbTVTY0ZsWGVGUmtWMHB4V2xkT01FbHFjRGRKYld4clNXcHZhVnBIYkd0UGJWWTBXVmN4
530+
ZDJKSFZUWmFWMHB0V2xkSmVGcHFZM2hOYlZacFdYcGFiVTFYVFhsT2VscHNUVlJLYkZsNlNYaEphW
531+
GRwV2tkV2JtTnRWbXhKYW5BM1NXNVNOV05IVldsUGFVcEdaVWRHZEdOSGVHeFJiVVpxWVVkV2MySX
532+
pTa1ZhVjJSNVdsZFZhVXhEU25WWlZ6RnNTV3B2YVZGdFJtcGhSMVp6WWpOSloySXlXV2RWTWs1d1d
533+
sYzFhbHBUUW1oaWJWRm5VVmhLTUdONVNqbE1RMHBvWWtoV2RHSnRiRkJhYVVrMlpYbEtkVmxYTVd4
534+
SmFtOXBVbGhvYUdKWVFuTmFVMEpXWW0xc01scFlTbnBoV0ZJMVNXNHhPVXhEU25CYVEwazJTVzFvT
535+
UdSSVFUWk1lVGt4WW0xc01scFlTbnBoV0ZJMVRHMVdORmxYTVhkaVIxVjJXVE5LYkZwSFZuVmtSMn
536+
hvWWtoTmRrMTZZM3BOYVVselNXNWFhR0pIYkd0U2JrcDJZbE5KTmtscVNYZE5WRUYwVFVSRmRFMUV
537+
SbFZOVkdzMlRXcE5OazFxVW1GSmFYZHBaRWhzZDFwVFNUWlhlVXBYV2xoS2NGcHRiR2haYlhoc1VU
538+
TktiRnBIVm5Wa1IyeG9Za05KYzBsclZqUlpWekYzWWtkV1JWcFhaSGxhVjFaRVkyMVdhMXBYTlRCa
539+
FYwWnpTV2wzYVZKWWFHaGlXRUp6V2xaQ2JHTnVUblppYTA1NVdsZFNiR0p1VW5CWlYzZHBXRk4zYV
540+
ZGSFRuWmlibEpzWlVoUmFVOXNjMmxoU0ZJd1kwaE5Oa3g1T1ROa00yTjFaSHBOZFdJelNtNU1NalY
541+
2VERKT2VWcFhVbXhpYmxKd1dWZDRla3d6V1hsSmFYZHBZVWhTTUdOSVRUWk1lVGt6WkROamRXUjZU
542+
WFZpTTBwdVRESTFla3d5VG5sYVYxSnNZbTVTY0ZsWGVIcE1NbFkwV1ZjeGQySkhWbnBNTTFsNVNXd
543+
3djMGx0Ykhwak0xWnNZMmxKTmtsdGFEQmtTRUo2VDJrNGRtUlhOWEJrYlZaNVl6SnNNR1ZUTld4bF
544+
IwWjBZMGQ0YkV3eWJIcGpNMVpzWTI1TmRrMVVVV2xtVVM1blUzSlZUalZ0VHpCcmJERkNZVk4zV25
545+
oRVltOWZVRWhPUnpGM2JXWXhiWFYwTW5SVVNFSk9XVWxhWlZkUVlrNWZlbDlJTFRsa2EwOHlaR1pq
546+
VURrelYwcDNlVVUxWlc5S05XdHhYMnBNZWxOaWIxWjZaeUlzSW5SNWNHVWlPaUpGYm5abGJHOXdaV
547+
1JXWlhKcFptbGhZbXhsUTNKbFpHVnVkR2xoYkNKOVhYMC5OcTB5aklUN2hPZGwzOURhb3p2c0pnMz
548+
d2OGFUdlU2cy1TQjVmXzNOLUZSc1dwY21xMkZkY0YxT2diWlh0UWhOS3h0TnhxaWpjMTIyTWhONHh
549+
uRWU2QSIsInR5cGUiOiJFbnZlbG9wZWRWZXJpZmlhYmxlUHJlc2VudGF0aW9uIiwiQGNvbnRleHQi
550+
OlsiaHR0cHM6Ly93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL2V4YW1wbGVzL3YyIiwiaHR0cHM6L
551+
y93d3cudzMub3JnL25zL2NyZWRlbnRpYWxzL3YyIl19.a65dZ3g-AXgKTex3yIV26LVD7GPp1WKPS
552+
dzjCqWI7QaURTHL3NjlBCELregYFtcdINMiFG9MJBEuk88MPUqBpw
553+
""";
554+
488555
@NotNull String EXAMPLE_JWT_VC_2_0 = """
489556
eyJraWQiOiJFeEhrQk1XOWZtYmt2VjI2Nm1ScHVQMnNVWV9OX0VXSU4xbGFwVXpPOHJvIiwiYWxnI
490557
joiRVMyNTYifQ .eyJAY29udGV4dCI6WyJodHRwczovL3d3dy53My5vcmcvbnMvY3JlZGVudGlhbH

extensions/common/iam/decentralized-claims/decentralized-claims-transform/src/test/java/org/eclipse/edc/iam/decentralizedclaims/transform/to/JwtToVerifiablePresentationTransformerTest.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,31 @@ void transform_inputIsNotValidJwt() {
149149

150150
@DisplayName("VP is an EnvelopedVerifiablePresentation")
151151
@Test
152-
void transform_envelopedPresentation() {
152+
void transform_envelopedPresentation_noHolder() {
153153
var vp = transformer.transform(TestData.EXAMPLE_ENVELOPED_PRESENTATION, context);
154154
assertThat(vp).isNotNull();
155+
assertThat(vp.getHolder()).isNull();
155156
assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation");
156157
assertThat(vp.getCredentials()).hasSize(2)
157158
.allSatisfy(vc -> {
159+
160+
assertThat(vc.getCredentialSubject()).isNotEmpty();
161+
assertThat(vc.getType()).containsExactlyInAnyOrder("ExampleDegreeCredential", "ExamplePersonCredential", "VerifiableCredential");
162+
});
163+
verify(context, never()).reportProblem(anyString());
164+
}
165+
166+
@DisplayName("VP is an EnvelopedVerifiablePresentation with a holder property")
167+
@Test
168+
void transform_envelopedPresentation_withHolder() {
169+
170+
var vp = transformer.transform(TestData.EXAMPLE_ENVELOPED_PRESENTATION_WITH_HOLDER, context);
171+
assertThat(vp).isNotNull();
172+
assertThat(vp.getHolder()).isNotNull().isEqualTo("did:web:example:holder");
173+
assertThat(vp.getTypes()).containsExactlyInAnyOrder("VerifiablePresentation");
174+
assertThat(vp.getCredentials()).hasSize(2)
175+
.allSatisfy(vc -> {
176+
158177
assertThat(vc.getCredentialSubject()).isNotEmpty();
159178
assertThat(vc.getType()).containsExactlyInAnyOrder("ExampleDegreeCredential", "ExamplePersonCredential", "VerifiableCredential");
160179
});

extensions/common/iam/decentralized-claims/lib/verifiable-credentials-lib/src/main/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImpl.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry;
3030
import org.eclipse.edc.spi.result.Result;
3131
import org.jetbrains.annotations.NotNull;
32+
import org.jetbrains.annotations.Nullable;
3233

3334
import java.time.Clock;
3435
import java.util.ArrayList;
@@ -68,16 +69,19 @@ public Result<Void> validate(List<VerifiablePresentationContainer> presentations
6869
}
6970

7071
@NotNull
71-
private Result<Void> validateVerifiableCredentials(List<VerifiableCredential> credentials, String presentationHolder, Collection<? extends CredentialValidationRule> additionalRules) {
72+
private Result<Void> validateVerifiableCredentials(List<VerifiableCredential> credentials, @Nullable String presentationHolder, Collection<? extends CredentialValidationRule> additionalRules) {
7273

7374
// in addition, verify that all VCs are valid
7475
var filters = new ArrayList<>(List.of(
7576
new IsInValidityPeriod(clock),
76-
new HasValidSubjectIds(presentationHolder),
7777
new IsNotRevoked(revocationServiceRegistry),
7878
new HasValidIssuer(trustedIssuerRegistry),
7979
new HasValidSubjectSchema(mapper)));
8080

81+
if (presentationHolder != null) {
82+
filters.add(new HasValidSubjectIds(presentationHolder));
83+
}
84+
8185
filters.addAll(additionalRules);
8286

8387

extensions/common/iam/decentralized-claims/lib/verifiable-credentials-lib/src/test/java/org/eclipse/edc/iam/verifiablecredentials/VerifiableCredentialValidationServiceImplTest.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ void verify_singlePresentation_singleCredential() {
155155
assertThat(result).isSucceeded();
156156
}
157157

158+
@Test
159+
void verify_singlePresentation_noHolderProperty_shouldSucceed() {
160+
var presentation = createPresentationBuilder()
161+
.holder(null) // un-set holder property
162+
.type("VerifiablePresentation")
163+
.credentials(List.of(createCredentialBuilder()
164+
.credentialSubjects(List.of(CredentialSubject.Builder.newInstance()
165+
.id(CONSUMER_DID)
166+
.claim("some-claim", "some-val")
167+
.build()))
168+
.build()))
169+
.build();
170+
var vpContainer = new VerifiablePresentationContainer("test-vp", CredentialFormat.VC1_0_LD, presentation);
171+
when(verifier.verifyPresentation(any(), any())).thenReturn(success());
172+
var result = validationService.validate(List.of(vpContainer), EXPECTED_AUDIENCE);
173+
assertThat(result).isSucceeded();
174+
}
175+
158176
@Test
159177
void verify_singlePresentation_multipleCredentials() {
160178
var presentation = createPresentationBuilder()

0 commit comments

Comments
 (0)