Skip to content

Hash each certificate's SPKI as encoded instead of rebuilding it - #372

Merged
alexanderdibenedetto merged 5 commits into
v4-0-0from
adb/issue-236-ready-spki-straight-from-certificate-der-encoding-and-hash-as-is
Oct 23, 2026
Merged

alexanderdibenedetto merged 5 commits into
v4-0-0from
adb/issue-236-ready-spki-straight-from-certificate-der-encoding-and-hash-as-is

Conversation

@alexanderdibenedetto

Copy link
Copy Markdown
Contributor

Also:

  • Stop building arm64e and arm64e.x1 simulator slices
  • Introduce Swift (internal only) to TrustKit (Read the certificate's SPKI in Swift)
  • Replace the Python helper scripts

Alexander DiBenedetto added 4 commits October 22, 2026 14:03
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.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 from spki-hash.cache at 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 filename kTSKSPKISharedHashCacheIdentifier (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.

Comment thread TrustKit.xcodeproj/project.pbxproj
Comment thread get_pin_from_certificate.sh Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread TrustKit/Pinning/TSKSPKIHashCache.m
Comment thread get_pin_from_certificate.sh Outdated
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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The pin helper can exit without printing a pin for large certificates because its byte-extraction pipeline triggers SIGPIPE.

1 open issue
2 resolved since last review

Review effort: Balanced

| 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" \
@alexanderdibenedetto
alexanderdibenedetto added this pull request to stack #374 October 22, 2026 20:56
@alexanderdibenedetto
alexanderdibenedetto merged commit 8794713 into v4-0-0 Oct 23, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants