From 3bfeb0b5db79e3a6e4ab0b34acfe6377e1367bd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakob=20M=C3=B6ller?= <jakob.moeller@sap.com>
Date: Fri, 5 Jun 2020 18:26:56 +0200
Subject: [PATCH] =?UTF-8?q?Implement=20SignatureCheck=20based=20on=20BKS?=
 =?UTF-8?q?=20Key=20Store=20Certificate=20for=20Appli=E2=80=A6=20(#206)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Implement SignatureCheck based on BKS Key Store Certificate for ApplicationConfig

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

* Lint Check

Signed-off-by: d067928 <jakob.moeller@sap.com>
---
 Corona-Warn-App/build.gradle                  |   8 +++
 .../src/main/assets/trusted-certs-cwa.bks     | Bin 0 -> 426 bytes
 ...pplicationConfigurationCorruptException.kt |   7 ++
 ...pplicationConfigurationInvalidException.kt |   5 ++
 .../exception/CwaSecurityException.kt         |   9 +++
 .../coronawarnapp/http/WebRequestBuilder.kt   |  37 ++++++----
 .../util/security/SecurityHelper.kt           |  63 ++++++++++++++++--
 gradle.properties                             |   3 +-
 8 files changed, 111 insertions(+), 21 deletions(-)
 create mode 100644 Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt
 create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt

diff --git a/Corona-Warn-App/build.gradle b/Corona-Warn-App/build.gradle
index de115e393..4e2526202 100644
--- a/Corona-Warn-App/build.gradle
+++ b/Corona-Warn-App/build.gradle
@@ -40,12 +40,16 @@ 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\""
 
         //override URLs with local variables
         Properties properties = new Properties()
         def propertiesFile = project.rootProject.file('local.properties')
         if (propertiesFile.exists()) {
             properties.load(propertiesFile.newDataInputStream())
+            def secretFile = project.rootProject.file('secrets.properties')
+            if (secretFile.exists())
+                properties.load(secretFile.newDataInputStream())
 
             def DOWNLOAD_CDN_URL = properties.getProperty('DOWNLOAD_CDN_URL')
             if (DOWNLOAD_CDN_URL)
@@ -58,6 +62,10 @@ android {
             def VERIFICATION_CDN_URL = properties.getProperty('VERIFICATION_CDN_URL')
             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\""
         }
     }
 
diff --git a/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks b/Corona-Warn-App/src/main/assets/trusted-certs-cwa.bks
new file mode 100644
index 0000000000000000000000000000000000000000..e67a6926a60ef6968aa6532d1fbbce7bb7eb0012
GIT binary patch
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

literal 0
HcmV?d00001

diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt
new file mode 100644
index 000000000..938289532
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationCorruptException.kt
@@ -0,0 +1,7 @@
+package de.rki.coronawarnapp.exception
+
+import java.lang.Exception
+
+class ApplicationConfigurationCorruptException : Exception(
+    "the application configuration is corrupt"
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt
new file mode 100644
index 000000000..73272e43d
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/ApplicationConfigurationInvalidException.kt
@@ -0,0 +1,5 @@
+package de.rki.coronawarnapp.exception
+
+class ApplicationConfigurationInvalidException : Exception(
+    "the application configuration is invalid"
+)
diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt
new file mode 100644
index 000000000..3e1c3efcb
--- /dev/null
+++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/exception/CwaSecurityException.kt
@@ -0,0 +1,9 @@
+package de.rki.coronawarnapp.exception
+
+import java.lang.Exception
+
+class CwaSecurityException(cause: Throwable) : Exception(
+    "something went wrong during a critical part of the application ensuring security, please refer" +
+            "to the details for more information",
+    cause
+)
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 a5e68d852..408169d26 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
@@ -21,6 +21,9 @@ package de.rki.coronawarnapp.http
 
 import KeyExportFormat
 import android.util.Log
+import com.google.protobuf.InvalidProtocolBufferException
+import de.rki.coronawarnapp.exception.ApplicationConfigurationCorruptException
+import de.rki.coronawarnapp.exception.ApplicationConfigurationInvalidException
 import de.rki.coronawarnapp.http.requests.RegistrationTokenRequest
 import de.rki.coronawarnapp.http.requests.ReqistrationRequest
 import de.rki.coronawarnapp.http.requests.TanRequestBody
@@ -40,6 +43,9 @@ import java.util.UUID
 object WebRequestBuilder {
     private val TAG: String? = WebRequestBuilder::class.simpleName
 
+    private const val EXPORT_BINARY_FILE_NAME = "export.bin"
+    private const val EXPORT_SIGNATURE_FILE_NAME = "export.sig"
+
     private val serviceFactory = ServiceFactory()
 
     private val distributionService = serviceFactory.distributionService()
@@ -81,25 +87,28 @@ object WebRequestBuilder {
 
     suspend fun asyncGetApplicationConfigurationFromServer(): ApplicationConfiguration =
         withContext(Dispatchers.IO) {
-            var applicationConfiguration: ApplicationConfiguration? = null
+            var exportBinary: ByteArray? = null
+            var exportSignature: ByteArray? = null
+
             distributionService.getApplicationConfiguration(
                 DiagnosisKeyConstants.COUNTRY_APPCONFIG_DOWNLOAD_URL
             ).byteStream().unzip { entry, entryContent ->
-                if (entry.name == "export.bin") {
-                    val appConfig = ApplicationConfiguration.parseFrom(entryContent)
-                    applicationConfiguration = appConfig
-                }
-                if (entry.name == "export.sig") {
-                    val signatures = KeyExportFormat.TEKSignatureList.parseFrom(entryContent)
-                    signatures.signaturesList.forEach {
-                        Log.d(TAG, it.signatureInfo.toString())
-                    }
-                }
+                if (entry.name == EXPORT_BINARY_FILE_NAME) exportBinary = entryContent.copyOf()
+                if (entry.name == EXPORT_SIGNATURE_FILE_NAME) exportSignature = entryContent.copyOf()
+            }
+            if (exportBinary == null || exportSignature == null) {
+                throw ApplicationConfigurationInvalidException()
             }
-            if (applicationConfiguration == null) {
-                throw IllegalArgumentException("no file was found in the downloaded zip")
+
+            if (!SecurityHelper.exportFileIsValid(exportBinary, exportSignature)) {
+                throw ApplicationConfigurationCorruptException()
+            }
+
+            try {
+                return@withContext ApplicationConfiguration.parseFrom(exportBinary)
+            } catch (e: InvalidProtocolBufferException) {
+                throw ApplicationConfigurationInvalidException()
             }
-            return@withContext applicationConfiguration!!
         }
 
     suspend fun asyncGetRegistrationToken(
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 31e77d527..d8df176a7 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,18 +19,25 @@
 
 package de.rki.coronawarnapp.util.security
 
+import KeyExportFormat.TEKSignatureList
 import android.content.Context
 import android.content.SharedPreferences
 import android.security.keystore.KeyGenParameterSpec
 import android.security.keystore.KeyProperties
 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.lang.Exception
+import java.lang.NullPointerException
 import java.security.KeyStore
 import java.security.MessageDigest
 import java.security.SecureRandom
 import javax.crypto.KeyGenerator
 import javax.crypto.SecretKey
+import java.security.Signature
+import java.security.cert.Certificate
 
 /**
  * Key Store and Password Access
@@ -41,16 +48,23 @@ object SecurityHelper {
     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 AndroidKeyStore = "AndroidKeyStore"
 
-    private val keyStore: KeyStore by lazy {
-        KeyStore.getInstance(AndroidKeyStore).also {
+    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"
+    private const val ANDROID_KEY_STORE = "AndroidKeyStore"
+
+    private val androidKeyStore: KeyStore by lazy {
+        KeyStore.getInstance(ANDROID_KEY_STORE).also {
             it.load(null)
         }
     }
 
     val globalEncryptedSharedPreferencesInstance: SharedPreferences by lazy {
-        CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME)
+        withSecurityCatch {
+            CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME)
+        }
     }
 
     /**
@@ -73,10 +87,10 @@ object SecurityHelper {
         .toCharArray()
 
     private fun getOrGenerateDBSecretKey(): SecretKey =
-        keyStore.getKey(CWA_APP_SQLITE_DB_PW, null).run {
+        androidKeyStore.getKey(CWA_APP_SQLITE_DB_PW, null).run {
             return if (this == null) {
                 val kg: KeyGenerator = KeyGenerator.getInstance(
-                    KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore
+                    KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE
                 )
                 val spec: KeyGenParameterSpec = KeyGenParameterSpec.Builder(
                     CWA_APP_SQLITE_DB_PW,
@@ -97,4 +111,41 @@ object SecurityHelper {
         .getInstance("SHA-256")
         .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 {
+        doInCatch.invoke()
+    } catch (e: Exception) {
+        throw CwaSecurityException(e)
+    }
 }
diff --git a/gradle.properties b/gradle.properties
index b63abd23e..ec7086cb5 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -21,4 +21,5 @@ org.gradle.dependency.verification.console=verbose
 # Variables for Server URLs. The variables in local.properties will override these
 SUBMISSION_CDN_URL=
 DOWNLOAD_CDN_URL=
-VERIFICATION_CDN_URL=
\ No newline at end of file
+VERIFICATION_CDN_URL=
+TRUSTED_CERTS_EXPORT_KEYSTORE_PW=
\ No newline at end of file
-- 
GitLab