iOS / macOS SDK
Integrate TrustPin into your Apple platform application with native certificate pinning protection.
Current version: TrustPinKit 6.3.0
Platform Requirements
| Platform | Minimum Version |
|---|---|
| iOS | 15.0+ |
| macOS | 13.0+ |
| watchOS | 8.0+ |
| tvOS | 15.0+ |
| Mac Catalyst | 15.0+ |
| visionOS | 2.0+ |
Swift Version: 6.1+ (Xcode 16.3+; async/await is required)
Installation
Swift Package Manager (Recommended)
Add TrustPin to your project in Xcode:
- Go to File → Add Package Dependencies
- Enter the repository URL:
https://github.com/trustpin-cloud/swift.sdk - Select version 6.3.0 or later (Up to Next Major).
The package vends two products:
| Product | What it is |
|---|---|
TrustPinKit | The SDK (binary framework). All you need for URLSession-based apps |
TrustPinKitAlamofire | Optional Alamofire adapter. Add it only if your app networks through Alamofire |
Package.swift
For command-line projects:
dependencies: [
.package(url: "https://github.com/trustpin-cloud/swift.sdk", from: "6.3.0")
],
targets: [
.target(
name: "YourApp",
dependencies: [
.product(name: "TrustPinKit", package: "swift.sdk"),
// Only when using Alamofire:
.product(name: "TrustPinKitAlamofire", package: "swift.sdk")
]
)
]CocoaPods
Add to your Podfile:
pod 'TrustPinKit', '~> 6.3'Then run:
pod installThe
TrustPinKitAlamofireadapter is distributed via Swift Package Manager only.
Quick Start
1. Get Your Credentials
Sign in to the TrustPin Dashboard and retrieve:
- Organization ID
- Project ID
- Public Key (Base64-encoded)
2. Initialize TrustPin
Ship a TrustPin-Info.plist in your app bundle and load it with TrustPinConfiguration.fromPlist(). Credentials stay out of source.
The plist must contain the following keys:
| Key | Type | Required | Notes |
|---|---|---|---|
OrganizationId | String | Yes | Non-empty |
ProjectId | String | Yes | Non-empty |
PublicKey | String | Yes | Base64-encoded ECDSA P-256 public key |
Mode | String | No | "strict" (default) or "permissive", lowercase |
ConfigurationURL | String | No | Must be HTTPS. Overrides the default CDN endpoint |
EmbeddedConfigurationFile | String | No | Resource filename (including extension) of a bundled signed configuration, resolved in the same bundle. See Embedded Configuration |
Add this to your app’s initialization (e.g., AppDelegate or @main struct):
import TrustPinKit
class AppDelegate: UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
Task {
do {
let config = try TrustPinConfiguration.fromPlist()
try await TrustPin.setup(config)
print("TrustPin initialized")
} catch {
print("TrustPin setup failed: \(error)")
}
}
return true
}
}TrustPinConfiguration.fromPlist() throws TrustPinErrors.invalidProjectConfig if any required key is missing or malformed.
Fail-closed startup gate
TrustPin.setup(_:) is non-blocking. It kicks off configuration loading and returns without waiting for the signed configuration to download and validate. If you need to guarantee that a validated configuration is in place before issuing pinned requests (fail closed), await awaitConfiguration(timeout:) after setup:
let config = try TrustPinConfiguration.fromPlist()
try await TrustPin.setup(config)
// Block until a validated configuration is loaded (default timeout: 30s).
// Throws if loading fails or times out.
try await TrustPin.awaitConfiguration()If you skip the gate, the first pinned request simply waits for the configuration to become ready on its own.
Per-environment plists
Point the factory at a different bundle or filename to ship different credentials per scheme:
#if DEBUG
let config = try TrustPinConfiguration.fromPlist(fileName: "TrustPin-Info-Debug.plist")
#else
let config = try TrustPinConfiguration.fromPlist(fileName: "TrustPin-Info.plist")
#endif
try await TrustPin.setup(config)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, an app’s very first start while every configuration source is unreachable, you can ship a signed configuration inside the app bundle as a last-resort fallback.
Download the signed configuration for your project from the dashboard , add it to the app target’s Copy Bundle Resources phase, then point the configuration at it:
let seed = Bundle.main.url(forResource: "trustpin-seed", withExtension: "b64")
try await TrustPin.setup(TrustPinConfiguration(
organizationId: "your-org-id",
projectId: "your-project-id",
publicKey: "your-base64-public-key",
embeddedConfigurationURL: seed
))embeddedConfigurationURL must be a file: URL pointing inside a loaded bundle. With TrustPin-Info.plist, set EmbeddedConfigurationFile to the resource filename instead and leave the call site unchanged.
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, andsetup()throwsTrustPinErrors.invalidProjectConfigif the file is not a bundled resource, cannot be read, or does not verify. - 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 the bundled copy 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 Android, Flutter, and React Native SDKs expose the equivalent option. The file format is identical across platforms.
Integration Approaches
TrustPin offers four integration methods:
| Approach | Best For | Setup Complexity |
|---|---|---|
| URLSessionDelegate (Recommended) | Most applications, precise control | 🟢 Low |
| Alamofire Adapter | Apps networking through Alamofire | 🟢 Low |
| System-Wide URLProtocol | Third-party library protection, legacy code | 🟡 Medium |
| Helper Methods | One-off requests, explicit control | 🟠 High |
URLSessionDelegate (Recommended)
Bind a URLSession to a TrustPin-backed delegate produced by the SDK:
import TrustPinKit
class NetworkManager {
private lazy var session: URLSession = {
let delegate = TrustPin.makeURLSessionDelegate()
return URLSession(
configuration: .default,
delegate: delegate,
delegateQueue: nil
)
}()
func fetchData() async throws -> Data {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await session.data(from: url)
return data
}
}Already have your own session delegate? Compose them with TrustPin.makeURLSessionDelegate(forwardingTo:). Pinning answers server-trust challenges, and every other callback reaches your delegate unchanged:
let delegate = TrustPin.makeURLSessionDelegate(forwardingTo: myExistingDelegate)Alamofire Adapter
The optional TrustPinKitAlamofire product (SPM only) wires TrustPin into Alamofire’s ServerTrustManager in one line:
import Alamofire
import TrustPinKit
import TrustPinKitAlamofire
// After TrustPin.setup(...):
let session = Session(serverTrustManager: ServerTrustManager(evaluators: [
"api.example.com": TrustPinServerTrustEvaluating()
]))
// Named instances and a custom per-evaluation timeout are supported:
let pinned = TrustPinServerTrustEvaluating(instance: try TrustPin.instance(id: "payments"),
timeout: 15)The evaluator blocks Alamofire’s session delegate queue while verification runs (bounded by timeout). The first handshake after launch may include the pinning-configuration fetch inside that window. Call try await TrustPin.awaitConfiguration() once at startup to keep handshakes fast.
System-Wide URLProtocol
Register TrustPinURLProtocol to apply pinning to every request that goes through the default URL Loading System, useful when third-party libraries don’t expose a URLSession you control.
You can either register automatically during setup, or register/unregister manually:
let config = try TrustPinConfiguration.fromPlist()
try await TrustPin.setup(config, autoRegisterURLProtocol: true)
// Or manage registration explicitly:
TrustPin.registerURLProtocol()
TrustPin.unregisterURLProtocol()Helper Methods
TrustPinURLProtocol exposes convenience helpers for individual requests:
let (data, _) = try await TrustPinURLProtocol.data(from: url)
let (fileURL, _) = try await TrustPinURLProtocol.download(for: request)Manual Verification
For custom transports or one-off checks, validate a domain/certificate pair directly:
try await TrustPin.verify(
domain: "api.example.com",
certificate: pemEncodedCertificate
)Named Instances (Multi-Tenant)
If your app talks to multiple TrustPin projects (for example, separate consumer and admin backends), ship one plist per project and create independent instances:
let customerConfig = try TrustPinConfiguration.fromPlist(fileName: "TrustPin-Customer.plist")
let customerApi = try TrustPin.instance(id: "customer-api")
try await customerApi.setup(customerConfig)
let adminConfig = try TrustPinConfiguration.fromPlist(fileName: "TrustPin-Admin.plist")
let adminApi = try TrustPin.instance(id: "admin-api")
try await adminApi.setup(adminConfig)The id must be non-empty and not equal to "default"; otherwise TrustPinErrors.invalidProjectConfig is thrown. Repeated calls with the same id return the same instance.
Logging
Set the desired verbosity before calling setup() to capture initialization logs:
await TrustPin.set(logLevel: .debug)Available levels: .none, .error, .info, .debug.
Custom Log Sink
By default, log output goes to unified logging (os.Logger, subsystem cloud.trustpin.swift, category = instance id). To route messages into your own logging pipeline instead, install a global TrustPinLogSink. One sink serves all instances and receives every message after per-instance level filtering, tagged with the producing instance id:
TrustPin.setLogSink(TrustPinClosureLogSink { level, instanceId, message in
myLogger.log("[\(instanceId)] \(message)")
})
TrustPin.setLogSink(nil) // restore the default sinkSinks are called synchronously from SDK internals, including TLS-handshake paths: keep them fast and non-blocking, don’t perform I/O inline, and never call back into TrustPin from a sink.
Monitoring Pin Validation
To feed pin-validation verdicts into your security monitoring, for example reporting suspected MITM attempts to your backend, install a global TrustPinValidationListener:
final class SecurityMonitor: TrustPinValidationListener {
func onValidationFailure(instanceId: String, domain: String,
error: TrustPinErrors, presentedCertificate: Data) {
// Fires only for definitive verdicts: .pinsMismatch, .allPinsExpired,
// .domainNotRegistered. `presentedCertificate` is the DER-encoded leaf
// as received from the network. Treat it as untrusted input.
}
func onValidationSuccess(instanceId: String, domain: String) {
// Optional. The default implementation does nothing.
}
}
TrustPin.setValidationListener(SecurityMonitor()) // pass nil to detachThe listener is observe-only: it is invoked strictly after the verdict is decided and cannot veto, approve, or alter a connection. Transient conditions (configuration fetch failures, timeouts) and permissive-mode connections to unregistered domains produce no callbacks. Like log sinks, listeners are called synchronously from TLS-handshake paths, so keep them non-blocking and never call back into TrustPin.
Error Handling
All SDK errors are cases of TrustPinErrors:
do {
try await TrustPin.setup(config)
} catch TrustPinErrors.invalidProjectConfig {
// Bad credentials or invalid configuration
} catch TrustPinErrors.errorFetchingPinningInfo {
// Network failure while loading the configuration
} catch TrustPinErrors.configurationValidationFailed {
// JWS signature didn't verify against the project's public key
} catch TrustPinErrors.domainNotRegistered {
// Strict mode and the host isn't in the configuration
} catch TrustPinErrors.pinsMismatch {
// Server certificate doesn't match any active pin
} catch TrustPinErrors.allPinsExpired {
// Configuration is stale. Rotate pins in the dashboard
} catch TrustPinErrors.invalidServerCert {
// Server returned an unparseable certificate
}Best Practices
Setup & Initialization
- Call
TrustPin.setup()once during app launch. Subsequent calls return immediately. setup()is non-blocking. Useawait TrustPin.awaitConfiguration()when you need to fail closed before the first pinned request.- Set the log level before
setup()to capture initialization output. - Handle setup errors gracefully. Don’t block app launch.
- Don’t call
setup()concurrently from multiple tasks for the same instance.
Security
- Use
.strictmode in production. - Prefer SPKI pinning; rotate pins in the dashboard before they expire.
- Monitor pin validation failures via
TrustPin.setValidationListener(_:)or logging. - Keep credentials outside source control.
TrustPinConfiguration.fromPlist()reads from a bundled plist. - 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
- Reuse
URLSessioninstances rather than creating a new one per request. - Configuration is cached for 10 minutes; the SDK refreshes automatically.
- Use
.erroror.nonelog levels in production.
Complete Documentation
For the full API reference, including every method signature, integration examples (Alamofire, third-party clients), and advanced configuration, visit:
Resources
- Repository: github.com/trustpin-cloud/swift.sdk
- API Reference: trustpin-cloud.github.io/swift.sdk
- Dashboard: app.trustpin.cloud
- Support: support@trustpin.cloud