Skip to content

Instantly share code, notes, and snippets.

@LamGC
Created March 15, 2022 02:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LamGC/93c197bf96ffb309534775bcfe3e037d to your computer and use it in GitHub Desktop.
Save LamGC/93c197bf96ffb309534775bcfe3e037d to your computer and use it in GitHub Desktop.
HttpClient 通过 AuthorityInformationAccess 自动修复证书链缺失的信任策略实现

当 Https 网站没有传回完整证书链时,HttpClient 将无法认证网站安全性,所以开发了这个 TrustStrategy 实现。

通过逐层验证证书,在证书验证失败后,假定其中存在中间证书缺失,通过获取下级证书的 AIA 信息,下载中间证书后插入证书链,继续检查。
例如原本的证书链为:

D(C) -> C(B) -> B(A) -> A(-)

而 Https 服务器只传回了:

D(C) -> A(-)

那么 TrustStrategy 将会通过 D 的 AIA 信息获取 C,然后发现 C 缺少证书,再获取 B,最后 A 认证 B 成功,再次通过 TrustManager 认证证书链通过(最终结果以 TrustManager 为准,防止出现差错),返回认证成功。

依赖需求(Gradle Kotlin DSL):

dependencies {
    implementation("org.apache.httpcomponents.client5:httpclient5:5.1.3")
    implementation("org.bouncycastle:bcprov-jdk15to18:1.70")
    implementation("org.bouncycastle:bcpkix-jdk15to18:1.70")
}

日志信息是调试时留下的,可以自行删除。

import mu.KotlinLogging
import org.apache.hc.client5.http.classic.HttpClient
import org.apache.hc.client5.http.classic.methods.HttpGet
import org.apache.hc.client5.http.impl.classic.HttpClients
import org.apache.hc.core5.http.HttpEntityContainer
import org.apache.hc.core5.ssl.TrustStrategy
import org.bouncycastle.asn1.ASN1IA5String
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.DEROctetString
import org.bouncycastle.asn1.x509.AuthorityInformationAccess
import org.bouncycastle.asn1.x509.GeneralName
import java.io.IOException
import java.security.KeyStore
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
class SslTrustStrategy(private val httpClient: HttpClient = HttpClients.createDefault()) : TrustStrategy {
override fun isTrusted(chain: Array<out X509Certificate>, authType: String): Boolean {
if (isCertificateChainTrusted(chain.toList())) {
return true
}
return isCertificateChainTrusted(repairCertificateChain(chain))
}
private fun repairCertificateChain(chain: Array<out X509Certificate>): List<X509Certificate> {
// 证书连续补偿次数, 防止恶意的过长证书链来拖慢运行速度.
val chainCompensationsLimit = 5
var chainCompensationsNumber = 0
val chainList = chain.toMutableList()
var index = 0
while (index in 0 until chainList.lastIndex) {
if (!certificateVerify(chainList[index + 1], chainList[index])) {
if (chainCompensationsNumber > chainCompensationsLimit) {
throw CertificateException("Too many supplementary intermediate certificates. " +
"(Limit: $chainCompensationsLimit)")
}
val parentCertificate = getParentCertificateByAIA(chainList[index])
if (parentCertificate == null) {
logger.warn { "无法获取中间证书的上级证书, 可能没有 AIA 信息." }
break
}
chainList.add(index + 1, parentCertificate)
chainCompensationsNumber ++
} else {
chainCompensationsNumber = 0
}
index ++
}
return chainList.toList()
}
/**
* 使用 Issuer 证书认证 Subject 证书.
* @param issuer 颁发者证书.
* @param subject 被颁发的证书.
* @return 如果使用颁发者证书成功验证了 Subject, 则返回 `true`.
*/
private fun certificateVerify(issuer: X509Certificate, subject: X509Certificate): Boolean {
return try {
subject.verify(issuer.publicKey)
true
} catch (e: Exception) {
logger.warn { "Certificate Verify failed. " +
"(Subject: `${subject.subjectDN.name}`, Issuer: `${subject.issuerDN.name}`, " +
"IssuerCert: `${issuer.subjectDN}`)" }
false
}
}
/**
* 通过 [AuthorityInformationAccess] 获取证书的颁发者证书并下载.
* @param certificate 要下载颁发者证书的证书对象.
* @return 如果成功下载, 返回颁发者证书(Issuer), 否则返回 `null`.
*/
private fun getParentCertificateByAIA(certificate: X509Certificate): X509Certificate? {
logger.debug { "正在获取证书的上级证书: ${certificate.subjectDN} (上级证书可能是: ${certificate.issuerDN})" }
val extensionValue = certificate.getExtensionValue("1.3.6.1.5.5.7.1.1") ?: null
val octetsValue = ASN1InputStream(extensionValue).use {
(it.readObject() as DEROctetString).octets
}
val aia = AuthorityInformationAccess.getInstance(ASN1InputStream(octetsValue).readObject())
val parentCertificateUrlEncoded = aia.accessDescriptions.firstOrNull {
logger.debug { "Method: ${it.accessMethod}, Location: ${it.accessLocation}" }
it.accessMethod.id == accessMethodId &&
it.accessLocation.tagNo == GeneralName.uniformResourceIdentifier
} ?: return null
val parentCertificateUrl = ASN1IA5String.getInstance(parentCertificateUrlEncoded.accessLocation.name).string
val httpRequest = HttpGet(parentCertificateUrl)
val httpResponse = httpClient.execute(httpRequest)
if (httpResponse.code != 200) {
throw IOException("HTTP response reported an error: ${httpResponse.code}")
} else if (httpResponse !is HttpEntityContainer) {
throw IOException("HTTP response does not include entities.")
}
return httpResponse.entity.use {
CertificateFactory.getInstance("X.509").generateCertificate(it.content) as X509Certificate
}
}
private val trustManagers = getTrustManagers()
/**
* 使用 Java 提供的 [X509TrustManager] 检查证书链是否可信.
* 通过逐层验证, 到证书链尾部如果都验证成功, 且尾部证书(CA Root 证书)为可信证书即为认证成功.
* @param chain 证书链.
* @return 如果证书链被验证为可信, 则返回 `true`.
*/
private fun isCertificateChainTrusted(chain: List<X509Certificate>): Boolean {
val chainArray = chain.toTypedArray()
for (trustManager in trustManagers) {
try {
trustManager.checkServerTrusted(chainArray, "RSA")
logger.debug { "TrustManager 认证证书链成功. (Impl: ${trustManager::class.java})" }
return true
} catch (e: CertificateException) {
logger.debug { "TrustManager 认证证书链失败 (Impl: ${trustManager::class.java}): " +
"${e::class.java}: ${e.message}" }
}
}
logger.debug { "证书链认证失败." }
return false
}
/**
* 获取系统中的 [X509TrustManager], 用于认证 TLS 证书链.
*/
private fun getTrustManagers(): List<X509TrustManager> {
val trustManagerFactory = TrustManagerFactory.getInstance("PKIX")
trustManagerFactory.init(null as KeyStore?)
return trustManagerFactory.trustManagers.map {
it as X509TrustManager
}
}
private companion object {
val logger = KotlinLogging.logger { }
const val accessMethodId = "1.3.6.1.5.5.7.48.2"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment