From 5e3215569bdc151966b263f2d941ce4b7d771ffd Mon Sep 17 00:00:00 2001 From: Paul Latzelsperger Date: Mon, 9 Jun 2025 15:22:39 +0200 Subject: [PATCH] feat: download status list credentials with EdcHttpClient --- .../core/IdentityAndTrustExtension.java | 28 +++-- .../core/IdentityAndTrustExtensionTest.java | 3 - .../verifiable-credentials/build.gradle.kts | 1 + .../revocation/BaseRevocationListService.java | 65 +++++++---- .../BitstringStatusListRevocationService.java | 17 ++- .../StatusList2021RevocationService.java | 18 ++- ...stringStatusListRevocationServiceTest.java | 106 +++++++++++------- .../StatusList2021RevocationServiceTest.java | 28 ++++- 8 files changed, 176 insertions(+), 90 deletions(-) diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java index c8d08af749b..e992ad3dd4b 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/main/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtension.java @@ -70,6 +70,8 @@ import java.net.URISyntaxException; import java.time.Clock; +import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; @@ -80,6 +82,7 @@ import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_NAMESPACE_V_1_0; import static org.eclipse.edc.iam.identitytrust.spi.DcpConstants.DSPACE_DCP_V_1_0_CONTEXT; import static org.eclipse.edc.iam.verifiablecredentials.spi.VcConstants.STATUSLIST_2021_URL; +import static org.eclipse.edc.iam.verifiablecredentials.spi.validation.TrustedIssuerRegistry.WILDCARD; import static org.eclipse.edc.spi.constants.CoreConstants.JSON_LD; import static org.eclipse.edc.verifiablecredentials.jwt.JwtPresentationVerifier.JWT_VC_TOKEN_CONTEXT; @@ -157,6 +160,8 @@ public class IdentityAndTrustExtension implements ServiceExtension { private PresentationVerifier presentationVerifier; private CredentialServiceClient credentialServiceClient; private ScheduledFuture jtiEntryReaperThread; + @Setting(key = "edc.iam.credential.revocation.mimetype", description = "A comma-separated list of accepted content types of the revocation list credential.", defaultValue = WILDCARD) + private String contentTypes; @Override public void initialize(ServiceExtensionContext context) { @@ -185,15 +190,20 @@ public void initialize(ServiceExtensionContext context) { participantAgentService.register(participantAgentServiceExtension); // register revocation services - revocationServiceRegistry.addService(StatusList2021Status.TYPE, new StatusList2021RevocationService(typeManager.getMapper(), revocationCacheValidity)); - revocationServiceRegistry.addService(BitstringStatusListStatus.TYPE, new BitstringStatusListRevocationService(typeManager.getMapper(), revocationCacheValidity)); + var acceptedContentTypes = parseAcceptedContentTypes(contentTypes); + revocationServiceRegistry.addService(StatusList2021Status.TYPE, new StatusList2021RevocationService(typeManager.getMapper(), revocationCacheValidity, acceptedContentTypes, httpClient)); + revocationServiceRegistry.addService(BitstringStatusListStatus.TYPE, new BitstringStatusListRevocationService(typeManager.getMapper(), revocationCacheValidity, acceptedContentTypes, httpClient)); + } + + private Collection parseAcceptedContentTypes(String contentTypes) { + return List.of(contentTypes.split(",")); } @Override public void start() { if (activateJtiValidation) { jtiEntryReaperThread = executorInstrumentation.instrument(Executors.newSingleThreadScheduledExecutor(), "JTI Validation Entry Reaper Thread") - .scheduleAtFixedRate(jtiValidationStore::deleteExpired, reaperCleanupPeriod, reaperCleanupPeriod, TimeUnit.SECONDS); + .scheduleAtFixedRate(jtiValidationStore::deleteExpired, reaperCleanupPeriod, reaperCleanupPeriod, TimeUnit.SECONDS); } } @@ -243,12 +253,12 @@ public PresentationVerifier createPresentationVerifier(ServiceExtensionContext c var jwtVerifier = new JwtPresentationVerifier(typeManager, JSON_LD, tokenValidationService, rulesRegistry, didPublicKeyResolver); var ldpVerifier = LdpVerifier.Builder.newInstance() - .signatureSuites(signatureSuiteRegistry) - .jsonLd(jsonLd) - .typeManager(typeManager) - .typeContext(JSON_LD) - .methodResolver(new DidMethodResolver(didResolverRegistry)) - .build(); + .signatureSuites(signatureSuiteRegistry) + .jsonLd(jsonLd) + .typeManager(typeManager) + .typeContext(JSON_LD) + .methodResolver(new DidMethodResolver(didResolverRegistry)) + .build(); presentationVerifier = new MultiFormatPresentationVerifier(issuerId, jwtVerifier, ldpVerifier); } diff --git a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtensionTest.java b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtensionTest.java index 90d97f8414e..53cbaadae94 100644 --- a/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtensionTest.java +++ b/extensions/common/iam/identity-trust/identity-trust-core/src/test/java/org/eclipse/edc/iam/identitytrust/core/IdentityAndTrustExtensionTest.java @@ -66,10 +66,7 @@ void setUp(ServiceExtensionContext context) { @Test void verifyCorrectService(ServiceExtensionContext context, ObjectFactory objectFactory) { - - var is = objectFactory.constructInstance(IdentityAndTrustExtension.class).createIdentityService(context); - assertThat(is).isInstanceOf(IdentityAndTrustService.class); } diff --git a/extensions/common/iam/verifiable-credentials/build.gradle.kts b/extensions/common/iam/verifiable-credentials/build.gradle.kts index 987c63ee3df..03296f699b4 100644 --- a/extensions/common/iam/verifiable-credentials/build.gradle.kts +++ b/extensions/common/iam/verifiable-credentials/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { testImplementation(testFixtures(project(":spi:common:verifiable-credentials-spi"))) testImplementation(libs.mockserver.netty) testImplementation(project(":tests:junit-base")) + testImplementation(project(":core:common:lib:http-lib")) testImplementation(project(":core:common:lib:util-lib")) testImplementation(testFixtures(project(":spi:common:identity-trust-spi"))) //test functions } diff --git a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/BaseRevocationListService.java b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/BaseRevocationListService.java index f715d465309..e1136debdd4 100644 --- a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/BaseRevocationListService.java +++ b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/BaseRevocationListService.java @@ -16,6 +16,8 @@ import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; +import okhttp3.Request; +import org.eclipse.edc.http.spi.EdcHttpClient; import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential; @@ -25,8 +27,8 @@ import org.eclipse.edc.util.collection.Cache; import java.io.IOException; -import java.net.URI; import java.time.Instant; +import java.util.Collection; import java.util.Map; import java.util.stream.Collectors; @@ -44,14 +46,18 @@ */ public abstract class BaseRevocationListService implements RevocationListService { private final Cache statusListCredentialCache; + private final Collection acceptedContentTypes; + private final EdcHttpClient httpClient; private final Class credentialClass; private final ObjectMapper objectMapper; - protected BaseRevocationListService(ObjectMapper mapper, long cacheValidity, Class credentialClass) { + protected BaseRevocationListService(ObjectMapper mapper, long cacheValidity, Collection acceptedContentTypes, EdcHttpClient httpClient, Class credentialClass) { this.objectMapper = mapper.copy() - .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) // technically, credential subjects and credential status can be objects AND Arrays - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // let's make sure this is disabled, because the "@context" would cause problems + .enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY) // technically, credential subjects and credential status can be objects AND Arrays + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // let's make sure this is disabled, because the "@context" would cause problems statusListCredentialCache = new Cache<>(this::downloadStatusListCredential, cacheValidity); + this.acceptedContentTypes = acceptedContentTypes; + this.httpClient = httpClient; this.credentialClass = credentialClass; } @@ -60,11 +66,11 @@ public Result checkValidity(CredentialStatus credential) { var credentialStatus = getCredentialStatus(credential); var credentialIndex = getStatusIndex(credentialStatus); return preliminaryChecks(credentialStatus) - .compose(v -> validateStatusPurpose(credentialStatus)) - .compose(v -> getStatusEntryValue(credentialStatus)) - .compose(status -> status != null ? - Result.failure("Credential status is '%s', status at index %d is '1'".formatted(status, credentialIndex)) : - Result.success()); + .compose(v -> validateStatusPurpose(credentialStatus)) + .compose(v -> getStatusEntryValue(credentialStatus)) + .compose(status -> status != null ? + Result.failure("Credential status is '%s', status at index %d is '1'".formatted(status, credentialIndex)) : + Result.success()); } @Override @@ -74,17 +80,17 @@ public Result getStatusPurpose(VerifiableCredential credential) { } var res = credential.getCredentialStatus().stream() - .map(this::getCredentialStatus) - .map(this::getStatusEntryValue) - .collect(Collectors.groupingBy(AbstractResult::succeeded)); + .map(this::getCredentialStatus) + .map(this::getStatusEntryValue) + .collect(Collectors.groupingBy(AbstractResult::succeeded)); if (res.containsKey(false)) { //if any failed return Result.failure(res.get(false).stream().map(AbstractResult::getFailureDetail).toList()); } var list = res.get(true).stream() - .filter(r -> r.getContent() != null) - .map(AbstractResult::getContent).toList(); + .filter(r -> r.getContent() != null) + .map(AbstractResult::getContent).toList(); return list.isEmpty() ? success(null) : success(String.join(", ", list)); } @@ -107,13 +113,17 @@ protected Result preliminaryChecks(S credentialStatus) { * @return the VerifiableCredential * @throws EdcException if it could not be downloaded */ - protected C getCredential(String credentialUrl) { - var credential = statusListCredentialCache.get(credentialUrl); - // credential is cached, but expired -> download again - if (credential != null && credential.getExpirationDate() != null && credential.getExpirationDate().isBefore(Instant.now())) { - statusListCredentialCache.evict(credentialUrl); + protected Result getCredential(String credentialUrl) { + try { + var credential = statusListCredentialCache.get(credentialUrl); + // credential is cached, but expired -> download again + if (credential != null && credential.getExpirationDate() != null && credential.getExpirationDate().isBefore(Instant.now())) { + statusListCredentialCache.evict(credentialUrl); + } + return success(statusListCredentialCache.get(credentialUrl)); + } catch (IllegalArgumentException ex) { + return Result.failure(ex.getMessage()); } - return statusListCredentialCache.get(credentialUrl); } /** @@ -150,8 +160,19 @@ protected C getCredential(String credentialUrl) { protected abstract S getCredentialStatus(CredentialStatus credentialStatus); private C downloadStatusListCredential(String credentialUrl) { - try { - return objectMapper.readValue(URI.create(credentialUrl).toURL(), credentialClass); + var request = new Request.Builder() + .url(credentialUrl) + .header("Accept", String.join(",", acceptedContentTypes)) + .get() + .build(); + try (var response = httpClient.execute(request)) { + if (response.isSuccessful()) { + if (response.body() == null) { + throw new EdcException("Response body is null for URL: " + credentialUrl); + } + return objectMapper.readValue(response.body().byteStream(), credentialClass); + } + throw new IllegalArgumentException("Failed to download status list credential from " + credentialUrl + ": " + response.code() + " " + response.message()); } catch (IOException e) { throw new EdcException(e); } diff --git a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationService.java b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationService.java index 5b63d7850d3..9c97591889a 100644 --- a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationService.java +++ b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationService.java @@ -15,6 +15,7 @@ package org.eclipse.edc.iam.verifiablecredentials.revocation.bitstring; import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.http.spi.EdcHttpClient; import org.eclipse.edc.iam.verifiablecredentials.revocation.BaseRevocationListService; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.BitString; @@ -24,6 +25,7 @@ import org.eclipse.edc.spi.result.Result; import java.util.Base64; +import java.util.Collection; import static org.eclipse.edc.spi.result.Result.success; @@ -33,8 +35,8 @@ */ public class BitstringStatusListRevocationService extends BaseRevocationListService { - public BitstringStatusListRevocationService(ObjectMapper mapper, long cacheValidity) { - super(mapper, cacheValidity, BitstringStatusListCredential.class); + public BitstringStatusListRevocationService(ObjectMapper mapper, long cacheValidity, Collection acceptedContentTypes, EdcHttpClient httpClient) { + super(mapper, cacheValidity, acceptedContentTypes, httpClient, BitstringStatusListCredential.class); } @Override @@ -48,7 +50,11 @@ protected Result preliminaryChecks(BitstringStatusListStatus credentialSta @Override protected Result getStatusEntryValue(BitstringStatusListStatus credentialStatus) { - var bitStringCredential = getCredential(credentialStatus.getStatusListCredential()); + var bitStringCredentialResult = getCredential(credentialStatus.getStatusListCredential()); + if (bitStringCredentialResult.failed()) { + return bitStringCredentialResult.mapEmpty(); + } + var bitStringCredential = bitStringCredentialResult.getContent(); var bitString = bitStringCredential.encodedList(); var decoder = Base64.getDecoder(); @@ -92,7 +98,10 @@ protected Result validateStatusPurpose(BitstringStatusListStatus credentia var credentialUrl = credentialStatus.getStatusListCredential(); var statusListCredential = getCredential(credentialUrl); - var credentialStatusPurpose = statusListCredential.statusPurpose(); + if (statusListCredential.failed()) { + return statusListCredential.mapEmpty(); + } + var credentialStatusPurpose = statusListCredential.getContent().statusPurpose(); if (!statusPurpose.equalsIgnoreCase(credentialStatusPurpose)) { return Result.failure("Credential's statusPurpose value must match the statusPurpose of the Bitstring Credential: '%s' != '%s'".formatted(statusPurpose, credentialStatusPurpose)); diff --git a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist2021/StatusList2021RevocationService.java b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist2021/StatusList2021RevocationService.java index 9e6235c9cd4..8362c2e1eef 100644 --- a/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist2021/StatusList2021RevocationService.java +++ b/extensions/common/iam/verifiable-credentials/src/main/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist2021/StatusList2021RevocationService.java @@ -15,6 +15,7 @@ package org.eclipse.edc.iam.verifiablecredentials.revocation.statuslist2021; import com.fasterxml.jackson.databind.ObjectMapper; +import org.eclipse.edc.http.spi.EdcHttpClient; import org.eclipse.edc.iam.verifiablecredentials.revocation.BaseRevocationListService; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.BitString; @@ -22,6 +23,8 @@ import org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Status; import org.eclipse.edc.spi.result.Result; +import java.util.Collection; + import static org.eclipse.edc.spi.result.Result.success; @@ -31,8 +34,8 @@ */ public class StatusList2021RevocationService extends BaseRevocationListService { - public StatusList2021RevocationService(ObjectMapper objectMapper, long cacheValidity) { - super(objectMapper, cacheValidity, StatusList2021Credential.class); + public StatusList2021RevocationService(ObjectMapper objectMapper, long cacheValidity, Collection acceptedContentTypes, EdcHttpClient httpClient) { + super(objectMapper, cacheValidity, acceptedContentTypes, httpClient, StatusList2021Credential.class); } @Override @@ -45,9 +48,11 @@ protected Result getStatusEntryValue(StatusList2021Status credentialStat var index = credentialStatus.getStatusListIndex(); var slCredUrl = credentialStatus.getStatusListCredential(); var credential = getCredential(slCredUrl); + if (credential.failed()) { + return credential.mapEmpty(); + } - - var bitStringResult = BitString.Parser.newInstance().parse(credential.encodedList()); + var bitStringResult = BitString.Parser.newInstance().parse(credential.getContent().encodedList()); if (bitStringResult.failed()) { return bitStringResult.mapEmpty(); @@ -64,10 +69,13 @@ protected Result getStatusEntryValue(StatusList2021Status credentialStat @Override protected Result validateStatusPurpose(StatusList2021Status credentialStatus) { var slCred = getCredential(credentialStatus.getStatusListCredential()); + if (slCred.failed()) { + return slCred.mapEmpty(); + } // check that the "statusPurpose" values match var purpose = credentialStatus.getStatusListPurpose(); - var slCredPurpose = slCred.statusPurpose(); + var slCredPurpose = slCred.getContent().statusPurpose(); if (!purpose.equalsIgnoreCase(slCredPurpose)) { return Result.failure("Credential's statusPurpose value must match the status list's purpose: '%s' != '%s'".formatted(purpose, slCredPurpose)); } diff --git a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationServiceTest.java b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationServiceTest.java index 5cf3c58ca46..cceaad9bd37 100644 --- a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationServiceTest.java +++ b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/bitstring/BitstringStatusListRevocationServiceTest.java @@ -16,6 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.failsafe.RetryPolicy; +import okhttp3.OkHttpClient; +import org.eclipse.edc.http.client.EdcHttpClientImpl; import org.eclipse.edc.iam.verifiablecredentials.TestData; import org.eclipse.edc.iam.verifiablecredentials.spi.TestFunctions; import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus; @@ -34,6 +37,7 @@ import java.util.List; import java.util.Map; +import static java.util.Collections.singleton; import static org.eclipse.edc.iam.verifiablecredentials.TestData.BitstringStatusList.BITSTRING_STATUS_LIST_CREDENTIAL_ARRAY_SUBJECT_TEMPLATE; import static org.eclipse.edc.iam.verifiablecredentials.TestData.BitstringStatusList.BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE; import static org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.bitstringstatuslist.BitstringStatusListCredential.BITSTRING_STATUSLIST_CREDENTIAL; @@ -44,6 +48,7 @@ import static org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.bitstringstatuslist.BitstringStatusListStatus.STATUS_LIST_SIZE; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.eclipse.edc.util.io.Ports.getFreePort; +import static org.mockito.Mockito.mock; import static org.mockserver.model.HttpRequest.request; class BitstringStatusListRevocationServiceTest { @@ -52,7 +57,7 @@ class BitstringStatusListRevocationServiceTest { private static final int NOT_REVOKED_INDEX = 15; private final BitstringStatusListRevocationService revocationService = new BitstringStatusListRevocationService(new ObjectMapper().registerModule(new JavaTimeModule()), - 5 * 60 * 1000); + 5 * 60 * 1000, singleton("application/vc+jwt"), new EdcHttpClientImpl(new OkHttpClient(), RetryPolicy.ofDefaults(), mock())); private ClientAndServer clientAndServer; @BeforeEach @@ -179,6 +184,21 @@ void checkValidity_whenSubjectIsArray_revoked() { assertThat(revocationService.checkValidity(credential)).isFailed() .detail().isEqualTo("Credential status is 'revocation', status at index 10 is '1'"); } + + @Test + void checkValidity_wrongContentType_expect415() { + clientAndServer.reset() + .when(request().withMethod("GET").withPath("/credentials/status/3")) + .respond(HttpResponse.response().withStatusCode(415)); + var credential = new CredentialStatus("test-id", BITSTRING_STATUSLIST_CREDENTIAL, + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, REVOKED_INDEX, + STATUS_LIST_SIZE, 1, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort()))); + assertThat(revocationService.checkValidity(credential)).isFailed() + .detail() + .matches("Failed to download status list credential .* 415 Unsupported Media Type"); + } } @Nested @@ -191,11 +211,11 @@ void getStatusPurpose_singleStatusSet() { .when(request().withMethod("GET").withPath("/credentials/status/3")) .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("revocation", generateBitstring(REVOKED_INDEX, 1)))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "revocation", - STATUS_LIST_INDEX, REVOKED_INDEX, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, REVOKED_INDEX, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("revocation"); } @@ -207,13 +227,13 @@ void getStatusPurpose_singleStatusSet_message() { .when(request().withMethod("GET").withPath("/credentials/status/3")) .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("message", generateBitstring(REVOKED_INDEX, 1)))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "message", - STATUS_LIST_INDEX, REVOKED_INDEX, - STATUS_LIST_SIZE, 1, - STATUS_LIST_MESSAGES, List.of(new StatusMessage("0x0", "accepted"), new StatusMessage("0x1", "rejected")), - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "message", + STATUS_LIST_INDEX, REVOKED_INDEX, + STATUS_LIST_SIZE, 1, + STATUS_LIST_MESSAGES, List.of(new StatusMessage("0x0", "accepted"), new StatusMessage("0x1", "rejected")), + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("rejected"); } @@ -225,13 +245,13 @@ void getStatusPurpose_singleStatusNotSet_message() { .when(request().withMethod("GET").withPath("/credentials/status/3")) .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("message", generateBitstring()))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "message", - STATUS_LIST_INDEX, 69, - STATUS_LIST_SIZE, 1, - STATUS_LIST_MESSAGES, List.of(new StatusMessage("0x0", "accepted"), new StatusMessage("0x1", "rejected")), - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "message", + STATUS_LIST_INDEX, 69, + STATUS_LIST_SIZE, 1, + STATUS_LIST_MESSAGES, List.of(new StatusMessage("0x0", "accepted"), new StatusMessage("0x1", "rejected")), + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("accepted"); } @@ -244,11 +264,11 @@ void getStatusPurpose_singleStatus_notSet() { .when(request().withMethod("GET").withPath("/credentials/status/3")) .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("revocation", generateBitstring(REVOKED_INDEX, 1)))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "revocation", - STATUS_LIST_INDEX, NOT_REVOKED_INDEX, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, NOT_REVOKED_INDEX, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isNull(); } @@ -264,15 +284,15 @@ void getStatusPurpose_multipleStatus_onlyOneSet() { .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("suspension", generateBitstring()))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "revocation", - STATUS_LIST_INDEX, REVOKED_INDEX, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "suspension", - STATUS_LIST_INDEX, NOT_REVOKED_INDEX, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/4".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, REVOKED_INDEX, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "suspension", + STATUS_LIST_INDEX, NOT_REVOKED_INDEX, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/4".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("revocation"); } @@ -289,15 +309,15 @@ void getStatusPurpose_multipleCredentialStatus() { .respond(HttpResponse.response().withStatusCode(200).withBody(BITSTRING_STATUS_LIST_CREDENTIAL_PURPOSE_TEMPLATE.formatted("suspension", generateBitstring(69, 1)))); var credential = TestFunctions.createCredentialBuilder() - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "revocation", - STATUS_LIST_INDEX, 42, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, - Map.of(STATUS_LIST_PURPOSE, "suspension", - STATUS_LIST_INDEX, 69, - STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/4".formatted(clientAndServer.getPort())))) - .build(); + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, 42, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) + .credentialStatus(new CredentialStatus("test-id", BitstringStatusListStatus.TYPE, + Map.of(STATUS_LIST_PURPOSE, "suspension", + STATUS_LIST_INDEX, 69, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/4".formatted(clientAndServer.getPort())))) + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("revocation, suspension"); } diff --git a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist/StatusList2021RevocationServiceTest.java b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist/StatusList2021RevocationServiceTest.java index 87602a03900..449c5383cc6 100644 --- a/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist/StatusList2021RevocationServiceTest.java +++ b/extensions/common/iam/verifiable-credentials/src/test/java/org/eclipse/edc/iam/verifiablecredentials/revocation/statuslist/StatusList2021RevocationServiceTest.java @@ -16,6 +16,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import dev.failsafe.RetryPolicy; +import okhttp3.OkHttpClient; +import org.eclipse.edc.http.client.EdcHttpClientImpl; import org.eclipse.edc.iam.verifiablecredentials.TestData; import org.eclipse.edc.iam.verifiablecredentials.revocation.statuslist2021.StatusList2021RevocationService; import org.eclipse.edc.iam.verifiablecredentials.spi.TestFunctions; @@ -36,18 +39,20 @@ import java.util.Map; import java.util.stream.Stream; +import static java.util.Collections.singleton; import static org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Status.STATUS_LIST_CREDENTIAL; import static org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Status.STATUS_LIST_INDEX; import static org.eclipse.edc.iam.verifiablecredentials.spi.model.revocation.statuslist2021.StatusList2021Status.STATUS_LIST_PURPOSE; import static org.eclipse.edc.junit.assertions.AbstractResultAssert.assertThat; import static org.eclipse.edc.util.io.Ports.getFreePort; +import static org.mockito.Mockito.mock; import static org.mockserver.model.HttpRequest.request; class StatusList2021RevocationServiceTest { private static final int NOT_REVOKED_INDEX = 1; private static final int REVOKED_INDEX = 2; private final StatusList2021RevocationService revocationService = new StatusList2021RevocationService(new ObjectMapper().registerModule(new JavaTimeModule()), - 5 * 60 * 1000); + 5 * 60 * 1000, singleton("application/vc+jwt"), new EdcHttpClientImpl(new OkHttpClient(), RetryPolicy.ofDefaults(), mock())); private ClientAndServer clientAndServer; @BeforeEach @@ -115,6 +120,21 @@ void checkRevocation_whenCached_valid() { clientAndServer.verify(request(), VerificationTimes.exactly(1)); } + @Test + void checkValidity_wrongContentType_expect415() { + clientAndServer.reset() + .when(request().withMethod("GET").withPath("/credentials/status/3")) + .respond(HttpResponse.response().withStatusCode(415)); + var credential = new CredentialStatus("test-id", "StatusList2021Entry", + Map.of(STATUS_LIST_PURPOSE, "revocation", + STATUS_LIST_INDEX, NOT_REVOKED_INDEX, + STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort()))); + assertThat(revocationService.checkValidity(credential)).isFailed() + .detail() + .matches("Failed to download status list credential .* 415 Unsupported Media Type"); + } + + @ParameterizedTest @ArgumentsSource(SingleSubjectProvider.class) void getStatusPurposes_whenSingleCredentialStatusRevoked(String testData) { @@ -125,7 +145,7 @@ void getStatusPurposes_whenSingleCredentialStatusRevoked(String testData) { Map.of(STATUS_LIST_PURPOSE, "revocation", STATUS_LIST_INDEX, REVOKED_INDEX, STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("revocation"); } @@ -140,7 +160,7 @@ void getStatusPurposes_whenMultipleCredentialStatusRevoked(String testData) { Map.of(STATUS_LIST_PURPOSE, "revocation", STATUS_LIST_INDEX, REVOKED_INDEX, STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isEqualTo("revocation"); } @@ -155,7 +175,7 @@ void getStatusPurpose_whenCredentialStatusNotActive(String testData) { Map.of(STATUS_LIST_PURPOSE, "revocation", STATUS_LIST_INDEX, NOT_REVOKED_INDEX, STATUS_LIST_CREDENTIAL, "http://localhost:%d/credentials/status/3".formatted(clientAndServer.getPort())))) - .build(); + .build(); assertThat(revocationService.getStatusPurpose(credential)).isSucceeded() .isNull(); }