Repository navigation
Hash each certificate's SPKI as encoded instead of rebuilding it - #372
Conversation
Read the Subject Public Key Info straight from the certificate's DER and hash it as is. This removes the five hardcoded ASN.1 headers and the extra trust evaluation per certificate on a cache miss, so any key type can be pinned. Adds a P-521 test certificate and malformed-input tests for the DER reader.
No simulator runtime can run them (all are arm64-only), and Xcode 27 ships no Swift standard library for them. Device, macOS and Mac Catalyst slices keep arm64e and arm64e.x1.
First Swift code in TrustKit. The DER reader moves to TrustKit/Swift/SubjectPublicKeyInfo.swift, exposed to Objective-C as TSKSubjectPublicKeyInfo. Xcode compiles it into the framework targets; SwiftPM builds it as a TrustKitSwift target. Uses Swift 6 language mode and swift-tools-version 6.0. Removes the recursive HEADER_SEARCH_PATHS, which resolved <TrustKit/…> to source headers and broke the framework module. Documents the empty Swift file that Objective-C-only apps using the static XCFramework need below iOS 16 / macOS 13.
get_pin_from_certificate.sh replaces get_pin_from_certificate.py. It hashes any key type and reads PEM or DER, including with macOS's built-in LibreSSL. Removes generate_test_certificates.py; the test certificates are committed and nothing regenerates them.
There was a problem hiding this comment.
Beyond the inline findings, I also checked the new DER walker in TrustKit/Swift/SubjectPublicKeyInfo.swift for out-of-bounds reads and length overflow. I found neither: every read stays inside the parent element's range, and long-form lengths are capped at 3 bytes, so the arithmetic can't overflow.
Extended reasoning...
This is a brief note on what was ruled out. It covers the bounds and length handling in readElement and subjectPublicKeyInfoRange, which the inline findings don't address.
Findings marked 🟡 are optional suggestions and need no follow-up push.
Additional findings (outside the current diff — GitHub can't attach inline comments there):
-
🟡
TrustKit/Pinning/TSKSPKIHashCache.m— Apps upgrading to this version keep pinning against hashes computed the old way, for any certificate already in the on-disk SPKI cache. The cache is keyed only by certificate bytes and is loaded fromspki-hash.cacheat init. So a hit at line 117 returns the old rebuilt-header hash and skips the new as-encoded hash at line 126. Fix: make sure no entry computed by the old algorithm is reused, for example by versioning the cache filenamekTSKSPKISharedHashCacheIdentifier(TSKSPKIHashCache.h:28) or discarding the persisted file on upgrade.Why this was flagged
The trigger is a device that ran an earlier TrustKit and cached a certificate whose SPKI as encoded differs from the old rebuilt form. Examples are an RSA key whose AlgorithmIdentifier is rsassaPss or omits the NULL parameter. The old code always prepended the fixed rsaEncryption header (rsa2048Asn1Header etc.). After upgrading, initWithIdentifier: loads the persisted dictionary via loadSPKICacheFromFileSystem (TSKSPKIHashCache.m:91, 177-190). _hashSubjectPublicKeyInfoFromCertificate: returns the cached old hash at lines 115-120 without calling TSKSubjectPublicKeyInfo. The cache is never evicted, so those devices pin against the old hash indefinitely. Fresh installs compute the new as-encoded hash, which matches what get_pin_from_certificate.sh produces. Devices with the same config therefore disagree: upgraded users can get pin failures (or matches) that fresh installs do not. No cache version or migration exists in the diff.
Verification: nit.
initWithIdentifier:loads the persisted dictionary at TSKSPKIHashCache.m:91, and the diff did not change that filename and added no version or format marker. A hit returns early at lines 117-120, so the new path at lines 126-134 never runs for those certificates. In the divergent cases the upgraded device behaves just as it does on the base branch.
There was a problem hiding this comment.
🟡 Changes recommended
Persisted hashes need invalidation, and the pin helper must preserve the same SPKI bytes as the runtime.
2 open issues
What changed in this PR
This PR changes TrustKit to hash each certificate’s encoded Subject Public Key Info directly, replacing reconstruction from algorithm-specific headers.
Changes:
- Adds a Swift SPKI extractor with P-521 and malformed-input tests.
- Integrates Swift into package and Xcode builds and excludes arm64e simulator slices.
- Replaces the Python pin helper with a shell script and updates setup documentation.
| File | Description |
|---|---|
| TrustKitTests/TSKPublicKeyAlgorithmTests.m | Adds P-521 and malformed-certificate tests. |
| TrustKit/Swift/SubjectPublicKeyInfo.swift | Extracts the original SPKI DER bytes. |
| TrustKit/Pinning/TSKSPKIHashCache.m | Hashes extracted SPKI instead of rebuilding it. |
| TrustKit/Pinning/pinning_utils.m | Removes the unused public-key helper. |
| TrustKit/Pinning/pinning_utils.h | Removes the helper’s declaration. |
| TrustKit.xcodeproj/project.pbxproj | Integrates Swift and test resources; excludes simulator architectures. |
| Package.swift | Adds the Swift target and requires Swift tools 6.0. |
| get_pin_from_certificate.sh | Adds an OpenSSL-based pin helper. |
| get_pin_from_certificate.py | Removes the Python pin helper. |
| generate_test_certificates.py | Removes the Python certificate generator. |
| docs/getting-started.md | Documents the shell helper and static-framework Swift linking. |
Review effort: Balanced
openssl pkey re-encodes the key, so for some certificates the script's pin never matched what TrustKit computes. The script now hashes the SPKI bytes straight from the certificate. The shared SPKI cache file is renamed so that hashes cached by TrustKit 3.x are recomputed.
| | sed -E 's/^ *([0-9]+):d=([0-9]+) +hl= *([0-9]+) +l= *([0-9]+) +(.*)$/\1 \2 \3 \4 \5/' \ | ||
| | awk '$2 == 2 { n++; if (n == 1 && /cont \[ 0 \]/) skip = 1; if (n == 6 + skip) { if (/SEQUENCE/) print $1, $3 + $4; exit } }') | ||
|
|
||
| PIN="$(tail -c +$((OFFSET + 1)) "$DER" | head -c "$LENGTH" \ |
TrustKit 4.0 is a major release. It raises the minimum OS versions, drops CocoaPods, adds visionOS, logs through `os_log`, and accepts any domain name or IP address in the pinning configuration. Some of these changes affect which connections get pinned, so test this beta before you ship it. Please report problems in [Issues](https://github.com/datatheorem/TrustKit/issues). ## Breaking changes ### Requirements - Minimum OS versions are now iOS 15, macOS 12, Mac Catalyst 15, tvOS 15, watchOS 9 and visionOS 1. They were iOS 12, macOS 10.13, tvOS 12 and watchOS 4. - Swift Package Manager needs Xcode 16 or later (`swift-tools-version:6.0`). So does building from the Xcode project, which now uses the Xcode 16 project format. TrustKit itself is built and tested with Xcode 27. - **CocoaPods is no longer supported.** TrustKit 4.0 and later aren't published to CocoaPods, but 3.x is still available there. Use Swift Package Manager, or the static XCFramework (`TrustKit-static.zip`) attached to each release. - The `TrustKit_Static` Xcode target is removed. Use `TrustKit-static.zip` instead. ### Removed APIs - `+[TrustKit setLoggerBlock:]` is removed. TrustKit now logs through `os_log` (see [Logging](#logging)). To act on validation results in code, set `pinningValidatorCallback`. - `kTSKPublicKeyAlgorithms`, `TSKSupportedAlgorithm`, `kTSKAlgorithmRsa2048`, `kTSKAlgorithmRsa4096`, `kTSKAlgorithmEcDsaSecp256r1` and `kTSKAlgorithmEcDsaSecp384r1` are removed. They have been deprecated since 1.6.0, so remove them from your code. A configuration or Info.plist that still sets `TSKPublicKeyAlgorithms` keeps working, because the key is ignored. ### macOS-only symbols no longer compile on other platforms `kTSKIgnorePinningForUserDefinedTrustAnchors` and `TSKTrustEvaluationFailedUserDefinedTrustAnchor` are now marked unavailable on iOS, Mac Catalyst, tvOS, watchOS and visionOS. Using them there is a compile error. In code shared across platforms, wrap them in `#if TARGET_OS_OSX` (Objective-C) or `#if os(macOS)` (Swift). ### Header location The public headers moved to `TrustKit/public/TrustKit/`. `#import <TrustKit/TrustKit.h>` and `@import TrustKit;` now work the same way with Swift Package Manager and with the framework. If you have header search paths pointing into a TrustKit checkout, update them. ### Static XCFramework in apps without Swift Part of TrustKit is now written in Swift. This is internal only, and the public API hasn't changed. If your app has no Swift code of its own and targets an OS earlier than iOS 16, tvOS 16, Mac Catalyst 16 or macOS 13, linking the XCFramework fails with `Undefined symbols … __swift_FORCE_LOAD_$_swiftCompatibility56`. To fix it, add an empty Swift file to your app target. ### Swift contributions Starting with 4.0, TrustKit accepts contributions in Swift. This release ships TrustKit's first Swift code. Over time we plan to move the Objective-C classes to Swift that Objective-C can still call, so existing apps keep working unchanged. - Put new Swift files in `TrustKit/Swift/` and add them to each framework target in `TrustKit.xcodeproj`. - Swift code builds in Swift 6 language mode, with strict concurrency checking. `scripts/lint.sh` runs swift-format and SwiftLint, and CI fails if lint fails. - For now, Swift code is internal only. Swift Package Manager builds it as a separate target, which doesn't support public Swift API yet. Open an issue before you add a public Swift type. ## Pinning configuration (#333, #341) These changes affect which connections are pinned, so review your configuration when you upgrade. - **Any domain name or IP address can be pinned.** This includes internal names such as `api.internal` or `localhost`, and IPv4 or IPv6 addresses. 3.x rejected names that didn't end in an entry of its bundled public suffix list. That list is now removed. - **Keys that can never match now throw an exception at startup.** Examples are `*.example.com`, or any key containing `:`, `/`, `@` or a space. 3.x loaded these keys, and the domain silently stayed unpinned. Like any other configuration error, this raises an `NSException` from `initSharedInstanceWithConfiguration:`. If your app downloads its configuration at runtime, validate it before you pass it to TrustKit. - **Matching now ignores case and a trailing dot.** IPv6 addresses match with or without brackets, and in any notation. In 3.x, a key such as `API.example.com` never matched, and the host stayed unpinned without any warning. In 4.0 that key matches and the host is pinned. - **`kTSKIncludeSubdomains` now uses plain suffix matching.** It throws for an IP address or a single-label domain such as `com`. Otherwise it is allowed on any domain, including public suffixes such as `co.uk` or `appspot.com`, which 3.x rejected. ## Any key type can be pinned TrustKit now hashes each certificate's Subject Public Key Info exactly as it is encoded in the certificate. Before, it rebuilt the Subject Public Key Info, and that only worked for a fixed list of key types. - Key types that 3.x couldn't handle now work, ECDSA P-521 for example. In 3.x, those certificates failed with `TSKTrustEvaluationErrorCouldNotGenerateSpkiHash`, and the connection was blocked. - Pins for standard RSA 2048, 3072 and 4096 keys and for ECDSA P-256 and P-384 keys are unchanged. If a certificate encodes its key in a non-standard way, its 4.0 pin can differ from its 3.x pin. Regenerate the pin to check. - `get_pin_from_certificate.sh` replaces the Python script. It computes pins the same way TrustKit does, and takes a PEM or DER file: `./get_pin_from_certificate.sh ca.pem`. Don't re-encode the key first, for example with `openssl pkey`. For some certificates that changes the bytes, and the pin won't match. ## Logging TrustKit logs through the unified logging system (#128), under the subsystem `com.datatheorem.trustkit` and the category `TrustKit`. - Validation failures and other errors are logged at the error level. Successful validations are logged at the info level, and the detail of each step at the debug level. - **Release builds now log too.** 3.x didn't log anything in Release builds. Pinned hostnames are logged as public data, so they appear in device logs. - To watch the logs, run `log stream --level debug --predicate 'subsystem == "com.datatheorem.trustkit"'`. For the simulator, put `xcrun simctl spawn booted` in front of it. ## Pin failure reports The `app-platform` field now reports `VISIONOS` for visionOS apps, `MAC_CATALYST` for Mac Catalyst apps, and `IOS_ON_MAC` for iPhone or iPad apps running on a Mac. 3.x reported the last two as `IOS`. If your report server filters on this field, update it. The other report fields haven't changed. ## New platforms - **visionOS** is supported through Swift Package Manager, the Xcode project and the static XCFramework. - **Mac Catalyst** is now officially supported. It is declared in `Package.swift`, tested in CI and has its own slice in the XCFramework. - The static XCFramework now also includes `arm64e` slices. ## Docs and demos - In the getting started guide, the `UIWebView` section is replaced by a `WKWebView` example in Objective-C and Swift. The example validates off the main thread, and both demo apps include it. - The demo apps now use TrustKit as a local Swift package and use the UIScene lifecycle. ## For contributors - CI runs the unit tests on macOS, Mac Catalyst, and the iOS, tvOS and visionOS simulators. It also runs UI tests for both demo apps. watchOS is turned off for now, until a runner image issue is fixed. - `scripts/lint.sh` runs swift-format, SwiftLint and clang-format, and CI fails when lint fails. - OCMock now comes from Swift Package Manager instead of bundled binaries. ## What's Changed * [Issue #365] Move to Xcode 27 builds; align minOS to Apple min on Xcode 27; Drop Cocoapods support; Add visionOS support by @alexanderdibenedetto in #366 * Cleanup code comments and add wkwebview example by @alexanderdibenedetto in #367 * Replace TSKLog and +setLoggerBlock: with os_log (#128) by @alexanderdibenedetto in #368 * Allow any domain name or IP address in the pinning configuration (#333, #341) by @alexanderdibenedetto in #371 * Hash each certificate's SPKI as encoded instead of rebuilding it by @alexanderdibenedetto in #372 * Introduce linting by @alexanderdibenedetto in #373 * Remove the deprecated public key algorithm API by @alexanderdibenedetto in #375 * Set the version to 4.0.0 by @alexanderdibenedetto in #376 * Rename SPKICacheDictionnary to SPKICacheDictionary by @uroboro in #377 **Full Changelog**: 3.0.8...4.0.0-beta.1

Also: