Sdk: badCertificateCallback is called with issuer certificate instead of leaf certificate for LetsEncrypt published certificate

Created on 18 Nov 2019  Â·  11Comments  Â·  Source: dart-lang/sdk

I want to do SSL pinning of server certificate that is issued by LetsEncrypt in my flutter code. Following is my code:

 static http.Client get _secureClient => IOClient(
      HttpClient(context: SecurityContext(withTrustedRoots: true))
      ..badCertificateCallback = (cert, host, port) {
            if (HOSTNAME != host) return false;

            final parser = ASN1Parser(cert.der);
            final ASN1Sequence signedCert = parser.nextObject();
            final ASN1Sequence _cert = signedCert.elements[0] as ASN1Sequence;
            final ASN1Sequence pubKeyElement = _cert.elements[6];
            final ASN1BitString pubKeyBits = pubKeyElement.elements[1];
            final List<int> encodedPubKey = pubKeyBits.stringValue;
            final ASN1Parser rsaParser = ASN1Parser(encodedPubKey);
            final ASN1Sequence keySeq = rsaParser.nextObject();
            final ASN1Integer modulus = keySeq.elements[0];
            final ASN1Integer exponent = keySeq.elements[1];
            //print("hostname: $host:$port");
            //print("modulus : ${modulus.valueAsBigInteger}");
            //print("exponent: ${exponent.intValue}");

            return PUB_KEY_MODULUS == modulus.valueAsBigInteger.toString() &&
                PUB_KEY_EXPONENT == exponent.intValue.toString();
          },
      );

The issue is that the callback if returning with the issuer certificate instead of the leaf certificate. Following is the PEM formatted cert that is being called with:

-----BEGIN CERTIFICATE-----
MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
-----END CERTIFICATE-----

Is this how it is supposed to be or is it a bug? If this is the correct behavior how can I validate the leaf certificate that is issued to my server?

$ flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[âś“] Flutter (Channel stable, v1.9.1+hotfix.6, on Linux, locale en_IN)

[âś“] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[âś“] Android Studio (version 3.5)
[âś“] Connected device (1 available)

• No issues found!

Related: https://github.com/dart-lang/sdk/issues/35981

area-vm

Most helpful comment

I hit this also. I dug into the code that calls badCertificateCallback. The dartlang code calls OpenSSL to perform the TSL connection. When OpenSSL cannot verify a certificate, it returns the certificate to dartlang, which passes it to the badCertificateCallback. This means that dartlang only sees the bad certificate, not the whole chain provided by the server. The fix will be non-trivial.

All 11 comments

I also realized that flutter doesn't respect the platform trusted CA store (checked with android, added a Charles proxy cert). It bundles the app with its own trusted root store (at least that is what I assume). If that is the case then do I need to pin the SSL certificate or Public Key of LetsEncrypt (which is already trusted) to prevent Men in the middle attack?

@mit-mit Could you answer the query in the comment? If I can go ahead with my hypothesis then I can publish my app.

I hit this also. I dug into the code that calls badCertificateCallback. The dartlang code calls OpenSSL to perform the TSL connection. When OpenSSL cannot verify a certificate, it returns the certificate to dartlang, which passes it to the badCertificateCallback. This means that dartlang only sees the bad certificate, not the whole chain provided by the server. The fix will be non-trivial.

I can confirm that this happens for all kinds of CAs, not just LetsCrypt.

If the default behaviour of OpenSSL is to return the unverified certificate, and not the whole keychain then how does iOS does it in AFNetworking. The flow in iOS is pretty similar to this. We disable trusting of certificates, the system calls badCertificateCallback and we extract the public key.
Haven't tried it though.

Is there any response for this issues. This seems very serious issues. Why would the certificate provided with only root CA instead of whole chain is not provided with parent,leaf or intermediated. Can we suspect that there is some configuration issues with LetsEncrypt ?

@sahilpatel16 has confirmed that it is an issue with all CAs not just LetsEncrypt

As Mentioned here: https://developer.android.com/training/articles/security-ssl#MissingCa

