Skip to content
This repository has been archived by the owner on Jun 20, 2023. It is now read-only.

Commit

Permalink
Event verification (EXPOSUREAPP-5423) (#2524)
Browse files Browse the repository at this point in the history
* Basic setup

* Test verification

* Update DefaultQRCodeVerifierTest.kt

* Invert boolean to reflect method name

* Adjustments to prepare for refactoring of "Verificationkeys" class.

* Add more tests and modify encoded event data

* Update ConfirmCheckInViewModelTest.kt

* Consider warning case

* Add test cases for warning

* Fix test

* Show error

* Add todo

* lint

* Move time warnings into result

* Update ConfirmCheckInViewModelTest.kt

Co-authored-by: Matthias Urhahn <matthias.urhahn@sap.com>
  • Loading branch information
mtwalli and d4rken authored Mar 10, 2021
1 parent 7a1d5b2 commit e9e7635
Show file tree
Hide file tree
Showing 13 changed files with 220 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

import de.rki.coronawarnapp.environment.EnvironmentSetup
import de.rki.coronawarnapp.util.security.SignatureValidation
import io.kotest.assertions.throwables.shouldNotThrowAny
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.matchers.shouldBe
import io.mockk.MockKAnnotations
import io.mockk.every
import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.test.runBlockingTest
import org.joda.time.Instant
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
import testhelpers.BaseTestInstrumentation

@RunWith(JUnit4::class)
class DefaultQRCodeVerifierTest : BaseTestInstrumentation() {

@MockK lateinit var environmentSetup: EnvironmentSetup
private lateinit var qrCodeVerifier: QRCodeVerifier

@Before
fun setUp() {
MockKAnnotations.init(this)
every { environmentSetup.appConfigVerificationKey } returns PUB_KEY
qrCodeVerifier = DefaultQRCodeVerifier(SignatureValidation(environmentSetup))
}

@Test
fun verifyEventSuccess() = runBlockingTest {
val time = 2687960 * 1_000L
val instant = Instant.ofEpochMilli(time)
shouldNotThrowAny {
val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT)
verifyResult.apply {
singedTraceLocation.event.description shouldBe "CWA Launch Party"
verifyResult.isBeforeStartTime(instant) shouldBe false
verifyResult.isAfterEndTime(instant) shouldBe false
}
}
}

@Test
fun verifyEventStartTimeWaning() = runBlockingTest {
val time = 2687940 * 1_000L
val instant = Instant.ofEpochMilli(time)
shouldNotThrowAny {
val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT)
verifyResult.apply {
singedTraceLocation.event.description shouldBe "CWA Launch Party"
}
verifyResult.isBeforeStartTime(instant) shouldBe true
verifyResult.isAfterEndTime(instant) shouldBe false
}
}

@Test
fun verifyEventEndTimeWarning() = runBlockingTest {
val instant = Instant.now()
shouldNotThrowAny {
val verifyResult = qrCodeVerifier.verify(ENCODED_EVENT)
verifyResult.apply {
singedTraceLocation.event.description shouldBe "CWA Launch Party"
}
verifyResult.isBeforeStartTime(instant) shouldBe false
verifyResult.isAfterEndTime(instant) shouldBe true
}
}

@Test
fun verifyEventWithInvalidKey() = runBlockingTest {
every { environmentSetup.appConfigVerificationKey } returns INVALID_PUB_KEY
shouldThrow<InvalidQRCodeSignatureException> {
qrCodeVerifier.verify(ENCODED_EVENT)
}
}

@Test
fun eventHasMalformedData() = runBlockingTest {
shouldThrow<InvalidQRCodeDataException> {
qrCodeVerifier.verify(INVALID_ENCODED_EVENT)
}
}

