← Articles

Certificate pinning in Flutter

By Ann Tech · 6 November 2025

Certificate pinning prevents man-in-the-middle (MITM) attacks by verifying the server's certificate against a known-good value, rather than trusting the system certificate store. Without pinning, an attacker with a rogue CA certificate (or a compromised device) could intercept traffic.

When pinning is worth the cost

Pinning adds operational complexity — if your certificate rotates and you forgot to update the app, users get locked out. Use it when:

  • The app handles financial transactions or medical data
  • Your threat model includes targeted attacks against specific users
  • Regulatory requirements (PCI DSS, HIPAA) mandate it

For most consumer apps, HTTPS with proper certificate validation is sufficient.

SPKI pinning vs certificate pinning

  • Certificate pinning: pin the exact certificate. Breaks when cert rotates (every 1-2 years).
  • SPKI pinning (Subject Public Key Info): pin the public key. Survives certificate renewal as long as you keep the same key pair.

SPKI pinning is strongly preferred in production.

Get your SPKI hash

# From the live server
openssl s_client -connect api.example.com:443 -servername api.example.com < /dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | openssl enc -base64

Or from a certificate file:

openssl x509 -in cert.pem -pubkey -noout \
  | openssl pkey -pubin -outform der \
  | openssl dgst -sha256 -binary \
  | openssl enc -base64

Implementation with Dio

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';

Dio buildSecureDio() {
  final dio = Dio();

  (dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
    final client = HttpClient();
    client.badCertificateCallback = (X509Certificate cert, String host, int port) {
      // Only allow specific hosts to bypass standard validation
      return false;
    };
    return client;
  };

  // Add pinning interceptor
  dio.interceptors.add(CertificatePinningInterceptor(
    allowedSHAFingerprints: [
      'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Primary cert
      'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // Backup cert
    ],
  ));

  return dio;
}

class CertificatePinningInterceptor extends Interceptor {
  CertificatePinningInterceptor({required this.allowedSHAFingerprints});
  final List<String> allowedSHAFingerprints;

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    // Note: Dio doesn't expose the certificate on response
    // For full SPKI pinning, use a custom HttpClient as shown below
    handler.next(response);
  }
}

HttpClient-level pinning

For full control, intercept at the HttpClient level:

import 'dart:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';

HttpClient buildPinnedHttpClient(List<String> spkiHashes) {
  final client = HttpClient();

  client.badCertificateCallback = (X509Certificate cert, String host, int port) {
    // Extract the public key from the certificate's DER encoding
    // and verify its SHA-256 hash matches our pinned value
    final spki = _extractSpki(cert.der);
    final hash = base64.encode(sha256.convert(spki).bytes);
    final pinnedHash = 'sha256/$hash';

    if (!spkiHashes.contains(pinnedHash)) {
      // Log the mismatch for investigation
      debugPrint('Certificate pin mismatch for $host: got $pinnedHash');
      return false; // Reject the connection
    }
    return true;
  };

  return client;
}

List<int> _extractSpki(Uint8List der) {
  // Parse the certificate DER to extract Subject Public Key Info
  // This is ASN.1 parsing — use a library for production code
  // ...
}

For production, use http_certificate_pinning package which handles the ASN.1 parsing:

dependencies:
  http_certificate_pinning: ^4.0.0

Backup pins

Always pin at least two certs — the primary and a backup:

const pinnedHashes = [
  'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // Current cert
  'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // Next cert (pre-generated)
];

Generate your next cert's SPKI hash before the current one expires. Deploy the new hash in an app update before rotating the certificate.

Certificate rotation procedure

  1. Generate new certificate/key pair
  2. Compute SPKI hash of new cert
  3. Add new hash to pinnedHashes (keep old hash too)
  4. Release app update with both hashes
  5. Wait for adoption (≥ 95% of users on new version)
  6. Rotate the server certificate
  7. Remove old hash in next app update

Common pitfalls

Pinning the leaf certificate. Leaf certificates expire annually. Pin the intermediate CA or root CA public key instead — they last 10-25 years.

No backup pin. If you pin only one cert and it needs emergency rotation, all users on the old app version lose access. Always have a backup pin.

Enabling pinning in debug builds. Certificate pinning breaks local proxy debugging (Charles, Proxyman). Disable pinning in debug mode:

if (!kDebugMode) {
  dio.interceptors.add(CertificatePinningInterceptor(hashes: pinnedHashes));
}

Sign in to like, dislike, or report.

Certificate pinning in Flutter — ANN Tech