-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Trust ISRG Root X1 certificate on Android < 7.1
- Loading branch information
Showing
7 changed files
with
209 additions
and
48 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,26 +1,39 @@ | ||
// SPDX-FileCopyrightText: 2017-2023 Alexey Rochev <[email protected]> | ||
// SPDX-FileCopyrightText: 2019 Thunderberry | ||
// | ||
// SPDX-License-Identifier: GPL-3.0-or-later | ||
|
||
package org.equeim.tremotesf.torrentfile.rpc | ||
|
||
import android.annotation.SuppressLint | ||
import android.content.Context | ||
import android.net.http.X509TrustManagerExtensions | ||
import android.os.Build | ||
import android.util.Base64 | ||
import androidx.annotation.Keep | ||
import org.equeim.tremotesf.rpc.R | ||
import java.io.InputStream | ||
import java.security.InvalidAlgorithmParameterException | ||
import java.security.KeyFactory | ||
import java.security.KeyStore | ||
import java.security.SecureRandom | ||
import java.security.cert.Certificate | ||
import java.security.cert.CertificateException | ||
import java.security.cert.CertificateFactory | ||
import java.security.cert.X509Certificate | ||
import java.security.spec.PKCS8EncodedKeySpec | ||
import javax.net.ssl.HostnameVerifier | ||
import javax.net.ssl.KeyManager | ||
import javax.net.ssl.KeyManagerFactory | ||
import javax.net.ssl.SSLContext | ||
import javax.net.ssl.SSLSocketFactory | ||
import javax.net.ssl.TrustManagerFactory | ||
import javax.net.ssl.X509TrustManager | ||
|
||
|
||
class TlsConfiguration( | ||
val sslSocketFactory: SSLSocketFactory, | ||
val trustManager: X509TrustManager, | ||
val hostnameVerifier: HostnameVerifier?, | ||
val clientCertificates: List<Certificate>, | ||
) | ||
|
||
|
@@ -30,59 +43,168 @@ class TlsConfiguration( | |
internal fun createTlsConfiguration( | ||
clientCertificatesString: String?, | ||
selfSignedCertificatesString: String?, | ||
): TlsConfiguration = try { | ||
val certificateFactory = CertificateFactory.getInstance("X.509") | ||
val trustManager = createTrustManager(selfSignedCertificatesString, certificateFactory) | ||
val clientCertificates: List<Certificate> | ||
val keyManager: KeyManager? | ||
if (clientCertificatesString != null) { | ||
clientCertificates = parseCertificates(certificateFactory, clientCertificatesString, "client") | ||
keyManager = createKeyManager(clientCertificates, clientCertificatesString) | ||
} else { | ||
clientCertificates = emptyList() | ||
keyManager = null | ||
serverHostname: String, | ||
context: Context, | ||
): TlsConfiguration? { | ||
// We need to set up ISRG Root X1 certificate for Android < 7.1 | ||
if (clientCertificatesString == null && selfSignedCertificatesString == null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { | ||
return null | ||
} | ||
return try { | ||
val certificateFactory = CertificateFactory.getInstance("X.509") | ||
|
||
val trustManager = selfSignedCertificatesString?.let { | ||
createTrustManagerForSelfSignedCertificates(it, certificateFactory) | ||
} ?: createDefaultTrustManager(certificateFactory, context) | ||
|
||
val hostnameVerifier = if (selfSignedCertificatesString != null) { | ||
HostnameVerifier { hostname, _ -> hostname == serverHostname } | ||
} else { | ||
null | ||
} | ||
|
||
val clientCertificates: List<Certificate> | ||
val keyManager: KeyManager? | ||
if (clientCertificatesString != null) { | ||
clientCertificates = try { | ||
certificateFactory.generateCertificates(clientCertificatesString.byteInputStream()).toList() | ||
} catch (e: Exception) { | ||
throw RuntimeException("Failed to parse client's certificate chain", e) | ||
} | ||
keyManager = createKeyManagerForClientCertificate(clientCertificates, clientCertificatesString) | ||
} else { | ||
clientCertificates = emptyList() | ||
keyManager = null | ||
} | ||
val sslContext = SSLContext.getInstance("TLS") | ||
sslContext.init(keyManager?.let { arrayOf(it) }, arrayOf(trustManager), null) | ||
TlsConfiguration( | ||
sslSocketFactory = sslContext.socketFactory, | ||
trustManager = trustManager, | ||
hostnameVerifier = hostnameVerifier, | ||
clientCertificates = clientCertificates, | ||
) | ||
} catch (e: Exception) { | ||
throw RuntimeException("Failed to set up TLS configuration", e) | ||
} | ||
val sslContext = SSLContext.getInstance("TLS") | ||
sslContext.init(keyManager?.let { arrayOf(it) }, arrayOf(trustManager), SecureRandom()) | ||
TlsConfiguration( | ||
sslSocketFactory = sslContext.socketFactory, | ||
trustManager = trustManager, | ||
clientCertificates = clientCertificates, | ||
) | ||
} catch (e: Exception) { | ||
throw RuntimeException("Failed to set up TLS configuration", e) | ||
} | ||
|
||
private fun parseCertificates(factory: CertificateFactory, certificatesString: String, who: String): List<Certificate> { | ||
return try { | ||
factory.generateCertificates(certificatesString.byteInputStream()).toList() | ||
private fun createTrustManagerForSelfSignedCertificates( | ||
selfSignedCertificatesString: String, | ||
certificateFactory: CertificateFactory, | ||
): X509TrustManager = | ||
try { | ||
createTrustManagerForCertificateChain(selfSignedCertificatesString.byteInputStream(), certificateFactory) | ||
} catch (e: Exception) { | ||
throw RuntimeException("Failed to parse $who's certificates") | ||
throw RuntimeException("Failed to create TrustManager for server's self signed certificate chain", e) | ||
} | ||
|
||
private fun createDefaultTrustManager( | ||
certificateFactory: CertificateFactory, | ||
context: Context, | ||
): X509TrustManager = try { | ||
val defaultTrustManager = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).run { | ||
init(null as KeyStore?) | ||
trustManagers.single() as X509TrustManager | ||
} | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { | ||
defaultTrustManager | ||
} else { | ||
CompositeX509TrustManager( | ||
defaultTrustManager, | ||
createTrustManagerForCertificateChain( | ||
context.resources.openRawResource(R.raw.isrgrootx1), | ||
certificateFactory | ||
) | ||
) | ||
} | ||
} catch (e: Exception) { | ||
throw RuntimeException("Failed to create default TrustManager", e) | ||
} | ||
|
||
private fun createTrustManager(selfSignedCertificatesString: String?, certificateFactory: CertificateFactory): X509TrustManager { | ||
val keyStore = selfSignedCertificatesString?.let { | ||
val selfSignedCertificates = parseCertificates(certificateFactory, it, "server") | ||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) | ||
keyStore.load(null, null) | ||
selfSignedCertificates.forEachIndexed { index, cert -> | ||
keyStore.setCertificateEntry(index.toString(), cert) | ||
} | ||
keyStore | ||
private fun createTrustManagerForCertificateChain( | ||
certificatesStream: InputStream, | ||
certificateFactory: CertificateFactory, | ||
): X509TrustManager { | ||
val certificates = certificatesStream.use(certificateFactory::generateCertificates)!! | ||
if (certificates.isEmpty()) { | ||
throw RuntimeException("Did not read any certificates") | ||
} | ||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) | ||
keyStore.load(null, null) | ||
certificates.forEachIndexed { index, cert -> | ||
keyStore.setCertificateEntry(index.toString(), cert) | ||
} | ||
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) | ||
trustManagerFactory.init(keyStore) | ||
return trustManagerFactory.trustManagers.single() as X509TrustManager | ||
} | ||
|
||
@SuppressLint("CustomX509TrustManager") | ||
private class CompositeX509TrustManager(vararg trustManagers: X509TrustManager) : X509TrustManager { | ||
private class TrustManagerAndExtensions( | ||
val trustManager: X509TrustManager, | ||
val extensions: X509TrustManagerExtensions, | ||
) { | ||
constructor(trustManager: X509TrustManager) : this(trustManager, X509TrustManagerExtensions(trustManager)) | ||
} | ||
|
||
private val trustManagers: List<TrustManagerAndExtensions> = trustManagers.map(::TrustManagerAndExtensions) | ||
|
||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { | ||
checkTrusted { trustManager.checkClientTrusted(chain, authType) } | ||
} | ||
|
||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { | ||
checkTrusted { trustManager.checkServerTrusted(chain, authType) } | ||
} | ||
|
||
override fun getAcceptedIssuers(): Array<X509Certificate> = | ||
trustManagers.asSequence().flatMap { it.trustManager.acceptedIssuers.asSequence() }.toList().toTypedArray() | ||
|
||
@Suppress("unused") | ||
@Keep | ||
fun checkServerTrusted( | ||
chain: Array<X509Certificate?>?, authType: String?, host: String?, | ||
): List<X509Certificate> { | ||
return checkTrusted { extensions.checkServerTrusted(chain, authType, host) } | ||
} | ||
|
||
private fun <T> checkTrusted(check: TrustManagerAndExtensions.() -> T): T { | ||
val certificateExceptions = mutableListOf<CertificateException>() | ||
for (trustManager in trustManagers) { | ||
try { | ||
return trustManager.check() | ||
} catch (e: CertificateException) { | ||
certificateExceptions.add(e) | ||
} catch (e: RuntimeException) { | ||
val cause = e.cause | ||
if (cause is InvalidAlgorithmParameterException) { | ||
// Handling of [InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty] | ||
// | ||
// This is most likely a result of using a TrustManager created from an empty KeyStore. | ||
// The exception will be thrown during the SSL Handshake. It is safe to suppress | ||
// and can be bundle with the other exceptions to proceed validating the counterparty with | ||
// the remaining TrustManagers. | ||
certificateExceptions.add(CertificateException(cause)) | ||
} else { | ||
throw e | ||
} | ||
} | ||
} | ||
val certificateException = CertificateException("None of the TrustManagers trust this certificate chain") | ||
certificateExceptions.forEach(certificateException::addSuppressed) | ||
throw certificateException | ||
} | ||
} | ||
|
||
private val PRIVATE_KEY_REGEX = | ||
Regex( | ||
""".*-----BEGIN PRIVATE KEY-----([A-Za-z0-9+/=\n]+)-----END PRIVATE KEY-----.*""", | ||
setOf(RegexOption.MULTILINE, RegexOption.DOT_MATCHES_ALL) | ||
) | ||
|
||
private fun createKeyManager(clientCertificates: List<Certificate>, clientCertificatesString: String): KeyManager { | ||
private fun createKeyManagerForClientCertificate(clientCertificates: List<Certificate>, clientCertificatesString: String): KeyManager { | ||
val clientKey = try { | ||
val spec = PKCS8EncodedKeySpec( | ||
Base64.decode( | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
-----BEGIN CERTIFICATE----- | ||
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 | ||
-----END CERTIFICATE----- |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters