diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt index 034044e601..f83efd2736 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactory.kt @@ -17,6 +17,7 @@ import io.element.android.libraries.di.annotations.AppCoroutineScope import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.matrix.impl.analytics.UtdTracker +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.matrix.impl.paths.SessionPaths import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.proxy.ProxyProvider @@ -57,6 +58,7 @@ class RustMatrixClientFactory( private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, private val proxyProvider: ProxyProvider, + private val userCertificatesProvider: UserCertificatesProvider, private val clock: SystemClock, private val analyticsService: AnalyticsService, private val featureFlagService: FeatureFlagService, @@ -141,6 +143,7 @@ class RustMatrixClientFactory( } .setSessionDelegate(sessionDelegate) .userAgent(userAgentProvider.provide()) + .addRootCertificates(userCertificatesProvider.provides()) .autoEnableBackups(true) .autoEnableCrossSigning(true) .roomKeyRecipientStrategy( diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt index d748698b1b..0603fddec4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeServerLoginCompatibilityChecker.kt @@ -13,16 +13,19 @@ import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.impl.ClientBuilderProvider +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import timber.log.Timber @ContributesBinding(AppScope::class) class RustHomeServerLoginCompatibilityChecker( private val clientBuilderProvider: ClientBuilderProvider, -) : HomeServerLoginCompatibilityChecker { + private val userCertificatesProvider: UserCertificatesProvider, + ) : HomeServerLoginCompatibilityChecker { override suspend fun check(url: String): Result = runCatchingExceptions { clientBuilderProvider.provide() .inMemoryStore() .serverNameOrHomeserverUrl(url) + .addRootCertificates(userCertificatesProvider.provides()) .build() .use { it.homeserverLoginDetails() diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt new file mode 100644 index 0000000000..39be03ce5b --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.certificates + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import timber.log.Timber +import java.security.KeyStore +import java.security.KeyStoreException + +@ContributesBinding(AppScope::class) +class DefaultUserCertificatesProvider : UserCertificatesProvider { + /** + * Get additional user-installed certificates from the `AndroidCAStore` `Keystore`. + * + * The Rust HTTP client doesn't include user-installed certificates in its internal certificate + * store. This means that whatever the user installs will be ignored. + * + * While most users don't need user-installed certificates some special deployments or debugging + * setups using a proxy might want to use them. + * + * @return A list of byte arrays where each byte array is a single user-installed certificate + * in encoded form. + */ + override fun provides(): List { + // At least for API 34 the `AndroidCAStore` `Keystore` type contained user certificates as well. + // I have not found this to be documented anywhere. + val keyStore: KeyStore = try { + KeyStore.getInstance("AndroidCAStore") + } catch (e: KeyStoreException) { + Timber.w(e, "Failed to get AndroidCAStore keystore") + return emptyList() + } + val aliases = try { + keyStore.load(null) + keyStore.aliases() + } catch (e: Exception) { + Timber.w(e, "Failed to load and get aliases AndroidCAStore keystore") + return emptyList() + } + return aliases.toList() + .filter { alias -> + // The certificate alias always contains the prefix `system` or + // `user` and the MD5 subject hash separated by a colon. + // + // The subject hash can be calculated using openssl as such: + // openssl x509 -subject_hash_old -noout -in mycert.cer + // + // Again, I have not found this to be documented somewhere. + alias.startsWith("user") + } + .mapNotNull { alias -> + try { + keyStore.getEntry(alias, null) + } catch (e: Exception) { + Timber.w(e, "Failed to get entry for alias $alias") + null + } + } + .filterIsInstance() + .map { trustedCertificateEntry -> + trustedCertificateEntry.trustedCertificate.encoded + } + .also { + // Let's at least log the number of user-installed certificates we found, + // since the alias isn't particularly useful nor does the issuer seem to + // be easily available. + Timber.i("Found ${it.size} additional user-provided certificates.") + } + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt new file mode 100644 index 0000000000..529dd4eb4e --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.certificates + +interface UserCertificatesProvider { + fun provides(): List +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt index 1c25acf088..670430e23e 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClientFactoryTest.kt @@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.auth.FakeProxyProvider +import io.element.android.libraries.matrix.impl.auth.FakeUserCertificatesProvider import io.element.android.libraries.matrix.impl.room.FakeTimelineEventFilterFactory import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider import io.element.android.libraries.network.useragent.SimpleUserAgentProvider @@ -57,6 +58,7 @@ fun TestScope.createRustMatrixClientFactory( coroutineDispatchers = testCoroutineDispatchers(), sessionStore = sessionStore, userAgentProvider = SimpleUserAgentProvider(), + userCertificatesProvider = FakeUserCertificatesProvider(), proxyProvider = FakeProxyProvider(), clock = FakeSystemClock(), analyticsService = FakeAnalyticsService(), diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.kt new file mode 100644 index 0000000000..955af4a23d --- /dev/null +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/FakeUserCertificatesProvider.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.matrix.impl.auth + +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider + +class FakeUserCertificatesProvider : UserCertificatesProvider { + override fun provides(): List { + return emptyList() + } +} diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt index f4a4a865a5..50d1f3723b 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/auth/RustHomeserverLoginCompatibilityCheckerTest.kt @@ -49,5 +49,6 @@ class RustHomeserverLoginCompatibilityCheckerTest { FakeFfiClient(homeserverLoginDetailsResult = result) } }, + userCertificatesProvider = FakeUserCertificatesProvider(), ) }