From b803af8037a2cfd47e06e691c54dea13f26d26c0 Mon Sep 17 00:00:00 2001
From: Chilja Gossow <49635654+chiljamgossow@users.noreply.github.com>
Date: Mon, 7 Jun 2021 11:06:35 +0200
Subject: [PATCH] COSE decryption, QR code generation, error codes
 (EXPOSUREAPP-7506) (#3346)

* wip

* wiring

* wip

* testing

* refactor exception
tests

* tests

* decompression

* decompression

* fix tests

* encrypt

* second test case

* add error codes

* fix tests

* move exception

* tests

* error codes

* error codes

* error codes

* address comments

* add more logging

* reverse string file

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
---
 Corona-Warn-App/build.gradle                  |   2 +
 .../coronatest/TestCertificateRepository.kt   |   2 +-
 .../type/TestCertificateContainer.kt          |   2 +-
 .../cryptography/AesCryptography.kt           |  27 +++-
 .../InvalidHealthCertificateException.kt      |  73 +++++++++++
 .../InvalidTestCertificateException.kt        |  92 +++++++++++++
 .../InvalidVaccinationCertificateException.kt |  61 +++++++++
 .../covidcertificate/test/TestCertificate.kt  |   2 +-
 .../test/TestCertificateDccParser.kt          |  62 +++++++++
 .../test/TestCertificateDccV1.kt              |  34 ++---
 .../test/TestCertificateQRCodeExtractor.kt    | 119 ++++++++++++++++-
 .../util/compression/ZLIBCompression.kt       |  20 +++
 .../util/encoding/Base45Decoder.kt            |   1 -
 .../core/CertificatePersonIdentifier.kt       |  11 +-
 .../HealthCertificateCOSEDecoder.kt           |  35 ++++-
 .../HealthCertificateHeaderParser.kt          |  13 +-
 .../InvalidHealthCertificateException.kt      |  95 --------------
 .../certificate/VaccinationDGCV1Parser.kt     |  25 ++--
 .../core/qrcode/VaccinationQRCodeExtractor.kt |  21 ++-
 .../core/qrcode/VaccinationQRCodeValidator.kt |   6 +-
 .../core/repository/VaccinationRepository.kt  |   6 +-
 .../values-de/green_certificate_strings.xml   |   2 +-
 .../res/values/green_certificate_strings.xml  |   2 +-
 .../coronatest/CoronaTestTestData.kt          |   8 +-
 .../cryptography/AesCryptographyTest.kt       |  18 +++
 .../test/TestCertificateDccParserTest.kt      |  42 ++++++
 .../TestCertificateQRCodeExtractorTest.kt     | 121 ++++++++++++++++++
 .../covidcertificate/test/TestData.java       |  20 +++
 .../core/VaccinatedPersonIdentifierTest.kt    |  17 +--
 .../qrcode/VaccinationQRCodeExtractorTest.kt  |  22 ++--
 .../repository/VaccinationRepositoryTest.kt   |  12 +-
 31 files changed, 780 insertions(+), 193 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt
 delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt
 create mode 100644 Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 948ff9a9f..e019130e2 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -441,4 +441,6 @@ dependencies {
 
     // HCert
     implementation("com.upokecenter:cbor:4.4.1")
+
+    implementation("bouncycastle:bcprov-jdk16:136")
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt
index c367daa52..8f3c1b148 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/TestCertificateRepository.kt
@@ -330,7 +330,7 @@ class TestCertificateRepository @Inject constructor(
 
             val extractedData = qrCodeExtractor.extract(
                 decryptionKey = encryptionkey.toByteArray(),
-                encryptedCoseComponents = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray()
+                rawCoseObjectEncrypted = components.encryptedCoseTestCertificateBase64.decodeBase64()!!.toByteArray()
             )
 
             val nowUtc = timeStamper.nowUTC
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt
index 40f3c02ce..741279d37 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/coronatest/type/TestCertificateContainer.kt
@@ -85,7 +85,7 @@ abstract class TestCertificateContainer {
                 get() = testCertificate.testNameAndManufactor?.let { valueSet?.getDisplayText(it) ?: it }
             override val sampleCollectedAt: Instant
                 get() = testCertificate.sampleCollectedAt
-            override val testResultAt: Instant
+            override val testResultAt: Instant?
                 get() = testCertificate.testResultAt
             override val testCenter: String
                 get() = testCertificate.testCenter
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt
index 80ad40d4c..261fc2a02 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptography.kt
@@ -1,16 +1,37 @@
 package de.rki.coronawarnapp.covidcertificate.cryptography
 
+import com.google.android.gms.common.util.Hex
 import dagger.Reusable
-import okio.ByteString
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.bouncycastle.util.encoders.Base64.decode
+import java.security.Security
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+import javax.crypto.spec.SecretKeySpec
 import javax.inject.Inject
 
 @Reusable
 class AesCryptography @Inject constructor() {
 
+    private val ivParameterSpec
+        get() = IvParameterSpec(Hex.stringToBytes("00000000000000000000000000000000"))
+
     fun decrypt(
         decryptionKey: ByteArray,
-        encryptedData: ByteString
+        encryptedData: ByteArray
     ): ByteArray {
-        throw NotImplementedError()
+        Security.addProvider(BouncyCastleProvider())
+        val keySpec = SecretKeySpec(decode(decryptionKey), ALGORITHM)
+        val input = decode(encryptedData)
+        return with(Cipher.getInstance(TRANSFORMATION)) {
+            init(Cipher.DECRYPT_MODE, keySpec, ivParameterSpec)
+            val output = ByteArray(getOutputSize(input.size))
+            var outputLength = update(input, 0, input.size, output, 0)
+            outputLength += doFinal(output, outputLength)
+            output.copyOfRange(0, outputLength)
+        }
     }
 }
+
+private const val ALGORITHM = "AES"
+private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt
new file mode 100644
index 000000000..16b90b3db
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidHealthCertificateException.kt
@@ -0,0 +1,73 @@
+package de.rki.coronawarnapp.covidcertificate.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
+import de.rki.coronawarnapp.util.ui.CachedString
+import de.rki.coronawarnapp.util.ui.LazyString
+
+@Suppress("MaxLineLength")
+open class InvalidHealthCertificateException(
+    val errorCode: ErrorCode
+) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) {
+    enum class ErrorCode(
+        val message: String
+    ) {
+        HC_BASE45_DECODING_FAILED("Base45 decoding failed."),
+        HC_BASE45_ENCODING_FAILED("Base45 encoding failed."),
+        HC_ZLIB_DECOMPRESSION_FAILED("Zlib decompression failed."),
+        HC_ZLIB_COMPRESSION_FAILED("Zlib compression failed."),
+        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."),
+        NO_TEST_ENTRY("Test certificate missing."),
+        VC_PREFIX_INVALID("Prefix invalid."),
+        VC_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."),
+        HC_CWT_NO_DGC("Dgc missing."),
+        HC_CWT_NO_EXP("Expiration date missing."),
+        HC_CWT_NO_HCERT("Health certificate missing."),
+        HC_CWT_NO_ISS("Issuer missing."),
+        AES_DECRYPTION_FAILED("AES decryption failed"),
+        DCC_COMP_202("DCC Test Certificate Components failed with error 202: DCC pending."),
+        DCC_COMP_400("DCC Test Certificate Components failed with error 400: Bad request (e.g. wrong format of registration token)"),
+        DCC_COMP_404("DCC Test Certificate Components failed with error 404: Registration token does not exist."),
+        DCC_COMP_410("DCC Test Certificate Components failed with error 410: DCC already cleaned up."),
+        DCC_COMP_412("DCC Test Certificate Components failed with error 412: Test result not yet received"),
+        DCC_COMP_500_INTERNAL("DCC Test Certificate Components failed with error 500: Internal server error."),
+        DCC_COMP_500_LAB_INVALID_RESPONSE("DCC Test Certificate Components failed with error 500: Lab Invalid response"),
+        DCC_COMP_500_SIGNING_CLIENT_ERROR("DCC Test Certificate Components failed with error 500: Signing client error"),
+        DCC_COMP_500_SIGNING_SERVER_ERROR("DCC Test Certificate Components failed with error 500: Signing server error"),
+        DCC_COMP_NO_NETWORK("DCC Test Certificate Components failed due to no network connection."),
+        DCC_COSE_MESSAGE_INVALID("COSE message invalid."),
+        DCC_COSE_TAG_INVALID("COSE tag invalid."),
+        PKR_400("Public Key Registration failed with error 400: Bad request (e.g. wrong format of registration token or public key)."),
+        PKR_403("Public Key Registration failed with error 403: Registration token is not allowed to issue a DCC."),
+        PKR_404("Public Key Registration failed with error 404: Registration token does not exist."),
+        PKR_409("Public Key Registration failed with error 409: Registration token is already assigned to a public key."),
+        PKR_500("Public Key Registration failed with error 500: Internal server error."),
+        PKR_FAILED("Private key request failed."),
+        PKR_NO_NETWORK("Private key request failed due to no network connection."),
+        RSA_DECRYPTION_FAILED("RSA decryption failed."),
+        RSA_KP_GENERATION_FAILED("RSA key pair generation failed."),
+    }
+
+    open val errorMessage: LazyString
+        get() = CachedString { context ->
+            context.getString(ERROR_MESSAGE_GENERIC)
+        }
+
+    override fun toHumanReadableError(context: Context): HumanReadableError {
+        return HumanReadableError(
+            description = errorMessage.get(context)
+        )
+    }
+}
+
+private const val ERROR_MESSAGE_GENERIC = R.string.errors_generic_text_unknown_error_cause
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt
new file mode 100644
index 000000000..ca8909e46
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidTestCertificateException.kt
@@ -0,0 +1,92 @@
+package de.rki.coronawarnapp.covidcertificate.exception
+
+import android.content.Context
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.util.HumanReadableError
+import de.rki.coronawarnapp.util.ui.CachedString
+import de.rki.coronawarnapp.util.ui.LazyString
+
+class InvalidTestCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) {
+    override fun toHumanReadableError(context: Context): HumanReadableError {
+        var errorCodeString = errorCode.toString()
+        errorCodeString = if (errorCodeString.startsWith(PREFIX_TC)) errorCodeString else PREFIX_TC + errorCodeString
+        return HumanReadableError(
+            description = errorMessage.get(context) + "\n\n$errorCodeString"
+        )
+    }
+
+    override val errorMessage: LazyString
+        get() = when (errorCode) {
+
+            ErrorCode.DCC_COMP_NO_NETWORK,
+            ErrorCode.PKR_NO_NETWORK -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_NO_NETWORK)
+            }
+
+            ErrorCode.DCC_COMP_202 -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_TRY_AGAIN_DCC_NOT_AVAILABLE_YET)
+            }
+
+            ErrorCode.DCC_COMP_410 -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_DCC_EXPIRED)
+            }
+
+            // TODO
+/*            ErrorCode.HC_BASE45_DECODING_FAILED,
+            ErrorCode.HC_CBOR_DECODING_FAILED,
+            ErrorCode.HC_COSE_MESSAGE_INVALID,
+            ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED,
+            ErrorCode.HC_COSE_TAG_INVALID,
+            ErrorCode.HC_CWT_NO_DGC,
+            ErrorCode.HC_CWT_NO_EXP,
+            ErrorCode.HC_CWT_NO_HCERT,
+            ErrorCode.HC_CWT_NO_ISS,
+            ErrorCode.JSON_SCHEMA_INVALID,*/
+
+            ErrorCode.AES_DECRYPTION_FAILED,
+            ErrorCode.RSA_DECRYPTION_FAILED,
+            ErrorCode.DCC_COSE_MESSAGE_INVALID,
+            ErrorCode.DCC_COSE_TAG_INVALID,
+            ErrorCode.DCC_COMP_404,
+            ErrorCode.DCC_COMP_412,
+            ErrorCode.PKR_403,
+            ErrorCode.PKR_404 -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_E2E_ERROR_CALL_HOTLINE)
+            }
+
+            ErrorCode.DCC_COMP_400,
+            ErrorCode.PKR_400 -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_CLIENT_ERROR_CALL_HOTLINE)
+            }
+
+            ErrorCode.PKR_FAILED,
+            ErrorCode.RSA_KP_GENERATION_FAILED,
+            ErrorCode.PKR_500,
+            ErrorCode.DCC_COMP_500_INTERNAL -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_TRY_AGAIN)
+            }
+
+            ErrorCode.HC_BASE45_ENCODING_FAILED,
+            ErrorCode.HC_ZLIB_COMPRESSION_FAILED,
+            ErrorCode.NO_TEST_ENTRY,
+            ErrorCode.DCC_COMP_500_LAB_INVALID_RESPONSE,
+            ErrorCode.DCC_COMP_500_SIGNING_CLIENT_ERROR,
+            ErrorCode.DCC_COMP_500_SIGNING_SERVER_ERROR,
+            ErrorCode.PKR_409 -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_GENERIC)
+            }
+            else -> super.errorMessage
+        }
+}
+
+private const val PREFIX_TC = "TC_"
+private const val ERROR_MESSAGE_GENERIC = R.string.errors_generic_text_unknown_error_cause
+
+// TODO change to correct error message once provided
+private const val ERROR_MESSAGE_TRY_AGAIN = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_DCC_NOT_SUPPORTED_BY_LAB = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_NO_NETWORK = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_E2E_ERROR_CALL_HOTLINE = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_TRY_AGAIN_DCC_NOT_AVAILABLE_YET = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_CLIENT_ERROR_CALL_HOTLINE = ERROR_MESSAGE_GENERIC
+private const val ERROR_MESSAGE_DCC_EXPIRED = ERROR_MESSAGE_GENERIC
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt
new file mode 100644
index 000000000..0309cf245
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/exception/InvalidVaccinationCertificateException.kt
@@ -0,0 +1,61 @@
+package de.rki.coronawarnapp.covidcertificate.exception
+
+import android.content.Context
+import de.rki.coronawarnapp.R
+import de.rki.coronawarnapp.util.HumanReadableError
+import de.rki.coronawarnapp.util.ui.CachedString
+import de.rki.coronawarnapp.util.ui.LazyString
+
+class InvalidVaccinationCertificateException(errorCode: ErrorCode) : InvalidHealthCertificateException(errorCode) {
+    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) + "\n\n$errorCodeString"
+        )
+    }
+
+    override val errorMessage: LazyString
+        get() = when (errorCode) {
+            ErrorCode.VC_PREFIX_INVALID,
+            ErrorCode.HC_BASE45_DECODING_FAILED,
+            ErrorCode.HC_CBOR_DECODING_FAILED,
+            ErrorCode.HC_COSE_MESSAGE_INVALID,
+            ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED,
+            ErrorCode.HC_COSE_TAG_INVALID,
+            ErrorCode.HC_CWT_NO_DGC,
+            ErrorCode.HC_CWT_NO_EXP,
+            ErrorCode.HC_CWT_NO_HCERT,
+            ErrorCode.HC_CWT_NO_ISS,
+            ErrorCode.JSON_SCHEMA_INVALID,
+            -> CachedString { context ->
+                context.getString(ERROR_MESSAGE_VC_INVALID)
+            }
+
+            ErrorCode.VC_NO_VACCINATION_ENTRY -> 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 ->
+                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 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/test/TestCertificate.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt
index 4d246e1dc..85196fbe9 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificate.kt
@@ -28,7 +28,7 @@ interface TestCertificate {
      */
     val testNameAndManufactor: String?
     val sampleCollectedAt: Instant
-    val testResultAt: Instant
+    val testResultAt: Instant?
     val testCenter: String
 
     val certificateIssuer: String
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt
new file mode 100644
index 000000000..78fe73ea1
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParser.kt
@@ -0,0 +1,62 @@
+package de.rki.coronawarnapp.covidcertificate.test
+
+import com.google.gson.Gson
+import com.upokecenter.cbor.CBORObject
+import dagger.Reusable
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
+import de.rki.coronawarnapp.covidcertificate.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 TestCertificateDccParser @Inject constructor(
+    @BaseGson private val gson: Gson,
+) {
+
+    fun parse(map: CBORObject): TestCertificateDccV1 = try {
+        val certificate: TestCertificateDccV1 = map[keyHCert]?.run {
+            this[keyEuDgcV1]?.run {
+                toCertificate()
+            } ?: throw InvalidTestCertificateException(HC_CWT_NO_DGC)
+        } ?: throw InvalidTestCertificateException(HC_CWT_NO_HCERT)
+
+        certificate.validate()
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        throw InvalidTestCertificateException(HC_CBOR_DECODING_FAILED)
+    }
+
+    private fun TestCertificateDccV1.validate(): TestCertificateDccV1 {
+        if (testCertificateData.isNullOrEmpty()) {
+            throw InvalidTestCertificateException(NO_TEST_ENTRY)
+        }
+        // Force date parsing
+        dateOfBirth
+        testCertificateData.forEach {
+            it.testResultAt
+            it.sampleCollectedAt
+        }
+        return this
+    }
+
+    private fun CBORObject.toCertificate() = try {
+        val json = ToJSONString()
+        gson.fromJson<TestCertificateDccV1>(json)
+    } 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/TestCertificateDccV1.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt
index 35182a1ce..1ef5539ab 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccV1.kt
@@ -8,7 +8,7 @@ data class TestCertificateDccV1(
     @SerializedName("ver") val version: String,
     @SerializedName("nam") val nameData: NameData,
     @SerializedName("dob") val dob: String,
-    @SerializedName("v") val testCertificateData: List<TestCertificateData>,
+    @SerializedName("t") val testCertificateData: List<TestCertificateData>,
 ) {
     data class NameData(
         @SerializedName("fn") val familyName: String?,
@@ -20,22 +20,19 @@ data class TestCertificateDccV1(
     data class TestCertificateData(
         // Disease or agent targeted, e.g. "tg": "840539006"
         @SerializedName("tg") val targetId: String,
-        // Type of Test (required)
+        // Type of Test (required) eg "LP217198-3"
         @SerializedName("tt") val testType: String,
-        // Test Result (required)
+        // Test Result (required) e. g. "tr": "260415000"
         @SerializedName("tr") val testResult: String,
-        // NAA Test Name (only for PCR tests, but not required)
-        @SerializedName("nm") val testName: 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?,
-        // Date/Time of Sample Collection (required)
-        // "sc": "2021-04-13T14:20:00+00:00",
-        @SerializedName("sc") val sampleCollectedAt: Instant,
-        // Date/Time of Test Result
-        // "dr": "2021-04-13T14:40:01+00:00",
-        @SerializedName("dr") val testResultAt: Instant,
-        // Testing Center (required)
-        // "tc": "GGD Fryslân, L-Heliconweg",
+        @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") val countryOfTest: String,
@@ -43,7 +40,14 @@ data class TestCertificateDccV1(
         @SerializedName("is") val certificateIssuer: String,
         // Unique Certificate Identifier, e.g.  "ci": "urn:uvci:01:NL:PlA8UWS60Z4RZXVALl6GAZ"
         @SerializedName("ci") val uniqueCertificateIdentifier: String
-    )
+    ) {
+
+        val testResultAt: Instant?
+            get() = dr?.let { Instant.parse(it) }
+
+        val sampleCollectedAt: Instant
+            get() = Instant.parse(sc)
+    }
 
     val dateOfBirth: LocalDate
         get() = LocalDate.parse(dob)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt
index 6737944ce..92c075703 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractor.kt
@@ -1,25 +1,130 @@
 package de.rki.coronawarnapp.covidcertificate.test
 
+import com.upokecenter.cbor.CBORObject
 import dagger.Reusable
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_ENCODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_COMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidTestCertificateException
+import de.rki.coronawarnapp.util.compression.deflate
+import de.rki.coronawarnapp.util.compression.inflate
+import de.rki.coronawarnapp.util.encoding.Base45Decoder
+import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder
+import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser
+import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
+import timber.log.Timber
 import javax.inject.Inject
 
 @Reusable
-class TestCertificateQRCodeExtractor @Inject constructor() {
+class TestCertificateQRCodeExtractor @Inject constructor(
+    private val coseDecoder: HealthCertificateCOSEDecoder,
+    private val headerParser: HealthCertificateHeaderParser,
+    private val bodyParser: TestCertificateDccParser,
+) {
 
     /**
-     * May throw an **[InvalidHealthCertificateException]**
+     * May throw an **[InvalidTestCertificateException]**
      */
     fun extract(
         decryptionKey: ByteArray,
-        encryptedCoseComponents: ByteArray,
+        rawCoseObjectEncrypted: ByteArray,
     ): TestCertificateQRCode {
-        throw NotImplementedError()
+        val rawCoseObject = rawCoseObjectEncrypted.decrypt(decryptionKey)
+        return TestCertificateQRCode(
+            testCertificateData = rawCoseObject.decode(),
+            qrCode = rawCoseObject.encode()
+        )
     }
 
     /**
-     * May throw an **[InvalidHealthCertificateException]**
+     * May throw an **[InvalidTestCertificateException]**
      */
-    fun extract(qrCode: String): TestCertificateQRCode {
-        throw NotImplementedError()
+    fun extract(qrCode: String) = TestCertificateQRCode(
+        testCertificateData = 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(): TestCertificateData =
+        removePrefix(PREFIX)
+            .decodeBase45()
+            .decompress()
+            .decode()
+
+    private fun RawCOSEObject.encode(): String {
+        return PREFIX + compress().encodeBase45()
+    }
+
+    private fun RawCOSEObject.decode(): TestCertificateData = 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(): TestCertificateData = try {
+        TestCertificateData(
+            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/util/compression/ZLIBCompression.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt
index d03ef22a3..e4ddeec39 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/compression/ZLIBCompression.kt
@@ -2,6 +2,9 @@ package de.rki.coronawarnapp.util.compression
 
 import okio.Buffer
 import okio.inflate
+import timber.log.Timber
+import java.io.ByteArrayOutputStream
+import java.util.zip.Deflater
 import java.util.zip.Inflater
 import javax.inject.Inject
 
@@ -25,8 +28,25 @@ class ZLIBCompression @Inject constructor() {
 
         sink.readByteArray()
     } catch (e: Throwable) {
+        Timber.e(e)
         throw InvalidInputException("ZLIB decompression failed.", e)
     }
+
+    fun compress(input: ByteArray): ByteArray {
+        val deflater = Deflater()
+        deflater.setInput(input)
+        val outputStream = ByteArrayOutputStream(input.size)
+        deflater.finish()
+        val buffer = ByteArray(1024)
+        while (!deflater.finished()) {
+            val count = deflater.deflate(buffer)
+            outputStream.write(buffer, 0, count)
+        }
+        outputStream.close()
+        return outputStream.toByteArray()
+    }
 }
 
 fun ByteArray.inflate(sizeLimit: Long = -1L) = ZLIBCompression().decompress(this, sizeLimit)
+
+fun ByteArray.deflate() = ZLIBCompression().compress(this)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt
index 39320eb27..b8cebf7a4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/encoding/Base45Decoder.kt
@@ -23,7 +23,6 @@ import java.math.BigInteger
  * Based on
  * https://github.com/ehn-digital-green-development/hcert-kotlin/blob/23203fbb71f53524ee643a9df116264f87b5b32a/src/main/kotlin/ehn/techiop/hcert/kotlin/chain/common/Base45Encoder.kt
  */
-@OptIn(ExperimentalUnsignedTypes::class)
 object Base45Decoder {
     private const val alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
     private val int45 = BigInteger.valueOf(45)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt
index 4350879cc..3c4744fc3 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/CertificatePersonIdentifier.kt
@@ -1,10 +1,11 @@
 package de.rki.coronawarnapp.vaccination.core
 
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateDccV1
 import de.rki.coronawarnapp.covidcertificate.test.TestCertificateQRCode
 import de.rki.coronawarnapp.util.HashExtensions.toSHA256
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode
 import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
 import org.joda.time.LocalDate
@@ -37,15 +38,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 InvalidHealthCertificateException(ErrorCode.VC_NAME_MISMATCH)
+            throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH)
         }
         if (firstNameStandardized != other.firstNameStandardized) {
             Timber.d("Given name does not match, got ${other.firstNameStandardized}, expected $firstNameStandardized")
-            throw InvalidHealthCertificateException(ErrorCode.VC_NAME_MISMATCH)
+            throw InvalidVaccinationCertificateException(VC_NAME_MISMATCH)
         }
         if (dateOfBirth != other.dateOfBirth) {
             Timber.d("Date of birth does not match, got ${other.dateOfBirth}, expected $dateOfBirth")
-            throw InvalidHealthCertificateException(ErrorCode.VC_DOB_MISMATCH)
+            throw InvalidVaccinationCertificateException(VC_DOB_MISMATCH)
         }
     }
 }
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt
index fbb291dcd..2fd765e1e 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateCOSEDecoder.kt
@@ -1,12 +1,18 @@
 package de.rki.coronawarnapp.vaccination.core.certificate
 
 import com.upokecenter.cbor.CBORObject
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID
+import de.rki.coronawarnapp.covidcertificate.cryptography.AesCryptography
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.AES_DECRYPTION_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID
+import de.rki.coronawarnapp.util.encoding.base64
 import timber.log.Timber
 import javax.inject.Inject
 
-class HealthCertificateCOSEDecoder @Inject constructor() {
+class HealthCertificateCOSEDecoder @Inject constructor(
+    private val aesEncryptor: AesCryptography
+) {
 
     fun decode(input: RawCOSEObject): CBORObject = try {
         val messageObject = CBORObject.DecodeFromBytes(input).validate()
@@ -19,6 +25,29 @@ class HealthCertificateCOSEDecoder @Inject constructor() {
         throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID)
     }
 
+    fun decryptMessage(input: RawCOSEObject, decryptionKey: ByteArray): RawCOSEObject = try {
+        val messageObject = CBORObject.DecodeFromBytes(input).validate()
+        val content = messageObject[2].GetByteString()
+        val decrypted = content.decrypt(decryptionKey)
+        messageObject[2] = CBORObject.FromObject(decrypted)
+        messageObject.EncodeToBytes()
+    } catch (e: InvalidHealthCertificateException) {
+        throw e
+    } catch (e: Throwable) {
+        Timber.e(e)
+        throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID)
+    }
+
+    private fun ByteArray.decrypt(decryptionKey: ByteArray) = try {
+        aesEncryptor.decrypt(
+            decryptionKey = decryptionKey,
+            encryptedData = this.base64().toByteArray()
+        )
+    } catch (e: Throwable) {
+        Timber.e(e)
+        throw InvalidHealthCertificateException(AES_DECRYPTION_FAILED)
+    }
+
     private fun CBORObject.validate(): CBORObject {
         if (size() != 4) {
             throw InvalidHealthCertificateException(HC_COSE_MESSAGE_INVALID)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt
index e93687820..3bfa90a6d 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/HealthCertificateHeaderParser.kt
@@ -2,9 +2,10 @@ package de.rki.coronawarnapp.vaccination.core.certificate
 
 import com.upokecenter.cbor.CBORObject
 import dagger.Reusable
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_EXP
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS
 import org.joda.time.Instant
 import javax.inject.Inject
 
@@ -12,15 +13,15 @@ import javax.inject.Inject
 class HealthCertificateHeaderParser @Inject constructor() {
 
     fun parse(map: CBORObject): CoseCertificateHeader = try {
-        val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS)
+        val issuer: String = map[keyIssuer]?.AsString() ?: throw InvalidHealthCertificateException(HC_CWT_NO_ISS)
 
         val issuedAt: Instant = map[keyIssuedAt]?.run {
             Instant.ofEpochSecond(AsNumber().ToInt64Checked())
-        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_ISS)
+        } ?: throw InvalidHealthCertificateException(HC_CWT_NO_ISS)
 
         val expiresAt: Instant = map[keyExpiresAt]?.run {
             Instant.ofEpochSecond(AsNumber().ToInt64Checked())
-        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_EXP)
+        } ?: throw InvalidHealthCertificateException(HC_CWT_NO_EXP)
 
         HealthCertificateHeader(
             issuer = issuer,
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt
deleted file mode 100644
index 700ab2ddc..000000000
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/InvalidHealthCertificateException.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package de.rki.coronawarnapp.vaccination.core.certificate
-
-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
-import de.rki.coronawarnapp.util.ui.CachedString
-import de.rki.coronawarnapp.util.ui.LazyString
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_MESSAGE_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_COSE_TAG_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_EXP
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_STORING_FAILED
-
-class InvalidHealthCertificateException(
-    val errorCode: ErrorCode
-) : HasHumanReadableError, InvalidQRCodeException(errorCode.message) {
-    enum class ErrorCode(
-        val message: String
-    ) {
-        HC_BASE45_DECODING_FAILED("Base45 decoding failed."),
-        HC_ZLIB_DECOMPRESSION_FAILED("Zlib decompression failed."),
-        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_PREFIX_INVALID("Prefix invalid."),
-        VC_STORING_FAILED("Storing failed."),
-        VC_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."),
-        VC_HC_CWT_NO_DGC("Dgc missing."),
-        VC_HC_CWT_NO_EXP("Expiration date missing."),
-        VC_HC_CWT_NO_HCERT("Health certificate missing."),
-        VC_HC_CWT_NO_ISS("Issuer missing."),
-    }
-
-    val errorMessage: LazyString
-        get() = when (errorCode) {
-            HC_BASE45_DECODING_FAILED,
-            HC_CBOR_DECODING_FAILED,
-            HC_COSE_MESSAGE_INVALID,
-            HC_ZLIB_DECOMPRESSION_FAILED,
-            HC_COSE_TAG_INVALID,
-            VC_PREFIX_INVALID,
-            VC_HC_CWT_NO_DGC,
-            VC_HC_CWT_NO_EXP,
-            VC_HC_CWT_NO_HCERT,
-            VC_HC_CWT_NO_ISS,
-            VC_JSON_SCHEMA_INVALID,
-            -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_INVALID)
-            }
-            VC_NO_VACCINATION_ENTRY -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_NOT_YET_SUPPORTED)
-            }
-            VC_STORING_FAILED -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_SCAN_AGAIN)
-            }
-            VC_NAME_MISMATCH, VC_DOB_MISMATCH -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_VC_DIFFERENT_PERSON)
-            }
-            VC_ALREADY_REGISTERED -> CachedString { context ->
-                context.getString(ERROR_MESSAGE_ALREADY_REGISTERED)
-            }
-        }
-
-    override fun toHumanReadableError(context: Context): HumanReadableError {
-        var errorCodeString = errorCode.toString()
-        errorCodeString = if (errorCodeString.startsWith(PREFIX)) errorCodeString else PREFIX + errorCodeString
-        return HumanReadableError(
-            description = errorMessage.get(context) + "\n\n$errorCodeString"
-        )
-    }
-}
-
-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_ALREADY_REGISTERED = R.string.error_vc_already_registered
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt
index e25defbc9..2f9e46d59 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/certificate/VaccinationDGCV1Parser.kt
@@ -3,13 +3,15 @@ package de.rki.coronawarnapp.vaccination.core.certificate
 import com.google.gson.Gson
 import com.upokecenter.cbor.CBORObject
 import dagger.Reusable
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_DGC
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_HCERT
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.JSON_SCHEMA_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.util.serialization.BaseGson
 import de.rki.coronawarnapp.util.serialization.fromJson
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_DGC
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_HCERT
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_JSON_SCHEMA_INVALID
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
 import javax.inject.Inject
 
 @Reusable
@@ -21,19 +23,18 @@ class VaccinationDGCV1Parser @Inject constructor(
         val certificate: VaccinationDGCV1 = map[keyHCert]?.run {
             this[keyEuDgcV1]?.run {
                 toCertificate()
-            } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_DGC)
-        } ?: throw InvalidHealthCertificateException(VC_HC_CWT_NO_HCERT)
-
+            } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_DGC)
+        } ?: throw InvalidVaccinationCertificateException(HC_CWT_NO_HCERT)
         certificate.validate()
     } catch (e: InvalidHealthCertificateException) {
         throw e
     } catch (e: Throwable) {
-        throw InvalidHealthCertificateException(HC_CBOR_DECODING_FAILED)
+        throw InvalidVaccinationCertificateException(HC_CBOR_DECODING_FAILED)
     }
 
     private fun VaccinationDGCV1.validate(): VaccinationDGCV1 {
-        if (vaccinationDatas.isEmpty()) {
-            throw InvalidHealthCertificateException(VC_NO_VACCINATION_ENTRY)
+        if (vaccinationDatas.isNullOrEmpty()) {
+            throw InvalidVaccinationCertificateException(VC_NO_VACCINATION_ENTRY)
         }
         // Force date parsing
         dateOfBirth
@@ -47,7 +48,7 @@ class VaccinationDGCV1Parser @Inject constructor(
         val json = ToJSONString()
         gson.fromJson<VaccinationDGCV1>(json)
     } catch (e: Throwable) {
-        throw InvalidHealthCertificateException(VC_JSON_SCHEMA_INVALID)
+        throw InvalidVaccinationCertificateException(JSON_SCHEMA_INVALID)
     }
 
     companion object {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
index ae3065df3..b987371d4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractor.kt
@@ -2,13 +2,15 @@ package de.rki.coronawarnapp.vaccination.core.qrcode
 
 import de.rki.coronawarnapp.bugreporting.censors.vaccination.CertificateQrCodeCensor
 import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CBOR_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.util.compression.inflate
 import de.rki.coronawarnapp.util.encoding.Base45Decoder
 import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder
 import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
 import de.rki.coronawarnapp.vaccination.core.certificate.RawCOSEObject
 import de.rki.coronawarnapp.vaccination.core.certificate.VaccinationDGCV1Parser
 import timber.log.Timber
@@ -41,21 +43,21 @@ class VaccinationQRCodeExtractor @Inject constructor(
         Base45Decoder.decode(this)
     } catch (e: Throwable) {
         Timber.e(e)
-        throw InvalidHealthCertificateException(HC_BASE45_DECODING_FAILED)
+        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 InvalidHealthCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
+        throw InvalidVaccinationCertificateException(HC_ZLIB_DECOMPRESSION_FAILED)
     }
 
-    fun RawCOSEObject.parse(): VaccinationCertificateData {
+    fun RawCOSEObject.parse(): VaccinationCertificateData = try {
         Timber.v("Parsing COSE for vaccination certificate.")
         val cbor = coseDecoder.decode(this)
 
-        return VaccinationCertificateData(
+        VaccinationCertificateData(
             header = headerParser.parse(cbor),
             certificate = bodyParser.parse(cbor)
         ).also {
@@ -63,6 +65,11 @@ class VaccinationQRCodeExtractor @Inject constructor(
         }.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 {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
index 3e7f60396..c5dae3f0c 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeValidator.kt
@@ -2,8 +2,8 @@ package de.rki.coronawarnapp.vaccination.core.qrcode
 
 import dagger.Reusable
 import de.rki.coronawarnapp.coronatest.qrcode.QrCodeExtractor
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_PREFIX_INVALID
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import timber.log.Timber
 import javax.inject.Inject
 
@@ -19,7 +19,7 @@ class VaccinationQRCodeValidator @Inject constructor(
         return findExtractor(rawString)
             ?.extract(rawString)
             ?.also { Timber.i("Extracted data from QR code is %s", it) }
-            ?: throw InvalidHealthCertificateException(VC_PREFIX_INVALID)
+            ?: throw InvalidVaccinationCertificateException(VC_PREFIX_INVALID)
     }
 
     private fun findExtractor(rawString: String): QrCodeExtractor<VaccinationCertificateQRCode>? {
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
index c134c34fc..7e38a1bbc 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepository.kt
@@ -1,6 +1,8 @@
 package de.rki.coronawarnapp.vaccination.core.repository
 
 import de.rki.coronawarnapp.bugreporting.reportProblem
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.util.coroutine.AppScope
 import de.rki.coronawarnapp.util.coroutine.DispatcherProvider
@@ -9,8 +11,6 @@ import de.rki.coronawarnapp.util.flow.combine
 import de.rki.coronawarnapp.vaccination.core.CertificatePersonIdentifier
 import de.rki.coronawarnapp.vaccination.core.VaccinatedPerson
 import de.rki.coronawarnapp.vaccination.core.VaccinationCertificate
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode
 import de.rki.coronawarnapp.vaccination.core.personIdentifier
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationCertificateQRCode
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
@@ -101,7 +101,7 @@ class VaccinationRepository @Inject constructor(
 
             if (originalPerson.data.vaccinations.any { it.certificateId == qrCode.uniqueCertificateIdentifier }) {
                 Timber.tag(TAG).e("Certificate is already registered: %s", qrCode.uniqueCertificateIdentifier)
-                throw InvalidHealthCertificateException(ErrorCode.VC_ALREADY_REGISTERED)
+                throw InvalidVaccinationCertificateException(VC_ALREADY_REGISTERED)
             }
 
             val newCertificate = qrCode.toVaccinationContainer(
diff --git a/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml b/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml
index 0e998b4cb..3061c74c4 100644
--- a/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml
+++ b/Corona-Warn-App/src/main/res/values-de/green_certificate_strings.xml
@@ -48,4 +48,4 @@
     <string name="info_banner_title_2">COVID-Testzertifikat</string>
     <!-- XTXT: Green certificate info card body  -->
     <string name="info_banner_body">Registrieren Sie einen Test auf der Startseite und stimmen Sie zu, ein digitales Testzertifikat zu erhalten. Sobald das Zertifikat vorliegt, wird es hier angezeigt.</string>
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml b/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml
index 231c92379..946f819e0 100644
--- a/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml
+++ b/Corona-Warn-App/src/main/res/values/green_certificate_strings.xml
@@ -49,4 +49,4 @@
     <!-- XTXT: Green certificate info card body  -->
     <string name="info_banner_body">Registrieren Sie einen Test auf der Startseite und stimmen Sie zu, ein digitales Testzertifikat zu erhalten. Sobald das Zertifikat vorliegt, wird es hier angezeigt.</string>
 
-</resources>
\ No newline at end of file
+</resources>
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt
index 9b4ce4c49..102501c7d 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/coronatest/CoronaTestTestData.kt
@@ -39,8 +39,8 @@ class CoronaTestTestData @Inject constructor(
             TestCertificateDccV1.TestCertificateData(
                 targetId = "840539006",
                 countryOfTest = "DE",
-                sampleCollectedAt = Instant.EPOCH,
-                testResultAt = Instant.EPOCH,
+                sc = Instant.EPOCH.toDateTime().toString(),
+                dr = Instant.EPOCH.toDateTime().toString(),
                 testCenter = "TODO",
                 testName = "TODO",
                 testNameAndManufactor = "TODO",
@@ -100,8 +100,8 @@ class CoronaTestTestData @Inject constructor(
             TestCertificateDccV1.TestCertificateData(
                 targetId = "840539006",
                 countryOfTest = "DE",
-                sampleCollectedAt = Instant.EPOCH,
-                testResultAt = Instant.EPOCH,
+                sc = Instant.EPOCH.toDateTime().toString(),
+                dr = Instant.EPOCH.toDateTime().toString(),
                 testCenter = "TODO",
                 testName = "TODO",
                 testNameAndManufactor = "TODO",
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt
new file mode 100644
index 000000000..1d9ecfde2
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/cryptography/AesCryptographyTest.kt
@@ -0,0 +1,18 @@
+package de.rki.coronawarnapp.covidcertificate.cryptography
+
+import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class AesCryptographyTest : BaseTest() {
+
+    @Test
+    fun `decrypt Hello World`() {
+        val des = "d56t/juMw5r4qNx1n1igs1pobUjZBT5yq0Ct7MHUuKM=".toByteArray()
+        val encryptedString = "WFOLewp8DWqY/8IWUHEDwg==".toByteArray()
+        AesCryptography().decrypt(
+            des,
+            encryptedData = encryptedString
+        ) shouldBe "Hello World".toByteArray()
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt
new file mode 100644
index 000000000..64d989fb9
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateDccParserTest.kt
@@ -0,0 +1,42 @@
+package de.rki.coronawarnapp.covidcertificate.test
+
+import com.google.gson.Gson
+import com.upokecenter.cbor.CBORObject
+import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.decodeHex
+import org.joda.time.LocalDate
+import org.junit.jupiter.api.Test
+
+class TestCertificateDccParserTest {
+
+    private val bodyParser = TestCertificateDccParser(Gson())
+
+    @Test
+    fun `happy path cose decryption with Ellen Cheng`() {
+        val coseObject = CBORObject.DecodeFromBytes(TestData.cborObject.decodeHex().toByteArray())
+        with(bodyParser.parse(coseObject)) {
+
+            with(nameData) {
+                familyName shouldBe "Musterfrau-Gößinger"
+                familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER"
+                givenName shouldBe "Gabriele"
+                givenNameStandardized shouldBe "GABRIELE"
+            }
+            dob shouldBe "1998-02-26"
+            dateOfBirth shouldBe LocalDate.parse("1998-02-26")
+            version shouldBe "1.2.1"
+
+            with(testCertificateData[0]) {
+                uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3"
+                countryOfTest shouldBe "AT"
+                certificateIssuer shouldBe "Ministry of Health, Austria"
+                targetId shouldBe "840539006"
+                sampleCollectedAt shouldBe org.joda.time.Instant.parse("2021-02-20T12:34:56+00:00")
+                testType shouldBe "LP217198-3"
+                testCenter shouldBe "Testing center Vienna 1"
+                testNameAndManufactor shouldBe "1232"
+                testResult shouldBe "260415000"
+            }
+        }
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt
new file mode 100644
index 000000000..ef3f817ac
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestCertificateQRCodeExtractorTest.kt
@@ -0,0 +1,121 @@
+package de.rki.coronawarnapp.covidcertificate.test
+
+import com.google.gson.Gson
+import de.rki.coronawarnapp.covidcertificate.cryptography.AesCryptography
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidTestCertificateException
+import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData
+import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateCOSEDecoder
+import de.rki.coronawarnapp.vaccination.core.certificate.HealthCertificateHeaderParser
+import io.kotest.assertions.throwables.shouldThrow
+import io.kotest.matchers.shouldBe
+import okio.ByteString.Companion.decodeBase64
+import org.joda.time.Instant
+import org.joda.time.LocalDate
+import org.junit.jupiter.api.Test
+import testhelpers.BaseTest
+
+class TestCertificateQRCodeExtractorTest : BaseTest() {
+    private val coseDecoder = HealthCertificateCOSEDecoder(AesCryptography())
+    private val headerParser = HealthCertificateHeaderParser()
+    private val bodyParser = TestCertificateDccParser(Gson())
+    private val extractor = TestCertificateQRCodeExtractor(coseDecoder, headerParser, bodyParser)
+
+    @Test
+    fun `happy path qr code`() {
+        val qrCode = extractor.extract(TestData.qrCodeTestCertificate)
+        with(qrCode.testCertificateData.header) {
+            issuer shouldBe "AT"
+            issuedAt shouldBe Instant.parse("2021-06-01T10:12:48.000Z")
+            expiresAt shouldBe Instant.parse("2021-06-03T10:12:48.000Z")
+        }
+
+        with(qrCode.testCertificateData.certificate) {
+            with(nameData) {
+                familyName shouldBe "Musterfrau-Gößinger"
+                familyNameStandardized shouldBe "MUSTERFRAU<GOESSINGER"
+                givenName shouldBe "Gabriele"
+                givenNameStandardized shouldBe "GABRIELE"
+            }
+            dob shouldBe "1998-02-26"
+            dateOfBirth shouldBe LocalDate.parse("1998-02-26")
+            version shouldBe "1.2.1"
+
+            with(testCertificateData[0]) {
+                uniqueCertificateIdentifier shouldBe "URN:UVCI:01:AT:71EE2559DE38C6BF7304FB65A1A451EC#3"
+                countryOfTest shouldBe "AT"
+                certificateIssuer shouldBe "Ministry of Health, Austria"
+                targetId shouldBe "840539006"
+                sampleCollectedAt shouldBe Instant.parse("2021-02-20T12:34:56+00:00")
+                testType shouldBe "LP217198-3"
+                testCenter shouldBe "Testing center Vienna 1"
+                testNameAndManufactor shouldBe "1232"
+                testResult shouldBe "260415000"
+            }
+        }
+    }
+
+    @Test
+    fun `happy path cose decryption with Ellen Cheng`() {
+        with(TestData.EllenCheng()) {
+            val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray()
+            val dek = dek.toByteArray()
+            val result = extractor.extract(dek, coseObject)
+            with(result.testCertificateData.certificate.nameData) {
+                familyName shouldBe "Cheng"
+                givenName shouldBe "Ellen"
+            }
+            val result2 = extractor.extract(result.qrCode)
+            with(result2.testCertificateData.certificate.nameData) {
+                familyName shouldBe "Cheng"
+                givenName shouldBe "Ellen"
+            }
+        }
+    }
+
+    @Test
+    fun `happy path cose decryption with Brian Calamandrei`() {
+        with(TestData.BrianCalamandrei()) {
+            val coseObject = coseWithEncryptedPayload.decodeBase64()!!.toByteArray()
+            val dek = dek.toByteArray()
+            val result = extractor.extract(dek, coseObject)
+            with(result.testCertificateData.certificate.nameData) {
+                familyName shouldBe "Calamandrei"
+                givenName shouldBe "Brian"
+            }
+            val result2 = extractor.extract(result.qrCode)
+            with(result2.testCertificateData.certificate.nameData) {
+                familyName shouldBe "Calamandrei"
+                givenName shouldBe "Brian"
+            }
+        }
+    }
+
+    @Test
+    fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() {
+        shouldThrow<InvalidTestCertificateException> {
+            extractor.extract(VaccinationQrCodeTestData.validEncoded)
+        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS
+    }
+
+    @Test
+    fun `random string fails with HC_BASE45_DECODING_FAILED`() {
+        shouldThrow<InvalidTestCertificateException> {
+            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> {
+            extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:")
+        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+    }
+
+    @Test
+    fun `certificate missing fails with VC_NO_VACCINATION_ENTRY`() {
+        shouldThrow<InvalidTestCertificateException> {
+            extractor.extract(VaccinationQrCodeTestData.certificateMissing)
+        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.NO_TEST_ENTRY
+    }
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java
new file mode 100644
index 000000000..035f48b7f
--- /dev/null
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/covidcertificate/test/TestData.java
@@ -0,0 +1,20 @@
+package de.rki.coronawarnapp.covidcertificate.test;
+
+public class TestData {
+
+    static public class EllenCheng {
+        public String dek = "/9o5eVNb9us5CsGD4F3J36Ju1enJ71Y6+FpVvScGWkE=";
+        public String coseWithDecryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAWGkAWtjd2EtYXBwLWNsaQQaYpdzwAYaYLZAQDkBA6EBpGN2ZXJlMS4wLjBjbmFtpGJnbmVFbGxlbmJmbmVDaGVuZ2NnbnRlRUxMRU5jZm50ZUNIRU5HY2RvYmoxOTY3LTA3LTE5YXSBq2J0Z2k4NDA1MzkwMDZidHRqTFAyMTcxOTgtM2JubXgZSVZDSUoxR1FaTTc4NzhFTE1VSU1BMDhER2JtYWQxMjQ0YnNjeBgyMDIxLTA1LTMxVDE2OjEzOjE2LjgzNlpiZHJ4GDIwMjEtMDYtMDFUMTQ6MTI6MTYuODM2WmJ0cmkyNjA0MTUwMDBidGNtVGVzdCBDZW50cmUgMWJjb2JERWJpc3ghQnVuZGVzbWluaXN0ZXJpdW0gZsO8ciBHZXN1bmRoZWl0YmNpeC8wMURFLzAwMDAwLzExMTkzNDkwMDcvRzdQU0JBWE1YVkEyTjBITTNVOFlWN1pNVFhA1NqKSb93S2El9dA0icVjK+DV4LbwVWajZmTmhqcsgzWhvl4/PmtAJ5/iT57FfoQvuOvlyhxRPgGSg33IuDnBCg==";
+        public String coseWithEncryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAXBxvo73+06cLc73F5KIFuQdo7fLUnb7yF9QFtX9tIEmgSzHIXKbHcEiep5RTtb2UVS80vybmnwYa1k36HR2R2yTKGwvDWAUumw2ZjCnfp8CxKx3zQVRl6JrVdLiskWmo4qiK/EwyTHrw/5PZy4rd11vt9Y6wuZtlpOvFGDIDhGKpcgK93zfIQWY59xjxusr/4J3FCWpcy9YNehB6m4Az1NozXxOrL9DmFM38mWCkiHaPeWgedbqfKTg3x/vSrXSkXYnLpc6QHsRqW99r7yTXJffbK8X44KvgkUI9sIlVU5+2+IuwT4XBY2p/MLW4d9gfnAhZYTsn0nGuoj4KFHTo6fNkXsuZ6BWm5MurXR0dqiCd00B1ZKuTNV0QhdzaaB2pYtwBnxD65TW8D0VDrDDjZuYRzni032f5hgB7YDlvcWYWiv7o6T8DeCNAsJ0RdL/X1qe3bHvLOBvzF9XlTrg4vNF/3aeRn9libOf+0ufr5dEcVhA1NqKSb93S2El9dA0icVjK+DV4LbwVWajZmTmhqcsgzWhvl4/PmtAJ5/iT57FfoQvuOvlyhxRPgGSg33IuDnBCg==";
+    }
+
+    static public class BrianCalamandrei {
+        public String dek = "RinMlpTdzQGw7kllamU9Pz6bTHEWVZi1Ocb4q8wfFSk=";
+        public String coseWithEncryptedPayload = "0oRNogEmBEiLxYhcyl5BXkBZAXAN4hKvLEngs3MYcLe/cIyy0q0+0auk5A/Bme/WlymolXU8JSLJJcj3D7kwXCJoEOvsnU9P/IrVlTNF2fJBdWF6Oq22UzhyOPRuQiF7PvspbVzyeEo+H/PmtvbTZss6l/wLDoXPixjtCOaFn7Com6Z2pNQOZqkYGZzz4BanJfchoggM4HaH20H4AzANfMMqYa/rytHnz4BjlR82ZSOlg5e/Jbl78NRen6RkgLTwT3YSI1XV+gbLPK6Fhp5saqRmQgUQTTSO99Q/rdk6BG18RZxqw70zKb77ddxBzolgySbmRdUrpWdK9SvnsnivN3V1Auv5X18KpHO58SwyFoex7OUq73q6FAS9p+MdI2jh1e3LcwU12ZJaN56bRbTEAmT5MelZsYY+c6WWvcIND7tj3aDI5o8D9PyWZHPdz/uHn/Cesn7MgVEXvLQnfCVvuPkLDSGAGi47nRRmUoaN7+7GjPRYvTyrX5VWwnMK31QLADg9kFhAQ7d9IZ02KQ5OXt/fc3bpcombylOcXT2U+JXDwQadrFwHQdjeK1dw+RZM7UkD4l/TOQjO9B8JN13DlidhiqljGw==";
+        public String coseWithDecryptedPayload = "kzriyBRYZu5L/VqCz6qsomYv8vjOdjXp/giOGRa8vpXBM7Mjc3mjGaSUpHy8QLlCrFfrDAh8vwY4PKsI/ad/jFyEpyZwI13R6PW+Vft1Ld1nxHD74BwiNbhNv1ZjlmDohQsHXaq6MEbfNfT/uqPbZHToZlTPO3V9aeOD0E+kuMx8npVIbB+zbL48AS3VsrVyYthINdpplYmNsXyFdxsCvZfefGO3G5XIRsDfMBqFPVZO3uCIq1nLb4U8dcbZ5O2yrMG5UOJ2fiwWiiqSLevyCnlYjOvyv8Ot6nM1X7fncXwze3i4OT7Y/Y+36SAds/bCcpypOso0QIim8BBVecy3MN7bQOivc/DKyRX9dHHtc5iD/dgT5i5HBwFEcvTqAa/rlJyTSO4XL6CRevHD7lt3wdd/guRMWxilY1kgjao1w/KEum48frD/FD+J2IgFX+R9aP0CLqvvwvC8OuIj8O+F1WlGp55gKEd+Ps35o1Y8UfHyKDTUMVN5yZno6V15bVFP";
+    }
+
+    public static String qrCodeTestCertificate = "HC1:NCFOXN%TS3DH3ZSUZK+.V0ETD%65NL-AH+VEIOOP-IARC$E08WAN$6%W0AT4V22F/8X*G3M9BM9Z0BFU2P4JY73JC3KD34LT7A3523*BBXSJ$IJGX8R/S+:KLD3JJ3.+IJYCDN0TA3RK37MBZD3K%I17JLXKB6J57TJK57ALD-I3 28XGL4LQ/SLE50$499TVU1PK9PYLPN1VUU8C1VTE5ZM376 IE2IML07Z ESF6C.M1EE*2NY*6UJE4:6Q56FPEXM6IS6-Q64Y6N$E.%66PP33M19V2+PFQ51C5EWAC1A.GUQ$9WC54FQ68ENV0XTCM*4CZKHKB-43.E3KD3OAJA70:ZH/O1:O1AT1NQ1 WUQRENS431TN$IK.G47HB%0WT072HFHN/SNP.0FVV/SN7Y431TCRV$.PTW5CL52U50$EZ*N4IU4FKCPACPI2YUFJ6LX3+KG% BTVB3UQFJ6GL28LHXOAYJAUVPQRHIY1* HOQ1PRAAUICO1 IGBY05*H%XGU-PNQCSJ1A+3HJ5VBQR6G/:DS3GH9FW-SAOPNRFBYMBPJEODEN6%99SLNS9KKFC$OKT1GVKJTCW.IIX2S3V0$1D243B/J.H1RJVN01HI7T.H";
+
+    public static String cborObject = "a4041a60bc9f38061a60b9fc3801624154390103a101a4617481a962736374323032312d30322d32305431323a33343a35365a626d6164313233326274746a4c503231373139382d336274637754657374696e672063656e746572205669656e6e61203162636f624154626369783155524e3a555643493a30313a41543a37314545323535394445333843364246373330344642363541314134353145432333626973781b4d696e6973747279206f66204865616c74682c20417573747269616274676938343035333930303662747269323630343135303030636e616da463666e74754d5553544552465241553c474f455353494e47455262666e754d7573746572667261752d47c3b6c39f696e67657263676e74684741425249454c4562676e684761627269656c656376657265312e322e3163646f626a313939382d30322d3236";
+}
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt
index 571eff96c..faac0915b 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/VaccinatedPersonIdentifierTest.kt
@@ -1,7 +1,8 @@
 package de.rki.coronawarnapp.vaccination.core
 
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_DOB_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import io.kotest.assertions.throwables.shouldNotThrowAny
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
@@ -54,16 +55,16 @@ class VaccinatedPersonIdentifierTest : BaseTest() {
             testPersonMaxData.requireMatch(testPersonMaxData)
         }
 
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(firstNameStandardized = "nope"))
-        }.errorCode shouldBe ErrorCode.VC_NAME_MISMATCH
+        }.errorCode shouldBe VC_NAME_MISMATCH
 
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(lastNameStandardized = "nope"))
-        }.errorCode shouldBe ErrorCode.VC_NAME_MISMATCH
+        }.errorCode shouldBe VC_NAME_MISMATCH
 
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             testPersonMaxData.requireMatch(testPersonMaxData.copy(dateOfBirth = LocalDate.parse("1900-12-31")))
-        }.errorCode shouldBe ErrorCode.VC_DOB_MISMATCH
+        }.errorCode shouldBe VC_DOB_MISMATCH
     }
 }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
index ef2d3bd05..5b125bfcc 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/qrcode/VaccinationQRCodeExtractorTest.kt
@@ -1,13 +1,13 @@
 package de.rki.coronawarnapp.vaccination.core.qrcode
 
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_CWT_NO_ISS
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent
 import de.rki.coronawarnapp.vaccination.core.VaccinationQrCodeTestData
 import de.rki.coronawarnapp.vaccination.core.VaccinationTestData
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_BASE45_DECODING_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.HC_ZLIB_DECOMPRESSION_FAILED
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_HC_CWT_NO_ISS
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException.ErrorCode.VC_NO_VACCINATION_ENTRY
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.matchers.shouldBe
 import org.joda.time.Instant
@@ -80,29 +80,29 @@ class VaccinationQRCodeExtractorTest : BaseTest() {
     }
 
     @Test
-    fun `valid encoding but not a health certificate fails with VC_HC_CWT_NO_ISS`() {
-        shouldThrow<InvalidHealthCertificateException> {
+    fun `valid encoding but not a health certificate fails with HC_CWT_NO_ISS`() {
+        shouldThrow<InvalidVaccinationCertificateException> {
             extractor.extract(VaccinationQrCodeTestData.validEncoded)
-        }.errorCode shouldBe VC_HC_CWT_NO_ISS
+        }.errorCode shouldBe HC_CWT_NO_ISS
     }
 
     @Test
     fun `random string fails with HC_BASE45_DECODING_FAILED`() {
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             extractor.extract("nothing here to see")
         }.errorCode shouldBe HC_BASE45_DECODING_FAILED
     }
 
     @Test
     fun `uncompressed base45 string fails with HC_ZLIB_DECOMPRESSION_FAILED`() {
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             extractor.extract("6BFOABCDEFGHIJKLMNOPQRSTUVWXYZ %*+-./:")
         }.errorCode shouldBe HC_ZLIB_DECOMPRESSION_FAILED
     }
 
     @Test
     fun `vaccination certificate missing fails with VC_NO_VACCINATION_ENTRY`() {
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             extractor.extract(VaccinationQrCodeTestData.certificateMissing)
         }.errorCode shouldBe VC_NO_VACCINATION_ENTRY
     }
diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt
index f0703302c..5b100c24c 100644
--- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt
+++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/vaccination/core/repository/VaccinationRepositoryTest.kt
@@ -1,9 +1,11 @@
 package de.rki.coronawarnapp.vaccination.core.repository
 
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+import de.rki.coronawarnapp.covidcertificate.exception.InvalidVaccinationCertificateException
 import de.rki.coronawarnapp.util.TimeStamper
 import de.rki.coronawarnapp.vaccination.core.DaggerVaccinationTestComponent
 import de.rki.coronawarnapp.vaccination.core.VaccinationTestData
-import de.rki.coronawarnapp.vaccination.core.certificate.InvalidHealthCertificateException
 import de.rki.coronawarnapp.vaccination.core.qrcode.VaccinationQRCodeExtractor
 import de.rki.coronawarnapp.vaccination.core.repository.errors.VaccinationCertificateNotFoundException
 import de.rki.coronawarnapp.vaccination.core.repository.storage.VaccinatedPersonData
@@ -109,9 +111,9 @@ class VaccinationRepositoryTest : BaseTest() {
         val instance = createInstance(this)
         advanceUntilIdle()
 
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             instance.registerVaccination(vaccinationTestData.personBVac1QRCode)
-        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.VC_NAME_MISMATCH
+        }.errorCode shouldBe VC_NAME_MISMATCH
 
         testStorage shouldBe setOf(vaccinationTestData.personAData2Vac)
     }
@@ -127,9 +129,9 @@ class VaccinationRepositoryTest : BaseTest() {
         val instance = createInstance(this)
         advanceUntilIdle()
 
-        shouldThrow<InvalidHealthCertificateException> {
+        shouldThrow<InvalidVaccinationCertificateException> {
             instance.registerVaccination(vaccinationTestData.personAVac1QRCode)
-        }.errorCode shouldBe InvalidHealthCertificateException.ErrorCode.VC_ALREADY_REGISTERED
+        }.errorCode shouldBe VC_ALREADY_REGISTERED
 
         testStorage.first() shouldBe dataBefore
     }
-- 
GitLab