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: track credential state in DB #344

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
2 changes: 0 additions & 2 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-verifiable-credentials/0.14.0, Apache-2.0, approved, clearlydefined
Expand Down Expand Up @@ -27,12 +27,10 @@
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.15.1, Apache-2.0, approved, #8802
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.16.2, Apache-2.0, approved, #11855
maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.17.1, Apache-2.0, approved, #13669
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.17.0, Apache-2.0, approved, #14161
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jakarta-jsonp/2.17.1, Apache-2.0, approved, #14161
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.14.0, Apache-2.0, approved, #4699
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.15.1, Apache-2.0, approved, #7930
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.16.2, Apache-2.0, approved, #11853
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.0, Apache-2.0, approved, #14160
maven/mavencentral/com.fasterxml.jackson.datatype/jackson-datatype-jsr310/2.17.1, Apache-2.0, approved, #14160
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-base/2.17.1, Apache-2.0, approved, #14194
maven/mavencentral/com.fasterxml.jackson.jakarta.rs/jackson-jakarta-rs-json-provider/2.15.1, Apache-2.0, approved, #9236
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package org.eclipse.edc.identityhub;

import org.eclipse.edc.iam.identitytrust.spi.verification.SignatureSuiteRegistry;
import org.eclipse.edc.iam.verifiablecredentials.StatusList2021RevocationService;
import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService;
import org.eclipse.edc.identityhub.accesstoken.rules.ClaimIsPresentRule;
import org.eclipse.edc.identityhub.defaults.InMemoryCredentialStore;
import org.eclipse.edc.identityhub.defaults.InMemoryKeyPairResourceStore;
Expand All @@ -28,8 +30,10 @@
import org.eclipse.edc.runtime.metamodel.annotation.Extension;
import org.eclipse.edc.runtime.metamodel.annotation.Inject;
import org.eclipse.edc.runtime.metamodel.annotation.Provider;
import org.eclipse.edc.runtime.metamodel.annotation.Setting;
import org.eclipse.edc.spi.system.ServiceExtension;
import org.eclipse.edc.spi.system.ServiceExtensionContext;
import org.eclipse.edc.spi.types.TypeManager;
import org.eclipse.edc.token.spi.TokenValidationRulesRegistry;

