Check in the onReceivedSslError() method of a WebViewClient if a certificate is signed from a specific self-signed CA
Asked Answered
I

3

20

I would like to override the onReceivedSslError() of a WebViewClient. Here I want to check if the error.getCertificate() certificate is signed from a self-signed CA and, only in this case, call the handler.proceed(). In pseudo-code:

@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);
    }
}

The public key of my CA is saved in a BouncyCastle resource called rootca.bks. How can I do?

Interdental answered 11/4, 2016 at 15:39 Comment(0)
P
27

I think you can try as the following:

@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://mcmap.net/q/267317/-how-do-i-validate-an-android-net-http-sslcertificate-with-an-x509trustmanager
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;
        }
    }
}

If failed validation, logcat will have some information such as java.security.SignatureException: Signature was not verified...

If success, here's a screenshot:

BNK's screenshot

Pistole answered 16/6, 2016 at 6:58 Comment(4)
Yes!!! This solution actually works and looks secure to me - it verifies if the serer certificate is indeed signed with CA key (given CA public key). Thank you very much!!! :-)Stag
what should I keep here --> R.raw.rootca ?Picklock
@Picklock it´s cert file, for example ´rootca.crt´, you can find more info at developer.android.com/training/articles/…Pistole
@Pistole I am stuck with the signature exception. I have changed the line cert.verify(cert.getPublicKey()); just to confirm. I am still getting same signature exception. please help.Picklock
R
7

I think this should work (SSL_IDMISMATCH means "Hostname mismatch").

@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);
    }
}

If "hasError()" is not working, try error.getPrimaryError() == SSL_IDMISMATCH

Check Documentation of SslError for all error-types.

EDIT: I tested the function on my own self-cert server (its a Xampp), and I got Error #3. That means you have to check for error.hasError(SslError.SSL_UNTRUSTED) for a self-signed cert.

Rodrigorodrigue answered 19/4, 2016 at 7:16 Comment(2)
Unfortunately this does not answer the original question. I don't want to check if the certificate authority is not trusted and in that case proceed (which will nullifies the use of SSL), but I want to check if the certificate is signed from a self-signed CA of which I know the public key.Interdental
You can add add a check: serverCertificate.getIssuedTo().getCName().equals(new URL(error.getUrl()).getHost())Rodrigorodrigue
S
0

based on documentation:

Have you tried using the method getIssuedBy().getDName() of class SslCertificate. This method returns a String representing "The entity that issued this certificate".

Take a look here: http://developer.android.com/reference/android/net/http/SslCertificate.html#getIssuedBy()

Then you just need to know wich string is returned when it is self signed.

EDIT: I think that if it is selfsigned, that should return empty string, and if not, it would return the entity

Regards

Spit answered 15/4, 2016 at 12:1 Comment(5)
That's not a good solution. Anyone can "fake" that name. Since we have the public key of the root CA, the correct solution is to check if the certificate is signed from it. And this brings us back to the original question...Interdental
good morning! just for self information, how could anyone set that? there are no set methods for that value. RegardsSpit
anyway, if you change the way condition, it could fix your problem, just check if ...getDName equals your known certifier entity. Or maybe im not understandig your problem ok. Excuse me if that is the problemSpit
Anyone can create a self-signed certificate, so anyone can create a certificate with a certain certifier entity. In short: your suggestion is not robust and nullifies the use of SSL.Interdental
Ok. Let us know how do you get it when achieved! RegardsSpit

© 2022 - 2024 — McMap. All rights reserved.