Skip to content

Commit

Permalink
feat: Add scope to CertificateStore with issuerPrivateAddress (#228)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdsantos authored Mar 9, 2022
1 parent c9bf51a commit a1d6a09
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,42 +9,55 @@ import tech.relaycorp.relaynet.wrappers.x509.Certificate
abstract class CertificateStore {

@Throws(KeyStoreBackendException::class)
suspend fun save(certificate: Certificate, chain: List<Certificate> = emptyList()) {
suspend fun save(
certificate: Certificate,
chain: List<Certificate> = emptyList(),
issuerPrivateAddress: String
) {
if (certificate.expiryDate < ZonedDateTime.now()) return

saveData(
certificate.subjectPrivateAddress,
certificate.expiryDate,
CertificationPath(certificate, chain).toData()
CertificationPath(certificate, chain).toData(),
issuerPrivateAddress
)
}

protected abstract suspend fun saveData(
subjectPrivateAddress: String,
leafCertificateExpiryDate: ZonedDateTime,
certificationPathData: ByteArray,
issuerPrivateAddress: String,
)

@Throws(KeyStoreBackendException::class)
suspend fun retrieveLatest(subjectPrivateAddress: String): CertificationPath? =
retrieveAll(subjectPrivateAddress)
suspend fun retrieveLatest(
subjectPrivateAddress: String,
issuerPrivateAddress: String
): CertificationPath? =
retrieveAll(subjectPrivateAddress, issuerPrivateAddress)
.maxByOrNull { it.leafCertificate.expiryDate }

@Throws(KeyStoreBackendException::class)
suspend fun retrieveAll(subjectPrivateAddress: String): List<CertificationPath> =
retrieveData(subjectPrivateAddress)
suspend fun retrieveAll(
subjectPrivateAddress: String,
issuerPrivateAddress: String
): List<CertificationPath> =
retrieveData(subjectPrivateAddress, issuerPrivateAddress)
.map { it.toCertificationPath() }
.filter { it.leafCertificate.expiryDate >= ZonedDateTime.now() }

protected abstract suspend fun retrieveData(
subjectPrivateAddress: String
subjectPrivateAddress: String,
issuerPrivateAddress: String
): List<ByteArray>

@Throws(KeyStoreBackendException::class)
abstract suspend fun deleteExpired()

@Throws(KeyStoreBackendException::class)
abstract fun delete(subjectPrivateAddress: String)
abstract fun delete(subjectPrivateAddress: String, issuerPrivateAddress: String)

// Helpers

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class CertificateStoreTest {

private val certificate = PDACertPath.PRIVATE_GW
private val certificateChain = listOf(PDACertPath.PUBLIC_GW, PDACertPath.PUBLIC_GW)
private val issuerAddress = PDACertPath.PUBLIC_GW.subjectPrivateAddress

private val aboutToExpireCertificate = Certificate.issue(
"foo",
Expand All @@ -46,21 +47,27 @@ class CertificateStoreTest {
fun `Certificate should be stored`() = runBlockingTest {
val store = MockCertificateStore()

store.save(certificate)
store.save(certificate, issuerPrivateAddress = issuerAddress)

assertTrue(store.data.containsKey(certificate.subjectPrivateAddress))
val certificationPaths = store.data[certificate.subjectPrivateAddress]!!
assertTrue(
store.data.containsKey(certificate.subjectPrivateAddress to issuerAddress)
)
val certificationPaths =
store.data[certificate.subjectPrivateAddress to issuerAddress]!!
assertEquals(1, certificationPaths.size)
}

@Test
fun `Certification path should be stored`() = runBlockingTest {
val store = MockCertificateStore()

store.save(certificate, certificateChain)
store.save(certificate, certificateChain, issuerAddress)

assertTrue(store.data.containsKey(certificate.subjectPrivateAddress))
val certificationPaths = store.data[certificate.subjectPrivateAddress]!!
assertTrue(
store.data.containsKey(certificate.subjectPrivateAddress to issuerAddress)
)
val certificationPaths =
store.data[certificate.subjectPrivateAddress to issuerAddress]!!
assertEquals(1, certificationPaths.size)
}
}
Expand All @@ -70,29 +77,51 @@ class CertificateStoreTest {
@Test
fun `Existing certification path should be returned`() = runBlockingTest {
val store = MockCertificateStore()
store.save(certificate, certificateChain)
store.save(certificate, certificateChain, issuerAddress)

val certificationPath = store.retrieveLatest(certificate.subjectPrivateAddress)!!
val certificationPath =
store.retrieveLatest(
certificate.subjectPrivateAddress,
issuerAddress
)!!

assertEquals(certificate, certificationPath.leafCertificate)
assertEquals(certificateChain, certificationPath.chain)
}

@Test
fun `Existing certification path of another issuer should not be returned`() =
runBlockingTest {
val store = MockCertificateStore()
store.save(certificate, certificateChain, issuerAddress)

assertNull(
store.retrieveLatest(
certificate.subjectPrivateAddress,
"another-address"
)
)
}

@Test
fun `Null should be returned if there are none`() = runBlockingTest {
val store = MockCertificateStore()

assertNull(store.retrieveLatest("non-existent"))
assertNull(store.retrieveLatest("non-existent", issuerAddress))
}

@Test
fun `Last to expire certificate should be returned`() = runBlockingTest {
val store = MockCertificateStore()

store.save(certificate, certificateChain)
store.save(aboutToExpireCertificate, certificateChain)
store.save(certificate, certificateChain, issuerAddress)
store.save(aboutToExpireCertificate, certificateChain, issuerAddress)

val certificationPath = store.retrieveLatest(certificate.subjectPrivateAddress)!!
val certificationPath =
store.retrieveLatest(
certificate.subjectPrivateAddress,
issuerAddress
)!!

assertEquals(certificate, certificationPath.leafCertificate)
}
Expand All @@ -104,18 +133,20 @@ class CertificateStoreTest {
fun `No certification path should be returned if there are none`() = runBlockingTest {
val store = MockCertificateStore()

assertEquals(0, store.retrieveAll("non-existent").size)
val results = store.retrieveAll("non-existent", issuerAddress)
assertEquals(0, results.size)
}

@Test
fun `All stored non-expired certification paths should be returned`() = runBlockingTest {
val store = MockCertificateStore()

store.save(certificate, certificateChain)
store.save(aboutToExpireCertificate, certificateChain)
store.save(expiredCertificate, certificateChain)
store.save(certificate, certificateChain, issuerAddress)
store.save(aboutToExpireCertificate, certificateChain, issuerAddress)
store.save(expiredCertificate, certificateChain, issuerAddress)

val allCertificationPaths = store.retrieveAll(certificate.subjectPrivateAddress)
val allCertificationPaths =
store.retrieveAll(certificate.subjectPrivateAddress, issuerAddress)

assertEquals(2, allCertificationPaths.size)
assertContains(
Expand All @@ -128,32 +159,48 @@ class CertificateStoreTest {
)
}

@Test
fun `Stored non-expired certification paths from another issuer should not be returned`() =
runBlockingTest {
val store = MockCertificateStore()

store.save(certificate, certificateChain, issuerAddress)
store.save(PDACertPath.PRIVATE_ENDPOINT, certificateChain, "another-issuer")

val allCertificationPaths =
store.retrieveAll(certificate.subjectPrivateAddress, issuerAddress)

assertEquals(1, allCertificationPaths.size)
assertEquals(certificate, allCertificationPaths.first().leafCertificate)
}

@Test
fun `Malformed certification path should throw KeyStoreBackendException`() =
runBlockingTest {
val store = MockCertificateStore()
store.data[certificate.subjectPrivateAddress] = listOf(
Pair(ZonedDateTime.now().plusDays(1), "malformed".toByteArray())
)
store.data[certificate.subjectPrivateAddress to issuerAddress] =
listOf(
Pair(ZonedDateTime.now().plusDays(1), "malformed".toByteArray())
)

val exception = assertThrows<KeyStoreBackendException> {
store.retrieveAll(certificate.subjectPrivateAddress)
store.retrieveAll(certificate.subjectPrivateAddress, issuerAddress)
}
assertEquals("Malformed certification path", exception.message)
}

@Test
fun `Empty certification path should throw KeyStoreBackendException`() = runBlockingTest {
val store = MockCertificateStore()
store.data[certificate.subjectPrivateAddress] = listOf(
store.data[certificate.subjectPrivateAddress to issuerAddress] = listOf(
Pair(
ZonedDateTime.now().plusDays(1),
ASN1Utils.serializeSequence(emptyList())
)
)

val exception = assertThrows<KeyStoreBackendException> {
store.retrieveAll(certificate.subjectPrivateAddress)
store.retrieveAll(certificate.subjectPrivateAddress, issuerAddress)
}
assertEquals("Empty certification path", exception.message)
}
Expand All @@ -164,7 +211,8 @@ class CertificateStoreTest {
@Test
fun `All expired certification paths are deleted`() = runBlockingTest {
val store = MockCertificateStore()
store.save(expiredCertificate, certificateChain)
store.save(expiredCertificate, certificateChain, issuerAddress)
store.save(expiredCertificate, certificateChain, "another-issuer")

store.deleteExpired()

Expand All @@ -177,14 +225,32 @@ class CertificateStoreTest {
@Test
fun `All certification paths of a certain address are deleted`() = runBlockingTest {
val store = MockCertificateStore()
store.save(certificate, certificateChain)
store.save(aboutToExpireCertificate, certificateChain)
store.save(unrelatedCertificate, certificateChain)
store.save(certificate, certificateChain, issuerAddress)
store.save(aboutToExpireCertificate, certificateChain, issuerAddress)
store.save(unrelatedCertificate, certificateChain, issuerAddress)

store.delete(certificate.subjectPrivateAddress)
store.delete(certificate.subjectPrivateAddress, issuerAddress)

assertNull(store.data[certificate.subjectPrivateAddress])
assertTrue(store.data[unrelatedCertificate.subjectPrivateAddress]!!.isNotEmpty())
assertNull(store.data[certificate.subjectPrivateAddress to issuerAddress])
assertTrue(
store.data[unrelatedCertificate.subjectPrivateAddress to issuerAddress]!!
.isNotEmpty()
)
}

@Test
fun `Only certification paths of a certain address and issuer are deleted`() =
runBlockingTest {
val store = MockCertificateStore()
store.save(certificate, certificateChain, issuerAddress)
store.save(certificate, certificateChain, "another-issuer")

store.delete(certificate.subjectPrivateAddress, issuerAddress)

assertNull(store.data[certificate.subjectPrivateAddress to issuerAddress])
assertTrue(
store.data[certificate.subjectPrivateAddress to "another-issuer"]!!.isNotEmpty()
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,34 @@ class MockCertificateStore(
private val retrievalException: Throwable? = null,
) : CertificateStore() {

val data: MutableMap<String, List<Pair<ZonedDateTime, ByteArray>>> = mutableMapOf()
val data: MutableMap<Pair<String, String>, List<Pair<ZonedDateTime, ByteArray>>> =
mutableMapOf()

override suspend fun saveData(
subjectPrivateAddress: String,
leafCertificateExpiryDate: ZonedDateTime,
certificationPathData: ByteArray
certificationPathData: ByteArray,
issuerPrivateAddress: String
) {
if (savingException != null) {
throw KeyStoreBackendException("Saving certificates isn't supported", savingException)
}
data[subjectPrivateAddress] =
data[subjectPrivateAddress].orEmpty() +
data[subjectPrivateAddress to issuerPrivateAddress] =
data[subjectPrivateAddress to issuerPrivateAddress].orEmpty() +
listOf(Pair(leafCertificateExpiryDate, certificationPathData))
}

override suspend fun retrieveData(subjectPrivateAddress: String): List<ByteArray> {
override suspend fun retrieveData(
subjectPrivateAddress: String,
issuerPrivateAddress: String
): List<ByteArray> {
if (retrievalException != null) {
throw KeyStoreBackendException(
"Retrieving certificates isn't supported",
retrievalException
)
}
return data[subjectPrivateAddress].orEmpty().map { it.second }
return data[subjectPrivateAddress to issuerPrivateAddress].orEmpty().map { it.second }
}

override suspend fun deleteExpired() {
Expand All @@ -40,7 +45,7 @@ class MockCertificateStore(
}
}

override fun delete(subjectPrivateAddress: String) {
data.remove(subjectPrivateAddress)
override fun delete(subjectPrivateAddress: String, issuerPrivateAddress: String) {
data.remove(subjectPrivateAddress to issuerPrivateAddress)
}
}

0 comments on commit a1d6a09

Please sign in to comment.