smart_dev_pinning_plugin 5.0.0 copy "smart_dev_pinning_plugin: ^5.0.0" to clipboard
smart_dev_pinning_plugin: ^5.0.0 copied to clipboard

This plugin creates a secure native TLS connection to execute HTTP requests with certificate pinning.

Smart Dev Pinning Plugin #

Certificate pinning for Flutter HTTP requests, with identical behavior on Android and iOS.

It supports pinning at the leaf or intermediate CA level, against either the certificate (full DER) or the public key (SPKI). Multiple hashes can be supplied as backup pins.

Requirements #

  • Flutter 3.3 / Dart 3.3 or newer
  • Android 5.0 (API 21) or newer
  • iOS 11 or newer

Install #

dependencies:
  smart_dev_pinning_plugin: ^4.0.0
flutter pub get

Quick start #

import 'package:smart_dev_pinning_plugin/smart_dev_pinning_plugin.dart';

final client = SecureClient();

final response = await client.httpRequest(
  method: 'GET',
  url: 'https://api.example.com/v1/profile',
  certificateHashes: ['/UzJAZYxLBnEpBwXAcmd4WHi7f8aYgfMExGnoyp5B04='],
  pinningMethod: PinningMethod.publicKey,
);

if (response.success) {
  print(response.data);
} else {
  print('Failed (${response.errorType}): ${response.error}');
}

SecureClient is a singleton; calling SecureClient() always returns the same instance. Requests run off the UI thread, so they never freeze the interface.

Pinning methods #

There are four strategies, picked with the pinningMethod argument. Which one you want depends on what you control and how often the certificate rotates.

Method Pins against When to use
publicKey Leaf public key (SPKI) Your own server; survives renewals that reuse the key
certificate Leaf certificate (full DER) Your own server; strictest, breaks on every renewal
intermediatePublicKey Intermediate CA public key Services behind a CDN/WAF (Cloudflare, Imperva, Akamai); most stable
intermediateCertificate Intermediate CA certificate (full DER) When you need a specific intermediate CA in the chain

For services behind a CDN or WAF the leaf certificate often changes between edge nodes and rotates frequently, so pinning the intermediate's public key is usually the practical choice — those keys tend to last years.

For the intermediate methods the plugin also verifies that the leaf actually chains (with valid signatures) to the pinned intermediate, and that the leaf's SAN/CN matches the host. Pinning a public intermediate alone is not enough to be accepted.

Generating the hash #

The hash is the base64 of a SHA-256 digest. There's one command per pinning method — pick the one that matches the PinningMethod you'll use.

Use the bare host and port (example.com:443) in -connect. Do not prefix it with https:// — that resolves to a different host and produces a hash that won't match what the plugin computes at runtime.

1. Leaf public key — for PinningMethod.publicKey

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64

2. Leaf certificate — for PinningMethod.certificate

openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | openssl x509 -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64

The two intermediate methods use the second certificate in the chain (-showcerts, index 1). The awk step selects it.

3. Intermediate public key — for PinningMethod.intermediatePublicKey

openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ if(/BEGIN/){n++}; if(n==2) print }' \
  | openssl x509 -pubkey -noout \
  | openssl pkey -pubin -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64

4. Intermediate certificate — for PinningMethod.intermediateCertificate

openssl s_client -showcerts -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
  | awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/{ if(/BEGIN/){n++}; if(n==2) print }' \
  | openssl x509 -outform DER \
  | openssl dgst -sha256 -binary \
  | openssl base64

Backup pins #

Pass more than one hash and the connection is accepted if any of them matches. This is how you roll certificates without an app update: ship the next certificate's hash alongside the current one before the rotation happens.

await client.httpRequest(
  method: 'GET',
  url: 'https://api.example.com/v1/ping',
  certificateHashes: [currentHash, nextHash],
  pinningMethod: PinningMethod.publicKey,
);

Request body and methods #

GET, POST, PUT, DELETE, PATCH, HEAD and OPTIONS are supported. An unrecognized method comes back as InvalidMethodError rather than silently falling back.

body accepts a String or a Map (encoded to JSON). encoding defaults to 'json', which sets Content-Type: application/json and forwards the body as is; any other value sends the raw body without forcing that header.

await client.httpRequest(
  method: 'POST',
  url: 'https://api.example.com/v1/users',
  headers: {'Authorization': 'Bearer $token'},
  body: {'name': 'Ada', 'email': 'ada@example.com'},
  certificateHashes: [hash],
  pinningMethod: PinningMethod.certificate,
);

Timeout #

