HTTPS basics
To understand certificate pinning you need to know first how HTTPS works in general. Look at the following diagram:
Let’s take a look at the Certificate verification step. By default, it bases on the trust chain. What is that chain? Let’s take a look at the picture.
The idea of the chain is that entities on the higher level trust the entities on 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 the ownership of your domain. Technically, you cannot own a domain but 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 is achievable only by someone who is 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 when you have to create a file on the server with unique content or 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.
Usually, the users can alter the list of roots. They can add the new entries or disable the existing ones in the system or browser settings. Here is what it looks like on Android:
However, those user settings are not always taken into account. On Android Flutter ignores them! Complicated? Look at the table below:
Platform | Root CAs 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 in the case of 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 the pinning may look more secure than using a chain of trust. Indeed there are scenarios where it is true.
In case of trust of chain, there may be many certificates for the given domain. Each of them issued by a trusted CA is also trusted. There are procedures of ownership verification. But, one may find bugs there or trick the CA by using social engineering and get the certificate for any domain.
It is possible to create your own CA and request to make it trusted. Governments may also enforce adding a custom CA by the users of controlled ISPs.
In all those cases the potential attacker may produce his own certificate trusted by your app. Keep in mind that the certificate itself is not enough to intercept or decrypt the data. The traffic has to go through the servers controlled by the attacker. 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 is producing a certificate the event is logged and available to the public. E.g. you can find the history of certificates issued to *.android.com (all the subdomains). That log also contains domains which 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 weak points of 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 users of your app.
In 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 from a trusted CA? If you pinned the intermediate CA certificate (not a leaf), there is a risk that the certificate becomes revoked.
It happened on Thanksgiving Day, in November, 2016. The Barclays Bank mobile app nearly stopped working. Fortunately for Barclays, the issuer exceptionally agreed to issue a short-living certificate.
Show me the code
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:
- Load the certificate(s) from the asset file.
- 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 the SNI mechanism. It allows the server to present the proper certificate if there are multiple domains associated with a given IP address.
The second domain is for DNS purposes. It is translated to an IP address by DNS server and not passed to the destination HTTPS server.
The same HttpClient can be used with dio:
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);
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 on mobile platforms only.
How to test the pining? Just replace the certificate (or fingerprint) with another one (for another domain). It must have a valid syntax.
Wrap up
Should you use pinning? Well, it may increase security but may also make your app unusable. You should consider pinning when developing a high-risk e.g. financial apps. In most of the apps, a pinning is usually 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!
Originally published at https://www.thedroidsonroids.com on March 23, 2023.
Published also in Google for Developers Europe at Medium.
Top comments (0)