From aee21f7fc2c83367e66a2f378b0195fdc68f9117 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakob=20M=C3=B6ller?= <jakob.moeller@sap.com>
Date: Sun, 7 Jun 2020 11:30:33 +0200
Subject: [PATCH] Setup Trust Anchors (Public Keys for Signature Verification)
 (#232)

* Setup Trust Anchors

Signed-off-by: d067928 <jakob.moeller@sap.com>

* Make Verification of the Export File dependant on build type

Signed-off-by: d067928 <jakob.moeller@sap.com>

* Reformatting after Merge

Signed-off-by: d067928 <jakob.moeller@sap.com>
---
 Corona-Warn-App/build.gradle                  |  39 ++-------
 ...er-public-keys-for-verification.properties |   2 +
 .../src/main/assets/trusted-certs-cwa.bks     | Bin 426 -> 0 bytes
 .../coronawarnapp/http/WebRequestBuilder.kt   |   5 +-
 .../util/security/SecurityConstants.kt        |  16 ++++
 .../util/security/SecurityHelper.kt           |  57 ++-----------
 .../util/security/VerificationKeys.kt         |  78 ++++++++++++++++++
 7 files changed, 114 insertions(+), 83 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/assets/export-server-public-keys-for-verification.properties
 delete mode 100644 Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityConstants.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index 1269d2c9a..8d2b3aa6a 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -40,7 +40,7 @@ android {
         buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\""
         buildConfigField "String", "SUBMISSION_CDN_URL", "\"$SUBMISSION_CDN_URL\""
         buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_CDN_URL\""
-        buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\""
+        buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp-dev\""
 
         //override URLs with local variables
         Properties properties = new Properties()
@@ -63,9 +63,6 @@ android {
             if (VERIFICATION_CDN_URL)
                 buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_CDN_URL\""
 
-            def TRUSTED_CERTS_EXPORT_KEYSTORE_PW = properties.getProperty('TRUSTED_CERTS_EXPORT_KEYSTORE_PW')
-            if (TRUSTED_CERTS_EXPORT_KEYSTORE_PW)
-                buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\""
         }
     }
 
@@ -74,38 +71,14 @@ android {
             minifyEnabled true
             shrinkResources true
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-
-            println "SECRET INPUT"
-            Properties properties = new Properties()
-            def secretFile = project.rootProject.file('secrets.properties')
-            if (secretFile.exists())
-                properties.load(secretFile.newDataInputStream())
-
-            def TRUSTED_CERTS_EXPORT_KEYSTORE_PW = properties.getProperty('TRUSTED_CERTS_EXPORT_KEYSTORE_PW')
-            if (TRUSTED_CERTS_EXPORT_KEYSTORE_PW) {
-                println "TRUSTED_CERTS_EXPORT_KEYSTORE_PW:$TRUSTED_CERTS_EXPORT_KEYSTORE_PW"
-                buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\""
-            }
-            println "SECRET END"
+            buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp\""
         }
         releaseForTest {
             applicationIdSuffix '.dev'
             minifyEnabled true
             shrinkResources true
             proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
-
-            println "SECRET INPUT"
-            Properties properties = new Properties()
-            def secretFile = project.rootProject.file('secrets.properties')
-            if (secretFile.exists())
-                properties.load(secretFile.newDataInputStream())
-
-            def TRUSTED_CERTS_EXPORT_KEYSTORE_PW = properties.getProperty('TRUSTED_CERTS_EXPORT_KEYSTORE_PW')
-            if (TRUSTED_CERTS_EXPORT_KEYSTORE_PW) {
-                println "TRUSTED_CERTS_EXPORT_KEYSTORE_PW:$TRUSTED_CERTS_EXPORT_KEYSTORE_PW"
-                buildConfigField "String", "TRUSTED_CERTS_EXPORT_KEYSTORE_PW", "\"$TRUSTED_CERTS_EXPORT_KEYSTORE_PW\""
-            }
-            println "SECRET END"
+            buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp-dev\""
         }
     }
 
