Skip to content

Commit 701ae60

Browse files
authored
Fix Conscrypt crash on accepting/rejecting certain certificates (#48)
* Make Conscrypt initialization explicit * Refactor Conscrypt initialization and improve test structure. Add test for crashing X509Certificate.toString(). * Downgrade Conscrypt version to 2.5.2 until issue #1268 is resolved * Fix unit tests * Fix unit tests * Explicitly provide crashing expired certificate and minor changes * CustomCertStore: find certificate alias instead of relying on tag for removal
1 parent 2b73a24 commit 701ae60

File tree

13 files changed

+244
-127
lines changed

13 files changed

+244
-127
lines changed

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ androidx-lifecycle = "2.8.7"
77
androidx-test-core = "1.6.1"
88
androidx-test-runner = "1.6.2"
99
androidx-test-rules = "1.6.1"
10-
conscrypt = "2.5.3"
10+
# don't update to 2.5.3 / until https://github.com/google/conscrypt/issues/1268 is fixed
11+
conscrypt = "2.5.2"
1112
compose-bom = "2025.01.01"
1213
dokka = "1.9.20"
1314
junit = "4.13.2"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package at.bitfire.cert4android
2+
3+
import org.junit.Before
4+
import org.junit.Test
5+
import java.security.cert.CertificateFactory
6+
import java.security.cert.X509Certificate
7+
8+
class ConscryptTest {
9+
10+
@Before
11+
fun setUp() {
12+
ConscryptIntegration.initialize()
13+
}
14+
15+
16+
@Test
17+
fun test_X509Certificate_toString() {
18+
val certFactory = CertificateFactory.getInstance("X.509")
19+
val testCert = certFactory.generateCertificate(RAW_EXPIRED_CERT.byteInputStream()) as X509Certificate
20+
21+
// Crashes with Conscrypt 2.5.3
22+
System.err.println(testCert.toString())
23+
}
24+
25+
26+
companion object {
27+
28+
const val RAW_EXPIRED_CERT = "-----BEGIN CERTIFICATE-----\n" +
29+
"MIIFdDCCBFygAwIBAgIQJ2buVutJ846r13Ci/ITeIjANBgkqhkiG9w0BAQwFADBv\n" +
30+
"MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk\n" +
31+
"ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF\n" +
32+
"eHRlcm5hbCBDQSBSb290MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFow\n" +
33+
"gYUxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAO\n" +
34+
"BgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9ETyBDQSBMaW1pdGVkMSswKQYD\n" +
35+
"VQQDEyJDT01PRE8gUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkq\n" +
36+
"hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAkehUktIKVrGsDSTdxc9EZ3SZKzejfSNw\n" +
37+
"AHG8U9/E+ioSj0t/EFa9n3Byt2F/yUsPF6c947AEYe7/EZfH9IY+Cvo+XPmT5jR6\n" +
38+
"2RRr55yzhaCCenavcZDX7P0N+pxs+t+wgvQUfvm+xKYvT3+Zf7X8Z0NyvQwA1onr\n" +
39+
"ayzT7Y+YHBSrfuXjbvzYqOSSJNpDa2K4Vf3qwbxstovzDo2a5JtsaZn4eEgwRdWt\n" +
40+
"4Q08RWD8MpZRJ7xnw8outmvqRsfHIKCxH2XeSAi6pE6p8oNGN4Tr6MyBSENnTnIq\n" +
41+
"m1y9TBsoilwie7SrmNnu4FGDwwlGTm0+mfqVF9p8M1dBPI1R7Qu2XK8sYxrfV8g/\n" +
42+
"vOldxJuvRZnio1oktLqpVj3Pb6r/SVi+8Kj/9Lit6Tf7urj0Czr56ENCHonYhMsT\n" +
43+
"8dm74YlguIwoVqwUHZwK53Hrzw7dPamWoUi9PPevtQ0iTMARgexWO/bTouJbt7IE\n" +
44+
"IlKVgJNp6I5MZfGRAy1wdALqi2cVKWlSArvX31BqVUa/oKMoYX9w0MOiqiwhqkfO\n" +
45+
"KJwGRXa/ghgntNWutMtQ5mv0TIZxMOmm3xaG4Nj/QN370EKIf6MzOi5cHkERgWPO\n" +
46+
"GHFrK+ymircxXDpqR+DDeVnWIBqv8mqYqnK8V0rSS527EPywTEHl7R09XiidnMy/\n" +
47+
"s1Hap0flhFMCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g\n" +
48+
"JMtUGjAdBgNVHQ4EFgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQD\n" +
49+
"AgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQGA1UdHwQ9\n" +
50+
"MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVzdEV4dGVy\n" +
51+
"bmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGGGWh0dHA6\n" +
52+
"Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEMBQADggEBAGS/g/FfmoXQ\n" +
53+
"zbihKVcN6Fr30ek+8nYEbvFScLsePP9NDXRqzIGCJdPDoCpdTPW6i6FtxFQJdcfj\n" +
54+
"Jw5dhHk3QBN39bSsHNA7qxcS1u80GH4r6XnTq1dFDK8o+tDb5VCViLvfhVdpfZLY\n" +
55+
"Uspzgb8c8+a4bmYRBbMelC1/kZWSWfFMzqORcUx8Rww7Cxn2obFshj5cqsQugsv5\n" +
56+
"B5a6SE2Q8pTIqXOi6wZ7I53eovNNVZ96YUWYGGjHXkBrI/V5eu+MtWuLt29G9Hvx\n" +
57+
"PUsE2JOAWVrgQSQdso8VYFhH2+9uRv0V9dlfmrPb2LjkQLPNlzmuhbsdjrzch5vR\n" +
58+
"pu/xO28QOG8=\n" +
59+
"-----END CERTIFICATE-----\n"
60+
61+
}
62+
63+
}

lib/src/androidTest/java/at/bitfire/cert4android/CustomCertManagerTest.kt

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,39 +12,24 @@ import java.io.IOException
1212
import java.net.URL
1313
import java.security.cert.CertificateException
1414
import java.security.cert.X509Certificate
15-
import javax.net.ssl.HttpsURLConnection
1615

1716
class CustomCertManagerTest {
1817

19-
companion object {
20-
private fun getSiteCertificates(url: URL): List<X509Certificate> {
21-
val conn = url.openConnection() as HttpsURLConnection
22-
try {
23-
conn.inputStream.read()
24-
val certs = mutableListOf<X509Certificate>()
25-
conn.serverCertificates.forEach { certs += it as X509Certificate }
26-
return certs
27-
} finally {
28-
conn.disconnect()
29-
}
30-
}
31-
}
32-
3318
private val context by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
3419

3520
private lateinit var certManager: CustomCertManager
3621
private lateinit var paranoidCertManager: CustomCertManager
3722

38-
private var siteCerts: List<X509Certificate>? = null
39-
init {
23+
private var siteCerts: List<X509Certificate>? =
4024
try {
41-
siteCerts = getSiteCertificates(URL("https://www.davx5.com"))
25+
TestCertificates.getSiteCertificates(URL("https://www.davx5.com"))
4226
} catch(_: IOException) {
27+
null
4328
}
44-
assumeNotNull(siteCerts)
29+
init {
30+
assumeNotNull("Couldn't load certificate from Web", siteCerts)
4531
}
4632

47-
4833
@Before
4934
fun createCertManager() {
5035
certManager = CustomCertManager(context, true, null)
@@ -85,6 +70,8 @@ class CustomCertManagerTest {
8570
}
8671

8772

73+
// helpers
74+
8875
private fun addTrustedCertificate() {
8976
CustomCertStore.getInstance(context).setTrustedByUser(siteCerts!!.first())
9077
}
@@ -93,4 +80,4 @@ class CustomCertManagerTest {
9380
CustomCertStore.getInstance(context).setUntrustedByUser(siteCerts!!.first())
9481
}
9582

96-
}
83+
}

lib/src/androidTest/java/at/bitfire/cert4android/CustomCertStoreTest.kt

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,13 @@ import androidx.test.platform.app.InstrumentationRegistry
88
import org.junit.Assert.*
99
import org.junit.Before
1010
import org.junit.Test
11-
import java.security.cert.CertificateFactory
12-
import java.security.cert.X509Certificate
1311

1412
class CustomCertStoreTest {
1513

1614
val context = InstrumentationRegistry.getInstrumentation().targetContext
1715
val certStore = CustomCertStore.getInstance(context)
1816

19-
val testCert = TestCertificate.testCert
20-
17+
val testCert = TestCertificates.testCert
2118

2219
@Before
2320
fun clearKeys() {
@@ -41,8 +38,9 @@ class CustomCertStoreTest {
4138
// test whether cert was stored to disk:
4239
// create another cert store to make sure data is loaded from disk again
4340
val anotherCertStore = CustomCertStore(context)
44-
assertEquals("4694b8d19ee1b087a21aa2383dddf7bc82baf4fc19ca17c6c064fad45a37085c8404073ccbef7736423797a6ddfc49fc500906f3b9c4eaab01c5cf4b79d16169b75c82c81fac437cbea36186e72e2f21cf3f30a2f82dac0091e0006a0a120adfae6bdd8c26146b54c99e0e7d9f95b9abc4a69987ff004a247acf2d17c0ee6774ce4d7b763303c521b699883ca7e89d7e3d89843ccc68a9b1b94c3624a31e14e3ecce0c3b74d061419659fdd7ce9e01b74a3d22244bcb25025079ca5060fd3946fcb9b343fe444aa213c97493268e5148b090df6886e3aac5d9a3d1715afbd435aa6d5f98770908dcdec4c1938820497a6bdb2b17eab4d4bac8ba45a449cb94f5",
45-
anotherCertStore.userKeyStore.aliases().nextElement())
41+
assertTrue(anotherCertStore.userKeyStore.aliases().toList().any { alias ->
42+
anotherCertStore.isTrustedByUser(testCert)
43+
})
4644
}
4745

4846
@Test
@@ -59,5 +57,4 @@ class CustomCertStoreTest {
5957
assertFalse(anotherCertStore.isTrustedByUser(testCert))
6058
}
6159

62-
6360
}

lib/src/androidTest/java/at/bitfire/cert4android/TestCertificate.kt

Lines changed: 0 additions & 30 deletions
This file was deleted.
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package at.bitfire.cert4android
2+
3+
import android.net.SSLCertificateSocketFactory
4+
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
5+
import java.net.URL
6+
import java.security.cert.CertificateFactory
7+
import java.security.cert.X509Certificate
8+
import javax.net.ssl.HttpsURLConnection
9+
import javax.net.ssl.X509TrustManager
10+
11+
/**
12+
* Provides certificates for testing.
13+
*/
14+
object TestCertificates {
15+
16+
init {
17+
ConscryptIntegration.initialize()
18+
}
19+
20+
val certFactory = CertificateFactory.getInstance("X.509")
21+
22+
const val RAW_TEST_CERT = "-----BEGIN CERTIFICATE-----\n" +
23+
"MIICxzCCAa+gAwIBAgIUe7x8TfMqQlJ+qTF/L+n6NqRqKAwwDQYJKoZIhvcNAQEL\n" +
24+
"BQAwDjEMMAoGA1UEAwwDdG50MB4XDTIwMDIxODE5NTYyMFoXDTMwMDIxNTE5NTYy\n" +
25+
"MFowDjEMMAoGA1UEAwwDdG50MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC\n" +
26+
"AQEAzqBnVAWwIp8oOrcEJzplOLzd8ZKscmrJ0oxMf7oiYUE5+D3I1IjsCIeUdwfH\n" +
27+
"9zDKmNo3lwyFUTjw2WotssawmEH0LGfabi6h/1bbFG4QOQC7KYMtD8tqzR6F0WVe\n" +
28+
"oZ5uM5VvQB+dRvlq2CdWb8NcBkyd2pQ8RvJsft3fWY5ix3CL+OZ8lXOGqkxN780m\n" +
29+
"RkrxdYog3fvWC1CMSix8Y28q4JwRRAMP0JhBGIdFpnEPLowh6HhhPRiwAn+ksSey\n" +
30+
"Rz39AsUErUUCD7soCsDSzu80uieF9enEVweqnn/ayPhJlX0Drw7UwrC88UqdLqRD\n" +
31+
"Da/ucJYzKkMgHZJ7EXNh2WZpbwIDAQABox0wGzAJBgNVHRMEAjAAMA4GA1UdEQQH\n" +
32+
"MAWCA3RudDANBgkqhkiG9w0BAQsFAAOCAQEARpS40Z7hsIeiGqI4Pd33vIK69PwZ\n" +
33+
"yhfGwGT61Fo3CFyEBAc8y+93NkI3l6bd/En8UAkG87nE6qsBxc9LedFhabdcgsgf\n" +
34+
"rEN8vqNhhucuLyHPPzCi+C2sAJHgAGoKEgrfrmvdjCYUa1TJng59n5W5q8SmmYf/\n" +
35+
"AEokes8tF8DuZ3TOTXt2MwPFIbaZiDyn6J1+PYmEPMxoqbG5TDYkox4U4+zODDt0\n" +
36+
"0GFBlln9186eAbdKPSIkS8slAlB5ylBg/TlG/LmzQ/5ESqITyXSTJo5RSLCQ32iG\n" +
37+
"46rF2aPRcVr71DWqbV+YdwkI3N7EwZOIIEl6a9srF+q01LrIukWkScuU9Q==\n" +
38+
"-----END CERTIFICATE-----\n"
39+
40+
/** some test certificate (untrusted Snakeoil certificate generated by Debian) */
41+
val testCert = certFactory.generateCertificate(RAW_TEST_CERT.byteInputStream()) as X509Certificate
42+
43+
44+
/**
45+
* Get the certificates of a site (bypassing all trusted checks).
46+
*
47+
* @param url the URL to get the certificates from
48+
* @return the certificates of the site
49+
*/
50+
fun getSiteCertificates(url: URL): List<X509Certificate> {
51+
val conn = url.openConnection() as HttpsURLConnection
52+
try {
53+
conn.hostnameVerifier = AllowAllHostnameVerifier()
54+
conn.sslSocketFactory = object : SSLCertificateSocketFactory(1000) {
55+
init {
56+
setTrustManagers(arrayOf(object : X509TrustManager {
57+
override fun checkClientTrusted(
58+
chain: Array<out X509Certificate?>?,
59+
authType: String?
60+
) { /* OK */ }
61+
override fun checkServerTrusted(
62+
chain: Array<out X509Certificate?>?,
63+
authType: String?
64+
) { /* OK */ }
65+
override fun getAcceptedIssuers(): Array<out X509Certificate?>? = emptyArray()
66+
}))
67+
}
68+
}
69+
conn.inputStream.read()
70+
val certs = mutableListOf<X509Certificate>()
71+
conn.serverCertificates.forEach { certs += it as X509Certificate }
72+
return certs
73+
} finally {
74+
conn.disconnect()
75+
}
76+
}
77+
78+
}

lib/src/androidTest/java/at/bitfire/cert4android/UserDecisionRegistryTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class UserDecisionRegistryTest {
2222
private val certStore = CustomCertStore.getInstance(context)
2323
private val registry = UserDecisionRegistry.getInstance(context)
2424

25-
private val testCert = TestCertificate.testCert
25+
private val testCert = TestCertificates.testCert
2626

2727

2828
@Before

lib/src/main/java/at/bitfire/cert4android/CertUtils.kt

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ import javax.net.ssl.X509TrustManager
1515

1616
object CertUtils {
1717

18-
fun fingerprint(cert: X509Certificate, algorithm: String): String {
19-
val md = MessageDigest.getInstance(algorithm)
20-
return hexString(md.digest(cert.encoded))
21-
}
22-
2318
fun getTrustManager(keyStore: KeyStore?): X509TrustManager? {
2419
try {
2520
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
@@ -33,21 +28,23 @@ object CertUtils {
3328
return null
3429
}
3530

36-
fun getTag(cert: X509Certificate): String {
37-
val str = StringBuilder()
38-
for (b in cert.signature)
39-
str.append(String.format(Locale.ROOT, "%02x", b))
40-
return str.toString()
31+
32+
fun getTag(cert: X509Certificate) =
33+
fingerprint(cert, "SHA-512", separator = null)
34+
35+
fun fingerprint(cert: X509Certificate, algorithm: String, separator: Char? = ':'): String {
36+
val md = MessageDigest.getInstance(algorithm)
37+
return hexString(md.digest(cert.encoded), separator)
4138
}
4239

43-
fun hexString(data: ByteArray): String {
40+
fun hexString(data: ByteArray, separator: Char? = ':'): String {
4441
val str = StringBuilder()
4542
for ((idx, b) in data.withIndex()) {
46-
if (idx != 0)
47-
str.append(':')
43+
if (idx != 0 && separator != null)
44+
str.append(separator)
4845
str.append(String.format("%02x", b).uppercase(Locale.ROOT))
4946
}
5047
return str.toString()
5148
}
5249

53-
}
50+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package at.bitfire.cert4android
2+
3+
import org.conscrypt.Conscrypt
4+
import java.security.Security
5+
import javax.net.ssl.SSLContext
6+
7+
object ConscryptIntegration {
8+
9+
private var initialized = false
10+
11+
@Synchronized
12+
fun initialize() {
13+
if (initialized)
14+
return
15+
16+
// initialize Conscrypt
17+
Security.insertProviderAt(Conscrypt.newProvider(), 1)
18+
19+
val version = Conscrypt.version()
20+
Cert4Android.log.info("Using Conscrypt/${version.major()}.${version.minor()}.${version.patch()} for TLS")
21+
22+
val engine = SSLContext.getDefault().createSSLEngine()
23+
Cert4Android.log.info("Enabled protocols: ${engine.enabledProtocols.joinToString(", ")}")
24+
Cert4Android.log.info("Enabled ciphers: ${engine.enabledCipherSuites.joinToString(", ")}")
25+
26+
initialized = true
27+
}
28+
29+
}

lib/src/main/java/at/bitfire/cert4android/CustomCertManager.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import javax.net.ssl.X509TrustManager
1515
/**
1616
* TrustManager to handle custom certificates.
1717
*
18+
* Initializes Conscrypt when it is first loaded.
19+
*
1820
* @param trustSystemCerts whether system certificates will be trusted
1921
* @param appInForeground - `true`: if needed, directly launches [TrustCertificateActivity] and shows notification (if possible)
2022
* - `false`: if needed, shows notification (if possible)
@@ -78,4 +80,14 @@ class CustomCertManager @JvmOverloads constructor(
7880

7981
}
8082

83+
84+
companion object {
85+
86+
init {
87+
// On first load of this class, initialize Conscrypt.
88+
ConscryptIntegration.initialize()
89+
}
90+
91+
}
92+
8193
}

0 commit comments

Comments
 (0)