diff --git a/changelog.d/2992.feature b/changelog.d/2992.feature new file mode 100644 index 0000000000..622f6fdc69 --- /dev/null +++ b/changelog.d/2992.feature @@ -0,0 +1 @@ +Allow user-installed certificates to be used by the HTTP client 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 0f1c47445b..96670d2a2d 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 @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.di.CacheDirectory +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.network.useragent.UserAgentProvider import io.element.android.libraries.sessionstorage.api.SessionData import io.element.android.libraries.sessionstorage.api.SessionStore @@ -37,6 +38,7 @@ class RustMatrixClientFactory @Inject constructor( private val coroutineDispatchers: CoroutineDispatchers, private val sessionStore: SessionStore, private val userAgentProvider: UserAgentProvider, + private val userCertificatesProvider: UserCertificatesProvider, private val clock: SystemClock, ) { suspend fun create(sessionData: SessionData): RustMatrixClient = withContext(coroutineDispatchers.io) { @@ -46,6 +48,7 @@ class RustMatrixClientFactory @Inject constructor( .username(sessionData.userId) .passphrase(sessionData.passphrase) .userAgent(userAgentProvider.provide()) + .addRootCertificates(userCertificatesProvider.provides()) // FIXME Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376 .serverVersions(listOf("v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5")) .use { it.build() } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt index c27ace88f6..afedc889da 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/auth/RustMatrixAuthenticationService.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.impl.RustMatrixClientFactory +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider import io.element.android.libraries.matrix.impl.exception.mapClientException import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator import io.element.android.libraries.matrix.impl.mapper.toSessionData @@ -56,6 +57,7 @@ class RustMatrixAuthenticationService @Inject constructor( userAgentProvider: UserAgentProvider, private val rustMatrixClientFactory: RustMatrixClientFactory, private val passphraseGenerator: PassphraseGenerator, + userCertificatesProvider: UserCertificatesProvider, private val buildMeta: BuildMeta, ) : MatrixAuthenticationService { // Passphrase which will be used for new sessions. Existing sessions will use the passphrase @@ -65,7 +67,7 @@ class RustMatrixAuthenticationService @Inject constructor( basePath = baseDirectory.absolutePath, passphrase = pendingPassphrase, userAgent = userAgentProvider.provide(), - additionalRootCertificates = emptyList(), + additionalRootCertificates = userCertificatesProvider.provides(), oidcConfiguration = oidcConfiguration, customSlidingSyncProxy = null, sessionDelegate = null, 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..5919952a9a --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/DefaultUserCertificatesProvider.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.certificates + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import timber.log.Timber +import java.security.KeyStore +import java.security.KeyStoreException +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultUserCertificatesProvider @Inject constructor() : 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..330e29ee47 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/certificates/UserCertificatesProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.impl.certificates + +interface UserCertificatesProvider { + fun provides(): List +} diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt index 30f5d2afe9..e87e0057d0 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/MainActivity.kt @@ -42,6 +42,7 @@ class MainActivity : ComponentActivity() { val baseDirectory = File(applicationContext.filesDir, "sessions") val userAgentProvider = SimpleUserAgentProvider("MinimalSample") val sessionStore = InMemorySessionStore() + val userCertificatesProvider = NoOpUserCertificatesProvider() RustMatrixAuthenticationService( baseDirectory = baseDirectory, coroutineDispatchers = Singleton.coroutineDispatchers, @@ -54,10 +55,12 @@ class MainActivity : ComponentActivity() { coroutineDispatchers = Singleton.coroutineDispatchers, sessionStore = sessionStore, userAgentProvider = userAgentProvider, + userCertificatesProvider = userCertificatesProvider, clock = DefaultSystemClock(), ), passphraseGenerator = NullPassphraseGenerator(), buildMeta = Singleton.buildMeta, + userCertificatesProvider = userCertificatesProvider, ) } diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt new file mode 100644 index 0000000000..a34fb4dbe0 --- /dev/null +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/NoOpUserCertificatesProvider.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.samples.minimal + +import io.element.android.libraries.matrix.impl.certificates.UserCertificatesProvider + +class NoOpUserCertificatesProvider : UserCertificatesProvider { + override fun provides(): List = emptyList() +}