diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepository.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepository.kt index 2bea9630018..ad56613a896 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepository.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepository.kt @@ -34,6 +34,7 @@ import com.wire.kalium.logic.di.MapperProvider import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.flatMap import com.wire.kalium.logic.functional.fold +import com.wire.kalium.logic.functional.foldToEitherWhileRight import com.wire.kalium.logic.functional.getOrFail import com.wire.kalium.logic.functional.left import com.wire.kalium.logic.functional.onSuccess @@ -344,24 +345,29 @@ class E2EIRepositoryImpl( override suspend fun fetchFederationCertificates() = discoveryUrl().flatMap { wrapApiRequest { - acmeApi.getACMEFederation(it) + acmeApi.getACMEFederationCertificateChain(it) }.fold({ E2EIFailure.IntermediateCert(it).left() }, { data -> - currentClientIdProvider().fold({ - E2EIFailure.TrustAnchors(it).left() - }, { clientId -> - mlsClientProvider.getCoreCrypto(clientId).fold({ - E2EIFailure.MissingMLSClient(it).left() - }, { coreCrypto -> - wrapE2EIRequest { - coreCrypto.registerIntermediateCa(data) - } - }) - }) + registerIntermediateCAs(data) }) } + private suspend fun registerIntermediateCAs(data: List) = + currentClientIdProvider().fold({ + E2EIFailure.TrustAnchors(it).left() + }, { clientId -> + mlsClientProvider.getCoreCrypto(clientId).fold({ + E2EIFailure.MissingMLSClient(it).left() + }, { coreCrypto -> + data.foldToEitherWhileRight(Unit) { item, _ -> + wrapE2EIRequest { + coreCrypto.registerIntermediateCa(item) + } + } + }) + }) + override fun discoveryUrl() = userConfigRepository.getE2EISettings().fold({ E2EIFailure.MissingTeamSettings.left() diff --git a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/EnrollE2EIUseCase.kt b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/EnrollE2EIUseCase.kt index 7213b2d3b96..56278f63874 100644 --- a/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/EnrollE2EIUseCase.kt +++ b/logic/src/commonMain/kotlin/com/wire/kalium/logic/feature/e2ei/usecase/EnrollE2EIUseCase.kt @@ -58,6 +58,10 @@ class EnrollE2EIUseCaseImpl internal constructor( e2EIRepository.initFreshE2EIClient(isNewClient = isNewClientRegistration) e2EIRepository.fetchAndSetTrustAnchors() + e2EIRepository.fetchFederationCertificates().getOrFail { + kaliumLogger.e("Failure fetching federation certificates during E2EI Enrolling!. Failure:$it") + return it.left() + } val acmeDirectories = e2EIRepository.loadACMEDirectories().getOrFail { return it.left() diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt index c3e219b1ed4..cab1b39ab8d 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/data/e2ei/E2EIRepositoryTest.kt @@ -55,7 +55,6 @@ import com.wire.kalium.network.api.base.unbound.acme.DtoAuthorizationChallengeTy import com.wire.kalium.network.exceptions.KaliumException import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.util.DateTimeUtil -import io.ktor.http.Url import io.mockative.Mock import io.mockative.any import io.mockative.anyInstanceOf @@ -881,7 +880,7 @@ class E2EIRepositoryTest { .wasInvoked(once) verify(arrangement.acmeApi) - .suspendFunction(arrangement.acmeApi::getACMEFederation) + .suspendFunction(arrangement.acmeApi::getACMEFederationCertificateChain) .with(any()) .wasNotInvoked() @@ -892,12 +891,12 @@ class E2EIRepositoryTest { } @Test - fun givenACMEFederationApiSucceed_whenFetchACMECertificates_thenItSucceed() = runTest { + fun givenACMEFederationApiSucceeds_whenFetchACMECertificates_thenAllCertificatesAreRegistered() = runTest { + val certificateList = listOf("a", "b", "potato") // Given - val (arrangement, e2eiRepository) = Arrangement() .withGettingE2EISettingsReturns(Either.Right(E2EI_TEAM_SETTINGS)) - .withAcmeFederationApiSucceed() + .withAcmeFederationApiSucceed(certificateList) .withCurrentClientIdProviderSuccessful() .withGetCoreCryptoSuccessful() .withRegisterIntermediateCABag() @@ -915,14 +914,16 @@ class E2EIRepositoryTest { .wasInvoked(once) verify(arrangement.acmeApi) - .suspendFunction(arrangement.acmeApi::getACMEFederation) + .suspendFunction(arrangement.acmeApi::getACMEFederationCertificateChain) .with(any()) .wasInvoked(once) - verify(arrangement.coreCryptoCentral) - .suspendFunction(arrangement.coreCryptoCentral::registerIntermediateCa) - .with(any()) - .wasInvoked(once) + certificateList.forEach { certificateValue -> + verify(arrangement.coreCryptoCentral) + .suspendFunction(arrangement.coreCryptoCentral::registerIntermediateCa) + .with(eq(certificateValue)) + .wasInvoked(once) + } } @Test @@ -949,7 +950,7 @@ class E2EIRepositoryTest { .wasInvoked(once) verify(arrangement.acmeApi) - .suspendFunction(arrangement.acmeApi::getACMEFederation) + .suspendFunction(arrangement.acmeApi::getACMEFederationCertificateChain) .with(any()) .wasNotInvoked() @@ -1280,16 +1281,16 @@ class E2EIRepositoryTest { .thenReturn(NetworkResponse.Error(INVALID_REQUEST_ERROR)) } - fun withAcmeFederationApiSucceed() = apply { + fun withAcmeFederationApiSucceed(certificateList: List) = apply { given(acmeApi) - .suspendFunction(acmeApi::getACMEFederation) + .suspendFunction(acmeApi::getACMEFederationCertificateChain) .whenInvokedWith(any()) - .thenReturn(NetworkResponse.Success("", mapOf(), 200)) + .thenReturn(NetworkResponse.Success(certificateList, mapOf(), 200)) } fun withAcmeFederationApiFails() = apply { given(acmeApi) - .suspendFunction(acmeApi::getACMEFederation) + .suspendFunction(acmeApi::getACMEFederationCertificateChain) .whenInvokedWith(any()) .thenReturn(NetworkResponse.Error(INVALID_REQUEST_ERROR)) } diff --git a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/EnrollE2EICertificateUseCaseTest.kt b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/EnrollE2EICertificateUseCaseTest.kt index 322fb0340ff..8eb27f74125 100644 --- a/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/EnrollE2EICertificateUseCaseTest.kt +++ b/logic/src/commonTest/kotlin/com/wire/kalium/logic/feature/e2ei/EnrollE2EICertificateUseCaseTest.kt @@ -63,6 +63,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(E2EIFailure.AcmeDirectories(TEST_CORE_FAILURE).left()) // when @@ -144,6 +145,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(Either.Right(ACME_DIRECTORIES)) arrangement.withGetACMENonceResulting(E2EIFailure.AcmeNonce(TEST_CORE_FAILURE).left()) @@ -224,6 +226,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(Either.Right(ACME_DIRECTORIES)) arrangement.withGetACMENonceResulting(Either.Right(RANDOM_NONCE)) arrangement.withCreateNewAccountResulting(E2EIFailure.AcmeNewAccount(TEST_CORE_FAILURE).left()) @@ -309,6 +312,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(Either.Right(ACME_DIRECTORIES)) arrangement.withGetACMENonceResulting(Either.Right(RANDOM_NONCE)) arrangement.withCreateNewAccountResulting(Either.Right(RANDOM_NONCE)) @@ -396,6 +400,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(Either.Right(ACME_DIRECTORIES)) arrangement.withGetACMENonceResulting(Either.Right(RANDOM_NONCE)) arrangement.withCreateNewAccountResulting(Either.Right(RANDOM_NONCE)) @@ -487,6 +492,7 @@ class EnrollE2EICertificateUseCaseTest { // given arrangement.withInitializingE2EIClientSucceed() arrangement.withLoadTrustAnchorsResulting(Either.Right(Unit)) + arrangement.withFetchFederationCertificateChainResulting(Either.Right(Unit)) arrangement.withLoadACMEDirectoriesResulting(Either.Right(ACME_DIRECTORIES)) arrangement.withGetACMENonceResulting(Either.Right(RANDOM_NONCE)) arrangement.withCreateNewAccountResulting(Either.Right(RANDOM_NONCE)) @@ -1111,6 +1117,13 @@ class EnrollE2EICertificateUseCaseTest { .thenReturn(result) } + fun withFetchFederationCertificateChainResulting(result: Either) = apply { + given(e2EIRepository) + .suspendFunction(e2EIRepository::fetchFederationCertificates) + .whenInvoked() + .thenReturn(result) + } + fun withLoadACMEDirectoriesResulting(result: Either) = apply { given(e2EIRepository) .suspendFunction(e2EIRepository::loadACMEDirectories) diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt index ef416961dea..6bddc72d47e 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEApi.kt @@ -25,6 +25,7 @@ import com.wire.kalium.network.utils.CustomErrors import com.wire.kalium.network.utils.NetworkResponse import com.wire.kalium.network.utils.flatMap import com.wire.kalium.network.utils.handleUnsuccessfulResponse +import com.wire.kalium.network.utils.mapSuccess import com.wire.kalium.network.utils.wrapKaliumResponse import io.ktor.client.call.body import io.ktor.client.request.accept @@ -50,7 +51,14 @@ interface ACMEApi { suspend fun sendACMERequest(url: String, body: ByteArray? = null): NetworkResponse suspend fun sendAuthorizationRequest(url: String, body: ByteArray? = null): NetworkResponse suspend fun sendChallengeRequest(url: String, body: ByteArray): NetworkResponse - suspend fun getACMEFederation(discoveryUrl: String): NetworkResponse + + /** + * Retrieves the ACME federation certificate chain from the specified discovery URL. + * + * @param discoveryUrl The non-blank URL of the ACME federation discovery endpoint. + * @return A [NetworkResponse] object containing the certificate chain as a list of strings. + */ + suspend fun getACMEFederationCertificateChain(discoveryUrl: String): NetworkResponse> suspend fun getClientDomainCRL(url: String): NetworkResponse } @@ -225,7 +233,7 @@ class ACMEApiImpl internal constructor( } } - override suspend fun getACMEFederation(discoveryUrl: String): NetworkResponse { + override suspend fun getACMEFederationCertificateChain(discoveryUrl: String): NetworkResponse> { val protocolWithAuthority = Url(discoveryUrl).protocolWithAuthority if (discoveryUrl.isBlank() || protocolWithAuthority.isBlank()) { return NetworkResponse.Error( @@ -239,9 +247,9 @@ class ACMEApiImpl internal constructor( ) } - return wrapKaliumResponse { + return wrapKaliumResponse { httpClient.get("$protocolWithAuthority/$PATH_ACME_FEDERATION") - } + }.mapSuccess { it.certificates } } override suspend fun getClientDomainCRL(url: String): NetworkResponse { diff --git a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEResponse.kt b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEResponse.kt index 4077c88cb44..ba257234c6a 100644 --- a/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEResponse.kt +++ b/network/src/commonMain/kotlin/com/wire/kalium/network/api/base/unbound/acme/ACMEResponse.kt @@ -124,5 +124,11 @@ enum class DtoAuthorizationChallengeType { OIDC } +@Serializable +data class FederationCertificateChainResponse( + @SerialName("crts") + val certificates: List +) + @JvmInline value class CertificateChain(val value: String) diff --git a/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt b/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt index 12d4d16cdd1..410e844f5d5 100644 --- a/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt +++ b/network/src/commonTest/kotlin/com/wire/kalium/api/common/ACMEApiTest.kt @@ -30,6 +30,27 @@ import kotlin.test.* internal class ACMEApiTest : ApiTest() { + @Test + fun givingASuccessfulResponse_whenGettingACMEFederationCertificateChain_thenAllCertificatesShouldBeParsed() = runTest { + val expected = listOf("a", "b", "potato") + + val networkClient = mockUnboundNetworkClient( + responseBody = """ + { + "crts": ["a", "b", "potato"] + } + """.trimIndent(), + statusCode = HttpStatusCode.OK + ) + + val acmeApi: ACMEApi = ACMEApiImpl(networkClient, networkClient) + + val result = acmeApi.getACMEFederationCertificateChain("someURL") + + assertTrue(result.isSuccessful()) + assertContentEquals(expected, result.value) + } + @Ignore @Test fun whenCallingGeTrustAnchorsApi_theResponseShouldBeConfigureCorrectly() = runTest { @@ -185,6 +206,7 @@ internal class ACMEApiTest : ApiTest() { assertEquals(expected, actual.value) } } + companion object { private const val ACME_DISCOVERY_URL = "https://balderdash.hogwash.work:9000/acme/google-android/directory" private const val ACME_DIRECTORIES_PATH = "https://balderdash.hogwash.work:9000/acme/google-android/directory"