Executive summary

SSL certificate pinning restricts accepted certificates to prevent man-in-middle attacks in high-risk mobile applications.

Certificate pinning bypasses default chain-of-trust validation by explicitly specifying which certificates your Flutter app accepts. While the chain of trust accepts any certificate from trusted CAs, pinning limits this to specific leaf, intermediate, or root certificates. This prevents attackers from obtaining fraudulent certificates through CA vulnerabilities or government-enforced custom CAs. However, pinning requires backup pins and coordination with backend teams since expired or revoked certificates can break your app.

Key takeaways:

  • Certificate pinning prevents MITM attacks even when attackers obtain trusted certificates through CA compromise
  • Flutter implements pinning via SecurityContext class accepting certificate chain files from assets
  • Standard certificates expire in 398 days maximum while Let’s Encrypt issues 90-day certificates
  • Always maintain backup pins to prevent app failures when primary certificates expire or get revoked
  • Pinning recommended only for financial apps due to maintenance overhead and potential app breakage

HTTPS basics

To understand certificate pinning, you need to know first how HTTPS works in general. Look at the following diagram:

Flutter SSL Certificate pinning 1

Let’s take a look at the Certificate verification step. By default, it is based on the trust chain. What is that chain? Let’s take a look at the picture.

Flutter SSL Certificate pinning2 1

The idea behind the chain is that entities at the higher level trust entities at the lower levels. So the root CAs trust the intermediate CAs, which in turn trust the leaves.

The leaves

Let’s start with the endmost certificates. They are usually bound to a given top-level domain (or more precisely, the public suffix). For example, a certificate may be issued to www.thedroidsonroids.com. It may also be a wildcard applicable to any subdomain (like *.thedroidsonroids.com).

To receive a certificate, you need to prove ownership of your domain. Technically, you do not own a domain; you only register it for up to 10 years and then renew it. So it is more like a rental. But the term “ownership” is widely used in practice, and we will follow that.

How to prove the ownership of a domain?

The simplest case is when you get the certificate from your domain registrar. They know that you are an owner, so they can issue you a certificate without verification.

If the issuer is a 3rd party, for example, Let’s Encrypt or GoDaddy, you have to pass a challenge. It consists of an action that can be performed only by someone controlling the domain.

Nowadays, it is usually a DNS challenge. You need to create a TXT record with unique content generated by the issuer. The alternative is an HTTP challenge, where you must create a file on the server with unique content or a unique name. The issuer fetches the DNS record or downloads a file to check if the content matches.

The roots

OK, but what is the source of root CAs? Well, it depends on the platform. The list may come from the operating system or (in the case of Flutter web) from the web browser.

Users can usually modify the list of roots. They can add new entries or disable existing ones in the system or in browser settings. Here is what it looks like on Android:

CB890E20 252D 46EC A198 F44EAC78D86A

However, those user settings are not always taken into account. On Android, Flutter ignores them! Complicated? Look at the table below:

Platform Root CA’s source Comment
Any browser or native app on iOS System trust store iOS does not allow browser apps to use their own trust stores; see available trusted root certificates for Apple operating systems.
Chrome (version 105 or newer) Own trust store Chrome Root Program Policy
Firefox Own trust store Mozilla Included CA Certificate List
Windows System trust store, fallback to Dart builtin store Source code

Trusted Root Certification Authorities Certificate Store

Linux System trust store, fallback to Dart builtin store Source code

A note about SSL/TLS trusted certificate stores, and platforms (OpenSSL and GnuTLS)

Android System trust store from ROM (not taking system settings into account) Source code

The chain of trust

Each CA in the chain maintains the certificate revocation list. A CA or you may revoke a certificate, for example, if its private key gets compromised.

Additionally, each certificate has a limited lifespan. At the time of writing (December 2022), the largest lifespan for the newly issued certificates is about 13 months (398 days). But the actual lifespan may be much shorter. For example, with Let’s Encrypt, it is 90 days.

What is the reason for short certificate lifespans? Well, the short lifespan reduces the time window of using the compromised certificates. Note that the maximum lifespan does not apply to private CAs or self-issued certificates. It only affects the certificates in the trust of

Certificate pinning

We can restrict the accepted range of certificates by explicitly specifying (pinning) them. You can pin the leaf, the intermediate CA, or even the root CA certificate. There can be more than one certificate pinned. You should have at least one backup pin. If you don’t have it, your app will stop functioning if the primary certificate needs to be changed.

The pros of certificate pinning

At first glance, pinning may appear more secure than a chain of trust. Indeed, there are scenarios where it is true.

In the case of a trust chain, there may be many certificates for the given domain. Each of them, issued by a trusted CA, is also trusted. There are procedures for ownership verification. But one may find bugs there or trick the CA via social engineering to obtain a certificate for any domain.

It is possible to create your own CA and request that it be trusted. Governments may also enforce adding a custom CA for the users of controlled ISPs.

In all those cases, the potential attacker may produce their own certificate trusted by your app. Keep in mind that the certificate itself is not enough to intercept or decrypt the data. The traffic must pass through the attacker-controlled servers. It is possible, for example, if he is also controlling the DNS server. So it is not tricky to perform the MITM attack.

