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
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 {
......
......@@ -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())
)
}
}
......
......@@ -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()
}
......
......@@ -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.")
}
......
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
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()
......
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()
......
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