flutter_ssl_pinning_client

Certificate and public key pinning for Dart IO HTTP requests.

Version 2.0.0 contains a security revision with breaking behavior changes. These guarantees do not describe the 1.1.1 implementation. Do not assume installing 1.1.1 includes these fixes.

Security model

The client first establishes a TLS connection using ordinary certificate chain, hostname and validity checks. It then checks the leaf certificate or its SubjectPublicKeyInfo (SPKI) fingerprint on that same socket before passing the socket to the HTTP transport. There is no separate preflight connection and no acceptance of invalid certificates through a callback.

Only configured domains receive additional pin checks. Other HTTPS hosts receive normal TLS validation, not pinning. Exact domain rules take precedence over wildcard rules. *.example.com matches one label, such as api.example.com, but not the apex or deeper subdomains. Rules are case insensitive and apply to every port for the host.

In strict mode a mismatched or empty pin set rejects the request. In reportOnly mode a pin mismatch emits callbacks but allows a normally valid TLS connection. Report mode never accepts an untrusted, expired or incorrectly named certificate.

This is not a security certification. Pinning cannot protect an app whose runtime or pin configuration has been compromised. It only covers traffic sent through this client, not WebViews, native SDKs or other network clients.

Installation and usage

Requires Dart 3.5 and Flutter 3.24 or later. Add the dependency and run flutter pub get:

dependencies:
  flutter_ssl_pinning_client: ^2.0.0
final config = SslPinningConfig(
  domainConfigs: {
    'api.example.com': const DomainPinConfig(
      domain: 'api.example.com',
      allowedSHA256Pins: {
        'REPLACE_WITH_VERIFIED_PRIMARY_PIN',
        'REPLACE_WITH_VERIFIED_BACKUP_PIN',
      },
    ),
  },
);
final client = SslPinningHttpClient(config: config);
try {
  final response = await client.get(Uri.parse('https://api.example.com/account'));
  // Handle response.statusCode, including returned redirects.
} finally {
  client.close();
}

Import package:flutter_ssl_pinning_client/flutter_ssl_pinning_client.dart. Placeholder pins intentionally reject connections. Use fingerprints verified through an independent authenticated channel and keep a backup key available for rotation.

Pin formats

Format Meaning
64 hexadecimal characters, optionally separated by colons SHA256 of the complete leaf certificate DER
Plain base64 SHA256 of the complete leaf certificate DER
sha256/<base64> SHA256 of the leaf SubjectPublicKeyInfo DER
sha256/<64 hexadecimal characters> Legacy certificate DER format, retained for compatibility

Base64 values are case sensitive. Hexadecimal values are not. Certificate renewal changes a certificate pin. An SPKI pin can survive renewal when the same key is retained. Only leaf pins are supported, not intermediate or root pins.

SslPinningHelper.extractServerFingerprint('https://api.example.com') returns certificate hex, base64, formattedPin, plus spkiBase64 and spkiPin. It validates TLS, sends no HTTP request and throws on errors instead of returning empty values. Network extraction is a development aid, not independent proof that a production pin is authentic.

Rotation and observability

Call config.updatePinsForDomain(host, newPins) to replace a pin set. Existing in flight requests retain their already verified connection; subsequent requests open a new connection and check the current pins. Use overlapping primary and backup pins. This package does not fetch, authenticate or persist remote configuration. Authenticate updates independently and protect against rollback before applying them.

onSecurityAuditEvent records pin checks after a successful TLS handshake. onPinningFailure records pin mismatches. Normal TLS failures surface as transport exceptions and do not produce a pin audit event. Callback exceptions reject the request; callbacks should be fast and should not throw. onCertificateNearExpiry uses the actual peer expiry unless a configured date overrides the notification date. That override never changes TLS validity enforcement.

Migration and limits

  • HTTP is rejected for every host, including unconfigured hosts.
  • Redirect responses are returned without being followed, even if the request asks to follow redirects. Validate the destination and issue a separate request, without forwarding credentials across origins.
  • Nonempty bypassPathPrefixes are rejected for matched hosts. Use a separate host and an explicit policy for public assets.
  • customHttpClient is deprecated and rejected. Use securityContext to explicitly configure trusted certificates or client credentials. Never weaken trust to make a failing test pass.
  • Connections are direct; HTTP proxies are unsupported. Each request uses a fresh connection, trading connection reuse performance for predictable pin rotation.
  • connectionTimeout defaults to 15 seconds and must be positive. It limits connection establishment, not the entire response body. Consume or cancel response streams and close the client when finished.
  • Dart IO only. Flutter Web is unsupported. Android and iOS device validation remains a release prerequisite.

Verification

Run flutter pub get, flutter analyze and flutter test. Integration tests require OpenSSL 3 on PATH. They generate a temporary local CA and server certificate, start a loopback HTTPS server and clean up the temporary test keys afterwards. No public service, production credentials or system trust changes are required.

Tests cover accepted certificates with wrong pins, valid certificate and SPKI pins, independent OpenSSL SPKI comparison, normal trust rejection, report mode, backup pins, runtime updates, empty pins, bypass rejection, redirects, HTTP rejection, helper extraction, wildcard boundaries and malformed DER. Physical device testing and independent security review are still needed before a release.

License

MIT. Olamilekan Adeyemi.