Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add getStatusPurpose method #4172

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
maven/mavencentral/com.apicatalog/carbon-did/0.3.0, Apache-2.0, approved, clearlydefined

Check warning on line 1 in DEPENDENCIES

View workflow job for this annotation

GitHub Actions / check / Dash-Verify-Licenses

Restricted Dependencies found

Some dependencies are marked 'restricted' - please review them
maven/mavencentral/com.apicatalog/copper-multibase/0.5.0, Apache-2.0, approved, #14501
maven/mavencentral/com.apicatalog/copper-multicodec/0.1.1, Apache-2.0, approved, #14500
maven/mavencentral/com.apicatalog/iron-ed25519-cryptosuite-2020/0.14.0, Apache-2.0, approved, #14503
Expand Down Expand Up @@ -80,8 +80,8 @@
maven/mavencentral/com.lmax/disruptor/3.4.4, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.networknt/json-schema-validator/1.0.76, Apache-2.0, approved, CQ22638
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.28, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.38, , restricted, clearlydefined
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.16.0, , restricted, clearlydefined
maven/mavencentral/com.nimbusds/nimbus-jose-jwt/9.38, Apache-2.0, approved, clearlydefined
maven/mavencentral/com.puppycrawl.tools/checkstyle/10.16.0, LGPL-2.1-or-later AND (Apache-2.0 AND LGPL-2.1-or-later) AND Apache-2.0, approved, #14689
maven/mavencentral/com.samskivert/jmustache/1.15, BSD-2-Clause, approved, clearlydefined
maven/mavencentral/com.squareup.okhttp3/okhttp-dnsoverhttps/4.12.0, Apache-2.0, approved, #11159
maven/mavencentral/com.squareup.okhttp3/okhttp/4.12.0, Apache-2.0, approved, #11156
Expand Down Expand Up @@ -125,7 +125,7 @@
maven/mavencentral/io.netty/netty-transport-native-unix-common/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.netty/netty-transport/4.1.86.Final, Apache-2.0 AND BSD-3-Clause AND MIT, approved, CQ20926
maven/mavencentral/io.opentelemetry.instrumentation/opentelemetry-instrumentation-annotations/1.32.0, Apache-2.0, approved, #11684
maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.3.1-alpha, , restricted, clearlydefined
maven/mavencentral/io.opentelemetry.proto/opentelemetry-proto/1.3.1-alpha, None, restricted, #14688
maven/mavencentral/io.opentelemetry/opentelemetry-api/1.32.0, Apache-2.0, approved, #11682
maven/mavencentral/io.opentelemetry/opentelemetry-context/1.32.0, Apache-2.0, approved, #11683
maven/mavencentral/io.prometheus/simpleclient/0.16.0, Apache-2.0, approved, clearlydefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@
import org.eclipse.edc.iam.verifiablecredentials.spi.model.statuslist.BitString;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.statuslist.StatusList2021Credential;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.statuslist.StatusListStatus;
import org.eclipse.edc.spi.result.AbstractResult;
import org.eclipse.edc.spi.result.Result;
import org.eclipse.edc.util.collection.Cache;

import java.io.IOException;
import java.net.URI;
import java.util.Map;
import java.util.stream.Collectors;

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

/**
* Service to check if a particular {@link VerifiableCredential} is "valid", where "validity" is defined as not revoked and not suspended.
Expand All @@ -44,7 +48,7 @@ public StatusList2021RevocationService(ObjectMapper objectMapper, long cacheVali
this.objectMapper = objectMapper.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
cache = new Cache<>(this::updateCredential, cacheValidity);
cache = new Cache<>(this::downloadStatusListCredential, cacheValidity);
}

@Override
Expand All @@ -55,7 +59,43 @@ public Result<Void> checkValidity(VerifiableCredential credential) {
.orElse(Result.failure("Could not check the validity of the credential with ID '%s'".formatted(credential.getId())));
}

@Override
public Result<String> getStatusPurpose(VerifiableCredential credential) {
if (credential.getCredentialStatus().isEmpty()) {
return success(null);
}
var res = credential.getCredentialStatus().stream()
.map(StatusListStatus::parse)
.map(this::getStatusInternal)
.collect(Collectors.groupingBy(AbstractResult::succeeded)); //partition by succeeded/failed

if (res.containsKey(false)) {
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();

// get(0) is OK, because there should only be 1 credentialStatus
return list.isEmpty() ? success(null) : success(list.get(0));

}

private Result<Void> checkStatus(StatusListStatus status) {
var index = status.getStatusListIndex();
return getStatusInternal(status)
.compose(purpose -> purpose != null ?
Result.failure("Credential status is '%s', status at index %d is '1'".formatted(purpose, index)) :
success());
}

/**
* Obtains the status purpose for a particular credentialStatus entry if it is set, otherwise returns a successful result with a {@code null} content.
* So, a successful result with a non-null content indicates, that the respective credentialStatus is set.
*/
private Result<String> getStatusInternal(StatusListStatus status) {
var index = status.getStatusListIndex();
var slCredUrl = status.getStatusListCredential();
var credential = cache.get(slCredUrl);
var slCred = StatusList2021Credential.parse(credential);
Expand All @@ -74,15 +114,14 @@ private Result<Void> checkStatus(StatusListStatus status) {
}
var bitString = bitStringResult.getContent();

var index = status.getStatusListIndex();
// check that the value at index in the bitset is "1"
if (bitString.get(index)) {
return Result.failure("Credential status is '%s', status at index %d is '1'".formatted(purpose, index));
return success(purpose);
}
return Result.success();
return success(null);
}

