WebView and SSL certificates



我在使用Androids WebView加载SSL安全网页时遇到问题。我总是会遇到这样的错误:onReceivedSslError: primary error: 3 certificate: Issued to: CN=intranet.<company>.de,C=DE,O=<company>,OU=<compay org unit>

我已经通过设置->安全->从SD卡安装将该服务器证书链的所有证书安装到安卓的密钥链中。我甚至可以看到,其中一个安装的证书与LogCat的错误输出完全匹配。如果我使用默认的浏览器应用程序,那就更奇怪了:即使我卸载了之前提到的所有证书,它也能毫无问题地加载页面。我真的不知道如何在不信任所有证书的情况下通过在onReceivedSslError()中调用handler.proceed()来解决这个问题,这是一个潜在的安全问题。感谢您的帮助。谢谢

干杯比约恩

编辑:根证书是自签名的,因为它只用于intranet服务器。我以为我添加到Android可信凭据中的所有证书都是可信的。

当连接到具有自签名证书的服务时,应该使用好的onReceiveSslError()handler.proceed(),Web视图无法处理它们。

我现在想看的是服务器端ssl实现。如果您有多个具有相同证书的服务,请检查SNI支持,以及它是否配置良好。然后查看您连接的服务是否返回了正确的证书。还要从服务器中检查使用者备用名称,并根据需要进行配置。

对于该任务,您可以使用这些命令。

openssl s_client -showcerts -connect yourhost.com:443
openssl s_client -connect yourhost.com:443
openssl s_client -servername yourhost.com -connect yourhost.com:443
openssl s_client -connect yourhost.com:443 | openssl x509 -text

这里你有一些来自Android文档的更多信息

主机名验证的常见问题如在本文的开头,验证SSL有两个关键部分联系第一个是验证证书是否来自受信任的来源,这是上一节的重点。这件事的重点部分是第二部分:确保与您交谈的服务器出示正确的证书。如果没有,你通常会看到像这样的错误:

java.io.IOException:未验证主机名"example.com"位于libcore.net.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223(位于libcore.net.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446(位于libcore.net.HttpEngine.sendSocketRequest(HttpEngine.java:290(位于libcore.net.HttpEngine.sendRequest(HttpEngine.java:240(位于libcore.net.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282(位于libcore.net.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177(位于libcore.net.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271(发生这种情况的一个原因是服务器配置错误。这个服务器配置了一个没有主题的证书或与您所在的服务器匹配的主题备选名称字段试图达到。可以使用一个证书许多不同的服务器。例如,在谷歌上带有openssl的证书s_client-连接google.com:443|opensslx509-你可以看到的文本是一个支持*.google.com的主题,但也使用*.youtube.com、*.android.com和其他。只有当您正在连接的服务器名称证书未将到列为可接受。

不幸的是,这种情况的发生还有另一个原因:虚拟主机。当使用HTTP共享一个以上主机名的服务器时,web服务器可以从HTTP/1.1请求中判断出哪个目标客户端正在查找的主机名。不幸的是,这很复杂使用HTTPS,因为服务器必须知道返回哪个证书在它看到HTTP请求之前。要解决此问题,更新SSL版本,特别是TLSv1.0及更高版本,支持服务器名称指示(SNI(,允许SSL客户端指定预期主机名,以便可以返回正确的证书。

幸运的是,HttpsURLConnection从Android 2.3开始就支持SNI。不幸的是,Apache HTTP客户端没有,这是众多我们不鼓励使用它的原因。如果您需要支持Android 2.2(及更早版本(或Apache HTTP客户端将设置唯一端口上的备用虚拟主机,使其明确无误返回哪个服务器证书。

希望能有所帮助。

2023年的情况似乎是,Android WebView在检查网页证书的有效性时仍然忽略用户安装的CA证书。我们将不得不自己做。

当您重写onReceivedSslError()时,您可以获得设备上用户安装的CA证书的列表,并检查其中是否有任何证书能够证明导致ssl错误的证书的有效性。

override fun onReceivedSslError(view: WebView, handler: SslErrorHandler, error: SslError) {
   if(verifyAgainstUserInstalledCerts(getX509Certificate(error.certificate))){
        handler.proceed();
    }
}

首先,我们需要查看传递到onReceivedSslError中的SslError,并获得其相应的证书。如果您的目标是API级别>29,这和error.certificate.x509Certificate一样简单,但对于较低的API级别,我们需要进行一些额外的欺骗。我使用下一个函数来实现这一点。

private fun getX509Certificate(sslCertificate: SslCertificate): Certificate? {
    if(Build.VERSION.SDK_INT>=29){
        return sslCertificate.x509Certificate
    }else{
        // credits to @Heath Borders at http://stackoverflow.com/questions/20228800/how-do-i-validate-an-android-net-http-sslcertificate-with-an-x509trustmanager
        val bundle: Bundle = SslCertificate.saveState(sslCertificate)
        val bytes: ByteArray? = bundle.getByteArray("x509-certificate")
        return if (bytes == null) {
            null
        } else {
            try {
                val certFactory = CertificateFactory.getInstance("X.509")
                certFactory.generateCertificate(ByteArrayInputStream(bytes))
            } catch (e: CertificateException) {
                null
            }
        }
    }
}

现在,根据我们的用户安装的CA证书来实际测试抛出错误的证书。

private fun verifyAgainstUserInstalledCerts(certificateInQuestion:Certificate?):Boolean {
    if(certificateInQuestion == null){return false}
    for (userInstalledCert in getUserInstalledCerts()) {
        try{
            certificateInQuestion.verify(userInstalledCert.publicKey)
            return true
        }catch(e:Exception){
            e.printStackTrace()
            /* The x509Certificate's verify function throws an error when it fails to verify
             and never returns anything. We are forced to use try/catch to get the boolean
              value that we want, which is whether or not any certificate authority can attest
              to the validity of the given certificate */
        }
    }
    return false
}

啊,但是我们还依赖另一个自定义函数。让我在这里定义它。

fun getUserInstalledCerts(): Collection<X509Certificate>{
        val results = mutableSetOf<X509Certificate>()
        try {
            val ks: KeyStore = KeyStore.getInstance("AndroidCAStore")
            ks.load(null, null)
            val aliases = ks.aliases()
            while (aliases.hasMoreElements()) {
                val alias = aliases.nextElement() as String
                val cert = ks.getCertificate(alias) as X509Certificate
                if (alias.startsWith("user:")) {
                    results.add(cert)
                }
            }
        } catch (e: Exception){
            e.printStackTrace()
        }
        return results
    }

2023

根据Android文档:

默认情况下,来自所有应用程序的安全连接(使用TLS和HTTPS等协议(都信任预先安装的系统CA,而以Android 6.0(API 23级(及更低版本为目标的应用程序也默认信任用户添加的CA存储您可以使用基本配置(用于应用程序范围的自定义(或域配置(用于每个域的自定义(自定义应用程序的连接。

意思是安卓系统>6.0和API>23默认情况下不信任用户添加的CA存储

相反,你必须这样做:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="@raw/debug_cas"/>
        </trust-anchors>
    </debug-overrides>
</network-security-config>

res/xml/network_security_config.xml

其中@raw/debug_cas是您的自签名或非公共CA证书,采用PEM或DER格式。

更多详细信息,请点击此处

最新更新