From 3bfeb0b5db79e3a6e4ab0b34acfe6377e1367bd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakob=20M=C3=B6ller?= 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 * Lint Check Signed-off-by: d067928 --- 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 de115e39311..4e2526202a1 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_A8EJCL}uty6!WD$t!X^K8(rYVDQ&o{Ro^ zK1KErvpa)<3zI@r*pm%CTiVaRc(oQG8=Ra&) zAia`3Y;pTp%PY&TW_^77blrV*dA@|`l=V!#3|8mft)D*e?wU2t - 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 31e77d527df..d8df176a766 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 withSecurityCatch(doInCatch: () -> T) = try { + doInCatch.invoke() + } catch (e: Exception) { + throw CwaSecurityException(e) + } } diff --git a/gradle.properties b/gradle.properties index b63abd23eaf..ec7086cb57a 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