companion object {

private const val INVALID_PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc7DEstcUIRcyk35OYDJ95/hTg" +
"3UVhsaDXKT0zK7NhHPXoyzipEnOp3GyNXDVpaPi3cAfQmxeuFMZAIX2+6A5Xg=="

private const val PUB_KEY = "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEafIKZOiRPuJWjKOUmKv7OTJWTyii" +
"4oCQLcGn3FgYoLQaJIvAM3Pl7anFDPPY/jxfqqrLyGc0f6hWQ9JPR3QjBw=="

private const val INVALID_ENCODED_EVENT =
"BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ" +
"4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3U" +
"U3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ"

private const val ENCODED_EVENT =
"BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2" +
"HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGC" +
"PUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU" +
"7TYERH23B746RQTABO3CTI="
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="qr_code_input" translatable="false">"HTTPS://CORONAWARN.APP/E1/BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA.GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ"</string>
<string name="qr_code_input" translatable="false">"HTTPS://CORONAWARN.APP/E1/BIYAUEDBZY6EIWF7QX6JOKSRPAGEB3H7CIIEGV2BEBGGC5LOMNUCAUDBOJ2HSGGTQ6SACIHXQ6SACKA6CJEDARQCEEAPHGEZ5JI2K2T422L5U3SMZY5DGCPUZ2RQACAYEJ3HQYMAFFBU2SQCEEAJAUCJSQJ7WDM675MCMOD3L2UL7ECJU7TYERH23B746RQTABO3CTI="</string>
</resources>
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package de.rki.coronawarnapp.eventregistration

import dagger.Binds
import dagger.Module
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.DefaultQRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier

@Suppress("EmptyClassBlock")
@Module
class EventRegistrationModule {
// TODO
abstract class EventRegistrationModule {
@Binds
abstract fun qrCodeVerifier(qrCodeVerifier: DefaultQRCodeVerifier): QRCodeVerifier
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

import de.rki.coronawarnapp.eventregistration.common.decodeBase32
import de.rki.coronawarnapp.server.protocols.internal.evreg.SignedEventOuterClass
import de.rki.coronawarnapp.util.security.SignatureValidation
import timber.log.Timber
import javax.inject.Inject

class DefaultQRCodeVerifier @Inject constructor(
private val signatureValidation: SignatureValidation
) : QRCodeVerifier {

override suspend fun verify(encodedEvent: String): QRCodeVerifyResult {
Timber.tag(TAG).v("Verifying: %s", encodedEvent)

val signedEvent = try {
SignedEventOuterClass.SignedEvent.parseFrom(encodedEvent.decodeBase32().toByteArray())
} catch (e: Exception) {
throw InvalidQRCodeDataException(cause = e, message = "QR-code data could not be parsed.")
}
Timber.tag(TAG).d("Parsed to signed event: %s", signedEvent)

val isValid = try {
signatureValidation.hasValidSignature(
signedEvent.event.toByteArray(),
sequenceOf(signedEvent.signature.toByteArray())
)
} catch (e: Exception) {
throw InvalidQRCodeDataException(cause = e, message = "Verification failed.")
}

if (!isValid) {
throw InvalidQRCodeSignatureException(message = "QR-code did not match signature.")
}

return QRCodeVerifyResult(signedEvent)
}

companion object {
private const val TAG = "DefaultQRCodeVerifier"
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

class InvalidQRCodeDataException constructor(
message: String? = null,
cause: Throwable? = null
) : QRCodeException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

class InvalidQRCodeSignatureException constructor(
message: String? = null,
cause: Throwable? = null
) : QRCodeException(message, cause)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

open class QRCodeException constructor(
message: String? = null,
cause: Throwable? = null
) : Exception(message, cause)
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@ package de.rki.coronawarnapp.eventregistration.checkins.qrcode

interface QRCodeVerifier {

suspend fun verify(code: EventQRCode): Boolean
suspend fun verify(encodedEvent: String): QRCodeVerifyResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package de.rki.coronawarnapp.eventregistration.checkins.qrcode

import de.rki.coronawarnapp.server.protocols.internal.evreg.SignedEventOuterClass
import de.rki.coronawarnapp.util.TimeAndDateExtensions.seconds
import org.joda.time.Instant

data class QRCodeVerifyResult(
val singedTraceLocation: SignedEventOuterClass.SignedEvent
) {
fun isBeforeStartTime(now: Instant): Boolean =
singedTraceLocation.event.start != 0 && singedTraceLocation.event.start > now.seconds

fun isAfterEndTime(now: Instant): Boolean =
singedTraceLocation.event.end != 0 && singedTraceLocation.event.end < now.seconds
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ class ConfirmCheckInFragment : Fragment(R.layout.fragment_confrim_check_in), Aut
}

// TODO bind data to actual UI
viewModel.eventData.observe2(this) {
viewModel.verifyResult.observe2(this) {
val event = it.singedTraceLocation.event
with(binding) {
eventGuid.text = "GUID: %s".format(it.guid)
startTime.text = "Start time: %s".format(it.start)
endTime.text = "End time: %s".format(it.end)
description.text = "Description: %s".format(it.description)
eventGuid.text = "GUID: %s".format(event.guid)
startTime.text = "Start time: %s".format(event.start)
endTime.text = "End time: %s".format(event.end)
description.text = "Description: %s".format(event.description)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,30 @@ package de.rki.coronawarnapp.ui.eventregistration.checkin
import androidx.lifecycle.MutableLiveData
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode
import de.rki.coronawarnapp.eventregistration.common.decodeBase32
import de.rki.coronawarnapp.server.protocols.internal.evreg.EventOuterClass
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifyResult
import de.rki.coronawarnapp.exception.ExceptionCategory
import de.rki.coronawarnapp.exception.reporting.report
import de.rki.coronawarnapp.util.ui.SingleLiveEvent
import de.rki.coronawarnapp.util.viewmodel.CWAViewModel
import de.rki.coronawarnapp.util.viewmodel.SimpleCWAViewModelFactory
import org.joda.time.Instant
import timber.log.Timber

class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() {
private val eventLiveData = MutableLiveData<EventQRCode>()
val eventData = eventLiveData
class ConfirmCheckInViewModel @AssistedInject constructor(
private val qrCodeVerifier: QRCodeVerifier
) : CWAViewModel() {
private val internalVerifyResult = MutableLiveData<QRCodeVerifyResult>()
val verifyResult = internalVerifyResult
val navigationEvents = SingleLiveEvent<ConfirmCheckInEvent>()

fun decodeEvent(encodedEvent: String) = launch {
// TODO Verify event(EXPOSUREAPP-5423)
// and finalise event parsing logic
val decodedEventString = encodedEvent.split(".")[0].decodeBase32()
val parseEvent = EventOuterClass.Event.parseFrom(decodedEventString.toByteArray())
eventLiveData.postValue(parseEvent.toEventQrCode())
// TODO this logic should moved from here. Here user should confirm event only
try {
internalVerifyResult.postValue(qrCodeVerifier.verify(encodedEvent))
} catch (e: Exception) {
Timber.d(e)
e.report(ExceptionCategory.INTERNAL)
}
}

fun onClose() {
Expand All @@ -32,13 +37,6 @@ class ConfirmCheckInViewModel @AssistedInject constructor() : CWAViewModel() {
navigationEvents.value = ConfirmCheckInEvent.ConfirmEvent
}

private fun EventOuterClass.Event.toEventQrCode() = EventQRCode(
guid = String(guid.toByteArray()),
description = description,
start = Instant.ofEpochMilli(start.toLong()),
end = Instant.ofEpochMilli(end.toLong())
)

@AssistedFactory
interface Factory : SimpleCWAViewModelFactory<ConfirmCheckInViewModel>
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package de.rki.coronawarnapp.ui.eventregistration.checkin

import de.rki.coronawarnapp.eventregistration.checkins.qrcode.EventQRCode
import de.rki.coronawarnapp.eventregistration.checkins.qrcode.QRCodeVerifier
import io.kotest.matchers.shouldBe
import org.joda.time.Instant
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

Expand All @@ -16,23 +17,12 @@ class ConfirmCheckInViewModelTest : BaseTest() {

private lateinit var viewModel: ConfirmCheckInViewModel

@MockK lateinit var qrCodeVerifier: QRCodeVerifier

@BeforeEach
fun setUp() {
viewModel = ConfirmCheckInViewModel()
}

@Test
fun decodeEvent() {
val decodedEvent =
"BIPEY33SMVWSA2LQON2W2IDEN5WG64RAONUXIIDBNVSXILBAMNXRBCM4UQARRKM6UQASAHRKCC7CTDWGQ4JCO7RVZSWVIMQK4UPA" +
".GBCAEIA7TEORBTUA25QHBOCWT26BCA5PORBS2E4FFWMJ3UU3P6SXOL7SHUBCA7UEZBDDQ2R6VRJH7WBJKVF7GZYJA6YMRN27IPEP7NKGGJSWX3XQ"
viewModel.decodeEvent(decodedEvent)
viewModel.eventData.getOrAwaitValue() shouldBe EventQRCode(
guid = "Lorem ipsum dolor sit amet, co",
description = "",
start = Instant.parse("1970-01-01T00:44:50.857Z"),
end = Instant.parse("1970-01-01T00:00:00.030Z")
)
MockKAnnotations.init(this)
viewModel = ConfirmCheckInViewModel(qrCodeVerifier)
}

@Test
Expand Down

0 comments on commit e9e7635

Please sign in to comment.