diff --git a/build.gradle.kts b/build.gradle.kts index f699378d54..92847f39b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -52,6 +52,9 @@ allprojects { tasks.withType().configureEach { exclude("io/element/android/tests/konsist/failures/**") + + // This file comes from another project and we want to keep it as close to the original as possible + exclude("org/rustls/platformverifier/**") } // KtLint @@ -79,6 +82,9 @@ allprojects { // This file comes from another project and we want to keep it as close to the original as possible exclude("**/SafeChildrenTransitionScope.kt") + + // This file comes from another project and we want to keep it as close to the original as possible + exclude("org/rustls/platformverifier/**") } } // Dependency check diff --git a/libraries/matrix/impl/build.gradle.kts b/libraries/matrix/impl/build.gradle.kts index eae96b5cd9..67386cc592 100644 --- a/libraries/matrix/impl/build.gradle.kts +++ b/libraries/matrix/impl/build.gradle.kts @@ -28,7 +28,7 @@ dependencies { } else { debugImplementation(libs.matrix.sdk) } - implementation(files("libs/rustls-platform-verifier-android.aar")) + implementation(projects.libraries.rustlsTls) implementation(projects.appconfig) implementation(projects.libraries.androidutils) diff --git a/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar deleted file mode 100644 index 8acc8b5fe0..0000000000 Binary files a/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar and /dev/null differ diff --git a/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version b/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version deleted file mode 100644 index a9cd6c6052..0000000000 --- a/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version +++ /dev/null @@ -1 +0,0 @@ -Updated rustls-platform-verifier-android.aar using `rustls-platform-verifier-0.1.1.aar` diff --git a/libraries/rustls-tls/README.md b/libraries/rustls-tls/README.md new file mode 100644 index 0000000000..aa45fe6743 --- /dev/null +++ b/libraries/rustls-tls/README.md @@ -0,0 +1,9 @@ +This module is a wrapper for the Android code distributed in the rustls-platform-verifier-android crate. + +To avoid the distribution mess that this library has (download a Rust crate, then search for it using Gradle and use it as local maven repo), +we previously just manually updated the AAR file instead using a script. This won't work for F-Droid because the AAR library is a black box with +no sources attached to it, so we can't use it like that. + +Instead, for the time being, we're adding the single `CertificateVerifier.kt` class this AAR had in it as part of our sources. + +When this file is updated, the [UPDATED.md](./UPDATED.md) file should be updated too with the commit SHA of the new version. diff --git a/libraries/rustls-tls/UPDATED.md b/libraries/rustls-tls/UPDATED.md new file mode 100644 index 0000000000..10dba2dd53 --- /dev/null +++ b/libraries/rustls-tls/UPDATED.md @@ -0,0 +1,7 @@ +Below is the commit SHA in [rustls-platform-verifier](https://github.com/rustls/rustls-platform-verifier) library used to update the code in this module: + +``` +996b1c903491641b17b3c9afb65d1352f6fc6b76 +``` + +Please update it after making manual changes. diff --git a/libraries/rustls-tls/build.gradle.kts b/libraries/rustls-tls/build.gradle.kts new file mode 100644 index 0000000000..85f3f4c476 --- /dev/null +++ b/libraries/rustls-tls/build.gradle.kts @@ -0,0 +1,24 @@ +import extension.buildConfigFieldBoolean + +/* + * 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. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "org.rustls.platformverifier" + + buildFeatures { + buildConfig = true + } + + defaultConfig { + buildConfigFieldBoolean("TEST", false) + } +} diff --git a/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt b/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt new file mode 100644 index 0000000000..38abed0d92 --- /dev/null +++ b/libraries/rustls-tls/src/main/kotlin/org/rustls/platformverifier/CertificateVerifier.kt @@ -0,0 +1,480 @@ +@file:SuppressLint("LogNotTimber", "ObsoleteSdkInt") +@file:Suppress("KotlinConstantConditions") + +// IMPORTANT: this file comes from rustls-platform-verifier and should not be modified locally. + +/* + * Copyright (c) 2022 1Password + * + * SPDX-License-Identifier: MIT + */ + +package org.rustls.platformverifier + +import android.annotation.SuppressLint +import android.content.Context +import android.net.http.X509TrustManagerExtensions +import android.os.Build +import android.util.Log +import java.io.ByteArrayInputStream +import java.io.File +import java.security.KeyStore +import java.security.KeyStoreException +import java.security.MessageDigest +import java.security.PublicKey +import java.security.cert.CertPathValidator +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateFactory +import java.security.cert.CertificateNotYetValidException +import java.security.cert.CertificateParsingException +import java.security.cert.PKIXBuilderParameters +import java.security.cert.PKIXRevocationChecker +import java.security.cert.X509Certificate +import java.util.Date +import java.util.EnumSet +import javax.net.ssl.TrustManagerFactory +import javax.net.ssl.X509TrustManager +import javax.security.auth.x500.X500Principal + +// If this is updated, update the Rust definition too. +// Marked private as this is not meant to be used in Android code. +private enum class StatusCode(val value: Int) { + Ok(0), + Unavailable(1), + Expired(2), + UnknownCert(3), + Revoked(4), + InvalidEncoding(5), + InvalidExtension(6), +} + +// Marked private as this is not meant to be used in Android code. +private class VerificationResult( + status: StatusCode, + @Suppress("unused") val message: String? = null +) { + @Suppress("unused") + private val code: Int = status.value +} + +// NOTE: All TrustManager and certificate validation methods are not thread safe. These +// are all guarded by Kotlin's `Synchronized` accessors to prevent undefined behavior. + +// Only JNI and test code calls this, so unused code warnings are suppressed. +// Internal for test code - no other Kotlin code should use this object directly. +@Suppress("unused") +// We want to show a difference between Kotlin-side logs and those in Rust code +@SuppressLint("LongLogTag") +internal object CertificateVerifier { + private const val TAG = "rustls-platform-verifier-android" + + private fun createTrustManager(keystore: KeyStore?): X509TrustManagerExtensions? { + // This can never throw since the default algorithm is used. + val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + + factory.init(keystore) + + val availableTrustManagers = try { + factory.trustManagers + } catch (e: RuntimeException) { + Log.w(TAG, "exception thrown creating a TrustManager: $e") + return null + } + + for (manager in availableTrustManagers) { + if (manager is X509TrustManager) { + // Kotlin ensures this can't throw at runtime since it knows that + // it must be the correct type by now. + return X509TrustManagerExtensions(manager) + } + } + + Log.e(TAG, "failed to find a usable trust manager") + return null + } + + private fun makeLazyTrustManager(keystore: KeyStore?): Lazy { + // Ensure the keystore is loaded. Since all of the trust managers are initialized in a + // `Lazy`, this will only run once. + keystore?.load(null) + + return lazy { createTrustManager(keystore) } + } + + // -- Test only -- + // Ideally, all of this will be optimized out at compile time due to not being accessed + // in release builds. + + @get:Synchronized + private val mockKeystore: KeyStore = KeyStore.getInstance(KeyStore.getDefaultType()) + + @get:Synchronized + private var mockTrustManager: Lazy = + makeLazyTrustManager(mockKeystore) + + @JvmStatic + private fun addMockRoot(root: ByteArray) { + if (!BuildConfig.TEST) { + throw Exception("attempted to add a mock root outside a test!") + } + + val alias = "root_${mockKeystore.size()}" + // Throwing here is fine since test roots should always be well-formed + val cert = certFactory.generateCertificate(ByteArrayInputStream(root)) + mockKeystore.setCertificateEntry(alias, cert) + + reloadMockData() + } + + @JvmStatic + private fun clearMockRoots() { + // Reload to get a completely fresh internal state + mockKeystore.load(null) + reloadMockData() + } + + @JvmStatic + private fun reloadMockData() { + if (mockTrustManager.isInitialized()) { + mockTrustManager = makeLazyTrustManager(mockKeystore) + } + } + + // Get a list of the system's root CAs. + // Function is public for testing only. + @JvmStatic + fun getSystemRootCAs(): List { + val rootCAs = mutableListOf() + + val factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) + factory.init(systemKeystore) + + val availableTrustManagers = try { + factory.trustManagers + } catch (e: RuntimeException) { + Log.w(TAG, "exception thrown creating a TrustManager: $e") + return rootCAs + } + + availableTrustManagers.forEach { trustManager -> + if (trustManager is X509TrustManager) { + rootCAs.addAll(trustManager.acceptedIssuers) + } + } + + return rootCAs + } + + // -- End testing requirements -- + + private val certFactory: CertificateFactory = CertificateFactory.getInstance("X.509") + + private var systemTrustAnchorCache = hashSetOf>() + + @get:Synchronized + private var systemCertificateDirectory: File? = System.getenv("ANDROID_ROOT")?.let { rootPath -> + File("$rootPath/etc/security/cacerts") + } + + @get:Synchronized + private val systemKeystore: KeyStore? = try { + KeyStore.getInstance("AndroidCAStore") + } catch (_: KeyStoreException) { + null + } + + @get:Synchronized + private val systemTrustManager: Lazy = + makeLazyTrustManager(systemKeystore) + + @JvmStatic + private fun verifyCertificateChain( + @Suppress("UNUSED_PARAMETER") context: Context, + serverName: String, + authMethod: String, + allowedEkus: Array, + ocspResponse: ByteArray?, + time: Long, + certChain: Array + ): VerificationResult { + // Convert the array of (supposedly) DER bytes into certificates. + val certificateChain = mutableListOf() + certChain.forEach { certBytes -> + val certificate = try { + certFactory.generateCertificate(ByteArrayInputStream(certBytes)) + } catch (e: CertificateException) { + return VerificationResult(StatusCode.InvalidEncoding) + } + certificateChain.add(certificate as X509Certificate) + } + + // Will never throw `ArrayIndexOutOfBoundsException` because `rustls`'s `ServerCertVerifier` trait + // has a mandatory `end_entity` parameter in `verify_server_cert`. + val endEntity = certificateChain[0] + + // Check that the certificate is valid at the point of time provided by `rustls`. + try { + endEntity.checkValidity(Date(time)) + } catch (e: CertificateExpiredException) { + return VerificationResult(StatusCode.Expired) + } catch (e: CertificateNotYetValidException) { + return VerificationResult(StatusCode.Expired) + } + + // Check that this certificate can be used in a TLS server. + if (!verifyCertUsage(endEntity, allowedEkus)) { + return VerificationResult(StatusCode.InvalidExtension) + } + + // Select the trust manager to use. + // + // We select them as follows: + // - If built for release, only use the system trust manager. This should let all test-related + // code be optimized out. + // - If built for tests: + // - If the mock CA store has any values, use the mock trust manager. + // - Otherwise, use the system trust manager. + val (trustManager, keystore) = if (!BuildConfig.TEST) { + val trustManager = + systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) + Pair(trustManager, systemKeystore) + } else { + if (mockKeystore.size() != 0) { + val trustManager = mockTrustManager.value!! + Pair(trustManager, mockKeystore) + } else { + val trustManager = + systemTrustManager.value ?: return VerificationResult(StatusCode.Unavailable) + Pair(trustManager, systemKeystore) + } + } + + // Verify that the certificate chain is valid and correct, and nothing more. + // + // NOTE: This does not validate `serverName` is valid for the end-entity certificate. + // That is handled in Rust as Android/Java do not currently provide a RFC 6125 compliant + // hostname verifier. Additionally, even the RFC 2818 verifier is not available until API 24. + // + // `serverName` is only used for pinning/CT requirements. + // + // Returns the "the properly ordered chain used for verification as a list of X509Certificates.", + // meaning a list from end-entity certificate to trust-anchor. + val validChain = try { + trustManager.checkServerTrusted(certificateChain.toTypedArray(), authMethod, serverName) + } catch (e: CertificateException) { + // In test configurations we may see `checkServerTrusted` fail once vendored test + // certificates pass their expiry date. We try to avoid that by using a fixed + // verification time when calling `endEntity.checkValidity` above, however we can't + // fix the time for the `checkServerTrusted` call. + // + // To make diagnosing CI test failures easier we try to find the root cause of + // checkServerTrusted failing, returning a different `StatusCode` as appropriate. + if (BuildConfig.TEST) { + var rootCause: Throwable? = e + while (rootCause?.cause != null && rootCause.cause != rootCause) { + rootCause = rootCause.cause + } + return when (rootCause) { + is CertificateExpiredException, is CertificateNotYetValidException -> VerificationResult( + StatusCode.Expired, + rootCause.toString() + ) + + else -> VerificationResult(StatusCode.UnknownCert, rootCause.toString()) + } + } + // In non-test configurations we should have caught expiry errors earlier and + // can simply return an unknown cert error without digging through the exception + // cause chain. + return VerificationResult(StatusCode.UnknownCert, e.toString()) + } + + // TEST ONLY: Mock test suite cannot attempt to check revocation status if no OSCP data has been stapled, + // because Android requires certificates to an specify OCSP responder for network fetch in this case. + // If in testing w/o OCSP stapled, short-circuit here - only prior checks apply. + if (BuildConfig.TEST && (mockKeystore.size() != 0) && (ocspResponse == null)) { + return VerificationResult(StatusCode.Ok) + } + + // Try to check the revocation status of the cert, if it is supported. + // + // This is supported at >= API 24, but we're supporting 22 (Android 5) for the best + // compatibility. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + // Note: + // + // 1. Android does not provide any way only to attempt to validate revocation from cached + // data like the other platforms do. This means it will always use the network for + // certificates which had no stapled response. + // + // 2: Likely because of 1, Android requires all issued certificates to have some form of + // revocation included in their authority information. This doesn't work universally as + // issuing certificates in use may omit authority access information (for example the + // Let's Encrypt R3 Intermediate Certificate). + // + // Given these constraints, the best option is to only check revocation information + // at the end-entity depth. We will prefer OCSP (to use stapled information if possible). + // If there is no stapled OCSP response, Android may use the network to attempt to fetch + // one. If OCSP checking fails, it may fall back to fetching CRLs. We allow "soft" + // failures, for example transient network errors. + // + // In the case of a non-public root, such as an internal CA or self-signed certificate, + // we opt to skip revocation checks entirely. The only exception is if the server + // provided stapled OCSP data, which is an explicit signal and won't introduce non-ideal + // platform behavior when attempting validation. + // + // This is because these are cases where a user or administrator has explicitly opted to + // trust a certificate they (at least believe) have control over. These certificates rarely + // contain revocation information as well, so these cases don't lose much. + // See https://github.com/rustls/rustls-platform-verifier/issues/69 as well. + if (ocspResponse == null && !isKnownRoot(validChain.last())) { + // Chain validation must have succeeded by this point. + return VerificationResult(StatusCode.Ok) + } + + val parameters = PKIXBuilderParameters(keystore, null) + + val validator = CertPathValidator.getInstance("PKIX") + val revocationChecker = validator.revocationChecker as PKIXRevocationChecker + + revocationChecker.options = EnumSet.of( + PKIXRevocationChecker.Option.SOFT_FAIL, + PKIXRevocationChecker.Option.ONLY_END_ENTITY + ) + + // Use the OCSP data `rustls` provided, if present. + // Its expected that the server only sends revocation data for its own leaf certificate. + // + // If this field is set, then Android will use it and skip any networking to + // attempt a fetch for that certificate. Otherwise, it will attempt to fetch it from the network. + // Ref: https://cs.android.com/android/platform/superproject/+/master:libcore/ojluni/src/main/java/sun/security/provider/certpath/RevocationChecker.java;l=694 + ocspResponse?.let { providedResponse -> + revocationChecker.ocspResponses = mapOf(endEntity to providedResponse) + } + + // Use the custom revocation definition. + // "Note that when a `PKIXRevocationChecker` is added to `PKIXParameters`, it clones the `PKIXRevocationChecker`; + // thus any subsequent modifications to the `PKIXRevocationChecker` have no effect." + // - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker + parameters.certPathCheckers = listOf(revocationChecker) + // "When supplying a revocation checker in this manner, it will be used to check revocation + // irrespective of the setting of the `RevocationEnabled` flag." + // - https://developer.android.com/reference/java/security/cert/PKIXRevocationChecker + parameters.isRevocationEnabled = false + + // Validate the revocation status of the end entity certificate. + try { + validator.validate(certFactory.generateCertPath(validChain), parameters) + } catch (e: CertPathValidatorException) { + // LetsEncrypt no longer include OCSP information (as OCSP is being deprecated) which Android is not + // happy with since it *only* tries OCSP by default. We aren't 100% decided on how to fix this yet for real + // (see https://github.com/rustls/rustls-platform-verifier/pull/179) so for now we implement an out for + // tests to allow regular maintenance to proceed. + if (BuildConfig.TEST && e.reason == CertPathValidatorException.BasicReason.UNSPECIFIED) { + return VerificationResult(StatusCode.Ok) + } + + return VerificationResult(StatusCode.Revoked, e.toString()) + } + } else { + // This is allowed to be skipped since revocation checking is best-effort. + Log.w(TAG, "did not attempt to validate OCSP due to Android version") + } + + return VerificationResult(StatusCode.Ok) + } + + private fun verifyCertUsage(certificate: X509Certificate, allowedEkus: Array): Boolean { + val ekus = try { + certificate.extendedKeyUsage + } + // This should be unreachable, but could happen. + catch (_: CertificateParsingException) { + return false + } catch (_: NullPointerException) { + // According to Chromium's implementation, this can crash when the EKU data is malformed. + Log.w(TAG, "exception handling certificate EKU") + return false + } ?: return true // If the list is empty, we have nothing to do. + + return ekus.any { allowedEkus.contains(it) } + } + + // Android hashes a principal using the first four bytes of its MD5 digest, encoded in + // lowercase hex and reversed. + // + // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=339 + private fun hashPrincipal(principal: X500Principal): String { + val hexDigits = "0123456789abcdef".toCharArray() + val digest = MessageDigest.getInstance("MD5").digest(principal.encoded) + val hexChars = CharArray(8) + + for (i in 0..3) { + // Kotlin doesn't support bitwise operators for bytes, only Int and Long. + val digestByte = digest[3 - i].toInt() + hexChars[2 * i] = hexDigits[(digestByte shr 4) and 0xf] + hexChars[2 * i + 1] = hexDigits[digestByte and 0xf] + } + + return String(hexChars) + } + + // Check if CA root is known or not. + // Known means installed in root CA store, either a preset public CA or a custom one installed by an enterprise/user. + // + // Ref: https://source.chromium.org/chromium/chromium/src/+/main:net/android/java/src/org/chromium/net/X509Util.java;l=351 + fun isKnownRoot(root: X509Certificate): Boolean { + // System keystore and cert directory must be non-null to perform checking + systemKeystore?.let { loadedSystemKeystore -> + systemCertificateDirectory?.let { loadedSystemCertificateDirectory -> + + // Check the in-memory cache first + val key = Pair(root.subjectX500Principal, root.publicKey) + if (systemTrustAnchorCache.contains(key)) { + return true + } + + // System trust anchors are stored under a hash of the principal. + // In case of collisions, append number. + val hash = hashPrincipal(root.subjectX500Principal) + var i = 0 + while (true) { + val alias = "$hash.$i" + + if (!File(loadedSystemCertificateDirectory, alias).exists()) { + break + } + + val anchor = loadedSystemKeystore.getCertificate("system:$alias") + + // It's possible for `anchor` to be `null` if the user deleted a trust anchor. + // Continue iterating as there may be further collisions after the deleted anchor. + if (anchor == null) { + continue + // This should never happen + } else if (anchor !is X509Certificate) { + // SAFETY: This logs a unique identifier (hash value) only in cases where a file within the + // system's root trust store is not a valid X509 certificate (extremely unlikely error). + // The hash doesn't tell us any sensitive information about the invalid cert or reveal any of + // its contents - it just lets us ID the bad file if a user is having TLS failure issues. + Log.e(TAG, "anchor is not a certificate, alias: $alias") + continue + // If subject and public key match, it's a system root. + } else { + if ((root.subjectX500Principal == anchor.subjectX500Principal) && (root.publicKey == anchor.publicKey)) { + systemTrustAnchorCache.add(key) + return true + } + } + + i += 1 + } + } + } + + // Not found in cache or store: non-public + return false + } +} diff --git a/plugins/src/main/kotlin/extension/KoverExtension.kt b/plugins/src/main/kotlin/extension/KoverExtension.kt index 27e44e31b9..5d6b1ddabb 100644 --- a/plugins/src/main/kotlin/extension/KoverExtension.kt +++ b/plugins/src/main/kotlin/extension/KoverExtension.kt @@ -43,6 +43,7 @@ val excludedKoverSubProjects = listOf( ":libraries:core", ":libraries:coroutines", ":libraries:di", + ":libraries:rustls-tls", ":tests:detekt-rules", ":tests:konsist", ":tests:testutils", diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt index f47621da0e..e7f82292a4 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistLicenseTest.kt @@ -48,6 +48,7 @@ class KonsistLicenseTest { .files .filter { it.moduleName.startsWith("enterprise").not() && + it.moduleName != "libraries/rustls-tls" && it.nameWithExtension != "locales.kt" && it.name.startsWith("Template ").not() } @@ -78,6 +79,7 @@ class KonsistLicenseTest { .scopeFromProject() .files .filter { + it.moduleName.endsWith("rustls-tls").not() && it.nameWithExtension != "locales.kt" && it.nameWithExtension != "KonsistLicenseTest.kt" && it.name.startsWith("Template ").not() diff --git a/tools/sdk/update-rustls b/tools/sdk/update-rustls deleted file mode 100755 index d8ad883d69..0000000000 --- a/tools/sdk/update-rustls +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env bash - -# 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. - -set -e -set -u - -VERSION=${1:-} -if [ -n "$VERSION" ]; then - PACKAGE=rustls-platform-verifier-android==$VERSION -else - PACKAGE=rustls-platform-verifier-android -fi - -cargo install cargo-download -mkdir -p tmp/rustls-platform-verifier-android -cargo download $PACKAGE > tmp/rustls-platform-verifier-android/rustls-platform-verifier-android.gz -ROOT=$(git rev-parse --show-toplevel) - -cd tmp/rustls-platform-verifier-android - -echo "Extracting rustls-platform-verifier-android.aar from \`rustls-platform-verifier-android.gz\`" - -tar -xzvf rustls-platform-verifier-android.gz &> /dev/null -DIR=$(find . -type d -name "rustls-platform-verifier-android-*") -AAR=$(find $DIR -type f -name "*.aar") -cp $AAR $ROOT/libraries/matrix/impl/libs/rustls-platform-verifier-android.aar -cd $ROOT -rm -r tmp/rustls-platform-verifier-android - -echo "Updated rustls-platform-verifier-android.aar using \`$(basename $AAR)\`" > libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version -cat libraries/matrix/impl/libs/rustls-platform-verifier-android.aar.version