Skip to Content
SDK IntegrationSwift (iOS & macOS)

iOS / macOS SDK

Integrate TrustPin into your Apple platform application with native certificate pinning protection.

Current version: TrustPinKit 6.2.0

Platform Requirements

PlatformMinimum Version
iOS15.0+
macOS13.0+
watchOS8.0+
tvOS15.0+
Mac Catalyst15.0+
visionOS2.0+

Swift Version: 6.1+ (Xcode 16.3+; async/await is required)


Installation

Add TrustPin to your project in Xcode:

  1. Go to File → Add Package Dependencies
  2. Enter the repository URL:
    https://github.com/trustpin-cloud/swift.sdk
  3. Select version 6.2.0 or later (Up to Next Major).

The package vends two products:

ProductWhat it is
TrustPinKitThe SDK (binary framework) — all you need for URLSession-based apps
TrustPinKitAlamofireOptional 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 install

The TrustPinKitAlamofire adapter 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:

KeyTypeRequiredNotes
OrganizationIdStringYesNon-empty
ProjectIdStringYesNon-empty
PublicKeyStringYesBase64-encoded ECDSA P-256 public key
ModeStringNo"strict" (default) or "permissive", lowercase
ConfigurationURLStringNoMust 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:

ApproachBest ForSetup Complexity
URLSessionDelegate (Recommended)Most applications, precise control🟢 Low
Alamofire AdapterApps networking through Alamofire🟢 Low
System-Wide URLProtocolThird-party library protection, legacy code🟡 Medium
Helper MethodsOne-off requests, explicit control🟠 High

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 sink

Sinks 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 detach

The 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

  1. Call TrustPin.setup() once during app launch. Subsequent calls return immediately.
  2. setup() is non-blocking. Use await TrustPin.awaitConfiguration() when you need to fail closed before the first pinned request.
  3. Set the log level before setup() to capture initialization output.
  4. Handle setup errors gracefully — don’t block app launch.
  5. Don’t call setup() concurrently from multiple tasks for the same instance.

Security

  1. Use .strict mode in production.
  2. Prefer SPKI pinning; rotate pins in the dashboard before they expire.
  3. Monitor pin validation failures via TrustPin.setValidationListener(_:) or logging.
  4. Keep credentials outside source control — TrustPinConfiguration.fromPlist() reads from a bundled plist.
  5. Use HTTPS for all pinned domains.

Performance

  1. Reuse URLSession instances rather than creating a new one per request.
  2. Configuration is cached for 10 minutes; the SDK refreshes automatically.
  3. Use .error or .none log levels in production.

Complete Documentation

For the full API reference, including every method signature, integration examples (Alamofire, third-party clients), and advanced configuration, visit:

TrustPinKit API Reference 


Resources