From d59e67844462d8193eb73a2b35675f72d3ed2256 Mon Sep 17 00:00:00 2001
From: Matthias Urhahn <matthias.urhahn@sap.com>
Date: Tue, 1 Jun 2021 09:46:40 +0200
Subject: [PATCH] DOB hash calculation & wiring (EXPOSUREAPP-7488,
 EXPOSUREAPP-7509) (#3317)

* Extend corona test data structures with digital covid certificate related properties.
+Some additional wiring, plumbing and tests for future PRs.

* LINTs

* Adjust TestRegistrationRequest to supply dcc consent and DOB on test registration.

* Remove explicit assignment, defaults are sufficient.

* A few additional unit tests to check defaults.

* DateOfBirthKey calculation, draft 1

* Fix date parser pattern.

* wip

* Adjust padding calculation to take the new dobHash into account.
Some refactoring to make it less complicated to adjust for future changes.

* klint, ofc.

Co-authored-by: Mohamed Metwalli <mohamed.metwalli@sap.com>
---
 .../coronatest/server/RegistrationData.kt     |   6 +
 .../coronatest/server/RegistrationRequest.kt  |   9 +
 .../coronatest/server/VerificationApiV1.kt    |  15 +-
 .../coronatest/server/VerificationKeyType.kt  |   8 +-
 .../coronatest/server/VerificationServer.kt   | 167 ++++++++++++++----
 .../coronatest/type/CoronaTestService.kt      |  33 +---
 .../coronatest/type/common/DateOfBirthKey.kt  |  25 +++
 .../coronatest/type/pcr/PCRProcessor.kt       |  28 ++-
 .../type/rapidantigen/RAProcessor.kt          |  17 +-
 .../coronawarnapp/playbook/DefaultPlaybook.kt |  29 +--
 .../de/rki/coronawarnapp/playbook/Playbook.kt |  12 +-
 .../server/VerificationApiV1Test.kt           |  50 +++++-
 .../server/VerificationServerTest.kt          | 145 ++++++++++-----
 .../type/common/DateOfBirthKeyTest.kt         |  34 ++++
 .../coronatest/type/pcr/PCRProcessorTest.kt   |  39 ++--
 .../rapidantigen/RapidAntigenProcessorTest.kt |  40 ++---
 .../http/playbook/DefaultPlaybookTest.kt      |  38 ++--
 ...erviceTest.kt => CoronaTestServiceTest.kt} |  64 ++++---
 18 files changed, 520 insertions(+), 239 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt
 rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/{SubmissionServiceTest.kt => CoronaTestServiceTest.kt} (64%)

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt
new file mode 100644
index 000000000..b004502d8
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationData.kt
@@ -0,0 +1,6 @@
+package de.rki.coronawarnapp.coronatest.server
+
+data class RegistrationData(
+    val registrationToken: String,
+    val testResultResponse: CoronaTestResultResponse
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt
new file mode 100644
index 000000000..08de195d1
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/RegistrationRequest.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.coronatest.server
+
+import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey
+
+data class RegistrationRequest(
+    val key: String,
+    val type: VerificationKeyType,
+    val dateOfBirthKey: DateOfBirthKey? = null,
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt
index d40f59b3b..4f4662767 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1.kt
@@ -8,9 +8,10 @@ import retrofit2.http.POST
 interface VerificationApiV1 {
 
     data class RegistrationTokenRequest(
-        @SerializedName("keyType") val keyType: String? = null,
-        @SerializedName("key") val key: String? = null,
-        @SerializedName("requestPadding") val requestPadding: String? = null
+        @SerializedName("keyType") val keyType: VerificationKeyType,
+        @SerializedName("key") val key: String,
+        @SerializedName("keyDob") val dateOfBirthKey: String? = null,
+        @SerializedName("requestPadding") val requestPadding: String? = null,
     )
 
     data class RegistrationTokenResponse(
@@ -25,8 +26,8 @@ interface VerificationApiV1 {
     ): RegistrationTokenResponse
 
     data class RegistrationRequest(
-        @SerializedName("registrationToken") val registrationToken: String? = null,
-        @SerializedName("requestPadding") val requestPadding: String? = null
+        @SerializedName("registrationToken") val registrationToken: String,
+        @SerializedName("requestPadding") val requestPadding: String
     )
 
     data class TestResultResponse(
@@ -42,8 +43,8 @@ interface VerificationApiV1 {
     ): TestResultResponse
 
     data class TanRequestBody(
-        @SerializedName("registrationToken") val registrationToken: String? = null,
-        @SerializedName("requestPadding") val requestPadding: String? = null
+        @SerializedName("registrationToken") val registrationToken: String,
+        @SerializedName("requestPadding") val requestPadding: String
     )
 
     data class TanResponse(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt
index c64cfe628..0f982bd2b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationKeyType.kt
@@ -1,5 +1,11 @@
 package de.rki.coronawarnapp.coronatest.server
 
+import com.google.gson.annotations.SerializedName
+
 enum class VerificationKeyType {
-    GUID, TELETAN;
+    @SerializedName("GUID")
+    GUID,
+
+    @SerializedName("TELETAN")
+    TELETAN;
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt
index 30d580cf9..ab80df04a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/server/VerificationServer.kt
@@ -20,32 +20,59 @@ class VerificationServer @Inject constructor(
         get() = verificationAPI.get()
 
     suspend fun retrieveRegistrationToken(
-        key: String,
-        keyType: VerificationKeyType
+        request: RegistrationRequest
     ): RegistrationToken = withContext(Dispatchers.IO) {
-        Timber.tag(TAG).v("retrieveRegistrationToken(key=%s, keyType=%s)", key, keyType)
-        val keyStr = if (keyType == VerificationKeyType.GUID) {
-            HashHelper.hash256(key)
-        } else {
-            key
-        }
+        Timber.tag(TAG).v("retrieveRegistrationToken(request=%s)", request)
+
+        val requiredHeaderPadding = run {
+            var size = HEADER_SIZE_OUR_DATA
+            size -= HEADER_SIZE_OVERHEAD
 
-        val paddingLength = when (keyType) {
-            VerificationKeyType.GUID -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID
-            VerificationKeyType.TELETAN -> PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN
+            // `POST /version/v1/registrationToken`
+            size -= 34
+
+            size
+        }
+        val requiredBodyPadding = run {
+            var size = BODY_SIZE_EXPECTED
+            size -= BODY_SIZE_OVERHEAD
+
+            size -= when (request.type) {
+                VerificationKeyType.GUID -> 17 // `"keyType":"GUID",`
+                VerificationKeyType.TELETAN -> 20 // `"keyType":"TELETAN",`
+            }
+
+            size -= when (request.type) {
+                VerificationKeyType.GUID -> {
+                    73 // `"key":"75552e6e1dae7a520bad64e92b7569447d0f5ca3c539335e0418a7695606147e",`
+                }
+                VerificationKeyType.TELETAN -> {
+                    19 // `"key":"ERYCJMM4DC",`
+                }
+            }
+
+            size -= when (request.dateOfBirthKey) {
+                null -> 0
+                else -> 76 // `"keyDob":"x9acafb78b330522e32b4bf4c90a3ebb7a4d20d8af8cc32018c550ea86a38cc1",`
+            }
+            size
         }
 
         val response = api.getRegistrationToken(
             fake = "0",
-            headerPadding = requestPadding(PADDING_LENGTH_HEADER_REGISTRATION_TOKEN),
+            headerPadding = requestPadding(requiredHeaderPadding),
             requestBody = VerificationApiV1.RegistrationTokenRequest(
-                keyType = keyType.name,
-                key = keyStr,
-                requestPadding = requestPadding(paddingLength)
+                keyType = request.type,
+                key = when (request.type) {
+                    VerificationKeyType.GUID -> HashHelper.hash256(request.key)
+                    else -> request.key
+                },
+                dateOfBirthKey = request.dateOfBirthKey?.key,
+                requestPadding = requestPadding(requiredBodyPadding),
             )
         )
 
-        Timber.tag(TAG).d("retrieveRegistrationToken(key=%s, keyType=%s) -> %s", key, keyType, response)
+        Timber.tag(TAG).d("retrieveRegistrationToken(request=%s) -> %s", request, response)
         response.registrationToken
     }
 
@@ -53,12 +80,32 @@ class VerificationServer @Inject constructor(
         token: RegistrationToken
     ): CoronaTestResultResponse = withContext(Dispatchers.IO) {
         Timber.tag(TAG).v("retrieveTestResults(token=%s)", token)
+
+        val requiredHeaderPadding = run {
+            var size = HEADER_SIZE_OUR_DATA
+            size -= HEADER_SIZE_OVERHEAD
+
+            // `POST /version/v1/testresult`
+            size -= 27
+
+            size
+        }
+        val requiredBodyPadding = run {
+            var size = BODY_SIZE_EXPECTED
+            size -= BODY_SIZE_OVERHEAD
+
+            // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
+            size -= 59
+
+            size
+        }
+
         val response = api.getTestResult(
             fake = "0",
-            headerPadding = requestPadding(PADDING_LENGTH_HEADER_TEST_RESULT),
+            headerPadding = requestPadding(requiredHeaderPadding),
             request = VerificationApiV1.RegistrationRequest(
                 token,
-                requestPadding(PADDING_LENGTH_BODY_TEST_RESULT)
+                requestPadding(requiredBodyPadding)
             )
         )
 
@@ -71,12 +118,31 @@ class VerificationServer @Inject constructor(
         registrationToken: RegistrationToken
     ): String = withContext(Dispatchers.IO) {
         Timber.tag(TAG).v("retrieveTan(registrationToken=%s)", registrationToken)
+        val requiredHeaderPadding = run {
+            var size = HEADER_SIZE_OUR_DATA
+            size -= HEADER_SIZE_OVERHEAD
+
+            // `POST /version/v1/tan`
+            size -= 20
+
+            size
+        }
+        val requiredBodyPadding = run {
+            var size = BODY_SIZE_EXPECTED
+            size -= BODY_SIZE_OVERHEAD
+
+            // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
+            size -= 59
+
+            size
+        }
+
         val response = api.getTAN(
             fake = "0",
-            headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN),
+            headerPadding = requestPadding(requiredHeaderPadding),
             requestBody = VerificationApiV1.TanRequestBody(
                 registrationToken,
-                requestPadding(PADDING_LENGTH_BODY_TAN)
+                requestPadding(requiredBodyPadding)
             )
         )
 
@@ -86,12 +152,31 @@ class VerificationServer @Inject constructor(
 
     suspend fun retrieveTanFake() = withContext(Dispatchers.IO) {
         Timber.tag(TAG).v("retrieveTanFake()")
+        val requiredHeaderPadding = run {
+            var size = HEADER_SIZE_OUR_DATA
+            size -= HEADER_SIZE_OVERHEAD
+
+            // `POST /version/v1/tan`
+            size -= 20
+
+            size
+        }
+        val requiredBodyPadding = run {
+            var size = BODY_SIZE_EXPECTED
+            size -= BODY_SIZE_OVERHEAD
+
+            // `"registrationToken":"63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f",`
+            size -= 59
+
+            size
+        }
+
         val response = api.getTAN(
             fake = "1",
-            headerPadding = requestPadding(PADDING_LENGTH_HEADER_TAN),
+            headerPadding = requestPadding(requiredHeaderPadding),
             requestBody = VerificationApiV1.TanRequestBody(
                 registrationToken = DUMMY_REGISTRATION_TOKEN,
-                requestPadding = requestPadding(PADDING_LENGTH_BODY_TAN_FAKE)
+                requestPadding = requestPadding(requiredBodyPadding)
             )
         )
         Timber.tag(TAG).v("retrieveTanFake() -> %s", response)
@@ -99,28 +184,40 @@ class VerificationServer @Inject constructor(
     }
 
     companion object {
-        // padding registration token
-        private const val VERIFICATION_BODY_FILL = 139
+        /**
+         * The specific sizes are not important, but all requests should be padded up to the same size.
+         * Pick a total size that is guaranteed to be above or equal to the maximum size a request can be.
+         */
+        // `"requestPadding":""`
+        private const val BODY_SIZE_PADDING_OVERHEAD = 19 //
+
+        // `{}` json brackets
+        private const val BODY_SIZE_OVERHEAD = BODY_SIZE_PADDING_OVERHEAD + 2
+        private const val BODY_SIZE_EXPECTED = 250
+
+        /**
+         * The header itself is larger.
+         * We care about the header fields we set that are request specific.
+         * We don't need to pad for device specific fields set by OK http.
+         */
+        // `POST /version/v1/registrationToken` -> 34 (longest method + url atm) use 64 to have a buffer
+        private const val HEADER_SIZE_LONGEST_METHOD = 34
 
-        const val PADDING_LENGTH_HEADER_REGISTRATION_TOKEN = 0
-        const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_TELETAN = 51 + VERIFICATION_BODY_FILL
-        const val PADDING_LENGTH_BODY_REGISTRATION_TOKEN_GUID = 0 + VERIFICATION_BODY_FILL
+        // `cwa-fake 0\n` -> 12
+        private const val HEADER_SIZE_VAL_FAKE = 12
 
-        // padding test result
-        const val PADDING_LENGTH_HEADER_TEST_RESULT = 7
-        const val PADDING_LENGTH_BODY_TEST_RESULT = 31 + VERIFICATION_BODY_FILL
+        // `cwa-header-padding\n` -> 22
+        private const val HEADER_SIZE_VAL_PADDING = 22
+        private const val HEADER_SIZE_OVERHEAD = HEADER_SIZE_VAL_FAKE + HEADER_SIZE_VAL_PADDING
+        private const val HEADER_SIZE_OUR_DATA = HEADER_SIZE_LONGEST_METHOD + HEADER_SIZE_OVERHEAD
 
-        // padding tan
-        const val PADDING_LENGTH_HEADER_TAN = 14
-        const val PADDING_LENGTH_BODY_TAN = 31 + VERIFICATION_BODY_FILL
-        const val PADDING_LENGTH_BODY_TAN_FAKE = 31 + VERIFICATION_BODY_FILL
         const val DUMMY_REGISTRATION_TOKEN = "11111111-2222-4444-8888-161616161616"
 
         /**
          * Test is available for this long on the server.
          * After this period the server will delete it and return PENDING if the regtoken is polled again.
          */
-        val TEST_AVAILABLBILITY = Duration.standardDays(60)
+        val TEST_AVAILABLBILITY: Duration = Duration.standardDays(60)
 
         private const val TAG = "VerificationServer"
     }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt
index dcc41c608..009fa8deb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/CoronaTestService.kt
@@ -1,7 +1,8 @@
 package de.rki.coronawarnapp.coronatest.type
 
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
-import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.deniability.NoiseScheduler
 import de.rki.coronawarnapp.playbook.Playbook
 import de.rki.coronawarnapp.worker.BackgroundConstants
@@ -13,34 +14,17 @@ class CoronaTestService @Inject constructor(
     private val noiseScheduler: NoiseScheduler,
 ) {
 
-    suspend fun asyncRequestTestResult(registrationToken: String): CoronaTestResultResponse {
+    suspend fun checkTestResult(registrationToken: String): CoronaTestResultResponse {
         return playbook.testResult(registrationToken)
     }
 
-    suspend fun asyncRegisterDeviceViaGUID(guid: String): RegistrationData {
-        val (registrationToken, testResult) =
-            playbook.initialRegistration(
-                guid,
-                VerificationKeyType.GUID
-            )
+    suspend fun registerTest(tokenRequest: RegistrationRequest): RegistrationData {
+        val response = playbook.initialRegistration(tokenRequest)
 
         Timber.d("Scheduling background noise.")
         scheduleDummyPattern()
 
-        return RegistrationData(registrationToken, testResult)
-    }
-
-    suspend fun asyncRegisterDeviceViaTAN(tan: String): RegistrationData {
-        val (registrationToken, testResult) =
-            playbook.initialRegistration(
-                tan,
-                VerificationKeyType.TELETAN
-            )
-
-        Timber.d("Scheduling background noise.")
-        scheduleDummyPattern()
-
-        return RegistrationData(registrationToken, testResult)
+        return response
     }
 
     private fun scheduleDummyPattern() {
@@ -48,9 +32,4 @@ class CoronaTestService @Inject constructor(
             noiseScheduler.setPeriodicNoise(enabled = true)
         }
     }
-
-    data class RegistrationData(
-        val registrationToken: String,
-        val testResultResponse: CoronaTestResultResponse
-    )
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt
new file mode 100644
index 000000000..efc8a828e
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKey.kt
@@ -0,0 +1,25 @@
+package de.rki.coronawarnapp.coronatest.type.common
+
+import de.rki.coronawarnapp.util.HashExtensions.toSHA256
+import org.joda.time.LocalDate
+import org.joda.time.format.DateTimeFormat
+
+data class DateOfBirthKey constructor(
+    private val testGuid: String,
+    private val dateOfBirth: LocalDate,
+) {
+
+    init {
+        require(testGuid.isNotEmpty()) { "GUID can't be empty." }
+    }
+
+    val key by lazy {
+        val dobFormatted = dateOfBirth.toString(DOB_FORMATTER)
+        val keyHash = "${testGuid}$dobFormatted".toSHA256()
+        "x${keyHash.substring(1)}"
+    }
+
+    companion object {
+        private val DOB_FORMATTER = DateTimeFormat.forPattern("ddMMYYYY")
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
index 8ee04a424..75f83e9dd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessor.kt
@@ -14,11 +14,15 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_NEGATIVE
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
+import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
 import de.rki.coronawarnapp.coronatest.server.VerificationServer
 import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
+import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey
 import de.rki.coronawarnapp.coronatest.type.isOlderThan21Days
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.testresult.AnalyticsTestResultCollector
@@ -51,7 +55,17 @@ class PCRProcessor @Inject constructor(
     private suspend fun createQR(request: CoronaTestQRCode.PCR): PCRCoronaTest {
         Timber.tag(TAG).d("createQR(data=%s)", request)
 
-        val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.qrCodeGUID).also {
+        val dateOfBirthKey = if (request.isDccConsentGiven && request.dateOfBirth != null) {
+            DateOfBirthKey(request.qrCodeGUID, request.dateOfBirth)
+        } else null
+
+        val serverRequest = RegistrationRequest(
+            key = request.qrCodeGUID,
+            dateOfBirthKey = dateOfBirthKey,
+            type = VerificationKeyType.GUID,
+        )
+
+        val registrationData = submissionService.registerTest(serverRequest).also {
             Timber.tag(TAG).d("Request %s gave us %s", request, it)
         }
 
@@ -64,7 +78,13 @@ class PCRProcessor @Inject constructor(
     private suspend fun createTAN(request: CoronaTestTAN.PCR): CoronaTest {
         Timber.tag(TAG).d("createTAN(data=%s)", request)
 
-        val registrationData = submissionService.asyncRegisterDeviceViaTAN(request.tan)
+        val serverRequest = RegistrationRequest(
+            key = request.tan,
+            dateOfBirthKey = null,
+            type = VerificationKeyType.TELETAN,
+        )
+
+        val registrationData = submissionService.registerTest(serverRequest)
 
         analyticsKeySubmissionCollector.reportRegisteredWithTeleTAN()
 
@@ -75,7 +95,7 @@ class PCRProcessor @Inject constructor(
 
     private suspend fun createCoronaTest(
         request: TestRegistrationRequest,
-        response: CoronaTestService.RegistrationData
+        response: RegistrationData
     ): PCRCoronaTest {
 
         analyticsKeySubmissionCollector.reset(type)
@@ -126,7 +146,7 @@ class PCRProcessor @Inject constructor(
             }
 
             val newTestResult = try {
-                submissionService.asyncRequestTestResult(test.registrationToken).coronaTestResult.let {
+                submissionService.checkTestResult(test.registrationToken).coronaTestResult.let {
                     Timber.tag(TAG).d("Raw test result was %s", it)
                     analyticsTestResultCollector.updatePendingTestResultReceivedTime(it, type)
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt
index c28253d5f..3dcd82592 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RAProcessor.kt
@@ -15,10 +15,13 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_PENDING
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
+import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
 import de.rki.coronawarnapp.coronatest.server.VerificationServer
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import de.rki.coronawarnapp.coronatest.type.CoronaTestProcessor
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
+import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey
 import de.rki.coronawarnapp.coronatest.type.isOlderThan21Days
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
 import de.rki.coronawarnapp.datadonation.analytics.modules.testresult.AnalyticsTestResultCollector
@@ -53,7 +56,17 @@ class RAProcessor @Inject constructor(
         analyticsKeySubmissionCollector.reset(type)
         analyticsTestResultCollector.clear(type)
 
-        val registrationData = submissionService.asyncRegisterDeviceViaGUID(request.registrationIdentifier).also {
+        val dateOfBirthKey = if (request.isDccConsentGiven && request.dateOfBirth != null) {
+            DateOfBirthKey(request.registrationIdentifier, request.dateOfBirth)
+        } else null
+
+        val serverRequest = RegistrationRequest(
+            key = request.registrationIdentifier,
+            dateOfBirthKey = dateOfBirthKey,
+            type = VerificationKeyType.GUID
+        )
+
+        val registrationData = submissionService.registerTest(serverRequest).also {
             Timber.tag(TAG).d("Request %s gave us %s", request, it)
         }
 
@@ -117,7 +130,7 @@ class RAProcessor @Inject constructor(
             }
 
             val newTestResult = try {
-                submissionService.asyncRequestTestResult(test.registrationToken).let {
+                submissionService.checkTestResult(test.registrationToken).let {
                     Timber.tag(TAG).v("Raw test result was %s", it)
                     it.copy(
                         coronaTestResult = it.coronaTestResult.toValidatedResult()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt
index 3e2070c78..959410f6a 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/DefaultPlaybook.kt
@@ -1,7 +1,8 @@
 package de.rki.coronawarnapp.playbook
 
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
-import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.coronatest.server.VerificationServer
 import de.rki.coronawarnapp.exception.TanPairingException
 import de.rki.coronawarnapp.exception.http.BadRequestException
@@ -26,23 +27,18 @@ class DefaultPlaybook @Inject constructor(
     private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
 
     override suspend fun initialRegistration(
-        key: String,
-        keyType: VerificationKeyType
-    ): Pair<String, CoronaTestResultResponse> {
+        request: RegistrationRequest
+    ): RegistrationData {
         Timber.i("[$uid] New Initial Registration Playbook")
 
         // real registration
-        val (registrationToken, registrationException) =
-            executeCapturingExceptions {
-                verificationServer.retrieveRegistrationToken(
-                    key,
-                    keyType
-                )
-            }
+        val (registrationToken, registrationException) = executeCapturingExceptions {
+            verificationServer.retrieveRegistrationToken(request)
+        }
 
         // if the registration succeeded continue with the real test result retrieval
         // if it failed, execute a dummy request to satisfy the required playbook pattern
-        val (testResult, testResultException) = if (registrationToken != null) {
+        val (testResultResponse, testResultException) = if (registrationToken != null) {
             executeCapturingExceptions { verificationServer.pollTestResult(registrationToken) }
         } else {
             ignoreExceptions { verificationServer.retrieveTanFake() }
@@ -55,8 +51,13 @@ class DefaultPlaybook @Inject constructor(
         coroutineScope.launch { followUpPlaybooks() }
 
         // if registration and test result retrieval succeeded, return the result
-        if (registrationToken != null && testResult != null)
-            return registrationToken to testResult
+        if (registrationToken != null && testResultResponse != null) {
+
+            return RegistrationData(
+                registrationToken = registrationToken,
+                testResultResponse = testResultResponse,
+            )
+        }
 
         // else propagate the exception of either the first or the second step
         propagateException(registrationException, testResultException)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt
index b631e93a1..5e72abca8 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/playbook/Playbook.kt
@@ -1,8 +1,8 @@
 package de.rki.coronawarnapp.playbook
 
-import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
-import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeyExportOuterClass
 import de.rki.coronawarnapp.server.protocols.internal.SubmissionPayloadOuterClass.SubmissionPayload
 import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass
@@ -21,13 +21,7 @@ import de.rki.coronawarnapp.server.protocols.internal.pt.CheckInOuterClass
  */
 interface Playbook {
 
-    /**
-     * @return pair of Registration token [String] & [CoronaTestResult]
-     */
-    suspend fun initialRegistration(
-        key: String,
-        keyType: VerificationKeyType
-    ): Pair<String, CoronaTestResultResponse>
+    suspend fun initialRegistration(tokenRequest: RegistrationRequest): RegistrationData
 
     suspend fun testResult(registrationToken: String): CoronaTestResultResponse
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt
index efa159300..9076ba3b7 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationApiV1Test.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.coronatest.server
 
 import android.content.Context
+import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey
 import de.rki.coronawarnapp.http.HttpModule
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
@@ -9,6 +10,7 @@ import io.mockk.impl.annotations.MockK
 import kotlinx.coroutines.runBlocking
 import okhttp3.ConnectionSpec
 import okhttp3.mockwebserver.MockWebServer
+import org.joda.time.LocalDate
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
@@ -65,7 +67,7 @@ class VerificationApiV1Test : BaseIOTest() {
     }
 
     @Test
-    fun `test getRegistrationToken`(): Unit = runBlocking {
+    fun `test getRegistrationToken - GUID`(): Unit = runBlocking {
         val api = createAPI()
 
         """
@@ -75,8 +77,9 @@ class VerificationApiV1Test : BaseIOTest() {
         """.toJsonResponse().apply { webServer.enqueue(this) }
 
         val requestBody = VerificationApiV1.RegistrationTokenRequest(
-            keyType = "testKeyType",
+            keyType = VerificationKeyType.GUID,
             key = "testKey",
+            dateOfBirthKey = DateOfBirthKey("testKeyGuid", LocalDate.parse("2020-09-11")).key,
             requestPadding = "testRequestPadding"
         )
 
@@ -94,7 +97,48 @@ class VerificationApiV1Test : BaseIOTest() {
             path shouldBe "/version/v1/registrationToken"
             body.readUtf8() shouldBe """
                 {
-                    "keyType": "testKeyType",
+                    "keyType": "GUID",
+                    "key": "testKey",
+                    "keyDob": "x9acafb78b330522e32b4bf4c90a3ebb7a4d20d8af8cc32018c550ea86a38cc1",
+                    "requestPadding": "testRequestPadding"
+                }
+            """.toComparableJson()
+        }
+
+        httpCacheDir.exists() shouldBe true
+    }
+
+    @Test
+    fun `test getRegistrationToken - TAN`(): Unit = runBlocking {
+        val api = createAPI()
+
+        """
+            {
+                "registrationToken": "testRegistrationToken"
+            }
+        """.toJsonResponse().apply { webServer.enqueue(this) }
+
+        val requestBody = VerificationApiV1.RegistrationTokenRequest(
+            keyType = VerificationKeyType.TELETAN,
+            key = "testKey",
+            requestPadding = "testRequestPadding",
+        )
+
+        api.getRegistrationToken(
+            fake = "0",
+            headerPadding = "testPadding",
+            requestBody
+        ) shouldBe VerificationApiV1.RegistrationTokenResponse(
+            registrationToken = "testRegistrationToken"
+        )
+
+        webServer.takeRequest(5, TimeUnit.SECONDS)!!.apply {
+            headers["cwa-fake"] shouldBe "0"
+            headers["cwa-header-padding"] shouldBe "testPadding"
+            path shouldBe "/version/v1/registrationToken"
+            body.readUtf8() shouldBe """
+                {
+                    "keyType": "TELETAN",
                     "key": "testKey",
                     "requestPadding": "testRequestPadding"
                 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt
index 6ac91f60f..07f8cbc45 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/server/VerificationServerTest.kt
@@ -1,8 +1,10 @@
 package de.rki.coronawarnapp.coronatest.server
 
 import android.content.Context
+import de.rki.coronawarnapp.coronatest.type.common.DateOfBirthKey
 import de.rki.coronawarnapp.http.HttpModule
 import de.rki.coronawarnapp.util.headerSizeIgnoringContentLength
+import de.rki.coronawarnapp.util.requestHeaderWithoutContentLength
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.coEvery
@@ -13,11 +15,14 @@ import kotlinx.coroutines.runBlocking
 import okhttp3.ConnectionSpec
 import okhttp3.mockwebserver.MockResponse
 import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
 import org.joda.time.Duration
+import org.joda.time.LocalDate
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseIOTest
+import timber.log.Timber
 import java.io.File
 
 class VerificationServerTest : BaseIOTest() {
@@ -30,7 +35,17 @@ class VerificationServerTest : BaseIOTest() {
 
     private val testDir = File(IO_TEST_BASEDIR, this::class.java.simpleName)
     private val cacheDir = File(testDir, "cache")
-    private val httpCacheDir = File(cacheDir, "http_verification")
+
+    private val requestTan = RegistrationRequest(
+        "testKeyTan",
+        VerificationKeyType.TELETAN
+    )
+
+    private val requestGuid = RegistrationRequest(
+        key = "testKeyGuid",
+        type = VerificationKeyType.GUID,
+        dateOfBirthKey = DateOfBirthKey("testKeyGuid", LocalDate.parse("2020-09-11"))
+    )
 
     @BeforeEach
     fun setup() {
@@ -54,16 +69,58 @@ class VerificationServerTest : BaseIOTest() {
         customApi: VerificationApiV1 = verificationApi
     ) = VerificationServer(verificationAPI = { customApi })
 
+    private fun createRealApi(): VerificationApiV1 {
+        val httpModule = HttpModule()
+        val defaultHttpClient = httpModule.defaultHttpClient()
+        val gsonConverterFactory = httpModule.provideGSONConverter()
+
+        return VerificationModule().let {
+            val downloadHttpClient = it.cdnHttpClient(
+                defaultHttpClient,
+                listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)
+            )
+            it.provideVerificationApi(
+                context = context,
+                client = downloadHttpClient,
+                url = serverAddress,
+                gsonConverterFactory = gsonConverterFactory
+            )
+        }
+    }
+
     @Test
-    fun `get registration token via GUID`(): Unit = runBlocking {
+    fun `get registration token via GUID - with dobHash`(): Unit = runBlocking {
         val server = createServer()
         coEvery { verificationApi.getRegistrationToken(any(), any(), any()) } answers {
             arg<String>(0) shouldBe "0"
             arg<String>(1) shouldBe ""
             arg<VerificationApiV1.RegistrationTokenRequest>(2).apply {
-                keyType shouldBe VerificationKeyType.GUID.name
-                key shouldBe "15291f67d99ea7bc578c3544dadfbb991e66fa69cb36ff70fe30e798e111ff5f"
+                keyType shouldBe VerificationKeyType.GUID
+                key shouldBe "7620a19f93374e8d5acff090d3c10d0242a32fe140c50bbd40c95edf3c0af5b7"
+                requestPadding!!.length shouldBe 63
+                dateOfBirthKey shouldBe requestGuid.dateOfBirthKey!!.key
+            }
+            VerificationApiV1.RegistrationTokenResponse(
+                registrationToken = "testRegistrationToken"
+            )
+        }
+
+        server.retrieveRegistrationToken(requestGuid) shouldBe "testRegistrationToken"
+
+        coVerify { verificationApi.getRegistrationToken(any(), any(), any()) }
+    }
+
+    @Test
+    fun `get registration token via GUID - without dobHash`(): Unit = runBlocking {
+        val server = createServer()
+        coEvery { verificationApi.getRegistrationToken(any(), any(), any()) } answers {
+            arg<String>(0) shouldBe "0"
+            arg<String>(1) shouldBe ""
+            arg<VerificationApiV1.RegistrationTokenRequest>(2).apply {
+                keyType shouldBe VerificationKeyType.GUID
+                key shouldBe "7620a19f93374e8d5acff090d3c10d0242a32fe140c50bbd40c95edf3c0af5b7"
                 requestPadding!!.length shouldBe 139
+                dateOfBirthKey shouldBe null
             }
             VerificationApiV1.RegistrationTokenResponse(
                 registrationToken = "testRegistrationToken"
@@ -71,8 +128,7 @@ class VerificationServerTest : BaseIOTest() {
         }
 
         server.retrieveRegistrationToken(
-            "testKey",
-            VerificationKeyType.GUID
+            requestGuid.copy(dateOfBirthKey = null)
         ) shouldBe "testRegistrationToken"
 
         coVerify { verificationApi.getRegistrationToken(any(), any(), any()) }
@@ -85,19 +141,17 @@ class VerificationServerTest : BaseIOTest() {
             arg<String>(0) shouldBe "0"
             arg<String>(1) shouldBe ""
             arg<VerificationApiV1.RegistrationTokenRequest>(2).apply {
-                keyType shouldBe VerificationKeyType.TELETAN.name
-                key shouldBe "testKey"
+                keyType shouldBe VerificationKeyType.TELETAN
+                key shouldBe "testKeyTan"
                 requestPadding!!.length shouldBe 190
+                dateOfBirthKey shouldBe null
             }
             VerificationApiV1.RegistrationTokenResponse(
                 registrationToken = "testRegistrationToken"
             )
         }
 
-        server.retrieveRegistrationToken(
-            "testKey",
-            VerificationKeyType.TELETAN
-        ) shouldBe "testRegistrationToken"
+        server.retrieveRegistrationToken(requestTan) shouldBe "testRegistrationToken"
 
         coVerify { verificationApi.getRegistrationToken(any(), any(), any()) }
     }
@@ -110,7 +164,7 @@ class VerificationServerTest : BaseIOTest() {
             arg<String>(1).length shouldBe 7 // Header-padding
             arg<VerificationApiV1.RegistrationRequest>(2).apply {
                 registrationToken shouldBe "testRegistrationToken"
-                requestPadding!!.length shouldBe 170
+                requestPadding.length shouldBe 170
             }
             VerificationApiV1.TestResultResponse(testResult = 2, sampleCollectedAt = null)
         }
@@ -131,7 +185,7 @@ class VerificationServerTest : BaseIOTest() {
             arg<String>(1).length shouldBe 14 // Header-padding
             arg<VerificationApiV1.TanRequestBody>(2).apply {
                 registrationToken shouldBe "testRegistrationToken"
-                requestPadding!!.length shouldBe 170
+                requestPadding.length shouldBe 170
             }
             VerificationApiV1.TanResponse(tan = "testTan")
         }
@@ -149,7 +203,7 @@ class VerificationServerTest : BaseIOTest() {
             arg<String>(1).length shouldBe 14 // Header-padding
             arg<VerificationApiV1.TanRequestBody>(2).apply {
                 registrationToken shouldBe "11111111-2222-4444-8888-161616161616"
-                requestPadding!!.length shouldBe 170
+                requestPadding.length shouldBe 170
             }
             VerificationApiV1.TanResponse(tan = "testTan")
         }
@@ -159,59 +213,52 @@ class VerificationServerTest : BaseIOTest() {
         coVerify { verificationApi.getTAN(any(), any(), any()) }
     }
 
-    private fun createRealApi(): VerificationApiV1 {
-        val httpModule = HttpModule()
-        val defaultHttpClient = httpModule.defaultHttpClient()
-        val gsonConverterFactory = httpModule.provideGSONConverter()
-
-        return VerificationModule().let {
-            val downloadHttpClient = it.cdnHttpClient(
-                defaultHttpClient,
-                listOf(ConnectionSpec.CLEARTEXT, ConnectionSpec.MODERN_TLS)
-            )
-            it.provideVerificationApi(
-                context = context,
-                client = downloadHttpClient,
-                url = serverAddress,
-                gsonConverterFactory = gsonConverterFactory
-            )
-        }
-    }
-
     @Test
     fun `all requests have the same footprint for pleasible deniability`(): Unit = runBlocking {
-        val guidExample = "3BF1D4-1C6003DD-733D-41F1-9F30-F85FA7406BF7"
-        val teletanExample = "9A3B578UMG"
         val registrationTokenExample = "63b4d3ff-e0de-4bd4-90c1-17c2bb683a2f"
 
+        val requests = mutableListOf<RecordedRequest>()
+
         val api = createServer(createRealApi())
+
+        // Default happy path
+        webServer.enqueue(MockResponse().setBody("{}"))
+        api.retrieveRegistrationToken(requestGuid)
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
+
+        // No dobHash
+        webServer.enqueue(MockResponse().setBody("{}"))
+        api.retrieveRegistrationToken(requestGuid)
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
+
+        // Second happy path try
         webServer.enqueue(MockResponse().setBody("{}"))
-        api.retrieveRegistrationToken(guidExample, VerificationKeyType.GUID)
+        api.retrieveRegistrationToken(requestGuid.copy(key = "3BF1D4-1C6003DD-733D-41F1-9F30-F85FA7406BF7"))
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
 
+        // Via tan
         webServer.enqueue(MockResponse().setBody("{}"))
-        api.retrieveRegistrationToken(teletanExample, VerificationKeyType.TELETAN)
+        api.retrieveRegistrationToken(requestTan.copy(key = "9A3B578UMG"))
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
 
+        // Polling for test result
         webServer.enqueue(MockResponse().setBody("{}"))
         api.pollTestResult(registrationTokenExample)
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
 
+        // Submission TAN
         webServer.enqueue(MockResponse().setBody("{}"))
         api.retrieveTan(registrationTokenExample)
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
 
+        // Playbook dummy request
         webServer.enqueue(MockResponse().setBody("{}"))
         api.retrieveTanFake()
-
-        val requests = listOf(
-            webServer.takeRequest(),
-            webServer.takeRequest(),
-            webServer.takeRequest(),
-            webServer.takeRequest(),
-            webServer.takeRequest()
-        )
-
-        // ensure all request have same size (header & body)
-        requests.forEach { it.bodySize shouldBe 250L }
+        webServer.takeRequest().also { requests.add(it) }.bodySize shouldBe 250L
 
         requests.zipWithNext().forEach { (a, b) ->
+            Timber.i("Header a: %s", a.requestHeaderWithoutContentLength().replace('\n', ' '))
+            Timber.i("Header b: %s", b.requestHeaderWithoutContentLength().replace('\n', ' '))
             a.headerSizeIgnoringContentLength() shouldBe b.headerSizeIgnoringContentLength()
         }
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt
new file mode 100644
index 000000000..4ef92cd80
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/common/DateOfBirthKeyTest.kt
@@ -0,0 +1,34 @@
+package de.rki.coronawarnapp.coronatest.type.common
+
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import org.joda.time.LocalDate
+import org.joda.time.format.DateTimeFormat
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class DateOfBirthKeyTest : BaseTest() {
+
+    @Test
+    fun `empty guids fail early`() {
+        shouldThrow<IllegalArgumentException> {
+            DateOfBirthKey("", LocalDate.parse("1990-10-24"))
+        }
+    }
+
+    @Test
+    fun `test case 1`() {
+        DateOfBirthKey(
+            testGuid = "E1277F-E1277F24-4AD2-40BC-AFF8-CBCCCD893E4B",
+            dateOfBirth = LocalDate.parse("2000-01-01", DateTimeFormat.forPattern("YYYY-MM-dd"))
+        ).key shouldBe "xfa760e171f000ef5a7f863ab180f6f6e8185c4890224550395281d839d85458"
+    }
+
+    @Test
+    fun `test case 2`() {
+        DateOfBirthKey(
+            testGuid = "F1EE0D-F1EE0D4D-4346-4B63-B9CF-1522D9200915",
+            dateOfBirth = LocalDate.parse("1995-06-07", DateTimeFormat.forPattern("YYYY-MM-dd"))
+        ).key shouldBe "x4a7788ef394bc7d52112014b127fe2bf142c51fe1bbb385214280e9db670193"
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
index f67ba4c3d..f7d32ec8e 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/pcr/PCRProcessorTest.kt
@@ -14,6 +14,8 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.coronatest.tan.CoronaTestTAN
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.PCR
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
@@ -58,24 +60,21 @@ class PCRProcessorTest : BaseTest() {
         every { timeStamper.nowUTC } returns nowUTC
 
         submissionService.apply {
-            coEvery { asyncRequestTestResult(any()) } returns CoronaTestResultResponse(
+            coEvery { checkTestResult(any()) } returns CoronaTestResultResponse(
                 coronaTestResult = PCR_OR_RAT_PENDING,
                 sampleCollectedAt = null,
             )
-            coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData(
-                registrationToken = "regtoken-qr",
-                testResultResponse = CoronaTestResultResponse(
-                    coronaTestResult = PCR_OR_RAT_PENDING,
-                    sampleCollectedAt = null,
-                ),
-            )
-            coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData(
-                registrationToken = "regtoken-tan",
-                testResultResponse = CoronaTestResultResponse(
-                    coronaTestResult = PCR_OR_RAT_PENDING,
-                    sampleCollectedAt = null,
-                ),
-            )
+            coEvery { registerTest(any()) } answers {
+                val request = arg<RegistrationRequest>(0)
+
+                RegistrationData(
+                    registrationToken = "regtoken-${request.type}",
+                    testResultResponse = CoronaTestResultResponse(
+                        coronaTestResult = PCR_OR_RAT_PENDING,
+                        sampleCollectedAt = null,
+                    ),
+                )
+            }
         }
 
         analyticsKeySubmissionCollector.apply {
@@ -118,14 +117,14 @@ class PCRProcessorTest : BaseTest() {
 
     @Test
     fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest {
-        var registrationData = CoronaTestService.RegistrationData(
+        var registrationData = RegistrationData(
             registrationToken = "regtoken",
             testResultResponse = CoronaTestResultResponse(
                 coronaTestResult = PCR_OR_RAT_PENDING,
                 sampleCollectedAt = null,
             )
         )
-        coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData }
+        coEvery { submissionService.registerTest(any()) } answers { registrationData }
 
         val instance = createInstance()
 
@@ -158,7 +157,7 @@ class PCRProcessorTest : BaseTest() {
     @Test
     fun `polling maps invalid results to INVALID state`() = runBlockingTest {
         var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING
-        coEvery { submissionService.asyncRequestTestResult(any()) } answers {
+        coEvery { submissionService.checkTestResult(any()) } answers {
             CoronaTestResultResponse(
                 coronaTestResult = pollResult,
                 sampleCollectedAt = null,
@@ -210,7 +209,7 @@ class PCRProcessorTest : BaseTest() {
 
     @Test
     fun `polling is skipped if test is older than 21 days and state was already REDEEMED`() = runBlockingTest {
-        coEvery { submissionService.asyncRequestTestResult(any()) } answers {
+        coEvery { submissionService.checkTestResult(any()) } answers {
             CoronaTestResultResponse(
                 coronaTestResult = PCR_POSITIVE,
                 sampleCollectedAt = null,
@@ -236,7 +235,7 @@ class PCRProcessorTest : BaseTest() {
     @Test
     fun `http 400 errors map to REDEEMED (EXPIRED) state after 21 days`() = runBlockingTest {
         val ourBadRequest = BadRequestException("Who?")
-        coEvery { submissionService.asyncRequestTestResult(any()) } throws ourBadRequest
+        coEvery { submissionService.checkTestResult(any()) } throws ourBadRequest
 
         val instance = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
index a3180a0da..5222c1be5 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/type/rapidantigen/RapidAntigenProcessorTest.kt
@@ -14,6 +14,8 @@ import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_POSITIVE
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.RAT_REDEEMED
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult.values
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.coronatest.type.CoronaTest.Type.RAPID_ANTIGEN
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
 import de.rki.coronawarnapp.datadonation.analytics.modules.keysubmission.AnalyticsKeySubmissionCollector
@@ -59,24 +61,22 @@ class RapidAntigenProcessorTest : BaseTest() {
         every { timeStamper.nowUTC } returns nowUTC
 
         submissionService.apply {
-            coEvery { asyncRequestTestResult(any()) } returns CoronaTestResultResponse(
+            coEvery { checkTestResult(any()) } returns CoronaTestResultResponse(
                 coronaTestResult = PCR_OR_RAT_PENDING,
                 sampleCollectedAt = null,
             )
-            coEvery { asyncRegisterDeviceViaGUID(any()) } returns CoronaTestService.RegistrationData(
-                registrationToken = "regtoken-qr",
-                testResultResponse = CoronaTestResultResponse(
-                    coronaTestResult = PCR_OR_RAT_PENDING,
-                    sampleCollectedAt = null,
-                )
-            )
-            coEvery { asyncRegisterDeviceViaTAN(any()) } returns CoronaTestService.RegistrationData(
-                registrationToken = "regtoken-tan",
-                testResultResponse = CoronaTestResultResponse(
-                    coronaTestResult = PCR_OR_RAT_PENDING,
-                    sampleCollectedAt = null,
+
+            coEvery { registerTest(any()) } answers {
+                val request = arg<RegistrationRequest>(0)
+
+                RegistrationData(
+                    registrationToken = "regtoken-${request.type}",
+                    testResultResponse = CoronaTestResultResponse(
+                        coronaTestResult = PCR_OR_RAT_PENDING,
+                        sampleCollectedAt = null,
+                    )
                 )
-            )
+            }
         }
 
         analyticsKeySubmissionCollector.apply {
@@ -115,7 +115,7 @@ class RapidAntigenProcessorTest : BaseTest() {
 
         (instance.pollServer(raTest) as RACoronaTest).sampleCollectedAt shouldBe null
 
-        coEvery { submissionService.asyncRequestTestResult(any()) } returns CoronaTestResultResponse(
+        coEvery { submissionService.checkTestResult(any()) } returns CoronaTestResultResponse(
             coronaTestResult = PCR_OR_RAT_PENDING,
             sampleCollectedAt = nowUTC,
         )
@@ -142,14 +142,14 @@ class RapidAntigenProcessorTest : BaseTest() {
 
     @Test
     fun `registering a new test maps invalid results to INVALID state`() = runBlockingTest {
-        var registrationData = CoronaTestService.RegistrationData(
+        var registrationData = RegistrationData(
             registrationToken = "regtoken",
             testResultResponse = CoronaTestResultResponse(
                 coronaTestResult = PCR_OR_RAT_PENDING,
                 sampleCollectedAt = null,
             ),
         )
-        coEvery { submissionService.asyncRegisterDeviceViaGUID(any()) } answers { registrationData }
+        coEvery { submissionService.registerTest(any()) } answers { registrationData }
 
         val instance = createInstance()
 
@@ -184,7 +184,7 @@ class RapidAntigenProcessorTest : BaseTest() {
     @Test
     fun `polling filters out invalid test result values`() = runBlockingTest {
         var pollResult: CoronaTestResult = PCR_OR_RAT_PENDING
-        coEvery { submissionService.asyncRequestTestResult(any()) } answers {
+        coEvery { submissionService.checkTestResult(any()) } answers {
             CoronaTestResultResponse(
                 coronaTestResult = pollResult,
                 sampleCollectedAt = null,
@@ -217,7 +217,7 @@ class RapidAntigenProcessorTest : BaseTest() {
 
     @Test
     fun `polling is skipped if test is older than 21 days and state was already REDEEMED`() = runBlockingTest {
-        coEvery { submissionService.asyncRequestTestResult(any()) } answers {
+        coEvery { submissionService.checkTestResult(any()) } answers {
             CoronaTestResultResponse(
                 coronaTestResult = RAT_POSITIVE,
                 sampleCollectedAt = null,
@@ -243,7 +243,7 @@ class RapidAntigenProcessorTest : BaseTest() {
     @Test
     fun `http 400 errors map to REDEEMED (EXPIRED) state after 21 days`() = runBlockingTest {
         val ourBadRequest = BadRequestException("Who?")
-        coEvery { submissionService.asyncRequestTestResult(any()) } throws ourBadRequest
+        coEvery { submissionService.checkTestResult(any()) } throws ourBadRequest
 
         val instance = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt
index 8259af24f..dd870ce11 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/http/playbook/DefaultPlaybookTest.kt
@@ -2,6 +2,7 @@ package de.rki.coronawarnapp.http.playbook
 
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
 import de.rki.coronawarnapp.coronatest.server.VerificationServer
 import de.rki.coronawarnapp.exception.TanPairingException
@@ -29,11 +30,20 @@ class DefaultPlaybookTest : BaseTest() {
     @MockK lateinit var submissionServer: SubmissionServer
     @MockK lateinit var verificationServer: VerificationServer
 
+    private val requestGuid = RegistrationRequest(
+        key = "guid",
+        type = VerificationKeyType.GUID
+    )
+    private val requestTan = RegistrationRequest(
+        key = "9A3B578UMG",
+        type = VerificationKeyType.TELETAN
+    )
+
     @BeforeEach
     fun setup() {
         MockKAnnotations.init(this)
 
-        coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns "token"
+        coEvery { verificationServer.retrieveRegistrationToken(any()) } returns "token"
         coEvery { verificationServer.pollTestResult(any()) } returns CoronaTestResultResponse(
             coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING,
             sampleCollectedAt = null
@@ -52,13 +62,13 @@ class DefaultPlaybookTest : BaseTest() {
 
     @Test
     fun `initial registration pattern matches`(): Unit = runBlocking {
-        coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns "response"
+        coEvery { verificationServer.retrieveRegistrationToken(any()) } returns "response"
 
-        createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN)
+        createPlaybook().initialRegistration(requestTan)
 
         coVerifySequence {
             // ensure request order is 2x verification and 1x submission
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
             verificationServer.pollTestResult(any())
             submissionServer.submitFakePayload()
         }
@@ -67,16 +77,16 @@ class DefaultPlaybookTest : BaseTest() {
     @Test
     fun ` registration pattern matches despite token failure`(): Unit = runBlocking {
         coEvery {
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
         } throws TestException()
 
         shouldThrow<TestException> {
-            createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN)
+            createPlaybook().initialRegistration(requestTan)
         }
 
         coVerifySequence {
             // ensure request order is 2x verification and 1x submission
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
             verificationServer.retrieveTanFake()
             submissionServer.submitFakePayload()
         }
@@ -200,7 +210,7 @@ class DefaultPlaybookTest : BaseTest() {
     @Test
     fun `failures during dummy requests should be ignored`(): Unit = runBlocking {
         val expectedToken = "token"
-        coEvery { verificationServer.retrieveRegistrationToken(any(), any()) } returns expectedToken
+        coEvery { verificationServer.retrieveRegistrationToken(any()) } returns expectedToken
         val expectedResult = CoronaTestResult.PCR_OR_RAT_PENDING
         coEvery { verificationServer.pollTestResult(expectedToken) } returns CoronaTestResultResponse(
             coronaTestResult = expectedResult,
@@ -209,7 +219,7 @@ class DefaultPlaybookTest : BaseTest() {
         coEvery { submissionServer.submitFakePayload() } throws TestException()
 
         val (registrationToken, testResult) = createPlaybook()
-            .initialRegistration("key", VerificationKeyType.GUID)
+            .initialRegistration(requestGuid)
 
         registrationToken shouldBe expectedToken
         testResult shouldBe CoronaTestResultResponse(
@@ -221,15 +231,15 @@ class DefaultPlaybookTest : BaseTest() {
     @Test
     fun `registration pattern matches despire token failure`(): Unit = runBlocking {
         coEvery {
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
         } throws TestException()
 
         shouldThrow<TestException> {
-            createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN)
+            createPlaybook().initialRegistration(requestTan)
         }
         coVerifySequence {
             // ensure request order is 2x verification and 1x submission
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
             verificationServer.retrieveTanFake()
             submissionServer.submitFakePayload()
         }
@@ -240,12 +250,12 @@ class DefaultPlaybookTest : BaseTest() {
         coEvery { verificationServer.pollTestResult(any()) } throws TestException()
 
         shouldThrow<TestException> {
-            createPlaybook().initialRegistration("9A3B578UMG", VerificationKeyType.TELETAN)
+            createPlaybook().initialRegistration(requestTan)
         }
 
         coVerifySequence {
             // ensure request order is 2x verification and 1x submission
-            verificationServer.retrieveRegistrationToken(any(), any())
+            verificationServer.retrieveRegistrationToken(any())
             verificationServer.pollTestResult(any())
             submissionServer.submitFakePayload()
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt
similarity index 64%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt
index 5fa4e49b6..b75849bb4 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/SubmissionServiceTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/service/submission/CoronaTestServiceTest.kt
@@ -2,6 +2,8 @@ package de.rki.coronawarnapp.service.submission
 
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResult
 import de.rki.coronawarnapp.coronatest.server.CoronaTestResultResponse
+import de.rki.coronawarnapp.coronatest.server.RegistrationData
+import de.rki.coronawarnapp.coronatest.server.RegistrationRequest
 import de.rki.coronawarnapp.coronatest.server.VerificationKeyType
 import de.rki.coronawarnapp.coronatest.type.CoronaTestService
 import de.rki.coronawarnapp.deniability.NoiseScheduler
@@ -20,7 +22,7 @@ import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 
-class SubmissionServiceTest : BaseTest() {
+class CoronaTestServiceTest : BaseTest() {
 
     private val tan = "123456-12345678-1234-4DA7-B166-B86D85475064"
     private val guid = "123456-12345678-1234-4DA7-B166-B86D85475064"
@@ -30,8 +32,6 @@ class SubmissionServiceTest : BaseTest() {
     @MockK lateinit var appComponent: ApplicationComponent
     @MockK lateinit var noiseScheduler: NoiseScheduler
 
-    lateinit var submissionService: CoronaTestService
-
     @BeforeEach
     fun setUp() {
         MockKAnnotations.init(this)
@@ -39,49 +39,45 @@ class SubmissionServiceTest : BaseTest() {
         every { AppInjector.component } returns appComponent
         every { appComponent.playbook } returns mockPlaybook
 
-        submissionService = CoronaTestService(
-            playbook = mockPlaybook,
-            noiseScheduler = noiseScheduler
-        )
-    }
-
-    @Test
-    fun registrationWithGUIDSucceeds() {
         coEvery {
-            mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID)
-        } returns (
-            registrationToken to CoronaTestResultResponse(
+            mockPlaybook.initialRegistration(any())
+        } returns RegistrationData(
+            registrationToken = registrationToken,
+            testResultResponse = CoronaTestResultResponse(
                 coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING,
                 sampleCollectedAt = null
             )
-            )
+        )
+    }
 
-        runBlocking {
-            submissionService.asyncRegisterDeviceViaGUID(guid)
-        }
+    private fun createInstance() = CoronaTestService(
+        playbook = mockPlaybook,
+        noiseScheduler = noiseScheduler
+    )
+
+    @Test
+    fun registrationWithGUIDSucceeds() = runBlocking {
+        val request = RegistrationRequest(
+            key = guid,
+            type = VerificationKeyType.GUID,
+        )
+        createInstance().registerTest(request)
 
         coVerify(exactly = 1) {
-            mockPlaybook.initialRegistration(guid, VerificationKeyType.GUID)
+            mockPlaybook.initialRegistration(request)
         }
     }
 
     @Test
-    fun registrationWithTeleTANSucceeds() {
-        coEvery {
-            mockPlaybook.initialRegistration(any(), VerificationKeyType.TELETAN)
-        } returns (
-            registrationToken to CoronaTestResultResponse(
-                coronaTestResult = CoronaTestResult.PCR_OR_RAT_PENDING,
-                sampleCollectedAt = null
-            )
-            )
-
-        runBlocking {
-            submissionService.asyncRegisterDeviceViaTAN(tan)
-        }
+    fun registrationWithTeleTANSucceeds() = runBlocking {
+        val request = RegistrationRequest(
+            key = tan,
+            type = VerificationKeyType.TELETAN,
+        )
+        createInstance().registerTest(request)
 
         coVerify(exactly = 1) {
-            mockPlaybook.initialRegistration(tan, VerificationKeyType.TELETAN)
+            mockPlaybook.initialRegistration(request)
         }
     }
 
@@ -93,7 +89,7 @@ class SubmissionServiceTest : BaseTest() {
         )
 
         runBlocking {
-            submissionService.asyncRequestTestResult(registrationToken) shouldBe CoronaTestResultResponse(
+            createInstance().checkTestResult(registrationToken) shouldBe CoronaTestResultResponse(
                 coronaTestResult = CoronaTestResult.PCR_NEGATIVE,
                 sampleCollectedAt = null,
             )
-- 
GitLab