Skip to content

Commit 819d8d1

Browse files
committed
WIP
1 parent 46cdd7a commit 819d8d1

File tree

7 files changed

+173
-134
lines changed

7 files changed

+173
-134
lines changed

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

Lines changed: 0 additions & 83 deletions
This file was deleted.

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

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,7 @@
11
package at.bitfire.cert4android
22

3-
import android.net.SSLCertificateSocketFactory
4-
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
5-
import java.net.URL
63
import java.security.cert.CertificateFactory
74
import java.security.cert.X509Certificate
8-
import javax.net.ssl.HttpsURLConnection
9-
import javax.net.ssl.X509TrustManager
105

116
/**
127
* Provides certificates for testing.
@@ -61,39 +56,4 @@ object TestCertificates {
6156

6257
fun testCert() = certFactory.generateCertificate(RAW_TEST_CERT.byteInputStream()) as X509Certificate
6358

64-
65-
/**
66-
* Get the certificates of a site (bypassing all trusted checks).
67-
*
68-
* @param url the URL to get the certificates from
69-
* @return the certificates of the site
70-
*/
71-
fun getSiteCertificates(url: URL): List<X509Certificate> {
72-
val conn = url.openConnection() as HttpsURLConnection
73-
try {
74-
conn.hostnameVerifier = AllowAllHostnameVerifier()
75-
conn.sslSocketFactory = object : SSLCertificateSocketFactory(1000) {
76-
init {
77-
setTrustManagers(arrayOf(object : X509TrustManager {
78-
override fun checkClientTrusted(
79-
chain: Array<out X509Certificate?>?,
80-
authType: String?
81-
) { /* OK */ }
82-
override fun checkServerTrusted(
83-
chain: Array<out X509Certificate?>?,
84-
authType: String?
85-
) { /* OK */ }
86-
override fun getAcceptedIssuers(): Array<out X509Certificate?>? = emptyArray()
87-
}))
88-
}
89-
}
90-
conn.inputStream.read()
91-
val certs = mutableListOf<X509Certificate>()
92-
conn.serverCertificates.forEach { certs += it as X509Certificate }
93-
return certs
94-
} finally {
95-
conn.disconnect()
96-
}
97-
}
98-
9959
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package at.bitfire.cert4android
2+
3+
import kotlinx.coroutines.flow.StateFlow
4+
import java.security.cert.X509Certificate
5+
6+
interface CertStore {
7+
8+
/**
9+
* Removes user (dis-)trust decisions for all certificates.
10+
*/
11+
fun clearUserDecisions()
12+
13+
/**
14+
* Determines whether a certificate chain is trusted.
15+
*/
16+
fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean
17+
18+
/**
19+
* Determines whether a certificate has been explicitly accepted by the user. In this case,
20+
* we can ignore an invalid host name for that certificate.
21+
*/
22+
fun isTrustedByUser(cert: X509Certificate): Boolean
23+
24+
/**
25+
* Sets this certificate as trusted.
26+
*/
27+
fun setTrustedByUser(cert: X509Certificate)
28+
29+
/**
30+
* Sets this certificate as untrusted.
31+
*/
32+
fun setUntrustedByUser(cert: X509Certificate)
33+
34+
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
package at.bitfire.cert4android
66

77
import android.annotation.SuppressLint
8-
import android.content.Context
98
import kotlinx.coroutines.flow.StateFlow
109
import java.security.cert.CertificateException
1110
import java.security.cert.X509Certificate
@@ -25,16 +24,14 @@ import javax.net.ssl.X509TrustManager
2524
*/
2625
@SuppressLint("CustomX509TrustManager")
2726
class CustomCertManager @JvmOverloads constructor(
28-
context: Context,
27+
private val certStore: CertStore,
2928
val trustSystemCerts: Boolean = true,
3029
var appInForeground: StateFlow<Boolean>?
3130
): X509TrustManager {
3231

3332
private val logger
3433
get() = Logger.getLogger(javaClass.name)
3534

36-
val certStore = CustomCertStore.getInstance(context)
37-
3835

3936
@Throws(CertificateException::class)
4037
override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import java.util.logging.Logger
2424
class CustomCertStore internal constructor(
2525
private val context: Context,
2626
private val userTimeout: Long = 60000L
27-
) {
27+
): CertStore {
2828

2929
companion object {
3030

@@ -67,7 +67,7 @@ class CustomCertStore internal constructor(
6767
}
6868

6969
@Synchronized
70-
fun clearUserDecisions() {
70+
override fun clearUserDecisions() {
7171
logger.info("Clearing user-(dis)trusted certificates")
7272

7373
for (alias in userKeyStore.aliases())
@@ -81,7 +81,7 @@ class CustomCertStore internal constructor(
8181
/**
8282
* Determines whether a certificate chain is trusted.
8383
*/
84-
fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean {
84+
override fun isTrusted(chain: Array<X509Certificate>, authType: String, trustSystemCerts: Boolean, appInForeground: StateFlow<Boolean>?): Boolean {
8585
if (chain.isEmpty())
8686
throw IllegalArgumentException("Certificate chain must not be empty")
8787
val cert = chain[0]
@@ -131,11 +131,11 @@ class CustomCertStore internal constructor(
131131
* we can ignore an invalid host name for that certificate.
132132
*/
133133
@Synchronized
134-
fun isTrustedByUser(cert: X509Certificate): Boolean =
134+
override fun isTrustedByUser(cert: X509Certificate): Boolean =
135135
userKeyStore.getCertificateAlias(cert) != null
136136

137137
@Synchronized
138-
fun setTrustedByUser(cert: X509Certificate) {
138+
override fun setTrustedByUser(cert: X509Certificate) {
139139
val alias = CertUtils.getTag(cert)
140140
logger.info("Trusted by user: ${cert.subjectDN.name} ($alias)")
141141

@@ -146,7 +146,7 @@ class CustomCertStore internal constructor(
146146
}
147147

148148
@Synchronized
149-
fun setUntrustedByUser(cert: X509Certificate) {
149+
override fun setUntrustedByUser(cert: X509Certificate) {
150150
logger.info("Distrusted by user: ${cert.subjectDN.name}")
151151

152152
// find certificate
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/***************************************************************************************************
2+
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
3+
**************************************************************************************************/
4+
5+
package at.bitfire.cert4android
6+
7+
import kotlinx.coroutines.flow.StateFlow
8+
import org.apache.http.conn.ssl.AllowAllHostnameVerifier
9+
import org.junit.Assume.assumeNotNull
10+
import org.junit.Before
11+
import org.junit.Test
12+
import java.io.IOException
13+
import java.net.URL
14+
import java.security.cert.CertificateException
15+
import java.security.cert.X509Certificate
16+
import javax.net.ssl.HttpsURLConnection
17+
18+
class CustomCertManagerTest {
19+
20+
private val fakeCertStore = TestCertStore()
21+
private lateinit var certManager: CustomCertManager
22+
private lateinit var paranoidCertManager: CustomCertManager
23+
24+
private var siteCerts: List<X509Certificate>? =
25+
try {
26+
getSiteCertificates(URL("https://www.davx5.com"))
27+
} catch(_: IOException) {
28+
null
29+
}
30+
init {
31+
assumeNotNull("Couldn't load certificate from Web", siteCerts)
32+
}
33+
34+
@Before
35+
fun createCertManager() {
36+
certManager = CustomCertManager(fakeCertStore, true, null)
37+
paranoidCertManager = CustomCertManager(fakeCertStore, false, null)
38+
}
39+
40+
41+
@Test(expected = CertificateException::class)
42+
fun testCheckClientCertificate() {
43+
certManager.checkClientTrusted(null, null)
44+
}
45+
46+
@Test
47+
fun testTrustedCertificate() {
48+
certManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
49+
}
50+
51+
@Test(expected = CertificateException::class)
52+
fun testParanoidCertificate() {
53+
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
54+
}
55+
56+
@Test
57+
fun testAddCustomCertificate() {
58+
fakeCertStore.setTrustedByUser(siteCerts!!.first())
59+
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
60+
}
61+
62+
@Test(expected = CertificateException::class)
63+
fun testRemoveCustomCertificate() {
64+
fakeCertStore.setTrustedByUser(siteCerts!!.first())
65+
66+
// remove certificate again
67+
// should now be rejected for the whole session
68+
fakeCertStore.setUntrustedByUser(siteCerts!!.first())
69+
70+
paranoidCertManager.checkServerTrusted(siteCerts!!.toTypedArray(), "RSA")
71+
}
72+
73+
74+
// helpers
75+
76+
/**
77+
* Get the certificates of a site (bypassing all trusted checks).
78+
*
79+
* @param url the URL to get the certificates from
80+
* @return the certificates of the site
81+
*/
82+
fun getSiteCertificates(url: URL): List<X509Certificate> {
83+
val conn = url.openConnection() as HttpsURLConnection
84+
try {
85+
conn.hostnameVerifier = AllowAllHostnameVerifier()
86+
// conn.sslSocketFactory = SSLSocketFactory.getDefault()
87+
conn.inputStream.read()
88+
val certs = mutableListOf<X509Certificate>()
89+
conn.serverCertificates.forEach { certs += it as X509Certificate }
90+
return certs
91+
} finally {
92+
conn.disconnect()
93+
}
94+
}
95+
96+
class TestCertStore: CertStore {
97+
98+
/**
99+
*
100+
* true = trusted, false = untrusted
101+
*/
102+
val fakeCertTrustStore = mutableMapOf<X509Certificate, Boolean>()
103+
104+
override fun clearUserDecisions() {
105+
fakeCertTrustStore.clear()
106+
}
107+
108+
override fun isTrusted(
109+
chain: Array<X509Certificate>,
110+
authType: String,
111+
trustSystemCerts: Boolean,
112+
appInForeground: StateFlow<Boolean>?
113+
): Boolean {
114+
return true
115+
}
116+
117+
override fun isTrustedByUser(cert: X509Certificate): Boolean {
118+
return fakeCertTrustStore[cert] == true
119+
}
120+
121+
override fun setTrustedByUser(cert: X509Certificate) {
122+
fakeCertTrustStore[cert] = true
123+
}
124+
125+
override fun setUntrustedByUser(cert: X509Certificate) {
126+
fakeCertTrustStore[cert] = false
127+
}
128+
129+
}
130+
131+
}

sample-app/src/main/java/at/bitfire/cert4android/demo/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class MainActivity : ComponentActivity() {
176176

177177
// set cert4android TrustManager and HostnameVerifier
178178
val certManager = CustomCertManager(
179-
context,
179+
certStore = CustomCertStore.getInstance(context),
180180
trustSystemCerts = trustSystemCerts,
181181
appInForeground = appInForeground
182182
)

0 commit comments

Comments
 (0)