Flutter SDK
Integrate TrustPin into your Flutter application for cross-platform certificate pinning on iOS, Android, and macOS.
Current version: trustpin_sdk 6.3.0
(bundles TrustPinKit 6.3.x for iOS/macOS and cloud.trustpin:kotlin-sdk 6.3.x for Android, where native dependencies accept patch updates within the 6.3.x line)
Platform Requirements
| Platform | Minimum Version |
|---|---|
| iOS | 15.0+ |
| Android | API 25+ |
| macOS | 13.0+ |
Additional Requirements:
- iOS/macOS: Swift 6.1+, Xcode 16.3+
- Android: Kotlin 2.3.0+, Java 11+
- macOS: network client entitlement required for sandboxed apps
Installation
pub.dev (Recommended)
Add TrustPin to your pubspec.yaml:
dependencies:
trustpin_sdk: ^6.3.0Then install:
flutter pub getQuick Start
1. Get Your Credentials
Sign in to the TrustPin Dashboard and retrieve:
- Organization ID
- Project ID
- Public Key (Base64-encoded)
2. Initialize TrustPin
setupWithNativeBundle() tells each platform’s native SDK to read its own bundled configuration file, so credentials never enter the Dart isolate.
Drop platform-native config files into your app:
iOS uses ios/Runner/TrustPin-Info.plist:
| Key | Type | Required | Notes |
|---|---|---|---|
OrganizationId | String | Yes | Non-empty |
ProjectId | String | Yes | Non-empty |
PublicKey | String | Yes | Base64-encoded |
Mode | String | No | "strict" (default) or "permissive", lowercase |
ConfigurationURL | String | No | Must be HTTPS |
EmbeddedConfigurationFile | String | No | Resource name of a bundled signed configuration. See Embedded Configuration |
After adding the file, open the project in Xcode and confirm Target Membership is checked for the
Runnertarget. Otherwise the plist won’t be copied into the app bundle andsetupWithNativeBundle()will fail.
macOS uses macos/Runner/TrustPin-Info.plist (same keys as iOS, same Target Membership requirement).
For sandboxed macOS apps, add the network client entitlement to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>Android uses android/app/src/main/assets/trustpin.json:
{
"organization_id": "your-org-id",
"project_id": "your-project-id",
"public_key": "LS0tLS1CRUdJTi...",
"mode": "strict",
"configuration_url": "https://your-server.com/pins.jws"
}An optional embedded_configuration_asset key names a bundled signed configuration. See Embedded Configuration.
Gradle includes the JSON in the APK automatically, so no pubspec.yaml assets: entry is needed. The plugin’s own AndroidManifest.xml already declares the required network permission.
Make sure your consuming app declares minSdk 25 (or higher) in android/app/build.gradle:
android {
defaultConfig {
minSdk 25
}
}Then call setupWithNativeBundle() during app startup:
import 'package:flutter/material.dart';
import 'package:trustpin_sdk/trustpin_sdk.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
try {
await TrustPin.shared.setupWithNativeBundle();
// setupWithNativeBundle() is non-blocking. Gate on
// awaitConfiguration() to fail closed before the first pinned request.
await TrustPin.shared.awaitConfiguration();
print('TrustPin initialized successfully');
} on TrustPinException catch (e) {
print('TrustPin initialization failed: ${e.code} - ${e.message}');
}
runApp(const MyApp());
}Non-blocking setup:
setup()andsetupWithNativeBundle()now return as soon as configuration loading begins.await TrustPin.shared.awaitConfiguration()blocks until a validated configuration is available (and throws on failure/timeout);TrustPin.shared.isConfigurationLoadedis a non-throwingFuture<bool>status check. Skip the gate and the first pinned request simply waits for the configuration to become ready. Setup is also one-shot: calling it a second time on the same instance throwsALREADY_INITIALIZED. Create a separate named instance viaTrustPin.instance(id)for a different pinning context.
Per-environment file names
setupWithNativeBundle() accepts optional per-platform filenames so you can ship different credentials per build flavor. null (the default) tells each native SDK to use its standard filename (TrustPin-Info.plist / trustpin.json):
await TrustPin.shared.setupWithNativeBundle(
iosFileName: 'TrustPin-Staging.plist',
macosFileName: 'TrustPin-Staging.plist',
androidFileName: 'trustpin-staging.json',
);Embedded Configuration
TrustPin fetches its signed pinning configuration online and keeps the last validated one on the device. For the one case where neither exists, the app’s very first start while every configuration source is unreachable, you can ship a signed configuration inside the app as a last-resort fallback.
Download the signed configuration for your project from the dashboard and add it to both platforms under the same file name:
- iOS / macOS:
ios/Runner/trustpin-seed.b64(andmacos/Runner/trustpin-seed.b64), added to the target’s Copy Bundle Resources phase. - Android:
android/app/src/main/assets/trustpin-seed.b64.
This is not a Flutter asset. Files listed under
assets:inpubspec.yamllive insideflutter_assets/and are invisible to the native bundle and asset loaders. Ship it per platform, exactly likeTrustPin-Info.plistandtrustpin.json.
Then reference it by name:
final config = TrustPinConfiguration(
organizationId: 'your-org-id',
projectId: 'your-project-id',
publicKey: 'LS0tLS1CRUdJTi...',
embeddedConfigurationFile: 'trustpin-seed.b64',
);
await TrustPin.shared.setup(config);With setupWithNativeBundle(), declare it in the platform config file instead (EmbeddedConfigurationFile in the plist, embedded_configuration_asset in trustpin.json). No Dart change is needed.
Requirements
- Use it only in apps protected by RASP (runtime application self-protection) that guards bundled resources against modification. An unprotected app must not ship an embedded configuration.
- The file must be the unmodified signed payload downloaded from the dashboard for this project. It is verified against
publicKeyduring setup, and a file that is missing, unreadable, or that fails verification throwsTrustPinExceptionwith codeINVALID_PROJECT_CONFIG. - Regenerate it in CI on every release, so it is never older than the app that ships it. Pins expire on their own schedule, and an embedded configuration whose pins have all expired is equivalent to having no fallback.
trustpin-cli projects jwswrites the currently published payload to a file, so a release pipeline can refresh both bundled copies on every build.
Behaviour
- It is never preferred over an online source, or over a configuration the SDK has already fetched and validated.
- It is subject to the same integrity checks as any other configuration: a device that has already trusted a newer configuration will not accept an older embedded one.
- The iOS, Android, and React Native SDKs expose the equivalent option. The file format is identical across platforms.
Validating Connections
TrustPin.shared.validateConnection() is the recommended way to check a host against the active configuration without going through your HTTP client. It performs a TLS handshake and verifies the leaf certificate against your pins.
Future<void> checkServer(String host) async {
try {
await TrustPin.shared.validateConnection(
host,
timeout: const Duration(seconds: 5),
);
print('Connection is allowed by the configured pins.');
} on TrustPinException catch (e) {
print('Validation failed: ${e.code} - ${e.message}');
}
}| Parameter | Type | Default |
|---|---|---|
host | String | None (positional, required) |
port | int | 443 |
timeout | Duration | const Duration(seconds: 30) |
Note: The older
fetchCertificate()andverify()pair is deprecated and will be removed in a future major release. Migrate tovalidateConnection().
Integration Approaches
| Approach | Best For | Setup Complexity |
|---|---|---|
| Dio HTTP Client (Recommended) | Most Flutter apps | 🟢 Low |
Standard http Client | Apps using package:http | 🟢 Low |
Manual validateConnection() | Custom transports, pre-flight checks | 🟡 Medium |
Dio HTTP Client (Recommended)
TrustPinDioInterceptor validates every request before it leaves the device:
import 'package:dio/dio.dart';
import 'package:trustpin_sdk/trustpin_sdk.dart';
final dio = Dio();
dio.interceptors.add(TrustPinDioInterceptor());
try {
final response = await dio.get('https://api.example.com/data');
print('Request successful: ${response.statusCode}');
} on DioException catch (e) {
if (e.error is TrustPinException) {
final trustPinError = e.error as TrustPinException;
print('Pinning failed: ${trustPinError.code}');
}
}Standard http Package
Create a pinned http.Client via TrustPinHttpClient.create():
import 'package:http/http.dart' as http;
import 'package:trustpin_sdk/trustpin_sdk.dart';
final client = TrustPinHttpClient.create();
try {
final response = await client.get(Uri.parse('https://api.example.com/data'));
print('Request successful: ${response.statusCode}');
} on TrustPinException catch (e) {
print('Pinning failed: ${e.code} - ${e.message}');
} finally {
client.close();
}Named Instances (Multi-Tenant)
If your app talks to multiple TrustPin projects, or if you’re shipping a library that needs its own isolated pinning configuration, ship one set of bundled config files per instance and load them via setupWithNativeBundle() on a named instance. Both TrustPinDioInterceptor and TrustPinHttpClient.create() accept an instance: parameter to bind to a specific named instance:
final pin = TrustPin.instance('com.mylib.networking');
await pin.setupWithNativeBundle(
iosFileName: 'TrustPin-MyLib.plist',
macosFileName: 'TrustPin-MyLib.plist',
androidFileName: 'trustpin-mylib.json',
);
// Dio bound to this instance
dio.interceptors.add(TrustPinDioInterceptor(instance: pin));
// http.Client bound to this instance
final client = TrustPinHttpClient.create(instance: pin);Repeated calls to TrustPin.instance('id') with the same ID return the same instance.
Logging
Set the desired verbosity before calling setup() to capture initialization logs:
await TrustPin.shared.setLogLevel(TrustPinLogLevel.info);Available levels: none, error, info, debug.
Log Stream
TrustPin.logs is a static broadcast stream of TrustPinLogEvent (level, instanceId, message) that routes SDK log output into your app’s logging pipeline. One stream covers all instances; the native sink is installed on first listen and removed on last cancel. While no one listens, the SDK keeps logging to its platform default. Per-instance verbosity remains controlled by setLogLevel:
final sub = TrustPin.logs.listen((event) {
myLogger.log('[${event.instanceId}] ${event.message}');
});Monitoring Pin Validation
TrustPin.validationEvents is a static broadcast stream of TrustPinValidationEvent surfacing the native SDKs’ validation telemetry, the signal to use for reporting suspected MITM attempts to your backend:
final sub = TrustPin.validationEvents.listen((event) {
if (!event.isSuccess) {
// Definitive failure verdicts only: PINS_MISMATCH, ALL_PINS_EXPIRED,
// DOMAIN_NOT_REGISTERED (strict mode). Failure events carry the
// presented leaf certificate as PEM. Treat it as untrusted input.
securityMonitor.report(event);
}
});Events are observe-only: the verdict is decided before the event is emitted, so a listener cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no events. One stream covers all instances; the native listener is installed on first listen and removed on last cancel.
Error Handling
All SDK errors surface as TrustPinException, which exposes a code and a message plus convenience getters for the common cases:
| Getter | True when |
|---|---|
isDomainNotRegistered | Strict mode and the host isn’t in the configuration |
isPinsMismatch | Server certificate doesn’t match any active pin |
isAllPinsExpired | Configuration is stale. Rotate pins in the dashboard |
isInvalidServerCert | Server returned an unparseable certificate |
isInvalidProjectConfig | Bad credentials or invalid configuration |
isAlreadyInitialized | setup() was called a second time on the same instance |
isErrorFetchingPinningInfo | Network failure while loading the configuration |
isConfigurationValidationFailed | JWS signature didn’t verify against the project’s public key |
isConfigIntegrityFailed | Configuration failed an integrity check. Hard stop |
isFetchCertificateTimeout | Connection timed out during certificate retrieval |
isSetupInProgress | Android only: an operation raced a setup() that hadn’t finished |
isLockTimeout | Android only: an internal lock couldn’t be acquired in time |
isSslContextSetupFailed | Android only: the pinned SSL context couldn’t be created |
isUnsupportedDevice | Android only: the runtime lacks required security primitives |
try {
await TrustPin.shared.validateConnection('api.example.com');
} on TrustPinException catch (e) {
if (e.isPinsMismatch) {
// Show a network-error UI and refuse the request.
} else if (e.isDomainNotRegistered) {
// Either the host is unexpected, or the configuration is out of date.
} else {
// Log everything else with e.code and e.message
}
}Best Practices
Setup & Initialization
- Call
WidgetsFlutterBinding.ensureInitialized()before initializing TrustPin. - Initialize in
main()before running the app. - Set the log level first to capture initialization output.
- Handle setup errors gracefully. Don’t block app launch.
Security
- Use
TrustPinMode.strictin production. - Prefer SPKI pinning; rotate pins in the dashboard before they expire.
- Monitor pin validation failures via the
TrustPin.validationEventsstream. - Keep credentials outside source control. Prefer
setupWithNativeBundle()with platform-native config files (TrustPin-Info.plist/trustpin.json), usingiosFileName/androidFileName/macosFileNamefor per-flavor overrides. Credentials never enter the Dart isolate this way. - Use HTTPS for all pinned domains.
- Ship an embedded configuration only if the app is protected by RASP, and regenerate it in CI on every release.
Performance
- Configuration is cached for 10 minutes with automatic refresh.
- Reuse HTTP client instances rather than creating one per request.
- Use minimal log levels in production.
Complete Documentation
For the full API reference, advanced configuration, macOS sandbox setup, and the example app, visit:
TrustPin Flutter API Reference
Resources
- Repository: github.com/trustpin-cloud/flutter.sdk
- Package: pub.dev/packages/trustpin_sdk
- API Reference: trustpin-cloud.github.io/flutter.sdk
- Dashboard: app.trustpin.cloud
- Support: support@trustpin.cloud