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

Add time window validation for the credential in the Verifiable Presentaion #27

Merged
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
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();
}
}
Loading