SSL Pinning & Certificate Validation
SSL Pinning & Certificate Validation
Transport Layer Security (TLS/SSL) encrypts data in transit, but it does not protect against a sophisticated attacker who presents a fraudulent certificate issued by a trusted Certificate Authority (CA) — a classic man-in-the-middle (MITM) attack. SSL pinning hardens your Flutter app by binding it to a known certificate or public key so that connections to any other server — even one holding a "valid" CA-signed certificate — are rejected outright.
Why Certificate Pinning Matters
The default TLS handshake trusts any certificate signed by one of hundreds of built-in CAs. An attacker who compromises or impersonates a CA can issue a certificate for your domain, allowing them to decrypt all traffic. Pinning restricts trust to a specific certificate or its public key, eliminating that risk.
- Certificate pinning: Store the server's full DER/PEM certificate and compare it byte-for-byte on each connection.
- Public-key pinning (HPKP-style): Store only the SHA-256 hash of the server's SubjectPublicKeyInfo (SPKI) and compare that hash. This survives certificate renewal as long as the key-pair is re-used.
- Leaf vs. intermediate vs. root pinning: Pinning the leaf certificate is most restrictive; pinning an intermediate CA is a common balance between security and operational flexibility.
Obtaining the Public-Key Hash
Run the following openssl commands against your server's certificate to extract the SPKI SHA-256 hash:
Extract SPKI SHA-256 hash with OpenSSL
# Download the certificate chain
openssl s_client -connect api.example.com:443 -showcerts </dev/null 2>&1 | \
openssl x509 -outform DER > server.der
# Compute the SHA-256 hash of the SubjectPublicKeyInfo
openssl x509 -in server.der -inform DER -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
openssl base64
This produces a Base64 string such as abc123+xyz…==. Store this string in your app — it becomes your pin.
Implementing SSL Pinning with Dio
Dio is the most popular HTTP client in Flutter. Its HttpClientAdapter exposes a BadCertificateCallback where you can intercept the TLS handshake and validate the server's certificate against your stored pin.
Dio — public-key pinning via BadCertificateCallback
import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
class PinnedDioClient {
/// SHA-256 Base64-encoded SPKI hashes for api.example.com
static const List<String> _pins = [
'abc123+primaryKeyHash==', // current certificate key
'def456+backupKeyHash==', // backup key for rotation
];
static Dio create() {
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
(dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback =
(X509Certificate cert, String host, int port) {
// Extract the raw public-key bytes (DER-encoded SubjectPublicKeyInfo)
final pubKeyBytes = cert.der; // full DER cert bytes
// Compute SHA-256 of the full DER cert as a proxy for the SPKI hash
// For a precise SPKI hash, parse the ASN.1 structure or use a native plugin
final digest = sha256.convert(pubKeyBytes);
final hash = base64.encode(digest.bytes);
// Allow the connection only when the hash matches a known pin
return _pins.contains(hash);
};
return client;
};
return dio;
}
}
// Usage
final dio = PinnedDioClient.create();
final response = await dio.get('/users/me');
badCertificateCallback returning true allows a certificate that would otherwise be rejected. Returning false keeps the default rejection. Do not return true unconditionally — this disables all certificate validation and is worse than no pinning at all.Implementing SSL Pinning with the http Package
The standard http package uses Dart's built-in HttpClient. You can wrap it in a custom IOClient and apply the same badCertificateCallback pattern.
http package — pinning via SecurityContext & IOClient
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
Future<http.Client> buildPinnedHttpClient(
List<int> pinnedCertDerBytes) async {
// Load the certificate that must be trusted
final context = SecurityContext(withTrustedRoots: false);
context.setTrustedCertificatesBytes(pinnedCertDerBytes);
final httpClient = HttpClient(context: context);
// Reject connections to any host not matching the pinned cert
httpClient.badCertificateCallback =
(X509Certificate cert, String host, int port) => false;
return IOClient(httpClient);
}
// In your service layer
Future<void> fetchSecureData(List<int> pinnedCert) async {
final client = await buildPinnedHttpClient(pinnedCert);
try {
final response = await client.get(
Uri.parse('https://api.example.com/data'),
);
print('Status: \${response.statusCode}');
} finally {
client.close();
}
}
Certificate Rotation & Backup Pins
Every pinning implementation should include at least two pins: the current certificate key and a pre-generated backup key. When the primary certificate expires:
- Generate a new key-pair and CSR server-side before the old certificate expires.
- Deploy a new app version with the backup key promoted to primary and a new backup added.
- Complete a staged rollout before the old certificate expires.
- Never let all active app versions depend on a single pin that is about to be revoked.
Platform Notes & Web Limitations
The dart:io HttpClient and SecurityContext APIs are available only on Android, iOS, and desktop. Flutter Web uses browser fetch under the hood, where pinning is not accessible from Dart. For web targets, rely on your server's HSTS headers and CAA DNS records instead. Structure your code so the pinned client is injected and can be swapped for a plain client on web builds using conditional imports (dart.library.io vs. dart.library.html).
Summary
SSL pinning is a critical defence-in-depth layer for Flutter apps that communicate with sensitive APIs. The key takeaways are:
- Prefer public-key (SPKI) hash pinning over full-certificate pinning for operational resilience.
- Use Dio's
IOHttpClientAdapteror thehttppackage'sIOClientwith a custombadCertificateCallbackto enforce the pin at the HTTP layer. - Always include a backup pin and plan a certificate rotation procedure before shipping.
- Never return
trueunconditionally frombadCertificateCallback— that disables all security.