How flutter uses certificate authorities



Few month ago I wrote an article explaining which CAs flutter compiles inside the app but recently I was trying to explore how it loads those certificaes from memory and found out that my assumption about compiled-in root certificates was probably wrong.

I did a quick skim through dart source code and it looked to me that it was not compiling in CAs into the final app. So I decided a quick test on android to see if the app even tries to look into system list of Certificate Authorities.

So I built a simple(default) flutter app first and looked at all the files android app was opening when it was launched. Strace didnd’t show anything special. The default app just opened some default app directory and read ashmem a few times. It is a good starting point.

Then I added ‘http’ package to load a secure version of example.com using default configurations. I chose ‘http’ package instead of raw HttpClient because I wanted to have a look at something used by most people and see if external packages somehow modify something in between. And when traced with strace it indeed showed that that app readed system CAs instead of something that was compiled in.

‘Stracing flutter app’

So once I started doing secure network activity the app went and read systems trusted root certificate. The app opens certificate ‘399e7759.0’ which is a system root certificate for digicert. And since I was opening a default page of ‘https://example.com’ so let’s check it certificate. If you open the certificate loaded by your browser you can see that i was signed by digicert.

‘Stracing flutter app’

This shows that at least on adroid flutter uses root certificates stored by the operating system in a default location.

Details

Since I was looking into source code and were trying to statically verify my ideas here are some checkpoints you could look at yourself.

In my experiment I was using ‘http’ package and in order to send a request I created a default client with Client() instance constructor. When we open ‘Client’ it is just an abstract class with a factory that loads cached client or creates either IOClient or BrowserClient based on the platform.

It will then send a request through BaseClient using _sendUnstreamed(…) which will later call send() method from specific platform Client. In our case it is IOClient. Then it will pass all request handling to HttpClient which is out of ‘http’ package. This is just an abstract which is implemented in ’_HttpClient’.

After that there are a lot of different function calls untill finally we arrive at ‘SecureSocket.startConnect’ if if we are using https or Socket.startConnect for other connections. ‘SecureSocket.startConnect’ takes a ‘_context’ which is devined in ‘_HttpClient’

In the end it all boils down to a call of native classes from dart-sdk which in turn uses openssl to do all encryption and decryption. So our main task is to prepare data so that openssl can do its part. The main thing that interests us in this exploration is ssl context which ‘we’ setup prior to passing it to openssl. And this context contains trusted root certificates that our app trusts.

class RawSecureSocket {
    ...
    secure(...)
    ...
}

class _RawSecureSocket {
    ...
    connect(...) {
        return new _RawSecureSocket(
            ...,
            context ?? SecurityContext.defaultContext,
            ..., );
    }
}

Where does this context comes from? My understanding is that you can pass this context but in general nobody does and system just loads a default one. And this default context is pretty interesting. The answer is located in security_context.dart inside dart sdk.

dart-sdk/lib/io/security_context.dart:35

/// The default security context used by most operation requiring one.
///
/// Secure networking classes with an optional `context` parameter
/// use the [defaultContext] object if the parameter is omitted.
/// This object can also be accessed, and modified, directly.
/// Each isolate has a different [defaultContext] object.
/// The [defaultContext] object uses a list of well-known trusted
/// certificate authorities as its trusted roots. On Linux and Windows, this
/// list is taken from Mozilla, who maintains it as part of Firefox. On,
/// MacOS, iOS, and Android, this list comes from the trusted certificates
/// stores built into the platforms.
external static SecurityContext get defaultContext;

So for android, macos and ios, dart uses default system locations for root certificates. For android it is ’/system/etc/security/cacerts’. For windows and linux it uses something else. Let’s see what.

In linux, dart first check where "/etc/pki/tls/certs/ca-bundle.crt" file or "/etc/ssl/certs" folder exists. If it exists then it loads root certificates based on found locations. Otherwise it loads certifices compiled inside the app.

// On Linux, we use the compiled-in trusted certs as a last resort. First,
// we try to find the trusted certs in various standard locations. A good
// discussion of the complexities of this endeavor can be found here:
// https://www.happyassassin.net/2015/01/12/a-note-about-ssltls-trusted-certificate-stores-and-platforms/

In windows it uses SSL_CTX_get_cert_store to load certificate store from context and if it cannot load it then it goes to load compiled-in root certificates. Since I am not interested in flutter for windows I didn’t spend any time digging in any deeper into it.

Conclusion

Even though flutter does have built-in certificates it uses them as a fallback for windows and linux in case if there are issues with finding default root certificates. On other platforms (at least from static code investigation) it just uses system default root certificates.