Skip to content

Allow any domain name or IP address in the pinning configuration (#333, #341) - #371

Merged
alexanderdibenedetto merged 3 commits into
v4-0-0from
adb/issue-341-relax-domain-name-restrictions-and-allow-ip-addresses
Oct 22, 2026
Merged

alexanderdibenedetto merged 3 commits into
v4-0-0from
adb/issue-341-relax-domain-name-restrictions-and-allow-ip-addresses

Conversation

@alexanderdibenedetto

Copy link
Copy Markdown
Contributor

Removes the bundled public suffix list. Keys are validated by shape, IP addresses are canonicalized with inet_pton/inet_ntop, and includeSubdomains uses plain suffix matching.

Additional Input Validation That Throws

Before: the only check was GetRegistryLength(key) == 0. That lookup reads labels from the right and stops at the first one it doesn't recognize, so it only ever validated the ending. For *.good.com it found com, returned a non-zero length, and the key loaded.

At runtime that key could never match:

  • Exact lookup compares the real host (api.good.com) with the string *.good.com, which is never equal.
  • includeSubdomains checks whether api.good ends with .*.good, which it never does.

So the connection came back as "domain not pinned", and only normal certificate validation ran. Nothing was logged.

Now: isValidPinnedDomain only allows letters, digits, -, _ and ., so *, :, /, @ and spaces are rejected. parseTrustKitConfiguration then raises the same NSException ("TrustKit configuration invalid") that it already raises for other config errors, such as having fewer than two pins. That happens inside initSharedInstanceWithConfiguration:. If it isn't caught, the app crashes at launch, so developers see it on the first run rather than shipping an unpinned app. The exception is apps that download their config at runtime: they'd hit it in production.

Alexander DiBenedetto added 2 commits October 22, 2026 02:36
#341)

Removes the bundled public suffix list. Keys are validated by shape, IP addresses are canonicalized with inet_pton/inet_ntop, and includeSubdomains uses plain suffix matching.

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.

🔵 Needs a closer look

Security-sensitive hostname normalization and pinning-scope changes warrant human review and native Apple-platform test confirmation.

No open issues 🎉

What changed in this PR

This PR expands TrustKit’s pinning configuration to support internal domains and IP addresses without relying on a bundled public suffix list.

Changes:

  • Adds hostname validation, IP canonicalization, duplicate-key detection, and suffix-based subdomain matching.
  • Removes the domain-registry dependency and adds configuration tests, IP certificate validation tests, and documentation.
  • Updates Xcode resources and temporarily removes watchOS simulator testing from CI.
File Description
TrustKitTests/​TSKPinningValidatorTests.m Adds IP-address certificate validation tests.
TrustKitTests/​TSKPinConfigurationTests.m Tests expanded hostname support and invalid configurations.
TrustKit/​public/​TrustKit/​TSKTrustKitConfig.h Documents IP keys and subdomain restrictions.
TrustKit/​Pinning/​ssl_pin_verifier.m Removes the domain-registry import.
TrustKit/​parse_configuration.m Validates canonical keys and rejects duplicates.
TrustKit/​Dependencies/​README.md Removes the domain-registry dependency listing.
TrustKit/​Dependencies/​domain_registry/​private/​tsk_assert.h Deletes registry assertion declarations.
TrustKit/​Dependencies/​domain_registry/​private/​tsk_assert.c Deletes registry assertion handling.
TrustKit/​Dependencies/​domain_registry/​private/​trie_search.h Deletes registry trie-search declarations.
TrustKit/​Dependencies/​domain_registry/​private/​trie_search.c Deletes registry trie-search logic.
TrustKit/​Dependencies/​domain_registry/​private/​trie_node.h Deletes the registry trie-node structure.
TrustKit/​Dependencies/​domain_registry/​private/​string_util.h Deletes registry string utilities.
TrustKit/​Dependencies/​domain_registry/​private/​registry_types.h Deletes registry-specific types.
TrustKit/​Dependencies/​domain_registry/​private/​registry_search.c Deletes public-suffix lookup logic.
TrustKit/​Dependencies/​domain_registry/​private/​init_registry_tables.c Deletes registry-table initialization.
TrustKit/​Dependencies/​domain_registry/​domain_registry.h Deletes the domain-registry interface.
TrustKit/​configuration_utils.m Implements canonicalization, validation, and suffix matching.
TrustKit/​configuration_utils.h Declares hostname validation and canonicalization helpers.
TrustKit.xcodeproj/​project.pbxproj Removes registry references and registers IP certificate fixtures.
docs/​getting-started.md Explains supported hosts and certificate trust requirements.
ATTRIBUTIONS Removes the domain-registry attribution.
.github/​workflows/​ci.yml Temporarily excludes watchOS simulator testing.

Review effort: Balanced

Comment thread TrustKit/configuration_utils.m Resolved

@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.

Besides the inline findings, I checked whether the new suffix-based isSubdomain in TrustKit/configuration_utils.m lets a lookalike host such as evilgood.com match a pinned good.com with includeSubdomains. It doesn't, because the match requires a leading "." before the pinned domain.

Extended reasoning...

This is a large change to pinning (security-sensitive). It replaces the public-suffix check with suffix matching and canonicalizes hostnames and IP addresses. Inline findings were reported. This note only records one more check: the lookalike-host case was looked at and is not affected.

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/Reporting/TSKBackgroundReporter.m:62 — Conditional, if the report collector validates app-platform: pin failure reports from Mac Catalyst, iOS-on-Mac and visionOS apps may now be rejected or misfiled. The reporter now sends three new values, "VISIONOS", "MAC_CATALYST" and "IOS_ON_MAC". The base sent "IOS" for all of these apps, and the documented example still shows only "IOS". Fix: confirm the default report URI backend and documented consumers accept the new values before shipping. Otherwise keep "IOS" for the iOS-family variants and report the variant in a separate new field.

    Why this was flagged

    TSKBackgroundReporter init (TSKBackgroundReporter.m:57-62) now sets _appPlatform to "VISIONOS", "MAC_CATALYST" or "IOS_ON_MAC". On the base, these targets all fell through to "IOS". The value goes into the JSON report as "app-platform" (TSKPinFailureReport.m:107) and is POSTed to every configured report URI, including the default Data Theorem endpoint. The only documented values are the existing platform names, with "IOS" in the TSKTrustKitConfig.h:165 example. Nothing in this checkout shows whether the receiving server parses app-platform as a closed enum. The condition I could not rule out: a collector or a customer's own report URI that validates or groups on this field. For it, pin failure reports from Catalyst, iPad-on-Mac and visionOS apps are dropped with a 4xx, or land in an unknown bucket, where the base filed them under IOS. Reports go to a background session with no retry on rejection (TSKBackgroundReporter.m:234-241 only logs the error), so those pin-failure signals are lost silently.

    Verification: TrustKit/Reporting/TSKBackgroundReporter.m:57-62 now sends "VISIONOS", "MAC_CATALYST" and "IOS_ON_MAC", where the base sent "IOS" for all three. The value goes out as @"app-platform": self.appPlatform (TSKPinFailureReport.m:107) to every report URI, including kTSKDefaultReportUri (TrustKit.m:29). TSKTrustKitConfig.h:165 still shows only "IOS".

Comment thread TrustKit/configuration_utils.m Resolved

@marcantoineg marcantoineg 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.

LGTM 👍

Comment thread TrustKit/configuration_utils.m Resolved
@alexanderdibenedetto
alexanderdibenedetto merged commit 8b40629 into v4-0-0 Oct 22, 2026
9 checks passed
@alexanderdibenedetto
alexanderdibenedetto deleted the adb/issue-341-relax-domain-name-restrictions-and-allow-ip-addresses branch October 22, 2026 20:22
alexanderdibenedetto added a commit that referenced this pull request Nov 3, 2026
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
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