private VerifiableCredential updateCredential(String credentialUrl) {
private VerifiableCredential downloadStatusListCredential(String credentialUrl) {
try {
return objectMapper.readValue(URI.create(credentialUrl).toURL(), VerifiableCredential.class);
} catch (IOException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ void tearDown() {
}

@Test
void checkRevocation_shenSubjectIsArray() {
void checkRevocation_whenSubjectIsArray() {
clientAndServer.reset();
clientAndServer.when(request().withMethod("GET").withPath("/credentials/status/3"))
.respond(HttpResponse.response().withStatusCode(200).withBody(TestData.STATUS_LIST_CREDENTIAL_SUBJECT_IS_ARRAY));
Expand All @@ -66,7 +66,6 @@ void checkRevocation_shenSubjectIsArray() {
assertThat(revocationService.checkValidity(credential)).isSucceeded();
}


@Test
void checkRevocation_whenNotCached_valid() {
var credential = TestFunctions.createCredentialBuilder().credentialStatus(new CredentialStatus("test-id", "StatusList2021",
Expand Down Expand Up @@ -110,4 +109,58 @@ void checkRevocation_whenCached_valid() {
assertThat(revocationService.checkValidity(credential)).isSucceeded();
clientAndServer.verify(request(), VerificationTimes.exactly(1));
}

@Test
void getStatusPurposes_whenSingleCredentialStatusRevoked() {
clientAndServer.reset();
clientAndServer.when(request().withMethod("GET").withPath("/credentials/status/3"))
.respond(HttpResponse.response().withStatusCode(200).withBody(TestData.STATUS_LIST_CREDENTIAL_SINGLE_SUBJECT));
var credential = TestFunctions.createCredentialBuilder().credentialStatus(new CredentialStatus("test-id", "StatusList2021",
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");
}

@Test
void getStatusPurposes_whenMultipleCredentialStatusRevoked() {
clientAndServer.reset();
clientAndServer.when(request().withMethod("GET").withPath("/credentials/status/3"))
.respond(HttpResponse.response().withStatusCode(200).withBody(TestData.STATUS_LIST_CREDENTIAL_SUBJECT_IS_ARRAY));
var credential = TestFunctions.createCredentialBuilder().credentialStatus(new CredentialStatus("test-id", "StatusList2021",
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");
}

@Test
void getStatusPurpose_whenCredentialStatusNotActive() {
clientAndServer.reset();
clientAndServer.when(request().withMethod("GET").withPath("/credentials/status/3"))
.respond(HttpResponse.response().withStatusCode(200).withBody(TestData.STATUS_LIST_CREDENTIAL_SINGLE_SUBJECT));
var credential = TestFunctions.createCredentialBuilder().credentialStatus(new CredentialStatus("test-id", "StatusList2021",
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();
}

@Test
void getStatusPurpose_whenNoCredentialStatus() {
clientAndServer.reset();
clientAndServer.when(request().withMethod("GET").withPath("/credentials/status/3"))
.respond(HttpResponse.response().withStatusCode(200).withBody(TestData.STATUS_LIST_CREDENTIAL_SINGLE_SUBJECT));
var credential = TestFunctions.createCredentialBuilder().build();
assertThat(revocationService.getStatusPurpose(credential))
.isNotNull()
.isSucceeded();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static Result<Void> success() {
return new Result<>(null, null);
}

public static <T> Result<T> success(@NotNull T content) {
public static <T> Result<T> success(T content) {
return new Result<>(content, null);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,24 @@
* A credential is regarded as "valid" if its {@code statusPurpose} field matches the status list credential and if
* the value at the index indicated by {@code statusListIndex} is "1".
*/
@FunctionalInterface
public interface RevocationListService {
/**
* Check the "validity" of a credential, where validity is understood as not-revoked and not-suspended. Credentials that don't have
* a {@code credentialStatus} object are deemed valid. If the {@code credentialStatus} object is invalid, the credential is deemed invalid.
*/
Result<Void> checkValidity(VerifiableCredential credential);

/**
* Determines the status of a credential. If a {@code credentialStatus} object exists, the service will determin the "status purpose". It can be:
* <ul>
* <li>null: {@code credentialStatus} object not present, or status purpose is present but the status credential's encoded bitstring resolves a "0" at the status index.
* i.e. the credential is "not revoked" and "not suspended". </li>
* <li>suspended: credential is temporarily deactivated, i.e. the status credential's encoded bitstring resolves a "1" at the status index</li>
* <li>revoked: credential is permanently deactivated, i.e. the status credential's encoded bitstring resolves a "1" at the status index</li>
* </ul>
*
* @param credential The credential to inspect.
* @return either the status purpose, if the status is active, or null, if not active or not present. returns a failure if the status check failed, or the {@code credentialStatus} object is invalid.
*/
Result<String> getStatusPurpose(VerifiableCredential credential);
}
Loading