Add the user certificates as additional certificates to the ClientBuilder

Now, this is a story all about how
Certificates work in Android town
And I'd like to take a minute
Enter, close the door
I'll tell you how I've figured out the inner workings of the Keystore

Well it all boils down the fact that Google got scared
It said, "You're certs are movin' to a place you won't find".

So the directory, user certificates are stored, is hard to find, and possibly
not readable by your application[1]. Instead, we need to use the Keystore[2]
API, specifically we'll need to open the `AndroidCAStore` Keystore type.

The various Keystore types are supposedly documented[3], but I'm failing to
find a logical path that would lead you to conclude that:

    a) System certificates can or should be accessed using the Keystore,
       specifically the AndroidCAStore type
    b) User certificates can be found in the same Keystore type as the system
       certificates

So this was mostly found using random googling, swearing, and a couple of
educated guesses.

[1]: https://android-developers.googleblog.com/2016/07/changes-to-trusted-certificate.html
[2]: https://developer.android.com/reference/java/security/KeyStore
[3]: https://docs.oracle.com/en/java/javase/17/docs/specs/security/standard-names.html#keystore-types
This commit is contained in:
Damir Jelić 2024-02-14 10:16:01 +01:00
parent 598bf96208
commit 73ba371a3e
2 changed files with 57 additions and 1 deletions

View file

@ -27,7 +27,9 @@ import kotlinx.coroutines.withContext
import org.matrix.rustcomponents.sdk.ClientBuilder
import org.matrix.rustcomponents.sdk.Session
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import java.io.File
import java.security.KeyStore
import javax.inject.Inject
class RustMatrixClientFactory @Inject constructor(
@ -46,6 +48,7 @@ class RustMatrixClientFactory @Inject constructor(
.username(sessionData.userId)
.passphrase(sessionData.passphrase)
.userAgent(userAgentProvider.provide())
.addRootCertificates(getAdditionalCertificates())
// 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() }
@ -68,6 +71,57 @@ class RustMatrixClientFactory @Inject constructor(
}
}
/**
* 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.
*/
fun getAdditionalCertificates(): List<ByteArray> {
val certs = mutableListOf<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 = KeyStore.getInstance("AndroidCAStore").apply {
load(null)
}
val aliases = keyStore.aliases()
while (aliases.hasMoreElements()) {
val alias = aliases.nextElement()
val entry = keyStore.getEntry(alias, null)
if (entry is KeyStore.TrustedCertificateEntry) {
// 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.
if (alias.startsWith("user")) {
certs.add(entry.trustedCertificate.encoded)
}
}
}
// 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.
val certCount = certs.count()
Timber.i("Found $certCount additional user-provided certificates.")
return certs
}
private fun SessionData.toSession() = Session(
accessToken = accessToken,
refreshToken = refreshToken,

View file

@ -30,6 +30,7 @@ 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.exception.mapClientException
import io.element.android.libraries.matrix.impl.getAdditionalCertificates
import io.element.android.libraries.matrix.impl.keys.PassphraseGenerator
import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.network.useragent.UserAgentProvider
@ -61,11 +62,12 @@ class RustMatrixAuthenticationService @Inject constructor(
// Passphrase which will be used for new sessions. Existing sessions will use the passphrase
// stored in the SessionData.
private val pendingPassphrase = getDatabasePassphrase()
private val additionalCertificates = getAdditionalCertificates()
private val authService: RustAuthenticationService = RustAuthenticationService(
basePath = baseDirectory.absolutePath,
passphrase = pendingPassphrase,
userAgent = userAgentProvider.provide(),
additionalRootCertificates = emptyList(),
additionalRootCertificates = additionalCertificates,
oidcConfiguration = oidcConfiguration,
customSlidingSyncProxy = null,
sessionDelegate = null,