Dynamic TLS certificate pinning for iOS with automatic pin management
- β Dynamic Pin Management - Fetches and updates pins from your backend
- π Fail-Closed Security - All SSL errors block the connection for configured domains
- π JWS Verification - Cryptographically verifies pins using ES256 signatures
- π Auto-Retry - Automatically refreshes pins and retries on SSL failures
- π― Simple Integration - 3-line setup, works with standard URLSession
- π Wildcard Support - Supports patterns like
*.example.com - π Async/Await Ready - Full Swift concurrency support
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/Free-cat/dynapins-ios.git", from: "0.2.0")
]Or in Xcode:
- File β Add Packages
- Enter:
https://github.com/Free-cat/dynapins-ios - Select version and add to target
pod 'dynapins-ios', '~> 0.2.0'In your AppDelegate or app initialization:
import DynamicPinning
DynamicPinning.initialize(
signingPublicKey: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...", // Your ES256 public key
pinningServiceURL: URL(string: "https://pins.example.com/v1/pins")!,
domains: ["api.example.com", "*.cdn.example.com"]
) { successCount, failureCount in
print("β
Pinning configured for \(successCount) domains")
}let session = DynamicPinning.session()
// All requests automatically use certificate pinning
session.dataTask(with: URL(string: "https://api.example.com/data")!) { data, response, error in
if let error = error {
print("Error: \(error)")
return
}
// Handle response
}.resume()let session = DynamicPinning.session()
let (data, response) = try await session.data(from: URL(string: "https://api.example.com")!)That's it! π
βββββββββββββββ
β iOS App β 1. Initialize with ES256 public key + domains
ββββββββ¬βββββββ
β
βΌ
βββββββββββββββββββ 2. Fetch JWS ββββββββββββββββββββ
β DynamicPinning β ββββββββββββββββββββββΆ β Dynapins Server β
β SDK β ββββββββββββββββββββ
βββββββββββββββββββ β
β 3. Verify JWS signature (ES256) β
ββββββββββββββββββββββββββββββββββββββββββββββββ
β 4. Configure TrustKit with validated pins
βΌ
βββββββββββββββββββ
β HTTPS Request β ββ 5. TLS Handshake + Pin Validation βββΆ β
or β
βββββββββββββββββββ
β
β On SSL failure:
βΌ
π Auto-refresh pins and retry once
Key Points:
- SDK fetches JWS-signed pins from your backend
- Verifies JWS signature using embedded ES256 public key
- Configures TrustKit for SSL validation
- Automatically retries on SSL failure (refreshes pins once per request)
You need to run the Dynapins Server to serve signed pins.
# 1. Generate ES256 keypair
openssl ecparam -genkey -name prime256v1 -out private_key.pem
openssl pkcs8 -topk8 -nocrypt -in private_key.pem -out private_key_pkcs8.pem
# 2. Extract public key (use this in iOS app)
openssl ec -in private_key.pem -pubout | grep -v "BEGIN\|END" | tr -d '\n' > public_key.txt
cat public_key.txt
# 3. Run server
docker run -p 8080:8080 \
-e ALLOWED_DOMAINS="api.example.com,*.example.com" \
-e PRIVATE_KEY_PEM="$(cat private_key_pkcs8.pem)" \
freecats/dynapins-server:latestSee Dynapins Server docs for production deployment.
DynamicPinning.refreshPins { successCount, failureCount in
print("Refreshed \(successCount) domains")
}session.dataTask(with: url) { data, response, error in
if let error = error as NSError? {
if error.domain == NSURLErrorDomain && error.code == NSURLErrorCancelled {
print("β SSL pinning validation failed")
} else {
print("β Other error: \(error)")
}
}
}DynamicPinning.initialize(
signingPublicKey: publicKey,
pinningServiceURL: serviceURL,
domains: [
"api.example.com", // Exact match
"*.cdn.example.com", // Wildcard: matches api.cdn.example.com, img.cdn.example.com
"*.internal.example.com"
]
) { successCount, failureCount in
print("β
Success: \(successCount), β Failed: \(failureCount)")
}DynamicPinning.initialize(
signingPublicKey: publicKey,
pinningServiceURL: serviceURL,
domains: domains,
includeBackupPins: true // Include backup pins for certificate rotation
) { successCount, failureCount in
// Handle result
}# Unit tests only (fast, ~6 seconds)
make test
# Integration tests (requires running server)
make test-integration
# All tests (unit + integration)
make test-all# Build local server + run integration tests
make e2e-build
# Or pull from Docker Hub + run tests
make e2e-pull
# Stop containers
make e2e-down
# Show all commands
make help# 1. Start server
docker run -p 8080:8080 \
-e ALLOWED_DOMAINS="api.example.com" \
-e PRIVATE_KEY_PEM="$(cat private_key.pem)" \
freecats/dynapins-server:latest
# 2. Set environment variables
export TEST_SERVICE_URL="http://localhost:8080/v1/pins"
export TEST_PUBLIC_KEY="<your-public-key>"
export TEST_DOMAIN="api.example.com"
# 3. Run integration tests
swift test --filter PinningIntegrationTests- JWS Signature Verification: All pins must be signed with your ES256 private key
- Domain Validation: Payload domain must match the requested domain
- Fail-Closed Policy: SSL errors for configured domains always block the connection
- No Downgrades: Uses explicit URLSessionDelegate (TrustKit swizzling disabled)
- Wildcard Matching: Secure wildcard support with proper validation
β
Good: Request to configured domain with valid pin β Connection allowed
β Fail: Request to configured domain with invalid pin β Connection blocked
β οΈ Warn: Request to non-configured domain β Standard iOS validation (no pinning)
- Algorithm: ES256 (ECDSA with P-256 and SHA-256)
- Signature Library: JOSESwift
- SSL Pinning: TrustKit
- Key Format: SPKI (SubjectPublicKeyInfo) in Base64
- iOS: 14.0+
- macOS: 10.15+
- Swift: 5.9+
- Xcode: 15.0+
This SDK uses battle-tested open-source libraries:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β DynamicPinning β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β DynamicPinning.swift (Public API) β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β β β
β βΌ βΌ β
β ββββββββββββββββ βββββββββββββββββ β
β β NetworkServiceβ β CryptoService β β
β β (Fetch pins) β β (Verify JWS) β β
β ββββββββββββββββ βββββββββββββββββ β
β β β β
β ββββββββββββββ¬ββββββββββββββββ β
β βΌ β
β ββββββββββββββββββββ β
β β TrustKitManager β β
β β (Configure pins) β β
β ββββββββββββββββββββ β
β β β
ββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββ
βΌ
βββββββββββββββββββ
β TrustKit (SSL) β
βββββββββββββββββββ
β
βΌ
βββββββββββββββββββ
β URLSession β
βββββββββββββββββββ
Q: Do I need to modify my networking code?
A: No. Just replace URLSession.shared with DynamicPinning.session().
Q: What happens if the backend is down?
A: Cached pins are used. If no cache exists, connection fails (fail-closed).
Q: Can I pin multiple domains?
A: Yes, pass an array of domains to initialize().
Q: Does it work with Alamofire/Moya/other networking libraries?
A: Yes, if they use URLSession internally. Pass DynamicPinning.session() to them.
Q: How often are pins refreshed?
A: On first access and when SSL validation fails. You can also call refreshPins() manually.
Q: What about certificate rotation?
A: Enable includeBackupPins: true to fetch backup pins for seamless rotation.
β [DynamicPinning] TrustKit validation failed for: api.example.com
Solutions:
- Check that domain is in
ALLOWED_DOMAINSon server - Verify
signingPublicKeymatches server's private key - Ensure server is reachable from the device
- Check logs for JWS verification errors
β οΈ [DynamicPinning] Failed to verify pins for example.com: invalidPublicKey
Solutions:
- Verify public key format (Base64, no headers/footers)
- Check that key is ES256 (not Ed25519 or RSA)
- Ensure server is returning valid JWS tokens
Set a symbolic breakpoint on NSLog with condition:
(BOOL)[$arg1 containsString:@"[DynamicPinning]"]
Or check Console.app for logs starting with [DynamicPinning].
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
Quick Contribution Guide:
- Fork the repo
- Create a feature branch:
git checkout -b feature/my-feature - Make changes and add tests
- Run tests:
make test - Commit:
git commit -m "feat: add something" - Push and open a Pull Request
- π Bug Reports: GitHub Issues
- π¬ Questions: GitHub Discussions
- π§ Security Issues: [email protected] (private disclosure)
- Dynapins Server - Go backend for serving signed pins
- Dynapins Android - Android SDK (coming soon)
This project is licensed under the MIT License - see LICENSE for details.
Built with these excellent open-source libraries:
Made with β€οΈ for secure iOS apps
GitHub β’ Issues β’ Discussions