在WebViewClient的onReceivedSslError()方法中检查证书是否由特定自签名CA签名。

20

我想重写 WebViewClientonReceivedSslError()。在这里,我想要检查 error.getCertificate() 证书是否由自签名 CA 签署,并且 仅在这种情况下 调用 handler.proceed()。伪代码如下:

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    SslCertificate serverCertificate = error.getCertificate();

    if (/* signed from my self-signed CA */) {
        handler.proceed();
    }
    else {
        super.onReceivedSslError(view, handler, error);
    }
}

我的CA的公钥保存在名为rootca.bks的BouncyCastle资源中。我该怎么办?

3个回答

27

我认为你可以尝试以下方法:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    try {
        WebView webView = (WebView) findViewById(R.id.webView);
        if (webView != null) {
            // Get cert from raw resource...
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            InputStream caInput = getResources().openRawResource(R.raw.rootca); // stored at \app\src\main\res\raw
            final Certificate certificate = cf.generateCertificate(caInput);
            caInput.close();

            String url = "https://www.yourserver.com";
            webView.setWebViewClient(new WebViewClient() {                    
                @Override
                public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                    // Get cert from SslError
                    SslCertificate sslCertificate = error.getCertificate();
                    Certificate cert = getX509Certificate(sslCertificate);
                    if (cert != null && certificate != null){
                        try {
                            // Reference: https://developer.android.com/reference/java/security/cert/Certificate.html#verify(java.security.PublicKey)
                            cert.verify(certificate.getPublicKey()); // Verify here...
                            handler.proceed();
                        } catch (CertificateException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | SignatureException e) {
                            super.onReceivedSslError(view, handler, error);
                            e.printStackTrace();
                        }
                    } else {
                        super.onReceivedSslError(view, handler, error);
                    }
                }
            });

            webView.loadUrl(url);
        }
    } catch (Exception e){
        e.printStackTrace();
    }
}

// credits to @Heath Borders at https://dev59.com/hGIj5IYBdhLWcg3weE-q
private Certificate getX509Certificate(SslCertificate sslCertificate){
    Bundle bundle = SslCertificate.saveState(sslCertificate);
    byte[] bytes = bundle.getByteArray("x509-certificate");
    if (bytes == null) {
        return null;
    } else {
        try {
            CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
            return certFactory.generateCertificate(new ByteArrayInputStream(bytes));
        } catch (CertificateException e) {
            return null;
        }
    }
}
如果验证失败,则logcat会显示一些信息,例如 java.security.SignatureException: Signature was not verified... 如果成功,这里是一张屏幕截图: BNK's screenshot

1
是的!!! 这个解决方案实际上很有效并且看起来很安全 - 它验证了服务器证书是否确实使用 CA 密钥签名 (给定 CA 公钥)。非常感谢!!! :-) - johndodo
2
我应该把什么放在这里 --> R.raw.rootca? - jeet.chanchawat
1
@jeet.chanchawat 这是证书文件,例如 'rootca.crt',您可以在 https://developer.android.com/training/articles/security-ssl.html#UnknownCa 找到更多信息。 - BNK
@BNK 我被签名异常卡住了。我已经更改了cert.verify(cert.getPublicKey());这一行以确认。但我仍然得到相同的签名异常。请帮忙。 - jeet.chanchawat

7
我认为这应该可以解决问题(SSL_IDMISMATCH表示“主机名不匹配”)。
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
    SslCertificate serverCertificate = error.getCertificate();

    if (error.hasError(SSL_UNTRUSTED)) {
        // Check if Cert-Domain equals the Uri-Domain
        String certDomain = serverCertificate.getIssuedTo().getCName();
        if(certDomain.equals(new URL(error.getUrl()).getHost())) {
          handler.proceed();
        }
    }
    else {
        super.onReceivedSslError(view, handler, error);
    }
}

如果 "hasError()" 不起作用,请尝试使用 error.getPrimaryError() == SSL_IDMISMATCH
请查看SslError文档以获取所有错误类型。 编辑:我在自己的自签名服务器(Xampp)上测试了这个函数,我得到了错误#3。这意味着您需要检查是否存在自签名证书,使用 error.hasError(SslError.SSL_UNTRUSTED)

1
不幸的是,这并没有回答原来的问题。我不想检查证书颁发机构是否不受信任,在这种情况下继续(这将使SSL的使用无效),但我想检查证书是否由我知道公钥的自签名CA签名。 - Dev
你可以添加一个检查:serverCertificate.getIssuedTo().getCName().equals(new URL(error.getUrl()).getHost()) - Radon8472

0

基于文档:

您尝试使用 SslCertificate 类的方法 getIssuedBy().getDName()。此方法返回一个字符串,表示“颁发此证书的实体”。

请参阅此处:http://developer.android.com/reference/android/net/http/SslCertificate.html#getIssuedBy()

然后,您只需要知道当它是自签名时返回哪个字符串。

编辑:我认为如果它是自签名的,应该返回空字符串,如果不是,则返回实体。

敬礼


1
这不是一个好的解决方案。任何人都可以“伪造”那个名字。由于我们拥有根CA的公钥,正确的解决方案是检查证书是否是从它签名的。这让我们回到了最初的问题... - Dev
早上好!只是为了自我了解,有人怎么设置那个值?这个值没有设置方法。问候。 - Oldskultxo
无论如何,如果您更改条件的方式,可能会解决您的问题,只需检查...getDName是否等于您已知的认证实体。或者也许我没有理解您的问题。如果是这个问题,请原谅我。 - Oldskultxo
任何人都可以创建自签名证书,因此任何人都可以创建具有特定认证实体的证书。简而言之:您的建议不够健壮,使SSL的使用无效化。 - Dev
好的。当你完成时,请告诉我们你是如何做到的!问候 - Oldskultxo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接