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
+ );
+ }
+ );
+ }
+
+
+}