Skip to content
Snippets Groups Projects
Unverified Commit aee21f7f authored by Jakob Möller's avatar Jakob Möller Committed by GitHub
Browse files

Setup Trust Anchors (Public Keys for Signature Verification) (#232)


* Setup Trust Anchors

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

* Make Verification of the Export File dependant on build type

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

* Reformatting after Merge

Signed-off-by: default avatard067928 <jakob.moeller@sap.com>
parent 9ce64dcd
No related branches found
No related tags found
No related merge requests found
...@@ -40,7 +40,7 @@ android { ...@@ -40,7 +40,7 @@ android {
buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\"" buildConfigField "String", "DOWNLOAD_CDN_URL", "\"$DOWNLOAD_CDN_URL\""
buildConfigField "String", "SUBMISSION_CDN_URL", "\"$SUBMISSION_CDN_URL\"" buildConfigField "String", "SUBMISSION_CDN_URL", "\"$SUBMISSION_CDN_URL\""
buildConfigField "String", "VERIFICATION_CDN_URL", "\"$VERIFICATION_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 //override URLs with local variables
Properties properties = new Properties() Properties properties = new Properties()
...@@ -63,9 +63,6 @@ android { ...@@ -63,9 +63,6 @@ android {
if (VERIFICATION_CDN_URL) if (VERIFICATION_CDN_URL)
buildConfigField "String", "VERIFICATION_CDN_URL", "\"$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 { ...@@ -74,38 +71,14 @@ android {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp\""
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"
} }
releaseForTest { releaseForTest {
applicationIdSuffix '.dev' applicationIdSuffix '.dev'
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
buildConfigField "String", "EXPORT_SIGNATURE_ID", "\"de.rki.coronawarnapp-dev\""
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"
} }
} }
...@@ -215,10 +188,10 @@ dependencies { ...@@ -215,10 +188,10 @@ dependencies {
// Play Services // Play Services
implementation 'com.google.android.play:core:1.7.3' 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-base:17.3.0'
implementation 'com.google.android.gms:play-services-basement:17.2.1' 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-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']) api fileTree(dir: 'libs', include: ['play-services-nearby-18.0.2-eap.aar'])
// HTTP // HTTP
......
de.rki.coronawarnapp-dev=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE3BYTxr2HuJYQG+d7Ezu6KS8GEbFkiEvyJFg0j+C839gTjT6j7Ho0EXXZ/a07ZfvKcC2cmc1SunsrqU9Jov1J5Q==
de.rki.coronawarnapp=MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg==
\ No newline at end of file
File deleted
...@@ -34,6 +34,7 @@ import de.rki.coronawarnapp.storage.FileStorageHelper ...@@ -34,6 +34,7 @@ import de.rki.coronawarnapp.storage.FileStorageHelper
import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat import de.rki.coronawarnapp.util.TimeAndDateExtensions.toServerFormat
import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.ZipHelper.unzip
import de.rki.coronawarnapp.util.security.SecurityHelper import de.rki.coronawarnapp.util.security.SecurityHelper
import de.rki.coronawarnapp.util.security.VerificationKeys
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
...@@ -52,6 +53,8 @@ object WebRequestBuilder { ...@@ -52,6 +53,8 @@ object WebRequestBuilder {
private val verificationService by lazy { serviceFactory.verificationService() } private val verificationService by lazy { serviceFactory.verificationService() }
private val submissionService by lazy { serviceFactory.submissionService() } private val submissionService by lazy { serviceFactory.submissionService() }
private val verificationKeys = VerificationKeys()
suspend fun asyncGetDateIndex(): List<String> = withContext(Dispatchers.IO) { suspend fun asyncGetDateIndex(): List<String> = withContext(Dispatchers.IO) {
return@withContext distributionService return@withContext distributionService
.getDateIndex(DiagnosisKeyConstants.AVAILABLE_DATES_URL).toList() .getDateIndex(DiagnosisKeyConstants.AVAILABLE_DATES_URL).toList()
...@@ -100,7 +103,7 @@ object WebRequestBuilder { ...@@ -100,7 +103,7 @@ object WebRequestBuilder {
throw ApplicationConfigurationInvalidException() throw ApplicationConfigurationInvalidException()
} }
if (!SecurityHelper.exportFileIsValid(exportBinary, exportSignature)) { if (verificationKeys.hasInvalidSignature(exportBinary, exportSignature)) {
throw ApplicationConfigurationCorruptException() throw ApplicationConfigurationCorruptException()
} }
......
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"
}
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
package de.rki.coronawarnapp.util.security package de.rki.coronawarnapp.util.security
import KeyExportFormat.TEKSignatureList
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
...@@ -27,34 +26,26 @@ import android.os.Build ...@@ -27,34 +26,26 @@ import android.os.Build
import android.util.Base64 import android.util.Base64
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys import androidx.security.crypto.MasterKeys
import de.rki.coronawarnapp.BuildConfig
import de.rki.coronawarnapp.CoronaWarnApplication import de.rki.coronawarnapp.CoronaWarnApplication
import de.rki.coronawarnapp.exception.CwaSecurityException 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.MessageDigest
import java.security.SecureRandom import java.security.SecureRandom
import java.security.Signature
import java.security.cert.Certificate
/** /**
* Key Store and Password Access * Key Store and Password Access
*/ */
object SecurityHelper { 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 keyGenParameterSpec = MasterKeys.AES256_GCM_SPEC
private val masterKeyAlias = MasterKeys.getOrCreate(keyGenParameterSpec) 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 { val globalEncryptedSharedPreferencesInstance: SharedPreferences by lazy {
withSecurityCatch { withSecurityCatch {
CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(SHARED_PREF_NAME) CoronaWarnApplication.getAppContext().getEncryptedSharedPrefs(ENCRYPTED_SHARED_PREFERENCES_FILE)
} }
} }
...@@ -119,43 +110,11 @@ object SecurityHelper { ...@@ -119,43 +110,11 @@ object SecurityHelper {
} }
fun hash256(input: String): String = MessageDigest fun hash256(input: String): String = MessageDigest
.getInstance("SHA-256") .getInstance(DIGEST_ALGORITHM)
.digest(input.toByteArray()) .digest(input.toByteArray())
.fold("", { str, it -> str + "%02x".format(it) }) .fold("", { str, it -> str + "%02x".format(it) })
fun exportFileIsValid(export: ByteArray?, sig: ByteArray?) = withSecurityCatch { fun <T> withSecurityCatch(doInCatch: () -> T) = try {
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() doInCatch.invoke()
} catch (e: Exception) { } catch (e: Exception) {
throw CwaSecurityException(e) throw CwaSecurityException(e)
......
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() }
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment