Skip to content

Commit

Permalink
feat: Renew Identity certificates over the Internet (#564)
Browse files Browse the repository at this point in the history
Closes #507
  • Loading branch information
sdsantos authored Feb 22, 2022
1 parent 8f01a2b commit 809ebbe
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 24 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ dependencies {
testImplementation "io.ktor:ktor-test-dispatcher:$ktorVersion"

// ORM
def room_version = '2.4.0-alpha04' // Mac M1 issue
def room_version = '2.4.1'
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
implementation "androidx.room:room-ktx:$room_version"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ open class TestApp : App() {
component.inject(this)
}

override fun startPublicSyncWhenPossible() {
override suspend fun startPublicSyncWhenPossible() {
// Disable automatic public sync start
}

Expand Down
29 changes: 17 additions & 12 deletions app/src/main/java/tech/relaycorp/gateway/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ open class App : Application() {
enqueuePublicSyncWorker()

setupStrictMode()
bootstrapGateway()
startPublicSyncWhenPossible()

backgroundScope.launch {
bootstrapGateway()
startPublicSyncWhenPossible()
deleteExpiredCertificates()
}

registerActivityLifecycleCallbacks(foregroundAppMonitor)
}

Expand Down Expand Up @@ -123,19 +128,19 @@ open class App : Application() {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}

private fun bootstrapGateway() {
backgroundScope.launch {
if (mode != Mode.Test) {
localConfig.bootstrap()
registerGateway.registerIfNeeded()
}
private suspend fun bootstrapGateway() {
if (mode != Mode.Test) {
localConfig.bootstrap()
registerGateway.registerIfNeeded()
}
}

protected open fun startPublicSyncWhenPossible() {
backgroundScope.launch {
publicSync.sync()
}
protected open suspend fun startPublicSyncWhenPossible() {
publicSync.sync()
}

private suspend fun deleteExpiredCertificates() {
localConfig.deleteExpiredCertificates()
}

protected open fun enqueuePublicSyncWorker() {
Expand Down
5 changes: 1 addition & 4 deletions app/src/main/java/tech/relaycorp/gateway/data/DataModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,7 @@ class DataModule {

@Provides
@Singleton
fun certificateStore(
context: Context,
keystoreRoot: Provider<FileKeystoreRoot>
): CertificateStore =
fun certificateStore(keystoreRoot: Provider<FileKeystoreRoot>): CertificateStore =
FileCertificateStore(keystoreRoot.get())

@Provides
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ class LocalConfig
fileStore.store(CDA_CERTIFICATE_FILE_NAME, cda.serialize())
}

suspend fun deleteExpiredCertificates() {
certificateStore.get().deleteExpired()
}

// Helpers

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class MigrateGateway
when (registerGateway.registerNewAddress(address)) {
RegisterGateway.Result.FailedToRegister -> Result.FailedToRegister
RegisterGateway.Result.FailedToResolve -> Result.FailedToResolve
RegisterGateway.Result.AlreadyRegistered -> Result.Successful
RegisterGateway.Result.AlreadyRegisteredAndNotExpiring -> Result.Successful
is RegisterGateway.Result.Registered -> {
deleteInvalidatedData()
Result.Successful
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import tech.relaycorp.relaynet.bindings.pdc.ClientBindingException
import tech.relaycorp.relaynet.bindings.pdc.ServerException
import tech.relaycorp.relaynet.keystores.SessionPublicKeyStore
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration
import java.time.Duration
import java.time.ZonedDateTime
import java.util.logging.Level
import javax.inject.Inject

Expand All @@ -25,8 +27,11 @@ class RegisterGateway
) {

suspend fun registerIfNeeded(): Result {
if (publicGatewayPreferences.getRegistrationState() != RegistrationState.ToDo) {
return Result.AlreadyRegistered
if (
publicGatewayPreferences.getRegistrationState() != RegistrationState.ToDo &&
!currentCertificateIsAboutToExpire()
) {
return Result.AlreadyRegisteredAndNotExpiring
}

val address = publicGatewayPreferences.getAddress()
Expand All @@ -45,6 +50,9 @@ class RegisterGateway
return result
}

private suspend fun currentCertificateIsAboutToExpire() =
localConfig.getIdentityCertificate().expiryDate < ZonedDateTime.now().plus(ABOUT_TO_EXPIRE)

private suspend fun register(address: String): Result {
return try {
val poWebAddress = resolveServiceAddress.resolvePoWeb(address)
Expand Down Expand Up @@ -99,6 +107,10 @@ class RegisterGateway
object FailedToResolve : Result()
object FailedToRegister : Result()
data class Registered(val pnr: PrivateNodeRegistration) : Result()
object AlreadyRegistered : Result()
object AlreadyRegisteredAndNotExpiring : Result()
}

companion object {
private val ABOUT_TO_EXPIRE = Duration.ofDays(90)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tech.relaycorp.gateway.domain

import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.test.runBlockingTest
Expand Down Expand Up @@ -121,4 +122,11 @@ class LocalConfigTest : BaseDataTestCase() {
assertEquals(originalCDAIssuer, cdaIssuer)
}
}

@Test
internal fun deleteExpiredCertificates() = runBlockingTest {
localConfig.deleteExpiredCertificates()

verify(certificateStore).deleteExpired()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import tech.relaycorp.gateway.test.BaseDataTestCase
import tech.relaycorp.poweb.PoWebClient
import tech.relaycorp.relaynet.SessionKey
import tech.relaycorp.relaynet.bindings.pdc.ClientBindingException
import tech.relaycorp.relaynet.issueGatewayCertificate
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationAuthorization
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistrationRequest
Expand Down Expand Up @@ -75,14 +76,46 @@ class RegisterGatewayTest : BaseDataTestCase() {
}

@Test
internal fun `does not register if not needed`() = runBlockingTest {
internal fun `does not register if already registered and not expiring`() = runBlockingTest {
whenever(pgwPreferences.getRegistrationState()).thenReturn(RegistrationState.Done)
localConfig.setIdentityCertificate(
issueGatewayCertificate(
KeyPairSet.PRIVATE_GW.public,
KeyPairSet.PUBLIC_GW.private,
ZonedDateTime.now().plusYears(1), // not expiring soon
PDACertPath.PUBLIC_GW,
validityStartDate = ZonedDateTime.now().minusSeconds(1)
)
)

registerGateway.registerIfNeeded()

verifyNoMoreInteractions(poWebClient)
}

@Test
internal fun `registers if needs to renew certificate`() = runBlockingTest {
whenever(pgwPreferences.getRegistrationState()).thenReturn(RegistrationState.Done)
localConfig.setIdentityCertificate(
issueGatewayCertificate(
KeyPairSet.PRIVATE_GW.public,
KeyPairSet.PUBLIC_GW.private,
ZonedDateTime.now().plusDays(1), // expiring soon
PDACertPath.PUBLIC_GW,
validityStartDate = ZonedDateTime.now().minusSeconds(1)
)
)
whenever(poWebClient.preRegisterNode(any()))
.thenReturn(buildPNRR())
whenever(poWebClient.registerNode(any()))
.thenReturn(buildPNR(publicGatewaySessionKeyPair.sessionKey))

registerGateway.registerIfNeeded()

verify(poWebClient).preRegisterNode(any())
verify(poWebClient).registerNode(any())
}

@Test
fun `successful registration stores new values`() = runBlockingTest {
whenever(pgwPreferences.getRegistrationState()).thenReturn(RegistrationState.ToDo)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package tech.relaycorp.gateway.test

import com.nhaarman.mockitokotlin2.spy
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import tech.relaycorp.relaynet.SessionKeyPair
Expand All @@ -16,7 +17,7 @@ import javax.inject.Provider

abstract class BaseDataTestCase {
protected val privateKeyStore = MockPrivateKeyStore()
protected val certificateStore = MockCertificateStore()
protected val certificateStore = spy(MockCertificateStore())
protected val privateKeyStoreProvider = Provider<PrivateKeyStore> { privateKeyStore }
protected val certificateStoreProvider = Provider<CertificateStore> { certificateStore }

Expand Down

0 comments on commit 809ebbe

Please sign in to comment.