From c3e990b31e78cb2e6424d5908d8a75a15d5e9e9b Mon Sep 17 00:00:00 2001 From: Matthias Urhahn <matthias.urhahn@sap.com> Date: Fri, 5 Mar 2021 11:33:59 +0100 Subject: [PATCH] Refactored VerificationKeys.kt: Made the class more general purpose (DEV) #2530 --- .../coronawarnapp/ui/statistics/Statistics.kt | 6 +- .../util/security/VerificationKeysTest.kt | 22 +++--- .../sources/remote/AppConfigServer.kt | 10 ++- .../statistics/source/StatisticsServer.kt | 11 ++- .../util/security/SignatureValidation.kt | 75 +++++++++++++++++++ .../util/security/VerificationKeys.kt | 75 ------------------- .../sources/remote/AppConfigServerTest.kt | 12 +-- .../statistics/source/StatisticsServerTest.kt | 12 +-- 8 files changed, 116 insertions(+), 107 deletions(-) create mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt delete mode 100644 Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/statistics/Statistics.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/statistics/Statistics.kt index f4a6f85e1..1e15b9f80 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/statistics/Statistics.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/ui/statistics/Statistics.kt @@ -9,7 +9,7 @@ import de.rki.coronawarnapp.statistics.StatisticsData import de.rki.coronawarnapp.statistics.StatisticsModule import de.rki.coronawarnapp.statistics.source.StatisticsParser import de.rki.coronawarnapp.statistics.source.StatisticsServer -import de.rki.coronawarnapp.util.security.VerificationKeys +import de.rki.coronawarnapp.util.security.SignatureValidation import de.rki.coronawarnapp.util.serialization.SerializationModule import io.mockk.every import io.mockk.mockk @@ -34,7 +34,7 @@ object Statistics { val httpClient = HttpModule().defaultHttpClient() val cdnClient = cdnModule.cdnHttpClient(httpClient) val url = cdnModule.provideDownloadServerUrl(environmentSetup) - val verificationKeys = VerificationKeys(environmentSetup) + val signatureValidation = SignatureValidation(environmentSetup) val gsonFactory = GsonConverterFactory.create() val statisticsServer = StatisticsServer( @@ -47,7 +47,7 @@ object Statistics { ) }, cache = cache, - verificationKeys = verificationKeys + signatureValidation = signatureValidation ) return runBlocking { diff --git a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt index 817159d68..8e8f54718 100644 --- a/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt +++ b/Corona-Warn-App/src/androidTest/java/de/rki/coronawarnapp/util/security/VerificationKeysTest.kt @@ -23,33 +23,33 @@ class VerificationKeysTest { every { environmentSetup.appConfigVerificationKey } returns PUB_KEY } - private fun createTool() = VerificationKeys(environmentSetup) + private fun createTool() = SignatureValidation(environmentSetup) @Test fun goodBinaryAndSignature() { val tool = createTool() - tool.hasInvalidSignature( + tool.hasValidSignature( GOOD_BINARY.decodeHex().toByteArray(), - GOOD_SIGNATURE.decodeHex().toByteArray() - ) shouldBe false + SignatureValidation.parseTEKStyleSignature(GOOD_SIGNATURE.decodeHex().toByteArray()) + ) shouldBe true } @Test fun badBinaryGoodSignature() { val tool = createTool() - tool.hasInvalidSignature( + tool.hasValidSignature( "123ABC".decodeHex().toByteArray(), - GOOD_SIGNATURE.decodeHex().toByteArray() - ) shouldBe true + SignatureValidation.parseTEKStyleSignature(GOOD_SIGNATURE.decodeHex().toByteArray()) + ) shouldBe false } @Test fun goodBinaryBadSignature() { val tool = createTool() shouldThrow<CwaSecurityException> { - tool.hasInvalidSignature( + tool.hasValidSignature( GOOD_BINARY.decodeHex().toByteArray(), - "123ABC".decodeHex().toByteArray() + SignatureValidation.parseTEKStyleSignature("123ABC".decodeHex().toByteArray()) ) } } @@ -58,9 +58,9 @@ class VerificationKeysTest { fun badEverything() { val tool = createTool() shouldThrow<CwaSecurityException> { - tool.hasInvalidSignature( + tool.hasValidSignature( "123ABC".decodeHex().toByteArray(), - "123ABC".decodeHex().toByteArray() + SignatureValidation.parseTEKStyleSignature("123ABC".decodeHex().toByteArray()) ) } } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt index 39b3c2cb6..02bbd1094 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServer.kt @@ -12,7 +12,7 @@ import de.rki.coronawarnapp.util.TimeStamper import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.retrofit.etag -import de.rki.coronawarnapp.util.security.VerificationKeys +import de.rki.coronawarnapp.util.security.SignatureValidation import okhttp3.CacheControl import org.joda.time.Duration import org.joda.time.Instant @@ -26,7 +26,7 @@ import javax.inject.Inject @Reusable class AppConfigServer @Inject constructor( private val api: Lazy<AppConfigApiV2>, - private val verificationKeys: VerificationKeys, + private val signatureValidation: SignatureValidation, private val timeStamper: TimeStamper, private val testSettings: TestSettings ) { @@ -49,7 +49,11 @@ class AppConfigServer @Inject constructor( throw ApplicationConfigurationInvalidException(message = "Unknown files: ${fileMap.keys}") } - if (verificationKeys.hasInvalidSignature(exportBinary, exportSignature)) { + val hasValidSignature = signatureValidation.hasValidSignature( + exportBinary, + SignatureValidation.parseTEKStyleSignature(exportSignature) + ) + if (!hasValidSignature) { throw ApplicationConfigurationCorruptException() } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/statistics/source/StatisticsServer.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/statistics/source/StatisticsServer.kt index 11526a01c..10eea6f67 100644 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/statistics/source/StatisticsServer.kt +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/statistics/source/StatisticsServer.kt @@ -5,7 +5,7 @@ import dagger.Reusable import de.rki.coronawarnapp.statistics.Statistics import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.unzip -import de.rki.coronawarnapp.util.security.VerificationKeys +import de.rki.coronawarnapp.util.security.SignatureValidation import okhttp3.Cache import retrofit2.HttpException import timber.log.Timber @@ -15,7 +15,7 @@ import javax.inject.Inject @Reusable class StatisticsServer @Inject constructor( private val api: Lazy<StatisticsApiV1>, - private val verificationKeys: VerificationKeys, + private val signatureValidation: SignatureValidation, @Statistics val cache: Cache ) { @@ -37,7 +37,12 @@ class StatisticsServer @Inject constructor( throw IOException("Unknown files: ${fileMap.keys}") } - if (verificationKeys.hasInvalidSignature(exportBinary, exportSignature)) { + val hasValidSignature = signatureValidation.hasValidSignature( + exportBinary, + SignatureValidation.parseTEKStyleSignature(exportSignature) + ) + + if (!hasValidSignature) { throw InvalidStatisticsSignatureException(message = "Statistics signature did not match.") } diff --git a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt new file mode 100644 index 000000000..a3221d6d2 --- /dev/null +++ b/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/SignatureValidation.kt @@ -0,0 +1,75 @@ +package de.rki.coronawarnapp.util.security + +import android.security.keystore.KeyProperties +import android.util.Base64 +import de.rki.coronawarnapp.environment.EnvironmentSetup +import de.rki.coronawarnapp.exception.CwaSecurityException +import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeySignatureList.TEKSignatureList +import timber.log.Timber +import java.security.KeyFactory +import java.security.Signature +import java.security.spec.X509EncodedKeySpec +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class SignatureValidation @Inject constructor( + private val environmentSetup: EnvironmentSetup +) { + + private val keyFactory by lazy { + KeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_EC) + } + private val signature by lazy { + Signature.getInstance(SecurityConstants.EXPORT_FILE_SIGNATURE_VERIFICATION_ALGORITHM) + } + + // Public keys within this server environment + private val publicKeys by lazy { + environmentSetup.appConfigVerificationKey.split(KEY_DELIMITER) + .mapNotNull { pubKeyBase64 -> Base64.decode(pubKeyBase64, Base64.DEFAULT) } + .map { pubKeyBinary -> keyFactory.generatePublic(X509EncodedKeySpec(pubKeyBinary)) } + .onEach { Timber.tag(TAG).v("ENV PubKey: %s", it) } + } + + fun hasValidSignature(toVerify: ByteArray, signatureList: Sequence<ByteArray>): Boolean = try { + val validSignatures = signature.findMatchingPublicKeys(toVerify, signatureList) + Timber.tag(TAG).v("${validSignatures.size} valid signatures found") + + validSignatures.isNotEmpty().also { + if (it) Timber.tag(TAG).d("Valid signatures found.") + else Timber.tag(TAG).w("No valid signature found.") + } + } catch (e: Exception) { + throw CwaSecurityException(e) + } + + private fun Signature.findMatchingPublicKeys( + toVerify: ByteArray, + signatures: Sequence<ByteArray> + ) = publicKeys.filter { publicKey -> + for (signature in signatures) { + initVerify(publicKey) + update(toVerify) + if (verify(signature)) return@filter true + } + return@filter false + } + + companion object { + private const val KEY_DELIMITER = "," + private val TAG = SignatureValidation::class.java.simpleName + + fun parseTEKStyleSignature(signatureListProto: ByteArray) = try { + TEKSignatureList + .parseFrom(signatureListProto) + .signaturesList + .asSequence() + .onEach { Timber.tag(TAG).v(it.toString()) } + .mapNotNull { it.signature.toByteArray() } + } catch (e: Exception) { + Timber.w("%s is not a valid TEKSignatureList", signatureListProto) + 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 deleted file mode 100644 index 1ba8319fe..000000000 --- a/Corona-Warn-App/src/main/java/de/rki/coronawarnapp/util/security/VerificationKeys.kt +++ /dev/null @@ -1,75 +0,0 @@ -package de.rki.coronawarnapp.util.security - -import android.security.keystore.KeyProperties -import android.util.Base64 -import de.rki.coronawarnapp.environment.EnvironmentSetup -import de.rki.coronawarnapp.server.protocols.external.exposurenotification.TemporaryExposureKeySignatureList.TEKSignatureList -import timber.log.Timber -import java.security.KeyFactory -import java.security.Signature -import java.security.spec.X509EncodedKeySpec -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class VerificationKeys @Inject constructor( - private val environmentSetup: EnvironmentSetup -) { - companion object { - private const val KEY_DELIMITER = "," - 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) - - fun hasInvalidSignature( - export: ByteArray, - signatureListBinary: ByteArray - ): Boolean = SecurityHelper.withSecurityCatch { - signature.getValidSignaturesForExport(export, signatureListBinary) - .isEmpty() - .also { - if (it) Timber.tag(TAG).d("export is invalid") - else Timber.tag(TAG).d("export is valid") - } - } - - private fun Signature.getValidSignaturesForExport( - export: ByteArray?, - signatures: ByteArray? - ) = getKeysForSignatureVerificationFilteredByEnvironment() - .filter { publicKey -> - var verified = false - getTEKSignaturesForEnvironment(signatures).forEach { tek -> - initVerify(publicKey) - update(export) - if (verify(tek)) verified = true - } - verified - } - .also { Timber.tag(TAG).v("${it.size} valid signatures found") } - - private fun getKeysForSignatureVerificationFilteredByEnvironment() = - environmentSetup.appConfigVerificationKey.split(KEY_DELIMITER) - .mapNotNull { delimitedString -> - Base64.decode(delimitedString, Base64.DEFAULT) - }.map { binaryPublicKey -> - keyFactory.generatePublic( - X509EncodedKeySpec( - binaryPublicKey - ) - ) - } - .onEach { Timber.tag(TAG).v("$it") } - - private fun getTEKSignaturesForEnvironment( - signatureListBinary: ByteArray? - ) = TEKSignatureList - .parseFrom(signatureListBinary) - .signaturesList - .asSequence() - .onEach { Timber.tag(TAG).v(it.toString()) } - .mapNotNull { it.signature.toByteArray() } -} diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt index f8e3b0aab..e5fffdcd3 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/appconfig/sources/remote/AppConfigServerTest.kt @@ -7,7 +7,7 @@ import de.rki.coronawarnapp.appconfig.internal.InternalConfigData import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.TimeStamper -import de.rki.coronawarnapp.util.security.VerificationKeys +import de.rki.coronawarnapp.util.security.SignatureValidation import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -33,7 +33,7 @@ import java.io.File class AppConfigServerTest : BaseIOTest() { @MockK lateinit var api: AppConfigApiV2 - @MockK lateinit var verificationKeys: VerificationKeys + @MockK lateinit var signatureValidation: SignatureValidation @MockK lateinit var timeStamper: TimeStamper @MockK lateinit var testSettings: TestSettings private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) @@ -45,7 +45,7 @@ class AppConfigServerTest : BaseIOTest() { testDir.exists() shouldBe true every { timeStamper.nowUTC } returns Instant.ofEpochMilli(123456789) - every { verificationKeys.hasInvalidSignature(any(), any()) } returns false + every { signatureValidation.hasValidSignature(any(), any()) } returns true mockkObject(CWADebug) every { CWADebug.isDeviceForTestersBuild } returns false @@ -59,7 +59,7 @@ class AppConfigServerTest : BaseIOTest() { private fun createInstance() = AppConfigServer( api = { api }, - verificationKeys = verificationKeys, + signatureValidation = signatureValidation, timeStamper = timeStamper, testSettings = testSettings ) @@ -92,7 +92,7 @@ class AppConfigServerTest : BaseIOTest() { cacheValidity = Duration.standardSeconds(123) ) - verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } + verify(exactly = 1) { signatureValidation.hasValidSignature(any(), any()) } } @Test @@ -113,7 +113,7 @@ class AppConfigServerTest : BaseIOTest() { coEvery { api.getApplicationConfiguration() } returns Response.success( APPCONFIG_BUNDLE.toResponseBody() ) - every { verificationKeys.hasInvalidSignature(any(), any()) } returns true + every { signatureValidation.hasValidSignature(any(), any()) } returns false val downloadServer = createInstance() diff --git a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/statistics/source/StatisticsServerTest.kt b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/statistics/source/StatisticsServerTest.kt index a9ce4ebd8..f1b7acf65 100644 --- a/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/statistics/source/StatisticsServerTest.kt +++ b/Corona-Warn-App/src/test/java/de/rki/coronawarnapp/statistics/source/StatisticsServerTest.kt @@ -1,6 +1,6 @@ package de.rki.coronawarnapp.statistics.source -import de.rki.coronawarnapp.util.security.VerificationKeys +import de.rki.coronawarnapp.util.security.SignatureValidation import io.kotest.assertions.throwables.shouldThrow import io.kotest.matchers.shouldBe import io.mockk.MockKAnnotations @@ -23,20 +23,20 @@ import java.io.IOException class StatisticsServerTest : BaseIOTest() { @MockK lateinit var api: StatisticsApiV1 - @MockK lateinit var verificationKeys: VerificationKeys + @MockK lateinit var signatureValidation: SignatureValidation @MockK lateinit var cache: Cache @BeforeEach fun setup() { MockKAnnotations.init(this) - every { verificationKeys.hasInvalidSignature(any(), any()) } returns false + every { signatureValidation.hasValidSignature(any(), any()) } returns true every { cache.evictAll() } just Runs } private fun createInstance() = StatisticsServer( api = { api }, - verificationKeys = verificationKeys, + signatureValidation = signatureValidation, cache = cache ) @@ -49,7 +49,7 @@ class StatisticsServerTest : BaseIOTest() { val rawStatistics = server.getRawStatistics() rawStatistics shouldBe STATS_PROTO - verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } + verify(exactly = 1) { signatureValidation.hasValidSignature(any(), any()) } } @Test @@ -66,7 +66,7 @@ class StatisticsServerTest : BaseIOTest() { @Test fun `verification fails`() = runBlockingTest { coEvery { api.getStatistics() } returns Response.success(STATS_ZIP.toResponseBody()) - every { verificationKeys.hasInvalidSignature(any(), any()) } returns true + every { signatureValidation.hasValidSignature(any(), any()) } returns false val server = createInstance() -- GitLab