TrustKit: SSL pinning by swizzling NSURLSession delegates
Easy SSL pinning validation and reporting for iOS, macOS, tvOS and watchOS.
At a glance
- What is it?
- TrustKit is a MIT-licensed Apple-platform framework for SSL public key pinning, built by the mobile teams at Data Theorem and Yahoo, presented at Black Hat USA 2015 and distributed through Swift Package Manager, CocoaPods and Carthage. It pins the certificate's Subject Public Key Info rather than the certificate, and it can swizzle your network delegates so pinning deploys without a source change. Its own sample configuration sets enforcement to false and an expiry date of 2017-12-01.
- Who is it for?
- Use TrustKit if you are building an Apple-platform app that needs certificate pinning and want the SPKI hash approach rather than rolling your own comparison logic, because the Subject Public Key Info decision is the one most hand-rolled implementations get wrong and this library gets it right for you.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 52 days ago.
- What is it written in?
- Mainly Objective-C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
SPKI, not the certificate and not the key bits
The second feature bullet in this README is the technical decision the whole library exists to get right, and the reasoning is cited to the article that is the canonical explanation.
It says the implementation is sane by pinning the certificate's Subject Public Key Info, as opposed to the certificate itself or the public key bits, and links to a post on imperialviolet.org from May 2011.
That article is the standard reference on the subject, and the distinction it draws is worth restating because it is where hand-rolled pinning goes wrong.
There are three things you can hash and they behave very differently. Hashing the whole certificate is the most obvious and it is fragile for reasons that are not obvious until you have been bitten: certificate renewal. A certificate has a validity period, so pinning the certificate means your app breaks when the certificate is replaced, even if the replacement has the same key. A reasonable operational practice rotates certificates on a schedule, so the app breaks on a schedule, and the fix in production is often to ship an update rather than to page anyone.
Hashing the public key bits is the other extreme, and it is fragile in the other direction. The public key lives inside the certificate and does not change on renewal, which sounds perfect, but it also does not change when the key is the same, which means an attacker who obtains a key that happens to match will be accepted even though the certificate is not the one you trust. In practice that matters because keys are moved between environments and reused across certificate generations more often than people assume.
Hashing the Subject Public Key Info is the middle. The SPKI is a structure inside the certificate containing the key plus its algorithm and parameters, and it is what the certificate signs. It survives certificate renewal, because a renewed certificate with the same key has the same SPKI, and it changes when the key changes, so a certificate with a different key is rejected. That is the behaviour you want from pinning, and it is why HPKP, RFC 7469 and most modern pinning implementations hash the SPKI rather than the certificate.
The README also says the policy settings are heavily based on the HTTP Public Key Pinning specification, RFC 7469, which is worth noting because HPKP itself is a deprecated, removed and subsequently reimplemented browser feature. TrustKit taking its vocabulary from the specification rather than from the browser implementation is the right call, because the specification describes the design and the browser implementation accumulated compatibility workarounds that have nothing to do with a mobile app.
So the library's headline feature is not the API, the swizzling or the reporting. It is that it hashes the right thing, and it tells you why, with a link.
The sample config does not enforce, and its expiry date is 2017
The configuration sample in this README is the first thing a new user copies, and two of its values mean the feature is off.
The Objective-C sample is a dictionary with two pinned domains. For www.datatheorem.com it sets an expiration date of 2017-12-01, two public key hashes, and kTSKEnforcePinning set to @NO. For yahoo.com it sets two public key hashes and kTSKIncludeSubdomains set to @YES, and no enforcement key at all.
kTSKEnforcePinning set to @NO is the one that matters. There is a difference between configuring a pinning policy and enforcing it, and this sample configures the policy for the primary domain and then explicitly declines to enforce it. A developer who copies this into their app, substitutes their own domain and their own hashes, and ships it has a pinning configuration that validates and reports but does not block. The connection still succeeds if the certificate is wrong. The reporting mechanism will still fire, so the developer may believe the feature is working while the only observable effect is a server-side report.
The expiration date compounds it. 2017-12-01 is in the past, and the field exists so that a pinning policy can be given a deliberate end of life. A sample carrying a date that expired years ago teaches the wrong thing about the field: that it is a formality rather than a control. A reader who copies the pattern will set a date, and if they set it to something already past they get a policy that has lapsed, and the behaviour of a lapsed policy is again worth checking rather than assuming.
The Swift sample has the same shape and the same two problems in a milder form. It sets kTSKSwizzleNetworkDelegates to false, gives yahoo.com an expiration date of 2017-12-01 and two public key hashes, and omits both the enforcement key and the include-subdomains key that the Objective-C sample shows.
So both language samples switch swizzling off, which is the defensible choice for documentation, and both carry a lapsed expiration date, which is not. The enforcement difference between the two samples is the part that makes the asymmetry a documentation bug rather than a stylistic one: the Objective-C sample shows the key, with the value that disables the feature, and the Swift sample omits it, so a reader of the Swift sample does not learn that the key exists at all.
There is a third, smaller sign of the same carelessness in the Swift sample. The end of the dictionary literal is printed as a run of closing brackets with a comma in the middle of them, which does not balance as written. Anyone copying that block into a playground gets a parse error, which is at least a loud failure rather than a silent one.
None of this makes the library wrong. The sample's job is to show the shape of the configuration, and it does that. But the shape it shows has pinning disabled and a date in the past, and the first thing an evaluator should tell a team using this framework is that the sample is not a safe starting point and the pinning has to be turned on deliberately.
Swizzling to deploy pinning without a source change
The fourth feature is the one that makes this library unusual, and it is a runtime code-injection mechanism, which the project's own conference talk was about.
The feature is auto-pinning functionality by swizzling the App's NSURLConnection and NSURLSession delegates in order to automatically add pinning validation to the App's HTTPS connections, and the claimed benefit is that this allows deploying TrustKit without even modifying the App's source code.
That is a precise description of method swizzling: exchanging the implementation of a method at runtime so that calls which would reach the original implementation reach yours first. It is a long-established Objective-C technique and it is exactly how instrumentation libraries, analytics SDKs and crash reporters work. It is also the technique the security community discusses most, because it is indistinguishable in effect from an attacker who has code execution.
The provenance is in the README's Getting Started section, which says TrustKit was initially released at Black Hat USA 2015, and the link reference for that briefing is titled TrustKit: Code Injection on iOS 8 For The Greater Good. So the technique was presented as a defensive technique at a security conference, and the name of the talk is an accurate description of the mechanism. There is a second link reference to a post from October 2015 about TrustKit and iOS 9, in the shared cache, which reads as the follow-up to an OS change that affected the technique.
For a team evaluating this, the trade is straightforward and worth stating without drama. In exchange for not writing the delegate code yourself, you get a framework whose enforcement is injected at runtime into classes the operating system owns. That has three consequences. Your stack traces in a pinning failure go through TrustKit. A class-cluster or OS change that alters the delegate signature can break pinning silently, which is what the iOS 9 reference is presumably about. And an App Store reviewer looking at an app that enforces a security control it did not write in its own source has to reason about code it cannot see.
The README is honest that this is optional. The key is kTSKSwizzleNetworkDelegates, and both language samples set it to false. So the supported configurations are: swizzle, and do not swizzle and call the validator yourself. The second is the one the sample's integration example shows, and it is the one that keeps the enforcement in code the team can read.
handleChallenge returning false means the connection proceeds unpinned
The integration example in the README is short, and the branch it takes is the most important thing to understand before copying it.
Here is the whole example:
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
{
TSKPinningValidator *pinningValidator = [[TrustKit sharedInstance] pinningValidator];
if (![pinningValidator handleChallenge:challenge completionHandler:completionHandler])
{
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}
}It is an NSURLSessionDelegate method for didReceiveChallenge. The body gets a TSKPinningValidator from the TrustKit singleton, passes the challenge and the completion handler to handleChallenge, and then branches on the result. If the validator returns true, the comment says the connection will be blocked if validation failed, so TrustKit has taken responsibility. If it returns false, the comment explains that TrustKit did not handle this challenge, perhaps it was not for server trust or the domain was not pinned, and the code calls completionHandler with NSURLSessionAuthChallengePerformDefaultHandling and a nil credential.
That fallback is correct and it is also the most dangerous line in the example, because the default handling for a server trust challenge is to accept the system trust evaluation. Which is to say: if TrustKit declines, the connection proceeds with normal TLS validation and no pinning.
So the enforcement model is opt-in per challenge, and there are three ways for a request to end up unpinned without anything failing. TrustKit returns false because the domain is not in the policy, so a domain you forgot to add is silently unprotected. TrustKit returns false because the challenge is not a server trust challenge, which is correct behaviour but means the guard is not the only path to the network. And the delegate is not called at all, because the app has another delegate, a different session configuration, or a code path that builds a session without the delegate, and then nothing in TrustKit is involved.
None of these produce an error. A request that should have been pinned and was not simply succeeds, and unless you have built a test for it you will not find out. That is the failure mode to design against, and the practical checks are: enumerate every host your app talks to and confirm each is in the policy; confirm every session you create has the delegate; and test with a deliberately wrong pin and assert that the request fails, rather than assuming that configuring a pin means it is enforced.
The example's own comment is doing the right thing by explaining why the false branch exists, which is a documentation decision that helps. What it cannot do is tell a reader that this branch is where their security quietly ends, and the two-language sample inconsistency in the configuration section means a Swift reader is even less likely to have the full picture of which keys exist.
The yahoo.com pins differ between the two language samples
The two configuration samples in this README describe the same policy in Objective-C and in Swift, and they do not agree.
The Objective-C sample pins yahoo.com with two hashes, one beginning TQEtdMbmwFgYUifM4LDF and one beginning rFjc3wG7lTZe43zeYTvPq. The Swift sample pins yahoo.com with two hashes, one beginning JbQbUG5JMJUoI6brnx0x3vZF6 and one beginning WoiWRyIOVNa9ihaBciRSC7. Those are four different hashes for the same domain in two samples of the same library.
There are only two explanations, and both are worth naming. Either the samples were written at different times and yahoo.com rotated its keys in between, in which case one of the two sets is stale and pinning yahoo.com with the wrong set fails. Or one sample was copy-pasted from a different context and its hashes belong elsewhere. Either way, the documentation contains two incompatible answers to the same question, and a reader has no way to tell which is current because nothing in the sample says when it was captured or for which certificate.
The datatheorem.com entry appears only in the Objective-C sample, so the Swift reader gets no example of the enforcement key at all. The swift sample also omits kTSKIncludeSubdomains, which the Objective-C sample shows on yahoo.com. So the Swift sample demonstrates a narrower configuration than the Objective-C one, and the differences are not signposted.
This matters more than a documentation nit, because pinning hashes are exactly the kind of value people do not verify. The whole point of a pin is that it is hardcoded and assumed correct, so a sample with a wrong hash teaches the habit of copying hashes without checking them, and it does so in a repository whose central technical claim is that it hashes the right thing.
The practical guidance for anyone using this library is to generate pins from your own certificate chain and never from a sample. The repository ships the tool for it: get_pin_from_certificate.py is at the repository root, and generate_test_certificates.py sits next to it, which suggests the test fixtures are generated rather than committed as opaque files. The .gitmodules at the root is presumably where those fixtures live, which also explains why a submodule is needed for a library whose only test material is certificates.
So the fix for a user is available in the repository and the fix for the project is to regenerate the samples. What an evaluator should take from this is that the two configuration examples should be read as illustrations of syntax rather than as examples of a working policy, and that a pinning policy has to be built from your own certificates.
Three package managers, Bitrise, Jazzy, and a dead chat room
The packaging and tooling in this repository is comprehensive for an Apple-platform library, and one of the links has stopped working.
Distribution runs through all three of the major Apple package managers. There is a Package.swift at the root for Swift Package Manager, a TrustKit.podspec for CocoaPods, and a Carthage compatible badge. The CocoaPods badges report the published version and the supported platform, and a link to the pod page. So an integrating team can use whichever of the three their build system already speaks, which is the right coverage for a library that predates most of its adopters' toolchain choices.
Continuous integration is Bitrise, which is the badge in the README pointing at app.bitrise.io. Bitrise was the first mainstream CI service built for mobile and Xcode projects specifically, and choosing it over a generic CI service was the right call for a project whose build is an Xcode project. TrustKit.xcodeproj/ is at the root, alongside TrustKit/ for the framework itself, TrustKitDemo/ and TrustKitTests/.
Documentation is generated with Jazzy, configured by .jazzy.yaml at the root, and published at datatheorem.github.io/TrustKit/documentation. Jazzy produces the three-pane Xcode-style documentation layout directly from the source comments, which means the API documentation and the header comments cannot drift apart. The Getting Started guide is a separate markdown document under docs/, alongside the Black Hat PDF.
Two files in the root are about the project rather than the code. AUTHORS and ATTRIBUTIONS, kept separately, which is a distinction worth preserving because one is who wrote it and the other is whose code or text is in it. And LICENSE, MIT, with the licence text in the repository and the README stating the same.
The dead link is the Gitter badge in the header row, pointing at gitter.im/TrustKit/Lobby. Gitter was acquired by GitHub in 2019 and its chat was migrated to a GitHub-owned platform before being wound down, with the remaining rooms moving to Element and Matrix. So the project's advertised chat is a service that no longer operates, and someone following that badge to ask a question will find nothing.
It is a small thing in a header full of badges, and it is the kind of artifact that a project with an active release history until June 2025 does not always get round to. It is worth noting because the two genuinely valuable links in that header row, the PayPal engineering post and the Black Hat briefing PDF, are both still the primary sources for understanding why this library is designed the way it is, and those are the ones a new user should actually follow.
3.0.5 is missing, and the last release is from June 2025
The release history has a gap in it, and a longer quiet period behind it.
The three most recent tags are 3.0.4 on 2024-03-27, 3.0.6 on 2025-05-27 and 3.0.7 on 2025-06-04. Version 3.0.5 is not in the list. So either that release was never tagged, or it was tagged and later removed, and a consumer pinning to 3.0.5 and finding nothing is not imagining it.
The intervals around the gap are also uneven. Three point zero four to three point zero six is fourteen months. Three point zero six to three point zero seven is eight days, which is the shape of a follow-up fix rather than a feature release. And the last push to the repository was on 2026-08-12, which is more than fourteen months after the newest tag.
So the state is: a library at 3.0.7, with the most recent release in June 2025, and fourteen months of commits since. For a security library, the relevant question is not whether the commits are interesting but whether the last release contains whatever fixes have landed since. A consumer pinning to 3.0.7 is running code from June 2025 against an operating system that has shipped at least one, and probably more, major version since, on four platforms whose minimum versions in the README are iOS 12, macOS 10.13, tvOS 12 and watchOS 4. Those minimums are themselves a signal: iOS 12 and macOS 10.13 both predate a decade, so the library is claiming to support a very wide range and the documentation does not say whether that range is still tested.
The project history gives some context for the pace. TrustKit was presented at Black Hat USA 2015 and the swizzling technique was written about at the time, and the repository topics still list ios, macos, objective-c, ssl, ssl-pinning, ssl-reporting and tvos, without watchOS despite watchOS 4 and later being named as supported in the README. So the topic list and the stated support have drifted apart, which is a small instance of a pattern that runs through the repository: the code is careful and the surrounding metadata is not.
None of that is a reason to avoid a ten-year-old framework. The SPKI decision, the fallthrough contract, the SPKI pin generation tooling and the three-package-manager distribution are all reasons to use it in preference to hand-rolling. It is a reason to pin deliberately, read the pin validation test rather than the sample, and check what has changed on the default branch since June 2025 before assuming the released version is the maintained one.
Editorial conclusion
Use TrustKit if you are building an Apple-platform app that needs certificate pinning and want the SPKI hash approach rather than rolling your own comparison logic, because the Subject Public Key Info decision is the one most hand-rolled implementations get wrong and this library gets it right for you. Do not deploy pinning by copying the README's sample configuration, because it sets kTSKEnforcePinning to false with an expiry date of 2017-12-01, so it demonstrates the API in a disabled state, and both language samples also switch swizzling off. Do not rely on the swizzling path as your only enforcement, because it is a runtime hook on your own delegates and a build that adds a delegate it does not notify leaves that connection unpinned. Verify five things. Generate your own pins rather than reusing the sample hashes, and get them from get_pin_from_certificate.py against your real server certificate chain. Decide explicitly between swizzling and explicit validator calls, because the sample's handleChallenge pattern falls through to default handling when TrustKit declines a challenge, so an integration that ignores the return value or omits a domain from the policy gets no pinning on it. Test that a wrong pin actually blocks the connection before you ship, since the sample is configured so that it would not. Check which version you pin, given that the tag list skips 3.0.5 and the newest release is 3.0.7 from 2025-06-04 while the last push is 2026-08-12. And read the reporting design, because a report-uri that ships certificate failure reports off-device is a privacy and abuse question you have to answer for your own service. The deciding fact is that this is a ten-year-old framework whose central design choice is still the right one and whose documentation has stopped being updated along with it.
Frequently asked questions
What does TrustKit do?
It is a framework for SSL public key pinning on iOS 12+, macOS 10.13+, tvOS 12+ and watchOS 4+, in Swift and Objective-C. It provides an API to configure a pinning policy, pins the certificate's Subject Public Key Info, reports validation failures to a server in the manner of the HPKP report-uri directive, and can swizzle NSURLConnection and NSURLSession delegates to add pinning without a source change. A separate TrustKit for Android exists.
Why does TrustKit pin the Subject Public Key Info rather than the certificate?
Because pinning the certificate breaks on renewal, since a renewed certificate with the same key is a different certificate, while pinning the public key bits does not change when the key does, so a certificate carrying a matching key is accepted even if it is not one you trust. The SPKI survives certificate renewal and changes when the key changes, which is the behaviour HPKP and RFC 7469 describe. The README links the canonical article on the subject from imperialviolet.org.
How do I integrate TrustKit with NSURLSession?
Get a TSKPinningValidator from the TrustKit singleton and pass the authentication challenge to handleChallenge along with the completion handler. If it returns true, TrustKit has handled the challenge and the connection is blocked on validation failure. If it returns false, the sample falls back to completionHandler with NSURLSessionAuthChallengePerformDefaultHandling, which means the connection proceeds with normal TLS validation and no pinning. So every host and every session has to be covered explicitly.
Is the pinning enforced in the TrustKit sample configuration?
No, and that is worth knowing before copying it. The Objective-C sample sets kTSKEnforcePinning to @NO for its primary domain and gives an expiration date of 2017-12-01, and the Swift sample omits the enforcement key entirely. Both samples also set kTSKSwizzleNetworkDelegates to false, and the two samples pin yahoo.com with different hash sets. Read the sample as syntax illustration rather than as a working policy.
How do I generate the correct public key pins?
Use the tooling in the repository rather than a sample. get_pin_from_certificate.py is at the repository root for deriving pins from a certificate, and generate_test_certificates.py sits beside it, which indicates the test fixtures are generated rather than committed as opaque files, with the .gitmodules at the root holding them. The README ships no instructions for the scripts, so read them before use.
How is TrustKit distributed and what is the latest version?
Through all three Apple package managers: Package.swift for Swift Package Manager, a TrustKit.podspec for CocoaPods, and Carthage compatibility. Continuous integration is on Bitrise and the API documentation is generated with Jazzy. The three most recent tags are 3.0.4 from 2024-03-27, 3.0.6 from 2025-05-27 and 3.0.7 from 2025-06-04, with no 3.0.5, and the last push was 2026-08-12.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/datatheorem-trustkit)