Skip to content

Commit 4e11c74

Browse files
authored
fix: Self-issue certificate when app is used for the first time (#267)
The problem here is that the certificate's start date was always later than the CCA's or the cargo's (whose creation time is 90 mins in the past). I'm proposing to fix this by creating this certificate upfront, along with its key pair. Fixes relaycorp/relaynet-courier-android#277
1 parent 28d385f commit 4e11c74

File tree

6 files changed

+152
-47
lines changed

6 files changed

+152
-47
lines changed

app/src/androidTest/java/tech/relaycorp/gateway/background/endpoint/EndpointPreRegistrationServiceTest.kt

+12-14
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,19 @@ import android.os.Message
88
import android.os.Messenger
99
import androidx.test.core.app.ApplicationProvider.getApplicationContext
1010
import androidx.test.rule.ServiceTestRule
11-
import com.schibsted.spain.barista.rule.cleardata.ClearPreferencesRule
1211
import kotlinx.coroutines.runBlocking
13-
import org.junit.After
1412
import org.junit.Assert.assertEquals
1513
import org.junit.Assert.assertNotNull
1614
import org.junit.Assert.assertTrue
1715
import org.junit.Before
1816
import org.junit.Rule
1917
import org.junit.Test
18+
import tech.relaycorp.gateway.App
2019
import tech.relaycorp.gateway.data.model.RegistrationState
2120
import tech.relaycorp.gateway.data.preference.PublicGatewayPreferences
2221
import tech.relaycorp.gateway.domain.LocalConfig
2322
import tech.relaycorp.gateway.test.AppTestProvider
23+
import tech.relaycorp.gateway.test.WaitAssertions.suspendWaitFor
2424
import tech.relaycorp.gateway.test.WaitAssertions.waitFor
2525
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationAuthorization
2626
import java.nio.charset.Charset
@@ -30,29 +30,29 @@ class EndpointPreRegistrationServiceTest {
3030

3131
@get:Rule
3232
val serviceRule = ServiceTestRule()
33-
@get:Rule
34-
val clearPreferencesRule = ClearPreferencesRule()
33+
34+
@Inject
35+
lateinit var app: App
3536

3637
@Inject
3738
lateinit var localConfig: LocalConfig
3839

3940
@Inject
4041
lateinit var publicGatewayPreferences: PublicGatewayPreferences
4142

43+
private val coroutineContext get() = app.backgroundScope.coroutineContext
44+
4245
@Before
4346
fun setUp() {
4447
AppTestProvider.component.inject(this)
45-
runBlocking {
48+
runBlocking(coroutineContext) {
49+
suspendWaitFor { localConfig.getKeyPair() }
4650
publicGatewayPreferences.setRegistrationState(RegistrationState.Done)
4751
}
4852
}
4953

50-
@After
51-
fun tearDown() {
52-
}
53-
5454
@Test
55-
fun requestPreRegistration() = runBlocking {
55+
fun requestPreRegistration() = runBlocking(coroutineContext) {
5656
val serviceIntent = Intent(
5757
getApplicationContext<Context>(),
5858
EndpointPreRegistrationService::class.java
@@ -109,10 +109,8 @@ class EndpointPreRegistrationServiceTest {
109109
}
110110

111111
@Test
112-
fun errorReturnedWhenGatewayIsNotRegisteredYet() {
113-
runBlocking {
114-
publicGatewayPreferences.setRegistrationState(RegistrationState.ToDo)
115-
}
112+
fun errorReturnedWhenGatewayIsNotRegisteredYet() = runBlocking(coroutineContext) {
113+
publicGatewayPreferences.setRegistrationState(RegistrationState.ToDo)
116114

117115
val serviceIntent = Intent(
118116
getApplicationContext<Context>(),

app/src/main/java/tech/relaycorp/gateway/App.kt

+15-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package tech.relaycorp.gateway
33
import android.app.Application
44
import android.os.Build
55
import android.os.StrictMode
6+
import androidx.annotation.VisibleForTesting
67
import androidx.work.Configuration
78
import androidx.work.Constraints
89
import androidx.work.ExistingPeriodicWorkPolicy
@@ -19,6 +20,7 @@ import tech.relaycorp.gateway.background.publicsync.PublicSyncWorkerFactory
1920
import tech.relaycorp.gateway.common.Logging
2021
import tech.relaycorp.gateway.common.di.AppComponent
2122
import tech.relaycorp.gateway.common.di.DaggerAppComponent
23+
import tech.relaycorp.gateway.domain.LocalConfig
2224
import tech.relaycorp.gateway.domain.publicsync.PublicSync
2325
import tech.relaycorp.gateway.domain.publicsync.RegisterGateway
2426
import java.security.Security
@@ -44,11 +46,15 @@ open class App : Application() {
4446
}
4547
}
4648

47-
private val ioScope = CoroutineScope(Dispatchers.IO)
49+
@VisibleForTesting
50+
val backgroundScope = CoroutineScope(Dispatchers.IO)
4851

4952
@Inject
5053
lateinit var foregroundAppMonitor: ForegroundAppMonitor
5154

55+
@Inject
56+
lateinit var localConfig: LocalConfig
57+
5258
@Inject
5359
lateinit var registerGateway: RegisterGateway
5460

@@ -67,7 +73,7 @@ open class App : Application() {
6773
enqueuePublicSyncWorker()
6874

6975
setupStrictMode()
70-
registerGateway()
76+
bootstrapGateway()
7177
startPublicSyncWhenPossible()
7278
registerActivityLifecycleCallbacks(foregroundAppMonitor)
7379
}
@@ -117,15 +123,17 @@ open class App : Application() {
117123
Security.insertProviderAt(Conscrypt.newProvider(), 1)
118124
}
119125

120-
private fun registerGateway() {
121-
if (mode == Mode.Test) return
122-
ioScope.launch {
123-
registerGateway.registerIfNeeded()
126+
private fun bootstrapGateway() {
127+
backgroundScope.launch {
128+
localConfig.bootstrap()
129+
if (mode != Mode.Test) {
130+
registerGateway.registerIfNeeded()
131+
}
124132
}
125133
}
126134

127135
protected open fun startPublicSyncWhenPossible() {
128-
ioScope.launch {
136+
backgroundScope.launch {
129137
publicSync.sync()
130138
}
131139
}

app/src/main/java/tech/relaycorp/gateway/data/disk/SensitiveStore.kt

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class SensitiveStore
2222
.build()
2323
}
2424

25+
@Synchronized
2526
suspend fun store(location: String, data: ByteArray) {
2627
withContext(Dispatchers.IO) {
2728
delete(location)

app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt

+27-8
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package tech.relaycorp.gateway.domain
22

3+
import androidx.annotation.VisibleForTesting
34
import tech.relaycorp.gateway.common.CryptoUtils.BC_PROVIDER
45
import tech.relaycorp.gateway.common.nowInUtc
56
import tech.relaycorp.gateway.data.disk.SensitiveStore
7+
import tech.relaycorp.gateway.domain.courier.CalculateCRCMessageCreationDate
68
import tech.relaycorp.relaynet.issueGatewayCertificate
79
import tech.relaycorp.relaynet.wrappers.generateRSAKeyPair
810
import tech.relaycorp.relaynet.wrappers.x509.Certificate
@@ -14,6 +16,7 @@ import java.security.spec.EncodedKeySpec
1416
import java.security.spec.PKCS8EncodedKeySpec
1517
import java.security.spec.RSAPublicKeySpec
1618
import javax.inject.Inject
19+
import kotlin.time.toJavaDuration
1720

1821
class LocalConfig
1922
@Inject constructor(
@@ -25,8 +28,10 @@ class LocalConfig
2528
sensitiveStore.read(PRIVATE_KEY_FILE_NAME)
2629
?.toPrivateKey()
2730
?.toKeyPair()
28-
?: generateKeyPair()
29-
.also { setKeyPair(it) }
31+
?: throw RuntimeException("No key pair was found")
32+
33+
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
34+
suspend fun generateKeyPair() = generateRSAKeyPair().also { setKeyPair(it) }
3035

3136
private suspend fun setKeyPair(value: KeyPair) {
3237
sensitiveStore.store(PRIVATE_KEY_FILE_NAME, value.private.encoded)
@@ -51,11 +56,25 @@ class LocalConfig
5156
suspend fun getCargoDeliveryAuth() =
5257
sensitiveStore.read(CDA_CERTIFICATE_FILE_NAME)
5358
?.let { Certificate.deserialize(it) }
54-
?: generateCertificate()
55-
.also { setCargoDeliveryAuth(it) }
59+
?: throw RuntimeException("No CDA issuer was found")
5660

57-
private suspend fun setCargoDeliveryAuth(value: Certificate) {
58-
sensitiveStore.store(CDA_CERTIFICATE_FILE_NAME, value.serialize())
61+
private suspend fun generateCargoDeliveryAuth() = generateCertificate().also {
62+
sensitiveStore.store(CDA_CERTIFICATE_FILE_NAME, it.serialize())
63+
}
64+
65+
@Synchronized
66+
suspend fun bootstrap() {
67+
try {
68+
getKeyPair()
69+
} catch (_: RuntimeException) {
70+
generateKeyPair()
71+
}
72+
73+
try {
74+
getCargoDeliveryAuth()
75+
} catch (_: RuntimeException) {
76+
generateCargoDeliveryAuth()
77+
}
5978
}
6079

6180
// Helpers
@@ -74,13 +93,13 @@ class LocalConfig
7493
return KeyPair(publicKey, this)
7594
}
7695

77-
private fun generateKeyPair() = generateRSAKeyPair()
78-
7996
private suspend fun generateCertificate(): Certificate {
8097
val keyPair = getKeyPair()
8198
return issueGatewayCertificate(
8299
subjectPublicKey = keyPair.public,
83100
issuerPrivateKey = keyPair.private,
101+
validityStartDate = nowInUtc()
102+
.minus(CalculateCRCMessageCreationDate.CLOCK_DRIFT_TOLERANCE.toJavaDuration()),
84103
validityEndDate = nowInUtc().plusYears(1)
85104
)
86105
}

app/src/main/java/tech/relaycorp/gateway/domain/courier/CalculateCRCMessageCreationDate.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@ class CalculateCRCMessageCreationDate
2121
)
2222

2323
companion object {
24-
private val CLOCK_DRIFT_TOLERANCE = 90.minutes
24+
val CLOCK_DRIFT_TOLERANCE = 90.minutes
2525
}
2626
}

app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt

+96-17
Original file line numberDiff line numberDiff line change
@@ -7,45 +7,124 @@ import kotlinx.coroutines.runBlocking
77
import kotlinx.coroutines.test.runBlockingTest
88
import org.junit.jupiter.api.Assertions.assertTrue
99
import org.junit.jupiter.api.BeforeEach
10+
import org.junit.jupiter.api.Nested
1011
import org.junit.jupiter.api.Test
12+
import org.junit.jupiter.api.assertThrows
1113
import tech.relaycorp.gateway.data.disk.SensitiveStore
14+
import kotlin.test.assertEquals
1215

13-
internal class LocalConfigTest {
16+
class LocalConfigTest {
1417

1518
private val sensitiveStore = mock<SensitiveStore>()
1619
private val localConfig = LocalConfig(sensitiveStore)
1720

1821
@BeforeEach
19-
internal fun setUp() {
22+
fun setUp() {
2023
runBlocking {
21-
var stored: ByteArray? = null
24+
val memoryStore = mutableMapOf<String, ByteArray>()
2225
whenever(sensitiveStore.store(any(), any())).then {
23-
stored = it.getArgument(1) as ByteArray
26+
val key = it.getArgument<String>(0)
27+
val value = it.getArgument(1) as ByteArray
28+
memoryStore[key] = value
2429
Unit
2530
}
26-
whenever(sensitiveStore.read(any())).thenAnswer { stored }
31+
whenever(sensitiveStore.read(any())).thenAnswer {
32+
val key = it.getArgument<String>(0)
33+
memoryStore[key]
34+
}
2735
}
2836
}
2937

30-
@Test
31-
internal fun `get key pair stores and recovers the same key pair`() = runBlockingTest {
32-
val keyPair1 = localConfig.getKeyPair()
33-
val keyPair2 = localConfig.getKeyPair()
34-
assertTrue(keyPair2.private.encoded!!.contentEquals(keyPair1.private.encoded))
35-
assertTrue(keyPair2.public.encoded!!.contentEquals(keyPair1.public.encoded))
38+
@Nested
39+
inner class GetKeyPair {
40+
@Test
41+
fun `Key pair should be returned if it exists`() = runBlockingTest {
42+
val keyPair = localConfig.generateKeyPair()
43+
44+
val retrievedKeyPair = localConfig.getKeyPair()
45+
46+
assertEquals(
47+
keyPair.private.encoded.asList(),
48+
retrievedKeyPair.private.encoded.asList()
49+
)
50+
}
51+
52+
@Test
53+
fun `Exception should be thrown if key pair does not exist`() = runBlockingTest {
54+
val exception = assertThrows<RuntimeException> {
55+
localConfig.getKeyPair()
56+
}
57+
58+
assertEquals("No key pair was found", exception.message)
59+
}
3660
}
3761

3862
@Test
39-
internal fun `get certificate stores and recovers the same certificate`() = runBlockingTest {
63+
fun `get certificate stores and recovers the same certificate`() = runBlockingTest {
64+
localConfig.generateKeyPair()
65+
4066
val certificate1 = localConfig.getCertificate().serialize()
4167
val certificate2 = localConfig.getCertificate().serialize()
4268
assertTrue(certificate1.contentEquals(certificate2))
4369
}
4470

45-
@Test
46-
internal fun `get CDA stores and recovers the same certificate`() = runBlockingTest {
47-
val certificate1 = localConfig.getCargoDeliveryAuth().serialize()
48-
val certificate2 = localConfig.getCargoDeliveryAuth().serialize()
49-
assertTrue(certificate1.contentEquals(certificate2))
71+
@Nested
72+
inner class GetCargoDeliveryAuth {
73+
@Test
74+
fun `Certificate should be returned if it exists`() = runBlockingTest {
75+
localConfig.bootstrap()
76+
77+
val certificate1 = localConfig.getCargoDeliveryAuth().serialize()
78+
val certificate2 = localConfig.getCargoDeliveryAuth().serialize()
79+
assertTrue(certificate1.contentEquals(certificate2))
80+
}
81+
82+
@Test
83+
fun `Exception should be thrown if certificate does not exist yet`() = runBlockingTest {
84+
val exception = assertThrows<RuntimeException> {
85+
localConfig.getCargoDeliveryAuth()
86+
}
87+
88+
assertEquals("No CDA issuer was found", exception.message)
89+
}
90+
}
91+
92+
@Nested
93+
inner class Bootstrap {
94+
@Test
95+
fun `Key pair should be created if it doesn't already exist`() = runBlockingTest {
96+
localConfig.bootstrap()
97+
98+
localConfig.getKeyPair()
99+
}
100+
101+
@Test
102+
fun `Key pair should not be created if it already exists`() = runBlockingTest {
103+
localConfig.bootstrap()
104+
val originalKeyPair = localConfig.getKeyPair()
105+
106+
localConfig.bootstrap()
107+
val keyPair = localConfig.getKeyPair()
108+
109+
assertEquals(originalKeyPair.private.encoded.asList(), keyPair.private.encoded.asList())
110+
}
111+
112+
@Test
113+
fun `CDA issuer should be created if it doesn't already exist`() = runBlockingTest {
114+
localConfig.bootstrap()
115+
116+
localConfig.getCargoDeliveryAuth()
117+
}
118+
119+
@Test
120+
fun `CDA issuer should not be created if it already exists`() = runBlockingTest {
121+
localConfig.bootstrap()
122+
val originalCDAIssuer = localConfig.getCargoDeliveryAuth()
123+
124+
localConfig.bootstrap()
125+
val cdaIssuer = localConfig.getCargoDeliveryAuth()
126+
127+
assertEquals(originalCDAIssuer, cdaIssuer)
128+
}
50129
}
51130
}

0 commit comments

Comments
 (0)