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" \ |

Also: