Restore custom user certificate provider (#6451)

It was removed because we temporarily lost this functionality in the Rust SDK
This commit is contained in:
Jorge Martin Espinosa 2026-03-24 11:58:08 +01:00 committed by GitHub
parent e8a2f97cf5
commit a512f7ff21
7 changed files with 114 additions and 1 deletions

View file

@ -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.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.impl.analytics.UtdTracker 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.SessionPaths
import io.element.android.libraries.matrix.impl.paths.getSessionPaths import io.element.android.libraries.matrix.impl.paths.getSessionPaths
import io.element.android.libraries.matrix.impl.proxy.ProxyProvider import io.element.android.libraries.matrix.impl.proxy.ProxyProvider
@ -57,6 +58,7 @@ class RustMatrixClientFactory(
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val userAgentProvider: UserAgentProvider, private val userAgentProvider: UserAgentProvider,
private val proxyProvider: ProxyProvider, private val proxyProvider: ProxyProvider,
private val userCertificatesProvider: UserCertificatesProvider,
private val clock: SystemClock, private val clock: SystemClock,
private val analyticsService: AnalyticsService, private val analyticsService: AnalyticsService,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
@ -141,6 +143,7 @@ class RustMatrixClientFactory(
} }
.setSessionDelegate(sessionDelegate) .setSessionDelegate(sessionDelegate)
.userAgent(userAgentProvider.provide()) .userAgent(userAgentProvider.provide())
.addRootCertificates(userCertificatesProvider.provides())
.autoEnableBackups(true) .autoEnableBackups(true)
.autoEnableCrossSigning(true) .autoEnableCrossSigning(true)
.roomKeyRecipientStrategy( .roomKeyRecipientStrategy(

View file

@ -13,16 +13,19 @@ import dev.zacsweers.metro.ContributesBinding
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker
import io.element.android.libraries.matrix.impl.ClientBuilderProvider import io.element.android.libraries.matrix.impl.ClientBuilderProvider
import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider
import timber.log.Timber import timber.log.Timber
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
class RustHomeServerLoginCompatibilityChecker( class RustHomeServerLoginCompatibilityChecker(
private val clientBuilderProvider: ClientBuilderProvider, private val clientBuilderProvider: ClientBuilderProvider,
) : HomeServerLoginCompatibilityChecker { private val userCertificatesProvider: UserCertificatesProvider,
) : HomeServerLoginCompatibilityChecker {
override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions { override suspend fun check(url: String): Result<Boolean> = runCatchingExceptions {
clientBuilderProvider.provide() clientBuilderProvider.provide()
.inMemoryStore() .inMemoryStore()
.serverNameOrHomeserverUrl(url) .serverNameOrHomeserverUrl(url)
.addRootCertificates(userCertificatesProvider.provides())
.build() .build()
.use { .use {
it.homeserverLoginDetails() it.homeserverLoginDetails()

View file

@ -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<ByteArray> {
// 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<KeyStore.TrustedCertificateEntry>()
.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.")
}
}
}

View file

@ -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<ByteArray>
}

View file

@ -12,6 +12,7 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.core.SessionId 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.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.room.FakeTimelineEventFilterFactory
import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider import io.element.android.libraries.matrix.impl.storage.FakeSqliteStoreBuilderProvider
import io.element.android.libraries.network.useragent.SimpleUserAgentProvider import io.element.android.libraries.network.useragent.SimpleUserAgentProvider
@ -57,6 +58,7 @@ fun TestScope.createRustMatrixClientFactory(
coroutineDispatchers = testCoroutineDispatchers(), coroutineDispatchers = testCoroutineDispatchers(),
sessionStore = sessionStore, sessionStore = sessionStore,
userAgentProvider = SimpleUserAgentProvider(), userAgentProvider = SimpleUserAgentProvider(),
userCertificatesProvider = FakeUserCertificatesProvider(),
proxyProvider = FakeProxyProvider(), proxyProvider = FakeProxyProvider(),
clock = FakeSystemClock(), clock = FakeSystemClock(),
analyticsService = FakeAnalyticsService(), analyticsService = FakeAnalyticsService(),

View file

@ -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<ByteArray> {
return emptyList()
}
}

View file

@ -49,5 +49,6 @@ class RustHomeserverLoginCompatibilityCheckerTest {
FakeFfiClient(homeserverLoginDetailsResult = result) FakeFfiClient(homeserverLoginDetailsResult = result)
} }
}, },
userCertificatesProvider = FakeUserCertificatesProvider(),
) )
} }