Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

badCertificateCallback is called with issuer certificate instead of leaf certificate for LetsEncrypt published certificate #39425

Open
daadu opened this issue Nov 18, 2019 · 20 comments
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends.

Comments

@daadu
Copy link

daadu commented Nov 18, 2019

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: #35981

@daadu
Copy link
Author

daadu commented Nov 18, 2019

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 mit-mit added the area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends. label Nov 20, 2019
@daadu
Copy link
Author

daadu commented Nov 22, 2019

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

@mleonhard
Copy link

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.

@sahil-oyo
Copy link

sahil-oyo commented Apr 23, 2020

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.

@basnetjiten
Copy link

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 ?

@daadu
Copy link
Author

daadu commented Apr 25, 2020

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

@basnetjiten
Copy link

basnetjiten commented Apr 27, 2020

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

@TMSantos
Copy link

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

@mleonhard
Copy link

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.

@daadu
Copy link
Author

daadu commented Aug 26, 2020

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

@TMSantos
Copy link

@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

@wederchr
Copy link

@mleonhard Any update on the badCertificateCallback-fix? You mentioned that it is non-trivial...

@yfyau
Copy link

yfyau commented Jul 16, 2021

Any update about this?
Is this possible to return a list of certificates?
May I having some hints about where the badCertificateCallback being triggered? I was looking around until io_natives but still not found

@xclud
Copy link

xclud commented Sep 30, 2021

Any progress on this?

Yesterday Let's Encrypt root certificate expired and none of my apps can connect to the servers. Will this issue fix this problem as well?

@spideythewebhead
Copy link

The same problem occured to me too today, as let's encrypt root certificate changed, the solution I have found is download the isrgrootx1.pem from https://letsencrypt.org/certificates/

And then read or provide it's path to Security context.defaultContext or your own context .

I have tried this on my flutter app and worked fine! I don't know exactly what might the drawbacks can be here @xclud

@DesmondFox
Copy link

Any updates?

@xclud
Copy link

xclud commented Nov 2, 2021

@DesmondFox

flutter/flutter#83792

@samuel-pastor
Copy link

any news on this? Is there a way to successfully implement ssl public key pinning or at least cert pinning?

@rollin-s
Copy link

Hello !
Any news on this ? This is pretty confusing and really needs to be fixed !
I don't see why we can't have access to the leaf certificate, or even all the certificates in the certificate chain.

@kevmoo
Copy link
Member

kevmoo commented Apr 5, 2024

@brianquinlan – anything you can look at?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-vm Use area-vm for VM related issues, including code coverage, FFI, and the AOT and JIT backends.
Projects
None yet
Development

No branches or pull requests