From 8f03091d528904e1680d97d182c66075bfa84e04 Mon Sep 17 00:00:00 2001
From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
Date: Thu, 17 Jun 2021 13:44:17 +0200
Subject: [PATCH] Dcc QR code extractor (incl. recovery) (EXPOSUREAPP-7676)
 (#3444)

* interface recovery

* interface recovery

* merge dcc extractors

* fix tests

* detekt

* tests and error handling

* detekt

* fix faq exception

* fix faq exception

* remove todo

* klint

* clean up

* clean up

* use dedicated class for each type

* klint

* fix tests

* fix tests

* fix comment

* change naming

* change naming

* klint

* simplify

* comments
---
 .../ui/CovidCertificateDetailsFragmentTest.kt |   2 -
 .../bugreporting/BugReportingSharedModule.kt  |   4 +-
 ...cateQrCodeCensor.kt => DccQrCodeCensor.kt} |  29 +--
 .../qrcode/CoronaTestQRCodeValidator.kt       |  10 +-
 .../coronatest/qrcode/PcrQrCodeExtractor.kt   |   2 +-
 .../qrcode/RapidAntigenQrCodeExtractor.kt     |   2 +-
 .../CertificatePersonIdentifier.kt            |  10 +-
 .../common/certificate/Dcc.kt                 |  75 ------
 .../common/certificate/DccData.kt             |   2 +-
 .../common/certificate/DccQrCodeExtractor.kt  | 225 ++++++++++++++++++
 .../common/certificate/DccV1.kt               | 195 +++++++++++++++
 .../common/certificate/DccV1Parser.kt         | 132 ++++++++++
 .../InvalidHealthCertificateException.kt      |  38 ++-
 .../InvalidRecoveryCertificateException.kt    |  14 ++
 .../InvalidTestCertificateException.kt        |   4 +-
 .../InvalidVaccinationCertificateException.kt |  30 +--
 .../common/qrcode/DccQrCode.kt                |   7 +-
 .../person/core/PersonCertificatesProvider.kt |   4 +-
 .../core/RecoveryCertificateRepository.kt     |   4 +-
 .../core/certificate/RecoveryDccParser.kt     |  14 --
 .../core/certificate/RecoveryDccV1.kt         |  37 ---
 .../core/qrcode/RecoveryCertificateQRCode.kt  |   7 +-
 .../RecoveryCertificateQRCodeExtractor.kt     |  28 ---
 .../storage/RecoveryCertificateContainer.kt   |  21 +-
 .../test/core/TestCertificate.kt              |   3 +-
 .../test/core/TestCertificateRepository.kt    |   7 +-
 .../test/core/certificate/TestDccParser.kt    |  69 ------
 .../test/core/certificate/TestDccV1.kt        |  45 ----
 .../test/core/qrcode/TestCertificateQRCode.kt |   7 +-
 .../qrcode/TestCertificateQRCodeExtractor.kt  | 133 -----------
 .../core/storage/TestCertificateContainer.kt  |  29 ++-
 .../core/storage/TestCertificateProcessor.kt  |   6 +-
 .../core/certificate/VaccinationDccV1.kt      |  42 ----
 .../certificate/VaccinationDccV1Parser.kt     |  77 ------
 .../core/qrcode/DccQrCodeValidator.kt         |  30 +++
 .../qrcode/VaccinationCertificateQRCode.kt    |   7 +-
 .../core/qrcode/VaccinationQRCodeExtractor.kt |  83 -------
 .../core/qrcode/VaccinationQRCodeValidator.kt |  28 ---
 .../core/repository/VaccinationRepository.kt  |  10 +-
 .../storage/ContainerPostProcessor.kt         |   4 +-
 .../storage/VaccinationContainer.kt           |  23 +-
 .../ui/scan/VaccinationQrCodeScanFragment.kt  |   8 +-
 .../ui/scan/VaccinationQrCodeScanViewModel.kt |  10 +-
 .../res/values-bg/vaccination_strings.xml     |   6 +-
 .../res/values-de/vaccination_strings.xml     |   4 +-
 .../res/values-en/vaccination_strings.xml     |   6 +-
 .../res/values-pl/vaccination_strings.xml     |   6 +-
 .../res/values-ro/vaccination_strings.xml     |   6 +-
 .../res/values-tr/vaccination_strings.xml     |   6 +-
 .../main/res/values/vaccination_strings.xml   |   4 +-
 ...deCensorTest.kt => DccQrCodeCensorTest.kt} |  59 ++---
 .../qrcode/CoronaTestQrCodeValidatorTest.kt   |   5 +-
 .../qrcode/PcrQrCodeExtractorTest.kt          |  12 +-
 .../qrcode/RapidAntigenQrCodeExtractorTest.kt |  11 +-
 .../common/CertificatePersonIdentifierTest.kt |  10 +-
 .../test/TestCertificateRepositoryTest.kt     |   8 +-
 .../test/TestCertificateTestData.kt           |   4 +-
 .../qrcode/TestCertificateDccParserTest.kt    |   8 +-
 .../TestCertificateQRCodeExtractorTest.kt     |  26 +-
 .../execution/TestCertificateProcessorTest.kt |  11 +-
 .../core/VaccinationQrCodeTestData.java       |   2 +
 .../core/VaccinationTestComponent.kt          |   8 +-
 .../vaccination/core/VaccinationTestData.kt   | 110 +++++----
 ...actorTest.kt => DccQrCodeExtractorTest.kt} |  86 +++++--
 .../core/qrcode/DccQrCodeValidatorTest.kt     |  46 ++++
 .../core/qrcode/RecoveryQrCodeTestData.java   |   5 +
 .../qrcode/VaccinationQrCodeValidatorTest.kt  |  34 ---
 .../repository/VaccinationRepositoryTest.kt   |  10 +-
 .../storage/VaccinationContainerTest.kt       |  17 +-
 69 files changed, 1055 insertions(+), 972 deletions(-)
 rename Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/{CertificateQrCodeCensor.kt => DccQrCodeCensor.kt} (82%)
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
 rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/{CertificateQrCodeCensorTest.kt => DccQrCodeCensorTest.kt} (65%)
 rename Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/{VaccinationQRCodeExtractorTest.kt => DccQrCodeExtractorTest.kt} (76%)
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java
 delete mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt

diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt
index 5411c5a5d..124ba8e74 100644
--- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt
+++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/covidcertificate/test/ui/CovidCertificateDetailsFragmentTest.kt
@@ -98,8 +98,6 @@ class VaccinationDetailsFragmentTest : BaseUITest() {
                     get() = "Xup"
                 override val sampleCollectedAt: Instant
                     get() = testDate
-                override val testResultAt: Instant
-                    get() = testDate
                 override val testCenter: String
                     get() = "AB123"
                 override val registeredAt: Instant
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
index 5d9d4d09f..b78711347 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/BugReportingSharedModule.kt
@@ -17,7 +17,7 @@ import de.rki.coronawarnapp.bugreporting.censors.submission.PcrTeleTanCensor
 import de.rki.coronawarnapp.bugreporting.censors.submission.RACoronaTestCensor
 import de.rki.coronawarnapp.bugreporting.censors.submission.RatProfileCensor
 import de.rki.coronawarnapp.bugreporting.censors.submission.RatQrCodeCensor
-import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor
+import de.rki.coronawarnapp.bugreporting.censors.vaccination.DccQrCodeCensor
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebugLoggerScope
 import de.rki.coronawarnapp.bugreporting.debuglog.internal.DebuggerScope
 import de.rki.coronawarnapp.bugreporting.debuglog.upload.server.LogUploadApiV1
@@ -126,5 +126,5 @@ class BugReportingSharedModule {
 
     @Provides
     @IntoSet
-    fun certificateQrCodeCensor(censor: CertificateQrCodeCensor): BugCensor = censor
+    fun certificateQrCodeCensor(censor: DccQrCodeCensor): BugCensor = censor
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt
similarity index 82%
rename from Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt
rename to Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt
index 8ea9f61d7..d97b5e1ec 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensor.kt
@@ -3,14 +3,14 @@ package de.rki.coronawarnapp.bugreporting.censors.vaccination
 import dagger.Reusable
 import de.rki.coronawarnapp.bugreporting.censors.BugCensor
 import de.rki.coronawarnapp.bugreporting.censors.BugCensor.CensorContainer
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1
 import java.util.LinkedList
 import javax.inject.Inject
 
 @Reusable
-class CertificateQrCodeCensor @Inject constructor() : BugCensor {
+class DccQrCodeCensor @Inject constructor() : BugCensor {
 
     override suspend fun checkLog(message: String): CensorContainer? {
         var newMessage = CensorContainer(message)
@@ -25,20 +25,15 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor {
 
                 newMessage = newMessage.censor(
                     dobFormatted,
-                    "vaccinationCertificate/dateOfBirth"
+                    "covidCertificate/dateOfBirth"
                 )
-                if (dobFormatted != dob) {
-                    newMessage = newMessage.censor(
-                        dob,
-                        "vaccinationCertificate/dob"
-                    )
-                }
 
                 newMessage = censorNameData(nameData, newMessage)
 
-                payload.let { data ->
-                    newMessage = censorVaccinationData(data, newMessage)
+                (it.certificate as? VaccinationDccV1)?.let {
+                    newMessage = censorVaccinationData(it.vaccination, newMessage)
                 }
+                // TODO test and recovery ?
             }
         }
 
@@ -46,7 +41,7 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor {
     }
 
     private fun censorVaccinationData(
-        vaccinationData: VaccinationDccV1.VaccinationData,
+        vaccinationData: DccV1.VaccinationData,
         message: CensorContainer
     ): CensorContainer {
         var newMessage = message
@@ -101,7 +96,7 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor {
         return newMessage
     }
 
-    private fun censorNameData(nameData: Dcc.NameData, message: CensorContainer): CensorContainer {
+    private fun censorNameData(nameData: DccV1.NameData, message: CensorContainer): CensorContainer {
         var newMessage = message
 
         nameData.familyName?.let { fName ->
@@ -147,8 +142,8 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor {
 
         fun clearQRCodeStringToCensor() = synchronized(qrCodeStringsToCensor) { qrCodeStringsToCensor.clear() }
 
-        private val certsToCensor = LinkedList<DccData<VaccinationDccV1>>()
-        fun addCertificateToCensor(cert: DccData<VaccinationDccV1>) = synchronized(certsToCensor) {
+        private val certsToCensor = LinkedList<DccData<out DccV1.MetaData>>()
+        fun addCertificateToCensor(cert: DccData<out DccV1.MetaData>) = synchronized(certsToCensor) {
             certsToCensor.apply {
                 if (contains(cert)) return@apply
                 addFirst(cert)
@@ -159,6 +154,6 @@ class CertificateQrCodeCensor @Inject constructor() : BugCensor {
 
         fun clearCertificateToCensor() = synchronized(certsToCensor) { certsToCensor.clear() }
 
-        private const val PLACEHOLDER = "########-####-####-####-########"
+        private const val PLACEHOLDER = "###"
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt
index 4f12913e4..7b31a9773 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQRCodeValidator.kt
@@ -13,7 +13,7 @@ class CoronaTestQrCodeValidator @Inject constructor(
 
     fun validate(rawString: String): CoronaTestQRCode {
         return findExtractor(rawString)
-            ?.extract(rawString, mode = QrCodeExtractor.Mode.TEST_STRICT)
+            ?.extract(rawString)
             ?.also { Timber.i("Extracted data from QR code is %s", it) }
             ?: throw InvalidQRCodeException()
     }
@@ -25,11 +25,5 @@ class CoronaTestQrCodeValidator @Inject constructor(
 
 interface QrCodeExtractor<T> {
     fun canHandle(rawString: String): Boolean
-    fun extract(rawString: String, mode: Mode): T
-
-    enum class Mode {
-        TEST_STRICT,
-        CERT_VAC_STRICT,
-        CERT_VAC_LENIENT
-    }
+    fun extract(rawString: String): T
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt
index c94eb873f..49633e3de 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractor.kt
@@ -8,7 +8,7 @@ class PcrQrCodeExtractor @Inject constructor() : QrCodeExtractor<CoronaTestQRCod
 
     override fun canHandle(rawString: String): Boolean = rawString.startsWith(prefix, ignoreCase = true)
 
-    override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.PCR {
+    override fun extract(rawString: String): CoronaTestQRCode.PCR {
         val guid = extractGUID(rawString)
         PcrQrCodeCensor.lastGUID = guid
         return CoronaTestQRCode.PCR(guid)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt
index 9175decb7..2265512e0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractor.kt
@@ -19,7 +19,7 @@ class RapidAntigenQrCodeExtractor @Inject constructor() : QrCodeExtractor<Corona
         return rawString.startsWith(PREFIX1, ignoreCase = true) || rawString.startsWith(PREFIX2, ignoreCase = true)
     }
 
-    override fun extract(rawString: String, mode: QrCodeExtractor.Mode): CoronaTestQRCode.RapidAntigen {
+    override fun extract(rawString: String): CoronaTestQRCode.RapidAntigen {
         Timber.v("extract(rawString=%s)", rawString)
         val payload = CleanPayload(extractData(rawString))
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt
index 1e71889eb..440f44388 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/CertificatePersonIdentifier.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.covidcertificate.common.certificate
 
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.DOB_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NAME_MISMATCH
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.util.HashExtensions.toSHA256
 import org.joda.time.LocalDate
@@ -34,15 +34,15 @@ data class CertificatePersonIdentifier(
     fun requireMatch(other: CertificatePersonIdentifier) {
         if (lastNameStandardized != other.lastNameStandardized) {
             Timber.d("Family name does not match, got ${other.lastNameStandardized}, expected $lastNameStandardized")
-            throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH)
+            throw InvalidVaccinationCertificateException(NAME_MISMATCH)
         }
         if (firstNameStandardized != other.firstNameStandardized) {
             Timber.d("Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized")
-            throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH)
+            throw InvalidVaccinationCertificateException(NAME_MISMATCH)
         }
         if (dateOfBirth != other.dateOfBirth) {
             Timber.d("Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth")
-            throw InvalidVaccinationCertificateException(VC_DOB_MISMATCH)
+            throw InvalidVaccinationCertificateException(DOB_MISMATCH)
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt
deleted file mode 100644
index 52d13dd08..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/Dcc.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.common.certificate
-
-import com.google.gson.annotations.SerializedName
-import org.joda.time.DateTime
-import org.joda.time.LocalDate
-import org.joda.time.format.DateTimeFormat
-import org.joda.time.format.DateTimeFormatterBuilder
-import org.joda.time.format.ISODateTimeFormat
-import timber.log.Timber
-
-abstract class Dcc<PayloadType : Dcc.Payload> {
-    data class NameData(
-        @SerializedName("fn") internal val familyName: String?,
-        @SerializedName("fnt") internal val familyNameStandardized: String,
-        @SerializedName("gn") internal val givenName: String?,
-        @SerializedName("gnt") internal val givenNameStandardized: String?,
-    ) {
-        val firstName: String?
-            get() = if (givenName.isNullOrBlank()) givenNameStandardized else givenName
-
-        val lastName: String
-            get() = if (familyName.isNullOrBlank()) familyNameStandardized else familyName
-
-        val fullName: String
-            get() = when {
-                firstName.isNullOrBlank() -> lastName
-                else -> "$firstName $lastName"
-            }
-    }
-
-    abstract val version: String
-    abstract val nameData: NameData
-    abstract val dob: String
-
-    // Can't use lazy because GSON will NULL it, as we have no no-args constructor
-    private var dateOfBirthCache: LocalDate? = null
-    val dateOfBirth: LocalDate
-        get() = dateOfBirthCache ?: dob.toLocalDateLeniently().also { dateOfBirthCache = it }
-
-    abstract val payloads: List<PayloadType>
-    val payload: PayloadType
-        get() = payloads.single()
-
-    val personIdentifier: CertificatePersonIdentifier
-        get() = CertificatePersonIdentifier(
-            dateOfBirth = dateOfBirth,
-            lastNameStandardized = nameData.familyNameStandardized,
-            firstNameStandardized = nameData.givenNameStandardized
-        )
-
-    interface Payload {
-        val targetId: String
-        val certificateCountry: String
-        val certificateIssuer: String
-        val uniqueCertificateIdentifier: String
-    }
-}
-
-internal fun String.toLocalDateLeniently(): LocalDate = try {
-    LocalDate.parse(this, DateTimeFormat.forPattern("yyyy-MM-dd"))
-} catch (e: Exception) {
-    Timber.w("Irregular date string: %s", this)
-    try {
-        DateTime.parse(
-            this,
-            DateTimeFormatterBuilder()
-                .append(ISODateTimeFormat.date())
-                .append(ISODateTimeFormat.timeParser().withOffsetParsed())
-                .toFormatter()
-        ).toLocalDate()
-    } catch (giveUp: Exception) {
-        Timber.e("Invalid date string: %s", this)
-        throw giveUp
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt
index 89a2aac6b..e0e83438d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccData.kt
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.covidcertificate.common.certificate
 
-data class DccData<CertT : Dcc<*>>(
+data class DccData<CertT : DccV1.MetaData>(
     val header: DccHeader,
     val certificate: CertT,
 )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt
new file mode 100644
index 000000000..03d32a9a3
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccQrCodeExtractor.kt
@@ -0,0 +1,225 @@
+package de.rki.coronawarnapp.covidcertificate.common.certificate
+
+import de.rki.coronawarnapp.bugreporting.censors.vaccination.DccQrCodeCensor
+import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_REC_STRICT
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_SINGLE_STRICT
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_TEST_STRICT
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_VAC_LENIENT
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode.CERT_VAC_STRICT
+import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder
+import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser
+import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_RECOVERY_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidRecoveryCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
+import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
+import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
+import de.rki.coronawarnapp.util.compression.deflate
+import de.rki.coronawarnapp.util.compression.inflate
+import de.rki.coronawarnapp.util.encoding.Base45Decoder
+import timber.log.Timber
+import javax.inject.Inject
+
+class DccQrCodeExtractor @Inject constructor(
+    private val coseDecoder: DccCoseDecoder,
+    private val headerParser: DccHeaderParser,
+    private val bodyParser: DccV1Parser,
+) : QrCodeExtractor<DccQrCode> {
+
+    override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX)
+
+    /**
+     * May throw an **[InvalidHealthCertificateException]**
+     */
+    override fun extract(rawString: String): DccQrCode = extract(rawString, CERT_SINGLE_STRICT)
+
+    /**
+     * May throw an **[InvalidHealthCertificateException]**
+     */
+    fun extractEncrypted(
+        decryptionKey: ByteArray,
+        rawCoseObjectEncrypted: ByteArray,
+    ): DccQrCode {
+        val qrCodeString = rawCoseObjectEncrypted.decrypt(decryptionKey).encode()
+        return extract(qrCodeString)
+    }
+
+    /**
+     * May throw an **[InvalidHealthCertificateException]**
+     */
+    fun extract(rawString: String, mode: DccV1Parser.Mode): DccQrCode {
+        DccQrCodeCensor.addQRCodeStringToCensor(rawString)
+
+        return try {
+            val parsedData = rawString
+                .removePrefix(PREFIX)
+                .decodeBase45()
+                .decompress()
+                .parse(mode)
+
+            toDccQrCode(rawString, parsedData).also {
+                when (mode) {
+                    CERT_VAC_STRICT, CERT_VAC_LENIENT -> if (it !is VaccinationCertificateQRCode)
+                        throw InvalidVaccinationCertificateException(NO_VACCINATION_ENTRY)
+                    CERT_REC_STRICT -> if (it !is RecoveryCertificateQRCode)
+                        throw InvalidRecoveryCertificateException(NO_RECOVERY_ENTRY)
+                    CERT_TEST_STRICT -> if (it !is TestCertificateQRCode)
+                        throw InvalidTestCertificateException(NO_TEST_ENTRY)
+                    else -> { /*anything goes*/
+                    }
+                }
+            }
+        } catch (e: InvalidHealthCertificateException) {
+            when (mode) {
+                CERT_VAC_STRICT, CERT_VAC_LENIENT ->
+                    throw InvalidVaccinationCertificateException(e.errorCode)
+                CERT_REC_STRICT ->
+                    throw InvalidRecoveryCertificateException(e.errorCode)
+                CERT_TEST_STRICT ->
+                    throw InvalidTestCertificateException(e.errorCode)
+                CERT_SINGLE_STRICT -> throw e
+            }
+        }
+    }
+
+    private fun RawCOSEObject.decrypt(decryptionKey: ByteArray): RawCOSEObject = try {
+        coseDecoder.decryptMessage(
+            input = this,
+            decryptionKey = decryptionKey
+        )
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        Timber.e(e, HC_COSE_MESSAGE_INVALID.toString())
+        throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID)
+    }
+
+    private fun RawCOSEObject.encode(): String {
+        return PREFIX + compress().encodeBase45()
+    }
+
+    private fun ByteArray.encodeBase45(): String = try {
+        Base45Decoder.encode(this)
+    } catch (e: Throwable) {
+        Timber.e(e, HC_BASE45_ENCODING_FAILED.toString())
+        throw InvalidHealthCertificateException(HC_BASE45_ENCODING_FAILED)
+    }
+
+    private fun RawCOSEObject.compress(): ByteArray = try {
+        this.deflate()
+    } catch (e: Throwable) {
+        Timber.e(e, HC_ZLIB_COMPRESSION_FAILED.toString())
+        throw InvalidTestCertificateException(HC_ZLIB_COMPRESSION_FAILED)
+    }
+
+    private fun toDccQrCode(rawString: String, parsedData: DccData<DccV1.MetaData>): DccQrCode =
+        when (parsedData.certificate) {
+            is VaccinationDccV1 -> VaccinationCertificateQRCode(
+                qrCode = rawString,
+                data = DccData(
+                    parsedData.header,
+                    parsedData.certificate
+                ),
+            )
+            is TestDccV1 -> TestCertificateQRCode(
+                qrCode = rawString,
+                data = DccData(
+                    parsedData.header,
+                    parsedData.certificate
+                ),
+            )
+            is RecoveryDccV1 -> RecoveryCertificateQRCode(
+                qrCode = rawString,
+                data = DccData(
+                    parsedData.header,
+                    parsedData.certificate
+                ),
+            )
+            else -> throw InvalidHealthCertificateException(JSON_SCHEMA_INVALID)
+        }
+
+    private fun String.decodeBase45(): ByteArray = try {
+        Base45Decoder.decode(this)
+    } catch (e: Throwable) {
+        Timber.e(e)
+        throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED)
+    }
+
+    private fun ByteArray.decompress(): RawCOSEObject = try {
+        this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT)
+    } catch (e: Throwable) {
+        Timber.e(e)
+        throw InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
+    }
+
+    fun RawCOSEObject.parse(mode: DccV1Parser.Mode): DccData<DccV1.MetaData> = try {
+        Timber.v("Parsing COSE for covid certificate.")
+        val cbor = coseDecoder.decode(this)
+        DccData(
+            header = headerParser.parse(cbor),
+            certificate = bodyParser.parse(cbor, mode).toCertificate
+        ).also {
+            DccQrCodeCensor.addCertificateToCensor(it)
+        }.also {
+            Timber.v("Parsed covid certificate for %s", it.certificate.nameData.familyNameStandardized)
+        }
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        Timber.e(e)
+        throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
+    }
+
+    private val DccV1.isVaccinationCertificate: Boolean
+        get() = this.vaccinations?.isNotEmpty() == true
+
+    private val DccV1.isTestCertificate: Boolean
+        get() = this.tests?.isNotEmpty() == true
+
+    private val DccV1.isRecoveryCertificate: Boolean
+        get() = this.recoveries?.isNotEmpty() == true
+
+    private val DccV1.toCertificate: DccV1.MetaData
+        get() = when {
+            isVaccinationCertificate -> VaccinationDccV1(
+                version = version,
+                nameData = nameData,
+                dateOfBirth = dateOfBirth,
+                personIdentifier = personIdentifier,
+                vaccination = vaccinations!!.first()
+            )
+            isTestCertificate -> TestDccV1(
+                version = version,
+                nameData = nameData,
+                dateOfBirth = dateOfBirth,
+                personIdentifier = personIdentifier,
+                test = tests!!.first()
+            )
+            isRecoveryCertificate -> RecoveryDccV1(
+                version = version,
+                nameData = nameData,
+                dateOfBirth = dateOfBirth,
+                personIdentifier = personIdentifier,
+                recovery = recoveries!!.first()
+            )
+            else -> throw InvalidHealthCertificateException(JSON_SCHEMA_INVALID)
+        }
+}
+
+private const val PREFIX = "HC1:"
+// Zip bomb
+private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt
new file mode 100644
index 000000000..8752cfda2
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1.kt
@@ -0,0 +1,195 @@
+package de.rki.coronawarnapp.covidcertificate.common.certificate
+
+import com.google.gson.annotations.SerializedName
+import org.joda.time.DateTime
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+import org.joda.time.format.DateTimeFormat
+import org.joda.time.format.DateTimeFormatterBuilder
+import org.joda.time.format.ISODateTimeFormat
+import timber.log.Timber
+
+data class DccV1(
+    @SerializedName("ver") val version: String,
+    @SerializedName("nam") val nameData: NameData,
+    @SerializedName("dob") val dob: String,
+    @SerializedName("v") val vaccinations: List<VaccinationData>? = null,
+    @SerializedName("t") val tests: List<TestCertificateData>? = null,
+    @SerializedName("r") val recoveries: List<RecoveryCertificateData>? = null,
+) {
+    data class NameData(
+        @SerializedName("fn") internal val familyName: String?,
+        @SerializedName("fnt") internal val familyNameStandardized: String,
+        @SerializedName("gn") internal val givenName: String?,
+        @SerializedName("gnt") internal val givenNameStandardized: String?,
+    ) {
+        val firstName: String?
+            get() = if (givenName.isNullOrBlank()) givenNameStandardized else givenName
+
+        val lastName: String
+            get() = if (familyName.isNullOrBlank()) familyNameStandardized else familyName
+
+        val fullName: String
+            get() = when {
+                firstName.isNullOrBlank() -> lastName
+                else -> "$firstName $lastName"
+            }
+    }
+
+    // Can't use lazy because GSON will NULL it, as we have no no-args constructor
+    private var dateOfBirthCache: LocalDate? = null
+    val dateOfBirth: LocalDate
+        get() = dateOfBirthCache ?: dob.toLocalDateLeniently().also { dateOfBirthCache = it }
+
+    val personIdentifier: CertificatePersonIdentifier
+        get() = CertificatePersonIdentifier(
+            dateOfBirth = dateOfBirth,
+            lastNameStandardized = nameData.familyNameStandardized,
+            firstNameStandardized = nameData.givenNameStandardized
+        )
+
+    interface MetaData {
+        val version: String
+        val nameData: NameData
+        val dateOfBirth: LocalDate
+        val payload: Payload
+        val personIdentifier: CertificatePersonIdentifier
+    }
+
+    interface Payload {
+        val targetId: String
+        val certificateCountry: String
+        val certificateIssuer: String
+        val uniqueCertificateIdentifier: String
+    }
+
+    data class RecoveryCertificateData(
+        // Disease or agent targeted, e.g. "tg": "840539006"
+        @SerializedName("tg") override val targetId: String,
+        // Date of First Positive NAA Test Result (required) e.g. "2021-04-21"
+        @SerializedName("fr") val fr: String,
+        // Certificate Valid From (required) e.g. "2021-05-01"
+        @SerializedName("df") val df: String,
+        // Certificate Valid Until (required) e.g. "2021-10-21"
+        @SerializedName("du") val du: String,
+        // Country of Test (required)
+        @SerializedName("co") override val certificateCountry: String,
+        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
+        @SerializedName("is") override val certificateIssuer: String,
+        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+        @SerializedName("ci") override val uniqueCertificateIdentifier: String
+    ) : Payload {
+        val testedPositiveOn: LocalDate
+            get() = LocalDate.parse(fr)
+        val validFrom: LocalDate
+            get() = LocalDate.parse(df)
+        val validUntil: LocalDate
+            get() = LocalDate.parse(du)
+    }
+
+    data class VaccinationData(
+        // Disease or agent targeted, e.g. "tg": "840539006"
+        @SerializedName("tg") override val targetId: String,
+        // Vaccine or prophylaxis, e.g. "vp": "1119349007"
+        @SerializedName("vp") val vaccineId: String,
+        // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528",
+        @SerializedName("mp") val medicalProductId: String,
+        // Marketing Authorization Holder, e.g. "ma": "ORG-100030215",
+        @SerializedName("ma") val marketAuthorizationHolderId: String,
+        // Dose Number, e.g. "dn": 2
+        @SerializedName("dn") val doseNumber: Int,
+        // Total Series of Doses, e.g. "sd": 2,
+        @SerializedName("sd") val totalSeriesOfDoses: Int,
+        // Date of Vaccination, e.g. "dt" : "2021-04-21"
+        @SerializedName("dt") val dt: String,
+        // Country of Vaccination, e.g. "co": "NL"
+        @SerializedName("co") override val certificateCountry: String,
+        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
+        @SerializedName("is") override val certificateIssuer: String,
+        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+        @SerializedName("ci") override val uniqueCertificateIdentifier: String
+    ) : Payload {
+        // Can't use lazy because GSON will NULL it, as we have no no-args constructor
+        private var vaccinatedAtCache: LocalDate? = null
+        val vaccinatedAt: LocalDate
+            get() = vaccinatedAtCache ?: dt.toLocalDateLeniently().also { vaccinatedAtCache = it }
+    }
+
+    data class TestCertificateData(
+        // Disease or agent targeted, e.g. "tg": "840539006"
+        @SerializedName("tg") override val targetId: String,
+        // Type of Test (required) eg "LP217198-3"
+        @SerializedName("tt") val testType: String,
+        // Test Result (required) e. g. "tr": "260415000"
+        @SerializedName("tr") val testResult: String,
+        // NAA Test Name (only for PCR tests, but not required) "nm": "Roche LightCycler qPCR",
+        @SerializedName("nm") val testName: String? = null,
+        // RAT Test name and manufacturer (only for RAT tests, but not required)
+        @SerializedName("ma") val testNameAndManufactor: String? = null,
+        // Date/Time of Sample Collection (required) "sc": "2021-04-13T14:20:00+00:00"
+        @SerializedName("sc") val sc: String,
+        // Testing Center (required) "tc": "GGD Fryslân, L-Heliconweg",
+        @SerializedName("tc") val testCenter: String?,
+        // Country of Test (required)
+        @SerializedName("co") override val certificateCountry: String,
+        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
+        @SerializedName("is") override val certificateIssuer: String,
+        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+        @SerializedName("ci") override val uniqueCertificateIdentifier: String
+    ) : Payload {
+
+        val sampleCollectedAt: Instant
+            get() = Instant.parse(sc)
+    }
+}
+
+internal fun String.toLocalDateLeniently(): LocalDate = try {
+    LocalDate.parse(this, DateTimeFormat.forPattern("yyyy-MM-dd"))
+} catch (e: Exception) {
+    Timber.w("Irregular date string: %s", this)
+    try {
+        DateTime.parse(
+            this,
+            DateTimeFormatterBuilder()
+                .append(ISODateTimeFormat.date())
+                .append(ISODateTimeFormat.timeParser().withOffsetParsed())
+                .toFormatter()
+        ).toLocalDate()
+    } catch (giveUp: Exception) {
+        Timber.e("Invalid date string: %s", this)
+        throw giveUp
+    }
+}
+
+data class VaccinationDccV1(
+    override val version: String,
+    override val nameData: DccV1.NameData,
+    override val dateOfBirth: LocalDate,
+    override val personIdentifier: CertificatePersonIdentifier,
+    val vaccination: DccV1.VaccinationData
+) : DccV1.MetaData {
+    override val payload: DccV1.Payload
+        get() = vaccination
+}
+
+data class TestDccV1(
+    override val version: String,
+    override val nameData: DccV1.NameData,
+    override val dateOfBirth: LocalDate,
+    override val personIdentifier: CertificatePersonIdentifier,
+    val test: DccV1.TestCertificateData
+) : DccV1.MetaData {
+    override val payload: DccV1.Payload
+        get() = test
+}
+
+data class RecoveryDccV1(
+    override val version: String,
+    override val nameData: DccV1.NameData,
+    override val dateOfBirth: LocalDate,
+    override val personIdentifier: CertificatePersonIdentifier,
+    val recovery: DccV1.RecoveryCertificateData
+) : DccV1.MetaData {
+    override val payload: DccV1.Payload
+        get() = recovery
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt
new file mode 100644
index 000000000..702d1ef93
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/certificate/DccV1Parser.kt
@@ -0,0 +1,132 @@
+package de.rki.coronawarnapp.covidcertificate.common.certificate
+
+import com.google.gson.Gson
+import com.upokecenter.cbor.CBORObject
+import dagger.Reusable
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.util.serialization.BaseGson
+import de.rki.coronawarnapp.util.serialization.fromJson
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class DccV1Parser @Inject constructor(
+    @BaseGson private val gson: Gson
+) {
+    fun parse(map: CBORObject, mode: Mode): DccV1 = try {
+        map[keyHCert]?.run {
+            this[keyEuDgcV1]?.run {
+                this.toCertificate().toValidated(mode)
+            } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_DGC)
+        } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_HCERT)
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        throw InvalidHealthCertificateException(ErrorCode.HC_CBOR_DECODING_FAILED, cause = e)
+    }
+
+    private fun CBORObject.toCertificate(): DccV1 = try {
+        val json = ToJSONString()
+        gson.fromJson(json)
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        throw InvalidHealthCertificateException(ErrorCode.JSON_SCHEMA_INVALID)
+    }
+
+    private fun DccV1.toValidated(mode: Mode): DccV1 = try {
+        checkModeRestrictions(mode)
+            .apply {
+                // Apply otherwise we risk accidentally accessing the original obj in the outer scope
+                require(isSingleCertificate())
+                checkFields()
+            }
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        throw InvalidHealthCertificateException(ErrorCode.JSON_SCHEMA_INVALID)
+    }
+
+    private fun DccV1.checkModeRestrictions(mode: Mode) = when (mode) {
+        Mode.CERT_VAC_STRICT ->
+            if (vaccinations?.size != 1)
+                throw InvalidVaccinationCertificateException(
+                    if (vaccinations.isNullOrEmpty()) ErrorCode.NO_VACCINATION_ENTRY
+                    else ErrorCode.MULTIPLE_VACCINATION_ENTRIES
+                )
+            else this
+        Mode.CERT_VAC_LENIENT -> {
+            if (vaccinations.isNullOrEmpty())
+                throw InvalidVaccinationCertificateException(ErrorCode.NO_VACCINATION_ENTRY)
+            Timber.w("Lenient: Vaccination data contained multiple entries.")
+            copy(vaccinations = listOf(vaccinations.maxByOrNull { it.vaccinatedAt }!!))
+        }
+        Mode.CERT_REC_STRICT ->
+            if (recoveries?.size != 1)
+                throw InvalidVaccinationCertificateException(
+                    if (recoveries.isNullOrEmpty()) ErrorCode.NO_RECOVERY_ENTRY
+                    else ErrorCode.MULTIPLE_RECOVERY_ENTRIES
+                )
+            else this
+        Mode.CERT_TEST_STRICT ->
+            if (tests?.size != 1)
+                throw InvalidVaccinationCertificateException(
+                    if (tests.isNullOrEmpty()) ErrorCode.NO_TEST_ENTRY
+                    else ErrorCode.MULTIPLE_TEST_ENTRIES
+                )
+            else this
+        else -> this
+    }
+
+    private fun DccV1.isSingleCertificate(): Boolean {
+        return (vaccinations?.size ?: 0) + (tests?.size ?: 0) + (recoveries?.size ?: 0) == 1
+    }
+
+    private fun DccV1.checkFields() {
+        // check for non null (Gson does not enforce it) + not blank & force date parsing
+        require(version.isNotBlank())
+        require(nameData.familyNameStandardized.isNotBlank())
+        dateOfBirth
+        vaccinations?.forEach {
+            it.vaccinatedAt
+            require(it.certificateIssuer.isNotBlank())
+            require(it.certificateCountry.isNotBlank())
+            require(it.marketAuthorizationHolderId.isNotBlank())
+            require(it.medicalProductId.isNotBlank())
+            require(it.targetId.isNotBlank())
+            require(it.doseNumber > 0)
+            require(it.totalSeriesOfDoses > 0)
+        }
+        tests?.forEach {
+            it.sampleCollectedAt
+            require(it.certificateIssuer.isNotBlank())
+            require(it.certificateCountry.isNotBlank())
+            require(it.targetId.isNotBlank())
+            require(it.testResult.isNotBlank())
+            require(it.testType.isNotBlank())
+        }
+        recoveries?.forEach {
+            it.testedPositiveOn
+            it.validFrom
+            it.validUntil
+            require(it.certificateIssuer.isNotBlank())
+            require(it.certificateCountry.isNotBlank())
+            require(it.targetId.isNotBlank())
+        }
+    }
+
+    enum class Mode {
+        CERT_VAC_STRICT, // exactly one vaccination certificate allowed
+        CERT_VAC_LENIENT, // multiple vaccination certificates allowed
+        CERT_REC_STRICT, // exactly one recovery certificate allowed
+        CERT_TEST_STRICT, // exactly one test certificate allowed
+        CERT_SINGLE_STRICT; // exactly one certificate allowed
+    }
+
+    companion object {
+        private val keyEuDgcV1 = CBORObject.FromObject(1)
+        private val keyHCert = CBORObject.FromObject(-260)
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt
index 401f396e2..d1c774e5b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidHealthCertificateException.kt
@@ -1,6 +1,7 @@
 package de.rki.coronawarnapp.covidcertificate.common.exception
 
 import android.content.Context
+import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.coronatest.qrcode.InvalidQRCodeException
 import de.rki.coronawarnapp.util.HasHumanReadableError
 import de.rki.coronawarnapp.util.HumanReadableError
@@ -22,15 +23,18 @@ open class InvalidHealthCertificateException(
         HC_COSE_TAG_INVALID("COSE tag invalid."),
         HC_COSE_MESSAGE_INVALID("COSE message invalid."),
         HC_CBOR_DECODING_FAILED("CBOR decoding failed."),
-        VC_NO_VACCINATION_ENTRY("Vaccination certificate missing."),
-        VC_MULTIPLE_VACCINATION_ENTRIES("Multiple vaccination certificates."),
+        NO_VACCINATION_ENTRY("Vaccination certificate missing."),
+        MULTIPLE_VACCINATION_ENTRIES("Multiple vaccination certificates."),
+        MULTIPLE_TEST_ENTRIES("Multiple test certificates."),
+        MULTIPLE_RECOVERY_ENTRIES("Multiple recovery certificates."),
         NO_TEST_ENTRY("Test certificate missing."),
-        VC_PREFIX_INVALID("Prefix invalid."),
-        VC_STORING_FAILED("Storing failed."),
+        NO_RECOVERY_ENTRY("Recovery certificate missing."),
+        PREFIX_INVALID("Prefix invalid."),
+        STORING_FAILED("Storing failed."),
         JSON_SCHEMA_INVALID("Json schema invalid."),
-        VC_NAME_MISMATCH("Name does not match."),
-        VC_ALREADY_REGISTERED("Certificate already registered."),
-        VC_DOB_MISMATCH("Date of birth does not match."),
+        NAME_MISMATCH("Name does not match."),
+        ALREADY_REGISTERED("Certificate already registered."),
+        DOB_MISMATCH("Date of birth does not match."),
         HC_CWT_NO_DGC("Dgc missing."),
         HC_CWT_NO_EXP("Expiration date missing."),
         HC_CWT_NO_HCERT("Health certificate missing."),
@@ -41,9 +45,22 @@ open class InvalidHealthCertificateException(
         RSA_KP_GENERATION_FAILED("RSA key pair generation failed."),
     }
 
+    open val showFaqButton: Boolean = false
+    open val faqButtonText: Int = 0
+    open val faqLink: Int = 0
+
     open val errorMessage: LazyString
-        get() = CachedString { context ->
-            context.getString(ERROR_MESSAGE_GENERIC)
+        get() = when (errorCode) {
+            ErrorCode.STORING_FAILED -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_SCAN_AGAIN)
+            }
+
+            ErrorCode.ALREADY_REGISTERED -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_ALREADY_REGISTERED)
+            }
+            else -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_GENERIC)
+            }
         }
 
     override fun toHumanReadableError(context: Context): HumanReadableError {
@@ -52,3 +69,6 @@ open class InvalidHealthCertificateException(
         )
     }
 }
+
+private const val ERROR_MESSAGE_SCAN_AGAIN = R.string.error_dcc_scan_again
+private const val ERROR_MESSAGE_ALREADY_REGISTERED = R.string.error_dcc_already_registered
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt
new file mode 100644
index 000000000..2b67a1f0d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidRecoveryCertificateException.kt
@@ -0,0 +1,14 @@
+package de.rki.coronawarnapp.covidcertificate.common.exception
+
+import android.content.Context
+import de.rki.coronawarnapp.util.HumanReadableError
+
+class InvalidRecoveryCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) {
+    override fun toHumanReadableError(context: Context): HumanReadableError {
+        return HumanReadableError(
+            description = errorMessage.get(context) + " ($PREFIX$errorCode)"
+        )
+    }
+}
+
+private const val PREFIX = "RC_"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt
index 9bdfc992a..7ffb6f2ee 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidTestCertificateException.kt
@@ -8,7 +8,7 @@ import de.rki.coronawarnapp.util.ui.LazyString
 class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) {
     override fun toHumanReadableError(context: Context): HumanReadableError {
         return HumanReadableError(
-            description = errorMessage.get(context) + " ($errorCode)"
+            description = errorMessage.get(context) + " ($PREFIX$errorCode)"
         )
     }
 
@@ -35,3 +35,5 @@ class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCerti
             else -> super.errorMessage
         }
 }
+
+private const val PREFIX = "TC_"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt
index 63fc7f672..0004db62f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/exception/InvalidVaccinationCertificateException.kt
@@ -11,15 +11,15 @@ class InvalidVaccinationCertificateException(
     cause: Throwable? = null,
 ) : InvalidHealthCertificateException(errorCode, cause) {
     override fun toHumanReadableError(context: Context): HumanReadableError {
-        var errorCodeString = errorCode.toString()
-        errorCodeString = if (errorCodeString.startsWith(PREFIX_VC)) errorCodeString else PREFIX_VC + errorCodeString
         return HumanReadableError(
-            description = errorMessage.get(context) + " ($errorCodeString)"
+            description = errorMessage.get(context) + " ($PREFIX$errorCode)"
         )
     }
 
-    val showFaqButton: Boolean
+    override val showFaqButton: Boolean
         get() = errorCode in codesVcInvalid
+    override val faqButtonText: Int = R.string.error_button_vc_faq
+    override val faqLink: Int = R.string.error_button_vc_faq_link
 
     private val codesVcInvalid = listOf(
         ErrorCode.HC_BASE45_DECODING_FAILED,
@@ -27,7 +27,7 @@ class InvalidVaccinationCertificateException(
         ErrorCode.HC_COSE_MESSAGE_INVALID,
         ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED,
         ErrorCode.HC_COSE_TAG_INVALID,
-        ErrorCode.VC_PREFIX_INVALID,
+        ErrorCode.PREFIX_INVALID,
         ErrorCode.HC_CWT_NO_DGC,
         ErrorCode.HC_CWT_NO_EXP,
         ErrorCode.HC_CWT_NO_HCERT,
@@ -41,34 +41,24 @@ class InvalidVaccinationCertificateException(
                 context.getString(ERROR_MESSAGE_VC_INVALID)
             }
 
-            ErrorCode.VC_NO_VACCINATION_ENTRY -> CachedString { context ->
+            ErrorCode.NO_VACCINATION_ENTRY -> CachedString { context ->
                 context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED)
             }
 
-            ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES -> CachedString { context ->
+            ErrorCode.MULTIPLE_VACCINATION_ENTRIES -> CachedString { context ->
                 context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED)
             }
 
-            ErrorCode.VC_STORING_FAILED -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN)
-            }
-
-            ErrorCode.VC_NAME_MISMATCH,
-            ErrorCode.VC_DOB_MISMATCH -> CachedString { context ->
+            ErrorCode.NAME_MISMATCH,
+            ErrorCode.DOB_MISMATCH -> CachedString { context ->
                 context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON)
             }
-
-            ErrorCode.VC_ALREADY_REGISTERED -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_ALREADY_REGISTERED)
-            }
             else -> super.errorMessage
         }
 }
 
-private const val PREFIX_VC = "VC_"
+private const val PREFIX = "VC_"
 
 private const val ERROR_MESSAGE_VC_INVALID = R.string.error_vc_invalid
 private const val ERROR_MESSAGE_VC_NOT_YET_SUPPORTED = R.string.error_vc_not_yet_supported
-private const val ERROR_MESSAGE_VC_SCAN_AGAIN = R.string.error_vc_scan_again
 private const val ERROR_MESSAGE_VC_DIFFERENT_PERSON = R.string.error_vc_different_person
-private const val ERROR_MESSAGE_VC_ALREADY_REGISTERED = R.string.error_vc_already_registered
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt
index 9d1527dd0..0fd67dd25 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/common/qrcode/DccQrCode.kt
@@ -1,16 +1,15 @@
 package de.rki.coronawarnapp.covidcertificate.common.qrcode
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1
 
-interface DccQrCode<DccT : Dcc<*>> {
+interface DccQrCode {
     val qrCode: QrCodeString
-    val data: DccData<DccT>
+    val data: DccData<out DccV1.MetaData>
 
     val personIdentifier: CertificatePersonIdentifier
         get() = data.certificate.personIdentifier
 
     val uniqueCertificateIdentifier: String
-        get() = data.certificate.payload.uniqueCertificateIdentifier
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt
index acee0e0a8..f8060e58c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/person/core/PersonCertificatesProvider.kt
@@ -90,9 +90,7 @@ class PersonCertificatesProvider @Inject constructor(
                 get() = "testNameAndManufacturer"
             override val sampleCollectedAt: Instant
                 get() = Instant.now()
-            override val testResultAt: Instant?
-                get() = Instant.now()
-            override val testCenter: String
+            override val testCenter: String?
                 get() = "testCenter"
             override val registeredAt: Instant
                 get() = Instant.now()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
index f3e9f6ba8..4f9aab5a4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/RecoveryCertificateRepository.kt
@@ -1,7 +1,7 @@
 package de.rki.coronawarnapp.covidcertificate.recovery.core
 
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateContainer
 import de.rki.coronawarnapp.covidcertificate.recovery.core.storage.RecoveryCertificateIdentifier
 import de.rki.coronawarnapp.covidcertificate.valueset.ValueSetsRepository
@@ -18,7 +18,7 @@ import javax.inject.Singleton
 class RecoveryCertificateRepository @Inject constructor(
     @AppScope private val appScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
-    private val qrCodeExtractor: RecoveryCertificateQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
     valueSetsRepository: ValueSetsRepository,
 ) {
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt
deleted file mode 100644
index 5118c4384..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccParser.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.recovery.core.certificate
-
-import com.google.gson.Gson
-import com.upokecenter.cbor.CBORObject
-import dagger.Reusable
-import de.rki.coronawarnapp.util.serialization.BaseGson
-import javax.inject.Inject
-
-@Reusable
-class RecoveryDccParser @Inject constructor(
-    @BaseGson private val gson: Gson,
-) {
-    fun parse(map: CBORObject): RecoveryDccV1 = throw NotImplementedError()
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt
deleted file mode 100644
index 0038ed3a7..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/certificate/RecoveryDccV1.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.recovery.core.certificate
-
-import com.google.gson.annotations.SerializedName
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
-import org.joda.time.LocalDate
-
-data class RecoveryDccV1(
-    @SerializedName("ver") override val version: String,
-    @SerializedName("nam") override val nameData: Dcc.NameData,
-    @SerializedName("dob") override val dob: String,
-    @SerializedName("t") override val payloads: List<RecoveryCertificateData>,
-) : Dcc<RecoveryDccV1.RecoveryCertificateData>() {
-
-    data class RecoveryCertificateData(
-        // Disease or agent targeted, e.g. "tg": "840539006"
-        @SerializedName("tg") override val targetId: String,
-        // Date of First Positive NAA Test Result (required) e.g. "2021-04-21"
-        @SerializedName("fr") val fr: String,
-        // Certificate Valid From (required) e.g. "2021-05-01"
-        @SerializedName("df") val df: String,
-        // Certificate Valid Until (required) e.g. "2021-10-21"
-        @SerializedName("du") val du: String,
-        // Country of Test (required)
-        @SerializedName("co") override val certificateCountry: String,
-        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
-        @SerializedName("is") override val certificateIssuer: String,
-        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
-        @SerializedName("ci") override val uniqueCertificateIdentifier: String
-    ) : Dcc.Payload {
-        val testedPositiveOn: LocalDate
-            get() = LocalDate.parse(fr)
-        val validFrom: LocalDate
-            get() = LocalDate.parse(df)
-        val validUntil: LocalDate
-            get() = LocalDate.parse(du)
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt
index cb9f4174d..f51885985 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCode.kt
@@ -1,11 +1,14 @@
 package de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.RecoveryDccV1
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
-import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1
 
 data class RecoveryCertificateQRCode(
     override val qrCode: QrCodeString,
     override val data: DccData<RecoveryDccV1>,
-) : DccQrCode<RecoveryDccV1>
+) : DccQrCode {
+    override val uniqueCertificateIdentifier: String
+        get() = data.certificate.recovery.uniqueCertificateIdentifier
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt
deleted file mode 100644
index c8fccba45..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/qrcode/RecoveryCertificateQRCodeExtractor.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode
-
-import dagger.Reusable
-import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
-import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccParser
-import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1
-import javax.inject.Inject
-
-@Reusable
-class RecoveryCertificateQRCodeExtractor @Inject constructor(
-    private val coseDecoder: DccCoseDecoder,
-    private val headerParser: DccHeaderParser,
-    private val bodyParser: RecoveryDccParser,
-) {
-
-    /**
-     * May throw an **[InvalidTestCertificateException]**
-     */
-    fun extract(qrCode: String) = RecoveryCertificateQRCode(
-        data = qrCode.extract(),
-        qrCode = qrCode
-    )
-
-    private fun String.extract(): DccData<RecoveryDccV1> = throw NotImplementedError()
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
index 65209c919..fc7326f6f 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/recovery/core/storage/RecoveryCertificateContainer.kt
@@ -2,10 +2,12 @@ package de.rki.coronawarnapp.covidcertificate.recovery.core.storage
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode
+import de.rki.coronawarnapp.covidcertificate.common.certificate.RecoveryDccV1
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
 import de.rki.coronawarnapp.covidcertificate.recovery.core.RecoveryCertificate
-import de.rki.coronawarnapp.covidcertificate.recovery.core.certificate.RecoveryDccV1
-import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.recovery.core.qrcode.RecoveryCertificateQRCode
 import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -13,17 +15,24 @@ import java.util.Locale
 
 data class RecoveryCertificateContainer(
     internal val data: StoredRecoveryCertificateData,
-    private val qrCodeExtractor: RecoveryCertificateQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
     val isUpdatingData: Boolean = false,
 ) : StoredRecoveryCertificate by data {
 
     @delegate:Transient
     private val certificateData: DccData<RecoveryDccV1> by lazy {
-        data.recoveryCertificateQrCode!!.let { qrCodeExtractor.extract(it).data }
+        data.recoveryCertificateQrCode!!.let {
+            (
+                qrCodeExtractor.extract(
+                    it,
+                    mode = Mode.CERT_REC_STRICT
+                ) as RecoveryCertificateQRCode
+                ).data
+        }
     }
 
     val certificateId: String
-        get() = certificateData.certificate.payload.uniqueCertificateIdentifier
+        get() = certificateData.certificate.recovery.uniqueCertificateIdentifier
 
     fun toRecoveryCertificate(
         valueSet: TestCertificateValueSets?,
@@ -31,7 +40,7 @@ data class RecoveryCertificateContainer(
     ): RecoveryCertificate {
         val header = certificateData.header
         val certificate = certificateData.certificate
-        val recoveryCertificate = certificate.payload
+        val recoveryCertificate = certificate.recovery
 
         return object : RecoveryCertificate {
             override val personIdentifier: CertificatePersonIdentifier
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt
index 82da1f8c4..aa7b70f2b 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificate.kt
@@ -22,8 +22,7 @@ interface TestCertificate : CwaCovidCertificate {
      */
     val testNameAndManufacturer: String?
     val sampleCollectedAt: Instant
-    val testResultAt: Instant?
-    val testCenter: String
+    val testCenter: String?
     val registeredAt: Instant
     val isUpdatingData: Boolean
     val isCertificateRetrievalPending: Boolean
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt
index 658d7b433..955f493fb 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/TestCertificateRepository.kt
@@ -2,8 +2,9 @@ package de.rki.coronawarnapp.covidcertificate.test.core
 
 import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer
@@ -36,7 +37,7 @@ class TestCertificateRepository @Inject constructor(
     @AppScope private val appScope: CoroutineScope,
     private val dispatcherProvider: DispatcherProvider,
     private val storage: TestCertificateStorage,
-    private val qrCodeExtractor: TestCertificateQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
     private val processor: TestCertificateProcessor,
     valueSetsRepository: ValueSetsRepository,
 ) {
@@ -187,7 +188,7 @@ class TestCertificateRepository @Inject constructor(
                     RefreshResult(
                         cert,
                         TestCertificateServerException(
-                            TestCertificateServerException.ErrorCode.DCC_NOT_SUPPORTED_BY_LAB
+                            DCC_NOT_SUPPORTED_BY_LAB
                         )
                     )
                 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt
deleted file mode 100644
index f30135698..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccParser.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.test.core.certificate
-
-import com.google.gson.Gson
-import com.upokecenter.cbor.CBORObject
-import dagger.Reusable
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
-import de.rki.coronawarnapp.util.serialization.BaseGson
-import de.rki.coronawarnapp.util.serialization.fromJson
-import timber.log.Timber
-import javax.inject.Inject
-
-@Reusable
-class TestDccParser @Inject constructor(
-    @BaseGson private val gson: Gson,
-) {
-    fun parse(map: CBORObject): TestDccV1 = try {
-        map[keyHCert]?.run {
-            this[keyEuDgcV1]?.run {
-                toCertificate()
-            } ?: throw InvalidTestCertificateException(HC_CWT_NO_DGC)
-        } ?: throw InvalidTestCertificateException(HC_CWT_NO_HCERT)
-    } catch (e: InvalidTestCertificateException) {
-        throw e
-    } catch (e: Throwable) {
-        throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED)
-    }
-
-    @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
-    private fun TestDccV1.validate(): TestDccV1 {
-        if (payloads.isNullOrEmpty()) {
-            throw InvalidTestCertificateException(NO_TEST_ENTRY)
-        }
-        // check for non null (Gson does not enforce it) & force date parsing
-        require(version.isNotBlank())
-        require(nameData.familyNameStandardized.isNotBlank())
-        dateOfBirth
-        payload.let {
-            it.testResultAt
-            it.sampleCollectedAt
-            require(it.certificateIssuer.isNotBlank())
-            require(it.certificateCountry.isNotBlank())
-            require(it.targetId.isNotBlank())
-            require(it.testCenter.isNotBlank())
-            require(it.testResult.isNotBlank())
-            require(it.testType.isNotBlank())
-        }
-        return this
-    }
-
-    private fun CBORObject.toCertificate() = try {
-        val json = ToJSONString()
-        gson.fromJson<TestDccV1>(json).validate()
-    } catch (e: InvalidTestCertificateException) {
-        throw e
-    } catch (e: Throwable) {
-        Timber.e(e)
-        throw InvalidTestCertificateException(JSON_SCHEMA_INVALID)
-    }
-
-    companion object {
-        private val keyEuDgcV1 = CBORObject.FromObject(1)
-        private val keyHCert = CBORObject.FromObject(-260)
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt
deleted file mode 100644
index 3ebb7e072..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/certificate/TestDccV1.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.test.core.certificate
-
-import com.google.gson.annotations.SerializedName
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
-import org.joda.time.Instant
-
-data class TestDccV1(
-    @SerializedName("ver") override val version: String,
-    @SerializedName("nam") override val nameData: NameData,
-    @SerializedName("dob") override val dob: String,
-    @SerializedName("t") override val payloads: List<TestCertificateData>,
-) : Dcc<TestDccV1.TestCertificateData>() {
-
-    data class TestCertificateData(
-        // Disease or agent targeted, e.g. "tg": "840539006"
-        @SerializedName("tg") override val targetId: String,
-        // Type of Test (required) eg "LP217198-3"
-        @SerializedName("tt") val testType: String,
-        // Test Result (required) e. g. "tr": "260415000"
-        @SerializedName("tr") val testResult: String,
-        // NAA Test Name (only for PCR tests, but not required) "nm": "Roche LightCycler qPCR",
-        @SerializedName("nm") val testName: String? = null,
-        // RAT Test name and manufacturer (only for RAT tests, but not required)
-        @SerializedName("ma") val testNameAndManufactor: String? = null,
-        // Date/Time of Sample Collection (required) "sc": "2021-04-13T14:20:00+00:00"
-        @SerializedName("sc") val sc: String,
-        // Date/Time of Test Result "dr": "2021-04-13T14:40:01+00:00",
-        @SerializedName("dr") val dr: String? = null,
-        // Testing Center (required) "tc": "GGD Fryslân, L-Heliconweg",
-        @SerializedName("tc") val testCenter: String,
-        // Country of Test (required)
-        @SerializedName("co") override val certificateCountry: String,
-        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
-        @SerializedName("is") override val certificateIssuer: String,
-        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
-        @SerializedName("ci") override val uniqueCertificateIdentifier: String
-    ) : Payload {
-
-        val testResultAt: Instant?
-            get() = dr?.let { Instant.parse(it) }
-
-        val sampleCollectedAt: Instant
-            get() = Instant.parse(sc)
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt
index 5e2063068..8fe8102b7 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCode.kt
@@ -1,11 +1,14 @@
 package de.rki.coronawarnapp.covidcertificate.test.core.qrcode
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.TestDccV1
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1
 
 data class TestCertificateQRCode(
     override val qrCode: QrCodeString,
     override val data: DccData<TestDccV1>,
-) : DccQrCode<TestDccV1>
+) : DccQrCode {
+    override val uniqueCertificateIdentifier: String
+        get() = data.certificate.test.uniqueCertificateIdentifier
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt
deleted file mode 100644
index d466e3636..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractor.kt
+++ /dev/null
@@ -1,133 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.test.core.qrcode
-
-import com.upokecenter.cbor.CBORObject
-import dagger.Reusable
-import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser
-import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1
-import de.rki.coronawarnapp.util.compression.deflate
-import de.rki.coronawarnapp.util.compression.inflate
-import de.rki.coronawarnapp.util.encoding.Base45Decoder
-import timber.log.Timber
-import javax.inject.Inject
-
-@Reusable
-class TestCertificateQRCodeExtractor @Inject constructor(
-    private val coseDecoder: DccCoseDecoder,
-    private val headerParser: DccHeaderParser,
-    private val bodyParser: TestDccParser,
-) {
-
-    /**
-     * May throw an **[InvalidTestCertificateException]**
-     */
-    fun extract(
-        decryptionKey: ByteArray,
-        rawCoseObjectEncrypted: ByteArray,
-    ): TestCertificateQRCode {
-        val rawCoseObject = rawCoseObjectEncrypted.decrypt(decryptionKey)
-        return TestCertificateQRCode(
-            data = rawCoseObject.decode(),
-            qrCode = rawCoseObject.encode()
-        )
-    }
-
-    /**
-     * May throw an **[InvalidTestCertificateException]**
-     */
-    fun extract(qrCode: String) = TestCertificateQRCode(
-        data = qrCode.extract(),
-        qrCode = qrCode
-    )
-
-    private fun RawCOSEObject.decrypt(decryptionKey: ByteArray): RawCOSEObject = try {
-        coseDecoder.decryptMessage(
-            input = this,
-            decryptionKey = decryptionKey
-        )
-    } catch (e: InvalidHealthCertificateException) {
-        throw InvalidTestCertificateException(e.errorCode)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_COSE_MESSAGE_INVALID.toString())
-        throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID)
-    }
-
-    private fun String.extract(): DccData<TestDccV1> =
-        removePrefix(PREFIX)
-            .decodeBase45()
-            .decompress()
-            .decode()
-
-    private fun RawCOSEObject.encode(): String {
-        return PREFIX + compress().encodeBase45()
-    }
-
-    private fun RawCOSEObject.decode(): DccData<TestDccV1> = try {
-        coseDecoder.decode(this).parse()
-    } catch (e: InvalidHealthCertificateException) {
-        throw InvalidTestCertificateException(e.errorCode)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_COSE_MESSAGE_INVALID.toString())
-        throw InvalidTestCertificateException(HC_COSE_MESSAGE_INVALID)
-    }
-
-    private fun CBORObject.parse(): DccData<TestDccV1> = try {
-        DccData(
-            header = headerParser.parse(this),
-            certificate = bodyParser.parse(this)
-        ).also {
-            Timber.v("Parsed test certificate for %s", it.certificate.nameData.givenNameStandardized)
-        }
-    } catch (e: InvalidHealthCertificateException) {
-        throw InvalidTestCertificateException(e.errorCode)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_CBOR_DECODING_FAILED.toString())
-        throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED)
-    }
-
-    private fun String.decodeBase45(): ByteArray = try {
-        Base45Decoder.decode(this)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_BASE45_DECODING_FAILED.toString())
-        throw InvalidTestCertificateException(HC_BASE45_DECODING_FAILED)
-    }
-
-    private fun ByteArray.encodeBase45(): String = try {
-        Base45Decoder.encode(this)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_BASE45_ENCODING_FAILED.toString())
-        throw InvalidTestCertificateException(HC_BASE45_ENCODING_FAILED)
-    }
-
-    private fun RawCOSEObject.compress(): ByteArray = try {
-        this.deflate()
-    } catch (e: Throwable) {
-        Timber.e(e, HC_ZLIB_COMPRESSION_FAILED.toString())
-        throw InvalidTestCertificateException(HC_ZLIB_COMPRESSION_FAILED)
-    }
-
-    private fun ByteArray.decompress(): RawCOSEObject = try {
-        this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT)
-    } catch (e: Throwable) {
-        Timber.e(e, HC_ZLIB_DECOMPRESSION_FAILED.toString())
-        throw InvalidTestCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
-    }
-
-    companion object {
-        private const val PREFIX = "HC1:"
-
-        // Zip bomb
-        private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt
index 216b2e326..3afa51c3c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateContainer.kt
@@ -1,11 +1,11 @@
 package de.rki.coronawarnapp.covidcertificate.test.core.storage
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
-import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
 import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificate
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccV1
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode
 import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.TestCertificateValueSets
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -13,13 +13,18 @@ import java.util.Locale
 
 data class TestCertificateContainer(
     internal val data: StoredTestCertificateData,
-    private val qrCodeExtractor: TestCertificateQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
     val isUpdatingData: Boolean = false,
 ) : StoredTestCertificateData by data {
 
     @delegate:Transient
-    private val certificateData: DccData<TestDccV1> by lazy {
-        data.testCertificateQrCode!!.let { qrCodeExtractor.extract(it).data }
+    private val testCertificateQRCode: TestCertificateQRCode by lazy {
+        data.testCertificateQrCode!!.let {
+            qrCodeExtractor.extract(
+                it,
+                DccV1Parser.Mode.CERT_TEST_STRICT
+            ) as TestCertificateQRCode
+        }
     }
 
     val isPublicKeyRegistered: Boolean
@@ -31,7 +36,7 @@ data class TestCertificateContainer(
     val certificateId: String?
         get() {
             if (isCertificateRetrievalPending) return null
-            return certificateData.certificate.payload.uniqueCertificateIdentifier
+            return testCertificateQRCode.uniqueCertificateIdentifier
         }
 
     fun toTestCertificate(
@@ -40,9 +45,9 @@ data class TestCertificateContainer(
     ): TestCertificate? {
         if (isCertificateRetrievalPending) return null
 
-        val header = certificateData.header
-        val certificate = certificateData.certificate
-        val testCertificate = certificate.payload
+        val header = testCertificateQRCode.data.header
+        val certificate = testCertificateQRCode.data.certificate
+        val testCertificate = certificate.test
 
         return object : TestCertificate {
             override val personIdentifier: CertificatePersonIdentifier
@@ -72,9 +77,7 @@ data class TestCertificateContainer(
                 get() = testCertificate.testNameAndManufactor?.let { valueSet?.getDisplayText(it) ?: it }
             override val sampleCollectedAt: Instant
                 get() = testCertificate.sampleCollectedAt
-            override val testResultAt: Instant?
-                get() = testCertificate.testResultAt
-            override val testCenter: String
+            override val testCenter: String?
                 get() = testCertificate.testCenter
 
             override val isUpdatingData: Boolean
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt
index f929e89fb..fe9960c7c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/core/storage/TestCertificateProcessor.kt
@@ -3,10 +3,10 @@ package de.rki.coronawarnapp.covidcertificate.test.core.storage
 import dagger.Reusable
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.exception.TestCertificateServerException
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents
 import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer
 import de.rki.coronawarnapp.util.TimeStamper
@@ -26,7 +26,7 @@ class TestCertificateProcessor @Inject constructor(
     private val rsaKeyPairGenerator: RSAKeyPairGenerator,
     private val rsaCryptography: RSACryptography,
     private val appConfigProvider: AppConfigProvider,
-    private val qrCodeExtractor: TestCertificateQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
 ) {
 
     /**
@@ -131,7 +131,7 @@ class TestCertificateProcessor @Inject constructor(
             throw InvalidTestCertificateException(InvalidHealthCertificateException.ErrorCode.RSA_DECRYPTION_FAILED)
         }
 
-        val extractedData = qrCodeExtractor.extract(
+        val extractedData = qrCodeExtractor.extractEncrypted(
             decryptionKey = encryptionKey.toByteArray(),
             rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray()
         )
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt
deleted file mode 100644
index 5a163335c..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate
-
-import com.google.gson.annotations.SerializedName
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
-import de.rki.coronawarnapp.covidcertificate.common.certificate.toLocalDateLeniently
-import org.joda.time.LocalDate
-
-data class VaccinationDccV1(
-    @SerializedName("ver") override val version: String,
-    @SerializedName("nam") override val nameData: Dcc.NameData,
-    @SerializedName("dob") override val dob: String,
-    @SerializedName("v") override val payloads: List<VaccinationData>,
-) : Dcc<VaccinationDccV1.VaccinationData>() {
-
-    data class VaccinationData(
-        // Disease or agent targeted, e.g. "tg": "840539006"
-        @SerializedName("tg") override val targetId: String,
-        // Vaccine or prophylaxis, e.g. "vp": "1119349007"
-        @SerializedName("vp") val vaccineId: String,
-        // Vaccine medicinal product,e.g. "mp": "EU/1/20/1528",
-        @SerializedName("mp") val medicalProductId: String,
-        // Marketing Authorization Holder, e.g. "ma": "ORG-100030215",
-        @SerializedName("ma") val marketAuthorizationHolderId: String,
-        // Dose Number, e.g. "dn": 2
-        @SerializedName("dn") val doseNumber: Int,
-        // Total Series of Doses, e.g. "sd": 2,
-        @SerializedName("sd") val totalSeriesOfDoses: Int,
-        // Date of Vaccination, e.g. "dt" : "2021-04-21"
-        @SerializedName("dt") val dt: String,
-        // Country of Vaccination, e.g. "co": "NL"
-        @SerializedName("co") override val certificateCountry: String,
-        // Certificate Issuer, e.g. "is": "Ministry of Public Health, Welfare and Sport",
-        @SerializedName("is") override val certificateIssuer: String,
-        // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
-        @SerializedName("ci") override val uniqueCertificateIdentifier: String
-    ) : Payload {
-        // Can't use lazy because GSON will NULL it, as we have no no-args constructor
-        private var vaccinatedAtCache: LocalDate? = null
-        val vaccinatedAt: LocalDate
-            get() = vaccinatedAtCache ?: dt.toLocalDateLeniently().also { vaccinatedAtCache = it }
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt
deleted file mode 100644
index 725d2082d..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/certificate/VaccinationDccV1Parser.kt
+++ /dev/null
@@ -1,77 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate
-
-import com.google.gson.Gson
-import com.upokecenter.cbor.CBORObject
-import dagger.Reusable
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
-import de.rki.coronawarnapp.util.serialization.BaseGson
-import de.rki.coronawarnapp.util.serialization.fromJson
-import timber.log.Timber
-import javax.inject.Inject
-
-@Reusable
-class VaccinationDccV1Parser @Inject constructor(
-    @BaseGson private val gson: Gson
-) {
-
-    fun parse(map: CBORObject, lenient: Boolean): VaccinationDccV1 = try {
-        map[keyHCert]?.run {
-            this[keyEuDgcV1]?.run {
-                this.toCertificate(lenient = lenient)
-            } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_DGC)
-        } ?: throw InvalidVaccinationCertificateException(ErrorCode.HC_CWT_NO_HCERT)
-    } catch (e: InvalidHealthCertificateException) {
-        throw e
-    } catch (e: Throwable) {
-        throw InvalidVaccinationCertificateException(ErrorCode.HC_CBOR_DECODING_FAILED, cause = e)
-    }
-
-    @Suppress("UNNECESSARY_NOT_NULL_ASSERTION")
-    private fun VaccinationDccV1.toValidated(lenient: Boolean): VaccinationDccV1 = this
-        .run {
-            if (payloads.isEmpty()) throw InvalidVaccinationCertificateException(ErrorCode.VC_NO_VACCINATION_ENTRY)
-
-            if (payloads.size == 1) return@run this
-
-            if (lenient) {
-                Timber.w("Lenient: Vaccination data contained multiple entries.")
-                copy(payloads = listOf(payloads.maxByOrNull { it.vaccinatedAt }!!))
-            } else {
-                throw InvalidVaccinationCertificateException(ErrorCode.VC_MULTIPLE_VACCINATION_ENTRIES)
-            }
-        }
-        .apply {
-            // Apply otherwise we risk accidentally accessing the original obj in the outer scope
-            // Force date parsing
-            // check for non null (Gson does not enforce it) & force date parsing
-            require(version.isNotBlank())
-            require(nameData.familyNameStandardized.isNotBlank())
-            dateOfBirth
-            payload.let {
-                it.vaccinatedAt
-                require(it.certificateIssuer.isNotBlank())
-                require(it.certificateCountry.isNotBlank())
-                require(it.marketAuthorizationHolderId.isNotBlank())
-                require(it.medicalProductId.isNotBlank())
-                require(it.targetId.isNotBlank())
-                require(it.doseNumber > 0)
-                require(it.totalSeriesOfDoses > 0)
-            }
-        }
-
-    private fun CBORObject.toCertificate(lenient: Boolean): VaccinationDccV1 = try {
-        val json = ToJSONString()
-        gson.fromJson<VaccinationDccV1>(json).toValidated(lenient = lenient)
-    } catch (e: InvalidVaccinationCertificateException) {
-        throw e
-    } catch (e: Throwable) {
-        throw InvalidVaccinationCertificateException(ErrorCode.JSON_SCHEMA_INVALID)
-    }
-
-    companion object {
-        private val keyEuDgcV1 = CBORObject.FromObject(1)
-        private val keyHCert = CBORObject.FromObject(-260)
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt
new file mode 100644
index 000000000..475d6ee81
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidator.kt
@@ -0,0 +1,30 @@
+package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
+
+import dagger.Reusable
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.PREFIX_INVALID
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
+import timber.log.Timber
+import javax.inject.Inject
+
+@Reusable
+class DccQrCodeValidator @Inject constructor(
+    dccQrCodeExtractor: DccQrCodeExtractor
+) {
+    private val extractors = setOf(dccQrCodeExtractor)
+
+    fun validate(rawString: String): DccQrCode {
+        // If there is more than one "extractor" in the future, check censoring again.
+        // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString)
+        return findExtractor(rawString)
+            ?.extract(rawString, mode = DccV1Parser.Mode.CERT_SINGLE_STRICT)
+            ?.also { Timber.i("Extracted data from QR code is %s", it) }
+            ?: throw InvalidVaccinationCertificateException(PREFIX_INVALID)
+    }
+
+    private fun findExtractor(rawString: String): DccQrCodeExtractor? {
+        return extractors.find { it.canHandle(rawString) }
+    }
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
index ad88fb591..a84cabaaf 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationCertificateQRCode.kt
@@ -1,11 +1,14 @@
 package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
+import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
 
 data class VaccinationCertificateQRCode(
     override val qrCode: QrCodeString,
     override val data: DccData<VaccinationDccV1>
-) : DccQrCode<VaccinationDccV1>
+) : DccQrCode {
+    override val uniqueCertificateIdentifier: String
+        get() = data.certificate.vaccination.uniqueCertificateIdentifier
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
deleted file mode 100644
index 99fd3123e..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
+++ /dev/null
@@ -1,83 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
-
-import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
-import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder
-import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser
-import de.rki.coronawarnapp.covidcertificate.common.decoder.RawCOSEObject
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1Parser
-import de.rki.coronawarnapp.util.compression.inflate
-import de.rki.coronawarnapp.util.encoding.Base45Decoder
-import timber.log.Timber
-import javax.inject.Inject
-
-class VaccinationQRCodeExtractor @Inject constructor(
-    private val coseDecoder: DccCoseDecoder,
-    private val headerParser: DccHeaderParser,
-    private val bodyParser: VaccinationDccV1Parser,
-) : QrCodeExtractor<VaccinationCertificateQRCode> {
-
-    override fun canHandle(rawString: String): Boolean = rawString.startsWith(PREFIX)
-
-    override fun extract(rawString: String, mode: QrCodeExtractor.Mode): VaccinationCertificateQRCode {
-        CertificateQrCodeCensor.addQRCodeStringToCensor(rawString)
-
-        val parsedData = rawString
-            .removePrefix(PREFIX)
-            .decodeBase45()
-            .decompress()
-            .parse(lenient = mode == QrCodeExtractor.Mode.CERT_VAC_LENIENT)
-
-        return VaccinationCertificateQRCode(
-            qrCode = rawString,
-            data = parsedData,
-        )
-    }
-
-    private fun String.decodeBase45(): ByteArray = try {
-        Base45Decoder.decode(this)
-    } catch (e: Throwable) {
-        Timber.e(e)
-        throw InvalidVaccinationCertificateException(HC_BASE45_DECODING_FAILED)
-    }
-
-    private fun ByteArray.decompress(): RawCOSEObject = try {
-        this.inflate(sizeLimit = DEFAULT_SIZE_LIMIT)
-    } catch (e: Throwable) {
-        Timber.e(e)
-        throw InvalidVaccinationCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
-    }
-
-    fun RawCOSEObject.parse(lenient: Boolean): DccData<VaccinationDccV1> = try {
-        Timber.v("Parsing COSE for vaccination certificate.")
-        val cbor = coseDecoder.decode(this)
-
-        DccData(
-            header = headerParser.parse(cbor),
-            certificate = bodyParser.parse(cbor, lenient = lenient)
-        ).also {
-            CertificateQrCodeCensor.addCertificateToCensor(it)
-        }.also {
-            Timber.v("Parsed vaccination certificate for %s", it.certificate.nameData.familyNameStandardized)
-        }
-    } catch (e: InvalidHealthCertificateException) {
-        throw InvalidVaccinationCertificateException(e.errorCode)
-    } catch (e: Throwable) {
-        Timber.e(e)
-        throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED)
-    }
-
-    companion object {
-        private const val PREFIX = "HC1:"
-
-        // Zip bomb
-        private const val DEFAULT_SIZE_LIMIT = 1024L * 1024 * 10L // 10 MB
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
deleted file mode 100644
index 57c93cbf5..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
-
-import dagger.Reusable
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
-import timber.log.Timber
-import javax.inject.Inject
-
-@Reusable
-class VaccinationQRCodeValidator @Inject constructor(
-    vaccinationQRCodeExtractor: VaccinationQRCodeExtractor
-) {
-    private val extractors = setOf(vaccinationQRCodeExtractor)
-
-    fun validate(rawString: String): VaccinationCertificateQRCode {
-        // If there is more than one "extractor" in the future, check censoring again.
-        // CertificateQrCodeCensor.addQRCodeStringToCensor(rawString)
-        return findExtractor(rawString)
-            ?.extract(rawString, mode = QrCodeExtractor.Mode.CERT_VAC_STRICT)
-            ?.also { Timber.i("Extracted data from QR code is %s", it) }
-            ?: throw InvalidVaccinationCertificateException(VC_PREFIX_INVALID)
-    }
-
-    private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? {
-        return extractors.find { it.canHandle(rawString) }
-    }
-}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt
index aba2b0b12..d611f5d55 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepository.kt
@@ -2,12 +2,12 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository
 
 import de.rki.coronawarnapp.bugreporting.reportProblem
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinatedPerson
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertificate
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.errors.VaccinationCertificateNotFoundException
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainer
@@ -38,7 +38,7 @@ class VaccinationRepository @Inject constructor(
     private val timeStamper: TimeStamper,
     private val storage: VaccinationStorage,
     valueSetsRepository: ValueSetsRepository,
-    private val vaccinationQRCodeExtractor: VaccinationQRCodeExtractor,
+    private val qrCodeExtractor: DccQrCodeExtractor,
 ) {
 
     private val internalData: HotDataFlow<Set<VaccinatedPerson>> = HotDataFlow(
@@ -95,12 +95,12 @@ class VaccinationRepository @Inject constructor(
 
             if (matchingPerson.data.vaccinations.any { it.certificateId == qrCode.uniqueCertificateIdentifier }) {
                 Timber.tag(TAG).e("Certificate is already registered: %s", qrCode.uniqueCertificateIdentifier)
-                throw InvalidVaccinationCertificateException(VC_ALREADY_REGISTERED)
+                throw InvalidVaccinationCertificateException(ALREADY_REGISTERED)
             }
 
             val newCertificate = qrCode.toVaccinationContainer(
                 scannedAt = timeStamper.nowUTC,
-                qrCodeExtractor = vaccinationQRCodeExtractor,
+                qrCodeExtractor = qrCodeExtractor,
             )
 
             val modifiedPerson = matchingPerson.copy(
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt
index ce35f0c23..4319f179c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/ContainerPostProcessor.kt
@@ -7,14 +7,14 @@ import com.google.gson.reflect.TypeToken
 import com.google.gson.stream.JsonReader
 import com.google.gson.stream.JsonWriter
 import dagger.Reusable
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import timber.log.Timber
 import java.io.IOException
 import javax.inject.Inject
 
 @Reusable
 class ContainerPostProcessor @Inject constructor(
-    private val vaccinationQrCodeExtractor: VaccinationQRCodeExtractor,
+    private val vaccinationQrCodeExtractor: DccQrCodeExtractor,
 ) : TypeAdapterFactory {
     override fun <T> create(gson: Gson, type: TypeToken<T>): TypeAdapter<T> {
         val delegate = gson.getDelegateAdapter(this, type)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt
index ca7f06e31..a2caa83cd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainer.kt
@@ -2,15 +2,16 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storag
 
 import androidx.annotation.Keep
 import com.google.gson.annotations.SerializedName
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccHeader
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
+import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1
 import de.rki.coronawarnapp.covidcertificate.common.qrcode.QrCodeString
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationCertificate
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.VaccinationValueSets
 import org.joda.time.Instant
 import org.joda.time.LocalDate
@@ -23,7 +24,7 @@ data class VaccinationContainer internal constructor(
 ) {
 
     // Either set by [ContainerPostProcessor] or via [toVaccinationContainer]
-    @Transient lateinit var qrCodeExtractor: VaccinationQRCodeExtractor
+    @Transient lateinit var qrCodeExtractor: DccQrCodeExtractor
     @Transient internal var preParsedData: DccData<VaccinationDccV1>? = null
 
     // Otherwise GSON unsafes reflection to create this class, and sets the LAZY to null
@@ -32,7 +33,13 @@ data class VaccinationContainer internal constructor(
 
     @delegate:Transient
     internal val certificateData: DccData<VaccinationDccV1> by lazy {
-        preParsedData ?: qrCodeExtractor.extract(vaccinationQrCode, mode = Mode.CERT_VAC_LENIENT).data
+        preParsedData ?: (
+            qrCodeExtractor.extract(
+                vaccinationQrCode,
+                mode = DccV1Parser.Mode.CERT_VAC_LENIENT
+            ) as VaccinationCertificateQRCode
+            )
+            .data
     }
 
     val header: DccHeader
@@ -41,8 +48,8 @@ data class VaccinationContainer internal constructor(
     val certificate: VaccinationDccV1
         get() = certificateData.certificate
 
-    val vaccination: VaccinationDccV1.VaccinationData
-        get() = certificate.payload
+    val vaccination: DccV1.VaccinationData
+        get() = certificate.vaccination
 
     val certificateId: String
         get() = vaccination.uniqueCertificateIdentifier
@@ -110,7 +117,7 @@ data class VaccinationContainer internal constructor(
 
 fun VaccinationCertificateQRCode.toVaccinationContainer(
     scannedAt: Instant,
-    qrCodeExtractor: VaccinationQRCodeExtractor,
+    qrCodeExtractor: DccQrCodeExtractor,
 ) = VaccinationContainer(
     vaccinationQrCode = this.qrCode,
     scannedAt = scannedAt,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
index 90c0de242..5fc1ecbdd 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanFragment.kt
@@ -10,7 +10,7 @@ import com.google.zxing.BarcodeFormat
 import com.journeyapps.barcodescanner.DefaultDecoderFactory
 import de.rki.coronawarnapp.R
 import de.rki.coronawarnapp.bugreporting.ui.toErrorDialogBuilder
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
 import de.rki.coronawarnapp.databinding.FragmentScanQrCodeBinding
 import de.rki.coronawarnapp.util.DialogHelper
 import de.rki.coronawarnapp.util.ExternalActionHelper.openUrl
@@ -69,9 +69,9 @@ class VaccinationQrCodeScanFragment :
             binding.qrCodeScanSpinner.hide()
             it.toErrorDialogBuilder(requireContext()).apply {
                 setOnDismissListener { popBackStack() }
-                if (it is InvalidVaccinationCertificateException && it.showFaqButton) {
-                    setNeutralButton(R.string.error_button_vc_faq) { _, _ ->
-                        openUrl(getString(R.string.error_button_vc_faq_link))
+                if (it is InvalidHealthCertificateException && it.showFaqButton) {
+                    setNeutralButton(it.faqButtonText) { _, _ ->
+                        openUrl(getString(it.faqLink))
                     }
                 }
             }.show()
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
index adbddadf7..64dfd89b0 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/vaccination/ui/scan/VaccinationQrCodeScanViewModel.kt
@@ -3,7 +3,10 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.ui.scan
 import com.journeyapps.barcodescanner.BarcodeResult
 import dagger.assisted.AssistedFactory
 import dagger.assisted.AssistedInject
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeValidator
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeValidator
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationRepository
 import de.rki.coronawarnapp.util.permission.CameraSettings
 import de.rki.coronawarnapp.util.ui.SingleLiveEvent
@@ -13,7 +16,7 @@ import timber.log.Timber
 
 class VaccinationQrCodeScanViewModel @AssistedInject constructor(
     private val cameraSettings: CameraSettings,
-    private val vaccinationQRCodeValidator: VaccinationQRCodeValidator,
+    private val vaccinationQRCodeValidator: DccQrCodeValidator,
     private val vaccinationRepository: VaccinationRepository
 ) : CWAViewModel() {
 
@@ -25,6 +28,9 @@ class VaccinationQrCodeScanViewModel @AssistedInject constructor(
         try {
             event.postValue(Event.QrCodeScanInProgress)
             val qrCode = vaccinationQRCodeValidator.validate(barcodeResult.text)
+            if (qrCode !is VaccinationCertificateQRCode) {
+                throw InvalidVaccinationCertificateException(NO_VACCINATION_ENTRY)
+            }
             val vaccinationCertificate = vaccinationRepository.registerVaccination(qrCode)
             event.postValue(Event.QrCodeScanSucceeded(vaccinationCertificate.personIdentifier.codeSHA256))
         } catch (e: Throwable) {
diff --git a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml
index 384be191b..f574d1824 100644
--- a/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-bg/vaccination_strings.xml
@@ -94,9 +94,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"Този сертификат за ваксиниране все още не се поддържа във версията на вашето приложение. Моля, актуализирайте приложението си или се свържете с горещата линия за технически проблеми от “Информация за приложението”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"Ваксинационният сертификат не може да бъде запазен на смартфона Ви. Моля, опитайте отново по-късно или се свържете с горещата линия за технически проблеми, посочена в „Информация за приложението“."</string>
+    <string name="error_dcc_scan_again">"Ваксинационният сертификат не може да бъде запазен на смартфона Ви. Моля, опитайте отново по-късно или се свържете с горещата линия за технически проблеми, посочена в „Информация за приложението“."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"Ваксинационният сертификат вече е регистриран в приложението Ви."</string>
+    <string name="error_dcc_already_registered">"Ваксинационният сертификат вече е регистриран в приложението Ви."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"Личната информация в този сертификат за ваксиниране не съответства на тази във вече регистрираните сертификати. В приложението можете да регистрирате сертификати само за едно лице."</string>
 
@@ -121,4 +121,4 @@
     <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid -->
     <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
index 4b580c2aa..15df45fa6 100644
--- a/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/vaccination_strings.xml
@@ -95,9 +95,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">Dieses Impfzertifikat wird in Ihrer App-Version noch nicht unterstützt. Bitte aktualisieren Sie Ihre App oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">Das Impfzertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string>
+    <string name="error_dcc_scan_again">Das Zertifikat konnte nicht auf Ihrem Smartphone gespeichert werden. Bitte versuchen Sie es später noch einmal oder wenden Sie sich an die technische Hotline unter „App-Informationen“.</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">Das Impfzertifikat ist bereits in Ihrer App registriert.</string>
+    <string name="error_dcc_already_registered">Das Zertifikat ist bereits in Ihrer App registriert.</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">Die persönlichen Daten dieses Impfzertifikats stimmen nicht mit denen der bereits registrierten Zertifikate überein. Sie können in der App nur Zertifikate einer Person registrieren.</string>
 
diff --git a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml
index fa39d43df..9f534b5c5 100644
--- a/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-en/vaccination_strings.xml
@@ -94,9 +94,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Information”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Information”."</string>
+    <string name="error_dcc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Information”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"The vaccination certificate is already registered in your app."</string>
+    <string name="error_dcc_already_registered">"The vaccination certificate is already registered in your app."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"The personal information of this vaccination certificate does not match that of the certificates already registered. You can only register certificates for one person in the app."</string>
 
@@ -121,4 +121,4 @@
     <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid -->
     <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml
index 58bcf8c67..596ff4268 100644
--- a/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-pl/vaccination_strings.xml
@@ -94,9 +94,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"To świadectwo szczepienia nie jest jeszcze obsługiwane w Twojej wersji aplikacji. Zaktualizuj aplikację lub skontaktuj się z infolinią techniczną dostępną w sekcji „Informacje o aplikacji”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"Świadectwo szczepienia nie mogło zostać zapisane na Twoim smartfonie. Spróbuj ponownie później lub skontaktuj się z infolinią techniczną dostępną w sekcji „Informacje o aplikacji”."</string>
+    <string name="error_dcc_scan_again">"Świadectwo szczepienia nie mogło zostać zapisane na Twoim smartfonie. Spróbuj ponownie później lub skontaktuj się z infolinią techniczną dostępną w sekcji „Informacje o aplikacji”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"Świadectwo szczepienia jest już zarejestrowane w Twojej aplikacji."</string>
+    <string name="error_dcc_already_registered">"Świadectwo szczepienia jest już zarejestrowane w Twojej aplikacji."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"Dane osobowe tego świadectwa szczepienia nie są zgodne z danymi już zarejestrowanych świadectw. W aplikacji można rejestrować świadectwa tylko dla jednej osoby."</string>
 
@@ -121,4 +121,4 @@
     <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid -->
     <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml
index a6fefa697..8a610b802 100644
--- a/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-ro/vaccination_strings.xml
@@ -94,9 +94,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"Acest certificat de vaccinare nu este încă acceptat de versiunea aplicației dvs. Actualizați-vă aplicația sau contactați hotline-ul tehnic din „Informații aplicație”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"Certificatul de vaccinare nu a putut fi salvat pe smartphone-ul dvs. Încercați din nou mai târziu sau contactați hotline-ul tehnic din „Informații aplicație”."</string>
+    <string name="error_dcc_scan_again">"Certificatul de vaccinare nu a putut fi salvat pe smartphone-ul dvs. Încercați din nou mai târziu sau contactați hotline-ul tehnic din „Informații aplicație”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"Certificatul de vaccinare este deja înregistrat în aplicația dvs."</string>
+    <string name="error_dcc_already_registered">"Certificatul de vaccinare este deja înregistrat în aplicația dvs."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"Informațiile personale din acest certificat de vaccinare nu corespund cu cele din certificatele deja înregistrate. Puteți înregistra certificate pentru o singură persoană în aplicație."</string>
 
@@ -121,4 +121,4 @@
     <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid -->
     <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml
index 111e60df3..1f9565c47 100644
--- a/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-tr/vaccination_strings.xml
@@ -94,9 +94,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"Bu aşı sertifikası henüz uygulamanızın sürümünde desteklenmiyor. Lütfen uygulamanızı güncelleyin veya “Uygulama Bilgileri” bölümünde belirtilen teknik yardım hattı ile iletişime geçin."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"Aşı sertifikası akıllı telefonunuza kaydedilemedi. Lütfen daha sonra yeniden deneyin veya “Uygulama Bilgileri” bölümünde belirtilen teknik yardım hattı ile iletişime geçin."</string>
+    <string name="error_dcc_scan_again">"Aşı sertifikası akıllı telefonunuza kaydedilemedi. Lütfen daha sonra yeniden deneyin veya “Uygulama Bilgileri” bölümünde belirtilen teknik yardım hattı ile iletişime geçin."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"Aşı sertifikası zaten uygulamanıza kaydedilmiş."</string>
+    <string name="error_dcc_already_registered">"Aşı sertifikası zaten uygulamanıza kaydedilmiş."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"Bu aşı sertifikasındaki kişisel bilgiler, kaydedilmiş sertifikalardaki bilgilerle eşleşmiyor. Uygulamaya yalnızca bir kişiye ait sertifikalar kaydedebilirsiniz."</string>
 
@@ -121,4 +121,4 @@
     <!-- XTXT: Explains user about vaccination certificate: URL, has to be "translated" into english (relevant for all languages except german) - https://www.coronawarn.app/en/faq/#vac_cert_invalid -->
     <string name="error_button_vc_faq_link">"https://www.coronawarn.app/en/faq/#vac_cert_invalid"</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
index 83b31ca56..a1ecec05d 100644
--- a/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
+++ b/Corona-Warn-App/src/main/res/values/vaccination_strings.xml
@@ -95,9 +95,9 @@
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_not_yet_supported">"This vaccination certificate is not supported in your app version yet. Please update your app or contact the technical hotline under “App Information”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Information”."</string>
+    <string name="error_dcc_scan_again">"The vaccination certificate could not be saved on your smartphone. Please try again later or contact the technical hotline under “App Information”."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
-    <string name="error_vc_already_registered">"The vaccination certificate is already registered in your app."</string>
+    <string name="error_dcc_already_registered">"The vaccination certificate is already registered in your app."</string>
     <!-- XTXT: Vaccination QR code scan error message-->
     <string name="error_vc_different_person">"The personal information of this vaccination certificate does not match that of the certificates already registered. You can only register certificates for one person in the app."</string>
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt
similarity index 65%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt
index ed3ee7d48..4f6210b46 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/CertificateQrCodeCensorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/bugreporting/censors/vaccination/DccQrCodeCensorTest.kt
@@ -1,18 +1,20 @@
 package de.rki.coronawarnapp.bugreporting.censors.vaccination
 
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
+import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1
 import io.kotest.matchers.shouldBe
 import io.mockk.MockKAnnotations
 import io.mockk.mockk
 import kotlinx.coroutines.test.runBlockingTest
+import org.joda.time.LocalDate
 import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
 
 @Suppress("MaxLineLength")
-internal class CertificateQrCodeCensorTest {
+internal class DccQrCodeCensorTest {
 
     private val testRawString =
         "HC1:6BFOXN*TS0BI\$ZD.P9UOL97O4-2HH77HRM3DSPTLRR+%3.ZH9M9ESIGUBA KWML/O6HXK 0D+4O5VC9:BPCNYKMXEE1JAA/CZIK0JK1WL260X638J3-E3GG396B-43FZT-43:S0X37*ZV+FNI6HXY0ZSVILVQJF//05MVZJ5V.499TXY9KK9+OC+G9QJPNF67J6QW67KQY466PPM4MLJE+.PDB9L6Q2+PFQ5DB96PP5/P-59A%N+892 7J235II3NJ7PK7SLQMIJSBHVA7UJQWT.+S+ND%%M%331BH.IA.C8KRDL4O54O4IGUJKJGI0JAXD15IAXMFU*GSHGHD63DAOC9JU0H11+*4.\$S6ZC0JBZAB-C3QHISKE MCAOI8%M3V96-PY\$N6XOWLIBPIAYU:*JIRHUF2XZQ4H9 XJ72WG1K36VF/9BL56%E8T1OEEG%5TW5A 6YO67N6UCE:WT6BT-UMM:ABJK2TMDN1:FW-%T+\$D78NDSC3%5F61NYS-P9LOE0%J/ZAY:N5L4H-H/LH:AO3FU JHG7K46IOIMT.RE%PHLA21JRI3HTC\$AH"
@@ -20,26 +22,29 @@ internal class CertificateQrCodeCensorTest {
         header = mockk(),
         certificate = VaccinationDccV1(
             version = "1",
-            nameData = Dcc.NameData(
+            nameData = DccV1.NameData(
                 familyName = "Kevin",
                 familyNameStandardized = "KEVIN",
                 givenName = "Bob",
                 givenNameStandardized = "BOB"
             ),
-            dob = "1969-11-16",
-            payloads = listOf(
-                VaccinationDccV1.VaccinationData(
-                    targetId = "12345",
-                    vaccineId = "1214765",
-                    medicalProductId = "aaEd/easd",
-                    marketAuthorizationHolderId = "ASD-2312",
-                    doseNumber = 2,
-                    totalSeriesOfDoses = 5,
-                    dt = "1969-04-20",
-                    certificateCountry = "DE",
-                    certificateIssuer = "Herbert",
-                    uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
-                )
+            dateOfBirth = LocalDate.parse("1969-11-16"),
+            vaccination = DccV1.VaccinationData(
+                targetId = "12345",
+                vaccineId = "1214765",
+                medicalProductId = "aaEd/easd",
+                marketAuthorizationHolderId = "ASD-2312",
+                doseNumber = 2,
+                totalSeriesOfDoses = 5,
+                dt = "1969-04-20",
+                certificateCountry = "DE",
+                certificateIssuer = "Herbert",
+                uniqueCertificateIdentifier = "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
+            ),
+            personIdentifier = CertificatePersonIdentifier(
+                dateOfBirth = LocalDate.parse("1969-11-16"),
+                lastNameStandardized = "KEVIN",
+                firstNameStandardized = "BOB"
             )
         )
     )
@@ -51,23 +56,23 @@ internal class CertificateQrCodeCensorTest {
 
     @AfterEach
     fun teardown() {
-        CertificateQrCodeCensor.clearCertificateToCensor()
-        CertificateQrCodeCensor.clearQRCodeStringToCensor()
+        DccQrCodeCensor.clearCertificateToCensor()
+        DccQrCodeCensor.clearQRCodeStringToCensor()
     }
 
-    private fun createInstance() = CertificateQrCodeCensor()
+    private fun createInstance() = DccQrCodeCensor()
 
     @Test
     fun `checkLog() should return censored LogLine`() = runBlockingTest {
-        CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString)
-        CertificateQrCodeCensor.addCertificateToCensor(testCertificateData)
+        DccQrCodeCensor.addQRCodeStringToCensor(testRawString)
+        DccQrCodeCensor.addCertificateToCensor(testCertificateData)
 
         val censor = createInstance()
 
         val logLineToCensor = "Here comes the rawString: $testRawString of the vaccine certificate"
 
         censor.checkLog(logLineToCensor)!!
-            .compile()!!.censored shouldBe "Here comes the rawString: ########-####-####-####-########C\$AH of the vaccine certificate"
+            .compile()!!.censored shouldBe "Here comes the rawString: ###C\$AH of the vaccine certificate"
 
         val certDataToCensor = "Hello my name is Kevin Bob, i was born at 1969-11-16, i have been " +
             "vaccinated with: 12345 1214765 aaEd/easd ASD-2312 1969-04-20 DE Herbert" +
@@ -75,7 +80,7 @@ internal class CertificateQrCodeCensorTest {
 
         censor.checkLog(certDataToCensor)!!
             .compile()!!.censored shouldBe "Hello my name is nameData/familyName nameData/givenName, i was born at " +
-            "vaccinationCertificate/dateOfBirth, i have been vaccinated with: vaccinationData/targetId " +
+            "covidCertificate/dateOfBirth, i have been vaccinated with: vaccinationData/targetId " +
             "vaccinationData/vaccineId vaccinationData/medicalProductId" +
             " vaccinationData/marketAuthorizationHolderId vaccinationData/vaccinatedAt" +
             " vaccinationData/certificateCountry vaccinationData/certificateIssuer" +
@@ -93,8 +98,8 @@ internal class CertificateQrCodeCensorTest {
 
     @Test
     fun `checkLog() should return null if nothing should be censored`() = runBlockingTest {
-        CertificateQrCodeCensor.addQRCodeStringToCensor(testRawString.replace("1", "2"))
-        CertificateQrCodeCensor.addCertificateToCensor(testCertificateData)
+        DccQrCodeCensor.addQRCodeStringToCensor(testRawString.replace("1", "2"))
+        DccQrCodeCensor.addCertificateToCensor(testCertificateData)
 
         val censor = createInstance()
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt
index e5cdb3f4c..90a812bae 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/CoronaTestQrCodeValidatorTest.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.coronatest.qrcode
 
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
@@ -46,8 +45,8 @@ class CoronaTestQrCodeValidatorTest : BaseTest() {
     fun `validator uses strict extraction mode`() {
         val instance = CoronaTestQrCodeValidator(raExtractor, pcrExtractor)
         instance.validate(pcrQrCode1).type shouldBe CoronaTest.Type.PCR
-        verify { pcrExtractor.extract(pcrQrCode1, Mode.TEST_STRICT) }
+        verify { pcrExtractor.extract(pcrQrCode1) }
         instance.validate(raQrCode1).type shouldBe CoronaTest.Type.RAPID_ANTIGEN
-        verify { raExtractor.extract(raQrCode1, Mode.TEST_STRICT) }
+        verify { raExtractor.extract(raQrCode1) }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt
index 86e181271..d73a307fe 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/PcrQrCodeExtractorTest.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.coronatest.qrcode
 
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT
 import io.kotest.matchers.shouldBe
 import org.junit.Test
 import testhelpers.BaseTest
@@ -17,7 +16,7 @@ class PcrQrCodeExtractorTest : BaseTest() {
         val extractor = PcrQrCodeExtractor()
         try {
             if (extractor.canHandle("$prefixString$guid")) {
-                extractor.extract("$prefixString$guid", mode = TEST_STRICT)
+                extractor.extract("$prefixString$guid")
                 conditionToMatch shouldBe true
             } else {
                 conditionToMatch shouldBe false
@@ -80,41 +79,32 @@ class PcrQrCodeExtractorTest : BaseTest() {
     fun extractGUID() {
         PcrQrCodeExtractor().extract(
             "$localhostUpperCase$guidUpperCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidUpperCase
         PcrQrCodeExtractor().extract(
             "$localhostUpperCase$guidLowerCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidLowerCase
         PcrQrCodeExtractor().extract(
             "$localhostUpperCase$guidMixedCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidMixedCase
 
         PcrQrCodeExtractor().extract(
             "$localhostLowerCase$guidUpperCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidUpperCase
         PcrQrCodeExtractor().extract(
             "$localhostLowerCase$guidLowerCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidLowerCase
         PcrQrCodeExtractor().extract(
             "$localhostLowerCase$guidMixedCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidMixedCase
 
         PcrQrCodeExtractor().extract(
             "$localhostMixedCase$guidUpperCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidUpperCase
         PcrQrCodeExtractor().extract(
             "$localhostMixedCase$guidLowerCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidLowerCase
         PcrQrCodeExtractor().extract(
             "$localhostMixedCase$guidMixedCase",
-            mode = TEST_STRICT
         ).qrCodeGUID shouldBe guidMixedCase
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt
index 4a746b6ed..e78658b69 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/qrcode/RapidAntigenQrCodeExtractorTest.kt
@@ -1,6 +1,5 @@
 package de.rki.coronawarnapp.coronatest.qrcode
 
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode.TEST_STRICT
 import de.rki.coronawarnapp.coronatest.type.CoronaTest
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
@@ -30,13 +29,13 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() {
     @Test
     fun `extracting valid codes does not throw exception`() {
         listOf(raQrCode1, raQrCode2, raQrCode3, raQrCode4, raQrCode5, raQrCode6, raQrCode7, raQrCode8).forEach {
-            instance.extract(it, mode = TEST_STRICT)
+            instance.extract(it)
         }
     }
 
     @Test
     fun `personal data is extracted`() {
-        val data = instance.extract(raQrCode3, mode = TEST_STRICT)
+        val data = instance.extract(raQrCode3)
         data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN
         data.hash shouldBe "7dce08db0d4abd5ac1d2498b571afb221ca947c75c847d05466b4cfe9d95dc66"
         data.createdAt shouldBe Instant.ofEpochMilli(1619618352000)
@@ -47,7 +46,7 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() {
 
     @Test
     fun `empty strings are treated as null or notset`() {
-        val data = instance.extract(raQrCodeEmptyStrings, mode = TEST_STRICT)
+        val data = instance.extract(raQrCodeEmptyStrings)
         data.type shouldBe CoronaTest.Type.RAPID_ANTIGEN
         data.hash shouldBe "d6e4d0181d8109bf05b346a0d2e0ef0cc472eed70d9df8c4b9ae5c7a009f3e34"
         data.createdAt shouldBe Instant.ofEpochMilli(1619012952000)
@@ -58,14 +57,14 @@ class RapidAntigenQrCodeExtractorTest : BaseTest() {
 
     @Test
     fun `personal data is only valid if complete or completely missing`() {
-        shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData, mode = TEST_STRICT) }
+        shouldThrow<InvalidQRCodeException> { instance.extract(raQrIncompletePersonalData) }
     }
 
     @Test
     fun `invalid json throws exception`() {
         val invalidCode = "https://s.coronawarn.app/?v=1#eyJ0aW1lc3RhbXAiOjE2"
         shouldThrow<InvalidQRCodeException> {
-            RapidAntigenQrCodeExtractor().extract(invalidCode, mode = TEST_STRICT)
+            RapidAntigenQrCodeExtractor().extract(invalidCode)
         }
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt
index f325680ff..407c664a5 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/common/CertificatePersonIdentifierTest.kt
@@ -1,8 +1,8 @@
 package de.rki.coronawarnapp.covidcertificate.common
 
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.DOB_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NAME_MISMATCH
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
 import io.kotest.assertions.throwables.shouldNotThrowAny
 import io.kotest.assertions.throwables.shouldThrow
@@ -58,14 +58,14 @@ class CertificatePersonIdentifierTest : BaseTest() {
 
         shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(firstNameStandardized = "nope"))
-        }.errorCode shouldBe VC_NAME_MISMATCH
+        }.errorCode shouldBe NAME_MISMATCH
 
         shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(lastNameStandardized = "nope"))
-        }.errorCode shouldBe VC_NAME_MISMATCH
+        }.errorCode shouldBe NAME_MISMATCH
 
         shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(dateOfBirth = LocalDate.parse("1900-12-31")))
-        }.errorCode shouldBe VC_DOB_MISMATCH
+        }.errorCode shouldBe DOB_MISMATCH
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt
index 75e7d3204..c170f4869 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateRepositoryTest.kt
@@ -1,9 +1,9 @@
 package de.rki.coronawarnapp.covidcertificate.test
 
 import de.rki.coronawarnapp.appconfig.CovidCertificateConfig
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.qrcode.DccQrCode
 import de.rki.coronawarnapp.covidcertificate.test.core.TestCertificateRepository
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.StoredTestCertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateProcessor
@@ -25,7 +25,7 @@ import testhelpers.TestDispatcherProvider
 class TestCertificateRepositoryTest : BaseTest() {
 
     @MockK lateinit var storage: TestCertificateStorage
-    @MockK lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor
+    @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor
     @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate
     @MockK lateinit var valueSetsRepository: ValueSetsRepository
     @MockK lateinit var testCertificateProcessor: TestCertificateProcessor
@@ -61,7 +61,7 @@ class TestCertificateRepositoryTest : BaseTest() {
             every { storage.testCertificates } answers { storageSet }
         }
 
-        coEvery { qrCodeExtractor.extract(any(), any()) } returns mockk<TestCertificateQRCode>().apply {
+        coEvery { qrCodeExtractor.extract(any()) } returns mockk<DccQrCode>().apply {
             every { qrCode } returns "qrCode"
             every { data } returns mockk()
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt
index 3082a41b5..ae152c4d2 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateTestData.kt
@@ -1,6 +1,6 @@
 package de.rki.coronawarnapp.covidcertificate.test
 
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.RACertificateData
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.TestCertificateContainer
@@ -11,7 +11,7 @@ import javax.inject.Inject
 
 @Suppress("MaxLineLength")
 class TestCertificateTestData @Inject constructor(
-    qrCodeExtractor: TestCertificateQRCodeExtractor
+    qrCodeExtractor: DccQrCodeExtractor
 ) {
 
     val personATest1CertQRCodeString =
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt
index f9f50f5c6..128df7653 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateDccParserTest.kt
@@ -2,8 +2,8 @@ package de.rki.coronawarnapp.covidcertificate.test.core.qrcode
 
 import com.google.gson.Gson
 import com.upokecenter.cbor.CBORObject
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
 import de.rki.coronawarnapp.covidcertificate.test.TestData
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser
 import io.kotest.matchers.shouldBe
 import okio.ByteString.Companion.decodeHex
 import org.joda.time.LocalDate
@@ -11,12 +11,12 @@ import org.junit.jupiter.api.Test
 
 class TestCertificateDccParserTest {
 
-    private val bodyParser = TestDccParser(Gson())
+    private val bodyParser = DccV1Parser(Gson())
 
     @Test
     fun `happy path cose decryption with Ellen Cheng`() {
         val coseObject = CBORObject.DecodeFromBytes(TestData.cborObject.decodeHex().toByteArray())
-        with(bodyParser.parse(coseObject)) {
+        with(bodyParser.parse(coseObject, DccV1Parser.Mode.CERT_TEST_STRICT)) {
 
             with(nameData) {
                 familyName shouldBe "Musterfrau-Gößinger"
@@ -28,7 +28,7 @@ class TestCertificateDccParserTest {
             dateOfBirth shouldBe LocalDate.parse("1998-02-26")
             version shouldBe "1.2.1"
 
-            with(payloads[0]) {
+            with(tests!!.single()) {
                 uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3"
                 certificateCountry shouldBe "AT"
                 certificateIssuer shouldBe "Ministry of Health, Austria"
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt
index b823fef1e..c9ee9ee0a 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/core/qrcode/TestCertificateQRCodeExtractorTest.kt
@@ -1,13 +1,14 @@
 package de.rki.coronawarnapp.covidcertificate.test.core.qrcode
 
 import com.google.gson.Gson
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
 import de.rki.coronawarnapp.covidcertificate.common.cryptography.AesCryptography
 import de.rki.coronawarnapp.covidcertificate.common.decoder.DccCoseDecoder
 import de.rki.coronawarnapp.covidcertificate.common.decoder.DccHeaderParser
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
 import de.rki.coronawarnapp.covidcertificate.test.TestData
-import de.rki.coronawarnapp.covidcertificate.test.core.certificate.TestDccParser
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationQrCodeTestData
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
@@ -20,12 +21,12 @@ import testhelpers.BaseTest
 class TestCertificateQRCodeExtractorTest : BaseTest() {
     private val coseDecoder = DccCoseDecoder(AesCryptography())
     private val headerParser = DccHeaderParser()
-    private val bodyParser = TestDccParser(Gson())
-    private val extractor = TestCertificateQRCodeExtractor(coseDecoder, headerParser, bodyParser)
+    private val bodyParser = DccV1Parser(Gson())
+    private val extractor = DccQrCodeExtractor(coseDecoder, headerParser, bodyParser)
 
     @Test
     fun `happy path qr code`() {
-        val qrCode = extractor.extract(TestData.qrCodeTestCertificate)
+        val qrCode = extractor.extract(TestData.qrCodeTestCertificate) as TestCertificateQRCode
         with(qrCode.data.header) {
             issuer shouldBe "AT"
             issuedAt shouldBe Instant.parse("2021-06-01T10:12:48.000Z")
@@ -39,11 +40,10 @@ class TestCertificateQRCodeExtractorTest : BaseTest() {
                 givenName shouldBe "Gabriele"
                 givenNameStandardized shouldBe "GABRIELE"
             }
-            dob shouldBe "1998-02-26"
             dateOfBirth shouldBe LocalDate.parse("1998-02-26")
             version shouldBe "1.2.1"
 
-            with(payloads[0]) {
+            with(test) {
                 uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3"
                 certificateCountry shouldBe "AT"
                 certificateIssuer shouldBe "Ministry of Health, Austria"
@@ -62,7 +62,7 @@ class TestCertificateQRCodeExtractorTest : BaseTest() {
         with(TestData.EllenCheng()) {
             val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray()
             val dek = dek.decodeBase64()!!.toByteArray()
-            val result = extractor.extract(dek, coseObject)
+            val result = extractor.extractEncrypted(dek, coseObject)
             with(result.data.certificate.nameData) {
                 familyName shouldBe "Cheng"
                 givenName shouldBe "Ellen"
@@ -80,7 +80,7 @@ class TestCertificateQRCodeExtractorTest : BaseTest() {
         with(TestData.BrianCalamandrei()) {
             val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray()
             val dek = dek.decodeBase64()!!.toByteArray()
-            val result = extractor.extract(dek, coseObject)
+            val result = extractor.extractEncrypted(dek, coseObject)
             with(result.data.certificate.nameData) {
                 familyName shouldBe "Calamandrei"
                 givenName shouldBe "Brian"
@@ -95,21 +95,21 @@ class TestCertificateQRCodeExtractorTest : BaseTest() {
 
     @Test
     fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() {
-        shouldThrow<InvalidTestCertificateException> {
+        shouldThrow<InvalidHealthCertificateException> {
             extractor.extract(VaccinationQrCodeTestData.validEncoded)
         }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS
     }
 
     @Test
     fun `random string fails with HC_BASE45_DECODING_FAILED`() {
-        shouldThrow<InvalidTestCertificateException> {
+        shouldThrow<InvalidHealthCertificateException> {
             extractor.extract("nothing here to see")
         }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
     }
 
     @Test
     fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() {
-        shouldThrow<InvalidTestCertificateException> {
+        shouldThrow<InvalidHealthCertificateException> {
             extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:")
         }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
     }
@@ -117,13 +117,13 @@ class TestCertificateQRCodeExtractorTest : BaseTest() {
     @Test
     fun `vaccination certificate fails with NO_TEST_ENTRY`() {
         shouldThrow<InvalidTestCertificateException> {
-            extractor.extract(VaccinationQrCodeTestData.certificateMissing)
+            extractor.extract(VaccinationQrCodeTestData.certificateMissing, mode = DccV1Parser.Mode.CERT_TEST_STRICT)
         }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
     }
 
     @Test
     fun `null values fail with JSON_SCHEMA_INVALID`() {
-        shouldThrow<InvalidTestCertificateException> {
+        shouldThrow<InvalidHealthCertificateException> {
             extractor.extract(TestData.qrCodeMssingValues)
         }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt
index 4d018467e..83d374bb7 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/execution/TestCertificateProcessorTest.kt
@@ -3,8 +3,8 @@ package de.rki.coronawarnapp.covidcertificate.test.execution
 import de.rki.coronawarnapp.appconfig.AppConfigProvider
 import de.rki.coronawarnapp.appconfig.ConfigData
 import de.rki.coronawarnapp.appconfig.CovidCertificateConfig
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.test.core.qrcode.TestCertificateQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateComponents
 import de.rki.coronawarnapp.covidcertificate.test.core.server.TestCertificateServer
 import de.rki.coronawarnapp.covidcertificate.test.core.storage.PCRCertificateData
@@ -35,7 +35,7 @@ class TestCertificateProcessorTest : BaseTest() {
     @MockK lateinit var timeStamper: TimeStamper
     @MockK lateinit var certificateServer: TestCertificateServer
     @MockK lateinit var rsaCryptography: RSACryptography
-    @MockK lateinit var qrCodeExtractor: TestCertificateQRCodeExtractor
+    @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor
     @MockK lateinit var appConfigProvider: AppConfigProvider
     @MockK lateinit var appConfigData: ConfigData
     @MockK lateinit var covidTestCertificateConfig: CovidCertificateConfig.TestCertificate
@@ -82,7 +82,12 @@ class TestCertificateProcessorTest : BaseTest() {
 
         every { rsaCryptography.decrypt(any(), any()) } returns ByteString.Companion.EMPTY
 
-        coEvery { qrCodeExtractor.extract(any(), any()) } returns mockk<TestCertificateQRCode>().apply {
+        coEvery {
+            qrCodeExtractor.extractEncrypted(
+                any<ByteArray>(),
+                any()
+            )
+        } returns mockk<TestCertificateQRCode>().apply {
             every { qrCode } returns "qrCode"
             every { data } returns mockk()
         }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java
index a4aa6186f..54c251877 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationQrCodeTestData.java
@@ -28,3 +28,5 @@ public class VaccinationQrCodeTestData {
     // vaccination date (`dt`) with real time information
     static public String passDatesWithRealTimeInfo = "HC1:6BFS80J80T9WTWGSLKC 4759S-JXYFZJU:6MFBBF+5*70HS8FN07LCK.KWY0LACLJED97TK0F90$PC5$CUZCY$5Y$5TPCBEC7ZKW.CUEEY3EAECWGDMXG2QDUW5*MEWUMLPCG/DD3E01ALB81C9%NASB9MN951A JC6/DYOACEC+EDR/OLECMPCG/D8EDETAG+9*NAWB8JPCT3E5JDKA7Q47%964W5-A67:EDOL9WEQDD+Q6TW6FA7C466KCK9E2H9G:6V6BEM6Q$D.UDRYA 96OF6L/5SW6KB7B$D% D3IA4W5646946846.96XJC +D3KC.SCXJCQWEF83846Y969464W5K57.964G72A6646VK5XF6646WJCT3E 6A%JCXQEIN8G/D6LE ZDQZCAJB0LEE4F0ECOPCY8FHZA1+9LZAZM81G72A6 6A2G7O/5L*8U%61H8E46H%6SG8H%6M*8:G85S8D46DM8+A8BM8-Q68+8O98J41PL2X+2P4OJ1K2:M7GKVJIG%MZ1TC 8X:6UL2C88TL9T DNKP/W5Z3QZ3T/EQHU7A78$JD0TPGDVYW778PC48+ADC%9:DHWCJ55";
 }
+
+
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt
index 8bd1d7837..88126d681 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestComponent.kt
@@ -2,8 +2,8 @@ package de.rki.coronawarnapp.covidcertificate.vaccination.core
 
 import dagger.Component
 import dagger.Module
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractorTest
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQrCodeValidatorTest
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeExtractorTest
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.DccQrCodeValidatorTest
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.VaccinationRepositoryTest
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainerTest
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationStorageTest
@@ -21,10 +21,10 @@ interface VaccinationTestComponent {
 
     fun inject(testClass: VaccinationStorageTest)
     fun inject(testClass: VaccinationContainerTest)
-    fun inject(testClass: VaccinationQRCodeExtractorTest)
+    fun inject(testClass: DccQrCodeExtractorTest)
     fun inject(testClass: VaccinatedPersonTest)
     fun inject(testClass: VaccinationRepositoryTest)
-    fun inject(testClass: VaccinationQrCodeValidatorTest)
+    fun inject(testClass: DccQrCodeValidatorTest)
 
     @Component.Factory
     interface Factory {
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt
index be9583f22..4811bef61 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/VaccinationTestData.kt
@@ -1,19 +1,21 @@
 package de.rki.coronawarnapp.covidcertificate.vaccination.core
 
-import de.rki.coronawarnapp.covidcertificate.common.certificate.Dcc
+import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccData
 import de.rki.coronawarnapp.covidcertificate.common.certificate.DccHeader
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.certificate.VaccinationDccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1
+import de.rki.coronawarnapp.covidcertificate.common.certificate.VaccinationDccV1
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationContainer
 import org.joda.time.Instant
+import org.joda.time.LocalDate
 import javax.inject.Inject
 
 @Suppress("MaxLineLength")
 class VaccinationTestData @Inject constructor(
-    private var qrCodeExtractor: VaccinationQRCodeExtractor,
+    private var qrCodeExtractor: DccQrCodeExtractor,
 ) {
 
     // AndreasAstra1.pdf
@@ -22,26 +24,30 @@ class VaccinationTestData @Inject constructor(
 
     val personAVac1Certificate = VaccinationDccV1(
         version = "1.0.0",
-        nameData = Dcc.NameData(
+        nameData = DccV1.NameData(
             givenName = "Andreas",
             givenNameStandardized = "ANDREAS",
             familyName = "Astrá Eins",
             familyNameStandardized = "ASTRA<EINS",
         ),
-        dob = "1966-11-11",
-        payloads = listOf(
-            VaccinationDccV1.VaccinationData(
-                targetId = "840539006",
-                vaccineId = "1119305005",
-                medicalProductId = "EU/1/21/1529",
-                marketAuthorizationHolderId = "ORG-100001699",
-                doseNumber = 1,
-                totalSeriesOfDoses = 2,
-                dt = "2021-03-01",
-                certificateCountry = "DE",
-                certificateIssuer = "Bundesministerium für Gesundheit - Test01",
-                uniqueCertificateIdentifier = "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S",
-            )
+        dateOfBirth = LocalDate.parse("1966-11-11"),
+        vaccination =
+        DccV1.VaccinationData(
+            targetId = "840539006",
+            vaccineId = "1119305005",
+            medicalProductId = "EU/1/21/1529",
+            marketAuthorizationHolderId = "ORG-100001699",
+            doseNumber = 1,
+            totalSeriesOfDoses = 2,
+            dt = "2021-03-01",
+            certificateCountry = "DE",
+            certificateIssuer = "Bundesministerium für Gesundheit - Test01",
+            uniqueCertificateIdentifier = "01DE/00001/1119305005/7T1UG87G61Y7NRXIBQJDTYQ9#S",
+        ),
+        personIdentifier = CertificatePersonIdentifier(
+            dateOfBirth = LocalDate.parse("1966-11-11"),
+            lastNameStandardized = "ASTRA<EINS",
+            firstNameStandardized = "ANDREAS"
         )
     )
 
@@ -74,26 +80,29 @@ class VaccinationTestData @Inject constructor(
 
     val personAVac2Certificate = VaccinationDccV1(
         version = "1.0.0",
-        nameData = Dcc.NameData(
+        nameData = DccV1.NameData(
             givenName = "Andreas",
             givenNameStandardized = "ANDREAS",
             familyName = "Astrá Eins",
             familyNameStandardized = "ASTRA<EINS",
         ),
-        dob = "1966-11-11",
-        payloads = listOf(
-            VaccinationDccV1.VaccinationData(
-                targetId = "840539006",
-                vaccineId = "1119305005",
-                medicalProductId = "EU/1/21/1529",
-                marketAuthorizationHolderId = "ORG-100001699",
-                doseNumber = 2,
-                totalSeriesOfDoses = 2,
-                dt = "2021-04-27",
-                certificateCountry = "DE",
-                certificateIssuer = "Bundesministerium für Gesundheit - Test01",
-                uniqueCertificateIdentifier = "01DE/00001/1119305005/6IPYBAIDWEWRWW73QEP92FQSN#S",
-            )
+        dateOfBirth = LocalDate.parse("1966-11-11"),
+        vaccination = DccV1.VaccinationData(
+            targetId = "840539006",
+            vaccineId = "1119305005",
+            medicalProductId = "EU/1/21/1529",
+            marketAuthorizationHolderId = "ORG-100001699",
+            doseNumber = 2,
+            totalSeriesOfDoses = 2,
+            dt = "2021-04-27",
+            certificateCountry = "DE",
+            certificateIssuer = "Bundesministerium für Gesundheit - Test01",
+            uniqueCertificateIdentifier = "01DE/00001/1119305005/6IPYBAIDWEWRWW73QEP92FQSN#S",
+        ),
+        personIdentifier = CertificatePersonIdentifier(
+            dateOfBirth = LocalDate.parse("1966-11-11"),
+            lastNameStandardized = "ASTRA<EINS",
+            firstNameStandardized = "ANDREAS"
         )
     )
 
@@ -130,26 +139,29 @@ class VaccinationTestData @Inject constructor(
 
     val personBVac1Certificate = VaccinationDccV1(
         version = "1.0.0",
-        nameData = Dcc.NameData(
+        nameData = DccV1.NameData(
             givenName = "Boris",
             givenNameStandardized = "BORIS",
             familyName = "Johnson Gültig",
             familyNameStandardized = "JOHNSON<GUELTIG",
         ),
-        dob = "1966-11-11",
-        payloads = listOf(
-            VaccinationDccV1.VaccinationData(
-                targetId = "840539006",
-                vaccineId = "1119305005",
-                medicalProductId = "EU/1/20/1525",
-                marketAuthorizationHolderId = "ORG-100001417",
-                doseNumber = 1,
-                totalSeriesOfDoses = 1,
-                dt = "2021-04-20",
-                certificateCountry = "DE",
-                certificateIssuer = "Bundesministerium für Gesundheit - Test01",
-                uniqueCertificateIdentifier = "01DE/00001/1119305005/3H24U2KVOTPCSINK7N64F2OB9#S",
-            )
+        dateOfBirth = LocalDate.parse("1966-11-11"),
+        vaccination = DccV1.VaccinationData(
+            targetId = "840539006",
+            vaccineId = "1119305005",
+            medicalProductId = "EU/1/20/1525",
+            marketAuthorizationHolderId = "ORG-100001417",
+            doseNumber = 1,
+            totalSeriesOfDoses = 1,
+            dt = "2021-04-20",
+            certificateCountry = "DE",
+            certificateIssuer = "Bundesministerium für Gesundheit - Test01",
+            uniqueCertificateIdentifier = "01DE/00001/1119305005/3H24U2KVOTPCSINK7N64F2OB9#S",
+        ),
+        personIdentifier = CertificatePersonIdentifier(
+            dateOfBirth = LocalDate.parse("1966-11-11"),
+            lastNameStandardized = "JOHNSON<GUELTIG",
+            firstNameStandardized = "BORIS"
         )
     )
 
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt
similarity index 76%
rename from Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
rename to Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt
index 3b07c2bc3..fe132556f 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeExtractorTest.kt
@@ -1,11 +1,16 @@
 package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
 
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser.Mode
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_RECOVERY_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidRecoveryCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidTestCertificateException
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationQrCodeTestData
@@ -19,9 +24,9 @@ import org.junit.jupiter.api.Test
 import testhelpers.BaseTest
 import javax.inject.Inject
 
-class VaccinationQRCodeExtractorTest : BaseTest() {
+class DccQrCodeExtractorTest : BaseTest() {
 
-    @Inject lateinit var extractor: VaccinationQRCodeExtractor
+    @Inject lateinit var extractor: DccQrCodeExtractor
     @Inject lateinit var vaccinationTestData: VaccinationTestData
 
     @BeforeEach
@@ -36,12 +41,15 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
 
     @Test
     fun `happy path extraction 2`() {
-        extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2, mode = Mode.CERT_VAC_STRICT)
+        extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode2)
     }
 
     @Test
     fun `happy path extraction with data`() {
-        val qrCode = extractor.extract(VaccinationQrCodeTestData.validVaccinationQrCode3, mode = Mode.CERT_VAC_STRICT)
+        val qrCode = extractor.extract(
+            VaccinationQrCodeTestData.validVaccinationQrCode3,
+            mode = Mode.CERT_VAC_STRICT
+        ) as VaccinationCertificateQRCode
 
         with(qrCode.data.header) {
             issuer shouldBe "AT"
@@ -56,11 +64,10 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
                 givenName shouldBe "Gabriele"
                 givenNameStandardized shouldBe "GABRIELE"
             }
-            dob shouldBe "1998-02-26"
             dateOfBirth shouldBe LocalDate.parse("1998-02-26")
             version shouldBe "1.0.0"
 
-            with(payloads[0]) {
+            with(vaccination) {
                 uniqueCertificateIdentifier shouldBe "urn:uvci:01:AT:10807843F94AEE0EE5093FBC254BD813P"
                 certificateCountry shouldBe "AT"
                 doseNumber shouldBe 1
@@ -86,7 +93,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
 
     @Test
     fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() {
-        shouldThrow<InvalidVaccinationCertificateException> {
+        shouldThrow<InvalidHealthCertificateException> {
             extractor.extract(
                 VaccinationQrCodeTestData.validEncoded,
                 mode = Mode.CERT_VAC_STRICT
@@ -121,7 +128,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
                 VaccinationQrCodeTestData.certificateMissing,
                 mode = Mode.CERT_VAC_STRICT
             )
-        }.errorCode shouldBe VC_NO_VACCINATION_ENTRY
+        }.errorCode shouldBe NO_VACCINATION_ENTRY
     }
 
     @Test
@@ -167,7 +174,7 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
         val qrCode = extractor.extract(
             VaccinationQrCodeTestData.qrCodeBulgaria,
             mode = Mode.CERT_VAC_STRICT
-        )
+        ) as VaccinationCertificateQRCode
         with(qrCode.data.header) {
             issuer shouldBe "BG"
             issuedAt shouldBe Instant.parse("2021-06-02T14:07:56.000Z")
@@ -181,11 +188,10 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
                 givenName shouldBe "СТАМО ГЕОРГИЕВ"
                 givenNameStandardized shouldBe "STAMO<GEORGIEV"
             }
-            dob shouldBe "1978-01-26T00:00:00"
             dateOfBirth shouldBe LocalDate.parse("1978-01-26")
             version shouldBe "1.0.0"
 
-            payload.apply {
+            vaccination.apply {
                 uniqueCertificateIdentifier shouldBe "urn:uvci:01:BG:UFR5PLGKU8WDSZK7#0"
                 certificateCountry shouldBe "BG"
                 doseNumber shouldBe 2
@@ -243,8 +249,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
             VaccinationQrCodeTestData.passGermanReferenceCase,
             mode = Mode.CERT_VAC_STRICT
         ).apply {
+            this as VaccinationCertificateQRCode
             data.certificate.dateOfBirth shouldBe LocalDate.parse("1964-08-12")
-            data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-05-29")
+            data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-05-29")
         }
     }
 
@@ -254,8 +261,9 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
             VaccinationQrCodeTestData.passDatesWithTimeAtMidnight,
             mode = Mode.CERT_VAC_STRICT
         ).apply {
+            this as VaccinationCertificateQRCode
             data.certificate.dateOfBirth shouldBe LocalDate.parse("1978-01-26")
-            data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-03-09")
+            data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-03-09")
         }
     }
 
@@ -265,8 +273,54 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
             VaccinationQrCodeTestData.passDatesWithRealTimeInfo,
             mode = Mode.CERT_VAC_STRICT
         ).apply {
+            this as VaccinationCertificateQRCode
             data.certificate.dateOfBirth shouldBe LocalDate.parse("1958-11-11")
-            data.certificate.payload.vaccinatedAt shouldBe LocalDate.parse("2021-03-18")
+            data.certificate.vaccination.vaccinatedAt shouldBe LocalDate.parse("2021-03-18")
         }
     }
+
+    @Test
+    fun `happy path extraction recovery`() {
+        extractor.extract(
+            RecoveryQrCodeTestData.validRecovery,
+        )
+    }
+
+    @Test
+    fun `happy path extraction recovery with strict mode`() {
+        extractor.extract(
+            RecoveryQrCodeTestData.validRecovery,
+            mode = Mode.CERT_REC_STRICT
+        )
+    }
+
+    @Test
+    fun `recovery cert fails in mode CERT_VAC_STRICT`() {
+        shouldThrow<InvalidVaccinationCertificateException> {
+            extractor.extract(
+                RecoveryQrCodeTestData.validRecovery,
+                mode = Mode.CERT_VAC_STRICT
+            )
+        }.errorCode shouldBe NO_VACCINATION_ENTRY
+    }
+
+    @Test
+    fun `recovery cert fails in mode CERT_TEST_STRICT`() {
+        shouldThrow<InvalidTestCertificateException> {
+            extractor.extract(
+                RecoveryQrCodeTestData.validRecovery,
+                mode = Mode.CERT_TEST_STRICT
+            )
+        }.errorCode shouldBe NO_TEST_ENTRY
+    }
+
+    @Test
+    fun `vaccination cert fails in mode CERT_REC_STRICT`() {
+        shouldThrow<InvalidRecoveryCertificateException> {
+            extractor.extract(
+                VaccinationQrCodeTestData.validVaccinationQrCode,
+                mode = Mode.CERT_REC_STRICT
+            )
+        }.errorCode shouldBe NO_RECOVERY_ENTRY
+    }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt
new file mode 100644
index 000000000..f91781522
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/DccQrCodeValidatorTest.kt
@@ -0,0 +1,46 @@
+package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
+
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent
+import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import io.mockk.spyk
+import io.mockk.verify
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+import javax.inject.Inject
+
+class DccQrCodeValidatorTest : BaseTest() {
+    @Inject lateinit var testData: VaccinationTestData
+    @Inject lateinit var vacExtractor: DccQrCodeExtractor
+    private lateinit var vacExtractorSpy: DccQrCodeExtractor
+
+    @BeforeEach
+    fun setup() {
+        DaggerVaccinationTestComponent.factory().create().inject(this)
+
+        vacExtractorSpy = spyk(vacExtractor)
+    }
+
+    @Test
+    fun `validator uses strict extraction mode`() {
+        val instance = DccQrCodeValidator(vacExtractorSpy)
+        instance.validate(testData.personAVac1QRCodeString).apply {
+            uniqueCertificateIdentifier shouldBe testData.personAVac1Container.certificateId
+        }
+        verify { vacExtractorSpy.extract(testData.personAVac1QRCodeString, DccV1Parser.Mode.CERT_SINGLE_STRICT) }
+    }
+
+    @Test
+    fun `validator throws invalid vaccination exception for pcr test qr code`() {
+        val instance = DccQrCodeValidator(vacExtractorSpy)
+        shouldThrow<InvalidVaccinationCertificateException> {
+            instance.validate("HTTPS://LOCALHOST/?123456-12345678-1234-4DA7-B166-B86D85475064")
+        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.PREFIX_INVALID
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java
new file mode 100644
index 000000000..3abe166d4
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/RecoveryQrCodeTestData.java
@@ -0,0 +1,5 @@
+package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode;
+
+public class RecoveryQrCodeTestData {
+    public static String validRecovery = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH-XIIOO6+IGURR%28WA-JHR6N+QI6M8SA3/-2E%5UR5+VBZXIFNJ1VCSWC1QDRCKH9CJZIS/1VY9X5QF36FY1OSMNV1L8VNF6O M9R1VNM8.MM.EM M%/EJ.E%+MXM6Q*ER56CL6F8E4IMI%6NJM3VE1*AA/98T5UEIY0Q$UPR$5:NLOEPNRAE69K P4NPDDAJP5DMH1$4D-I/2DBAJDAJCNB-437Y4-U2UYLX76SW6B699D9ISU39GEA7IB6$C94JB2E9Z3E8AE-QD+PB.QCD-H/8O3BEQ8L9VN.6A4JBLHLXHQT*QR$M% OP*B9YOB*16CQJN9TW5F/94O5 9E6UEDTUD1VVY95CQ-8EDS9%PP%.P3Y9UM97H98$Q PQ4T9AKPCPP0%M0YMBXR7+21KUO8UFGKSBV1/C.4W2MEGHPH:UM-JZ535IT1$1KQQQFF+ C5BBPHT/.VJ*H71I3-EIJ9Q 5P+DQ3K%USN7MS6L2VHX+KOD9VODFL96Y0441:*G";
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt
deleted file mode 100644
index 1a169d430..000000000
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/qrcode/VaccinationQrCodeValidatorTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode
-
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor.Mode
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData
-import io.kotest.matchers.shouldBe
-import io.mockk.spyk
-import io.mockk.verify
-import org.junit.jupiter.api.BeforeEach
-import org.junit.jupiter.api.Test
-import testhelpers.BaseTest
-import javax.inject.Inject
-
-class VaccinationQrCodeValidatorTest : BaseTest() {
-    @Inject lateinit var testData: VaccinationTestData
-    @Inject lateinit var vacExtractor: VaccinationQRCodeExtractor
-    private lateinit var vacExtractorSpy: VaccinationQRCodeExtractor
-
-    @BeforeEach
-    fun setup() {
-        DaggerVaccinationTestComponent.factory().create().inject(this)
-
-        vacExtractorSpy = spyk(vacExtractor)
-    }
-
-    @Test
-    fun `validator uses strict extraction mode`() {
-        val instance = VaccinationQRCodeValidator(vacExtractorSpy)
-        instance.validate(testData.personAVac1QRCodeString).apply {
-            uniqueCertificateIdentifier shouldBe testData.personAVac1Container.certificateId
-        }
-        verify { vacExtractorSpy.extract(testData.personAVac1QRCodeString, Mode.CERT_VAC_STRICT) }
-    }
-}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt
index 107dae36a..fabc048ab 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/VaccinationRepositoryTest.kt
@@ -1,10 +1,10 @@
 package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository
 
-import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidHealthCertificateException.ErrorCode.ALREADY_REGISTERED
 import de.rki.coronawarnapp.covidcertificate.common.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.errors.VaccinationCertificateNotFoundException
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinatedPersonData
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage.VaccinationStorage
@@ -35,7 +35,7 @@ class VaccinationRepositoryTest : BaseTest() {
     @MockK lateinit var storage: VaccinationStorage
     @MockK lateinit var valueSetsRepository: ValueSetsRepository
     @MockK lateinit var vaccinationValueSet: VaccinationValueSets
-    @MockK lateinit var qrCodeExtractor: VaccinationQRCodeExtractor
+    @MockK lateinit var qrCodeExtractor: DccQrCodeExtractor
 
     private var testStorage: Set<VaccinatedPersonData> = emptySet()
 
@@ -66,7 +66,7 @@ class VaccinationRepositoryTest : BaseTest() {
         timeStamper = timeStamper,
         storage = storage,
         valueSetsRepository = valueSetsRepository,
-        vaccinationQRCodeExtractor = qrCodeExtractor,
+        qrCodeExtractor = qrCodeExtractor,
     )
 
     @Test
@@ -134,7 +134,7 @@ class VaccinationRepositoryTest : BaseTest() {
 
         shouldThrow<InvalidVaccinationCertificateException> {
             instance.registerVaccination(vaccinationTestData.personAVac1QRCode)
-        }.errorCode shouldBe VC_ALREADY_REGISTERED
+        }.errorCode shouldBe ALREADY_REGISTERED
 
         testStorage.first() shouldBe dataBefore
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt
index 2f27974c9..486398e30 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/vaccination/core/repository/storage/VaccinationContainerTest.kt
@@ -1,11 +1,11 @@
 package de.rki.coronawarnapp.covidcertificate.vaccination.core.repository.storage
 
-import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.common.certificate.CertificatePersonIdentifier
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccQrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.common.certificate.DccV1Parser
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.DaggerVaccinationTestComponent
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.VaccinationTestData
 import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationCertificateQRCode
-import de.rki.coronawarnapp.covidcertificate.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.DefaultValueSet
 import de.rki.coronawarnapp.covidcertificate.valueset.valuesets.VaccinationValueSets
 import io.kotest.matchers.shouldBe
@@ -151,8 +151,13 @@ class VaccinationContainerTest : BaseTest() {
             vaccinationQrCode = testData.personYVacTwoEntriesQrCode,
             scannedAt = Instant.EPOCH
         )
-        val extractor = mockk<VaccinationQRCodeExtractor>().apply {
-            every { extract(any(), any()) } returns mockk<VaccinationCertificateQRCode>().apply {
+        val extractor = mockk<DccQrCodeExtractor>().apply {
+            every {
+                extract(
+                    any(),
+                    DccV1Parser.Mode.CERT_VAC_LENIENT
+                )
+            } returns mockk<VaccinationCertificateQRCode>().apply {
                 every { data } returns mockk()
             }
         }
@@ -160,11 +165,11 @@ class VaccinationContainerTest : BaseTest() {
 
         container.certificateData shouldNotBe null
 
-        verify { extractor.extract(testData.personYVacTwoEntriesQrCode, QrCodeExtractor.Mode.CERT_VAC_LENIENT) }
+        verify { extractor.extract(testData.personYVacTwoEntriesQrCode, DccV1Parser.Mode.CERT_VAC_LENIENT) }
     }
 
     @Test
     fun `gracefully handle semi invalid data - multiple entries`() {
-        testData.personYVacTwoEntriesContainer.certificate.payloads.size shouldBe 1
+        testData.personYVacTwoEntriesContainer.certificate.vaccination
     }
 }
-- 
GitLab