Last but not least is the certificate transparency. Each time a CA produces a certificate, the event is logged and available to the public. E.g., you can view the history of certificates issued for *.android.com (all subdomains). That log also contains domains that may not (yet) be announced. Including ones related to the development or staging environments.

The cons of certificate pinning

Every rose has its thorn. There are a few weaknesses in certificate pinning.

If the private key of your certificate gets compromised and you are using the chain of trust, you can revoke it. Your legitimate server can start using a new certificate. The old one will start being rejected. Everything seamlessly for your app’s users.

In the case of pinning, you cannot just replace a certificate with any trusted one. It has to match the pins.

Do you remember that you can pin a certificate issued by a trusted CA? If you pinned the intermediate CA certificate (not a leaf), there is a risk that the certificate will be revoked.
It happened on Thanksgiving Day, in November, 2016. The Barclays Bank mobile app nearly stopped working. Fortunately for Barclays, the issuer agreed to issue a short-lived certificate.

Step-by-Step: Implementing SSL Pinning with SecurityContext

Using pinned certificates is pretty simple. You just need to set up your HTTP client. In case of Dart’s built-in HttpClient, the code may look like this:

final securityContext = SecurityContext();
final certificates = await rootBundle.load('assets/certificates/google.crt'); //1
securityContext.setTrustedCertificatesBytes(certificates.buffer.asUint8List()); //2
final httpClient = HttpClient(context: securityContext);

final httpClientRequest = await httpClient.getUrl(Uri.parse('https://google.pl'));
final response = await httpClientRequest.close();
print(response.statusCode);

The code above does the following things:

  1. Load the certificate(s) from the asset file.
  2. Set the certificate(s) in a SecurityContext.

Use the following command to create a file with certificates:

openssl s_client -showcerts -servername google.pl -connect google.pl:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > google.crt

Note the plural. The result consists of the full certificate chain, from root to leaf.

Replace google.pl with your domain. Note that a domain name is passed twice. Firstly, as a server name for SNI. It allows the server to present the correct certificate when multiple domains share an IP address.

The second domain is for DNS purposes. It is translated to an IP address by the DNS server and not passed to the destination HTTPS server.

The same HttpClient can be used with dio:

final certificates = await rootBundle.load('assets/openssl/google.crt');
final dio = Dio();
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate = (client) {
  final securityContext = SecurityContext(); //1
  securityContext.setTrustedCertificatesBytes(certificates.buffer.asUint8List());
  return HttpClient(context: securityContext);
};

final response = await dio.getUri(Uri.parse('https://google.pl'));
print(response.statusCode);

Note that it is not possible to create a custom SecurityContext on the web platform.

There are also 3rd party plugins like http_certificate_pinning, which allows you to specify only the fingerprint (SHA checksum, short string) of the full certificate. However, they usually work only on mobile platforms.

How to test the pining? Just replace the certificate (or fingerprint) with another one (for another domain). It must have a valid syntax.

SSL Pinning Implementation Methods: Manual vs Package

Flutter developers can implement SSL pinning in two ways: manually with SecurityContext or via third-party packages.

Manual Implementation (SecurityContext): You extract the server certificate, store it in app assets, and configure HttpClient to trust only that certificate. This approach gives you full control but requires updating the app whenever the certificate expires (typically 90 days for Let’s Encrypt, up to 398 days for commercial CAs).

Using the http_certificate_pinning Package: This package allows you to pin certificates using SHA-256 fingerprints instead of the full certificate file. You only need to store the fingerprint string (e.g., “59:58:57:5A:5B…”) in your code. Works with both Dio and standard HTTP clients. To get the SHA-256 fingerprint, run: openssl x509 -noout -fingerprint -sha256 -inform pem -in certificate.pem

Which method to choose: Use SecurityContext for maximum control and no dependencies. Use http_certificate_pinning for easier certificate rotation (fingerprint changes less frequently) and cleaner code. Both methods work on Android and iOS, but not on Flutter Web.

Wrap up

Should you use pinning? Well, it may increase security, but it may also make your app unusable. You should consider pinning when developing a high-risk, e.g., financial apps. In most apps, pinning is not recommended. Don’t set any pins without agreement from the backend administrators! They may change certificates unexpectedly, otherwise. Prepare at least one backup pin.

We hope you liked this article. If you have any questions or feedback, don’t hesitate to leave a comment below!

Frequently Asked Questions About SSL Pinning in Flutter

Does SSL pinning work on Flutter web?

No, SSL pinning is not supported on the Flutter web platform. SecurityContext and certificate validation are only available on mobile (Android/iOS) and desktop (Windows/Linux/macOS) platforms. Flutter Web relies on the browser’s native certificate validation.

What happens when my pinned certificate expires?

Your app will stop connecting to the server and throw HandshakeException errors. You must update the certificate in your app assets and release a new version before the old certificate expires. Always maintain a backup pin to avoid service disruption.

Can attackers extract the pinned certificate from my APK?

Yes, certificates stored in app assets can be extracted from the APK file. However, this doesn’t compromise security – the certificate is public information. The private key remains on your server and is never shared. For additional security, consider using public key pinning instead of full certificate pinning.

Should I pin the root, intermediate, or leaf certificate?

Pin the leaf certificate (your domain’s certificate) for maximum security. Pinning intermediate or root CA certificates is risky – if the CA is compromised or revoked, your app will stop working, and you’ll need to release an emergency update.