diff --git a/README.md b/README.md index d22bd8515..89ee42aa4 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ For both API client and WebSocket client, the following error codes apply: | Code | Reason | | ---- | -------------------------------------------------------- | | -200 | SSL handshake failed due to a missing client certificate | + | -299 | Server SSL certificate is not trusted or invalid | ## Method Swizzling diff --git a/android/src/main/java/com/mattermost/networkclient/NetworkClientBase.kt b/android/src/main/java/com/mattermost/networkclient/NetworkClientBase.kt index 150a15084..7103af75e 100644 --- a/android/src/main/java/com/mattermost/networkclient/NetworkClientBase.kt +++ b/android/src/main/java/com/mattermost/networkclient/NetworkClientBase.kt @@ -18,9 +18,14 @@ import org.json.JSONArray import org.json.JSONObject import java.io.IOException import java.net.URI +import java.security.SecureRandom +import java.security.cert.CertificateException +import java.security.cert.X509Certificate import java.util.* -import kotlin.collections.ArrayList -import kotlin.collections.HashMap +import javax.net.ssl.HttpsURLConnection +import javax.net.ssl.SSLContext +import javax.net.ssl.SSLSocketFactory +import javax.net.ssl.X509TrustManager import kotlin.reflect.KProperty internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { @@ -75,9 +80,14 @@ internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { val handshakeCertificates = buildHandshakeCertificates(options) if (handshakeCertificates != null) { + val sslContext = SSLContext.getInstance("TLS") + val trustManager = getTrustManager(handshakeCertificates.trustManager) + val trustManagers = arrayOf(trustManager) + sslContext.init(null, trustManagers, SecureRandom()) + builder.sslSocketFactory( - handshakeCertificates.sslSocketFactory(), - handshakeCertificates.trustManager + sslContext.socketFactory, + getTrustManager(handshakeCertificates.trustManager) ) } @@ -115,11 +125,24 @@ internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { val handshakeCertificates = buildHandshakeCertificates() if (handshakeCertificates != null) { + val sslContext = SSLContext.getInstance("TLS") + val trustManager = getTrustManager(handshakeCertificates.trustManager) + val trustManagers = arrayOf(trustManager) + sslContext.init(null, trustManagers, SecureRandom()) + okHttpClient = okHttpClient.newBuilder() .sslSocketFactory( - handshakeCertificates.sslSocketFactory(), - handshakeCertificates.trustManager + sslContext.socketFactory, + getTrustManager(handshakeCertificates.trustManager) ) + .hostnameVerifier {hostname, session -> + val hv = HttpsURLConnection.getDefaultHostnameVerifier() + val result = hv.verify(hostname, session) + if (!result) { + emitInvalidCertificateError() + } + result + } .build() } } @@ -380,6 +403,37 @@ internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { return null } + internal fun getTrustManager(defaultTrustManager: X509TrustManager): X509TrustManager { + return object : X509TrustManager { + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + defaultTrustManager.checkClientTrusted(chain, authType) + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + try { + defaultTrustManager.checkServerTrusted(chain, authType) + } catch (ce: CertificateException) { + emitInvalidCertificateError() + throw ce + } + } + + override fun getAcceptedIssuers(): Array { + return defaultTrustManager.acceptedIssuers + } + } + } + + internal fun emitInvalidCertificateError() { + val data = Arguments.createMap() + data.putString("serverUrl", BASE_URL_STRING) + data.putInt("errorCode", -299) + data.putString("errorDescription", "The certificate for this server is invalid.\nYou might be connecting to a server that is pretending to be “${URI(BASE_URL_STRING).host}” which could put your confidential information at risk.") + APIClientModule.sendJSEvent(APIClientEvents.CLIENT_ERROR.event, data) + } + internal fun buildHandshakeCertificates(options: ReadableMap?): HandshakeCertificates? { if (options != null) { // `trustSelfSignedServerCertificate` can be in `options.sessionConfiguration` for @@ -390,11 +444,15 @@ internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { sessionConfiguration.getBoolean("trustSelfSignedServerCertificate")) { trustSelfSignedServerCertificate = true builder.hostnameVerifier { _, _ -> true } + } else { + buildHostnameVerifier() } } else if (options.hasKey("trustSelfSignedServerCertificate") && options.getBoolean("trustSelfSignedServerCertificate")) { trustSelfSignedServerCertificate = true builder.hostnameVerifier { _, _ -> true } + } else { + buildHostnameVerifier() } if (options.hasKey("clientP12Configuration")) { @@ -420,15 +478,23 @@ internal open class NetworkClientBase(private val baseUrl: HttpUrl? = null) { return buildHandshakeCertificates() } + private fun buildHostnameVerifier() { + builder.hostnameVerifier {hostname, session -> + val hv = HttpsURLConnection.getDefaultHostnameVerifier() + val result = hv.verify(hostname, session) + if (!result) { + emitInvalidCertificateError() + } + result + } + } + private fun buildHandshakeCertificates(): HandshakeCertificates? { if (baseUrl == null) return null val (heldCertificate, intermediates) = KeyStoreHelper.getClientCertificates(P12_ALIAS) - if (!trustSelfSignedServerCertificate && heldCertificate == null) - return null - val builder = HandshakeCertificates.Builder() .addPlatformTrustedCertificates() diff --git a/ios/APIClient.swift b/ios/APIClient.swift index c78cdbd25..5234cfcec 100644 --- a/ios/APIClient.swift +++ b/ios/APIClient.swift @@ -13,12 +13,14 @@ import React enum APIClientError: Error { case ClientCertificateMissing + case ServerCertificateInvalid } extension APIClientError: LocalizedError { var errorCode: Int { switch self { case .ClientCertificateMissing: return -200 + case .ServerCertificateInvalid: return -299 } } @@ -26,6 +28,8 @@ extension APIClientError: LocalizedError { switch self { case .ClientCertificateMissing: return "Failed to authenticate: missing client certificate" + case .ServerCertificateInvalid: + return "Invalid or not trusted server certificate" } } } @@ -61,6 +65,17 @@ class APIClientSessionDelegate: SessionDelegate { completionHandler(disposition, credential) } + + override open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + if let err = error as? NSError, + let urlSession = SessionManager.default.getSession(for: session), + err.domain == NSURLErrorDomain && err.code == NSURLErrorServerCertificateUntrusted { + NotificationCenter.default.post(name: Notification.Name(API_CLIENT_EVENTS["CLIENT_ERROR"]!), + object: nil, + userInfo: ["serverUrl": urlSession.baseUrl.absoluteString, "errorCode": APIClientError.ServerCertificateInvalid.errorCode, "errorDescription": err.localizedDescription]) + super.urlSession(session, task: task, didCompleteWithError: error) + } + } } @objc(APIClient)