secure_pinning
Certificate pinning for Flutter. package:http/Dio/raw-HttpClient pinning works anywhere dart:io's TLS stack runs — Android, iOS, macOS, Windows, and Linux. The native SecurePinning.check() probe API is available on Android, iOS, and macOS. Web is permanently unsupported for everything (browsers never expose TLS certificate bytes to page JavaScript).
Why another certificate-pinning plugin?
Most Flutter HTTP traffic (package:http's default client, Dio's default adapter) runs on dart:io's own TLS stack, not on OkHttp or NSURLSession. secure_pinning's core mechanism validates the actual connection your request uses — no separate preflight connection, no double round-trip — via dart:io's HttpClient.badCertificateCallback. See docs/SECURITY_MODEL.md for the full design rationale, SPKI vs. leaf vs. CA pinning tradeoffs, and what certificate pinning does and doesn't protect against.
Features
- Three pinning modes —
PinningMode.spki(public-key hash, survives certificate renewal, the default),PinningMode.legacyLeafHash(whole-certificate hash, for compatibility with plugins that pin the full leaf certificate instead of just its public key), andPinningMode.legacyCaHash(CA/root pinning, gated behind a requiredacknowledgedRiskjustification). - Validates the real connection — the pure-Dart engine hooks
dart:io'sHttpClient.badCertificateCallbackdirectly, sopackage:httpand Dio traffic is checked on the actual request with no separate preflight round trip. - SHA-256 and SHA-1 hash algorithm support (
HashAlgorithm). - Backup-pin support — list multiple pins so a future key rotation doesn't require an app update.
- Configurable connect/read timeouts, enforced even where
dart:iohas no native read-timeout concept. - A typed exception hierarchy —
SecurePinningValidationException,SecurePinningTimeoutException,SecurePinningConfigurationException,SecurePinningUnsupportedPlatformException,SecurePinningNetworkException— consistent across every platform and integration surface, never a rawHandshakeException/PlatformException. package:httpand Dio integrations (secure_pinning_http,secure_pinning_dio) as separate opt-in packages — pull in only what you use.- A native probe API (
SecurePinning.check()+SecurePinning.isPlatformSupported()) for one-off/out-of-band checks, and the only path that can validatePinningMode.legacyCaHash(which requires walking the full certificate chain). Available on Android, iOS, and macOS. - Federated plugin architecture — the core
secure_pinningpackage has zero third-party dependencies beyondcrypto; native platform code lives in its own package per platform. - Platform coverage —
package:http/Dio/raw-HttpClientpinning works on Android, iOS, macOS, Windows, and Linux (anywheredart:io's TLS stack runs); the native probe API is Android/iOS/macOS only; Web is permanently unsupported for everything (browsers never expose TLS certificate bytes to page JavaScript).
Packages
| Package | Purpose |
|---|---|
secure_pinning (this repo's root package) |
Core: config, validation engine, raw HttpClient factory, typed exceptions, native probe API. Zero third-party dependencies. |
secure_pinning_http |
package:http-compatible BaseClient wrapper. |
secure_pinning_dio |
Dio interceptor-shaped integration. |
secure_pinning_platform_interface |
Pigeon-generated platform channel schema. |
secure_pinning_android |
Android (Kotlin/javax.net.ssl) native probe engine. |
secure_pinning_apple |
iOS + macOS (Swift/Security.framework) native probe engine, one shared package. |
Windows, Linux, and Web don't have a secure_pinning_* platform package —
package:http/Dio/raw-HttpClient pinning still works on Windows and
Linux regardless (it's pure-Dart, not gated by a platform package at all),
but calling the native SecurePinning.check() probe on those two, or on
Web, isn't supported. See Platform coverage above.
Installation
Add whichever integration package(s) match your HTTP stack — you don't need all of them:
flutter pub add secure_pinning_http # package:http
flutter pub add secure_pinning_dio # Dio
flutter pub add secure_pinning # raw HttpClient / native probe only, no HTTP client wrapper
secure_pinning_http and secure_pinning_dio both depend on
secure_pinning automatically — you don't need to add it yourself unless
you're using the raw client or the native probe API directly.
Getting your pins
Every usage example below needs at least one SPKI pin for the host you're pinning, as a hex string (colon-tolerant, case-insensitive — not base64). Compute it with:
openssl s_client -connect HOST:443 -servername HOST < /dev/null 2>/dev/null \
| openssl x509 -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -hex \
| awk '{print $NF}'
Always include a second, backup pin (e.g. the SPKI hash of the issuing CA, or a planned replacement key) — a single pin means any future key rotation on the server locks your app out until an update ships.
Usage
The easiest path for most apps is secure_pinning_http or
secure_pinning_dio — pick whichever matches your existing HTTP stack.
Both are thin wrappers with one required call to get pinning working.
package:http
import 'package:secure_pinning_http/secure_pinning_http.dart';
final client = SecurePinningHttpClient.forHost(
'api.example.com',
pins: ['hex-spki-hash-1', 'hex-spki-hash-2'], // include a backup pin
);
final response = await client.get(Uri.https('api.example.com', '/'));
SecurePinningHttpClient is a full http.BaseClient — get, post,
put, patch, delete, head, read, readBytes, and streaming via
send() all work as usual, validated against the pinned connection.
Dio
import 'package:dio/dio.dart';
import 'package:secure_pinning_dio/secure_pinning_dio.dart';
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'));
dio.interceptors.add(
SecurePinningInterceptor.forHost(
dio,
'api.example.com',
pins: ['hex-spki-hash-1', 'hex-spki-hash-2'],
),
);
final response = await dio.get('/');
Add SecurePinningInterceptor before making any request on dio — it
installs a pinned IOHttpClientAdapter at construction time.
Raw HttpClient / a custom stack
If you're not using package:http or Dio, SecurePinning.createHttpClient
gives you a pinned dart:io HttpClient directly:
import 'package:secure_pinning/secure_pinning.dart';
final config = SecurePinningConfig(
host: 'api.example.com',
pins: ['hex-spki-hash-1', 'hex-spki-hash-2'],
);
final client = SecurePinning.createHttpClient(config);
final request = await client.getUrl(Uri.https('api.example.com', '/'));
final response = await SecurePinning.enforceReadTimeout(
request.close(),
config.readTimeout,
);
enforceReadTimeout is needed because dart:io's HttpClient has no
built-in read-timeout concept the way OkHttp/URLSession do — both
integration packages above already call it internally.
One-off checks and CA/root pinning
SecurePinning.check() validates a pin set against a URL independently
of any HTTP request — useful for a startup connectivity check, or as the
only way to validate PinningMode.legacyCaHash (CA-level pinning
requires walking the full certificate chain, which the pure-Dart engine
above can't do):
import 'package:secure_pinning/secure_pinning.dart';
final result = await SecurePinning.check(
url: 'https://api.example.com',
config: SecurePinningConfig(
host: 'api.example.com',
pins: ['hex-spki-hash-1', 'hex-spki-hash-2'],
),
);
print(result.isTrusted ? 'Trusted' : 'Rejected: ${result.errorCode}');
A pin mismatch here is returned as data (result.isTrusted == false),
not thrown — only genuine infrastructure failures (DNS failure,
malformed config) throw. Check SecurePinning.isPlatformSupported()
first if you need to know whether it's available — Android, iOS, and
macOS only; unlike package:http/Dio/raw-HttpClient pinning, this
probe doesn't work on Windows or Linux.
Handling failures
All three integrations above throw the same typed exception hierarchy on
a pinning or connectivity failure — catch SecurePinningException (or a
specific subtype) instead of a raw HandshakeException/SocketException:
try {
final response = await client.get(Uri.https('api.example.com', '/'));
} on SecurePinningValidationException catch (e) {
// The presented certificate didn't match the configured pins.
} on SecurePinningTimeoutException catch (e) {
// Connect or read timed out.
} on SecurePinningException catch (e) {
// Any other pinning-related failure — see e.message.
}
See each package's README for its full API, and example for
a runnable app covering the raw client, package:http, Dio, and native
probe integrations side by side.
Using the example app
cd example && flutter pub getflutter runand pick a connected device or simulator.- Enter a Host and Pins — the shipped values are placeholders that
won't validate; see
example/README.mdfor theopensslone-liner to compute real SPKI pins for your own host. - Pick a pinning mode from the segmented selector (SPKI, Legacy leaf hash, or Legacy CA hash — selecting CA hash reveals a required "acknowledged risk" field).
- Tap Raw HttpClient, package:http, Dio, or Native probe
(check()) to run a pinned request through that integration surface.
Results (or the typed exception thrown) appear in the log below —
note that CA hash only actually validates through the native probe
button; the other three correctly reject it with
SecurePinningConfigurationException, since the pure-Dart engine can't walk the full certificate chain that CA-level pinning requires.
License
Apache-2.0 — see LICENSE.
Libraries
- secure_pinning
- Certificate pinning for Flutter with zero third-party dependencies in this package: SPKI (public key) pinning by default, a pure-Dart engine that validates the real connection your app uses, and a typed exception hierarchy consistent across every platform.