From 8da3a09daefe02ab4d7512640bcdfdb0e5bab1e7 Mon Sep 17 00:00:00 2001 From: CharlesShuller Date: Wed, 25 Oct 2023 16:52:03 -0500 Subject: [PATCH] Add Clock Skew Tolerance for Verifiable Presentations (#377) --- examples/java-springboot/pom.xml | 43 +++- .../DIDKitExampleApplication.java | 3 + .../auth/VPAuthenticationProvider.java | 22 +- .../didkitexample/config/DIDKitConfig.java | 21 ++ .../config/WebSecurityConfig.java | 9 +- ...rifiablePresentationRequestController.java | 16 +- .../didkitexample/user/UserService.java | 9 +- .../didkitexample/util/DIDKitOptions.java | 26 ++ .../util/VerifiablePresentation.java | 171 ++++++++++--- .../src/main/resources/application.properties | 3 +- .../java/VerifiablePresentationTests.java | 231 ++++++++++++++++++ 11 files changed, 505 insertions(+), 49 deletions(-) create mode 100644 examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/DIDKitConfig.java create mode 100644 examples/java-springboot/src/test/java/VerifiablePresentationTests.java diff --git a/examples/java-springboot/pom.xml b/examples/java-springboot/pom.xml index b2c7762b..8b39956a 100644 --- a/examples/java-springboot/pom.xml +++ b/examples/java-springboot/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 2.4.1 + 2.7.17 com.spruceid @@ -43,6 +43,12 @@ org.springframework.boot spring-boot-starter-websocket + + org.springframework.boot + spring-boot-configuration-processor + true + + redis.clients jedis @@ -63,14 +69,15 @@ mysql mysql-connector-java runtime + 8.0.33 - - org.projectlombok - lombok - true - 1.18.20 - + + org.projectlombok + lombok + 1.18.30 + provided + com.google.zxing @@ -103,6 +110,17 @@ 2.21.0 + + org.postgresql + postgresql + 42.6.0 + + + + org.json + json + 20231013 + @@ -115,4 +133,15 @@ + + + + + org.apache.maven.plugins + maven-surefire-report-plugin + 2.22.0 + + + + diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/DIDKitExampleApplication.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/DIDKitExampleApplication.java index a954d1d7..8064c611 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/DIDKitExampleApplication.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/DIDKitExampleApplication.java @@ -4,10 +4,13 @@ import com.spruceid.didkitexample.util.Resources; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import java.nio.file.Paths; +import com.spruceid.didkitexample.config.DIDKitConfig; @SpringBootApplication +@ConfigurationPropertiesScan("com.spruceid.didkitexample.config") public class DIDKitExampleApplication { public static void main(String[] args) throws Throwable { diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/auth/VPAuthenticationProvider.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/auth/VPAuthenticationProvider.java index ad696a30..e759354f 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/auth/VPAuthenticationProvider.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/auth/VPAuthenticationProvider.java @@ -11,15 +11,26 @@ import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; +import org.springframework.beans.factory.annotation.Autowired; -import java.nio.file.Files; + +import com.spruceid.didkitexample.config.DIDKitConfig; + +import java.util.Optional; import java.util.Map; +import java.nio.file.Files; + + @Component @AllArgsConstructor public class VPAuthenticationProvider implements AuthenticationProvider { private final UserService userService; + @Autowired + private final DIDKitConfig didkitConfig; + + @Override public Authentication authenticate(Authentication auth) { final VPAuthenticationToken token = (VPAuthenticationToken) auth; @@ -37,7 +48,13 @@ public Authentication authenticate(Authentication auth) { final Map vc; try { - vc = VerifiablePresentation.verifyPresentation(key, presentation, null); + vc = VerifiablePresentation.verifyPresentation( + key, + presentation, + Optional.empty(), + Optional.empty(), + Optional.of(didkitConfig.maxClockSkew) + ); } catch (Exception e) { throw new BadCredentialsException("Failed to verify presentation"); } @@ -55,4 +72,3 @@ public boolean supports(Class authentication) { return VPAuthenticationToken.class.isAssignableFrom(authentication); } } - diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/DIDKitConfig.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/DIDKitConfig.java new file mode 100644 index 00000000..0d4e158f --- /dev/null +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/DIDKitConfig.java @@ -0,0 +1,21 @@ +package com.spruceid.didkitexample.config; + +import java.time.Duration; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.boot.context.properties.ConstructorBinding; + +@Getter +@ConfigurationProperties(prefix = "didkit") +@ConfigurationPropertiesScan +public class DIDKitConfig { + public Duration maxClockSkew; + + + @ConstructorBinding + DIDKitConfig(Duration maxClockSkew) { + this.maxClockSkew = maxClockSkew; + } +} diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/WebSecurityConfig.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/WebSecurityConfig.java index 3affe807..7f1bc427 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/WebSecurityConfig.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/config/WebSecurityConfig.java @@ -15,6 +15,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import com.spruceid.didkitexample.config.DIDKitConfig; + + @EnableWebSecurity @AllArgsConstructor public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @@ -24,8 +27,12 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private final StringRedisTemplate redisTemplate; + @Autowired + private final DIDKitConfig didkitConfig; + + public AuthenticationProvider customAuthenticationProvider() { - return new VPAuthenticationProvider(userService); + return new VPAuthenticationProvider(userService, didkitConfig); } public VPAuthenticationFilter authenticationFilter() throws Exception { diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/controller/VerifiablePresentationRequestController.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/controller/VerifiablePresentationRequestController.java index b5f7df72..0334faca 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/controller/VerifiablePresentationRequestController.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/controller/VerifiablePresentationRequestController.java @@ -29,6 +29,10 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.Optional; + +import com.spruceid.didkitexample.config.DIDKitConfig; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -43,6 +47,9 @@ public class VerifiablePresentationRequestController { @Autowired private final ConcurrentHashMap sessionMap; + @Autowired + private final DIDKitConfig didkitConfig; + @GetMapping(value = "/verifiable-presentation-request/{challenge}", produces = MediaType.APPLICATION_JSON_VALUE) public VerifiablePresentationRequest vpRequestGet( @PathVariable("challenge") String challenge @@ -85,7 +92,14 @@ public void vpRequestPost( } logger.info("VerifiablePresentation.verifyPresentation"); - final Map vc = VerifiablePresentation.verifyPresentation(key, presentation, challenge); + final Map vc = + VerifiablePresentation.verifyPresentation( + key, + presentation, + Optional.of(challenge), + Optional.empty(), + Optional.of(didkitConfig.maxClockSkew) + ); final Map credentialSubject = (Map) vc.get("credentialSubject"); final String username = credentialSubject.get("alumniOf").toString(); diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/user/UserService.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/user/UserService.java index f6e8f29a..66a0e4dd 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/user/UserService.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/user/UserService.java @@ -48,7 +48,13 @@ public String issueCredential(String id, User user) throws DIDKitException, IOEx final String verificationMethod = DIDKit.keyToVerificationMethod("key", key); final UserCredential credential = new UserCredential(didKey, id, user.getUsername()); - final DIDKitOptions options = new DIDKitOptions("assertionMethod", verificationMethod, null, null); + final DIDKitOptions options = new DIDKitOptions( + "assertionMethod", // proofPurpose + verificationMethod, // verificationMethos + Optional.empty(), // challenge + null, // domain + Optional.empty() // created + ); final ObjectMapper mapper = new ObjectMapper(); final String credentialJson = mapper.writeValueAsString(credential); @@ -57,4 +63,3 @@ public String issueCredential(String id, User user) throws DIDKitException, IOEx return DIDKit.issueCredential(credentialJson, optionsJson, key); } } - diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/DIDKitOptions.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/DIDKitOptions.java index c279db04..1ce4a999 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/DIDKitOptions.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/DIDKitOptions.java @@ -4,6 +4,11 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; +import lombok.NonNull; +import java.util.Optional; +import java.time.Instant; +import java.time.format.DateTimeFormatter; + @Getter @Setter @@ -14,4 +19,25 @@ public class DIDKitOptions { private String verificationMethod; private String challenge; private String domain; + + // This will be the "system time" for when something is processed + // not the time the VP is created. + private String created; + + public DIDKitOptions( + String proofPurpose, + String verificationMethod, + @NonNull Optional challenge, + String domain, + @NonNull Optional created + ) { + this.proofPurpose = proofPurpose; + this.verificationMethod = verificationMethod; + this.challenge = challenge.orElse(null); + this.domain = domain; + this.created = + created + .map(i -> DateTimeFormatter.ISO_INSTANT.format(i)) + .orElse(DateTimeFormatter.ISO_INSTANT.format(Instant.now())); + } } diff --git a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/VerifiablePresentation.java b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/VerifiablePresentation.java index a9b4b1cc..4f85ec78 100644 --- a/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/VerifiablePresentation.java +++ b/examples/java-springboot/src/main/java/com/spruceid/didkitexample/util/VerifiablePresentation.java @@ -3,106 +3,209 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import com.spruceid.DIDKit; +import com.spruceid.didkitexample.config.DIDKitConfig; import org.springframework.http.HttpStatus; import org.springframework.web.server.ResponseStatusException; import java.util.AbstractList; import java.util.List; import java.util.Map; +import java.util.Optional; + +import java.time.Instant; +import java.time.Duration; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import lombok.NonNull; public class VerifiablePresentation { private static Logger logger = LogManager.getLogger(); public static Map verifyPresentation( - final String key, - final String presentation, - final String challenge + @NonNull final String key, + @NonNull final String presentation, + @NonNull final Optional challenge, + @NonNull final Optional processAtTime, + @NonNull final Optional maxClockSkew ) throws Exception { logger.info("Converting String VP into Map"); logger.info("VP: " + presentation); - final ObjectMapper mapper = new ObjectMapper(); final Map presentationMap = - mapper.readValue(presentation, new TypeReference<>() {}); + presentationToMap(presentation); return VerifiablePresentation - .verifyPresentation(key, presentationMap, challenge); + .verifyPresentation( + key, + presentationMap, + challenge, + processAtTime, + maxClockSkew + ); } public static Map verifyPresentation( - final String key, - final Map presentation, - final String challenge - ) { + @NonNull final String key, + @NonNull final Map presentation, + @NonNull final Optional challenge, + @NonNull final Optional processAtTime, + @NonNull final Optional maxClockSkew + ) throws Exception { logger.info("Attempting to verify Map presentation"); + final Duration maxSkewOrZero = maxClockSkew.orElse(Duration.ofSeconds(0)); final ObjectMapper mapper = new ObjectMapper(); + + validateCreatedTimes(processAtTime.orElse(Instant.now()), maxSkewOrZero, presentation); + + // Verify the Presentation try { - final DIDKitOptions options = new DIDKitOptions( - "authentication", - null, - challenge, - Resources.baseUrl - ); + final var vpOptions = + new DIDKitOptions( + "authentication", // proofPurpose + null, // verificationMethos + challenge, // challenge + Resources.baseUrl, // domain + processAtTime.map(i -> i.plus(maxSkewOrZero)) // created + ); + final String vpStr = mapper.writeValueAsString(presentation); - final String optionsStr = mapper.writeValueAsString(options); + final String vpOptionsStr = mapper.writeValueAsString(vpOptions); logger.info("vpStr: " + vpStr); - logger.info("optionsStr: " + optionsStr); + logger.info("vpOptionsStr: " + vpOptionsStr); - final String result = DIDKit.verifyPresentation(vpStr, optionsStr); + final String result = DIDKit.verifyPresentation(vpStr, vpOptionsStr); logger.info("DIDKit.verifyPresentation result: " + result); final Map resultMap = mapper.readValue(result, new TypeReference<>() { }); if (((List) resultMap.get("errors")).size() > 0) { logger.error("VP: " + resultMap.get("errors")); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid presentation"); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Invalid presentation" + ); } } catch (Exception e) { logger.error("Failed to verify presentation: " + e.toString()); - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to verify presentation"); + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to verify presentation" + ); } //Select the first vc if we have multiple in the presentation final Object vcs = presentation.get("verifiableCredential"); logger.info("vcs type: " + vcs.getClass()); - //final Map vc = (Map) (vcs instanceof Object[] ? ((Object[]) vcs)[0] : vcs); final Map vc = getFirstVc(vcs); + // Verify the Credential try { - final DIDKitOptions options = new DIDKitOptions( - "assertionMethod", - null, - null, - null - ); + final var vcOptions = + new DIDKitOptions( + "assertionMethod", // proofPurpose + null, // verificationMethod + Optional.empty(), // challenge + null, // domain + processAtTime.map(i -> i.plus(maxSkewOrZero)) // created + ); final String vcStr = mapper.writeValueAsString(vc); - final String optionsStr = mapper.writeValueAsString(options); + final String vcOptionsStr = mapper.writeValueAsString(vcOptions); + + logger.info("vcStr: " + vcStr); + logger.info("vcOptionsStr: " + vcOptionsStr); - final String result = DIDKit.verifyCredential(vcStr, optionsStr); + final String result = DIDKit.verifyCredential(vcStr, vcOptionsStr); + logger.info("DIDKit.verifyCredential result: " + result); final Map resultMap = mapper.readValue(result, new TypeReference<>() { }); if (((List) resultMap.get("errors")).size() > 0) { - System.out.println("[ERROR] VC: " + resultMap.get("errors")); - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid credential"); + logger.error("VC: " + resultMap.get("errors")); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Invalid credential" + ); } } catch (Exception e) { - throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to verify credential"); + logger.error("Exception validating credential: " + e); + throw new ResponseStatusException( + HttpStatus.INTERNAL_SERVER_ERROR, + "Failed to verify credential" + ); } return vc; } - private static Map getFirstVc(Object vcs) { + public static void validateCreatedTimes( + @NonNull final Instant now, + @NonNull final Duration maxClockSkew, + @NonNull final Map presentation + ) throws Exception { + final Instant credentialCreated = getCredentialCreated(presentation); + final Instant presentationCreated = getPresentationCreated(presentation); + final Instant nowWithSkew = now.plus(maxClockSkew); + + if(credentialCreated.compareTo(nowWithSkew) > 0) { + logger.error("The Credential in the presentation is not yet valid"); + logger.error("credentialCreated: " + credentialCreated); + logger.error("processedAt: " + nowWithSkew); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Credential in presentation is not yet valid" + ); + } + + if(presentationCreated.compareTo(nowWithSkew) > 0) { + logger.error("The presentation is not yet valid"); + logger.error("presentationCreated: " + presentationCreated); + logger.error("processedAt: " + nowWithSkew); + throw new ResponseStatusException( + HttpStatus.BAD_REQUEST, + "Presentation is not yet valid" + ); + } + } + + public static Instant getCredentialCreated( + @NonNull final Map presentation + ) { + final Object vcs = presentation.get("verifiableCredential"); + final Map vc = getFirstVc(vcs); + final Map proof = (Map)vc.get("proof"); + final String createdStr = (String)proof.get("created"); + final Instant created = Instant.parse(createdStr); + return created; + } + + public static Instant getPresentationCreated( + @NonNull final Map presentation + ) { + final Map proof = (Map)presentation.get("proof"); + final String createdStr = (String)proof.get("created"); + final Instant created = Instant.parse(createdStr); + return created; + } + + public static Map presentationToMap( + @NonNull String presentation + ) throws Exception { + final ObjectMapper mapper = new ObjectMapper(); + + final Map presentationMap = + mapper.readValue(presentation, new TypeReference<>() {}); + + return presentationMap; + } + + private static Map getFirstVc(Object vcs) { if(vcs instanceof Object[]) { Object r = ((Object[]) vcs)[0]; logger.info("r type: " + r.getClass()); diff --git a/examples/java-springboot/src/main/resources/application.properties b/examples/java-springboot/src/main/resources/application.properties index ee62a41b..a0ee1359 100644 --- a/examples/java-springboot/src/main/resources/application.properties +++ b/examples/java-springboot/src/main/resources/application.properties @@ -2,4 +2,5 @@ spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3306/didkit spring.datasource.username=root spring.datasource.password=root -server.port=8081 \ No newline at end of file +server.port=8081 +didkit.maxClockSkew=5s \ No newline at end of file diff --git a/examples/java-springboot/src/test/java/VerifiablePresentationTests.java b/examples/java-springboot/src/test/java/VerifiablePresentationTests.java new file mode 100644 index 00000000..57cce15f --- /dev/null +++ b/examples/java-springboot/src/test/java/VerifiablePresentationTests.java @@ -0,0 +1,231 @@ +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.api.Test; + +import com.spruceid.didkitexample.util.VerifiablePresentation; +import com.spruceid.didkitexample.util.DIDKitOptions; + +import java.util.Optional; + +import java.time.Instant; +import java.time.Duration; + +class VerifiablePresenationTests { + public static String validKey() { + return "{\"kty\":\"OKP\",\"crv\":\"Ed25519\",\"x\":\"2dmpl0ZBbTA2X501O8XbDf2maPkKluXaZfI6pSuBPJg\",\"d\":\"dR6sD1Coca1lttJt1KceJa9XuPMEx4mR8DZ174WGffg\"}"; + } + + public static String validPresentation() { + return "{\"@context\":[\"https://www.w3.org/2018/credentials/v1\"],\"id\":\"urn:uuid:5905fb5a-238d-4677-b925-b7c422b62650\",\"type\":[\"VerifiablePresentation\"],\"verifiableCredential\":{\"@context\":[\"https://www.w3.org/2018/credentials/v1\",\"https://www.w3.org/2018/credentials/examples/v1\"],\"id\":\"urn:uuid:9f12872b-6d31-4b74-97bf-337c839e0ba4\",\"type\":[\"VerifiableCredential\"],\"credentialSubject\":{\"id\":\"did:tz:tz1c6HxLrqR2cmm554qZ16jM1noBT242FoDg\",\"alumniOf\":\"charles\"},\"issuer\":\"did:key:z6Mku7f1yjNfra5q1FFFFQuUgmNCB337CBYAEhWKqDkSeECF\",\"issuanceDate\":\"2023-10-04T16:22:49.558339114Z\",\"proof\":{\"type\":\"Ed25519Signature2018\",\"proofPurpose\":\"assertionMethod\",\"verificationMethod\":\"did:key:z6Mku7f1yjNfra5q1FFFFQuUgmNCB337CBYAEhWKqDkSeECF#z6Mku7f1yjNfra5q1FFFFQuUgmNCB337CBYAEhWKqDkSeECF\",\"created\":\"2023-10-04T21:22:49.561Z\",\"jws\":\"eyJhbGciOiJFZERTQSIsImNyaXQiOlsiYjY0Il0sImI2NCI6ZmFsc2V9..iieKtJ0Pp8f4CnwiQI3clmX6E_WCHnP6TOrJUQ4Irjf4I5ewmgtWCuAFwPwiJj12CNdkGFRB-DHMfnI08H0EAA\"},\"expirationDate\":\"2024-04-04T16:22:49.558339114Z\"},\"proof\":{\"@context\":{\"Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021\":{\"@context\":{\"@protected\":true,\"@version\":1.1,\"challenge\":\"https://w3id.org/security#challenge\",\"created\":{\"@id\":\"http://purl.org/dc/terms/created\",\"@type\":\"http://www.w3.org/2001/XMLSchema#dateTime\"},\"domain\":\"https://w3id.org/security#domain\",\"expires\":{\"@id\":\"https://w3id.org/security#expiration\",\"@type\":\"http://www.w3.org/2001/XMLSchema#dateTime\"},\"id\":\"@id\",\"jws\":\"https://w3id.org/security#jws\",\"nonce\":\"https://w3id.org/security#nonce\",\"proofPurpose\":{\"@context\":{\"@protected\":true,\"@version\":1.1,\"assertionMethod\":{\"@container\":\"@set\",\"@id\":\"https://w3id.org/security#assertionMethod\",\"@type\":\"@id\"},\"authentication\":{\"@container\":\"@set\",\"@id\":\"https://w3id.org/security#authenticationMethod\",\"@type\":\"@id\"},\"id\":\"@id\",\"type\":\"@type\"},\"@id\":\"https://w3id.org/security#proofPurpose\",\"@type\":\"@vocab\"},\"publicKeyJwk\":{\"@id\":\"https://w3id.org/security#publicKeyJwk\",\"@type\":\"@json\"},\"type\":\"@type\",\"verificationMethod\":{\"@id\":\"https://w3id.org/security#verificationMethod\",\"@type\":\"@id\"}},\"@id\":\"https://w3id.org/security#Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021\"},\"Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021\":{\"@id\":\"https://w3id.org/security#Ed25519PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021\"},\"P256BLAKE2BDigestSize20Base58CheckEncodedSignature2021\":{\"@context\":{\"@protected\":true,\"@version\":1.1,\"challenge\":\"https://w3id.org/security#challenge\",\"created\":{\"@id\":\"http://purl.org/dc/terms/created\",\"@type\":\"http://www.w3.org/2001/XMLSchema#dateTime\"},\"domain\":\"https://w3id.org/security#domain\",\"expires\":{\"@id\":\"https://w3id.org/security#expiration\",\"@type\":\"http://www.w3.org/2001/XMLSchema#dateTime\"},\"id\":\"@id\",\"jws\":\"https://w3id.org/security#jws\",\"nonce\":\"https://w3id.org/security#nonce\",\"proofPurpose\":{\"@context\":{\"@protected\":true,\"@version\":1.1,\"assertionMethod\":{\"@container\":\"@set\",\"@id\":\"https://w3id.org/security#assertionMethod\",\"@type\":\"@id\"},\"authentication\":{\"@container\":\"@set\",\"@id\":\"https://w3id.org/security#authenticationMethod\",\"@type\":\"@id\"},\"id\":\"@id\",\"type\":\"@type\"},\"@id\":\"https://w3id.org/security#proofPurpose\",\"@type\":\"@vocab\"},\"publicKeyJwk\":{\"@id\":\"https://w3id.org/security#publicKeyJwk\",\"@type\":\"@json\"},\"type\":\"@type\",\"verificationMethod\":{\"@id\":\"https://w3id.org/security#verificationMethod\",\"@type\":\"@id\"}},\"@id\":\"https://w3id.org/security#P256BLAKE2BDigestSize20Base58CheckEncodedSignature2021\"},\"P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021\":{\"@id\":\"https://w3id.org/security#P256PublicKeyBLAKE2BDigestSize20Base58CheckEncoded2021\"}},\"type\":\"Ed25519BLAKE2BDigestSize20Base58CheckEncodedSignature2021\",\"proofPurpose\":\"authentication\",\"challenge\":\"c031a95f-2a23-460b-bd42-39544ab19030\",\"verificationMethod\":\"did:tz:tz1c6HxLrqR2cmm554qZ16jM1noBT242FoDg#blockchainAccountId\",\"created\":\"2023-10-23T20:22:59.251Z\",\"domain\":\"open-actually-wahoo.ngrok-free.app\",\"jws\":\"eyJhbGciOiJFZEJsYWtlMmIiLCJjcml0IjpbImI2NCJdLCJiNjQiOmZhbHNlfQ..AOqTVGtFlHXfHCUT6ua68G__s5LBkjeBK5jGxQGvTVXNvEZqxzBBExeeGG5EXC2CVqS1wkO8ucEMQpcej3KQCg\",\"publicKeyJwk\":{\"crv\":\"Ed25519\",\"kty\":\"OKP\",\"x\":\"E993DJyLL2QEcWmmYIA6TuxOZqI9wEMywu67JF5xYjA\"}},\"holder\":\"did:tz:tz1c6HxLrqR2cmm554qZ16jM1noBT242FoDg\"}"; + } + + public static String validChallenge() { + return "c031a95f-2a23-460b-bd42-39544ab19030"; + } + + public static Instant presentationCreatedAt() { + return Instant.parse("2023-10-23T20:22:59.251Z"); + } + + public static Instant credentialCreatedAt() { + return Instant.parse("2023-10-04T21:22:49.561Z"); + } + + @Test + void verifyPresentationAllEmptyString() throws Exception { + final var key = ""; + final var presentation = ""; + final var challenge = ""; + + assertThrows( + com.fasterxml.jackson.databind.exc.MismatchedInputException.class, + () -> { + VerifiablePresentation + .verifyPresentation( + key, + presentation, + Optional.of(challenge), + Optional.empty(), + Optional.empty() + ); + } + ); + } + + @Test + void verifyPresentationGoodPresentation() throws Exception { + // key, presentation, and challenge will need to be updated about once + // a year until we get test code up to generate VPs for us. + final var key = validKey(); + final var presentation = validPresentation(); + final var challenge = validChallenge(); + + VerifiablePresentation + .verifyPresentation( + key, + presentation, + Optional.of(challenge), + Optional.empty(), + Optional.empty() + ); + } + + @Test + void verifyPresentationPresentationWithinMaxClockSkew() throws Exception { + // key, presentation, and challenge will need to be updated about once + // a year until we get test code up to generate VPs for us. + final var key = validKey(); + final var presentation = validPresentation(); + final var challenge = validChallenge(); + final var maxClockSkew = Duration.ofSeconds(5); + final var processAtTime = + presentationCreatedAt().plus(Duration.ofSeconds(4)); + + VerifiablePresentation + .verifyPresentation( + key, + presentation, + Optional.of(challenge), + Optional.of(processAtTime), + Optional.of(maxClockSkew) + ); + } + + + @Test + void verifyPresentationCreatedInFuture() throws Exception { + // key, presentation, and challenge will need to be updated about once + // a year until we get test code up to generate VPs for us. + final var key = validKey(); + final var presentation = validPresentation(); + final var challenge = validChallenge(); + + //We process the presentation in the past to simulate a presentation + //created in the future + final Instant pastTime = + Instant + .now() + .minus(Duration.ofDays(10)); + + + assertThrows( + org.springframework.web.server.ResponseStatusException.class, + () -> { + VerifiablePresentation + .verifyPresentation( + key, + presentation, + Optional.of(challenge), + Optional.of(pastTime), + Optional.empty() + ); + } + ); + } + + @Test + void verifyPresentationCreated() throws Exception { + final var expectedCreated = presentationCreatedAt(); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + final var actualCreated = + VerifiablePresentation.getPresentationCreated(presentationMap); + + assertTrue(actualCreated.compareTo(expectedCreated) == 0); + } + + @Test + void verifyCredentialCreated() throws Exception { + final var expectedCreated = credentialCreatedAt(); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + final var actualCreated = + VerifiablePresentation.getCredentialCreated(presentationMap); + + assertTrue(actualCreated.compareTo(expectedCreated) == 0); + } + + + @Test + void verifyCreatedTimesGood() throws Exception { + final Duration maxClockSkew = Duration.ofSeconds(5); + // now is set to 1 min after the presentation created date + final Instant now = presentationCreatedAt().plus(Duration.ofMinutes(1)); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + + VerifiablePresentation.validateCreatedTimes( + now, + maxClockSkew, + presentationMap + ); + } + + @Test + void verifyCreatedTimesPresentationInFutureLessThanSkew() throws Exception { + final Duration maxClockSkew = Duration.ofSeconds(5); + // now is set to 4 sec before the presentation created date + final Instant now = presentationCreatedAt().minus(Duration.ofSeconds(4)); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + + VerifiablePresentation.validateCreatedTimes( + now, + maxClockSkew, + presentationMap + ); + } + + @Test + void verifyCreatedPresentationInFuture() throws Exception { + final Duration maxClockSkew = Duration.ofSeconds(5); + // now is set to 4 min before the presentation created date + final Instant now = presentationCreatedAt().minus(Duration.ofMinutes(4)); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + assertThrows( + org.springframework.web.server.ResponseStatusException.class, + () -> { + VerifiablePresentation.validateCreatedTimes( + now, + maxClockSkew, + presentationMap + ); + } + ); + } + + + @Test + void verifyCreatedCredentialInFuture() throws Exception { + final Duration maxClockSkew = Duration.ofSeconds(5); + // now is set to 4 min before the presentation created date + final Instant now = credentialCreatedAt().minus(Duration.ofMinutes(4)); + + final var presentationMap = + VerifiablePresentation.presentationToMap(validPresentation()); + + assertThrows( + org.springframework.web.server.ResponseStatusException.class, + () -> { + VerifiablePresentation.validateCreatedTimes( + now, + maxClockSkew, + presentationMap + ); + } + ); + } + + +}