Skip to content

Commit

Permalink
feat: track credential state in DB (#344)
Browse files Browse the repository at this point in the history
* add status field + timestamp

* wip

* add status flag to SQL stack

* collapse credential status

* store keys and credentials in vault/store (wip)

* updated PresentationApiComponentTest

* fix tests, checkstyle, DEPENDENCIES

* moved test data to static files

* DEPENDENCIES
  • Loading branch information
paullatzelsperger authored May 8, 2024
1 parent 05ededa commit f3a413e
Show file tree
Hide file tree
Showing 23 changed files with 703 additions and 375 deletions.
2 changes: 0 additions & 2 deletions DEPENDENCIES
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ maven/mavencentral/com.fasterxml.jackson.dataformat/jackson-dataformat-yaml/2.14
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

0 comments on commit f3a413e

Please sign in to comment.