The third case of SSLHandshakeException occurs due to a missing intermediate CA. Most public CAs don't sign server certificates directly. Instead, they use their main CA certificate, referred to as the root CA, to sign intermediate CAs. They do this so the root CA can be stored offline to reduce risk of compromise. However, operating systems like Android typically trust only root CAs directly, which leaves a short gap of trust between the server certificate—signed by the intermediate CA—and the certificate verifier, which knows the root CA. To solve this, the server doesn't send the client only it's certificate during the SSL handshake, but a chain of certificates from the server CA through any intermediates necessary to reach a trusted root CA.

There are two approaches to solve this issue:

Configure the server to include the intermediate CA in the server chain. Most CAs provide documentation on how to do this for all common web servers. This is the only approach if you need the site to work with default Android browsers at least through Android 4.2.

Or, treat the intermediate CA like any other unknown CA, and create a TrustManager to trust it directly, as done in the previous two sections.

I think adding these three things in the .conf file (Apache 2.4.8) file will solve the issues.
You need to use:
SSLCertificateFile /etc/letsencrypt/live/kultureshock.net/cert.pem
SSLCertificateChainFile /etc/letsencrypt/live/kultureshock.net/chain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/kultureshock.net/privkey.pem

@daadu you found any workaround? Having same issue, it is returning the intermediate certificate

badCertificateCallback should not be used to trust root or intermediate CAs. Use setTrustedCertificates for that (although it is broken #35462).

The point of badCertificateCallback is to facilitate certificate pinning. The purpose of certificate pinning is to avoid trusting the CA ecosystem or the device's list of trusted CAs. Until badCertificateCallback is changed to pass the leaf certificate, it remains useless for certificate pinning.

@mleonhard setTrustedCertificates cannot be used to do Public Key Pinning.

Anyways, I am pinning public key of Let'sEncrypt CA, as a workaround, as LetsEncrypt in theory would not release sign certificate to someone else, for the hostname that I use. This will prevent any sniffing tool like Charles Proxy to function.

Also this article (I have quoted below) suggests that flutter relies on its own trusted CA bundle (embedded within app) and does not rely on platform's trusted CA (so say in Android, user importing custom CA would not cause MITM on our app)

So I dug further, after some further research I found out that Flutter bundles the BoringSSL libraries into “libflutter.so” and performs its own verification steps rather than trust the OS’s systems. It also forces the use of a known set of Root certificates which goes someway to explain why I couldn’t MITM the connection.

If what the article says is true, that in a way setting SecurityContext(withTrustedRoots: true) (which is BTW default), flutter will do the "SSL CA Bundle Pinning" for you, if the API service that the app consume uses SSL cert signed by some reputed CA.

Please point me if I am wrong, I am assuming for SSL cert signed by reputed CA, flutter does the "pinning" for you to prevent MINTM attack. (Although, as the article suggests it can be bypassed too! By modifying the libflutter.so in APK)

@daadu @mleonhard only way I found to retrieve the correct certificate was using HttpClientResponse object

HttpClientRequest request = await _httpClient.getUrl(Uri.parse(url));
HttpClientResponse response = await request.close();

Here you can access:

response.certificate.pem
response.certificate.subject

Etc and it has the correct certificate, instead of CA certificate only.

I somehow made this to be compatible with "http" by returning response like this:

    final streamedResponse = IOStreamedResponse(
        response.handleError((error) {
          final httpException = error as HttpException;
          throw ClientException(httpException.message, httpException.uri);
        }, test: (error) => error is HttpException),
        response.statusCode,
        contentLength:
            response.contentLength == -1 ? null : response.contentLength,
        isRedirect: response.isRedirect,
        persistentConnection: response.persistentConnection,
        reasonPhrase: response.reasonPhrase,
        inner: response);
    return Response.fromStream(streamedResponse);

I would prefer to do the pinning normally at badCertificateCallback, but until it is fixed, this was the only way I found to perform certificate pinning, at response level

Was this page helpful?
0 / 5 - 0 ratings