import static org.eclipse.edc.identityhub.DefaultServicesExtension.NAME;
Expand All @@ -42,10 +46,14 @@
public class DefaultServicesExtension implements ServiceExtension {

public static final String NAME = "IdentityHub Default Services Extension";


public static final long DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS = 15 * 60 * 1000L;
@Setting(value = "Validity period of cached StatusList2021 credential entries in milliseconds.", defaultValue = DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS + "", type = "long")
public static final String REVOCATION_CACHE_VALIDITY = "edc.iam.credential.revocation.cache.validity";
@Inject
private TokenValidationRulesRegistry registry;
@Inject
private TypeManager typeManager;
private RevocationListService revocationService;

@Override
public String name() {
Expand Down Expand Up @@ -84,6 +92,15 @@ public ScopeToCriterionTransformer createScopeTransformer(ServiceExtensionContex
return new EdcScopeToCriterionTransformer();
}

@Provider
public RevocationListService createRevocationListService(ServiceExtensionContext context) {
if (revocationService == null) {
var validity = context.getConfig().getLong(REVOCATION_CACHE_VALIDITY, DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS);
revocationService = new StatusList2021RevocationService(typeManager.getMapper(), validity);
}
return revocationService;
}

@Provider(isDefault = true)
public SignatureSuiteRegistry createSignatureSuiteRegistry() {
return new InMemorySignatureSuiteRegistry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

import org.eclipse.edc.iam.did.spi.resolution.DidPublicKeyResolver;
import org.eclipse.edc.iam.identitytrust.spi.verification.SignatureSuiteRegistry;
import org.eclipse.edc.iam.verifiablecredentials.StatusList2021RevocationService;
import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
import org.eclipse.edc.identithub.verifiablepresentation.PresentationCreatorRegistryImpl;
Expand Down Expand Up @@ -82,9 +81,6 @@ public class CoreServicesExtension implements ServiceExtension {
@Setting(value = "Public key in PEM format")
public static final String PUBLIC_KEY_PEM = "edc.ih.iam.publickey.pem";

public static final long DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS = 15 * 60 * 1000L;
@Setting(value = "Validity period of cached StatusList2021 credential entries in milliseconds.", defaultValue = DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS + "", type = "long")
public static final String REVOCATION_CACHE_VALIDITY = "edc.iam.credential.revocation.cache.validity";

public static final String PRESENTATION_EXCHANGE_V_1_JSON = "presentation-exchange.v1.json";
public static final String PRESENTATION_QUERY_V_08_JSON = "iatp.v08.json";
Expand Down Expand Up @@ -122,7 +118,7 @@ public class CoreServicesExtension implements ServiceExtension {
private SignatureSuiteRegistry suiteRegistry;
@Inject
private KeyPairService keyPairService;

@Inject
private RevocationListService revocationService;

@Override
Expand All @@ -146,7 +142,7 @@ public AccessTokenVerifier createAccessTokenVerifier(ServiceExtensionContext con

@Provider
public CredentialQueryResolver createCredentialQueryResolver(ServiceExtensionContext context) {
return new CredentialQueryResolverImpl(credentialStore, transformer, createRevocationListService(context), context.getMonitor().withPrefix("Credential Query"));
return new CredentialQueryResolverImpl(credentialStore, transformer, revocationService, context.getMonitor().withPrefix("Credential Query"));
}

@Provider
Expand All @@ -162,18 +158,10 @@ public PresentationCreatorRegistry presentationCreatorRegistry(ServiceExtensionC
return presentationCreatorRegistry;
}

@Provider
public RevocationListService createRevocationListService(ServiceExtensionContext context) {
if (revocationService == null) {
var validity = context.getConfig().getLong(REVOCATION_CACHE_VALIDITY, DEFAULT_REVOCATION_CACHE_VALIDITY_MILLIS);
revocationService = new StatusList2021RevocationService(typeManager.getMapper(), validity);
}
return revocationService;
}

@Provider
public VerifiablePresentationService presentationGenerator(ServiceExtensionContext context) {
return new VerifiablePresentationServiceImpl(CredentialFormat.JSON_LD, presentationCreatorRegistry(context), context.getMonitor());
return new VerifiablePresentationServiceImpl(CredentialFormat.JWT, presentationCreatorRegistry(context), context.getMonitor());
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ class AccessTokenVerifierImplTest {
private final AccessTokenVerifierImpl verifier = new AccessTokenVerifierImpl(tokenValidationSerivce, publicKeySupplier, tokenValidationRulesRegistry, mock(), pkResolver);
private final ClaimToken idToken = ClaimToken.Builder.newInstance()
.claim("token", "test-at")
.claim("scope", "org.eclipse.edc.vc.type:SomeTestCredential:read")
.claim("scope", "org.eclipse.edc.vc.type:AlumniCredential:read")
.build();

@Test
void verify_validSiToken_validAccessToken() {
when(tokenValidationSerivce.validate(anyString(), any(), anyList()))
.thenReturn(Result.success(idToken));
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant"))
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant"))
.isSucceeded()
.satisfies(strings -> Assertions.assertThat(strings).containsOnly(JwtCreationUtil.TEST_SCOPE));
verify(tokenValidationSerivce, times(2)).validate(anyString(), any(PublicKeyResolver.class), anyList());
Expand All @@ -66,7 +66,7 @@ void verify_validSiToken_validAccessToken() {
void verify_siTokenValidationFails() {
when(tokenValidationSerivce.validate(anyString(), any(), anyList()))
.thenReturn(Result.failure("test-failure"));
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
.detail().contains("test-failure");
}

Expand All @@ -75,7 +75,7 @@ void verify_noAccessTokenClaim() {
when(tokenValidationSerivce.validate(anyString(), any(PublicKeyResolver.class), anyList()))
.thenReturn(Result.failure("no access token"));

AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OWN_DID, OTHER_PARTICIPANT_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
AbstractResultAssert.assertThat(verifier.verify(JwtCreationUtil.generateSiToken(OWN_DID, OTHER_PARTICIPANT_DID), "did:web:test_participant")).isFailed()
.detail().contains("no access token");
verify(tokenValidationSerivce).validate(anyString(), any(PublicKeyResolver.class), anyList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService;
import org.eclipse.edc.identityhub.spi.ScopeToCriterionTransformer;
import org.eclipse.edc.identityhub.spi.store.CredentialStore;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VcStatus;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.model.VerifiableCredentialResource;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.resolution.CredentialQueryResolver;
import org.eclipse.edc.identityhub.spi.verifiablecredentials.resolution.QueryResult;
Expand Down Expand Up @@ -94,7 +95,7 @@ public QueryResult query(String participantContextId, PresentationQueryMessage q
// filter out any expired, revoked or suspended credentials
return isValidQuery ?
QueryResult.success(requestedCredentials.stream()
.filter(this::filterInvalidCredentials)
.filter(this::filterInvalidCredentials) // we still have to filter invalid creds, b/c a revocation may not have been detected yet
.map(VerifiableCredentialResource::getVerifiableCredential))
: QueryResult.unauthorized("Invalid query: requested Credentials outside of scope.");
}
Expand All @@ -111,7 +112,7 @@ private boolean filterInvalidCredentials(VerifiableCredentialResource verifiable
monitor.warning("Credential '%s' is expired.".formatted(credential.getId()));
return false;
}
var revocationResult = revocationService.checkValidity(credential);
var revocationResult = credential.getCredentialStatus().isEmpty() ? Result.success() : revocationService.checkValidity(credential);
if (revocationResult.failed()) {
monitor.warning("Credential '%s' not valid: %s".formatted(credential.getId(), revocationResult.getFailureDetail()));
return false;
Expand Down Expand Up @@ -154,8 +155,10 @@ private Result<Collection<VerifiableCredentialResource>> queryCredentials(List<C

private QuerySpec convertToQuerySpec(Criterion criteria, String participantContextId) {
var filterByParticipant = new Criterion("participantId", "=", participantContextId);
var filterNotRevoked = new Criterion("state", "!=", VcStatus.REVOKED.code());
var filterNotExpired = new Criterion("state", "!=", VcStatus.EXPIRED.code());
return QuerySpec.Builder.newInstance()
.filter(List.of(criteria, filterByParticipant))
.filter(List.of(criteria, filterByParticipant, filterNotRevoked, filterNotExpired))
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.eclipse.edc.iam.identitytrust.spi.model.PresentationQueryMessage;
import org.eclipse.edc.iam.verifiablecredentials.spi.RevocationListService;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialFormat;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialStatus;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.CredentialSubject;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.Issuer;
import org.eclipse.edc.iam.verifiablecredentials.spi.model.VerifiableCredential;
Expand Down Expand Up @@ -274,7 +275,12 @@ void query_whenNotYetValidCredential_doesNotInclude() {
@Test
void query_whenRevokedCredential_doesNotInclude() {
when(revocationServiceMock.checkValidity(any())).thenReturn(Result.failure("revoked"));
var credential = createCredential("TestCredential").build();
var credential = createCredential("TestCredential")
.credentialStatus(new CredentialStatus("test-cred-stat-id", "StatusList2021Entry",
Map.of("statusListCredential", "https://university.example/credentials/status/3",
"statusPurpose", "suspension",
"statusListIndex", 69)))
.build();
var resource = createCredentialResource(credential).build();
when(storeMock.query(any())).thenAnswer(i -> success(List.of(resource)));
var res = resolver.query(TEST_PARTICIPANT_CONTEXT_ID,
Expand Down
1 change: 1 addition & 0 deletions e2e-tests/api-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dependencies {
testImplementation(libs.awaitility)
testImplementation(libs.testcontainers.junit)
// needed for the Participant
testImplementation(project(":core:lib:credential-query-lib"))
testImplementation(testFixtures(project(":spi:verifiable-credential-spi")))
testImplementation(testFixtures(libs.edc.testfixtures.managementapi))
testImplementation(libs.nimbus.jwt)
Expand Down
Loading
Loading