Commit 846f0cd3 authored by Ricki Hirner's avatar Ricki Hirner

Use Conscrypt

parent aae988de
......@@ -31,6 +31,24 @@ Discussion: https://forums.bitfire.at/category/7/transport-level-security
1. Close the instance when it's not required anymore (will disconnect from the
`CustomCertService`, thus allowing it to be destroyed).
Example of initialzing an okhttp client:
val keyManager = ...
CustomCertManager(...).use { trustManager ->
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null
)
val builder = OkHttpClient.Builder()
builder.sslSocketFactory(sslContext.socketFactory, trustManager)
.hostnameVerifier(hostnameVerifier)
val httpClient = builder.build()
// use httpClient
}
You can overwrite resources when you want, just have a look at the `res/strings`
directory. Especially `certificate_notification_connection_security` and
`trust_certificate_unknown_certificate_found` should contain your app name.
......
......@@ -2,7 +2,8 @@
buildscript {
ext.versions = [
kotlin: '1.3.30',
dokka: '0.9.17'
dokka: '0.9.17',
conscrypt: '2.1.0'
]
repositories {
......@@ -55,6 +56,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'androidx.lifecycle:lifecycle-livedata:2.0.0'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.0.0'
implementation "org.conscrypt:conscrypt-android:${versions.conscrypt}"
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test:rules:1.1.1'
......
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.mockwebserver.MockWebServer
import org.apache.commons.io.IOUtils
import org.apache.commons.lang3.ArrayUtils.contains
import org.junit.After
import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.net.Socket
import java.security.KeyFactory
import java.security.KeyStore
import java.security.Principal
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.PKCS8EncodedKeySpec
import javax.net.ssl.SSLSocket
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509ExtendedKeyManager
import javax.net.ssl.X509TrustManager
class CertTlsSocketFactoryTest {
private lateinit var certMgr: CustomCertManager
private lateinit var factory: CertTlsSocketFactory
private val server = MockWebServer()
@Before
fun startServer() {
certMgr = CustomCertManager(getInstrumentation().context, false, true)
factory = CertTlsSocketFactory(null, certMgr)
server.start()
}
@After
fun stopServer() {
server.shutdown()
certMgr.close()
}
@Test
fun testSendClientCertificate() {
var public: X509Certificate? = null
javaClass.classLoader!!.getResourceAsStream("sample.crt").use {
public = CertificateFactory.getInstance("X509").generateCertificate(it) as? X509Certificate
}
assertNotNull(public)
val keyFactory = KeyFactory.getInstance("RSA")
val private = keyFactory.generatePrivate(PKCS8EncodedKeySpec(readResource("sample.key")))
assertNotNull(private)
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val alias = "sample"
keyStore.setKeyEntry(alias, private, null, arrayOf(public))
assertTrue(keyStore.containsAlias(alias))
val trustManagerFactory = TrustManagerFactory.getInstance("X509")
trustManagerFactory.init(null as KeyStore?)
val trustManager = trustManagerFactory.trustManagers.first() as X509TrustManager
val factory = CertTlsSocketFactory(object: X509ExtendedKeyManager() {
override fun getServerAliases(p0: String?, p1: Array<out Principal>?): Array<String>? = null
override fun chooseServerAlias(p0: String?, p1: Array<out Principal>?, p2: Socket?) = null
override fun getClientAliases(p0: String?, p1: Array<out Principal>?) =
arrayOf(alias)
override fun chooseClientAlias(p0: Array<out String>?, p1: Array<out Principal>?, p2: Socket?) =
alias
override fun getCertificateChain(forAlias: String?) =
arrayOf(public).takeIf { forAlias == alias }
override fun getPrivateKey(forAlias: String?) =
private.takeIf { forAlias == alias }
}, trustManager)
/* known client cert test URLs (thanks!):
* - https://prod.idrix.eu/secure/
* - https://server.cryptomix.com/secure/
*/
val client = OkHttpClient.Builder()
.sslSocketFactory(factory, trustManager)
.build()
client.newCall(Request.Builder()
.get()
.url("https://prod.idrix.eu/secure/")
.build()).execute().use { response ->
assertTrue(response.isSuccessful)
assertTrue(response.body()!!.string().contains("CN=User Cert,O=Internet Widgits Pty Ltd,ST=Some-State,C=CA"))
}
}
@Test
fun testUpgradeTLS() {
val s = factory.createSocket(server.hostName, server.port)
assertTrue(s is SSLSocket)
val ssl = s as SSLSocket
assertFalse(contains(ssl.enabledProtocols, "SSLv3"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.1"))
assertTrue(contains(ssl.enabledProtocols, "TLSv1.2"))
}
private fun readResource(name: String): ByteArray {
javaClass.classLoader!!.getResourceAsStream(name).use {
return IOUtils.toByteArray(it)
}
}
}
/*
* Copyright © Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.cert4android
import android.os.Build
import android.util.Log
import java.io.IOException
import java.net.InetAddress
import java.net.Socket
import java.security.GeneralSecurityException
import java.util.*
import javax.net.ssl.*
/**
* Custom TLS socket factory with support for
* - enabling/disabling algorithms depending on the Android version,
* - client certificate authentication
*
* @param keyManager a key manager which provides client certificates, or null
* @param trustManager trust manager to use (most likely a [CustomCertManager] instance)
*/
class CertTlsSocketFactory(
keyManager: KeyManager?,
trustManager: X509TrustManager
): SSLSocketFactory() {
private var delegate: SSLSocketFactory
companion object {
// Android 5.0+ (API level 21) provides reasonable default settings
// but it still allows SSLv3
// https://developer.android.com/reference/javax/net/ssl/SSLSocket.html
var protocols: Array<String>? = null
var cipherSuites: Array<String>? = null
init {
if (Build.VERSION.SDK_INT >= 23) {
// Since Android 6.0 (API level 23),
// - TLSv1.1 and TLSv1.2 is enabled by default
// - SSLv3 is disabled by default
// - all modern ciphers are activated by default
protocols = null
cipherSuites = null
Log.d(Constants.TAG, "Using device default TLS protocols/ciphers")
} else {
(SSLSocketFactory.getDefault().createSocket() as? SSLSocket)?.use { socket ->
try {
/* set reasonable protocol versions */
// - enable all supported protocols (enables TLSv1.1 and TLSv1.2 on Android <5.0)
// - remove all SSL versions (especially SSLv3) because they're insecure now
val whichProtocols = LinkedList<String>()
for (protocol in socket.supportedProtocols.filterNot { it.contains("SSL", true) })
whichProtocols += protocol
Log.i(Constants.TAG, "Enabling (only) these TLS protocols: ${whichProtocols.joinToString(", ")}")
protocols = whichProtocols.toTypedArray()
/* set up reasonable cipher suites */
val knownCiphers = arrayOf(
// TLS 1.2
"TLS_RSA_WITH_AES_256_GCM_SHA384",
"TLS_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
// maximum interoperability
"TLS_RSA_WITH_3DES_EDE_CBC_SHA",
"SSL_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_RSA_WITH_AES_128_CBC_SHA",
// additionally
"TLS_RSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA"
)
val availableCiphers = socket.supportedCipherSuites
Log.i(Constants.TAG, "Available cipher suites: ${availableCiphers.joinToString(", ")}")
/* For maximum security, preferredCiphers should *replace* enabled ciphers (thus
* disabling ciphers which are enabled by default, but have become unsecure), but for
* the security level of DAVx5 and maximum compatibility, disabling of insecure
* ciphers should be a server-side task */
// for the final set of enabled ciphers, take the ciphers enabled by default, ...
val whichCiphers = LinkedList<String>()
whichCiphers.addAll(socket.enabledCipherSuites)
Log.i(Constants.TAG, "Cipher suites enabled by default: ${whichCiphers.joinToString(", ")}")
// ... add explicitly allowed ciphers ...
whichCiphers.addAll(knownCiphers)
// ... and keep only those which are actually available
whichCiphers.retainAll(availableCiphers)
Log.i(Constants.TAG, "Enabling (only) these TLS ciphers: " + whichCiphers.joinToString(", "))
cipherSuites = whichCiphers.toTypedArray()
} catch (e: IOException) {
Log.e(Constants.TAG, "Couldn't determine default TLS settings")
}
}
}
}
}
init {
try {
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(
if (keyManager != null) arrayOf(keyManager) else null,
arrayOf(trustManager),
null)
delegate = sslContext.socketFactory
} catch (e: GeneralSecurityException) {
throw IllegalStateException() // system has no TLS
}
}
override fun getDefaultCipherSuites(): Array<String>? = cipherSuites ?: delegate.defaultCipherSuites
override fun getSupportedCipherSuites(): Array<String>? = cipherSuites ?: delegate.supportedCipherSuites
override fun createSocket(s: Socket, host: String, port: Int, autoClose: Boolean): Socket {
val ssl = delegate.createSocket(s, host, port, autoClose)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: String, port: Int): Socket {
val ssl = delegate.createSocket(host, port)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
val ssl = delegate.createSocket(host, port, localHost, localPort)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(host: InetAddress, port: Int): Socket {
val ssl = delegate.createSocket(host, port)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
val ssl = delegate.createSocket(address, port, localAddress, localPort)
if (ssl is SSLSocket)
upgradeTLS(ssl)
return ssl
}
private fun upgradeTLS(ssl: SSLSocket) {
protocols?.let { ssl.enabledProtocols = it }
cipherSuites?.let { ssl.enabledCipherSuites = it }
}
}
......@@ -12,23 +12,27 @@ import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import org.conscrypt.Conscrypt
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.security.KeyStore
import java.security.KeyStoreException
import java.security.Security
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.*
import java.util.logging.Level
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
/**
* The service which manages the certificates. Communications with
* the [CustomCertManager]s over IPC.
* the [CustomCertManager]s over IPC. Initializes Conscrypt when class is loaded.
*
* This services is both a started and a bound service.
*/
......@@ -51,6 +55,18 @@ class CustomCertService: Service() {
const val KEYSTORE_DIR = "KeyStore"
const val KEYSTORE_NAME = "KeyStore.bks"
init {
// initialize Conscrypt
Security.insertProviderAt(Conscrypt.newProvider(), 1)
val version = Conscrypt.version()
Log.i(Constants.TAG, "Using Conscrypt/${version.major()}.${version.minor()}.${version.patch()} for TLS")
val engine = SSLContext.getDefault().createSSLEngine()
Log.i(Constants.TAG, "Enabled protocols: ${engine.enabledProtocols.joinToString(", ")}")
Log.i(Constants.TAG, "Enabled ciphers: ${engine.enabledCipherSuites.joinToString(", ")}")
}
}
private lateinit var keyStoreFile: File
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment