From 809ebbe4452375efb2514d146bb5b5eb86fecbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9rgio=20Santos?= Date: Tue, 22 Feb 2022 12:32:27 +0000 Subject: [PATCH] feat: Renew Identity certificates over the Internet (#564) Closes #507 --- app/build.gradle | 2 +- .../tech/relaycorp/gateway/test/TestApp.kt | 2 +- .../main/java/tech/relaycorp/gateway/App.kt | 29 ++++++++------- .../tech/relaycorp/gateway/data/DataModule.kt | 5 +-- .../relaycorp/gateway/domain/LocalConfig.kt | 4 +++ .../domain/publicsync/MigrateGateway.kt | 2 +- .../domain/publicsync/RegisterGateway.kt | 18 ++++++++-- .../gateway/domain/LocalConfigTest.kt | 8 +++++ .../domain/publicsync/RegisterGatewayTest.kt | 35 ++++++++++++++++++- .../gateway/test/BaseDataTestCase.kt | 3 +- 10 files changed, 84 insertions(+), 24 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index dc6651ce..fda9a36e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/androidTest/java/tech/relaycorp/gateway/test/TestApp.kt b/app/src/androidTest/java/tech/relaycorp/gateway/test/TestApp.kt index 05647fef..142e553d 100644 --- a/app/src/androidTest/java/tech/relaycorp/gateway/test/TestApp.kt +++ b/app/src/androidTest/java/tech/relaycorp/gateway/test/TestApp.kt @@ -13,7 +13,7 @@ open class TestApp : App() { component.inject(this) } - override fun startPublicSyncWhenPossible() { + override suspend fun startPublicSyncWhenPossible() { // Disable automatic public sync start } diff --git a/app/src/main/java/tech/relaycorp/gateway/App.kt b/app/src/main/java/tech/relaycorp/gateway/App.kt index dcdf38f1..6cf1a7ab 100644 --- a/app/src/main/java/tech/relaycorp/gateway/App.kt +++ b/app/src/main/java/tech/relaycorp/gateway/App.kt @@ -73,8 +73,13 @@ open class App : Application() { enqueuePublicSyncWorker() setupStrictMode() - bootstrapGateway() - startPublicSyncWhenPossible() + + backgroundScope.launch { + bootstrapGateway() + startPublicSyncWhenPossible() + deleteExpiredCertificates() + } + registerActivityLifecycleCallbacks(foregroundAppMonitor) } @@ -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() { diff --git a/app/src/main/java/tech/relaycorp/gateway/data/DataModule.kt b/app/src/main/java/tech/relaycorp/gateway/data/DataModule.kt index aab2ad7b..5465a592 100644 --- a/app/src/main/java/tech/relaycorp/gateway/data/DataModule.kt +++ b/app/src/main/java/tech/relaycorp/gateway/data/DataModule.kt @@ -140,10 +140,7 @@ class DataModule { @Provides @Singleton - fun certificateStore( - context: Context, - keystoreRoot: Provider - ): CertificateStore = + fun certificateStore(keystoreRoot: Provider): CertificateStore = FileCertificateStore(keystoreRoot.get()) @Provides diff --git a/app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt b/app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt index d787f615..82b7caf6 100644 --- a/app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt +++ b/app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt @@ -106,6 +106,10 @@ class LocalConfig fileStore.store(CDA_CERTIFICATE_FILE_NAME, cda.serialize()) } + suspend fun deleteExpiredCertificates() { + certificateStore.get().deleteExpired() + } + // Helpers companion object { diff --git a/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/MigrateGateway.kt b/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/MigrateGateway.kt index 55cf6ce0..202df970 100644 --- a/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/MigrateGateway.kt +++ b/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/MigrateGateway.kt @@ -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 diff --git a/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/RegisterGateway.kt b/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/RegisterGateway.kt index e046add0..35e28938 100644 --- a/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/RegisterGateway.kt +++ b/app/src/main/java/tech/relaycorp/gateway/domain/publicsync/RegisterGateway.kt @@ -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 @@ -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() @@ -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) @@ -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) } } diff --git a/app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt b/app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt index e8b9f108..524c88d1 100644 --- a/app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt +++ b/app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt @@ -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 @@ -121,4 +122,11 @@ class LocalConfigTest : BaseDataTestCase() { assertEquals(originalCDAIssuer, cdaIssuer) } } + + @Test + internal fun deleteExpiredCertificates() = runBlockingTest { + localConfig.deleteExpiredCertificates() + + verify(certificateStore).deleteExpired() + } } diff --git a/app/src/test/java/tech/relaycorp/gateway/domain/publicsync/RegisterGatewayTest.kt b/app/src/test/java/tech/relaycorp/gateway/domain/publicsync/RegisterGatewayTest.kt index f86c7d78..d78923af 100644 --- a/app/src/test/java/tech/relaycorp/gateway/domain/publicsync/RegisterGatewayTest.kt +++ b/app/src/test/java/tech/relaycorp/gateway/domain/publicsync/RegisterGatewayTest.kt @@ -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 @@ -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) diff --git a/app/src/test/java/tech/relaycorp/gateway/test/BaseDataTestCase.kt b/app/src/test/java/tech/relaycorp/gateway/test/BaseDataTestCase.kt index d8f8f116..75a1baf4 100644 --- a/app/src/test/java/tech/relaycorp/gateway/test/BaseDataTestCase.kt +++ b/app/src/test/java/tech/relaycorp/gateway/test/BaseDataTestCase.kt @@ -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 @@ -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 } protected val certificateStoreProvider = Provider { certificateStore }