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.postgresql postgresql + 42.2.14 org.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.model com.google.protobuf byte - + 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 invalidArguments() { + return ResponseEntity.badRequest().build(); + } + + @ExceptionHandler({WrongScopeException.class}) + @ResponseStatus(HttpStatus.FORBIDDEN) + public ResponseEntity forbidden() { + return ResponseEntity.status(HttpStatus.FORBIDDEN).build(); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java new file mode 100644 index 00000000..af9f3871 --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/NoValidateRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.dpppt.backend.sdk.ws.security; + +import org.dpppt.backend.sdk.model.gaen.GaenKey; +import org.dpppt.backend.sdk.model.gaen.GaenUnit; +import org.dpppt.backend.sdk.utils.UTCInstant; + +public class NoValidateRequest implements ValidateRequest { + + @Override + public boolean isValid(Object authObject) throws WrongScopeException { + return true; + } + + @Override + public long validateKeyDate(UTCInstant now, Object authObject, Object others) + throws ClaimIsBeforeOnsetException { + if (others instanceof GaenKey) { + GaenKey request = (GaenKey) others; + var keyDate = UTCInstant.of(request.getRollingStartNumber(), GaenUnit.TenMinutes); + return keyDate.getTimestamp(); + } + throw new IllegalArgumentException(); + } + + @Override + public boolean isFakeRequest(Object authObject, Object others) { + if (others instanceof GaenKey) { + GaenKey request = (GaenKey) others; + boolean fake = false; + if (request.getFake() == 1) { + fake = true; + } + return fake; + } + throw new IllegalArgumentException(); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java index 26d51067..48f74812 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/main/java/org/dpppt/backend/sdk/ws/security/signature/ProtoSignature.java @@ -32,6 +32,7 @@ import org.dpppt.backend.sdk.model.gaen.GaenUnit; import org.dpppt.backend.sdk.model.gaen.proto.TemporaryExposureKeyFormat; import org.dpppt.backend.sdk.model.gaen.proto.TemporaryExposureKeyFormat.SignatureInfo; +import org.dpppt.backend.sdk.model.gaen.proto.v2.TemporaryExposureKeyFormatV2; import org.dpppt.backend.sdk.utils.UTCInstant; public class ProtoSignature { @@ -120,6 +121,57 @@ public ProtoSignatureWrapper getPayload(List keys) return new ProtoSignatureWrapper(hashOut.toByteArray(), byteOut.toByteArray()); } + /** + * Creates a ZIP file containing the given keys and the corresponding signature. The keys are + * returned in the new v2 protobuf format. + * + * @param keys + * @return + * @throws IOException + * @throws InvalidKeyException + * @throws SignatureException + * @throws NoSuchAlgorithmException + */ + public ProtoSignatureWrapper getPayloadV2(List keys) + throws IOException, InvalidKeyException, SignatureException, NoSuchAlgorithmException { + if (keys.isEmpty()) { + throw new IOException("Keys should not be empty"); + } + ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); + ZipOutputStream zip = new ZipOutputStream(byteOut); + ByteArrayOutputStream hashOut = new ByteArrayOutputStream(); + var digest = MessageDigest.getInstance("SHA256"); + var keyDate = Duration.of(keys.get(0).getRollingStartNumber(), GaenUnit.TenMinutes); + var protoFile = getProtoKeyV2(keys, keyDate); + + zip.putNextEntry(new ZipEntry("export.bin")); + byte[] protoFileBytes = protoFile.toByteArray(); + byte[] exportBin = new byte[EXPORT_MAGIC.length + protoFileBytes.length]; + System.arraycopy(EXPORT_MAGIC, 0, exportBin, 0, EXPORT_MAGIC.length); + System.arraycopy(protoFileBytes, 0, exportBin, EXPORT_MAGIC.length, protoFileBytes.length); + zip.write(exportBin); + zip.closeEntry(); + + var signatureList = getSignatureObjectV2(exportBin); + digest.update(exportBin); + digest.update(keyPair.getPublic().getEncoded()); + hashOut.write(digest.digest()); + + byte[] exportSig = signatureList.toByteArray(); + zip.putNextEntry(new ZipEntry("export.sig")); + zip.write(exportSig); + zip.closeEntry(); + + zip.flush(); + zip.close(); + byteOut.flush(); + byteOut.close(); + hashOut.flush(); + hashOut.close(); + + return new ProtoSignatureWrapper(hashOut.toByteArray(), byteOut.toByteArray()); + } + private byte[] sign(byte[] data) throws SignatureException, InvalidKeyException, NoSuchAlgorithmException { Signature signature = Signature.getInstance(oidToJavaSignature.get(algorithm)); @@ -128,6 +180,32 @@ private byte[] sign(byte[] data) return signature.sign(); } + private org.dpppt.backend.sdk.model.gaen.proto.v2.TemporaryExposureKeyFormatV2.TEKSignatureList + getSignatureObjectV2(byte[] keyExport) + throws InvalidKeyException, SignatureException, NoSuchAlgorithmException { + byte[] exportSignature = sign(keyExport); + var signatureList = TemporaryExposureKeyFormatV2.TEKSignatureList.newBuilder(); + var theSignature = TemporaryExposureKeyFormatV2.TEKSignature.newBuilder(); + theSignature + .setSignatureInfo(tekSignatureV2()) + .setSignature(ByteString.copyFrom(exportSignature)) + .setBatchNum(1) + .setBatchSize(1); + signatureList.addSignatures(theSignature); + return signatureList.build(); + } + + private org.dpppt.backend.sdk.model.gaen.proto.v2.TemporaryExposureKeyFormatV2.SignatureInfo + tekSignatureV2() { + var tekSignature = TemporaryExposureKeyFormatV2.SignatureInfo.newBuilder(); + tekSignature + .setAppBundleId(appBundleId) + .setVerificationKeyVersion(keyVersion) + .setVerificationKeyId(keyVerificationId) + .setSignatureAlgorithm(algorithm); + return tekSignature.build(); + } + private TemporaryExposureKeyFormat.TEKSignatureList getSignatureObject(byte[] keyExport) throws InvalidKeyException, SignatureException, NoSuchAlgorithmException { byte[] exportSignature = sign(keyExport); @@ -169,7 +247,7 @@ public byte[] getPayload(Map> groupedBuckets) } var keyDate = Duration.of(keys.get(0).getRollingStartNumber(), GaenUnit.TenMinutes); - var protoFile = getProtoKey(keys, keyDate); + var protoFile = getProtoKeyV2(keys, keyDate); var zipFileName = new StringBuilder(); zipFileName.append("key_export_").append(group); @@ -186,7 +264,7 @@ public byte[] getPayload(Map> groupedBuckets) zip.write(exportBin); zip.closeEntry(); - var signatureList = getSignatureObject(exportBin); + var signatureList = getSignatureObjectV2(exportBin); byte[] exportSig = signatureList.toByteArray(); zip.putNextEntry(new ZipEntry("export.sig")); @@ -249,6 +327,35 @@ private TemporaryExposureKeyFormat.TemporaryExposureKeyExport getProtoKey( return file.build(); } + private TemporaryExposureKeyFormatV2.TemporaryExposureKeyExport getProtoKeyV2( + List exposedKeys, Duration batchReleaseTimeDuration) { + var file = TemporaryExposureKeyFormatV2.TemporaryExposureKeyExport.newBuilder(); + + var tekList = new ArrayList(); + for (var key : exposedKeys) { + var protoKey = + TemporaryExposureKeyFormatV2.TemporaryExposureKey.newBuilder() + .setKeyData(ByteString.copyFrom(Base64.getDecoder().decode(key.getKeyData()))) + .setRollingPeriod(key.getRollingPeriod()) + .setRollingStartIntervalNumber(key.getRollingStartNumber()) + .setDaysSinceOnsetOfSymptoms(0) // hardcode to zero + .build(); + tekList.add(protoKey); + } + + file.addAllKeys(tekList); + + file.setRegion(gaenRegion) + .setBatchNum(1) + .setBatchSize(1) + .setStartTimestamp(batchReleaseTimeDuration.toSeconds()) + .setEndTimestamp(batchReleaseTimeDuration.toSeconds() + releaseBucketDuration.toSeconds()); + + file.addSignatureInfos(tekSignatureV2()); + + return file.build(); + } + public class ProtoSignatureWrapper { private final byte[] hash; private final byte[] zip; diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenV2ControllerTest.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenV2ControllerTest.java new file mode 100644 index 00000000..5c4aaf9a --- /dev/null +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/controller/GaenV2ControllerTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2020 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package org.dpppt.backend.sdk.ws.controller; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.time.Duration; +import org.dpppt.backend.sdk.data.gaen.GAENDataService; +import org.dpppt.backend.sdk.ws.security.KeyVault; +import org.dpppt.backend.sdk.ws.security.signature.ProtoSignature; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@ActiveProfiles({"actuator-security"}) +@SpringBootTest( + properties = { + "ws.app.jwt.publickey=classpath://generated_pub.pem", + "logging.level.org.springframework.security=DEBUG", + "ws.exposedlist.releaseBucketDuration=7200000", + "ws.gaen.randomkeysenabled=true", + "ws.monitor.prometheus.user=prometheus", + "ws.monitor.prometheus.password=prometheus", + "management.endpoints.enabled-by-default=true", + "management.endpoints.web.exposure.include=*" + }) +@Transactional +public class GaenV2ControllerTest extends BaseControllerTest { + @Autowired ProtoSignature signer; + @Autowired KeyVault keyVault; + @Autowired GAENDataService gaenDataService; + + Duration releaseBucketDuration = Duration.ofMillis(7200000L); + + private static final Logger logger = LoggerFactory.getLogger(GaenV2ControllerTest.class); + + @Test + public void testHello() throws Exception { + MockHttpServletResponse response = + mockMvc + .perform(get("/v2/gaen")) + .andExpect(status().is2xxSuccessful()) + .andReturn() + .getResponse(); + + assertNotNull(response); + assertEquals("Hello from DP3T WS GAEN V2", response.getContentAsString()); + } +} diff --git a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java index 82d6d01d..cb476612 100644 --- a/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java +++ b/dpppt-backend-sdk/dpppt-backend-sdk-ws/src/test/java/org/dpppt/backend/sdk/ws/insertmanager/MockDataSource.java @@ -28,4 +28,10 @@ public List getSortedExposedForKeyDate( @Override public void cleanDB(Duration retentionPeriod) {} + + @Override + public List getSortedExposedSince( + UTCInstant keysSince, UTCInstant now) { // TODO Auto-generated method stub + return null; + } }