Skip to content

Commit

Permalink
feat: create worker to periodically CRL (WPB-3243) (#2397)
Browse files Browse the repository at this point in the history
* feat(MLS): check revocation list

* feat(MLS): cover CheckRevocationListUseCase with unit test

* chore: detekt

* chore: apply new changes from CC

* feat: store urls with expiration time

* feat: pass url as param to the use case

* chore: detekt

* chore: unit test

* chore: cleanup

* feat: observe current client certificate

* feat: unit test

* chore: remove ObserveCertificateForCurrentClientUseCase

* feat: create worker to periodically check CRL

* chore: detekt

* chore: cleanup

* chore: cleanup

* chore: cover CrlRepository with unit test

* chore: cover CheckCrlWorker with unit test

* chore: address comments
  • Loading branch information
ohassine authored Jan 25, 2024
1 parent a4a5d14 commit 3f9c328
Show file tree
Hide file tree
Showing 8 changed files with 467 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.data.e2ei

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.functional.Either
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.wrapApiRequest
import com.wire.kalium.network.api.base.unbound.acme.ACMEApi
import com.wire.kalium.persistence.config.CRLUrlExpirationList
import com.wire.kalium.persistence.config.CRLWithExpiration
import com.wire.kalium.persistence.dao.MetadataDAO
import io.ktor.http.Url
import io.ktor.http.protocolWithAuthority

interface CertificateRevocationListRepository {

/**
* Returns CRLs with expiration time.
*
* @return the [CRLUrlExpirationList] representing a list of CRLs with expiration time.
*/
suspend fun getCRLs(): CRLUrlExpirationList?
suspend fun addOrUpdateCRL(url: String, timestamp: ULong)
suspend fun getCurrentClientCrlUrl(): Either<CoreFailure, String>
suspend fun getClientDomainCRL(url: String): Either<CoreFailure, ByteArray>
}

internal class CertificateRevocationListRepositoryDataSource(
private val acmeApi: ACMEApi,
private val metadataDAO: MetadataDAO,
private val userConfigRepository: UserConfigRepository
) : CertificateRevocationListRepository {
override suspend fun getCRLs(): CRLUrlExpirationList? =
metadataDAO.getSerializable(CRL_LIST_KEY, CRLUrlExpirationList.serializer())

override suspend fun addOrUpdateCRL(url: String, timestamp: ULong) {

metadataDAO.getSerializable(CRL_LIST_KEY, CRLUrlExpirationList.serializer())
?.let { crlExpirationList ->
val crlWithExpiration = crlExpirationList.cRLWithExpirationList.find {
it.url == url
}
val newCRLs = crlWithExpiration?.let { item ->
crlExpirationList.cRLWithExpirationList.map { current ->
if (current.url == url) {
return@map item.copy(expiration = timestamp)
} else {
return@map current
}
}
} ?: run {
// add new CRL
crlExpirationList.cRLWithExpirationList.plus(
CRLWithExpiration(url, timestamp)
)
}

metadataDAO.putSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList(newCRLs),
CRLUrlExpirationList.serializer()
)
}
}

override suspend fun getCurrentClientCrlUrl(): Either<CoreFailure, String> =
userConfigRepository.getE2EISettings().map {
(Url(it.discoverUrl).protocolWithAuthority)
}

override suspend fun getClientDomainCRL(url: String): Either<CoreFailure, ByteArray> =
wrapApiRequest {
acmeApi.getClientDomainCRL(url)
}

companion object {
const val CRL_LIST_KEY = "crl_list_key"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@ import com.wire.kalium.logic.data.conversation.ProposalTimer
import com.wire.kalium.logic.data.conversation.SubconversationRepositoryImpl
import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProvider
import com.wire.kalium.logic.data.conversation.UpdateKeyingMaterialThresholdProviderImpl
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepositoryDataSource
import com.wire.kalium.logic.data.e2ei.E2EIRepository
import com.wire.kalium.logic.data.e2ei.E2EIRepositoryImpl
import com.wire.kalium.logic.data.event.EventDataSource
Expand Down Expand Up @@ -205,6 +207,8 @@ import com.wire.kalium.logic.feature.conversation.mls.OneOnOneResolverImpl
import com.wire.kalium.logic.feature.debug.DebugScope
import com.wire.kalium.logic.feature.e2ei.ACMECertificatesSyncWorker
import com.wire.kalium.logic.feature.e2ei.ACMECertificatesSyncWorkerImpl
import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorker
import com.wire.kalium.logic.feature.e2ei.CertificateRevocationListCheckWorkerImpl
import com.wire.kalium.logic.feature.e2ei.usecase.CheckRevocationListUseCase
import com.wire.kalium.logic.feature.e2ei.usecase.CheckRevocationListUseCaseImpl
import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase
Expand Down Expand Up @@ -1509,6 +1513,12 @@ class UserSessionScope internal constructor(
userStorage.database.clientDAO,
userStorage.database.metadataDAO,
)
private val certificateRevocationListRepository: CertificateRevocationListRepository
get() = CertificateRevocationListRepositoryDataSource(
acmeApi = globalScope.unboundNetworkContainer.acmeApi,
metadataDAO = userStorage.database.metadataDAO,
userConfigRepository = userConfigRepository
)

private val proteusPreKeyRefiller: ProteusPreKeyRefiller
get() = ProteusPreKeyRefillerImpl(preKeyRepository)
Expand All @@ -1521,6 +1531,14 @@ class UserSessionScope internal constructor(
)
}

private val certificateRevocationListCheckWorker: CertificateRevocationListCheckWorker by lazy {
CertificateRevocationListCheckWorkerImpl(
certificateRevocationListRepository = certificateRevocationListRepository,
incrementalSyncRepository = incrementalSyncRepository,
checkRevocationList = checkRevocationList,
)
}

private val featureFlagsSyncWorker: FeatureFlagsSyncWorker by lazy {
FeatureFlagSyncWorkerImpl(
incrementalSyncRepository = incrementalSyncRepository,
Expand Down Expand Up @@ -1882,7 +1900,7 @@ class UserSessionScope internal constructor(

private val checkRevocationList: CheckRevocationListUseCase
get() = CheckRevocationListUseCaseImpl(
e2EIRepository = e2eiRepository,
certificateRevocationListRepository = certificateRevocationListRepository,
currentClientIdProvider = clientIdProvider,
mlsClientProvider = mlsClientProvider,
mLSConversationsVerificationStatusesHandler = mlsConversationsVerificationStatusesHandler
Expand Down Expand Up @@ -1920,6 +1938,10 @@ class UserSessionScope internal constructor(
proteusSyncWorker.execute()
}

launch {
certificateRevocationListCheckWorker.execute()
}

launch {
avsSyncStateReporter.execute()
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.feature.e2ei

import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository
import com.wire.kalium.logic.data.sync.IncrementalSyncRepository
import com.wire.kalium.logic.data.sync.IncrementalSyncStatus
import com.wire.kalium.logic.feature.e2ei.usecase.CheckRevocationListUseCase
import com.wire.kalium.logic.functional.map
import com.wire.kalium.logic.kaliumLogger
import kotlinx.coroutines.flow.filter
import kotlinx.datetime.Clock

/**
* This worker will wait until the sync is done and then check the CRLs if needed.
*
*/
internal interface CertificateRevocationListCheckWorker {
suspend fun execute()
}

/**
* Base implementation of [CertificateRevocationListCheckWorker].
* @param certificateRevocationListRepository The CRL repository.
* @param incrementalSyncRepository The incremental sync repository.
* @param checkRevocationList The check revocation list use case.
*
*/
internal class CertificateRevocationListCheckWorkerImpl(
private val certificateRevocationListRepository: CertificateRevocationListRepository,
private val incrementalSyncRepository: IncrementalSyncRepository,
private val checkRevocationList: CheckRevocationListUseCase
) : CertificateRevocationListCheckWorker {

override suspend fun execute() {
incrementalSyncRepository.incrementalSyncState
.filter { it is IncrementalSyncStatus.Live }
.collect {
kaliumLogger.i("Checking certificate revocation list (CRL)..")
certificateRevocationListRepository.getCRLs()?.cRLWithExpirationList?.forEach { crl ->
if (crl.expiration < Clock.System.now().epochSeconds.toULong()) {
checkRevocationList(crl.url).map { newExpirationTime ->
newExpirationTime?.let {
certificateRevocationListRepository.addOrUpdateCRL(crl.url, it)
}
}
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ package com.wire.kalium.logic.feature.e2ei.usecase

import com.wire.kalium.logic.CoreFailure
import com.wire.kalium.logic.data.client.MLSClientProvider
import com.wire.kalium.logic.data.e2ei.E2EIRepository
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepository
import com.wire.kalium.logic.data.id.CurrentClientIdProvider
import com.wire.kalium.logic.feature.conversation.MLSConversationsVerificationStatusesHandler
import com.wire.kalium.logic.functional.Either
Expand All @@ -34,13 +34,13 @@ interface CheckRevocationListUseCase {
}

internal class CheckRevocationListUseCaseImpl(
private val e2EIRepository: E2EIRepository,
private val certificateRevocationListRepository: CertificateRevocationListRepository,
private val currentClientIdProvider: CurrentClientIdProvider,
private val mlsClientProvider: MLSClientProvider,
private val mLSConversationsVerificationStatusesHandler: MLSConversationsVerificationStatusesHandler
) : CheckRevocationListUseCase {
override suspend fun invoke(url: String): Either<CoreFailure, ULong?> {
return e2EIRepository.getClientDomainCRL(url).flatMap {
return certificateRevocationListRepository.getClientDomainCRL(url).flatMap {
currentClientIdProvider().flatMap { clientId ->
mlsClientProvider.getMLSClient(clientId).map { mlsClient ->
mlsClient.registerCrl(url, it).run {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Wire
* Copyright (C) 2024 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.kalium.logic.data.e2ei

import com.wire.kalium.logic.configuration.UserConfigRepository
import com.wire.kalium.logic.data.e2ei.CertificateRevocationListRepositoryDataSource.Companion.CRL_LIST_KEY
import com.wire.kalium.network.api.base.unbound.acme.ACMEApi
import com.wire.kalium.persistence.config.CRLWithExpiration
import com.wire.kalium.persistence.config.CRLUrlExpirationList
import com.wire.kalium.persistence.dao.MetadataDAO
import io.mockative.Mock
import io.mockative.classOf
import io.mockative.given
import io.mockative.mock
import io.mockative.once
import io.mockative.verify
import kotlinx.coroutines.test.runTest
import kotlin.test.Test

class CertificateRevocationListRepositoryTest {

@Test
fun givenAnEmptyStoredList_whenUpdatingCRLs_thenAddNewCRL() = runTest {
val (arrangement, crlRepository) = Arrangement()
.withEmptyList()
.arrange()

crlRepository.addOrUpdateCRL(DUMMY_URL, TIMESTAMP)

verify(arrangement.metadataDAO).coroutine {
putSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList(listOf(CRLWithExpiration(DUMMY_URL, TIMESTAMP))),
CRLUrlExpirationList.serializer()
)
}.wasInvoked(once)
}

@Test
fun givenPassedCRLExistsInStoredList_whenUpdatingCRLs_thenUpdateCurrentCRL() = runTest {
val (arrangement, crlRepository) = Arrangement()
.withCRLs()
.arrange()

crlRepository.addOrUpdateCRL(DUMMY_URL, TIMESTAMP2)

verify(arrangement.metadataDAO).coroutine {
putSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList(listOf(CRLWithExpiration(DUMMY_URL, TIMESTAMP2))),
CRLUrlExpirationList.serializer()
)
}.wasInvoked(once)
}

@Test
fun givenNewCRLUrl_whenUpdatingCRLs_thenAddNewCRL() = runTest {
val (arrangement, crlRepository) = Arrangement()
.withCRLs()
.arrange()

crlRepository.addOrUpdateCRL(DUMMY_URL2, TIMESTAMP)

verify(arrangement.metadataDAO).coroutine {
putSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList(
listOf(
CRLWithExpiration(DUMMY_URL, TIMESTAMP),
CRLWithExpiration(DUMMY_URL2, TIMESTAMP)
)
),
CRLUrlExpirationList.serializer()
)
}.wasInvoked(once)
}

private class Arrangement {

@Mock
val acmeApi = mock(classOf<ACMEApi>())

@Mock
val metadataDAO = mock(classOf<MetadataDAO>())

@Mock
val userConfigRepository = mock(classOf<UserConfigRepository>())

fun arrange() = this to CertificateRevocationListRepositoryDataSource(acmeApi, metadataDAO, userConfigRepository)

suspend fun withEmptyList() = apply {
given(metadataDAO).coroutine {
metadataDAO.getSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList.serializer()
)
}.thenReturn(CRLUrlExpirationList(listOf()))
}

suspend fun withCRLs() = apply {
given(metadataDAO).coroutine {
metadataDAO.getSerializable(
CRL_LIST_KEY,
CRLUrlExpirationList.serializer()
)
}.thenReturn(CRLUrlExpirationList(listOf(CRLWithExpiration(DUMMY_URL, TIMESTAMP))))
}
}

companion object {
private const val DUMMY_URL = "https://dummy.url"
private const val DUMMY_URL2 = "https://dummy-2.url"
private val TIMESTAMP = 1234567890.toULong()
private val TIMESTAMP2 = 5453222.toULong()
}
}
Loading

0 comments on commit 3f9c328

Please sign in to comment.