diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt index 6fb8c006..3066a028 100644 --- a/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt +++ b/lib/src/main/java/tech/relaycorp/awaladroid/Awala.kt @@ -9,7 +9,9 @@ import tech.relaycorp.awala.keystores.file.FileKeystoreRoot import tech.relaycorp.awala.keystores.file.FileSessionPublicKeystore import tech.relaycorp.awaladroid.background.ServiceInteractor import tech.relaycorp.awaladroid.endpoint.ChannelManager +import tech.relaycorp.awaladroid.endpoint.FirstPartyEndpoint import tech.relaycorp.awaladroid.endpoint.HandleGatewayCertificateChange +import tech.relaycorp.awaladroid.endpoint.RenewExpiringCertificates import tech.relaycorp.awaladroid.storage.StorageImpl import tech.relaycorp.awaladroid.storage.persistence.DiskPersistence import tech.relaycorp.relaynet.nodes.EndpointManager @@ -53,7 +55,10 @@ public object Awala { ) coroutineScope { - launch { fileCertificateStore.deleteExpired() } + launch { + RenewExpiringCertificates(androidPrivateKeyStore, FirstPartyEndpoint::load)() + fileCertificateStore.deleteExpired() + } } } diff --git a/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt new file mode 100644 index 00000000..9fc72885 --- /dev/null +++ b/lib/src/main/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificates.kt @@ -0,0 +1,31 @@ +package tech.relaycorp.awaladroid.endpoint + +import java.time.ZonedDateTime +import kotlin.time.Duration +import kotlin.time.days +import tech.relaycorp.relaynet.keystores.PrivateKeyStore +import tech.relaycorp.relaynet.wrappers.privateAddress +import tech.relaycorp.relaynet.wrappers.x509.Certificate + +internal class RenewExpiringCertificates( + private val privateKeyStore: PrivateKeyStore, + private val firstPartyEndpointLoader: suspend (String) -> FirstPartyEndpoint? +) { + + suspend operator fun invoke() { + privateKeyStore.retrieveAllIdentityKeys() + .mapNotNull { firstPartyEndpointLoader(it.privateAddress) } + .forEach { + if (it.identityCertificate.isExpiring) { + it.reRegister() + } + } + } + + private val Certificate.isExpiring get() = + expiryDate <= ZonedDateTime.now().plusSeconds(EXPIRATION_THRESHOLD.inWholeSeconds) + + companion object { + internal val EXPIRATION_THRESHOLD = Duration.days(60) + } +} diff --git a/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt new file mode 100644 index 00000000..b103946c --- /dev/null +++ b/lib/src/test/java/tech/relaycorp/awaladroid/endpoint/RenewExpiringCertificatesTest.kt @@ -0,0 +1,55 @@ +package tech.relaycorp.awaladroid.endpoint + +import com.nhaarman.mockitokotlin2.mock +import com.nhaarman.mockitokotlin2.never +import com.nhaarman.mockitokotlin2.verify +import com.nhaarman.mockitokotlin2.whenever +import java.time.ZonedDateTime +import kotlinx.coroutines.test.runBlockingTest +import org.junit.Before +import org.junit.Test +import tech.relaycorp.relaynet.issueEndpointCertificate +import tech.relaycorp.relaynet.keystores.PrivateKeyStore +import tech.relaycorp.relaynet.testing.pki.KeyPairSet + +internal class RenewExpiringCertificatesTest() { + + private val privateKeyStore = mock() + + @Before + fun setUp() = runBlockingTest { + whenever(privateKeyStore.retrieveAllIdentityKeys()) + .thenReturn(listOf(KeyPairSet.PRIVATE_ENDPOINT.private)) + } + + @Test + fun `renews expiring certificates`() = runBlockingTest { + val expiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(50)) + val subject = RenewExpiringCertificates(privateKeyStore) { expiringEndpoint } + + subject() + + verify(expiringEndpoint).reRegister() + } + + @Test + fun `does not renew not expiring certificates`() = runBlockingTest { + val notExpiringEndpoint = buildFirstPartyEndpoint(ZonedDateTime.now().plusDays(70)) + val subject = RenewExpiringCertificates(privateKeyStore) { notExpiringEndpoint } + + subject() + + verify(notExpiringEndpoint, never()).reRegister() + } + + private fun buildFirstPartyEndpoint(certExpiryDate: ZonedDateTime): FirstPartyEndpoint { + val firstPartyEndpoint = mock() + val expiringCert = issueEndpointCertificate( + KeyPairSet.PRIVATE_ENDPOINT.public, + KeyPairSet.PRIVATE_GW.private, + certExpiryDate + ) + whenever(firstPartyEndpoint.identityCertificate).thenReturn(expiringCert) + return firstPartyEndpoint + } +}