iOS / macOS SDK
Integrate TrustPin into your Apple platform application with native certificate pinning protection.
Current version: TrustPinKit 6.2.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.2.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.2.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.2'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 |
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)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 — 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 — 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.
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