Skip to content

Commit

Permalink
Add time window validation for the credential in the Verifiable Prese…
Browse files Browse the repository at this point in the history
…ntation (#27)
  • Loading branch information
rubenmodamioin2 authored Dec 13, 2024
1 parent 5f957a6 commit b4f534a
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v1.0.16](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.16)
### Fixed
- Add time window validation for the credential in the Verifiable Presentation

## [v1.0.15](https://github.com/in2workspace/in2-verifier-api/releases/tag/v1.0.15)
### Fixed
- Fix token serialization issue
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {
}

group = 'es.in2'
version = '1.0.15'
version = '1.0.16'

java {
toolchain {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package es.in2.verifier.exception;

public class CredentialExpiredException extends RuntimeException {
public CredentialExpiredException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package es.in2.verifier.exception;

public class CredentialNotActiveException extends RuntimeException {
public CredentialNotActiveException(String message) {
super(message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ public GlobalErrorMessage handleException(MismatchOrganizationIdentifierExceptio
return new GlobalErrorMessage("","","");
}

@ExceptionHandler(CredentialExpiredException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public GlobalErrorMessage handleException(CredentialExpiredException ex) {
log.error("The credential has expired: ", ex);
return new GlobalErrorMessage("","","");
}

@ExceptionHandler(CredentialNotActiveException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public GlobalErrorMessage handleException(CredentialNotActiveException ex) {
log.error("The credential is not active yet: ", ex);
return new GlobalErrorMessage("","","");
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public GlobalErrorMessage handleException(Exception ex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ public interface LEARCredential {
String issuerId(); // Adjusted to be common
String mandateeId();
String mandatorOrganizationIdentifier();
String validFrom();
String validUntil();
}
49 changes: 38 additions & 11 deletions src/main/java/es/in2/verifier/service/impl/VpServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import java.security.PublicKey;
import java.text.ParseException;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -59,36 +61,39 @@ public boolean validateVerifiablePresentation(String verifiablePresentation) {
// Step 1.1: Map the payload to a VerifiableCredential object
LEARCredential learCredential = mapPayloadToVerifiableCredential(payload);

// Step 2: Validate the credential id is not in the revoked list
// Step 2: Validate the time window of the credential
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Validating the time window of the credential");
validateCredentialTimeWindow(learCredential);

// Step 3: Validate the credential id is not in the revoked list
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Validating that the credential is not revoked");
validateCredentialNotRevoked(learCredential.id());
log.info("Credential is not revoked");

// Step 3: Validate the issuer
// Step 4: Validate the issuer
String credentialIssuerDid = learCredential.issuerId();
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Retrieved issuer DID from payload: {}", credentialIssuerDid);

// Step 4: Extract and validate credential types
// Step 5: Extract and validate credential types
List<String> credentialTypes = learCredential.type();
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Credential types extracted: {}", credentialTypes);

// Step 5: Retrieve the list of issuer capabilities
// Step 6: Retrieve the list of issuer capabilities
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Retrieving issuer capabilities for DID {}", credentialIssuerDid);
List<IssuerCredentialsCapabilities> issuerCapabilitiesList = trustFrameworkService.getTrustedIssuerListData(credentialIssuerDid);
log.info("Retrieved issuer capabilities");

// Step 6: Validate credential type against issuer capabilities
// Step 7: Validate credential type against issuer capabilities
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Validating credential types against issuer capabilities");
validateCredentialTypeWithIssuerCapabilities(issuerCapabilitiesList, credentialTypes);
log.info("Issuer DID {} is a trusted participant", credentialIssuerDid);

// TODO remove step 7 after the advanced certificate validation component is implemented
// Step 7: Verify the signature and the organizationId of the credential signature
// FIXME: comented. need to be implemented when all users has a valid credentials
// Map<String, Object> vcHeader = jwtCredential.getHeader().toJSONObject();
// certificateValidationService.extractAndVerifyCertificate(jwtCredential.serialize(),vcHeader, credentialIssuerDid.substring("did:elsi:".length())); // Extract public key from x5c certificate and validate OrganizationIdentifier
// Step 8: Verify the signature and the organizationId of the credential signature
Map<String, Object> vcHeader = jwtCredential.getHeader().toJSONObject();
certificateValidationService.extractAndVerifyCertificate(jwtCredential.serialize(),vcHeader, credentialIssuerDid.substring("did:elsi:".length())); // Extract public key from x5c certificate and validate OrganizationIdentifier

// Step 8: Extract the mandator organization identifier from the Verifiable Credential
// Step 9: Extract the mandator organization identifier from the Verifiable Credential
String mandatorOrganizationIdentifier = learCredential.mandatorOrganizationIdentifier();
log.debug("VpServiceImpl -- validateVerifiablePresentation -- Extracted Mandator Organization Identifier from Verifiable Credential: {}", mandatorOrganizationIdentifier);

Expand All @@ -97,7 +102,7 @@ public boolean validateVerifiablePresentation(String verifiablePresentation) {
trustFrameworkService.getTrustedIssuerListData(DID_ELSI_PREFIX + mandatorOrganizationIdentifier);
log.info("Mandator OrganizationIdentifier {} is valid and allowed", mandatorOrganizationIdentifier);

// Step 9: Validate the VP's signature with the DIDService (the DID of the holder of the VP)
// Step 10: Validate the VP's signature with the DIDService (the DID of the holder of the VP)
String mandateeId = learCredential.mandateeId();
PublicKey holderPublicKey = didService.getPublicKeyFromDid(mandateeId); // Get the holder's public key in bytes
jwtService.verifyJWTWithECKey(verifiablePresentation, holderPublicKey); // Validate the VP was signed by the holder DID
Expand Down Expand Up @@ -207,6 +212,28 @@ private void validateCredentialNotRevoked(String credentialId) {

}

private void validateCredentialTimeWindow(LEARCredential credential) {
try {
ZonedDateTime validFrom = ZonedDateTime.parse(credential.validFrom());
ZonedDateTime validUntil = ZonedDateTime.parse(credential.validUntil());
ZonedDateTime now = ZonedDateTime.now();

// Check if the credential is not yet valid
if (now.isBefore(validFrom)) {
throw new CredentialNotActiveException("Credential is not yet valid. Valid from: " + validFrom);
}

// Check if the credential has expired
if (now.isAfter(validUntil)) {
throw new CredentialExpiredException("Credential has expired. Valid until: " + validUntil);
}

} catch (DateTimeParseException e) {
throw new CredentialMappingException("Invalid date format in credential: " + e.getMessage());
}
}



private JsonNode convertObjectToJSONNode(Object vcObject) throws JsonConversionException {
JsonNode jsonNode;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
package es.in2.vcverifier.exception.handler;

import es.in2.verifier.exception.InvalidVPtokenException;
import es.in2.verifier.exception.CredentialRevokedException;
import es.in2.verifier.exception.MismatchOrganizationIdentifierException;
import es.in2.verifier.exception.QRCodeGenerationException;
import es.in2.verifier.exception.ResourceNotFoundException;
import es.in2.verifier.exception.*;
import es.in2.verifier.exception.handler.GlobalExceptionHandler;
import es.in2.verifier.model.GlobalErrorMessage;
import jakarta.servlet.http.HttpServletRequest;
Expand All @@ -14,6 +10,7 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import javax.security.auth.login.CredentialExpiredException;
import java.util.NoSuchElementException;

import static org.assertj.core.api.Assertions.assertThat;
Expand Down Expand Up @@ -107,4 +104,26 @@ void testHandleInvalidVPtokenException() {
assertThat(response.message()).isEqualTo("Invalid VP token");
assertThat(response.path()).isEqualTo("/test-path");
}

@Test
void testHandleCredentialExpiredException() {
CredentialExpiredException exception = new CredentialExpiredException("Credential expired");

GlobalErrorMessage response = globalExceptionHandler.handleException(exception);

assertThat(response.title()).isEmpty();
assertThat(response.message()).isEmpty();
assertThat(response.path()).isEmpty();
}

@Test
void testHandleCredentialNotActiveException() {
CredentialNotActiveException exception = new CredentialNotActiveException("Credential not active");

GlobalErrorMessage response = globalExceptionHandler.handleException(exception);

assertThat(response.title()).isEmpty();
assertThat(response.message()).isEmpty();
assertThat(response.path()).isEmpty();
}
}
102 changes: 102 additions & 0 deletions src/test/java/es/in2/verifier/service/VpServiceImplTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap;
import com.nimbusds.jwt.JWTClaimsSet;
Expand Down Expand Up @@ -32,6 +33,7 @@
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.time.Instant;
import java.time.ZonedDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -313,6 +315,18 @@ void validateVerifiablePresentation_success() throws Exception {
// Step 7: Validate the mandator with trusted issuer service
when(trustFrameworkService.getTrustedIssuerListData(DID_ELSI_PREFIX + learCredentialEmployee.mandatorOrganizationIdentifier())).thenReturn(issuerCapabilitiesList);

// Step 7: Verify the signature and the organizationId of the credential signature
Map<String, Object> vcHeader = new HashMap<>();
vcHeader.put("x5c", List.of("base64Cert"));
JWSHeader header = mock(JWSHeader.class);
when(jwtCredential.getHeader()).thenReturn(header);
when(header.toJSONObject()).thenReturn(vcHeader);


when(jwtCredential.serialize()).thenReturn(vcJwt);

doNothing().when(certificateValidationService).extractAndVerifyCertificate(any(), eq(vcHeader),eq("issuer"));

// Step 8: Get the holder's public key
PublicKey holderPublicKey = generateECPublicKey();
when(didService.getPublicKeyFromDid(learCredentialEmployee.mandateeId())).thenReturn(holderPublicKey);
Expand All @@ -331,6 +345,92 @@ void validateVerifiablePresentation_success() throws Exception {
}
}

@Test
void validateVerifiablePresentation_invalidTimeWindowForExpired() throws Exception {
// Given
String invalidVP = "invalid-time-window.vp.jwt";
ZonedDateTime now = ZonedDateTime.now();
LEARCredentialEmployee expiredCredential = LEARCredentialEmployee.builder()
.validUntil(now.minusDays(1).toString())
.validFrom(now.minusDays(2).toString())
.build();

// Mock parsing del VP
SignedJWT vpSignedJWT = mock(SignedJWT.class);
try (MockedStatic<SignedJWT> mockedSignedJWT = mockStatic(SignedJWT.class)) {
mockedSignedJWT.when(() -> SignedJWT.parse(invalidVP)).thenReturn(vpSignedJWT);

// Configurar claims del VP
JWTClaimsSet vpClaimsSet = mock(JWTClaimsSet.class);
when(vpSignedJWT.getJWTClaimsSet()).thenReturn(vpClaimsSet);

Map<String, Object> vcClaimMap = new HashMap<>();
String vcJwt = "invalid-time-window.vc.jwt";
vcClaimMap.put("verifiableCredential", List.of(vcJwt));
when(vpClaimsSet.getClaim("vp")).thenReturn(vcClaimMap);

// Mock parsing del VC
SignedJWT jwtCredential = mock(SignedJWT.class);
mockedSignedJWT.when(() -> SignedJWT.parse(vcJwt)).thenReturn(jwtCredential);

Payload payload = mock(Payload.class);
when(jwtService.getPayloadFromSignedJWT(jwtCredential)).thenReturn(payload);

LinkedTreeMap<String, Object> vcFromPayload = new LinkedTreeMap<>();
vcFromPayload.put("type", List.of("LEARCredentialEmployee"));
when(jwtService.getVCFromPayload(payload)).thenReturn(vcFromPayload);
when(objectMapper.convertValue(vcFromPayload, LEARCredentialEmployee.class)).thenReturn(expiredCredential);

boolean result = vpServiceImpl.validateVerifiablePresentation(invalidVP);

Assertions.assertThat(result).isFalse();

}
}

@Test
void validateVerifiablePresentation_invalidTimeWindowForNotValidYet() throws Exception {
// Given
String invalidVP = "invalid-time-window.vp.jwt";
ZonedDateTime now = ZonedDateTime.now();
LEARCredentialEmployee expiredCredential = LEARCredentialEmployee.builder()
.validUntil(now.plusDays(1).toString())
.validFrom(now.plusDays(1).toString())
.build();

// Mock parsing del VP
SignedJWT vpSignedJWT = mock(SignedJWT.class);
try (MockedStatic<SignedJWT> mockedSignedJWT = mockStatic(SignedJWT.class)) {
mockedSignedJWT.when(() -> SignedJWT.parse(invalidVP)).thenReturn(vpSignedJWT);

// Configurar claims del VP
JWTClaimsSet vpClaimsSet = mock(JWTClaimsSet.class);
when(vpSignedJWT.getJWTClaimsSet()).thenReturn(vpClaimsSet);

Map<String, Object> vcClaimMap = new HashMap<>();
String vcJwt = "invalid-time-window.vc.jwt";
vcClaimMap.put("verifiableCredential", List.of(vcJwt));
when(vpClaimsSet.getClaim("vp")).thenReturn(vcClaimMap);

// Mock parsing del VC
SignedJWT jwtCredential = mock(SignedJWT.class);
mockedSignedJWT.when(() -> SignedJWT.parse(vcJwt)).thenReturn(jwtCredential);

Payload payload = mock(Payload.class);
when(jwtService.getPayloadFromSignedJWT(jwtCredential)).thenReturn(payload);

LinkedTreeMap<String, Object> vcFromPayload = new LinkedTreeMap<>();
vcFromPayload.put("type", List.of("LEARCredentialEmployee"));
when(jwtService.getVCFromPayload(payload)).thenReturn(vcFromPayload);
when(objectMapper.convertValue(vcFromPayload, LEARCredentialEmployee.class)).thenReturn(expiredCredential);

boolean result = vpServiceImpl.validateVerifiablePresentation(invalidVP);

Assertions.assertThat(result).isFalse();

}
}

private ECPublicKey generateECPublicKey() throws NoSuchAlgorithmException, InvalidAlgorithmParameterException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
Expand Down Expand Up @@ -360,6 +460,8 @@ private LEARCredentialEmployee getLEARCredentialEmployee(){
.id("urn:uuid:1234")
.issuer("did:elsi:issuer")
.credentialSubject(credentialSubject)
.validUntil(ZonedDateTime.now().plusDays(1).toString())
.validFrom(ZonedDateTime.now().toString())
.build();
}
}

0 comments on commit b4f534a

Please sign in to comment.