Skip to content

Commit

Permalink
fix: Update CertificateStore to use scope (#588)
Browse files Browse the repository at this point in the history
Related with #571 
Closes #570
  • Loading branch information
sdsantos authored Apr 1, 2022
1 parent 2690206 commit 2a213ee
Show file tree
Hide file tree
Showing 19 changed files with 206 additions and 65 deletions.
6 changes: 3 additions & 3 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ buildscript {
ext {
ktorVersion = '1.6.2'
junitVersion = '5.7.2'
awalaTestingVersion = '1.4.0'
awalaTestingVersion = '1.5.0'
}
}

Expand Down Expand Up @@ -139,8 +139,8 @@ dependencies {
kapt 'com.google.dagger:dagger-compiler:2.37'

// Awala
implementation 'tech.relaycorp:awala:1.62.1'
implementation 'tech.relaycorp:awala-keystore-file:1.5.0'
implementation 'tech.relaycorp:awala:1.63.1'
implementation 'tech.relaycorp:awala-keystore-file:1.6.0'
implementation 'tech.relaycorp:cogrpc:1.1.18'
implementation 'tech.relaycorp:cogrpc-okhttp:1.1.11'
testImplementation "tech.relaycorp:awala-testing:$awalaTestingVersion"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlinx.coroutines.runBlocking
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement
import tech.relaycorp.gateway.data.preference.PublicGatewayPreferences
import tech.relaycorp.relaynet.keystores.CertificateStore
import tech.relaycorp.relaynet.keystores.PrivateKeyStore
import tech.relaycorp.relaynet.testing.pki.KeyPairSet
Expand All @@ -20,6 +21,9 @@ class KeystoreResetTestRule : TestRule {
@Inject
lateinit var privateKeyStore: PrivateKeyStore

@Inject
lateinit var publicGatewayPreferences: PublicGatewayPreferences

@Inject
lateinit var certificateStore: CertificateStore

Expand All @@ -32,7 +36,11 @@ class KeystoreResetTestRule : TestRule {
keystoresFile.deleteRecursively()
runBlocking {
privateKeyStore.saveIdentityKey(KeyPairSet.PRIVATE_GW.private)
certificateStore.save(PDACertPath.PRIVATE_GW)
certificateStore.save(
PDACertPath.PRIVATE_GW,
emptyList(),
publicGatewayPreferences.getPrivateAddress()
)
}

base.evaluate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import tech.relaycorp.gateway.data.disk.ReadRawFile
import tech.relaycorp.gateway.data.doh.PublicAddressResolutionException
import tech.relaycorp.gateway.data.doh.ResolveServiceAddress
import tech.relaycorp.gateway.data.model.RegistrationState
import tech.relaycorp.relaynet.wrappers.deserializeRSAPublicKey
import tech.relaycorp.relaynet.wrappers.privateAddress
import tech.relaycorp.relaynet.wrappers.x509.Certificate
import java.security.PublicKey
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton

@Singleton
class PublicGatewayPreferences
@Inject constructor(
private val preferences: Provider<FlowSharedPreferences>,
Expand All @@ -36,26 +41,46 @@ class PublicGatewayPreferences
@Throws(PublicAddressResolutionException::class)
suspend fun getPoWebAddress() = resolveServiceAddress.resolvePoWeb(getAddress())

// Certificate
// Public Key

private val certificate by lazy {
preferences.get().getString("public_gateway_certificate")
private val publicKey by lazy {
preferences.get().getString("public_gateway_public_key")
}

suspend fun getCertificate() = observeCertificate().first()
suspend fun getPublicKey(): PublicKey = observePublicKey().first()

private fun observeCertificate() = { certificate }.toFlow()
private fun observePublicKey(): Flow<PublicKey> = { publicKey }.toFlow()
.map {
val certificateBytes = if (it.isEmpty()) {
if (it.isEmpty()) {
readRawFile.read(R.raw.public_gateway_cert)
.let(Certificate.Companion::deserialize)
.subjectPublicKey
} else {
Base64.decode(it, Base64.DEFAULT)
.deserializeRSAPublicKey()
}
Certificate.deserialize(certificateBytes)
}

suspend fun setCertificate(value: Certificate) {
certificate.setAndCommit(Base64.encodeToString(value.serialize(), Base64.DEFAULT))
suspend fun setPublicKey(value: PublicKey) {
publicKey.setAndCommit(Base64.encodeToString(value.encoded, Base64.DEFAULT))
setPrivateAddress(value.privateAddress)
}

// Private Address

private val privateAddress by lazy {
preferences.get().getString("public_gateway_private_address")
}

suspend fun getPrivateAddress(): String =
privateAddress.get().ifEmpty {
getPublicKey().privateAddress.also {
setPrivateAddress(it)
}
}

private suspend fun setPrivateAddress(value: String) {
privateAddress.setAndCommit(value)
}

// Registration State
Expand Down
16 changes: 12 additions & 4 deletions app/src/main/java/tech/relaycorp/gateway/domain/LocalConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tech.relaycorp.gateway.domain
import tech.relaycorp.gateway.common.nowInUtc
import tech.relaycorp.gateway.common.toPublicKey
import tech.relaycorp.gateway.data.disk.FileStore
import tech.relaycorp.gateway.data.preference.PublicGatewayPreferences
import tech.relaycorp.gateway.domain.courier.CalculateCRCMessageCreationDate
import tech.relaycorp.relaynet.issueGatewayCertificate
import tech.relaycorp.relaynet.keystores.CertificateStore
Expand All @@ -22,7 +23,8 @@ class LocalConfig
@Inject constructor(
private val fileStore: FileStore,
private val privateKeyStore: Provider<PrivateKeyStore>,
private val certificateStore: Provider<CertificateStore>
private val certificateStore: Provider<CertificateStore>,
private val publicGatewayPreferences: PublicGatewayPreferences
) {
// Private Gateway Key Pair

Expand All @@ -42,21 +44,24 @@ class LocalConfig
getIdentityCertificationPath().leafCertificate

private suspend fun getIdentityCertificationPath(): CertificationPath = getIdentityKey().let {
certificateStore.get().retrieveLatest(it.privateAddress)
certificateStore.get()
.retrieveLatest(it.privateAddress, getPublicGatewayPrivateAddress())
?: CertificationPath(generateIdentityCertificate(it), emptyList())
}

suspend fun getAllValidIdentityCertificates(): List<Certificate> =
getAllValidIdentityCertificationPaths().map { it.leafCertificate }

private suspend fun getAllValidIdentityCertificationPaths(): List<CertificationPath> =
certificateStore.get().retrieveAll(getIdentityKey().privateAddress)
certificateStore.get()
.retrieveAll(getIdentityKey().privateAddress, getPublicGatewayPrivateAddress())

suspend fun setIdentityCertificate(
leafCertificate: Certificate,
certificateChain: List<Certificate> = emptyList()
) {
certificateStore.get().save(leafCertificate, certificateChain)
certificateStore.get()
.save(leafCertificate, certificateChain, getPublicGatewayPrivateAddress())
}

private suspend fun generateIdentityCertificate(privateKey: PrivateKey): Certificate {
Expand Down Expand Up @@ -110,6 +115,9 @@ class LocalConfig
certificateStore.get().deleteExpired()
}

private suspend fun getPublicGatewayPrivateAddress() =
publicGatewayPreferences.getPrivateAddress()

// Helpers

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class GenerateCCA
suspend fun generateSerialized(): ByteArray {
val identityPrivateKey = localConfig.getIdentityKey()
val cdaIssuer = localConfig.getCargoDeliveryAuth()
val publicGatewayPublicKey = publicGatewayPreferences.getCertificate().subjectPublicKey
val publicGatewayPublicKey = publicGatewayPreferences.getPublicKey()
val cda = issueDeliveryAuthorization(
publicGatewayPublicKey,
identityPrivateKey,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ class GenerateCargo
logger.info("Generating cargo for $recipientAddress")
val cargoMessageSetCiphertext = gatewayManager.get().wrapMessagePayload(
cargoMessageSet,
publicGatewayPreferences.getCertificate().subjectPrivateAddress,
publicGatewayPreferences.getPrivateAddress(),
identityCert.subjectPrivateAddress
)
val cargo = Cargo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class RotateCertificate @Inject constructor(

localConfig.setIdentityCertificate(newIdCert)
certRotation.chain.first().let { publicGatewayCert ->
publicGatewayPreferences.setCertificate(publicGatewayCert)
publicGatewayPreferences.setPublicKey(publicGatewayCert.subjectPublicKey)
}

notifyEndpointsChangeNotifier.notifyAll()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ class EndpointRegistration
val endpointCertificate = issueEndpointCertificate(
request.privateNodePublicKey,
identityKey,
ZonedDateTime.now().plusYears(ENDPOINT_CERTIFICATE_VALIDITY_YEARS),
identityCert.expiryDate,
identityCert
)
val registration = PrivateNodeRegistration(endpointCertificate, identityCert)
Expand All @@ -63,6 +63,5 @@ class EndpointRegistration

companion object {
private const val AUTHORIZATION_VALIDITY_SECONDS: Long = 15
private const val ENDPOINT_CERTIFICATE_VALIDITY_YEARS = 3L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ class RegisterGateway
) {
publicGatewayPreferences.setRegistrationState(RegistrationState.ToDo)
publicGatewayPreferences.setAddress(publicGatewayPublicAddress)
publicGatewayPreferences.setCertificate(registration.gatewayCertificate)
publicGatewayPreferences.setPublicKey(registration.gatewayCertificate.subjectPublicKey)
localConfig.setIdentityCertificate(registration.privateNodeCertificate)
publicKeyStore.save(
registration.gatewaySessionKey!!,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package tech.relaycorp.gateway.data.preference

import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.anyOrNull
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.whenever
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import com.fredporciuncula.flow.preferences.Preference
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
Expand All @@ -14,6 +17,7 @@ import tech.relaycorp.gateway.data.disk.ReadRawFile
import tech.relaycorp.gateway.data.doh.PublicAddressResolutionException
import tech.relaycorp.gateway.data.doh.ResolveServiceAddress
import tech.relaycorp.gateway.data.model.ServiceAddress
import tech.relaycorp.relaynet.testing.pki.PDACertPath
import javax.inject.Provider
import kotlin.test.assertEquals

Expand All @@ -31,6 +35,10 @@ class PublicGatewayPreferencesTest {
private val publicGatewayTargetHost = "poweb.example.com"
private val publicGatewayTargetPort = 135
private val mockPublicGatewayAddressPreference = mock<Preference<String>>()
private val emptyStringPreference = mock<Preference<String>> {
whenever(it.asFlow()).thenReturn(flowOf(""))
whenever(it.get()).thenReturn("")
}

@BeforeEach
internal fun setUp() {
Expand All @@ -40,6 +48,8 @@ class PublicGatewayPreferencesTest {
mockSharedPreferences
.getString("address", PublicGatewayPreferences.DEFAULT_ADDRESS)
).thenReturn(mockPublicGatewayAddressPreference)
whenever(mockSharedPreferences.getString(eq("public_gateway_public_key"), anyOrNull()))
.thenReturn(emptyStringPreference)
}
}

Expand All @@ -66,4 +76,50 @@ class PublicGatewayPreferencesTest {
}
}
}

@Nested
inner class GetPublicKey {
@Test
fun `getPublicKey returns certificate public key`() = runBlockingTest {
whenever(mockReadRawFile.read(any())).thenReturn(PDACertPath.PUBLIC_GW.serialize())

val publicKey = gwPreferences.getPublicKey()

assertEquals(PDACertPath.PUBLIC_GW.subjectPublicKey, publicKey)
}
}

@Nested
inner class GetPrivateAddress {
@Test
fun `getPrivateAddress returns certificate private address`() = runBlockingTest {
whenever(
mockSharedPreferences.getString(
eq("public_gateway_private_address"),
anyOrNull()
)
)
.thenReturn(emptyStringPreference)
whenever(mockReadRawFile.read(any())).thenReturn(PDACertPath.PUBLIC_GW.serialize())

val address = gwPreferences.getPrivateAddress()

assertEquals(PDACertPath.PUBLIC_GW.subjectPrivateAddress, address)
}

@Test
fun `getPrivateAddress returns cached private address`() = runBlockingTest {
val preference = mock<Preference<String>> {
whenever(it.get()).thenReturn("private_address")
}
whenever(
mockSharedPreferences.getString(eq("public_gateway_private_address"), anyOrNull())
)
.thenReturn(preference)

val address = gwPreferences.getPrivateAddress()

assertEquals("private_address", address)
}
}
}
24 changes: 22 additions & 2 deletions app/src/test/java/tech/relaycorp/gateway/domain/LocalConfigTest.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tech.relaycorp.gateway.domain

import com.nhaarman.mockitokotlin2.any
import com.nhaarman.mockitokotlin2.eq
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import com.nhaarman.mockitokotlin2.whenever
Expand All @@ -12,14 +13,18 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import tech.relaycorp.gateway.data.disk.FileStore
import tech.relaycorp.gateway.data.preference.PublicGatewayPreferences
import tech.relaycorp.gateway.test.BaseDataTestCase
import tech.relaycorp.relaynet.testing.pki.PDACertPath
import kotlin.test.assertEquals

class LocalConfigTest : BaseDataTestCase() {

private val fileStore = mock<FileStore>()
private val localConfig =
LocalConfig(fileStore, privateKeyStoreProvider, certificateStoreProvider)
private val publicGatewayPreferences = mock<PublicGatewayPreferences>()
private val localConfig = LocalConfig(
fileStore, privateKeyStoreProvider, certificateStoreProvider, publicGatewayPreferences
)

@BeforeEach
fun setUp() {
Expand All @@ -35,6 +40,8 @@ class LocalConfigTest : BaseDataTestCase() {
val key = it.getArgument<String>(0)
memoryStore[key]
}
whenever(publicGatewayPreferences.getPrivateAddress())
.thenReturn(PDACertPath.PUBLIC_GW.subjectPrivateAddress)
}
}

Expand Down Expand Up @@ -104,6 +111,19 @@ class LocalConfigTest : BaseDataTestCase() {
assertEquals(originalKeyPair, keyPair)
}

@Test
fun `Correct public gateway private address used as issuer in set identity certificate `() =
runBlockingTest {
localConfig.bootstrap()

verify(certificateStore).setCertificate(
any(),
any(),
any(),
eq(PDACertPath.PUBLIC_GW.subjectPrivateAddress)
)
}

@Test
fun `CDA issuer should be created if it doesn't already exist`() = runBlockingTest {
localConfig.bootstrap()
Expand Down
Loading

0 comments on commit 2a213ee

Please sign in to comment.