Welcome to mirror list, hosted at ThFree Co, Russian Federation.

CustomCertManager.kt « cert4android « bitfire « at « java « main « src - github.com/bitfireAT/cert4android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 4e5df4a5b57bf248afef44e23da634791f3e4ce7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
/*
 * 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.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.os.Looper
import java.io.Closeable
import java.security.cert.CertificateException
import java.security.cert.X509Certificate
import java.util.logging.Level
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLPeerUnverifiedException
import javax.net.ssl.SSLSession
import javax.net.ssl.X509TrustManager

/**
 * TrustManager to handle custom certificates. Communicates with
 * [CustomCertService] to fetch information about custom certificate
 * trustworthiness. The IPC with a service is required when multiple processes,
 * each of them with an own [CustomCertManager], want to access a synchronized central
 * certificate trust store + UI (for accepting certificates etc.).
 *
 * @param context used to bind to [CustomCertService]
 * @param interactive true: users will be notified in case of unknown certificates;
 *                    false: unknown certificates will be rejected (only uses custom certificate key store)
 * @param trustSystemCerts whether system certificates will be trusted
 * @param appInForeground  Whether to launch [TrustCertificateActivity] directly. The notification will always be shown.
 *
 * @constructor Creates a new instance, using a certain [CustomCertService] messenger (for testing).
 * Must not be run from the main thread because this constructor may request binding to [CustomCertService].
 * The actual binding code is called by the looper in the main thread, so waiting for the
 * service would block forever.
 *
 * @throws IllegalStateException if run from main thread
 */
class CustomCertManager @JvmOverloads constructor(
        val context: Context,
        val interactive: Boolean = true,
        trustSystemCerts: Boolean = true,

        @Volatile
        var appInForeground: Boolean = false
): X509TrustManager, Closeable {

    companion object {

        /** how long to wait for a decision from [CustomCertService] before giving up temporarily */
        var SERVICE_TIMEOUT: Long = 3*60*1000

        fun resetCertificates(context: Context): Boolean {
            val intent = Intent(context, CustomCertService::class.java)
            intent.action = CustomCertService.CMD_RESET_CERTIFICATES
            return context.startService(intent) != null
        }

    }

    var service: ICustomCertService? = null
    private var serviceConn: ServiceConnection? = null
    private var serviceLock = Object()

    /** system-default trust store */
    private val systemTrustManager: X509TrustManager? =
            if (trustSystemCerts) CertUtils.getTrustManager(null) else null


    init {
        val newServiceConn = object: ServiceConnection {
            override fun onServiceConnected(className: ComponentName, binder: IBinder) {
                Constants.log.fine("Connected to service")
                synchronized(serviceLock) {
                    this@CustomCertManager.service = ICustomCertService.Stub.asInterface(binder)
                    serviceLock.notify()
                }
            }

            override fun onServiceDisconnected(className: ComponentName) {
                synchronized(serviceLock) {
                    this@CustomCertManager.service = null
                }
            }
        }

        check(Looper.myLooper() != Looper.getMainLooper()) {
            // service is actually created after bindService() by code running in looper, so this would block
            "must not be run on main thread"
        }

        Constants.log.fine("Binding to service")
        if (context.bindService(Intent(context, CustomCertService::class.java), newServiceConn, Context.BIND_AUTO_CREATE)) {
            serviceConn = newServiceConn

            Constants.log.fine("Waiting for service to be bound")
            synchronized(serviceLock) {
                while (service == null)
                    try {
                        serviceLock.wait()
                    } catch(e: InterruptedException) {
                    }
            }
        } else
            Constants.log.severe("Couldn't bind CustomCertService to context")
    }

    override fun close() {
        serviceConn?.let {
            try {
                context.unbindService(it)
                serviceConn = null
            } catch (e: Exception) {
                Constants.log.log(Level.WARNING, "Couldn't unbind CustomCertService", e)
            }
        }
    }


    @Throws(CertificateException::class)
    override fun checkClientTrusted(chain: Array<X509Certificate>?, authType: String?) {
        throw CertificateException("cert4android doesn't validate client certificates")
    }

    /**
     * Checks whether a certificate is trusted. If {@link #systemTrustManager} is null (because
     * system certificates are not being trusted or available), the first certificate in the chain
     * (which is the lowest one, i.e. the actual server certificate) is passed to
     * {@link CustomCertService} for further decision.
     * @param chain        certificate chain to check
     * @param authType     authentication type (ignored)
     * @throws CertificateException in case of an untrusted or questionable certificate
     */
    @Throws(CertificateException::class)
    override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
        var trusted = false

        systemTrustManager?.let {
            try {
                it.checkServerTrusted(chain, authType)
                trusted = true
            } catch(ignored: CertificateException) {
                Constants.log.fine("Certificate not trusted by system")
            }
        }

        if (!trusted)
            // not trusted by system, let's check ourselves
            checkCustomTrusted(chain[0])
    }

    internal fun checkCustomTrusted(cert: X509Certificate) {
        val svc = service ?: throw CertificateException("Not bound to CustomCertService")

        val lock = Object()
        var valid: Boolean? = null

        val callback = object: IOnCertificateDecision.Stub() {
            override fun accept() {
                synchronized(lock) {
                    valid = true
                    lock.notify()
                }
            }
            override fun reject() {
                synchronized(lock) {
                    valid = false
                    lock.notify()
                }
            }
        }

        try {
            svc.checkTrusted(cert.encoded, interactive, appInForeground, callback)
            synchronized(lock) {
                if (valid == null) {
                    Constants.log.fine("Waiting for reply from service")
                    try {
                        lock.wait(SERVICE_TIMEOUT)
                    } catch(e: InterruptedException) {
                    }
                }
            }
        } catch(e: Exception) {
            throw CertificateException("Couldn't check certificate", e)
        }

        when (valid) {
            null -> {
                svc.abortCheck(callback)
                throw CertificateException("Timeout when waiting for certificate trustworthiness decision")
            }

            true -> { /* OK */ }
            false -> throw CertificateException("Certificate not accepted by CustomCertService")
        }
    }

    override fun getAcceptedIssuers() = arrayOf<X509Certificate>()


    // custom methods

    fun hostnameVerifier(defaultVerifier: HostnameVerifier?) = CustomHostnameVerifier(defaultVerifier)


    // hostname verifier

    inner class CustomHostnameVerifier(
            private val defaultVerifier: HostnameVerifier?
    ): HostnameVerifier {

        override fun verify(host: String, sslSession: SSLSession): Boolean {
            Constants.log.fine("Verifying certificate for $host")

            if (defaultVerifier?.verify(host, sslSession) == true)
                return true

            // default hostname verifier couldn't verify the hostname →
            // accept the hostname as verified only if the certificate has been accepted be the user

            try {
                val cert = sslSession.peerCertificates
                if (cert.isNotEmpty() && cert[0] is X509Certificate) {
                    checkCustomTrusted(cert[0] as X509Certificate)
                    Constants.log.fine("Certificate is in custom trust store, accepting")
                    return true
                }
            } catch(e: SSLPeerUnverifiedException) {
                Constants.log.log(Level.WARNING, "Couldn't get certificate for host name verification", e)
            } catch (ignored: CertificateException) {
            }

            return false
        }

    }

}