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