@@ -215,10 +188,10 @@ dependencies {
 
     // Play Services
     implementation 'com.google.android.play:core:1.7.3'
-    implementation 'com.google.android.gms:play-services-base:17.2.1'
-    implementation 'com.google.android.gms:play-services-basement:17.2.1'
+    implementation 'com.google.android.gms:play-services-base:17.3.0'
+    implementation 'com.google.android.gms:play-services-basement:17.3.0'
     implementation 'com.google.android.gms:play-services-safetynet:17.0.0'
-    implementation 'com.google.android.gms:play-services-tasks:17.0.2'
+    implementation 'com.google.android.gms:play-services-tasks:17.1.0'
     api fileTree(dir: 'libs', include: ['play-services-nearby-18.0.2-eap.aar'])
 
     // HTTP
diff --git a/Corona-Warn-App/src/main/assets/export-server-public-keys-for-verification.properties b/Corona-Warn-App/src/main/assets/export-server-public-keys-for-verification.properties
new file mode 100644
index 000000000..39a3689f7
--- /dev/null
+++ b/Corona-Warn-App/src/main/assets/export-server-public-keys-for-verification.properties
@@ -0,0 +1,2 @@
+de.rki.coronawarnapp-dev=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3BYTxr2HuJYQG+d7Ezu6KS8GEbFkiEvyJFg0j+C839gTjT6j7Ho0EXXZ/a07ZfvKcC2cmc1SunsrqU9Jov1J5Q==
+de.rki.coronawarnapp=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==
\ No newline at end of file
diff --git a/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks b/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks
deleted file mode 100644
index e67a6926a60ef6968aa6532d1fbbce7bb7eb0012..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 426
zcmZQzU|?ckU=YdGFYlJGU-Du}6r1e){X9!dS7qxlFtG1pWROTMPgKau&(kd^%1=>9
zPAw|QOv_A8EJ<ZxU@U4m7-t96&cMJLp=WAf2^4ZRXkxTCXnf8jlDUx~vP)x;Vy<IH
zf&wdh`s&nBQ3EbE4y`tibG9tZOa{t^iU#s*%%LpIJQB{~jtYK2O9DWaI3p}EkQ3)M
zGB7YTvM@3*u`n=*0&)$JxWpL}X&}tT4z`zx5$XVDMs{W=29`Tw!pHWu@0cba{k&S(
zdY7g?o8ZQj4)0GY5hnc)_T0ZA+-tY^O_hmY>CL}uty6!WD$t!X^K8(rYVDQ&o{Ro^
zK1KErvpa)<3zI@r*pm%CTiVaRc(o<b&CZHzX1v4ElX__rR+ZkVuJI`CW>QG8=Ra&)
zAia`3Y;pTp%PY&TW_^77blrV*dA@|`l=V!#3|8mft)D*e?wU2t<tBa$ruN50=*<8C
DwgQff

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
index 5e8369c71..b33fa7103 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/http/WebRequestBuilder.kt
@@ -34,6 +34,7 @@ import de.rki.coronawarnapp.storage.FileStorageHelper
 import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat
 import de.rki.coronawarnapp.util.ZipHelper.unzip
 import de.rki.coronawarnapp.util.security.SecurityHelper
+import de.rki.coronawarnapp.util.security.VerificationKeys
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.withContext
 import java.io.File
@@ -52,6 +53,8 @@ object WebRequestBuilder {
     private val verificationService by lazy { serviceFactory.verificationService() }
     private val submissionService by lazy { serviceFactory.submissionService() }
 
+    private val verificationKeys = VerificationKeys()
+
     suspend fun asyncGetDateIndex(): List<String> = withContext(Dispatchers.IO) {
         return@withContext distributionService
             .getDateIndex(DiagnosisKeyConstants.AVAILABLE_DATES_URL).toList()
@@ -100,7 +103,7 @@ object WebRequestBuilder {
                 throw ApplicationConfigurationInvalidException()
             }
 
-            if (!SecurityHelper.exportFileIsValid(exportBinary, exportSignature)) {
+            if (verificationKeys.hasInvalidSignature(exportBinary, exportSignature)) {
                 throw ApplicationConfigurationCorruptException()
             }
 
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityConstants.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityConstants.kt
new file mode 100644
index 000000000..a489b1603
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityConstants.kt
@@ -0,0 +1,16 @@
+package de.rki.coronawarnapp.util.security
+
+import de.rki.coronawarnapp.BuildConfig
+
+object SecurityConstants {
+    const val DIGEST_ALGORITHM = "SHA-256"
+    const val DB_PASSWORD_MIN_LENGTH = 32
+    const val DB_PASSWORD_MAX_LENGTH = 48
+    const val CWA_APP_SQLITE_DB_PW = "CWA_APP_SQLITE_DB_PW"
+    const val ENCRYPTED_SHARED_PREFERENCES_FILE = "shared_preferences_cwa"
+
+    const val EXPORT_SIGNATURE_VERIFICATION_PUBLIC_KEYS =
+        "export-server-public-keys-for-verification.properties"
+    const val EXPORT_ENVIRONMENT_IDENTIFIER = BuildConfig.EXPORT_SIGNATURE_ID
+    const val EXPORT_FILE_SIGNATURE_VERIFICATION_ALGORITHM = "SHA256withECDSA"
+}
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
index 91ac13d91..0b095a9f4 100644
--- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SecurityHelper.kt
@@ -19,7 +19,6 @@
 
 package de.rki.coronawarnapp.util.security
 
-import KeyExportFormat.TEKSignatureList
 import android.annotation.SuppressLint
 import android.content.Context
 import android.content.SharedPreferences
@@ -27,34 +26,26 @@ import android.os.Build
 import android.util.Base64
 import androidx.security.crypto.EncryptedSharedPreferences
 import androidx.security.crypto.MasterKeys
-import de.rki.coronawarnapp.BuildConfig
 import de.rki.coronawarnapp.CoronaWarnApplication
 import de.rki.coronawarnapp.exception.CwaSecurityException
-import java.security.KeyStore
+import de.rki.coronawarnapp.util.security.SecurityConstants.CWA_APP_SQLITE_DB_PW
+import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MAX_LENGTH
+import de.rki.coronawarnapp.util.security.SecurityConstants.DB_PASSWORD_MIN_LENGTH
+import de.rki.coronawarnapp.util.security.SecurityConstants.DIGEST_ALGORITHM
+import de.rki.coronawarnapp.util.security.SecurityConstants.ENCRYPTED_SHARED_PREFERENCES_FILE
 import java.security.MessageDigest
 import java.security.SecureRandom
-import java.security.Signature
-import java.security.cert.Certificate
 
 /**
  * Key Store and Password Access
  */
 object SecurityHelper {
-    private const val CWA_APP_SQLITE_DB_PW = "CWA_APP_SQLITE_DB_PW"
-    private const val DB_PASSWORD_MIN_LENGTH = 32
-    private const val DB_PASSWORD_MAX_LENGTH = 48
-    private const val SHARED_PREF_NAME = "shared_preferences_cwa"
     private val keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
     private val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec)
 
-    private const val EXPORT_SIGNATURE_ALGORITHM = "SHA256withECDSA"
-    private const val CWA_EXPORT_CERTIFICATE_NAME_NON_PROD = "cwa non-prod certificate"
-
-    private const val CWA_EXPORT_CERTIFICATE_KEY_STORE = "trusted-certs-cwa.bks"
-
     val globalEncryptedSharedPreferencesInstance: SharedPreferences by lazy {
         withSecurityCatch {
-            CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME)
+            CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(ENCRYPTED_SHARED_PREFERENCES_FILE)
         }
     }
 
@@ -119,43 +110,11 @@ object SecurityHelper {
     }
 
     fun hash256(input: String): String = MessageDigest
-        .getInstance("SHA-256")
+        .getInstance(DIGEST_ALGORITHM)
         .digest(input.toByteArray())
         .fold("", { str, it -> str + "%02x".format(it) })
 
-    fun exportFileIsValid(export: ByteArray?, sig: ByteArray?) = withSecurityCatch {
-        Signature.getInstance(EXPORT_SIGNATURE_ALGORITHM).run {
-            initVerify(trustedCertForSignature)
-            update(export)
-            verify(
-                TEKSignatureList
-                    .parseFrom(sig)
-                    .signaturesList
-                    .first()
-                    .signature
-                    .toByteArray()
-            )
-        }
-    }
-
-    private val cwaKeyStore: KeyStore by lazy {
-        val keystoreFile = CoronaWarnApplication.getAppContext()
-            .assets.open(CWA_EXPORT_CERTIFICATE_KEY_STORE)
-        val keystore = KeyStore.getInstance(KeyStore.getDefaultType())
-        val keyStorePw = BuildConfig.TRUSTED_CERTS_EXPORT_KEYSTORE_PW
-        val password = keyStorePw.toCharArray()
-        if (password.isEmpty())
-            throw NullPointerException("TRUSTED_CERTS_EXPORT_KEYSTORE_PW is null")
-        keystore.load(keystoreFile, password)
-        keystore
-    }
-
-    private val trustedCertForSignature: Certificate by lazy {
-        val alias = CWA_EXPORT_CERTIFICATE_NAME_NON_PROD
-        cwaKeyStore.getCertificate(alias)
-    }
-
-    private fun <T> withSecurityCatch(doInCatch: () -> T) = try {
+    fun <T> withSecurityCatch(doInCatch: () -> T) = try {
         doInCatch.invoke()
     } catch (e: Exception) {
         throw CwaSecurityException(e)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt
new file mode 100644
index 000000000..6eb6c1e8d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt
@@ -0,0 +1,78 @@
+package de.rki.coronawarnapp.util.security
+
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import android.util.Log
+import de.rki.coronawarnapp.BuildConfig
+import de.rki.coronawarnapp.CoronaWarnApplication
+import de.rki.coronawarnapp.util.security.SecurityConstants.EXPORT_ENVIRONMENT_IDENTIFIER
+import de.rki.coronawarnapp.util.security.SecurityConstants.EXPORT_SIGNATURE_VERIFICATION_PUBLIC_KEYS
+import java.security.KeyFactory
+import java.security.Signature
+import java.security.spec.X509EncodedKeySpec
+import java.util.Properties
+
+class VerificationKeys {
+    companion object {
+        private val TAG = VerificationKeys::class.java.simpleName
+    }
+
+    private val keyFactory = KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_EC)
+    private val signature =
+        Signature.getInstance(SecurityConstants.EXPORT_FILE_SIGNATURE_VERIFICATION_ALGORITHM)
+
+    private val verificationKeyProperties = Properties().apply {
+        load(
+            CoronaWarnApplication
+                .getAppContext()
+                .assets
+                .open(EXPORT_SIGNATURE_VERIFICATION_PUBLIC_KEYS)
+        )
+    }
+
+    fun hasInvalidSignature(
+        export: ByteArray?,
+        signatureListBinary: ByteArray?
+    ): Boolean = SecurityHelper.withSecurityCatch {
+        signature.getValidSignaturesForExport(export, signatureListBinary)
+            .isEmpty()
+            .also {
+                if (BuildConfig.DEBUG) {
+                    if (it) Log.d(TAG, "export is valid")
+                    else Log.d(TAG, "export is invalid")
+                }
+            }
+    }
+
+    private fun Signature.getValidSignaturesForExport(
+        export: ByteArray?,
+        signatures: ByteArray?
+    ) = getKeysForSignatureVerificationFilteredByEnvironment()
+        .flatMap { filteredIdAndKeyBinary ->
+            getTEKSignaturesForEnvironment(signatures)
+                .filter { signatureBinary ->
+                    initVerify(filteredIdAndKeyBinary.value)
+                    update(export)
+                    verify(signatureBinary)
+                }
+                .toList()
+        }
+        .also { Log.v(TAG, "${it.size} valid signatures found") }
+
+    private fun getKeysForSignatureVerificationFilteredByEnvironment() = verificationKeyProperties
+        .entries
+        .associate { it.key as String to Base64.decode(it.value as String, Base64.DEFAULT) }
+        .mapValues { keyFactory.generatePublic(X509EncodedKeySpec(it.value)) }
+        .onEach { Log.v(TAG, "$it") }
+        .filterKeys { publicKeyIdentifier -> publicKeyIdentifier == EXPORT_ENVIRONMENT_IDENTIFIER }
+
+    private fun getTEKSignaturesForEnvironment(
+        signatureListBinary: ByteArray?
+    ) = KeyExportFormat.TEKSignatureList
+        .parseFrom(signatureListBinary)
+        .signaturesList
+        .asSequence()
+        .filter { TEKSig -> TEKSig.signatureInfo.appBundleId == EXPORT_ENVIRONMENT_IDENTIFIER }
+        .onEach { Log.v(TAG, "$it") }
+        .mapNotNull { it.signature.toByteArray() }
+}
-- 
GitLab