Skip to content

Commit

Permalink
Merge pull request #123 from mattermost/invalid-ssl
Browse files Browse the repository at this point in the history
feat: emit event when server certificate is not trusted
  • Loading branch information
enahum authored Sep 5, 2023
2 parents 8796671 + 0b7c85d commit 06ecfd6
Show file tree
Hide file tree
Showing 3 changed files with 91 additions and 9 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
)
}

Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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<X509Certificate>, authType: String) {
defaultTrustManager.checkClientTrusted(chain, authType)
}

@Throws(CertificateException::class)
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
try {
defaultTrustManager.checkServerTrusted(chain, authType)
} catch (ce: CertificateException) {
emitInvalidCertificateError()
throw ce
}
}

override fun getAcceptedIssuers(): Array<X509Certificate> {
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
Expand All @@ -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")) {
Expand All @@ -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()

Expand Down
15 changes: 15 additions & 0 deletions ios/APIClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,23 @@ 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
}
}

var errorDescription: String? {
switch self {
case .ClientCertificateMissing:
return "Failed to authenticate: missing client certificate"
case .ServerCertificateInvalid:
return "Invalid or not trusted server certificate"
}
}
}
Expand Down Expand Up @@ -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)
Expand Down

0 comments on commit 06ecfd6

Please sign in to comment.