Skip to content

Commit 5a15f0b

Browse files
committed
Trust ISRG Root X1 certificate on Android < 7.1
1 parent a9ba015 commit 5a15f0b

File tree

7 files changed

+209
-48
lines changed

7 files changed

+209
-48
lines changed

.reuse/dep5

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Source: https://github.com/equeim/tremotesf-android
99
# Copyright: $YEAR $NAME <$CONTACT>
1010
# License: ...
1111

12-
Files: CHANGELOG.md README.md app/src/main/res/raw*/*.html app/src/main/res/raw/translators fastlane/metadata/android/**/*.txt fastlane/metadata/android/**/*.png torrentfile/src/test/resources/org/equeim/tremotesf/torrentfile/*.torrent
12+
Files: CHANGELOG.md README.md app/src/main/res/raw*/*.html app/src/main/res/raw/translators fastlane/metadata/android/**/*.txt fastlane/metadata/android/**/*.png rpc/src/main/res/raw/isrgrootx1.pem torrentfile/src/test/resources/org/equeim/tremotesf/torrentfile/*.torrent
1313
Copyright: 2017-2022 Alexey Rochev
1414
License: CC0-1.0
1515

app/src/main/kotlin/org/equeim/tremotesf/rpc/GlobalRpcClient.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package org.equeim.tremotesf.rpc
66

7+
import android.annotation.SuppressLint
78
import android.content.Context
89
import androidx.annotation.StringRes
910
import kotlinx.coroutines.CoroutineScope
@@ -15,14 +16,16 @@ import kotlinx.coroutines.channels.Channel
1516
import kotlinx.coroutines.flow.distinctUntilChanged
1617
import kotlinx.coroutines.launch
1718
import org.equeim.tremotesf.R
19+
import org.equeim.tremotesf.TremotesfApplication
1820
import org.equeim.tremotesf.torrentfile.rpc.RpcClient
1921
import org.equeim.tremotesf.torrentfile.rpc.RpcRequestError
2022
import org.equeim.tremotesf.torrentfile.rpc.requests.DUPLICATE_TORRENT_RESULT
2123
import org.equeim.tremotesf.torrentfile.rpc.shouldUpdateConnectionConfiguration
2224
import org.equeim.tremotesf.ui.AppForegroundTracker
2325
import java.util.concurrent.atomic.AtomicBoolean
2426

25-
object GlobalRpcClient : RpcClient(CoroutineScope(SupervisorJob() + Dispatchers.Default)) {
27+
@SuppressLint("StaticFieldLeak")
28+
object GlobalRpcClient : RpcClient(CoroutineScope(SupervisorJob() + Dispatchers.Default), TremotesfApplication.instance) {
2629
data class BackgroundRpcRequestError(val error: RpcRequestError, @StringRes val errorContext: Int)
2730

2831
val backgroundRpcRequestsErrors: Channel<BackgroundRpcRequestError> = Channel(Channel.UNLIMITED)

rpc/src/main/kotlin/org/equeim/tremotesf/torrentfile/rpc/ConnectionConfiguration.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package org.equeim.tremotesf.torrentfile.rpc
66

7+
import android.content.Context
78
import okhttp3.Credentials
89
import okhttp3.HttpUrl
910
import okhttp3.OkHttpClient
@@ -24,7 +25,7 @@ internal data class ConnectionConfiguration(
2425
/**
2526
* @throws RuntimeException
2627
*/
27-
internal fun createConnectionConfiguration(server: Server): ConnectionConfiguration {
28+
internal fun createConnectionConfiguration(server: Server, context: Context): ConnectionConfiguration {
2829
val url = createUrl(server)
2930
val builder = OkHttpClient.Builder()
3031
.addNetworkInterceptor(RealRequestHeadersInterceptor())
@@ -39,14 +40,16 @@ internal fun createConnectionConfiguration(server: Server): ConnectionConfigurat
3940
if (server.httpsEnabled) {
4041
val clientCertificate = if (server.clientCertificateEnabled) server.clientCertificate.takeIf { it.isNotBlank() } else null
4142
val serverCertificate = if (server.selfSignedCertificateEnabled) server.selfSignedCertificate.takeIf { it.isNotBlank() } else null
42-
if (clientCertificate != null || serverCertificate != null) {
43-
val configuration = createTlsConfiguration(
44-
if (server.clientCertificateEnabled) server.clientCertificate.takeIf { it.isNotBlank() } else null,
45-
if (server.selfSignedCertificateEnabled) server.selfSignedCertificate.takeIf { it.isNotBlank() } else null
46-
)
43+
val configuration = createTlsConfiguration(
44+
clientCertificatesString = clientCertificate,
45+
selfSignedCertificatesString = serverCertificate,
46+
serverHostname = url.host,
47+
context = context
48+
)
49+
if (configuration != null) {
4750
builder.sslSocketFactory(configuration.sslSocketFactory, configuration.trustManager)
48-
if (serverCertificate != null) {
49-
builder.hostnameVerifier { hostname, _ -> hostname == url.host }
51+
configuration.hostnameVerifier?.let {
52+
builder.hostnameVerifier(it)
5053
}
5154
clientCertificates = configuration.clientCertificates
5255
}

rpc/src/main/kotlin/org/equeim/tremotesf/torrentfile/rpc/RpcClient.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
package org.equeim.tremotesf.torrentfile.rpc
66

7+
import android.content.Context
78
import kotlinx.coroutines.CoroutineScope
89
import kotlinx.coroutines.flow.MutableStateFlow
910
import kotlinx.coroutines.flow.StateFlow
@@ -30,7 +31,7 @@ import org.equeim.tremotesf.torrentfile.rpc.requests.ServerVersionResponseArgume
3031
import timber.log.Timber
3132
import java.net.HttpURLConnection
3233

33-
open class RpcClient(protected val coroutineScope: CoroutineScope) {
34+
open class RpcClient(protected val coroutineScope: CoroutineScope, private val context: Context) {
3435
private val connectionConfiguration = MutableStateFlow<Result<ConnectionConfiguration>?>(null)
3536
internal fun getConnectionConfiguration(): StateFlow<Result<ConnectionConfiguration>?> = connectionConfiguration
3637

@@ -70,7 +71,7 @@ open class RpcClient(protected val coroutineScope: CoroutineScope) {
7071
Timber.d("setConnectionConfiguration() called with: server = $server")
7172
val newConnectionConfiguration = server?.let {
7273
try {
73-
Result.success(createConnectionConfiguration(it))
74+
Result.success(createConnectionConfiguration(it, context))
7475
} catch (e: Exception) {
7576
Timber.e(e, "Bad connection configuration")
7677
Result.failure(e)

rpc/src/main/kotlin/org/equeim/tremotesf/torrentfile/rpc/TlsConfiguration.kt

Lines changed: 157 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,39 @@
11
// SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]>
2+
// SPDX-FileCopyrightText: 2019 Thunderberry
23
//
34
// SPDX-License-Identifier: GPL-3.0-or-later
45

56
package org.equeim.tremotesf.torrentfile.rpc
67

8+
import android.annotation.SuppressLint
9+
import android.content.Context
10+
import android.net.http.X509TrustManagerExtensions
11+
import android.os.Build
712
import android.util.Base64
13+
import androidx.annotation.Keep
14+
import org.equeim.tremotesf.rpc.R
15+
import java.io.InputStream
16+
import java.security.InvalidAlgorithmParameterException
817
import java.security.KeyFactory
918
import java.security.KeyStore
10-
import java.security.SecureRandom
1119
import java.security.cert.Certificate
20+
import java.security.cert.CertificateException
1221
import java.security.cert.CertificateFactory
22+
import java.security.cert.X509Certificate
1323
import java.security.spec.PKCS8EncodedKeySpec
24+
import javax.net.ssl.HostnameVerifier
1425
import javax.net.ssl.KeyManager
1526
import javax.net.ssl.KeyManagerFactory
1627
import javax.net.ssl.SSLContext
1728
import javax.net.ssl.SSLSocketFactory
1829
import javax.net.ssl.TrustManagerFactory
1930
import javax.net.ssl.X509TrustManager
2031

32+
2133
class TlsConfiguration(
2234
val sslSocketFactory: SSLSocketFactory,
2335
val trustManager: X509TrustManager,
36+
val hostnameVerifier: HostnameVerifier?,
2437
val clientCertificates: List<Certificate>,
2538
)
2639

@@ -30,59 +43,168 @@ class TlsConfiguration(
3043
internal fun createTlsConfiguration(
3144
clientCertificatesString: String?,
3245
selfSignedCertificatesString: String?,
33-
): TlsConfiguration = try {
34-
val certificateFactory = CertificateFactory.getInstance("X.509")
35-
val trustManager = createTrustManager(selfSignedCertificatesString, certificateFactory)
36-
val clientCertificates: List<Certificate>
37-
val keyManager: KeyManager?
38-
if (clientCertificatesString != null) {
39-
clientCertificates = parseCertificates(certificateFactory, clientCertificatesString, "client")
40-
keyManager = createKeyManager(clientCertificates, clientCertificatesString)
41-
} else {
42-
clientCertificates = emptyList()
43-
keyManager = null
46+
serverHostname: String,
47+
context: Context,
48+
): TlsConfiguration? {
49+
// We need to set up ISRG Root X1 certificate for Android < 7.1
50+
if (clientCertificatesString == null && selfSignedCertificatesString == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
51+
return null
52+
}
53+
return try {
54+
val certificateFactory = CertificateFactory.getInstance("X.509")
55+
56+
val trustManager = selfSignedCertificatesString?.let {
57+
createTrustManagerForSelfSignedCertificates(it, certificateFactory)
58+
} ?: createDefaultTrustManager(certificateFactory, context)
59+
60+
val hostnameVerifier = if (selfSignedCertificatesString != null) {
61+
HostnameVerifier { hostname, _ -> hostname == serverHostname }
62+
} else {
63+
null
64+
}
65+
66+
val clientCertificates: List<Certificate>
67+
val keyManager: KeyManager?
68+
if (clientCertificatesString != null) {
69+
clientCertificates = try {
70+
certificateFactory.generateCertificates(clientCertificatesString.byteInputStream()).toList()
71+
} catch (e: Exception) {
72+
throw RuntimeException("Failed to parse client's certificate chain", e)
73+
}
74+
keyManager = createKeyManagerForClientCertificate(clientCertificates, clientCertificatesString)
75+
} else {
76+
clientCertificates = emptyList()
77+
keyManager = null
78+
}
79+
val sslContext = SSLContext.getInstance("TLS")
80+
sslContext.init(keyManager?.let { arrayOf(it) }, arrayOf(trustManager), null)
81+
TlsConfiguration(
82+
sslSocketFactory = sslContext.socketFactory,
83+
trustManager = trustManager,
84+
hostnameVerifier = hostnameVerifier,
85+
clientCertificates = clientCertificates,
86+
)
87+
} catch (e: Exception) {
88+
throw RuntimeException("Failed to set up TLS configuration", e)
4489
}
45-
val sslContext = SSLContext.getInstance("TLS")
46-
sslContext.init(keyManager?.let { arrayOf(it) }, arrayOf(trustManager), SecureRandom())
47-
TlsConfiguration(
48-
sslSocketFactory = sslContext.socketFactory,
49-
trustManager = trustManager,
50-
clientCertificates = clientCertificates,
51-
)
52-
} catch (e: Exception) {
53-
throw RuntimeException("Failed to set up TLS configuration", e)
5490
}
5591

56-
private fun parseCertificates(factory: CertificateFactory, certificatesString: String, who: String): List<Certificate> {
57-
return try {
58-
factory.generateCertificates(certificatesString.byteInputStream()).toList()
92+
private fun createTrustManagerForSelfSignedCertificates(
93+
selfSignedCertificatesString: String,
94+
certificateFactory: CertificateFactory,
95+
): X509TrustManager =
96+
try {
97+
createTrustManagerForCertificateChain(selfSignedCertificatesString.byteInputStream(), certificateFactory)
5998
} catch (e: Exception) {
60-
throw RuntimeException("Failed to parse $who's certificates")
99+
throw RuntimeException("Failed to create TrustManager for server's self signed certificate chain", e)
100+
}
101+
102+
private fun createDefaultTrustManager(
103+
certificateFactory: CertificateFactory,
104+
context: Context,
105+
): X509TrustManager = try {
106+
val defaultTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).run {
107+
init(null as KeyStore?)
108+
trustManagers.single() as X509TrustManager
109+
}
110+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
111+
defaultTrustManager
112+
} else {
113+
CompositeX509TrustManager(
114+
defaultTrustManager,
115+
createTrustManagerForCertificateChain(
116+
context.resources.openRawResource(R.raw.isrgrootx1),
117+
certificateFactory
118+
)
119+
)
61120
}
121+
} catch (e: Exception) {
122+
throw RuntimeException("Failed to create default TrustManager", e)
62123
}
63124

64-
private fun createTrustManager(selfSignedCertificatesString: String?, certificateFactory: CertificateFactory): X509TrustManager {
65-
val keyStore = selfSignedCertificatesString?.let {
66-
val selfSignedCertificates = parseCertificates(certificateFactory, it, "server")
67-
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
68-
keyStore.load(null, null)
69-
selfSignedCertificates.forEachIndexed { index, cert ->
70-
keyStore.setCertificateEntry(index.toString(), cert)
71-
}
72-
keyStore
125+
private fun createTrustManagerForCertificateChain(
126+
certificatesStream: InputStream,
127+
certificateFactory: CertificateFactory,
128+
): X509TrustManager {
129+
val certificates = certificatesStream.use(certificateFactory::generateCertificates)!!
130+
if (certificates.isEmpty()) {
131+
throw RuntimeException("Did not read any certificates")
132+
}
133+
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
134+
keyStore.load(null, null)
135+
certificates.forEachIndexed { index, cert ->
136+
keyStore.setCertificateEntry(index.toString(), cert)
73137
}
74138
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
75139
trustManagerFactory.init(keyStore)
76140
return trustManagerFactory.trustManagers.single() as X509TrustManager
77141
}
78142

143+
@SuppressLint("CustomX509TrustManager")
144+
private class CompositeX509TrustManager(vararg trustManagers: X509TrustManager) : X509TrustManager {
145+
private class TrustManagerAndExtensions(
146+
val trustManager: X509TrustManager,
147+
val extensions: X509TrustManagerExtensions,
148+
) {
149+
constructor(trustManager: X509TrustManager) : this(trustManager, X509TrustManagerExtensions(trustManager))
150+
}
151+
152+
private val trustManagers: List<TrustManagerAndExtensions> = trustManagers.map(::TrustManagerAndExtensions)
153+
154+
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
155+
checkTrusted { trustManager.checkClientTrusted(chain, authType) }
156+
}
157+
158+
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
159+
checkTrusted { trustManager.checkServerTrusted(chain, authType) }
160+
}
161+
162+
override fun getAcceptedIssuers(): Array<X509Certificate> =
163+
trustManagers.asSequence().flatMap { it.trustManager.acceptedIssuers.asSequence() }.toList().toTypedArray()
164+
165+
@Suppress("unused")
166+
@Keep
167+
fun checkServerTrusted(
168+
chain: Array<X509Certificate?>?, authType: String?, host: String?,
169+
): List<X509Certificate> {
170+
return checkTrusted { extensions.checkServerTrusted(chain, authType, host) }
171+
}
172+
173+
private fun <T> checkTrusted(check: TrustManagerAndExtensions.() -> T): T {
174+
val certificateExceptions = mutableListOf<CertificateException>()
175+
for (trustManager in trustManagers) {
176+
try {
177+
return trustManager.check()
178+
} catch (e: CertificateException) {
179+
certificateExceptions.add(e)
180+
} catch (e: RuntimeException) {
181+
val cause = e.cause
182+
if (cause is InvalidAlgorithmParameterException) {
183+
// Handling of [InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty]
184+
//
185+
// This is most likely a result of using a TrustManager created from an empty KeyStore.
186+
// The exception will be thrown during the SSL Handshake. It is safe to suppress
187+
// and can be bundle with the other exceptions to proceed validating the counterparty with
188+
// the remaining TrustManagers.
189+
certificateExceptions.add(CertificateException(cause))
190+
} else {
191+
throw e
192+
}
193+
}
194+
}
195+
val certificateException = CertificateException("None of the TrustManagers trust this certificate chain")
196+
certificateExceptions.forEach(certificateException::addSuppressed)
197+
throw certificateException
198+
}
199+
}
200+
79201
private val PRIVATE_KEY_REGEX =
80202
Regex(
81203
""".*-----BEGIN PRIVATE KEY-----([A-Za-z0-9+/=\n]+)-----END PRIVATE KEY-----.*""",
82204
setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL)
83205
)
84206

85-
private fun createKeyManager(clientCertificates: List<Certificate>, clientCertificatesString: String): KeyManager {
207+
private fun createKeyManagerForClientCertificate(clientCertificates: List<Certificate>, clientCertificatesString: String): KeyManager {
86208
val clientKey = try {
87209
val spec = PKCS8EncodedKeySpec(
88210
Base64.decode(

rpc/src/main/res/raw/isrgrootx1.pem

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
3+
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
4+
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
5+
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
6+
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
7+
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
8+
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
9+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
10+
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
11+
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
12+
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
13+
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
14+
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
15+
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
16+
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
17+
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
18+
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
19+
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
20+
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
21+
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
22+
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
23+
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
24+
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
25+
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
26+
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
27+
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
28+
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
29+
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
30+
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
31+
-----END CERTIFICATE-----

rpc/src/test/kotlin/org/equeim/tremotesf/torrentfile/rpc/RpcClientTest.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package org.equeim.tremotesf.torrentfile.rpc
66

77
import android.os.SystemClock
88
import io.mockk.every
9+
import io.mockk.mockk
910
import io.mockk.mockkStatic
1011
import io.mockk.unmockkStatic
1112
import kotlinx.coroutines.Dispatchers
@@ -43,7 +44,7 @@ import kotlin.time.Duration.Companion.seconds
4344
class RpcClientTest {
4445
private val dispatcher = StandardTestDispatcher()
4546
private val server = MockWebServer()
46-
private val client = RpcClient(TestScope(dispatcher))
47+
private val client = RpcClient(TestScope(dispatcher), mockk())
4748

4849
private class TestTree : Timber.DebugTree() {
4950
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {

0 commit comments

Comments
 (0)