diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/pom.xml b/dpppt-backend-sdk/dpppt-backend-sdk-data/pom.xml
index e880ae58..6456830b 100644
--- a/dpppt-backend-sdk/dpppt-backend-sdk-data/pom.xml
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/pom.xml
@@ -45,6 +45,7 @@
org.postgresqlpostgresql
+ 42.2.14org.hsqldb
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java
index fa2fff37..febffe7b 100644
--- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/GAENDataService.java
@@ -56,4 +56,13 @@ List getSortedExposedForKeyDate(
* @param retentionPeriod in milliseconds
*/
void cleanDB(Duration retentionPeriod);
+
+ /**
+ * Returns all exposed keys since keySince.
+ *
+ * @param keysSince
+ * @param now
+ * @return
+ */
+ List getSortedExposedSince(UTCInstant keysSince, UTCInstant now);
}
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java
index be19c704..863f947b 100644
--- a/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-data/src/main/java/org/dpppt/backend/sdk/data/gaen/JDBCGAENDataServiceImpl.java
@@ -125,6 +125,53 @@ public List getSortedExposedForKeyDate(
return jt.query(sql, params, new GaenKeyRowMapper());
}
+ @Override
+ @Transactional(readOnly = true)
+ public List getSortedExposedSince(UTCInstant keysSince, UTCInstant now) {
+ MapSqlParameterSource params = new MapSqlParameterSource();
+ params.addValue("since", keysSince.getDate());
+ params.addValue("maxBucket", now.roundToBucketStart(releaseBucketDuration).getDate());
+ params.addValue("timeSkewSeconds", timeSkew.toSeconds());
+
+ // Select keys since the given date. We need to make sure, only keys are returned
+ // that are allowed to be published.
+ // For this, we calculate the expiry for each key in a sub query. The expiry is then used for
+ // the where clause:
+ // - if expiry <= received_at: the key was ready to publish when we received it. Release this
+ // key, if received_at in [since, maxBucket)
+ // - if expiry > received_at: we have to wait until expiry till we can release this key. This
+ // means we only release the key if expiry in [since, maxBucket)
+ // This problem arises, because we only want key with received_at after since, but we need to
+ // ensure that we relase ALL keys meaning keys which were still valid when they were received
+
+ // we need to add the time skew to calculate the expiry timestamp of a key:
+ // TO_TIMESTAMP((rolling_start_number + rolling_period) * 10 * 60 + :timeSkewSeconds
+
+ String sql =
+ "select keys.pk_exposed_id, keys.key, keys.rolling_start_number, keys.rolling_period,"
+ + " keys.transmission_risk_level from (select pk_exposed_id, key,"
+ + " rolling_start_number, rolling_period, transmission_risk_level, received_at, "
+ + getSQLExpressionForExpiry()
+ + " as expiry from t_gaen_exposed)"
+ + " as keys where ((keys.received_at >= :since AND"
+ + " keys.received_at < :maxBucket AND keys.expiry <= keys.received_at) OR (keys.expiry"
+ + " >= :since AND keys.expiry < :maxBucket AND keys.expiry > keys.received_at))";
+
+ sql += " order by keys.pk_exposed_id desc";
+
+ return jt.query(sql, params, new GaenKeyRowMapper());
+ }
+
+ private String getSQLExpressionForExpiry() {
+ if (this.dbType.equals(PGSQL)) {
+ return "TO_TIMESTAMP((rolling_start_number + rolling_period) * 10 * 60 +"
+ + " :timeSkewSeconds)";
+ } else {
+ return "TIMESTAMP_WITH_ZONE((rolling_start_number + rolling_period) * 10 * 60 +"
+ + " :timeSkewSeconds)";
+ }
+ }
+
@Override
@Transactional(readOnly = false)
public void cleanDB(Duration retentionPeriod) {
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/model/gaen/GaenV2UploadKeysRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/model/gaen/GaenV2UploadKeysRequest.java
new file mode 100644
index 00000000..1c2bccdf
--- /dev/null
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/java/org/dpppt/backend/sdk/model/gaen/GaenV2UploadKeysRequest.java
@@ -0,0 +1,27 @@
+package org.dpppt.backend.sdk.model.gaen;
+
+import ch.ubique.openapi.docannotations.Documentation;
+import java.util.List;
+import javax.validation.Valid;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Size;
+
+public class GaenV2UploadKeysRequest {
+
+ @NotNull
+ @NotEmpty
+ @Valid
+ @Size(min = 30, max = 30)
+ @Documentation(
+ description = "30 Temporary Exposure Keys - zero or more of them might be fake keys.")
+ private List gaenKeys;
+
+ public List getGaenKeys() {
+ return this.gaenKeys;
+ }
+
+ public void setGaenKeys(List gaenKeys) {
+ this.gaenKeys = gaenKeys;
+ }
+}
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/proto/TemporaryExposureKeyFormatV2.proto b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/proto/TemporaryExposureKeyFormatV2.proto
new file mode 100644
index 00000000..f04d69b7
--- /dev/null
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-model/src/main/proto/TemporaryExposureKeyFormatV2.proto
@@ -0,0 +1,100 @@
+syntax = "proto2";
+package org.dpppt.backend.sdk.model.gaen.proto.v2;
+option java_package = "org.dpppt.backend.sdk.model.gaen.proto.v2";
+
+message TemporaryExposureKeyExport {
+ // Time window of keys in the file, based on arrival
+ // at the server, in UTC seconds.
+ optional fixed64 start_timestamp = 1;
+ optional fixed64 end_timestamp = 2;
+
+ // The region from which these keys came
+ optional string region = 3;
+
+ // Reserved for future use. Both batch_num and batch_size
+ // must be set to a value of 1.
+ optional int32 batch_num = 4;
+ optional int32 batch_size = 5;
+
+ // Information about associated signatures
+ repeated SignatureInfo signature_infos = 6;
+
+ // Exposure keys that are new.
+ repeated TemporaryExposureKey keys = 7;
+
+ // Keys that have changed status from previous key archives,
+ // including keys that are being revoked.
+ repeated TemporaryExposureKey revised_keys = 8;
+}
+message SignatureInfo {
+ // The first two fields have been deprecated.
+// reserved 1, 2;
+// reserved "app_bundle_id", "android_package";
+ optional string app_bundle_id = 1;
+ optional string android_package = 2;
+
+ // Key version in case the key server signing key is rotated. (e.g. "v1")
+ // A PHA can only have one active public key at a time, so they must rotate
+ // keys on all devices and servers at the same time to avoid problems.
+ optional string verification_key_version = 3;
+
+ // Implementation-specific string that can be used in key verification.
+ // Valid character in this string are all alphanumeric characters,
+ // underscores, and periods.
+ optional string verification_key_id = 4;
+
+ // All keys must be signed using the SHA-256 with ECDSA algorithm.
+ // This field must contain the string "1.2.840.10045.4.3.2".
+ optional string signature_algorithm = 5;
+}
+message TemporaryExposureKey {
+ // Temporary exposure key for an infected user.
+ optional bytes key_data = 1;
+
+ // Varying risk associated with a key depending on diagnosis method.
+ // Deprecated and no longer used.
+ optional int32 transmission_risk_level = 2 [deprecated = true];
+
+ // The interval number since epoch for which a key starts
+ optional int32 rolling_start_interval_number = 3;
+
+ // How long this key is valid, specified in increments of 10 minutes
+ optional int32 rolling_period = 4
+ [default = 144]; // defaults to 24 hours
+
+ // Data type that represents why this key was published.
+ enum ReportType {
+ UNKNOWN = 0; // Never returned by the client API.
+ CONFIRMED_TEST = 1;
+ CONFIRMED_CLINICAL_DIAGNOSIS = 2;
+ SELF_REPORT = 3;
+ RECURSIVE = 4; // Reserved for future use.
+ REVOKED = 5; // Used to revoke a key, never returned by client API.
+ }
+
+ // Type of diagnosis associated with a key.
+ optional ReportType report_type = 5;
+
+ // Number of days elapsed between symptom onset and the TEK being used.
+ // E.g. 2 means TEK is from 2 days after onset of symptoms.
+ // Valid values range is from -14 to 14.
+ optional sint32 days_since_onset_of_symptoms = 6;
+}
+message TEKSignatureList {
+ // Information about associated signatures
+ repeated TEKSignature signatures = 1;
+}
+
+message TEKSignature {
+ // Information to uniquely identify the public key associated
+ // with the key server's signing key.
+ optional SignatureInfo signature_info = 1;
+
+ // Reserved for future use. Both batch_num and batch_size
+ // must be set to a value of 1.
+ optional int32 batch_num = 2;
+ optional int32 batch_size = 3;
+
+ // Signature in X9.62 format (ASN.1 SEQUENCE of two INTEGER fields).
+ optional bytes signature = 4;
+}
\ No newline at end of file
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/pom.xml b/dpppt-backend-sdk/dpppt-backend-sdk-ws/pom.xml
index 0585219a..56bc0761 100644
--- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/pom.xml
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/pom.xml
@@ -1,12 +1,8 @@
-
+
spring-security-oauth2
+
+ org.postgresql
+ postgresql
+ 42.2.14
+
+
org.dpppt
@@ -123,27 +125,27 @@
- ch.ubique.openapi
- springboot-swagger-3
-
- 1.0-gapple
- org.dpppt.backend.sdk.model
+ ch.ubique.openapi
+ springboot-swagger-3
+
+ 1.0-gapple
+ org.dpppt.backend.sdk.modelcom.google.protobufbyte
-
+ org.dpppt.backend.sdk.ws.controller.GaenController
-
- DP3T API
-
- https://demo.dpppt.org
-
- DP3T API
-
-
+
+ DP3T API
+
+ https://demo.dpppt.org
+
+ DP3T API
+
+
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java
index 38915295..ec2f1f60 100644
--- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/config/WSBaseConfig.java
@@ -30,6 +30,7 @@
import org.dpppt.backend.sdk.data.gaen.GAENDataService;
import org.dpppt.backend.sdk.data.gaen.JDBCGAENDataServiceImpl;
import org.dpppt.backend.sdk.ws.controller.GaenController;
+import org.dpppt.backend.sdk.ws.controller.GaenV2Controller;
import org.dpppt.backend.sdk.ws.filter.ResponseWrapperFilter;
import org.dpppt.backend.sdk.ws.insertmanager.InsertManager;
import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.AssertKeyFormat;
@@ -43,6 +44,7 @@
import org.dpppt.backend.sdk.ws.insertmanager.insertionmodifier.OldAndroid0RPModifier;
import org.dpppt.backend.sdk.ws.interceptor.HeaderInjector;
import org.dpppt.backend.sdk.ws.security.KeyVault;
+import org.dpppt.backend.sdk.ws.security.NoValidateRequest;
import org.dpppt.backend.sdk.ws.security.ValidateRequest;
import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature;
import org.dpppt.backend.sdk.ws.util.ValidationUtils;
@@ -277,6 +279,9 @@ public ValidationUtils gaenValidationUtils() {
@Bean
public GaenController gaenController() {
ValidateRequest theValidator = gaenRequestValidator;
+ if (theValidator == null) {
+ theValidator = backupValidator();
+ }
return new GaenController(
insertManagerExposed(),
insertManagerExposedNextDay(),
@@ -291,6 +296,25 @@ public GaenController gaenController() {
keyVault.get("nextDayJWT").getPrivate());
}
+ @Bean
+ public GaenV2Controller gaenV2Controller() {
+ ValidateRequest theValidator = gaenRequestValidator;
+ if (theValidator == null) {
+ theValidator = backupValidator();
+ }
+ return new GaenV2Controller(
+ insertManagerExposed(),
+ theValidator,
+ gaenValidationUtils(),
+ fakeKeyService(),
+ gaenSigner(),
+ gaenDataService(),
+ Duration.ofMillis(releaseBucketDuration),
+ Duration.ofMillis(requestTime),
+ Duration.ofMillis(exposedListCacheControl),
+ Duration.ofDays(retentionDays));
+ }
+
@Bean
public GAENDataService gaenDataService() {
return new JDBCGAENDataServiceImpl(
@@ -302,6 +326,11 @@ public RedeemDataService redeemDataService() {
return new JDBCRedeemDataServiceImpl(dataSource());
}
+ @Bean
+ ValidateRequest backupValidator() {
+ return new NoValidateRequest();
+ }
+
@Bean
public MappingJackson2HttpMessageConverter converter() {
ObjectMapper mapper =
diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenV2Controller.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenV2Controller.java
new file mode 100644
index 00000000..0f4f2be6
--- /dev/null
+++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/controller/GaenV2Controller.java
@@ -0,0 +1,221 @@
+package org.dpppt.backend.sdk.ws.controller;
+
+import ch.ubique.openapi.docannotations.Documentation;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.io.IOException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SignatureException;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.List;
+import java.util.concurrent.Callable;
+import javax.validation.Valid;
+import org.dpppt.backend.sdk.data.gaen.FakeKeyService;
+import org.dpppt.backend.sdk.data.gaen.GAENDataService;
+import org.dpppt.backend.sdk.model.gaen.GaenKey;
+import org.dpppt.backend.sdk.model.gaen.GaenV2UploadKeysRequest;
+import org.dpppt.backend.sdk.utils.DurationExpiredException;
+import org.dpppt.backend.sdk.utils.UTCInstant;
+import org.dpppt.backend.sdk.ws.insertmanager.InsertException;
+import org.dpppt.backend.sdk.ws.insertmanager.InsertManager;
+import org.dpppt.backend.sdk.ws.insertmanager.insertionfilters.AssertKeyFormat.KeyFormatException;
+import org.dpppt.backend.sdk.ws.security.ValidateRequest;
+import org.dpppt.backend.sdk.ws.security.ValidateRequest.ClaimIsBeforeOnsetException;
+import org.dpppt.backend.sdk.ws.security.ValidateRequest.InvalidDateException;
+import org.dpppt.backend.sdk.ws.security.ValidateRequest.WrongScopeException;
+import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature;
+import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature.ProtoSignatureWrapper;
+import org.dpppt.backend.sdk.ws.util.ValidationUtils;
+import org.dpppt.backend.sdk.ws.util.ValidationUtils.BadBatchReleaseTimeException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.CacheControl;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.core.annotation.AuthenticationPrincipal;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.MethodArgumentNotValidException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+/**
+ * This is a new controller to simplify the sending and receiving of keys. It will be used by the
+ * new SwissCovid client and allows for ENV2 usage.
+ */
+@Controller
+@RequestMapping("/v2/gaen")
+@Documentation(
+ description =
+ "The GAEN V2 endpoint for the mobile clients supporting international key sharing")
+public class GaenV2Controller {
+
+ private static final Logger logger = LoggerFactory.getLogger(GaenV2Controller.class);
+
+ private final InsertManager insertManager;
+
+ private final ValidateRequest validateRequest;
+
+ private final ValidationUtils validationUtils;
+ private final FakeKeyService fakeKeyService;
+ private final ProtoSignature gaenSigner;
+ private final GAENDataService dataService;
+ private final Duration releaseBucketDuration;
+ private final Duration requestTime;
+ private final Duration exposedListCacheControl;
+ private final Duration retentionPeriod;
+
+ public GaenV2Controller(
+ InsertManager insertManager,
+ ValidateRequest validateRequest,
+ ValidationUtils validationUtils,
+ FakeKeyService fakeKeyService,
+ ProtoSignature gaenSigner,
+ GAENDataService dataService,
+ Duration releaseBucketDuration,
+ Duration requestTime,
+ Duration exposedListCacheControl,
+ Duration retentionPeriod) {
+ this.insertManager = insertManager;
+ this.validateRequest = validateRequest;
+ this.validationUtils = validationUtils;
+ this.fakeKeyService = fakeKeyService;
+ this.gaenSigner = gaenSigner;
+ this.dataService = dataService;
+ this.releaseBucketDuration = releaseBucketDuration;
+ this.requestTime = requestTime;
+ this.exposedListCacheControl = exposedListCacheControl;
+ this.retentionPeriod = retentionPeriod;
+ }
+
+ @GetMapping(value = "")
+ @Documentation(
+ description = "Hello return",
+ responses = {"200=>server live"})
+ public @ResponseBody ResponseEntity hello() {
+ return ResponseEntity.ok().header("X-HELLO", "dp3t").body("Hello from DP3T WS GAEN V2");
+ }
+
+ @PostMapping(value = "/exposed")
+ @Documentation(
+ description = "Endpoint used to upload exposure keys to the backend",
+ responses = {
+ "200=>The exposed keys have been stored in the database",
+ "400=> "
+ + "- Invalid base64 encoding in GaenRequest"
+ + "- negative rolling period"
+ + "- fake claim with non-fake keys",
+ "403=>Authentication failed"
+ })
+ public @ResponseBody Callable> addExposed(
+ @Documentation(description = "JSON Object containing all keys.") @Valid @RequestBody
+ GaenV2UploadKeysRequest gaenV2Request,
+ @RequestHeader(value = "User-Agent")
+ @Documentation(
+ description =
+ "App Identifier (PackageName/BundleIdentifier) + App-Version + OS (Android/iOS)"
+ + " + OS-Version",
+ example = "ch.ubique.android.starsdk;1.0;iOS;13.3")
+ String userAgent,
+ @AuthenticationPrincipal
+ @Documentation(description = "JWT token that can be verified by the backend server")
+ Object principal)
+ throws WrongScopeException, InsertException {
+ var now = UTCInstant.now();
+
+ this.validateRequest.isValid(principal);
+
+ // Filter out non valid keys and insert them into the database (c.f.
+ // InsertManager and
+ // configured Filters in the WSBaseConfig)
+ insertManager.insertIntoDatabase(gaenV2Request.getGaenKeys(), userAgent, principal, now);
+ var responseBuilder = ResponseEntity.ok();
+ Callable> cb =
+ () -> {
+ try {
+ now.normalizeDuration(requestTime);
+ } catch (DurationExpiredException e) {
+ logger.error("Total time spent in endpoint is longer than requestTime");
+ }
+ return responseBuilder.body("OK");
+ };
+ return cb;
+ }
+
+ // GET for Key Download
+ @GetMapping(value = "/exposed")
+ @Documentation(
+ description =
+ "Requests the exposed keys published _since_ originating from list of _country_",
+ responses = {
+ "200 => zipped export.bin and export.sig of all keys in that interval",
+ "404 => Invalid _since_ (too far in the past/future, not at bucket" + " boundaries)"
+ })
+ public @ResponseBody ResponseEntity getExposedKeys(
+ @Documentation(
+ description =
+ "Timestamp to retrieve exposed keys since, in milliseconds since Unix epoch"
+ + " (1970-01-01). It must indicate the beginning of a bucket. Optional, if"
+ + " no since set, all keys for the retention period are returned",
+ example = "1593043200000")
+ @RequestParam(required = false)
+ Long since)
+ throws BadBatchReleaseTimeException, InvalidKeyException, SignatureException,
+ NoSuchAlgorithmException, IOException {
+ var now = UTCInstant.now();
+
+ if (since == null) {
+ // if no since given, go back to the start of the retention period and select next bucket.
+ since = now.minus(retentionPeriod).roundToNextBucket(releaseBucketDuration).getTimestamp();
+ }
+ var keysSince = UTCInstant.ofEpochMillis(since);
+
+ if (!validationUtils.isValidBatchReleaseTime(keysSince, now)) {
+ return ResponseEntity.notFound().build();
+ }
+ UTCInstant publishedUntil = now.roundToBucketStart(releaseBucketDuration);
+
+ List exposedKeys = dataService.getSortedExposedSince(keysSince, now);
+
+ if (exposedKeys.isEmpty()) {
+ return ResponseEntity.noContent()
+ .cacheControl(CacheControl.maxAge(exposedListCacheControl))
+ .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil.getTimestamp()))
+ .build();
+ }
+ ProtoSignatureWrapper payload = gaenSigner.getPayloadV2(exposedKeys);
+
+ return ResponseEntity.ok()
+ .cacheControl(CacheControl.maxAge(exposedListCacheControl))
+ .header("X-PUBLISHED-UNTIL", Long.toString(publishedUntil.getTimestamp()))
+ .body(payload.getZip());
+ }
+
+ @ExceptionHandler({
+ IllegalArgumentException.class,
+ InvalidDateException.class,
+ JsonProcessingException.class,
+ MethodArgumentNotValidException.class,
+ BadBatchReleaseTimeException.class,
+ DateTimeParseException.class,
+ ClaimIsBeforeOnsetException.class,
+ KeyFormatException.class
+ })
+ @ResponseStatus(HttpStatus.BAD_REQUEST)
+ public ResponseEntity