feature: add option for pinning root ca or pinning leaf ca #93#94
feature: add option for pinning root ca or pinning leaf ca #93#94weixuefeng wants to merge 8 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request renames the package to http_certificate_pinning_plus and introduces key features such as leaf or root certificate fingerprint validation, in-memory caching for successful checks, and coalescing of concurrent checks. The feedback suggests defensive checks on Android to handle empty validated certificate chains and catch CertificateException specifically for better error reporting. Additionally, it recommends forwarding HTTP headers in SecureHttpClient to prevent handshake failures on servers requiring specific headers.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| private fun getValidatedCertificateChain(host: String, serverCertificates: Array<Certificate>): List<X509Certificate> { | ||
| val certificateChain = serverCertificates.filterIsInstance<X509Certificate>().toTypedArray() | ||
| if (certificateChain.isEmpty()) { | ||
| throw CertificateException("No X509 certificates found in connection") | ||
| } | ||
| val trustManagerExtensions = this.getTrustManagerExtensions() | ||
| val authType = certificateChain.first().publicKey.algorithm | ||
| return trustManagerExtensions.checkServerTrusted(certificateChain, authType, host) | ||
| } |
There was a problem hiding this comment.
trustManagerExtensions.checkServerTrusted can theoretically return null or an empty list depending on the platform's trust manager implementation or custom configurations. To prevent potential NoSuchElementException or NullPointerException when calling .first() or .last() on the returned chain, we should defensively verify that the validated chain is not null or empty.
private fun getValidatedCertificateChain(host: String, serverCertificates: Array<Certificate>): List<X509Certificate> {
val certificateChain = serverCertificates.filterIsInstance<X509Certificate>().toTypedArray()
if (certificateChain.isEmpty()) {
throw CertificateException("No X509 certificates found in connection")
}
val trustManagerExtensions = this.getTrustManagerExtensions()
val authType = certificateChain.first().publicKey.algorithm
val validatedChain = trustManagerExtensions.checkServerTrusted(certificateChain, authType, host)
if (validatedChain.isNullOrEmpty()) {
throw CertificateException("Validated certificate chain is empty")
}
return validatedChain
}| } catch (e: UnknownHostException) { | ||
| handler?.post { | ||
| result.error("NO_INTERNET", "No Internet Connection", e.localizedMessage) | ||
| } | ||
| } catch (e: SocketTimeoutException) { | ||
| handler?.post { | ||
| result.error("TIMEOUT", "Connection Timeout", e.localizedMessage) | ||
| } | ||
| } catch (e: IOException) { | ||
| handler?.post { | ||
| result.error("NETWORK_ERROR", "Network Error", e.localizedMessage) | ||
| } | ||
| } catch (e: Exception) { | ||
| handler?.post { | ||
| result.error("UNKNOWN_ERROR", "An Unknown Error Occurred", e.localizedMessage) | ||
| } | ||
| } |
There was a problem hiding this comment.
If checkServerTrusted throws a CertificateException (e.g., due to an expired certificate, untrusted CA, or hostname mismatch), it will currently be caught by the generic catch (e: Exception) block and return UNKNOWN_ERROR. Since this is a certificate validation failure, it should be caught specifically and return CONNECTION_NOT_SECURE to help developers easily diagnose certificate issues.
} catch (e: CertificateException) {
handler?.post {
result.error("CONNECTION_NOT_SECURE", "Certificate validation failed", e.localizedMessage)
}
} catch (e: UnknownHostException) {
handler?.post {
result.error("NO_INTERNET", "No Internet Connection", e.localizedMessage)
}
} catch (e: SocketTimeoutException) {
handler?.post {
result.error("TIMEOUT", "Connection Timeout", e.localizedMessage)
}
} catch (e: IOException) {
handler?.post {
result.error("NETWORK_ERROR", "Network Error", e.localizedMessage)
}
} catch (e: Exception) {
handler?.post {
result.error("UNKNOWN_ERROR", "An Unknown Error Occurred", e.localizedMessage)
}
}| final secureString = await HttpCertificatePinning.check( | ||
| serverURL: url.toString(), | ||
| headerHttp: {}, |
There was a problem hiding this comment.
In _sendUnstreamed, the HTTP headers passed to the client request are not forwarded to HttpCertificatePinning.check. If the target server or API gateway requires specific headers (such as User-Agent, Authorization, or custom routing headers) to establish a connection or complete the handshake, the certificate check might fail. Forwarding the headers parameter will make the client more robust.
| final secureString = await HttpCertificatePinning.check( | |
| serverURL: url.toString(), | |
| headerHttp: {}, | |
| final secureString = await HttpCertificatePinning.check( | |
| serverURL: url.toString(), | |
| headerHttp: headers, |
No description provided.