diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java
--- a/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java
+++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/config/SubmissionServiceConfig.java
@@ -116,13 +116,13 @@ public void setPath(String path) {
   }

   private static class Monitoring {
-    private Integer batchSize;
+    private Long batchSize;

-    public Integer getBatchSize() {
+    public Long getBatchSize() {
       return batchSize;
     }

-    public void setBatchSize(Integer batchSize) {
+    public void setBatchSize(Long batchSize) {
       this.batchSize = batchSize;
     }
   }
@@ -135,11 +135,11 @@ public void setMonitoring(Monitoring monitoring) {
     this.monitoring = monitoring;
   }

-  public Integer getMonitoringBatchSize() {
+  public Long getMonitoringBatchSize() {
     return this.monitoring.getBatchSize();
   }

-  public void setMonitoringBatchSize(Integer batchSize) {
+  public void setMonitoringBatchSize(Long batchSize) {
     this.monitoring.setBatchSize(batchSize);
   }
 }
diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/BatchCounter.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/BatchCounter.java
--- a/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/BatchCounter.java
+++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/BatchCounter.java
@@ -22,6 +22,7 @@

 import io.micrometer.core.instrument.Counter;
 import io.micrometer.core.instrument.MeterRegistry;
+import java.util.concurrent.atomic.AtomicLong;

 /**
  * Batch counter for counting requests for monitoring. Counts up in batches, given batch size. This way, single requests
@@ -33,11 +34,11 @@ public class BatchCounter {
   private static final String SUBMISSION_CONTROLLER_REQUESTS_COUNTER_DESCRIPTION
       = "Counts requests to the Submission Controller.";

-  private final int batchSize;
+  private final long batchSize;
   private final Counter counter;
-  private Double batch = 0.;
+  private final AtomicLong count = new AtomicLong(0L);

-  BatchCounter(MeterRegistry meterRegistry, int batchSize, String type) {
+  BatchCounter(MeterRegistry meterRegistry, long batchSize, String type) {
     this.batchSize = batchSize;
     counter = Counter.builder(SUBMISSION_CONTROLLER_REQUESTS_COUNTER_NAME)
         .tag("type", type)
@@ -50,11 +51,8 @@ public class BatchCounter {
    * counter is incremented.
    */
   public void increment() {
-    if (batch < batchSize) {
-      batch++;
-    } else {
-      counter.increment(batch);
-      batch = 1.;
+    if (0 == count.incrementAndGet() % batchSize) {
+      counter.increment(batchSize);
     }
   }
 }
diff --git a/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/SubmissionControllerMonitor.java b/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/SubmissionControllerMonitor.java
--- a/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/SubmissionControllerMonitor.java
+++ b/services/submission/src/main/java/app/coronawarn/server/services/submission/monitoring/SubmissionControllerMonitor.java
@@ -38,7 +38,7 @@ public class SubmissionControllerMonitor {

   private final MeterRegistry meterRegistry;

-  private final Integer batchSize;
+  private final long batchSize;
   private BatchCounter requests;
   private BatchCounter realRequests;
   private BatchCounter fakeRequests;

diff --git a/services/submission/src/test/java/app/coronawarn/server/services/submission/monitoring/BatchCounterTest.java b/services/submission/src/test/java/app/coronawarn/server/services/submission/monitoring/BatchCounterTest.java
new file mode 100644
--- /dev/null
+++ b/services/submission/src/test/java/app/coronawarn/server/services/submission/monitoring/BatchCounterTest.java
@@ -0,0 +1,64 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.server.services.submission.monitoring;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import io.micrometer.core.instrument.Counter;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.MeterRegistryMock;
+import java.util.stream.LongStream;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class BatchCounterTest {
+
+  private static final String COUNTER_TYPE = "FooCounter";
+  private MeterRegistry meterRegistry;
+  private Counter meterCounter;
+
+  @BeforeEach
+  void setUpCounter() {
+    meterCounter = mock(Counter.class);
+    meterRegistry = spy(new MeterRegistryMock(meterCounter));
+  }
+
+  @ParameterizedTest
+  @ValueSource(longs = {1L, 2L, 4L})
+  void incrementSubmittedOnceIfBatchSizeReached(long batchSize) {
+    BatchCounter batchCounter = new BatchCounter(meterRegistry, batchSize, COUNTER_TYPE);
+    LongStream.range(0, batchSize).forEach(__ -> batchCounter.increment());
+    verify(meterCounter, times(1)).increment(batchSize);
+  }
+
+  @ParameterizedTest
+  @ValueSource(longs = {2L, 4L, 7L})
+  void doesNotIncrementIfLesserThanBatchSize(long batchSize) {
+    BatchCounter batchCounter = new BatchCounter(meterRegistry, batchSize, COUNTER_TYPE);
+    LongStream.range(0, batchSize - 1).forEach(__ -> batchCounter.increment());
+    verify(meterCounter, never()).increment(batchSize);
+  }
+}
diff --git a/services/submission/src/test/java/io/micrometer/core/instrument/MeterRegistryMock.java b/services/submission/src/test/java/io/micrometer/core/instrument/MeterRegistryMock.java
new file mode 100644
--- /dev/null
+++ b/services/submission/src/test/java/io/micrometer/core/instrument/MeterRegistryMock.java
@@ -0,0 +1,40 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package io.micrometer.core.instrument;
+
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+
+/**
+ * Used to get access to the {@link Counter} instance in BatchCounterTest.
+ */
+public class MeterRegistryMock extends SimpleMeterRegistry {
+
+  final Counter counter;
+
+  public MeterRegistryMock(Counter counter) {
+    this.counter = counter;
+  }
+
+  @Override
+  Counter counter(Meter.Id id) {
+    return counter;
+  }
+}

BatchCounter uses Double for counting
## Describe the bug

BatchCounter uses `double` for integer arithmetics, which is commonly discouraged.

## Expected behaviour

BatchCounter uses `int` for counting up to `batchSize`, since `batchSize` is also of type `int`.
It's faster.
In theory it's more accurate. (In practice there is no difference in this case since the maximum value that is representable in a double is larger than `Integer.MAX_VALUE`. But for `long` instead of `int`, there would be a difference.)

## Possible Fix

~~~java
private int batch;
~~~

While here, `batch` should be renamed to `batchCount` to better express its actual meaning.
BatchCounter is not thread-safe
## Describe the bug

When multiple threads call `BatchCounter.increment` at the same time, the resulting count is unspecified.

## Expected behaviour

`synchronized void increment`

## Steps to reproduce the issue

Call `BatchCounter` from several threads at the same time.

## Possible Fix

Add `synchronized`.


diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapper.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapper.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapper.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapper.java
@@ -20,9 +20,11 @@

 package app.coronawarn.server.services.distribution.objectstore.client;

+import static java.lang.Boolean.TRUE;
 import static java.util.stream.Collectors.toList;

 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
@@ -78,13 +80,20 @@ public boolean bucketExists(String bucketName) {
       backoff = @Backoff(delayExpression = "${services.distribution.objectstore.retry-backoff}"))
   public List<S3Object> getObjects(String bucket, String prefix) {
     logRetryStatus("object download");
-
-    ListObjectsV2Response response =
-        s3Client.listObjectsV2(ListObjectsV2Request.builder().prefix(prefix).bucket(bucket).build());
-
-    return response.contents().stream()
-        .map(s3Object -> buildS3Object(s3Object, bucket))
-        .collect(toList());
+    List<S3Object> allS3Objects = new ArrayList<>();
+    String continuationToken = null;
+
+    do {
+      ListObjectsV2Request request =
+          ListObjectsV2Request.builder().prefix(prefix).bucket(bucket).continuationToken(continuationToken).build();
+      ListObjectsV2Response response = s3Client.listObjectsV2(request);
+      response.contents().stream()
+          .map(s3Object -> buildS3Object(s3Object, bucket))
+          .forEach(allS3Objects::add);
+      continuationToken = TRUE.equals(response.isTruncated()) ? response.continuationToken() : null;
+    } while (continuationToken != null);
+
+    return allS3Objects;
   }

   @Recover

diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapperTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapperTest.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapperTest.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapperTest.java
@@ -150,6 +150,25 @@ void testGetObjects(List<S3Object> expResult) {
     assertThat(actResult).isEqualTo(expResult);
   }

+  @Test
+  void testContinuationToken() {
+    var continuationToken = "1ueGcxLPRx1Tr/XYExHnhbYLgveDs2J/wm36Hy4vbOwM=<";
+
+    when(s3Client.listObjectsV2(any(ListObjectsV2Request.class)))
+        .thenReturn(ListObjectsV2Response.builder().isTruncated(true).continuationToken(continuationToken).build(),
+            ListObjectsV2Response.builder().isTruncated(false).build());
+
+    s3ClientWrapper.getObjects(VALID_BUCKET_NAME, VALID_PREFIX);
+
+    ListObjectsV2Request continuationRequest = ListObjectsV2Request.builder()
+        .prefix(VALID_PREFIX).bucket(VALID_BUCKET_NAME).continuationToken(continuationToken).build();
+    ListObjectsV2Request noContinuationRequest = ListObjectsV2Request.builder()
+        .prefix(VALID_PREFIX).bucket(VALID_BUCKET_NAME).build();
+
+    verify(s3Client, times(1)).listObjectsV2(eq(continuationRequest));
+    verify(s3Client, times(1)).listObjectsV2(eq(noContinuationRequest));
+  }
+
   private static Stream<Arguments> createGetObjectsResults() {
     return Stream.of(
         Lists.emptyList(),

S3ClientWrapper getObjects: Not all objects could be received
<!--
Thanks for reporting a bug 🙌 ❤

Before opening a new issue, please make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead.

Also, be sure to check our documentation first: <URL>
-->

## Describe the bug

https://github.com/corona-warn-app/cwa-server/blob/a953c054f8b4dc97d6a40525c5c3d5aa4d867394/services/distribution/src/main/java/app/coronawarn/server/services/distribution/objectstore/client/S3ClientWrapper.java#L83

It is possible that not all objects are received, because the **listObjectsV2(ListObjectsV2Request listObjectsV2Request)** method  limits them up to 1000.

Javadoc [listObjectsV2](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/S3Client.html#listObjectsV2-software.amazon.awssdk.services.s3.model.ListObjectsV2Request-)
> Returns some or all (up to 1,000) of the objects in a bucket.

## Possible Fix

1. Checking the response for more data with the **isTruncated** method.
JavaDoc [isTruncated](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/model/ListObjectsV2Response.html#isTruncated--)
> Set to false if all of the results were returned. Set to true if more keys are available to return. If the number of results exceeds that specified by MaxKeys, all of the results might not be returned.
2. When the response is truncated, calling the **nextContinuationToken** method for the next token.
JavaDoc [nextContinuationToken](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/model/ListObjectsV2Response.html#nextContinuationToken--)

> NextContinuationToken is sent when isTruncated is true, which means there are more keys in the bucket that can be listed. The next list requests to Amazon S3 can be continued with this NextContinuationToken.

3. Build a new ListObjectsV2Request with the **continuationToken** and call **listObjectsV2** again
JavaDoc [continuationToken](https://sdk.amazonaws.com/java/api/latest/software/amazon/awssdk/services/s3/model/ListObjectsV2Request.Builder.html#continuationToken-java.lang.String-)

> ContinuationToken indicates Amazon S3 that the list is being continued on this bucket with a token. ContinuationToken is obfuscated and is not a real key.

4. Repeat everything until isTruncate is false.

Here is a pretty good example from AWS. They use a different S3Client, but it is very similar to the one you use. ([source](https://docs.aws.amazon.com/AmazonS3/latest/dev/ListingObjectKeysUsingJava.html))

```java
import com.amazonaws.AmazonServiceException;
import com.amazonaws.SdkClientException;
import com.amazonaws.auth.profile.ProfileCredentialsProvider;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.ListObjectsV2Request;
import com.amazonaws.services.s3.model.ListObjectsV2Result;
import com.amazonaws.services.s3.model.S3ObjectSummary;

import java.io.IOException;

public class ListKeys {

    public static void main(String[] args) throws IOException {
        Regions clientRegion = Regions.DEFAULT_REGION;
        String bucketName = "*** Bucket name ***";

        try {
            AmazonS3 s3Client = AmazonS3ClientBuilder.standard()
                    .withCredentials(new ProfileCredentialsProvider())
                    .withRegion(clientRegion)
                    .build();

            System.out.println("Listing objects");

            // maxKeys is set to 2 to demonstrate the use of
            // ListObjectsV2Result.getNextContinuationToken()
            ListObjectsV2Request req = new ListObjectsV2Request().withBucketName(bucketName).withMaxKeys(2);
            ListObjectsV2Result result;

            do {
                result = s3Client.listObjectsV2(req);

                for (S3ObjectSummary objectSummary : result.getObjectSummaries()) {
                    System.out.printf(" - %s (size: %d)\n", objectSummary.getKey(), objectSummary.getSize());
                }
                // If there are more than maxKeys keys in the bucket, get a continuation token
                // and list the next objects.
                String token = result.getNextContinuationToken();
                System.out.println("Next Continuation Token: " + token);
                req.setContinuationToken(token);
            } while (result.isTruncated());
        } catch (AmazonServiceException e) {
            // The call was transmitted successfully, but Amazon S3 couldn't process
            // it, so it returned an error response.
            e.printStackTrace();
        } catch (SdkClientException e) {
            // Amazon S3 couldn't be contacted for a response, or the client
            // couldn't parse the response from Amazon S3.
            e.printStackTrace();
        }
    }
}
``


diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java
@@ -24,6 +24,7 @@

 import app.coronawarn.server.common.persistence.domain.DiagnosisKeyBuilders.Builder;
 import app.coronawarn.server.common.persistence.domain.validation.ValidRollingStartIntervalNumber;
+import app.coronawarn.server.common.persistence.domain.validation.ValidSubmissionTimestamp;
 import java.time.Instant;
 import java.time.LocalDateTime;
 import java.util.Arrays;
@@ -70,6 +71,7 @@ public class DiagnosisKey {
   @Range(min = 0, max = 8, message = "Risk level must be between 0 and 8.")
   private int transmissionRiskLevel;

+  @ValidSubmissionTimestamp
   private long submissionTimestamp;

   protected DiagnosisKey() {
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilder.java
@@ -24,6 +24,7 @@
 import static app.coronawarn.server.common.persistence.domain.DiagnosisKeyBuilders.FinalBuilder;
 import static app.coronawarn.server.common.persistence.domain.DiagnosisKeyBuilders.RollingStartIntervalNumberBuilder;
 import static app.coronawarn.server.common.persistence.domain.DiagnosisKeyBuilders.TransmissionRiskLevelBuilder;
+import static app.coronawarn.server.common.persistence.domain.validation.ValidSubmissionTimestampValidator.SECONDS_PER_HOUR;

 import app.coronawarn.server.common.persistence.exception.InvalidDiagnosisKeyException;
 import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey;
@@ -47,7 +48,7 @@ public class DiagnosisKeyBuilder implements
   private int rollingStartIntervalNumber;
   private int rollingPeriod = DiagnosisKey.EXPECTED_ROLLING_PERIOD;
   private int transmissionRiskLevel;
-  private long submissionTimestamp = -1L;
+  private Long submissionTimestamp = null;

   DiagnosisKeyBuilder() {
   }
@@ -93,9 +94,9 @@ public FinalBuilder withRollingPeriod(int rollingPeriod) {

   @Override
   public DiagnosisKey build() {
-    if (submissionTimestamp < 0) {
+    if (submissionTimestamp == null) {
       // hours since epoch
-      submissionTimestamp = Instant.now().getEpochSecond() / 3600L;
+      submissionTimestamp = Instant.now().getEpochSecond() / SECONDS_PER_HOUR;
     }

     var diagnosisKey = new DiagnosisKey(
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestamp.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestamp.java
new file mode 100644
--- /dev/null
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestamp.java
@@ -0,0 +1,58 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.server.common.persistence.domain.validation;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import javax.validation.Constraint;
+import javax.validation.Payload;
+
+@Constraint(validatedBy = ValidSubmissionTimestampValidator.class)
+@Target({ElementType.FIELD})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ValidSubmissionTimestamp {
+
+  /**
+   * Error message.
+   *
+   * @return the error message
+   */
+  String message() default "Submission timestamp (hours since epoch)"
+      + " must be greater or equal 0 and cannot be in the future.";
+
+  /**
+   * Groups.
+   *
+   * @return
+   */
+  Class<?>[] groups() default {};
+
+  /**
+   * Payload.
+   *
+   * @return
+   */
+  Class<? extends Payload>[] payload() default {};
+}
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestampValidator.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestampValidator.java
new file mode 100644
--- /dev/null
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/validation/ValidSubmissionTimestampValidator.java
@@ -0,0 +1,38 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.server.common.persistence.domain.validation;
+
+import java.time.Instant;
+import java.util.concurrent.TimeUnit;
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class ValidSubmissionTimestampValidator
+    implements ConstraintValidator<ValidSubmissionTimestamp, Long> {
+
+  public static final long SECONDS_PER_HOUR = TimeUnit.HOURS.toSeconds(1);
+
+  @Override
+  public boolean isValid(Long submissionTimestamp, ConstraintValidatorContext constraintValidatorContext) {
+    long currentHoursSinceEpoch = Instant.now().getEpochSecond() / SECONDS_PER_HOUR;
+    return submissionTimestamp >= 0L && submissionTimestamp <= currentHoursSinceEpoch;
+  }
+}
diff --git a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
--- a/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
+++ b/common/persistence/src/main/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyService.java
@@ -20,6 +20,7 @@

 package app.coronawarn.server.common.persistence.service;

+import static app.coronawarn.server.common.persistence.domain.validation.ValidSubmissionTimestampValidator.SECONDS_PER_HOUR;
 import static java.time.ZoneOffset.UTC;

 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
@@ -109,7 +110,7 @@ public void applyRetentionPolicy(int daysToRetain) {
     long threshold = LocalDateTime
         .ofInstant(Instant.now(), UTC)
         .minusDays(daysToRetain)
-        .toEpochSecond(UTC) / 3600L;
+        .toEpochSecond(UTC) / SECONDS_PER_HOUR;
     int numberOfDeletions = keyRepository.deleteBySubmissionTimestampIsLessThanEqual(threshold);
     logger.info("Deleted {} diagnosis key(s) with a submission timestamp older than {} day(s) ago.",
         numberOfDeletions, daysToRetain);

diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilderTest.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilderTest.java
--- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilderTest.java
+++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilderTest.java
@@ -20,6 +20,8 @@

 package app.coronawarn.server.common.persistence.domain;

+import static app.coronawarn.server.common.persistence.domain.validation.ValidSubmissionTimestampValidator.SECONDS_PER_HOUR;
+import static app.coronawarn.server.common.persistence.service.DiagnosisKeyServiceTestHelper.buildDiagnosisKeyForSubmissionTimestamp;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
 import static org.assertj.core.api.Assertions.catchThrowable;
@@ -28,6 +30,7 @@
 import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey;
 import com.google.protobuf.ByteString;
 import java.nio.charset.StandardCharsets;
+import java.time.Duration;
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.ZoneOffset;
@@ -189,6 +192,33 @@ void keyDataDoesNotThrowOnValid() {
         .doesNotThrowAnyException();
   }

+  @ParameterizedTest
+  @ValueSource(longs = {-1L, Long.MAX_VALUE})
+  void submissionTimestampMustBeValid(long submissionTimestamp) {
+    assertThat(
+        catchThrowable(() -> buildDiagnosisKeyForSubmissionTimestamp(submissionTimestamp)))
+        .isInstanceOf(InvalidDiagnosisKeyException.class);
+  }
+
+  @Test
+  void submissionTimestampMustNotBeInTheFuture() {
+    assertThat(catchThrowable(
+        () -> buildDiagnosisKeyForSubmissionTimestamp(getCurrentHoursSinceEpoch() + 1)))
+            .isInstanceOf(InvalidDiagnosisKeyException.class);
+    assertThat(catchThrowable(() -> buildDiagnosisKeyForSubmissionTimestamp(
+        Instant.now().getEpochSecond() /* accidentally forgot to divide by SECONDS_PER_HOUR */)))
+            .isInstanceOf(InvalidDiagnosisKeyException.class);
+  }
+
+  @Test
+  void submissionTimestampDoesNotThrowOnValid() {
+    assertThatCode(() -> buildDiagnosisKeyForSubmissionTimestamp(0L)).doesNotThrowAnyException();
+    assertThatCode(() -> buildDiagnosisKeyForSubmissionTimestamp(getCurrentHoursSinceEpoch())).doesNotThrowAnyException();
+    assertThatCode(
+        () -> buildDiagnosisKeyForSubmissionTimestamp(Instant.now().minus(Duration.ofHours(2)).getEpochSecond() / SECONDS_PER_HOUR))
+            .doesNotThrowAnyException();
+  }
+
   private DiagnosisKey keyWithKeyData(byte[] expKeyData) {
     return DiagnosisKey.builder()
         .withKeyData(expKeyData)
@@ -223,7 +253,7 @@ private void assertDiagnosisKeyEquals(DiagnosisKey actDiagnosisKey) {
   }

   private long getCurrentHoursSinceEpoch() {
-    return Instant.now().getEpochSecond() / 3600L;
+    return Instant.now().getEpochSecond() / SECONDS_PER_HOUR;
   }

   private void assertDiagnosisKeyEquals(DiagnosisKey actDiagnosisKey, long expSubmissionTimestamp) {
diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
--- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
+++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTest.java
@@ -21,6 +21,8 @@
 package app.coronawarn.server.common.persistence.service;

 import static app.coronawarn.server.common.persistence.service.DiagnosisKeyServiceTestHelper.assertDiagnosisKeysEqual;
+import static app.coronawarn.server.common.persistence.service.DiagnosisKeyServiceTestHelper.buildDiagnosisKeyForDateTime;
+import static app.coronawarn.server.common.persistence.service.DiagnosisKeyServiceTestHelper.buildDiagnosisKeyForSubmissionTimestamp;
 import static java.time.ZoneOffset.UTC;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatCode;
@@ -33,7 +35,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Random;
 import org.assertj.core.util.Lists;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.DisplayName;
@@ -171,19 +172,4 @@ void shouldNotUpdateExistingKey() {
     assertThat(actKeys.size()).isEqualTo(1);
     assertThat(actKeys.iterator().next().getTransmissionRiskLevel()).isEqualTo(2);
   }
-
-  public static DiagnosisKey buildDiagnosisKeyForSubmissionTimestamp(long submissionTimeStamp) {
-    byte[] randomBytes = new byte[16];
-    Random random = new Random(submissionTimeStamp);
-    random.nextBytes(randomBytes);
-    return DiagnosisKey.builder()
-        .withKeyData(randomBytes)
-        .withRollingStartIntervalNumber(600)
-        .withTransmissionRiskLevel(2)
-        .withSubmissionTimestamp(submissionTimeStamp).build();
-  }
-
-  public static DiagnosisKey buildDiagnosisKeyForDateTime(OffsetDateTime dateTime) {
-    return buildDiagnosisKeyForSubmissionTimestamp(dateTime.toEpochSecond() / 3600);
-  }
 }
diff --git a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java
--- a/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java
+++ b/common/persistence/src/test/java/app/coronawarn/server/common/persistence/service/DiagnosisKeyServiceTestHelper.java
@@ -23,7 +23,9 @@
 import static org.assertj.core.api.Assertions.assertThat;

 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
+import java.time.OffsetDateTime;
 import java.util.List;
+import java.util.Random;

 public class DiagnosisKeyServiceTestHelper {

@@ -48,4 +50,19 @@ public static void assertDiagnosisKeysEqual(List<DiagnosisKey> expKeys,
           .isEqualTo(expKey.getSubmissionTimestamp());
     }
   }
+
+  public static DiagnosisKey buildDiagnosisKeyForSubmissionTimestamp(long submissionTimeStamp) {
+    byte[] randomBytes = new byte[16];
+    Random random = new Random(submissionTimeStamp);
+    random.nextBytes(randomBytes);
+    return DiagnosisKey.builder()
+        .withKeyData(randomBytes)
+        .withRollingStartIntervalNumber(600)
+        .withTransmissionRiskLevel(2)
+        .withSubmissionTimestamp(submissionTimeStamp).build();
+  }
+
+  public static DiagnosisKey buildDiagnosisKeyForDateTime(OffsetDateTime dateTime) {
+    return buildDiagnosisKeyForSubmissionTimestamp(dateTime.toEpochSecond() / 3600);
+  }
 }

Add JPA range validator for submissionTimestamp of DiagnosisKey
DiagnosisKey uses a submissionTimestamp field of type long

https://github.com/corona-warn-app/cwa-server/blob/7059232b48d2507d142fe683fbebdda371a98652/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKey.java#L73

the value is actually the *hours* since epoch but there are no sanity checks on the value.

Just from reading the code you could e.g. easily think it's seconds since epoch and use it the wrong way without noticing at runtime.

Proposal:

* Add a JPA ConstraintValidator similar to ValidRollingStartIntervalNumberValidator which checks the value range to be greater or equal zero and not in the future (current hour since epoch rounded up)
* Add field javadoc to submissionTimestamp: "hours since epoch"

Expected Benefits:

* discard invalid data
* less potential for programming errors

I could open a PR wit the proposal if you think it makes sense
the getter and Builder already  do have javadoc "hours since epoch"

https://github.com/corona-warn-app/cwa-server/blob/master/common/persistence/src/main/java/app/coronawarn/server/common/persistence/domain/DiagnosisKeyBuilders.java#L80

so it's actually documented. Would still be nice to validate it though.
@jsievers makes sense. Please go ahead with a proposal PR.


diff --git a/services/distribution/src/main/resources/bootstrap-cloud.yaml b/services/distribution/src/main/resources/bootstrap-cloud.yaml
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/main/resources/bootstrap-cloud.yaml
@@ -0,0 +1,27 @@
+---
+spring:
+  application:
+    name: cwa-server
+  cloud:
+    vault:
+      enabled: true
+      generic:
+        enabled: false
+      kv:
+        enabled: true
+        backend: ${VAULT_BACKEND}
+        profile-separator: '/'
+        application-name: 'distribution'
+        default-context: ''
+        profiles: cloud
+      fail-fast: true
+      authentication: KUBERNETES
+      kubernetes:
+        role: ${VAULT_ROLE}
+        kubernetes-path: kubernetes
+        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
+      uri: ${VAULT_URI}
+      connection-timeout: 5000
+      read-timeout: 15000
+      config:
+        order: -10
diff --git a/services/distribution/src/main/resources/bootstrap.yaml b/services/distribution/src/main/resources/bootstrap.yaml
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/main/resources/bootstrap.yaml
@@ -0,0 +1,5 @@
+---
+spring:
+  cloud:
+    vault:
+      enabled: false
diff --git a/services/submission/pom.xml b/services/submission/pom.xml
--- a/services/submission/pom.xml
+++ b/services/submission/pom.xml
@@ -17,6 +17,11 @@
   </properties>

   <dependencies>
+    <dependency>
+      <groupId>org.springframework.cloud</groupId>
+      <artifactId>spring-cloud-starter-vault-config</artifactId>
+      <version>2.2.3.RELEASE</version>
+    </dependency>
     <dependency>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-web</artifactId>
diff --git a/services/submission/src/main/resources/bootstrap-cloud.yaml b/services/submission/src/main/resources/bootstrap-cloud.yaml
new file mode 100644
--- /dev/null
+++ b/services/submission/src/main/resources/bootstrap-cloud.yaml
@@ -0,0 +1,27 @@
+---
+spring:
+  application:
+    name: cwa-server
+  cloud:
+    vault:
+      enabled: true
+      generic:
+        enabled: false
+      kv:
+        enabled: true
+        backend: ${VAULT_BACKEND}
+        profile-separator: '/'
+        application-name: 'submission'
+        default-context: ''
+        profiles: cloud
+      fail-fast: true
+      authentication: KUBERNETES
+      kubernetes:
+        role: ${VAULT_ROLE}
+        kubernetes-path: kubernetes
+        service-account-token-file: /var/run/secrets/kubernetes.io/serviceaccount/token
+      uri: ${VAULT_URI}
+      connection-timeout: 5000
+      read-timeout: 15000
+      config:
+        order: -10
diff --git a/services/submission/src/main/resources/bootstrap.yaml b/services/submission/src/main/resources/bootstrap.yaml
new file mode 100644
--- /dev/null
+++ b/services/submission/src/main/resources/bootstrap.yaml
@@ -0,0 +1,5 @@
+---
+spring:
+  cloud:
+    vault:
+      enabled: false

diff --git a/services/distribution/src/test/resources/bootstrap.yaml b/services/distribution/src/test/resources/bootstrap.yaml
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/test/resources/bootstrap.yaml
@@ -0,0 +1,5 @@
+---
+spring:
+  cloud:
+    vault:
+      enabled: false
diff --git a/services/submission/src/test/resources/bootstrap.yaml b/services/submission/src/test/resources/bootstrap.yaml
new file mode 100644
--- /dev/null
+++ b/services/submission/src/test/resources/bootstrap.yaml
@@ -0,0 +1,5 @@
+---
+spring:
+  cloud:
+    vault:
+      enabled: false

Integrate Spring Cloud Vault
## Suggested Enhancement
Evaluate how the project can integrate the  [Spring Cloud Vault](https://cloud.spring.io/spring-cloud-vault/reference/html/) to enhance the process of credential handling.


diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/appconfig/structure/directory/AppConfigurationDirectory.java
@@ -32,6 +32,7 @@
 import app.coronawarn.server.services.distribution.assembly.structure.directory.decorator.indexing.IndexingDecoratorOnDisk;
 import app.coronawarn.server.services.distribution.assembly.structure.file.FileOnDisk;
 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import java.util.Optional;
 import java.util.Set;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -86,6 +87,7 @@ private void addConfigurationArchiveIfValid(String archiveName) {
     ArchiveOnDisk appConfigurationFile = new ArchiveOnDisk(archiveName);
     appConfigurationFile.addWritable(new FileOnDisk("export.bin", applicationConfiguration.toByteArray()));
     countryDirectory.addWritableToAll(ignoredValue ->
-        new AppConfigurationSigningDecorator(appConfigurationFile, cryptoProvider, distributionServiceConfig));
+        Optional
+            .of(new AppConfigurationSigningDecorator(appConfigurationFile, cryptoProvider, distributionServiceConfig)));
   }
 }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CwaApiStructureProvider.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CwaApiStructureProvider.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CwaApiStructureProvider.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/component/CwaApiStructureProvider.java
@@ -25,6 +25,7 @@
 import app.coronawarn.server.services.distribution.assembly.structure.directory.IndexDirectoryOnDisk;
 import app.coronawarn.server.services.distribution.assembly.structure.directory.decorator.indexing.IndexingDecoratorOnDisk;
 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import java.util.Optional;
 import java.util.Set;
 import org.springframework.stereotype.Component;

@@ -59,8 +60,10 @@ public Directory<WritableOnDisk> getDirectory() {
         ignoredValue -> Set.of(distributionServiceConfig.getApi().getVersionV1()),
         Object::toString);

-    versionDirectory.addWritableToAll(ignoredValue -> appConfigurationStructureProvider.getAppConfiguration());
-    versionDirectory.addWritableToAll(ignoredValue -> diagnosisKeysStructureProvider.getDiagnosisKeys());
+    versionDirectory.addWritableToAll(ignoredValue ->
+        Optional.of(appConfigurationStructureProvider.getAppConfiguration()));
+    versionDirectory.addWritableToAll(ignoredValue ->
+        Optional.of(diagnosisKeysStructureProvider.getDiagnosisKeys()));

     return new IndexingDecoratorOnDisk<>(versionDirectory, distributionServiceConfig.getOutputFileName());
   }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/DiagnosisKeyBundler.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/DiagnosisKeyBundler.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/DiagnosisKeyBundler.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/DiagnosisKeyBundler.java
@@ -95,6 +95,13 @@ public void setDiagnosisKeys(Collection<DiagnosisKey> diagnosisKeys, LocalDateTi
     this.createDiagnosisKeyDistributionMap(diagnosisKeys);
   }

+  /**
+   * Returns the {@link LocalDateTime} at which the distribution runs.
+   */
+  public LocalDateTime getDistributionTime() {
+    return this.distributionTime;
+  }
+
   /**
    * Returns all {@link DiagnosisKey DiagnosisKeys} contained by this {@link DiagnosisKeyBundler}.
    */
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysCountryDirectory.java
@@ -23,7 +23,6 @@
 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
 import app.coronawarn.server.services.distribution.assembly.component.CryptoProvider;
 import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.DiagnosisKeyBundler;
-import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory.decorator.DateAggregatingDecorator;
 import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory.decorator.DateIndexingDecorator;
 import app.coronawarn.server.services.distribution.assembly.structure.WritableOnDisk;
 import app.coronawarn.server.services.distribution.assembly.structure.directory.IndexDirectory;
@@ -31,6 +30,7 @@
 import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
 import java.time.LocalDate;
+import java.util.Optional;
 import java.util.Set;

 public class DiagnosisKeysCountryDirectory extends IndexDirectoryOnDisk<String> {
@@ -57,14 +57,12 @@ public DiagnosisKeysCountryDirectory(DiagnosisKeyBundler diagnosisKeyBundler,

   @Override
   public void prepare(ImmutableStack<Object> indices) {
-    this.addWritableToAll(ignoredValue ->
-        decorateDateDirectory(
-            new DiagnosisKeysDateDirectory(diagnosisKeyBundler, cryptoProvider, distributionServiceConfig)));
+    this.addWritableToAll(ignoredValue -> Optional.of(decorateDateDirectory(
+        new DiagnosisKeysDateDirectory(diagnosisKeyBundler, cryptoProvider, distributionServiceConfig))));
     super.prepare(indices);
   }

   private IndexDirectory<LocalDate, WritableOnDisk> decorateDateDirectory(DiagnosisKeysDateDirectory dateDirectory) {
-    return new DateAggregatingDecorator(new DateIndexingDecorator(dateDirectory, distributionServiceConfig),
-        cryptoProvider, distributionServiceConfig, diagnosisKeyBundler);
+    return new DateIndexingDecorator(dateDirectory, distributionServiceConfig);
   }
 }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectory.java
@@ -23,14 +23,23 @@
 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
 import app.coronawarn.server.services.distribution.assembly.component.CryptoProvider;
 import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.DiagnosisKeyBundler;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.archive.decorator.signing.DiagnosisKeySigningDecorator;
 import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory.decorator.HourIndexingDecorator;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.file.TemporaryExposureKeyExportFile;
+import app.coronawarn.server.services.distribution.assembly.structure.Writable;
 import app.coronawarn.server.services.distribution.assembly.structure.WritableOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.archive.Archive;
+import app.coronawarn.server.services.distribution.assembly.structure.archive.ArchiveOnDisk;
 import app.coronawarn.server.services.distribution.assembly.structure.directory.Directory;
 import app.coronawarn.server.services.distribution.assembly.structure.directory.IndexDirectoryOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.file.File;
 import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
 import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
 import java.time.LocalDate;
+import java.time.ZoneOffset;
 import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.Optional;

 public class DiagnosisKeysDateDirectory extends IndexDirectoryOnDisk<LocalDate> {

@@ -61,12 +70,39 @@ public void prepare(ImmutableStack<Object> indices) {
     this.addWritableToAll(ignoredValue -> {
       DiagnosisKeysHourDirectory hourDirectory =
           new DiagnosisKeysHourDirectory(diagnosisKeyBundler, cryptoProvider, distributionServiceConfig);
-      return decorateHourDirectory(hourDirectory);
+      return Optional.of(decorateHourDirectory(hourDirectory));
     });
+    this.addWritableToAll(this::indicesToDateDirectoryArchive);
     super.prepare(indices);
   }

+  private Optional<Writable<WritableOnDisk>> indicesToDateDirectoryArchive(ImmutableStack<Object> currentIndices) {
+    LocalDate currentDate = (LocalDate) currentIndices.peek();
+    if (currentDate.equals(diagnosisKeyBundler.getDistributionTime().toLocalDate())) {
+      return Optional.empty();
+    }
+    String region = (String) currentIndices.pop().peek();
+
+    List<DiagnosisKey> diagnosisKeysForCurrentHour =
+        this.diagnosisKeyBundler.getDiagnosisKeysForDate(currentDate);
+
+    long startTimestamp = currentDate.atStartOfDay().toEpochSecond(ZoneOffset.UTC);
+    long endTimestamp = currentDate.plusDays(1).atStartOfDay().toEpochSecond(ZoneOffset.UTC);
+
+    File<WritableOnDisk> temporaryExposureKeyExportFile = TemporaryExposureKeyExportFile.fromDiagnosisKeys(
+        diagnosisKeysForCurrentHour, region, startTimestamp, endTimestamp, distributionServiceConfig);
+
+    Archive<WritableOnDisk> dateArchive = new ArchiveOnDisk(distributionServiceConfig.getOutputFileName());
+    dateArchive.addWritable(temporaryExposureKeyExportFile);
+
+    return Optional.of(decorateDiagnosisKeyArchive(dateArchive));
+  }
+
   private Directory<WritableOnDisk> decorateHourDirectory(DiagnosisKeysHourDirectory hourDirectory) {
     return new HourIndexingDecorator(hourDirectory, distributionServiceConfig);
   }
+
+  private Directory<WritableOnDisk> decorateDiagnosisKeyArchive(Archive<WritableOnDisk> archive) {
+    return new DiagnosisKeySigningDecorator(archive, cryptoProvider, distributionServiceConfig);
+  }
 }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectory.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectory.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectory.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectory.java
@@ -37,6 +37,7 @@
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 import java.util.List;
+import java.util.Optional;

 public class DiagnosisKeysHourDirectory extends IndexDirectoryOnDisk<LocalDateTime> {

@@ -80,7 +81,7 @@ public void prepare(ImmutableStack<Object> indices) {
       Archive<WritableOnDisk> hourArchive = new ArchiveOnDisk(distributionServiceConfig.getOutputFileName());
       hourArchive.addWritable(temporaryExposureKeyExportFile);

-      return decorateDiagnosisKeyArchive(hourArchive);
+      return Optional.of(decorateDiagnosisKeyArchive(hourArchive));
     });
     super.prepare(indices);
   }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/decorator/DateAggregatingDecorator.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/decorator/DateAggregatingDecorator.java
deleted file mode 100644
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/decorator/DateAggregatingDecorator.java
+++ /dev/null
@@ -1,202 +0,0 @@
-/*-
- * ---license-start
- * Corona-Warn-App
- * ---
- * Copyright (C) 2020 SAP SE and all other contributors
- * ---
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ---license-end
- */
-
-package app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory.decorator;
-
-import static app.coronawarn.server.services.distribution.assembly.structure.util.functional.CheckedFunction.uncheckedFunction;
-
-import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKey;
-import app.coronawarn.server.common.protocols.external.exposurenotification.TemporaryExposureKeyExport;
-import app.coronawarn.server.services.distribution.assembly.component.CryptoProvider;
-import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.DiagnosisKeyBundler;
-import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.archive.decorator.signing.DiagnosisKeySigningDecorator;
-import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.file.TemporaryExposureKeyExportFile;
-import app.coronawarn.server.services.distribution.assembly.structure.Writable;
-import app.coronawarn.server.services.distribution.assembly.structure.WritableOnDisk;
-import app.coronawarn.server.services.distribution.assembly.structure.archive.Archive;
-import app.coronawarn.server.services.distribution.assembly.structure.archive.ArchiveOnDisk;
-import app.coronawarn.server.services.distribution.assembly.structure.directory.Directory;
-import app.coronawarn.server.services.distribution.assembly.structure.directory.DirectoryOnDisk;
-import app.coronawarn.server.services.distribution.assembly.structure.directory.IndexDirectory;
-import app.coronawarn.server.services.distribution.assembly.structure.directory.decorator.DirectoryDecorator;
-import app.coronawarn.server.services.distribution.assembly.structure.directory.decorator.IndexDirectoryDecorator;
-import app.coronawarn.server.services.distribution.assembly.structure.file.File;
-import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
-import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
-import java.time.LocalDate;
-import java.util.List;
-import java.util.NoSuchElementException;
-import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-/**
- * A {@link DirectoryDecorator} that will bundle hour aggregates into date aggregates and sign them.
- */
-public class DateAggregatingDecorator extends IndexDirectoryDecorator<LocalDate, WritableOnDisk> {
-
-  private final CryptoProvider cryptoProvider;
-  private final DistributionServiceConfig distributionServiceConfig;
-  private final DiagnosisKeyBundler diagnosisKeyBundler;
-
-  /**
-   * Creates a new DateAggregatingDecorator.
-   */
-  public DateAggregatingDecorator(IndexDirectory<LocalDate, WritableOnDisk> directory, CryptoProvider cryptoProvider,
-      DistributionServiceConfig distributionServiceConfig, DiagnosisKeyBundler diagnosisKeyBundler) {
-    super(directory);
-    this.cryptoProvider = cryptoProvider;
-    this.distributionServiceConfig = distributionServiceConfig;
-    this.diagnosisKeyBundler = diagnosisKeyBundler;
-  }
-
-  @Override
-  public void prepare(ImmutableStack<Object> indices) {
-    super.prepare(indices);
-    Set<Directory<WritableOnDisk>> dayDirectories = this.getWritables().stream()
-        .filter(writable -> writable instanceof DirectoryOnDisk)
-        .map(directory -> (DirectoryOnDisk) directory)
-        .collect(Collectors.toSet());
-    if (dayDirectories.isEmpty()) {
-      return;
-    }
-
-    Set<String> dates = this.getIndex(indices).stream()
-        .filter(diagnosisKeyBundler::numberOfKeysForDateBelowMaximum)
-        .map(this.getIndexFormatter())
-        .map(Object::toString)
-        .collect(Collectors.toSet());
-
-    dayDirectories.stream()
-        .filter(dayDirectory -> dates.contains(dayDirectory.getName()))
-        .forEach(currentDirectory -> Stream.of(currentDirectory)
-            .map(this::getSubSubDirectoryArchives)
-            .map(this::getTemporaryExposureKeyExportFilesFromArchives)
-            .map(this::parseTemporaryExposureKeyExportsFromFiles)
-            .map(this::reduceTemporaryExposureKeyExportsToNewFile)
-            .map(temporaryExposureKeyExportFile -> {
-              Archive<WritableOnDisk> aggregate = new ArchiveOnDisk(distributionServiceConfig.getOutputFileName());
-              aggregate.addWritable(temporaryExposureKeyExportFile);
-              return aggregate;
-            })
-            .map(file -> new DiagnosisKeySigningDecorator(file, cryptoProvider, distributionServiceConfig))
-            .forEach(aggregate -> {
-              currentDirectory.addWritable(aggregate);
-              aggregate.prepare(indices);
-            })
-        );
-  }
-
-  /**
-   * Returns all archives that are 3 levels down from the root directory.
-   */
-  private Set<Archive<WritableOnDisk>> getSubSubDirectoryArchives(Directory<WritableOnDisk> rootDirectory) {
-    return getWritablesInDirectory(rootDirectory, 3).stream()
-        .filter(Writable::isArchive)
-        .map(archive -> (Archive<WritableOnDisk>) archive)
-        .collect(Collectors.toSet());
-  }
-
-  /**
-   * Traverses a directory {@code depth} levels deep and returns a flattened list of all writables at that depth. A
-   * {@code depth} of 0 or less returns a set only containing the root directory. A depth of 1 returns a set of
-   * writables in the root directory. A depth of 2 returns a set of all writables in all directories in the root
-   * directory, and so on.
-   *
-   * @param rootDirectory The directory in which to start traversal.
-   * @param depth         The depth to traverse.
-   * @return All writables that are {@code depth} levels down.
-   */
-  private Set<Writable<WritableOnDisk>> getWritablesInDirectory(Directory<WritableOnDisk> rootDirectory, int depth) {
-    if (depth <= 0) {
-      return Set.of(rootDirectory);
-    } else if (depth == 1) {
-      return rootDirectory.getWritables();
-    } else {
-      return rootDirectory.getWritables().stream()
-          .filter(Writable::isDirectory)
-          .flatMap(directory -> getWritablesInDirectory((Directory<WritableOnDisk>) directory, depth - 1).stream())
-          .collect(Collectors.toSet());
-    }
-  }
-
-  private Set<TemporaryExposureKeyExportFile> getTemporaryExposureKeyExportFilesFromArchives(
-      Set<Archive<WritableOnDisk>> hourArchives) {
-    return hourArchives.stream()
-        .map(Directory::getWritables)
-        .map(writables -> writables.stream()
-            .filter(writable -> writable.getName().equals("export.bin")))
-        .map(Stream::findFirst)
-        .map(Optional::orElseThrow)
-        .filter(writable -> writable instanceof File)
-        .map(file -> (TemporaryExposureKeyExportFile) file)
-        .collect(Collectors.toSet());
-  }
-
-  private Set<TemporaryExposureKeyExport> parseTemporaryExposureKeyExportsFromFiles(
-      Set<TemporaryExposureKeyExportFile> temporaryExposureKeyExportFiles) {
-    return temporaryExposureKeyExportFiles.stream()
-        .map(TemporaryExposureKeyExportFile::getBytesWithoutHeader)
-        .map(uncheckedFunction(TemporaryExposureKeyExport::parseFrom))
-        .collect(Collectors.toSet());
-  }
-
-  private TemporaryExposureKeyExportFile reduceTemporaryExposureKeyExportsToNewFile(
-      Set<TemporaryExposureKeyExport> temporaryExposureKeyExports) {
-    return TemporaryExposureKeyExportFile.fromTemporaryExposureKeys(
-        getTemporaryExposureKeys(temporaryExposureKeyExports),
-        getRegion(temporaryExposureKeyExports),
-        getStartTimestamp(temporaryExposureKeyExports),
-        getEndTimestamp(temporaryExposureKeyExports),
-        distributionServiceConfig
-    );
-  }
-
-  private static Set<TemporaryExposureKey> getTemporaryExposureKeys(
-      Set<TemporaryExposureKeyExport> temporaryExposureKeyExports) {
-    return temporaryExposureKeyExports.stream()
-        .map(TemporaryExposureKeyExport::getKeysList)
-        .flatMap(List::stream)
-        .collect(Collectors.toSet());
-  }
-
-  private static String getRegion(Set<TemporaryExposureKeyExport> temporaryExposureKeyExports) {
-    return temporaryExposureKeyExports.stream()
-        .map(TemporaryExposureKeyExport::getRegion)
-        .findAny()
-        .orElseThrow(NoSuchElementException::new);
-  }
-
-  private static long getStartTimestamp(
-      Set<TemporaryExposureKeyExport> temporaryExposureKeyExports) {
-    return temporaryExposureKeyExports.stream()
-        .mapToLong(TemporaryExposureKeyExport::getStartTimestamp)
-        .min()
-        .orElseThrow(NoSuchElementException::new);
-  }
-
-  private static long getEndTimestamp(Set<TemporaryExposureKeyExport> temporaryExposureKeyExports) {
-    return temporaryExposureKeyExports.stream()
-        .mapToLong(TemporaryExposureKeyExport::getEndTimestamp)
-        .max()
-        .orElseThrow(NoSuchElementException::new);
-  }
-}
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryOnDisk.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryOnDisk.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryOnDisk.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryOnDisk.java
@@ -27,6 +27,7 @@
 import app.coronawarn.server.services.distribution.assembly.structure.util.functional.IndexFunction;
 import app.coronawarn.server.services.distribution.assembly.structure.util.functional.WritableFunction;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;

 /**
@@ -74,9 +75,9 @@ public void addWritableToAll(WritableFunction<WritableOnDisk> writableFunction)
   }

   /**
-   * Creates a new subdirectory for every element of the {@link IndexDirectory#getIndex index} and writes all its
-   * {@link IndexDirectory#addWritableToAll writables} to those. The respective element of the index will be pushed
-   * onto the Stack for subsequent {@link Writable#prepare} calls.
+   * Creates a new subdirectory for every element of the {@link IndexDirectory#getIndex index} and writes all its {@link
+   * IndexDirectory#addWritableToAll writables} to those. The respective element of the index will be pushed onto the
+   * Stack for subsequent {@link Writable#prepare} calls.
    *
    * @param indices A {@link ImmutableStack} of parameters from all {@link IndexDirectory IndexDirectories} further up
    *                in the hierarchy. The Stack may contain different types, depending on the types {@code T} of {@link
@@ -104,9 +105,11 @@ private DirectoryOnDisk makeSubDirectory(T index) {

   private void prepareMetaWritables(ImmutableStack<Object> indices, DirectoryOnDisk target) {
     this.metaWritables.forEach(metaWritableFunction -> {
-      Writable<WritableOnDisk> newWritable = metaWritableFunction.apply(indices);
-      target.addWritable(newWritable);
-      newWritable.prepare(indices);
+      Optional<Writable<WritableOnDisk>> maybeNewWritable = metaWritableFunction.apply(indices);
+      maybeNewWritable.ifPresent(newWritable -> {
+        target.addWritable(newWritable);
+        newWritable.prepare(indices);
+      });
     });
   }
 }
diff --git a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/util/functional/WritableFunction.java b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/util/functional/WritableFunction.java
--- a/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/util/functional/WritableFunction.java
+++ b/services/distribution/src/main/java/app/coronawarn/server/services/distribution/assembly/structure/util/functional/WritableFunction.java
@@ -22,6 +22,7 @@

 import app.coronawarn.server.services.distribution.assembly.structure.Writable;
 import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
+import java.util.Optional;
 import java.util.function.Function;

 /**
@@ -30,7 +31,8 @@
  * @param <T> The specific type of {@link Writable} that this {@link WritableFunction} can create.
  */
 @FunctionalInterface
-public interface WritableFunction<T extends Writable<T>> extends Function<ImmutableStack<Object>, Writable<T>> {
+public interface WritableFunction<T extends Writable<T>> extends
+    Function<ImmutableStack<Object>, Optional<Writable<T>>> {

-  Writable<T> apply(ImmutableStack<Object> t);
+  Optional<Writable<T>> apply(ImmutableStack<Object> t);
 }

diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/ProdDiagnosisKeyBundlerKeyRetrievalTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/ProdDiagnosisKeyBundlerKeyRetrievalTest.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/ProdDiagnosisKeyBundlerKeyRetrievalTest.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/ProdDiagnosisKeyBundlerKeyRetrievalTest.java
@@ -180,4 +180,12 @@ void testEmptyListWhenGettingDiagnosisKeysForHourBeforeEarliestDiagnosisKey() {
     bundler.setDiagnosisKeys(diagnosisKeys, LocalDateTime.of(1970, 1, 5, 0, 0));
     assertThat(bundler.getDiagnosisKeysForHour(LocalDateTime.of(1970, 1, 1, 0, 0, 0))).isEmpty();
   }
+
+  @Test
+  void testGetsCorrectDistributionDate(){
+    LocalDateTime expected = LocalDateTime.of(1970, 1, 5, 0, 0);
+    List<DiagnosisKey> diagnosisKeys = buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 2, 4, 0), 5);
+    bundler.setDiagnosisKeys(diagnosisKeys, expected);
+    assertThat(bundler.getDistributionTime()).isEqualTo(expected);
+  }
 }
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectoryTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectoryTest.java
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysDateDirectoryTest.java
@@ -0,0 +1,185 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory;
+
+import static app.coronawarn.server.services.distribution.common.Helpers.buildDiagnosisKeys;
+import static app.coronawarn.server.services.distribution.common.Helpers.getExpectedDateAndHourFiles;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
+import app.coronawarn.server.services.distribution.assembly.component.CryptoProvider;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.DiagnosisKeyBundler;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.ProdDiagnosisKeyBundler;
+import app.coronawarn.server.services.distribution.assembly.structure.WritableOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.directory.Directory;
+import app.coronawarn.server.services.distribution.assembly.structure.directory.DirectoryOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
+import app.coronawarn.server.services.distribution.common.Helpers;
+import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@EnableConfigurationProperties(value = DistributionServiceConfig.class)
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {CryptoProvider.class, DistributionServiceConfig.class},
+    initializers = ConfigFileApplicationContextInitializer.class)
+class DiagnosisKeysDateDirectoryTest {
+
+  @Rule
+  private final TemporaryFolder outputFolder = new TemporaryFolder();
+
+  @Autowired
+  CryptoProvider cryptoProvider;
+
+  @Autowired
+  DistributionServiceConfig distributionServiceConfig;
+
+  private File outputFile;
+
+  @BeforeEach
+  void setupAll() throws IOException {
+    outputFolder.create();
+    outputFile = outputFolder.newFolder();
+  }
+
+  private void runDateDistribution(Collection<DiagnosisKey> diagnosisKeys, LocalDateTime distributionTime) {
+    DiagnosisKeyBundler bundler = new ProdDiagnosisKeyBundler(distributionServiceConfig);
+    bundler
+        .setDiagnosisKeys(diagnosisKeys, distributionTime);
+    DiagnosisKeysDateDirectory dateDirectory = new DiagnosisKeysDateDirectory(bundler, cryptoProvider,
+        distributionServiceConfig);
+    Directory<WritableOnDisk> outputDirectory = new DirectoryOnDisk(outputFile);
+    outputDirectory.addWritable(dateDirectory);
+    dateDirectory.prepare(new ImmutableStack<>()
+        .push("version-directory")
+        .push("country-directory"));
+    outputDirectory.write();
+  }
+
+  @Test
+  void testCreatesCorrectDirectoryStructureForMultipleDates() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 3)
+        .mapToObj(currentDate -> IntStream.range(0, 5)
+            .mapToObj(currentHour ->
+                buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3 + currentDate, 0, 0).plusHours(currentHour), 5))
+            .flatMap(List::stream)
+            .collect(Collectors.toList()))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runDateDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 6, 0, 0));
+    Set<String> actualFiles = Helpers.getFilePaths(outputFile, outputFile.getAbsolutePath());
+    Set<String> expectedDateAndHourFiles = getExpectedDateAndHourFiles(Map.of(
+        "1970-01-03", List.of("0", "1", "2", "3", "4"),
+        "1970-01-04", List.of("0", "1", "2", "3", "4"),
+        "1970-01-05", List.of("0", "1", "2", "3", "4")), "1970-01-06");
+    assertThat(actualFiles).isEqualTo(expectedDateAndHourFiles);
+  }
+
+  @Test
+  void testDoesNotIncludeCurrentDateInDirectoryStructure() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 3)
+        .mapToObj(currentDate -> IntStream.range(0, 5)
+            .mapToObj(currentHour ->
+                buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3 + currentDate, 0, 0).plusHours(currentHour), 5))
+            .flatMap(List::stream)
+            .collect(Collectors.toList()))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runDateDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 5, 12, 0));
+    Set<String> actualFiles = Helpers.getFilePaths(outputFile, outputFile.getAbsolutePath());
+    Set<String> expectedDateAndHourFiles = getExpectedDateAndHourFiles(Map.of(
+        "1970-01-03", List.of("0", "1", "2", "3", "4"),
+        "1970-01-04", List.of("0", "1", "2", "3", "4"),
+        "1970-01-05", List.of("0", "1", "2", "3", "4")), "1970-01-05");
+    assertThat(actualFiles).isEqualTo(expectedDateAndHourFiles);
+  }
+
+  @Test
+  void testDoesNotIncludeEmptyDatesInDirectoryStructure() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 3)
+        .filter(currentDate -> currentDate != 1)
+        .mapToObj(currentDate -> IntStream.range(0, 5)
+            .mapToObj(currentHour ->
+                buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3 + currentDate, 0, 0).plusHours(currentHour), 5))
+            .flatMap(List::stream)
+            .collect(Collectors.toList()))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+
+    runDateDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 5, 12, 0));
+    Set<String> actualFiles = Helpers.getFilePaths(outputFile, outputFile.getAbsolutePath());
+    Set<String> expectedDateAndHourFiles = getExpectedDateAndHourFiles(Map.of(
+        "1970-01-03", List.of("0", "1", "2", "3", "4"),
+        "1970-01-05", List.of("0", "1", "2", "3", "4")), "1970-01-05");
+    assertThat(actualFiles).isEqualTo(expectedDateAndHourFiles);
+  }
+
+  @Test
+  void testDoesNotIncludeDatesWithTooFewKeysInDirectoryStructure() {
+    Collection<DiagnosisKey> diagnosisKeys = List.of(
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 1, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 4, 1, 0), 4),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 5, 1, 0), 5))
+        .stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runDateDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 6, 12, 0));
+    Set<String> actualFiles = Helpers.getFilePaths(outputFile, outputFile.getAbsolutePath());
+    Set<String> expectedDateAndHourFiles = getExpectedDateAndHourFiles(Map.of(
+        "1970-01-03", List.of("1"),
+        "1970-01-05", List.of("1")), "1970-01-06");
+    assertThat(actualFiles).isEqualTo(expectedDateAndHourFiles);
+  }
+
+  @Test
+  void testDoesNotIncludeDatesInTheFuture() {
+    Collection<DiagnosisKey> diagnosisKeys = List.of(
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 1, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 4, 1, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 5, 1, 0), 5))
+        .stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runDateDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 4, 12, 0));
+    Set<String> actualFiles = Helpers.getFilePaths(outputFile, outputFile.getAbsolutePath());
+    Set<String> expectedDateAndHourFiles = getExpectedDateAndHourFiles(Map.of(
+        "1970-01-03", List.of("1"),
+        "1970-01-04", List.of("1")), "1970-01-04");
+    assertThat(actualFiles).isEqualTo(expectedDateAndHourFiles);
+  }
+}
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectoryTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectoryTest.java
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/diagnosiskeys/structure/directory/DiagnosisKeysHourDirectoryTest.java
@@ -0,0 +1,156 @@
+/*-
+ * ---license-start
+ * Corona-Warn-App
+ * ---
+ * Copyright (C) 2020 SAP SE and all other contributors
+ * ---
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ---license-end
+ */
+
+package app.coronawarn.server.services.distribution.assembly.diagnosiskeys.structure.directory;
+
+import static app.coronawarn.server.services.distribution.common.Helpers.buildDiagnosisKeys;
+import static app.coronawarn.server.services.distribution.common.Helpers.getExpectedHourFiles;
+import static app.coronawarn.server.services.distribution.common.Helpers.getFilePaths;
+import static org.assertj.core.api.Assertions.assertThat;
+
+import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
+import app.coronawarn.server.services.distribution.assembly.component.CryptoProvider;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.DiagnosisKeyBundler;
+import app.coronawarn.server.services.distribution.assembly.diagnosiskeys.ProdDiagnosisKeyBundler;
+import app.coronawarn.server.services.distribution.assembly.structure.WritableOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.directory.Directory;
+import app.coronawarn.server.services.distribution.assembly.structure.directory.DirectoryOnDisk;
+import app.coronawarn.server.services.distribution.assembly.structure.util.ImmutableStack;
+import app.coronawarn.server.services.distribution.config.DistributionServiceConfig;
+import java.io.File;
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.rules.TemporaryFolder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.boot.test.context.ConfigFileApplicationContextInitializer;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+@EnableConfigurationProperties(value = DistributionServiceConfig.class)
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {CryptoProvider.class, DistributionServiceConfig.class},
+    initializers = ConfigFileApplicationContextInitializer.class)
+class DiagnosisKeysHourDirectoryTest {
+
+  @Rule
+  private final TemporaryFolder outputFolder = new TemporaryFolder();
+
+  @Autowired
+  CryptoProvider cryptoProvider;
+
+  @Autowired
+  DistributionServiceConfig distributionServiceConfig;
+
+  private File outputFile;
+
+  @BeforeEach
+  void setupAll() throws IOException {
+    outputFolder.create();
+    outputFile = outputFolder.newFolder();
+  }
+
+  private void runHourDistribution(Collection<DiagnosisKey> diagnosisKeys, LocalDateTime distributionTime) {
+    DiagnosisKeyBundler bundler = new ProdDiagnosisKeyBundler(distributionServiceConfig);
+    bundler.setDiagnosisKeys(diagnosisKeys, distributionTime);
+    DiagnosisKeysHourDirectory hourDirectory = new DiagnosisKeysHourDirectory(bundler, cryptoProvider,
+        distributionServiceConfig);
+    Directory<WritableOnDisk> outputDirectory = new DirectoryOnDisk(outputFile);
+    outputDirectory.addWritable(hourDirectory);
+    hourDirectory.prepare(new ImmutableStack<>()
+        .push("version-directory")
+        .push("country-directory")
+        .push(LocalDate.of(1970, 1, 3)) // date-directory
+    );
+    outputDirectory.write();
+  }
+
+  @Test
+  void testCreatesCorrectStructureForMultipleHours() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 5)
+        .mapToObj(currentHour -> buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 0, 0).plusHours(currentHour), 5))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runHourDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 5, 0, 0));
+    Set<String> actualFiles = getFilePaths(outputFile, outputFile.getAbsolutePath());
+    assertThat(actualFiles).isEqualTo(getExpectedHourFiles(Set.of("0", "1", "2", "3", "4")));
+  }
+
+  @Test
+  void testDoesNotIncludeEmptyHours() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 5)
+        .filter(currentHour -> currentHour != 3)
+        .mapToObj(currentHour -> buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 0, 0).plusHours(currentHour), 5))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runHourDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 5, 0, 0));
+    Set<String> actualFiles = getFilePaths(outputFile, outputFile.getAbsolutePath());
+    assertThat(actualFiles).isEqualTo(getExpectedHourFiles(Set.of("0", "1", "2", "4")));
+  }
+
+  @Test
+  void testDoesNotIncludeCurrentHour() {
+    Collection<DiagnosisKey> diagnosisKeys = IntStream.range(0, 5)
+        .mapToObj(currentHour -> buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 0, 0).plusHours(currentHour), 5))
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runHourDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 3, 4, 0));
+    Set<String> actualFiles = getFilePaths(outputFile, outputFile.getAbsolutePath());
+    assertThat(actualFiles).isEqualTo(getExpectedHourFiles(Set.of("0", "1", "2", "3")));
+  }
+
+  @Test
+  void testDoesNotIncludeHoursWithTooFewKeys() {
+    Collection<DiagnosisKey> diagnosisKeys = List.of(
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 0, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 1, 0), 4),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 2, 0), 5))
+        .stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runHourDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 6, 12, 0));
+    Set<String> actualFiles = getFilePaths(outputFile, outputFile.getAbsolutePath());
+    assertThat(actualFiles).isEqualTo(getExpectedHourFiles(Set.of("0", "2")));
+  }
+
+  @Test
+  void testDoesNotIncludeHoursInTheFuture() {
+    Collection<DiagnosisKey> diagnosisKeys = List.of(
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 0, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 1, 0), 5),
+        buildDiagnosisKeys(6, LocalDateTime.of(1970, 1, 3, 2, 0), 5))
+        .stream()
+        .flatMap(List::stream)
+        .collect(Collectors.toList());
+    runHourDistribution(diagnosisKeys, LocalDateTime.of(1970, 1, 3, 1, 0));
+    Set<String> actualFiles = getFilePaths(outputFile, outputFile.getAbsolutePath());
+    assertThat(actualFiles).isEqualTo(getExpectedHourFiles(Set.of("0")));
+  }
+}
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryTest.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryTest.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/structure/directory/IndexDirectoryTest.java
@@ -32,6 +32,7 @@
 import java.util.Arrays;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -79,7 +80,7 @@ void checkAddFileToAll() {
     indexDirectory.addWritableToAll(ignoredValue -> {
       FileOnDisk newFile = new FileOnDisk("index", new byte[0]);
       expectedFileList.add(newFile);
-      return newFile;
+      return Optional.of(newFile);
     });

     prepareAndWrite(outputDirectory);
@@ -104,7 +105,7 @@ void checkAddDirectoryToAll() {
     indexDirectory.addWritableToAll(ignoredValue -> {
       DirectoryOnDisk newDirectory = new DirectoryOnDisk("something");
       expectedFileList.add(newDirectory);
-      return newDirectory;
+      return Optional.of(newDirectory);
     });

     prepareAndWrite(outputDirectory);
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/common/Helpers.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/common/Helpers.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/common/Helpers.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/common/Helpers.java
@@ -21,6 +21,7 @@
 package app.coronawarn.server.services.distribution.common;

 import static app.coronawarn.server.services.distribution.assembly.appconfig.YamlLoader.loadYamlIntoProtobufBuilder;
+import static java.io.File.separator;

 import app.coronawarn.server.common.persistence.domain.DiagnosisKey;
 import app.coronawarn.server.common.protocols.internal.ApplicationConfiguration;
@@ -31,7 +32,10 @@
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -95,4 +99,34 @@ public static Set<String> getFilePaths(java.io.File root, String basePath) {
   public static ApplicationConfiguration loadApplicationConfiguration(String path) throws UnableToLoadFileException {
     return loadYamlIntoProtobufBuilder(path, ApplicationConfiguration.Builder.class).build();
   }
+
+  public static Set<String> getExpectedHourFiles(Collection<String> hours) {
+    return hours.stream()
+        .map(hour -> Set.of(
+            String.join(separator, "hour", hour, "index"),
+            String.join(separator, "hour", hour, "index.checksum")))
+        .flatMap(Set::stream)
+        .collect(Collectors.toSet());
+  }
+
+  public static Set<String> getExpectedDateAndHourFiles(Map<String, List<String>> datesAndHours, String currentDate) {
+    Set<String> expectedFiles = new HashSet<>();
+
+    datesAndHours.forEach((date, hours) -> {
+      if (!date.equals(currentDate)) {
+        expectedFiles.add(String.join(separator, "date", date, "index"));
+        expectedFiles.add(String.join(separator, "date", date, "index.checksum"));
+      }
+
+      expectedFiles.add(String.join(separator, "date", date, "hour", "index"));
+      expectedFiles.add(String.join(separator, "date", date, "hour", "index.checksum"));
+
+      hours.forEach(hour -> {
+        expectedFiles.add(String.join(separator, "date", date, "hour", hour, "index"));
+        expectedFiles.add(String.join(separator, "date", date, "hour", hour, "index.checksum"));
+      });
+    });
+
+    return expectedFiles;
+  }
 }

Simplifying DateAggregatingDecorator
The DateAggregatingDecorator should make more use of the `DiagnosisKeyBundler`. Please team up with @pithumke on this.


diff --git a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto
--- a/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto
+++ b/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto
@@ -59,6 +59,6 @@ message RiskScoreParameters {
     RiskLevel gt_27_le_33_dbm = 5;      // 27 < A <= 33 dBm
     RiskLevel gt_15_le_27_dbm = 6;      // 15 < A <= 27 dBm
     RiskLevel gt_10_le_15_dbm = 7;      // 10 < A <= 15 dBm
-    RiskLevel lt_10_dbm = 8;            // A <= 10 dBm, highest risk
+    RiskLevel le_10_dbm = 8;            // A <= 10 dBm, highest risk
   }
-}
\ No newline at end of file
+}
diff --git a/services/distribution/src/main/resources/master-config/exposure-config.yaml b/services/distribution/src/main/resources/master-config/exposure-config.yaml
--- a/services/distribution/src/main/resources/master-config/exposure-config.yaml
+++ b/services/distribution/src/main/resources/master-config/exposure-config.yaml
@@ -55,4 +55,4 @@ attenuation:
   gt_27_le_33_dbm: 1
   gt_15_le_27_dbm: 1
   gt_10_le_15_dbm: 1
-  lt_10_dbm: 1
+  le_10_dbm: 1

diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/YamlLoaderTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/YamlLoaderTest.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/YamlLoaderTest.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/YamlLoaderTest.java
@@ -40,8 +40,9 @@ void okFile() throws UnableToLoadFileException {
   @ValueSource(strings = {
       "configtests/app-config_empty.yaml",
       "configtests/wrong_file.yaml",
-      "configtests/broken_syntax.yaml",
-      "file_does_not_exist_anywhere.yaml"
+      "configtests/app-config_broken_syntax.yaml",
+      "configtests/naming_mismatch.yaml",
+      "configtests/file_does_not_exist_anywhere.yaml"
   })
   void throwsLoadFailure(String fileName) {
     assertThatExceptionOfType(UnableToLoadFileException.class).isThrownBy(() -> loadApplicationConfiguration(fileName));
diff --git a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ExposureConfigurationValidatorTest.java b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ExposureConfigurationValidatorTest.java
--- a/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ExposureConfigurationValidatorTest.java
+++ b/services/distribution/src/test/java/app/coronawarn/server/services/distribution/assembly/appconfig/validation/ExposureConfigurationValidatorTest.java
@@ -22,8 +22,7 @@

 import static app.coronawarn.server.services.distribution.assembly.appconfig.validation.ExposureConfigurationValidator.CONFIG_PREFIX;
 import static app.coronawarn.server.services.distribution.assembly.appconfig.validation.RiskScoreClassificationValidatorTest.buildError;
-import static app.coronawarn.server.services.distribution.assembly.appconfig.validation.ValidationError.ErrorType.TOO_MANY_DECIMAL_PLACES;
-import static app.coronawarn.server.services.distribution.assembly.appconfig.validation.ValidationError.ErrorType.VALUE_OUT_OF_BOUNDS;
+import static app.coronawarn.server.services.distribution.assembly.appconfig.validation.ValidationError.ErrorType.*;
 import static org.assertj.core.api.Assertions.assertThat;

 import app.coronawarn.server.common.protocols.internal.RiskLevel;
diff --git a/services/distribution/src/test/resources/configtests/exposure-config_ok.yaml b/services/distribution/src/test/resources/configtests/exposure-config_ok.yaml
--- a/services/distribution/src/test/resources/configtests/exposure-config_ok.yaml
+++ b/services/distribution/src/test/resources/configtests/exposure-config_ok.yaml
@@ -55,4 +55,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/configtests/naming_mismatch.yaml b/services/distribution/src/test/resources/configtests/naming_mismatch.yaml
new file mode 100644
--- /dev/null
+++ b/services/distribution/src/test/resources/configtests/naming_mismatch.yaml
@@ -0,0 +1,43 @@
+transmission_weight: 10.001
+duration_weight: 0.001
+attenuation_weight: 100
+
+transmission:
+  foo: 2 # "foo" does not exist in proto
+  app_defined_2: 2
+  app_defined_3: 3
+  app_defined_4: 4
+  app_defined_5: 5
+  app_defined_6: 6
+  app_defined_7: 7
+  app_defined_8: 8
+
+duration:
+  eq_0_min: 1
+  gt_0_le_5_min: 2
+  gt_5_le_10_min: 3
+  gt_10_le_15_min: 4
+  gt_15_le_20_min: 5
+  gt_20_le_25_min: 6
+  gt_25_le_30_min: 7
+  gt_30_min: 8
+
+days_since_last_exposure:
+  ge_14_days: 1
+  ge_12_lt_14_days: 2
+  ge_10_lt_12_days: 3
+  ge_8_lt_10_days: 4
+  ge_6_lt_8_days: 5
+  ge_4_lt_6_days: 6
+  ge_2_lt_4_days: 7
+  ge_0_lt_2_days: 8
+
+attenuation:
+  gt_73_dbm: 1
+  gt_63_le_73_dbm: 2
+  gt_51_le_63_dbm: 3
+  gt_33_le_51_dbm: 4
+  gt_27_le_33_dbm: 5
+  gt_15_le_27_dbm: 6
+  gt_10_le_15_dbm: 7
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/objectstore/publisher/examplefile b/services/distribution/src/test/resources/objectstore/publisher/examplefile
--- a/services/distribution/src/test/resources/objectstore/publisher/examplefile
+++ b/services/distribution/src/test/resources/objectstore/publisher/examplefile
@@ -43,4 +43,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/all_ok.yaml b/services/distribution/src/test/resources/parameters/all_ok.yaml
--- a/services/distribution/src/test/resources/parameters/all_ok.yaml
+++ b/services/distribution/src/test/resources/parameters/all_ok.yaml
@@ -43,4 +43,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/broken_syntax.yaml b/services/distribution/src/test/resources/parameters/broken_syntax.yaml
--- a/services/distribution/src/test/resources/parameters/broken_syntax.yaml
+++ b/services/distribution/src/test/resources/parameters/broken_syntax.yaml
@@ -40,4 +40,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/partly_filled.yaml b/services/distribution/src/test/resources/parameters/partly_filled.yaml
--- a/services/distribution/src/test/resources/parameters/partly_filled.yaml
+++ b/services/distribution/src/test/resources/parameters/partly_filled.yaml
@@ -43,4 +43,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/score_too_high.yaml b/services/distribution/src/test/resources/parameters/score_too_high.yaml
--- a/services/distribution/src/test/resources/parameters/score_too_high.yaml
+++ b/services/distribution/src/test/resources/parameters/score_too_high.yaml
@@ -40,4 +40,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/weight_negative.yaml b/services/distribution/src/test/resources/parameters/weight_negative.yaml
--- a/services/distribution/src/test/resources/parameters/weight_negative.yaml
+++ b/services/distribution/src/test/resources/parameters/weight_negative.yaml
@@ -41,4 +41,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/weight_ok.yaml b/services/distribution/src/test/resources/parameters/weight_ok.yaml
--- a/services/distribution/src/test/resources/parameters/weight_ok.yaml
+++ b/services/distribution/src/test/resources/parameters/weight_ok.yaml
@@ -40,4 +40,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8
diff --git a/services/distribution/src/test/resources/parameters/weight_too_high.yaml b/services/distribution/src/test/resources/parameters/weight_too_high.yaml
--- a/services/distribution/src/test/resources/parameters/weight_too_high.yaml
+++ b/services/distribution/src/test/resources/parameters/weight_too_high.yaml
@@ -40,4 +40,4 @@ attenuation:
   gt_27_le_33_dbm: 5
   gt_15_le_27_dbm: 6
   gt_10_le_15_dbm: 7
-  lt_10_dbm: 8
\ No newline at end of file
+  le_10_dbm: 8

[MINOR] Incorrect value range in attenuation risk parameter
## Describe the bug
The `AttenuationRiskParameters` proto message (see [here](https://github.com/corona-warn-app/cwa-server/blob/master/common/protocols/src/main/proto/app/coronawarn/server/common/protocols/internal/risk_score_parameters.proto)) is defined as follows:
```proto3
message AttenuationRiskParameters {
  RiskLevel gt_73_dbm = 1;            // A > 73 dBm, lowest risk
  RiskLevel gt_63_le_73_dbm = 2;      // 63 < A <= 73 dBm
  RiskLevel gt_51_le_63_dbm = 3;      // 51 < A <= 63 dBm
  RiskLevel gt_33_le_51_dbm = 4;      // 33 < A <= 51 dBm
  RiskLevel gt_27_le_33_dbm = 5;      // 27 < A <= 33 dBm
  RiskLevel gt_15_le_27_dbm = 6;      // 15 < A <= 27 dBm
  RiskLevel gt_10_le_15_dbm = 7;      // 10 < A <= 15 dBm
  RiskLevel lt_10_dbm = 8;            // A <= 10 dBm, highest risk
}
```

There is no value range that includes an attenuation of **exactly** 10dBm.

## Expected behaviour
According to the [Google/Apple spec](https://developer.apple.com/documentation/exposurenotification/enexposureconfiguration), the last value range should be "less than or equal to 10dBm" (`le_10_dbm`):

![image](https://user-images.githubusercontent.com/8984460/86034663-57f91580-ba3b-11ea-9444-e5020e6dcc46.png)

## Possible Fix
- Correct the last attenuation value range according to spec
- Update dependent code/configuration respectively ([here](https://github.com/corona-warn-app/cwa-server/blob/b8afdce9a3bc8fd927fe4ec9be2e910ee8f1635d/services/distribution/src/test/resources/objectstore/publisher/examplefile), [here](https://github.com/corona-warn-app/cwa-server/tree/b8afdce9a3bc8fd927fe4ec9be2e910ee8f1635d/services/distribution/src/test/resources/parameters), [here](https://github.com/corona-warn-app/cwa-server/tree/d6c0ad58f6473bd55af9a0cbe2c0cc8db3914af6/services/distribution/src/main/resources/master-config) and possibly more).

## Additional context
- This "bug" doesn't have any technical implications and should only be fixed for consistency
- Downwards-compatibility of the proto messages should not be infringed by the proposed change