Skip to content

Commit a8a3da7

Browse files
feat: download status list credential with EdcHttpClient (#5030)
feat: download status list credentials with EdcHttpClient
1 parent a07d0de commit a8a3da7

File tree

8 files changed

+176
-90
lines changed

8 files changed

+176
-90
lines changed

extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070

7171
import java.net.URISyntaxException;
7272
import java.time.Clock;
73+
import java.util.Collection;
74+
import java.util.List;
7375
import java.util.Map;
7476
import java.util.concurrent.Executors;
7577
import java.util.concurrent.ScheduledFuture;
@@ -80,6 +82,7 @@
8082
import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0;
8183
import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_V_1_0_CONTEXT;
8284
import static org.eclipse.edc.iam.verifiablecredentials.spi.VcConstants.STATUSLIST_2021_URL;
85+
import static org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry.WILDCARD;
8386
import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD;
8487
import static org.eclipse.edc.verifiablecredentials.jwt.JwtPresentationVerifier.JWT_VC_TOKEN_CONTEXT;
8588

@@ -157,6 +160,8 @@ public class IdentityAndTrustExtension implements ServiceExtension {
157160
private PresentationVerifier presentationVerifier;
158161
private CredentialServiceClient credentialServiceClient;
159162
private ScheduledFuture<?> jtiEntryReaperThread;
163+
@Setting(key = "edc.iam.credential.revocation.mimetype", description = "A comma-separated list of accepted content types of the revocation list credential.", defaultValue = WILDCARD)
164+
private String contentTypes;
160165

161166
@Override
162167
public void initialize(ServiceExtensionContext context) {
@@ -185,15 +190,20 @@ public void initialize(ServiceExtensionContext context) {
185190
participantAgentService.register(participantAgentServiceExtension);
186191

187192
// register revocation services
188-
revocationServiceRegistry.addService(StatusList2021Status.TYPE, new StatusList2021RevocationService(typeManager.getMapper(), revocationCacheValidity));
189-
revocationServiceRegistry.addService(BitstringStatusListStatus.TYPE, new BitstringStatusListRevocationService(typeManager.getMapper(), revocationCacheValidity));
193+
var acceptedContentTypes = parseAcceptedContentTypes(contentTypes);
194+
revocationServiceRegistry.addService(StatusList2021Status.TYPE, new StatusList2021RevocationService(typeManager.getMapper(), revocationCacheValidity, acceptedContentTypes, httpClient));
195+
revocationServiceRegistry.addService(BitstringStatusListStatus.TYPE, new BitstringStatusListRevocationService(typeManager.getMapper(), revocationCacheValidity, acceptedContentTypes, httpClient));
196+
}
197+
198+
private Collection<String> parseAcceptedContentTypes(String contentTypes) {
199+
return List.of(contentTypes.split(","));
190200
}
191201

192202
@Override
193203
public void start() {
194204
if (activateJtiValidation) {
195205
jtiEntryReaperThread = executorInstrumentation.instrument(Executors.newSingleThreadScheduledExecutor(), "JTI Validation Entry Reaper Thread")
196-
.scheduleAtFixedRate(jtiValidationStore::deleteExpired, reaperCleanupPeriod, reaperCleanupPeriod, TimeUnit.SECONDS);
206+
.scheduleAtFixedRate(jtiValidationStore::deleteExpired, reaperCleanupPeriod, reaperCleanupPeriod, TimeUnit.SECONDS);
197207
}
198208
}
199209

@@ -243,12 +253,12 @@ public PresentationVerifier createPresentationVerifier(ServiceExtensionContext c
243253

244254
var jwtVerifier = new JwtPresentationVerifier(typeManager, JSON_LD, tokenValidationService, rulesRegistry, didPublicKeyResolver);
245255
var ldpVerifier = LdpVerifier.Builder.newInstance()
246-
.signatureSuites(signatureSuiteRegistry)
247-
.jsonLd(jsonLd)
248-
.typeManager(typeManager)
249-
.typeContext(JSON_LD)
250-
.methodResolver(new DidMethodResolver(didResolverRegistry))
251-
.build();
256+
.signatureSuites(signatureSuiteRegistry)
257+
.jsonLd(jsonLd)
258+
.typeManager(typeManager)
259+
.typeContext(JSON_LD)
260+
.methodResolver(new DidMethodResolver(didResolverRegistry))
261+
.build();
252262

253263
presentationVerifier = new MultiFormatPresentationVerifier(issuerId, jwtVerifier, ldpVerifier);
254264
}

extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtensionTest.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,7 @@ void setUp(ServiceExtensionContext context) {
6666

6767
@Test
6868
void verifyCorrectService(ServiceExtensionContext context, ObjectFactory objectFactory) {
69-
70-
7169
var is = objectFactory.constructInstance(IdentityAndTrustExtension.class).createIdentityService(context);
72-
7370
assertThat(is).isInstanceOf(IdentityAndTrustService.class);
7471
}
7572

extensions/common/iam/verifiable-credentials/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dependencies {
2626
testImplementation(testFixtures(project(":spi:common:verifiable-credentials-spi")))
2727
testImplementation(libs.mockserver.netty)
2828
testImplementation(project(":tests:junit-base"))
29+
testImplementation(project(":core:common:lib:http-lib"))
2930
testImplementation(project(":core:common:lib:util-lib"))
3031
testImplementation(testFixtures(project(":spi:common:identity-trust-spi"))) //test functions
3132
}

extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/BaseRevocationListService.java

Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
import com.fasterxml.jackson.databind.DeserializationFeature;
1818
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import okhttp3.Request;
20+
import org.eclipse.edc.http.spi.EdcHttpClient;
1921
import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService;
2022
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
2123
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
@@ -25,8 +27,8 @@
2527
import org.eclipse.edc.util.collection.Cache;
2628

2729
import java.io.IOException;
28-
import java.net.URI;
2930
import java.time.Instant;
31+
import java.util.Collection;
3032
import java.util.Map;
3133
import java.util.stream.Collectors;
3234

@@ -44,14 +46,18 @@
4446
*/
4547
public abstract class BaseRevocationListService<C extends VerifiableCredential, S> implements RevocationListService {
4648
private final Cache<String, C> statusListCredentialCache;
49+
private final Collection<String> acceptedContentTypes;
50+
private final EdcHttpClient httpClient;
4751
private final Class<C> credentialClass;
4852
private final ObjectMapper objectMapper;
4953

50-
protected BaseRevocationListService(ObjectMapper mapper, long cacheValidity, Class<C> credentialClass) {
54+
protected BaseRevocationListService(ObjectMapper mapper, long cacheValidity, Collection<String> acceptedContentTypes, EdcHttpClient httpClient, Class<C> credentialClass) {
5155
this.objectMapper = mapper.copy()
52-
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) // technically, credential subjects and credential status can be objects AND Arrays
53-
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // let's make sure this is disabled, because the "@context" would cause problems
56+
.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) // technically, credential subjects and credential status can be objects AND Arrays
57+
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // let's make sure this is disabled, because the "@context" would cause problems
5458
statusListCredentialCache = new Cache<>(this::downloadStatusListCredential, cacheValidity);
59+
this.acceptedContentTypes = acceptedContentTypes;
60+
this.httpClient = httpClient;
5561
this.credentialClass = credentialClass;
5662
}
5763

@@ -60,11 +66,11 @@ public Result<Void> checkValidity(CredentialStatus credential) {
6066
var credentialStatus = getCredentialStatus(credential);
6167
var credentialIndex = getStatusIndex(credentialStatus);
6268
return preliminaryChecks(credentialStatus)
63-
.compose(v -> validateStatusPurpose(credentialStatus))
64-
.compose(v -> getStatusEntryValue(credentialStatus))
65-
.compose(status -> status != null ?
66-
Result.failure("Credential status is '%s', status at index %d is '1'".formatted(status, credentialIndex)) :
67-
Result.success());
69+
.compose(v -> validateStatusPurpose(credentialStatus))
70+
.compose(v -> getStatusEntryValue(credentialStatus))
71+
.compose(status -> status != null ?
72+
Result.failure("Credential status is '%s', status at index %d is '1'".formatted(status, credentialIndex)) :
73+
Result.success());
6874
}
6975

7076
@Override
@@ -74,17 +80,17 @@ public Result<String> getStatusPurpose(VerifiableCredential credential) {
7480
}
7581

7682
var res = credential.getCredentialStatus().stream()
77-
.map(this::getCredentialStatus)
78-
.map(this::getStatusEntryValue)
79-
.collect(Collectors.groupingBy(AbstractResult::succeeded));
83+
.map(this::getCredentialStatus)
84+
.map(this::getStatusEntryValue)
85+
.collect(Collectors.groupingBy(AbstractResult::succeeded));
8086

8187
if (res.containsKey(false)) { //if any failed
8288
return Result.failure(res.get(false).stream().map(AbstractResult::getFailureDetail).toList());
8389
}
8490

8591
var list = res.get(true).stream()
86-
.filter(r -> r.getContent() != null)
87-
.map(AbstractResult::getContent).toList();
92+
.filter(r -> r.getContent() != null)
93+
.map(AbstractResult::getContent).toList();
8894

8995
return list.isEmpty() ? success(null) : success(String.join(", ", list));
9096
}
@@ -107,13 +113,17 @@ protected Result<Void> preliminaryChecks(S credentialStatus) {
107113
* @return the VerifiableCredential
108114
* @throws EdcException if it could not be downloaded
109115
*/
110-
protected C getCredential(String credentialUrl) {
111-
var credential = statusListCredentialCache.get(credentialUrl);
112-
// credential is cached, but expired -> download again
113-
if (credential != null && credential.getExpirationDate() != null && credential.getExpirationDate().isBefore(Instant.now())) {
114-
statusListCredentialCache.evict(credentialUrl);
116+
protected Result<C> getCredential(String credentialUrl) {
117+
try {
118+
var credential = statusListCredentialCache.get(credentialUrl);
119+
// credential is cached, but expired -> download again
120+
if (credential != null && credential.getExpirationDate() != null && credential.getExpirationDate().isBefore(Instant.now())) {
121+
statusListCredentialCache.evict(credentialUrl);
122+
}
123+
return success(statusListCredentialCache.get(credentialUrl));
124+
} catch (IllegalArgumentException ex) {
125+
return Result.failure(ex.getMessage());
115126
}
116-
return statusListCredentialCache.get(credentialUrl);
117127
}
118128

119129
/**
@@ -150,8 +160,19 @@ protected C getCredential(String credentialUrl) {
150160
protected abstract S getCredentialStatus(CredentialStatus credentialStatus);
151161

152162
private C downloadStatusListCredential(String credentialUrl) {
153-
try {
154-
return objectMapper.readValue(URI.create(credentialUrl).toURL(), credentialClass);
163+
var request = new Request.Builder()
164+
.url(credentialUrl)
165+
.header("Accept", String.join(",", acceptedContentTypes))
166+
.get()
167+
.build();
168+
try (var response = httpClient.execute(request)) {
169+
if (response.isSuccessful()) {
170+
if (response.body() == null) {
171+
throw new EdcException("Response body is null for URL: " + credentialUrl);
172+
}
173+
return objectMapper.readValue(response.body().byteStream(), credentialClass);
174+
}
175+
throw new IllegalArgumentException("Failed to download status list credential from " + credentialUrl + ": " + response.code() + " " + response.message());
155176
} catch (IOException e) {
156177
throw new EdcException(e);
157178
}

extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationService.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
package org.eclipse.edc.iam.verifiablecredentials.revocation.bitstring;
1616

1717
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import org.eclipse.edc.http.spi.EdcHttpClient;
1819
import org.eclipse.edc.iam.verifiablecredentials.revocation.BaseRevocationListService;
1920
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
2021
import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.BitString;
@@ -24,6 +25,7 @@
2425
import org.eclipse.edc.spi.result.Result;
2526

2627
import java.util.Base64;
28+
import java.util.Collection;
2729

2830
import static org.eclipse.edc.spi.result.Result.success;
2931

@@ -33,8 +35,8 @@
3335
*/
3436
public class BitstringStatusListRevocationService extends BaseRevocationListService<BitstringStatusListCredential, BitstringStatusListStatus> {
3537

36-
public BitstringStatusListRevocationService(ObjectMapper mapper, long cacheValidity) {
37-
super(mapper, cacheValidity, BitstringStatusListCredential.class);
38+
public BitstringStatusListRevocationService(ObjectMapper mapper, long cacheValidity, Collection<String> acceptedContentTypes, EdcHttpClient httpClient) {
39+
super(mapper, cacheValidity, acceptedContentTypes, httpClient, BitstringStatusListCredential.class);
3840
}
3941

4042
@Override
@@ -48,7 +50,11 @@ protected Result<Void> preliminaryChecks(BitstringStatusListStatus credentialSta
4850

4951
@Override
5052
protected Result<String> getStatusEntryValue(BitstringStatusListStatus credentialStatus) {
51-
var bitStringCredential = getCredential(credentialStatus.getStatusListCredential());
53+
var bitStringCredentialResult = getCredential(credentialStatus.getStatusListCredential());
54+
if (bitStringCredentialResult.failed()) {
55+
return bitStringCredentialResult.mapEmpty();
56+
}
57+
var bitStringCredential = bitStringCredentialResult.getContent();
5258

5359
var bitString = bitStringCredential.encodedList();
5460
var decoder = Base64.getDecoder();
@@ -92,7 +98,10 @@ protected Result<Void> validateStatusPurpose(BitstringStatusListStatus credentia
9298

9399
var credentialUrl = credentialStatus.getStatusListCredential();
94100
var statusListCredential = getCredential(credentialUrl);
95-
var credentialStatusPurpose = statusListCredential.statusPurpose();
101+
if (statusListCredential.failed()) {
102+
return statusListCredential.mapEmpty();
103+
}
104+
var credentialStatusPurpose = statusListCredential.getContent().statusPurpose();
96105

97106
if (!statusPurpose.equalsIgnoreCase(credentialStatusPurpose)) {
98107
return Result.failure("Credential's statusPurpose value must match the statusPurpose of the Bitstring Credential: '%s' != '%s'".formatted(statusPurpose, credentialStatusPurpose));

extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist2021/StatusList2021RevocationService.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
package org.eclipse.edc.iam.verifiablecredentials.revocation.statuslist2021;
1616

1717
import com.fasterxml.jackson.databind.ObjectMapper;
18+
import org.eclipse.edc.http.spi.EdcHttpClient;
1819
import org.eclipse.edc.iam.verifiablecredentials.revocation.BaseRevocationListService;
1920
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
2021
import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.BitString;
2122
import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Credential;
2223
import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Status;
2324
import org.eclipse.edc.spi.result.Result;
2425

26+
import java.util.Collection;
27+
2528
import static org.eclipse.edc.spi.result.Result.success;
2629

2730

@@ -31,8 +34,8 @@
3134
*/
3235
public class StatusList2021RevocationService extends BaseRevocationListService<StatusList2021Credential, StatusList2021Status> {
3336

34-
public StatusList2021RevocationService(ObjectMapper objectMapper, long cacheValidity) {
35-
super(objectMapper, cacheValidity, StatusList2021Credential.class);
37+
public StatusList2021RevocationService(ObjectMapper objectMapper, long cacheValidity, Collection<String> acceptedContentTypes, EdcHttpClient httpClient) {
38+
super(objectMapper, cacheValidity, acceptedContentTypes, httpClient, StatusList2021Credential.class);
3639
}
3740

3841
@Override
@@ -45,9 +48,11 @@ protected Result<String> getStatusEntryValue(StatusList2021Status credentialStat
4548
var index = credentialStatus.getStatusListIndex();
4649
var slCredUrl = credentialStatus.getStatusListCredential();
4750
var credential = getCredential(slCredUrl);
51+
if (credential.failed()) {
52+
return credential.mapEmpty();
53+
}
4854

49-
50-
var bitStringResult = BitString.Parser.newInstance().parse(credential.encodedList());
55+
var bitStringResult = BitString.Parser.newInstance().parse(credential.getContent().encodedList());
5156

5257
if (bitStringResult.failed()) {
5358
return bitStringResult.mapEmpty();
@@ -64,10 +69,13 @@ protected Result<String> getStatusEntryValue(StatusList2021Status credentialStat
6469
@Override
6570
protected Result<Void> validateStatusPurpose(StatusList2021Status credentialStatus) {
6671
var slCred = getCredential(credentialStatus.getStatusListCredential());
72+
if (slCred.failed()) {
73+
return slCred.mapEmpty();
74+
}
6775

6876
// check that the "statusPurpose" values match
6977
var purpose = credentialStatus.getStatusListPurpose();
70-
var slCredPurpose = slCred.statusPurpose();
78+
var slCredPurpose = slCred.getContent().statusPurpose();
7179
if (!purpose.equalsIgnoreCase(slCredPurpose)) {
7280
return Result.failure("Credential's statusPurpose value must match the status list's purpose: '%s' != '%s'".formatted(purpose, slCredPurpose));
7381
}

0 commit comments

Comments
 (0)