There's a 30 second total timeout and a 10 second connection timeout by default. The total can be overridden per request; the connection timeout is fixed.

await client.httpRequest(
  method: 'GET',
  url: 'https://api.example.com/v1/slow',
  certificateHashes: [hash],
  pinningMethod: PinningMethod.publicKey,
  timeout: const Duration(seconds: 15),
);

Binary responses #

Text responses come back in response.data as before. When the body isn't valid UTF-8 (an image, protobuf, etc.) it's returned base64-encoded and flagged, so nothing is corrupted by a lossy text decode. Use dataBytes to get the raw bytes regardless of type:

final res = await client.httpRequest(/* ... */);
if (res.success) {
  if (res.isBinary) {
    final Uint8List? bytes = res.dataBytes; // base64 decoded
  } else {
    final String? text = res.data;
  }
}

Error handling #

Network and pinning failures are not thrown — they come back in the SmartResponse with success: false and an errorType. The only exceptions are argument validation (ArgumentError) and running on an unsupported platform (UnsupportedError).

final res = await client.httpRequest(/* ... */);

if (!res.success) {
  switch (res.errorType) {
    case 'ConnectionError':   // pinning mismatch, TLS failure, timeout, DNS
    case 'HttpError':         // non-2xx response; res.statusCode is set
    case 'SSLPinningError':   // the pinned client could not be built
    default:
      print('${res.errorType}: ${res.error}');
  }
}
errorType Cause
ConnectionError Pinning mismatch, TLS handshake failure, timeout, or DNS error
HttpError Server returned a non-2xx status (statusCode is set)
SSLPinningError The pinned TLS client could not be created
RequestError Other request-level failure (e.g. malformed URL)
ResponseReadError The response body could not be read
InvalidMethodError Unsupported HTTP method
ValidationError Missing URL or certificate hash
JSONParseError The response could not be parsed

A pinning mismatch surfaces as ConnectionError rather than a dedicated type, because it's reported as a connection-level failure. Release builds return a generic message so the failure reason isn't leaked; the specific detail (e.g. hash does not match) is only included in debug builds.

API reference #

SecureClient.httpRequest #

Future<SmartResponse> httpRequest({
  required String method,
  required String url,
  Object? body,
  Map<String, String>? headers,
  String? encoding,
  required List<String> certificateHashes,
  required PinningMethod pinningMethod,
  Duration? timeout,
})
Parameter Type Notes
method String GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS
url String Must be https; required
body Object? String or Map (JSON-encoded)
headers Map<String, String>? Request headers
encoding String? Defaults to 'json'
certificateHashes List<String> One or more base64 SHA-256 hashes; required, non-empty
pinningMethod PinningMethod Validation strategy; required
timeout Duration? Total request timeout; defaults to 30s

SmartResponse #

Field Type Notes
success bool true for a 2xx response
data String? Response body (base64 when isBinary)
isBinary bool true when data is base64-encoded binary
dataBytes Uint8List? Body as raw bytes
statusCode int? HTTP status code
error String? Error message when success is false
errorType String? See the table above

PinningMethod #

enum PinningMethod {
  certificate,             // leaf certificate DER hash
  publicKey,               // leaf public key (SPKI) hash
  intermediateCertificate, // intermediate CA certificate DER hash
  intermediatePublicKey,   // intermediate CA public key (SPKI) hash
}

What gets verified #

During the TLS handshake, for every request, the plugin:

  • checks the certificate or public key against your pin(s);
  • validates the handshake signature, so presenting a public certificate alone is not enough to impersonate the server;
  • checks the certificate's validity dates.

For intermediate pinning it additionally verifies that the leaf certificate chains to the pinned intermediate and that its host matches the request.

Platform support #

Platform Supported
Android Yes
iOS Yes
Web / Desktop No

Notes on pinning in production #

  • Prefer public-key pinning over full-certificate pinning so renewals that keep the same key don't break the app.
  • Always ship at least one backup pin and rotate it ahead of time.
  • For CDN/WAF-backed services pin the intermediate's public key; leaf certificates there change often and vary by edge node.
  • Test against staging before shipping a pin change to production.

License #

MIT. See LICENSE.

A changelog is kept in CHANGELOG.md.

4
likes
160
points
153
downloads

Documentation

API reference

Publisher

verified publishersmart-dev.com.co

Weekly Downloads

This plugin creates a secure native TLS connection to execute HTTP requests with certificate pinning.

Homepage

License

MIT (license)

Dependencies

ffi, flutter, plugin_platform_interface

More

Packages that depend on smart_dev_pinning_plugin

Packages that implement smart_dev_pinning_plugin