Skip to content
Snippets Groups Projects
Unverified Commit c3e990b3 authored by Matthias Urhahn's avatar Matthias Urhahn Committed by GitHub
Browse files

Refactored VerificationKeys.kt: Made the class more general purpose (DEV) #2530

parent 2d32a0ec
No related branches found
No related tags found
No related merge requests found
Showing with 116 additions and 107 deletions
...@@ -9,7 +9,7 @@ import de.rki.coronawarnapp.statistics.StatisticsData ...@@ -9,7 +9,7 @@ import de.rki.coronawarnapp.statistics.StatisticsData
import de.rki.coronawarnapp.statistics.StatisticsModule import de.rki.coronawarnapp.statistics.StatisticsModule
import de.rki.coronawarnapp.statistics.source.StatisticsParser import de.rki.coronawarnapp.statistics.source.StatisticsParser
import de.rki.coronawarnapp.statistics.source.StatisticsServer 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 de.rki.coronawarnapp.util.serialization.SerializationModule
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
...@@ -34,7 +34,7 @@ object Statistics { ...@@ -34,7 +34,7 @@ object Statistics {
val httpClient = HttpModule().defaultHttpClient() val httpClient = HttpModule().defaultHttpClient()
val cdnClient = cdnModule.cdnHttpClient(httpClient) val cdnClient = cdnModule.cdnHttpClient(httpClient)
val url = cdnModule.provideDownloadServerUrl(environmentSetup) val url = cdnModule.provideDownloadServerUrl(environmentSetup)
val verificationKeys = VerificationKeys(environmentSetup) val signatureValidation = SignatureValidation(environmentSetup)
val gsonFactory = GsonConverterFactory.create() val gsonFactory = GsonConverterFactory.create()
val statisticsServer = StatisticsServer( val statisticsServer = StatisticsServer(
...@@ -47,7 +47,7 @@ object Statistics { ...@@ -47,7 +47,7 @@ object Statistics {
) )
}, },
cache = cache, cache = cache,
verificationKeys = verificationKeys signatureValidation = signatureValidation
) )
return runBlocking { return runBlocking {
......
...@@ -23,33 +23,33 @@ class VerificationKeysTest { ...@@ -23,33 +23,33 @@ class VerificationKeysTest {
every { environmentSetup.appConfigVerificationKey } returns PUB_KEY every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
} }
private fun createTool() = VerificationKeys(environmentSetup) private fun createTool() = SignatureValidation(environmentSetup)
@Test @Test
fun goodBinaryAndSignature() { fun goodBinaryAndSignature() {
val tool = createTool() val tool = createTool()
tool.hasInvalidSignature( tool.hasValidSignature(
GOOD_BINARY.decodeHex().toByteArray(), GOOD_BINARY.decodeHex().toByteArray(),
GOOD_SIGNATURE.decodeHex().toByteArray() SignatureValidation.parseTEKStyleSignature(GOOD_SIGNATURE.decodeHex().toByteArray())
) shouldBe false ) shouldBe true
} }
@Test @Test
fun badBinaryGoodSignature() { fun badBinaryGoodSignature() {
val tool = createTool() val tool = createTool()
tool.hasInvalidSignature( tool.hasValidSignature(
"123ABC".decodeHex().toByteArray(), "123ABC".decodeHex().toByteArray(),
GOOD_SIGNATURE.decodeHex().toByteArray() SignatureValidation.parseTEKStyleSignature(GOOD_SIGNATURE.decodeHex().toByteArray())
) shouldBe true ) shouldBe false
} }
@Test @Test
fun goodBinaryBadSignature() { fun goodBinaryBadSignature() {
val tool = createTool() val tool = createTool()
shouldThrow<CwaSecurityException> { shouldThrow<CwaSecurityException> {
tool.hasInvalidSignature( tool.hasValidSignature(
GOOD_BINARY.decodeHex().toByteArray(), GOOD_BINARY.decodeHex().toByteArray(),
"123ABC".decodeHex().toByteArray() SignatureValidation.parseTEKStyleSignature("123ABC".decodeHex().toByteArray())
) )
} }
} }
...@@ -58,9 +58,9 @@ class VerificationKeysTest { ...@@ -58,9 +58,9 @@ class VerificationKeysTest {
fun badEverything() { fun badEverything() {
val tool = createTool() val tool = createTool()
shouldThrow<CwaSecurityException> { shouldThrow<CwaSecurityException> {
tool.hasInvalidSignature( tool.hasValidSignature(
"123ABC".decodeHex().toByteArray(), "123ABC".decodeHex().toByteArray(),
"123ABC".decodeHex().toByteArray() SignatureValidation.parseTEKStyleSignature("123ABC".decodeHex().toByteArray())
) )
} }
} }
......
...@@ -12,7 +12,7 @@ import de.rki.coronawarnapp.util.TimeStamper ...@@ -12,7 +12,7 @@ import de.rki.coronawarnapp.util.TimeStamper
import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
import de.rki.coronawarnapp.util.ZipHelper.unzip import de.rki.coronawarnapp.util.ZipHelper.unzip
import de.rki.coronawarnapp.util.retrofit.etag 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 okhttp3.CacheControl
import org.joda.time.Duration import org.joda.time.Duration
import org.joda.time.Instant import org.joda.time.Instant
...@@ -26,7 +26,7 @@ import javax.inject.Inject ...@@ -26,7 +26,7 @@ import javax.inject.Inject
@Reusable @Reusable
class AppConfigServer @Inject constructor( class AppConfigServer @Inject constructor(
private val api: Lazy<AppConfigApiV2>, private val api: Lazy<AppConfigApiV2>,
private val verificationKeys: VerificationKeys, private val signatureValidation: SignatureValidation,
private val timeStamper: TimeStamper, private val timeStamper: TimeStamper,
private val testSettings: TestSettings private val testSettings: TestSettings
) { ) {
...@@ -49,7 +49,11 @@ class AppConfigServer @Inject constructor( ...@@ -49,7 +49,11 @@ class AppConfigServer @Inject constructor(
throw ApplicationConfigurationInvalidException(message = "Unknown files: ${fileMap.keys}") throw ApplicationConfigurationInvalidException(message = "Unknown files: ${fileMap.keys}")
} }
if (verificationKeys.hasInvalidSignature(exportBinary, exportSignature)) { val hasValidSignature = signatureValidation.hasValidSignature(
exportBinary,
SignatureValidation.parseTEKStyleSignature(exportSignature)
)
if (!hasValidSignature) {
throw ApplicationConfigurationCorruptException() throw ApplicationConfigurationCorruptException()
} }
......
...@@ -5,7 +5,7 @@ import dagger.Reusable ...@@ -5,7 +5,7 @@ import dagger.Reusable
import de.rki.coronawarnapp.statistics.Statistics import de.rki.coronawarnapp.statistics.Statistics
import de.rki.coronawarnapp.util.ZipHelper.readIntoMap import de.rki.coronawarnapp.util.ZipHelper.readIntoMap
import de.rki.coronawarnapp.util.ZipHelper.unzip 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 okhttp3.Cache
import retrofit2.HttpException import retrofit2.HttpException
import timber.log.Timber import timber.log.Timber
...@@ -15,7 +15,7 @@ import javax.inject.Inject ...@@ -15,7 +15,7 @@ import javax.inject.Inject
@Reusable @Reusable
class StatisticsServer @Inject constructor( class StatisticsServer @Inject constructor(
private val api: Lazy<StatisticsApiV1>, private val api: Lazy<StatisticsApiV1>,
private val verificationKeys: VerificationKeys, private val signatureValidation: SignatureValidation,
@Statistics val cache: Cache @Statistics val cache: Cache
) { ) {
...@@ -37,7 +37,12 @@ class StatisticsServer @Inject constructor( ...@@ -37,7 +37,12 @@ class StatisticsServer @Inject constructor(
throw IOException("Unknown files: ${fileMap.keys}") 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.") throw InvalidStatisticsSignatureException(message = "Statistics signature did not match.")
} }
......
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)
}
}
}
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() }
}
...@@ -7,7 +7,7 @@ import de.rki.coronawarnapp.appconfig.internal.InternalConfigData ...@@ -7,7 +7,7 @@ import de.rki.coronawarnapp.appconfig.internal.InternalConfigData
import de.rki.coronawarnapp.storage.TestSettings import de.rki.coronawarnapp.storage.TestSettings
import de.rki.coronawarnapp.util.CWADebug import de.rki.coronawarnapp.util.CWADebug
import de.rki.coronawarnapp.util.TimeStamper 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.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
...@@ -33,7 +33,7 @@ import java.io.File ...@@ -33,7 +33,7 @@ import java.io.File
class AppConfigServerTest : BaseIOTest() { class AppConfigServerTest : BaseIOTest() {
@MockK lateinit var api: AppConfigApiV2 @MockK lateinit var api: AppConfigApiV2
@MockK lateinit var verificationKeys: VerificationKeys @MockK lateinit var signatureValidation: SignatureValidation
@MockK lateinit var timeStamper: TimeStamper @MockK lateinit var timeStamper: TimeStamper
@MockK lateinit var testSettings: TestSettings @MockK lateinit var testSettings: TestSettings
private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!) private val testDir = File(IO_TEST_BASEDIR, this::class.simpleName!!)
...@@ -45,7 +45,7 @@ class AppConfigServerTest : BaseIOTest() { ...@@ -45,7 +45,7 @@ class AppConfigServerTest : BaseIOTest() {
testDir.exists() shouldBe true testDir.exists() shouldBe true
every { timeStamper.nowUTC } returns Instant.ofEpochMilli(123456789) every { timeStamper.nowUTC } returns Instant.ofEpochMilli(123456789)
every { verificationKeys.hasInvalidSignature(any(), any()) } returns false every { signatureValidation.hasValidSignature(any(), any()) } returns true
mockkObject(CWADebug) mockkObject(CWADebug)
every { CWADebug.isDeviceForTestersBuild } returns false every { CWADebug.isDeviceForTestersBuild } returns false
...@@ -59,7 +59,7 @@ class AppConfigServerTest : BaseIOTest() { ...@@ -59,7 +59,7 @@ class AppConfigServerTest : BaseIOTest() {
private fun createInstance() = AppConfigServer( private fun createInstance() = AppConfigServer(
api = { api }, api = { api },
verificationKeys = verificationKeys, signatureValidation = signatureValidation,
timeStamper = timeStamper, timeStamper = timeStamper,
testSettings = testSettings testSettings = testSettings
) )
...@@ -92,7 +92,7 @@ class AppConfigServerTest : BaseIOTest() { ...@@ -92,7 +92,7 @@ class AppConfigServerTest : BaseIOTest() {
cacheValidity = Duration.standardSeconds(123) cacheValidity = Duration.standardSeconds(123)
) )
verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } verify(exactly = 1) { signatureValidation.hasValidSignature(any(), any()) }
} }
@Test @Test
...@@ -113,7 +113,7 @@ class AppConfigServerTest : BaseIOTest() { ...@@ -113,7 +113,7 @@ class AppConfigServerTest : BaseIOTest() {
coEvery { api.getApplicationConfiguration() } returns Response.success( coEvery { api.getApplicationConfiguration() } returns Response.success(
APPCONFIG_BUNDLE.toResponseBody() APPCONFIG_BUNDLE.toResponseBody()
) )
every { verificationKeys.hasInvalidSignature(any(), any()) } returns true every { signatureValidation.hasValidSignature(any(), any()) } returns false
val downloadServer = createInstance() val downloadServer = createInstance()
......
package de.rki.coronawarnapp.statistics.source 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.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
...@@ -23,20 +23,20 @@ import java.io.IOException ...@@ -23,20 +23,20 @@ import java.io.IOException
class StatisticsServerTest : BaseIOTest() { class StatisticsServerTest : BaseIOTest() {
@MockK lateinit var api: StatisticsApiV1 @MockK lateinit var api: StatisticsApiV1
@MockK lateinit var verificationKeys: VerificationKeys @MockK lateinit var signatureValidation: SignatureValidation
@MockK lateinit var cache: Cache @MockK lateinit var cache: Cache
@BeforeEach @BeforeEach
fun setup() { fun setup() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
every { verificationKeys.hasInvalidSignature(any(), any()) } returns false every { signatureValidation.hasValidSignature(any(), any()) } returns true
every { cache.evictAll() } just Runs every { cache.evictAll() } just Runs
} }
private fun createInstance() = StatisticsServer( private fun createInstance() = StatisticsServer(
api = { api }, api = { api },
verificationKeys = verificationKeys, signatureValidation = signatureValidation,
cache = cache cache = cache
) )
...@@ -49,7 +49,7 @@ class StatisticsServerTest : BaseIOTest() { ...@@ -49,7 +49,7 @@ class StatisticsServerTest : BaseIOTest() {
val rawStatistics = server.getRawStatistics() val rawStatistics = server.getRawStatistics()
rawStatistics shouldBe STATS_PROTO rawStatistics shouldBe STATS_PROTO
verify(exactly = 1) { verificationKeys.hasInvalidSignature(any(), any()) } verify(exactly = 1) { signatureValidation.hasValidSignature(any(), any()) }
} }
@Test @Test
...@@ -66,7 +66,7 @@ class StatisticsServerTest : BaseIOTest() { ...@@ -66,7 +66,7 @@ class StatisticsServerTest : BaseIOTest() {
@Test @Test
fun `verification fails`() = runBlockingTest { fun `verification fails`() = runBlockingTest {
coEvery { api.getStatistics() } returns Response.success(STATS_ZIP.toResponseBody()) 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() val server = createInstance()
......
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