diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbc28693..9b4d52a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,8 +37,6 @@ jobs: platform: [ios-simulator, macos, maccatalyst, tvos-simulator, visionos-simulator] steps: - uses: actions/checkout@v7 - - if: matrix.platform == 'watchos-simulator' - run: ./scripts/install_simulator_runtime.sh ${{ matrix.platform }} - run: ./scripts/run_unit_tests.sh ${{ matrix.platform }} -retry-tests-on-failure -test-iterations 3 - uses: actions/upload-artifact@v7 if: failure() diff --git a/TrustKit/Reporting/vendor_identifier.h b/TrustKit/Reporting/vendor_identifier.h index d003041e..2a8b6d8c 100644 --- a/TrustKit/Reporting/vendor_identifier.h +++ b/TrustKit/Reporting/vendor_identifier.h @@ -15,5 +15,5 @@ #import #endif -// Will return the IDFV on platforms that support it (iOS, tvOS) and a randomly generated UUID on other platforms (macOS, watchOS) +// Not the IDFV: a random UUID generated once per install and stored in the App's user defaults, on every platform NSString *identifier_for_vendor(void); diff --git a/TrustKit/Swizzling/TSKNSURLConnectionDelegateProxy.m b/TrustKit/Swizzling/TSKNSURLConnectionDelegateProxy.m index 08cde199..d98264a4 100644 --- a/TrustKit/Swizzling/TSKNSURLConnectionDelegateProxy.m +++ b/TrustKit/Swizzling/TSKNSURLConnectionDelegateProxy.m @@ -39,7 +39,7 @@ + (void)swizzleNSURLConnectionConstructors:(TrustKit *)trustKit { RSSWReplacement({ NSURLConnection *connection; - if ([NSStringFromClass([delegate class]) hasPrefix:@"TSK"]) { + if ([delegate isKindOfClass:[TSKNSURLConnectionDelegateProxy class]]) { // Don't proxy ourselves connection = RSSWCallOriginal(request, delegate); } else { @@ -62,7 +62,7 @@ + (void)swizzleNSURLConnectionConstructors:(TrustKit *)trustKit { RSSWReplacement({ NSURLConnection *connection; - if ([NSStringFromClass([delegate class]) hasPrefix:@"TSK"]) { + if ([delegate isKindOfClass:[TSKNSURLConnectionDelegateProxy class]]) { // Don't proxy ourselves connection = RSSWCallOriginal(request, delegate, startImmediately); } else { diff --git a/TrustKit/Swizzling/TSKNSURLSessionDelegateProxy.m b/TrustKit/Swizzling/TSKNSURLSessionDelegateProxy.m index d329e9ff..ee022598 100644 --- a/TrustKit/Swizzling/TSKNSURLSessionDelegateProxy.m +++ b/TrustKit/Swizzling/TSKNSURLSessionDelegateProxy.m @@ -11,6 +11,7 @@ #import "TSKNSURLSessionDelegateProxy.h" #import "../Dependencies/RSSwizzle/RSSwizzle.h" +#import "../Reporting/TSKBackgroundReporter.h" #import "../TSKLog.h" #import "../public/TrustKit/TSKPinningValidator.h" #import "../public/TrustKit/TSKTrustDecision.h" @@ -48,7 +49,7 @@ + (void)swizzleNSURLSessionConstructors:(TrustKit *)trustKit { } // Do not swizzle TrustKit objects (such as the reporter) - else if ([NSStringFromClass([delegate class]) hasPrefix:@"TSK"]) { + else if ([delegate isKindOfClass:[TSKBackgroundReporter class]] || [delegate isKindOfClass:[TSKNSURLSessionDelegateProxy class]]) { session = RSSWCallOriginal(configuration, delegate, queue); } else { // Replace the delegate with our own so we can intercept and handle authentication challenges diff --git a/TrustKit/TSKPinningValidator.m b/TrustKit/TSKPinningValidator.m index 6924fb09..1780e84b 100644 --- a/TrustKit/TSKPinningValidator.m +++ b/TrustKit/TSKPinningValidator.m @@ -49,17 +49,6 @@ to platforms that support a user trust store (Mac OS). @implementation TSKPinningValidator -+ (BOOL)allowsAdditionalTrustAnchors { - static const BOOL allowAdditionalTrustAnchors = { -#if DEBUG == 1 - YES -#else - NO -#endif - }; - return allowAdditionalTrustAnchors; -} - #pragma mark Instance Methods - (instancetype _Nullable)initWithDomainPinningPolicies:(NSDictionary *_Nonnull)domainPinningPolicies @@ -127,7 +116,11 @@ - (TSKTrustDecision)evaluateTrust:(SecTrustRef _Nonnull)serverTrust forHostname: } else #endif { - if (validationResult == TSKTrustEvaluationFailedNoMatchingPin) { + BOOL noMatchingPin = (validationResult == TSKTrustEvaluationFailedNoMatchingPin); +#if !TARGET_OS_IPHONE + noMatchingPin = noMatchingPin || (validationResult == TSKTrustEvaluationFailedUserDefinedTrustAnchor); +#endif + if (noMatchingPin) { // Is pinning enforced? if ([domainConfig[kTSKEnforcePinning] boolValue] == YES) { // Yes - Block the connection diff --git a/TrustKit/TSKTrustKitConfig.m b/TrustKit/TSKTrustKitConfig.m index 4f8403d6..5c326a24 100644 --- a/TrustKit/TSKTrustKitConfig.m +++ b/TrustKit/TSKTrustKitConfig.m @@ -22,7 +22,7 @@ // Keys for each domain within the TSKPinnedDomains entry const TSKDomainConfigurationKey kTSKPublicKeyHashes = @"TSKPublicKeyHashes"; const TSKDomainConfigurationKey kTSKEnforcePinning = @"TSKEnforcePinning"; -const TSKDomainConfigurationKey kTSKExcludeSubdomainFromParentPolicy = @"kSKExcludeSubdomainFromParentPolicy"; +const TSKDomainConfigurationKey kTSKExcludeSubdomainFromParentPolicy = @"TSKExcludeSubdomainFromParentPolicy"; const TSKDomainConfigurationKey kTSKIncludeSubdomains = @"TSKIncludeSubdomains"; const TSKDomainConfigurationKey kTSKReportUris = @"TSKReportUris"; diff --git a/TrustKit/TrustKit.m b/TrustKit/TrustKit.m index 3dad2d88..0cace030 100644 --- a/TrustKit/TrustKit.m +++ b/TrustKit/TrustKit.m @@ -185,7 +185,7 @@ - (void)sendValidationReport:(TSKPinningValidatorResult *)result // Send a report only if the there was a pinning failure if (validationResult != TSKTrustEvaluationSuccess) { #if !TARGET_OS_IPHONE - if (validationResult != TSKTrustEvaluationFailedUserDefinedTrustAnchor) + if ((validationResult != TSKTrustEvaluationFailedUserDefinedTrustAnchor) || ![self.configuration[kTSKIgnorePinningForUserDefinedTrustAnchors] boolValue]) #endif { // Pin validation failed: retrieve the list of configured report URLs diff --git a/TrustKit/parse_configuration.m b/TrustKit/parse_configuration.m index c48dadfb..866d5383 100644 --- a/TrustKit/parse_configuration.m +++ b/TrustKit/parse_configuration.m @@ -52,6 +52,11 @@ @"TSKPinnedDomains key within TSKConfiguration."]; } + NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; + dateFormat.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + dateFormat.dateFormat = @"yyyy-MM-dd"; + dateFormat.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; + for (NSString *configuredDomainName in trustKitArguments[kTSKPinnedDomains]) { // Sanity checks on the domain name NSString *domainName = canonicalHostname(configuredDomainName); @@ -106,11 +111,11 @@ NSString *expirationDateStr = domainPinningPolicy[kTSKExpirationDate]; if (expirationDateStr != nil) { // Convert the string in the yyyy-MM-dd format into an actual date in UTC - NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init]; - dateFormat.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; - dateFormat.dateFormat = @"yyyy-MM-dd"; - dateFormat.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; - NSDate *expirationDate = [dateFormat dateFromString:expirationDateStr]; + NSDate *expirationDate = [expirationDateStr isKindOfClass:[NSString class]] ? [dateFormat dateFromString:expirationDateStr] : nil; + if (expirationDate == nil) { + [NSException raise:@"TrustKit configuration invalid" + format:@"TrustKit was initialized with an invalid value for %@ for domain %@; expected a date in yyyy-MM-dd format", kTSKExpirationDate, domainName]; + } domainFinalConfiguration[kTSKExpirationDate] = expirationDate; } diff --git a/TrustKit/public/TrustKit/TSKTrustKitConfig.h b/TrustKit/public/TrustKit/TSKTrustKitConfig.h index 2ce5b613..35af5fa5 100644 --- a/TrustKit/public/TrustKit/TSKTrustKitConfig.h +++ b/TrustKit/public/TrustKit/TSKTrustKitConfig.h @@ -161,15 +161,16 @@ FOUNDATION_EXPORT const TSKDomainConfigurationKey kTSKReportUris; By default, pin failure reports are sent to a report server hosted by Data Theorem, for detecting potential CA compromises and man-in-the-middle attacks, as well as providing a free dashboard for developers; email info@datatheorem.com if you'd like a dashboard for - your App. Only pin failure reports are sent, which contain the App's bundle ID, the IDFV, - and the server's hostname and certificate chain that failed validation. + your App. Only pin failure reports are sent, which contain the App's bundle ID, a random UUID + that TrustKit generates once per install, and the server's hostname and certificate chain that + failed validation. */ FOUNDATION_EXPORT const TSKDomainConfigurationKey kTSKDisableDefaultReportUri; /** A string containing the date, in yyyy-MM-dd format, on which the domain's configured SSL pins expire, thus disabling pinning validation. If the key is not set, then the pins do - not expire. + not expire. Any other value is a configuration error. Expiration helps prevent connectivity issues in Apps which do not get updates to their pin set, such as when the user disables App updates. diff --git a/TrustKit/public/TrustKit/TrustKit.h b/TrustKit/public/TrustKit/TrustKit.h index fa1b8267..7768984e 100644 --- a/TrustKit/public/TrustKit/TrustKit.h +++ b/TrustKit/public/TrustKit/TrustKit.h @@ -65,6 +65,7 @@ NS_ASSUME_NONNULL_BEGIN | ____ TSKEnforcePinning | Boolean | | ____ TSKReportUris | Array | | ____ TSKDisableDefaultReportUri | Boolean | + | ____ TSKExpirationDate | String | ``` When setting the pinning policy programmatically, it has to be supplied to the diff --git a/TrustKitTests/TSKPinConfigurationTests.m b/TrustKitTests/TSKPinConfigurationTests.m index d3949210..9e70d643 100644 --- a/TrustKitTests/TSKPinConfigurationTests.m +++ b/TrustKitTests/TSKPinConfigurationTests.m @@ -69,6 +69,7 @@ - (void)testDisablePinningForSubdomainAndNoPublicKey { NSString *serverConfigKey = getPinningConfigurationKeyForDomain(@"unsecured.good.com", trustKitConfig[kTSKPinnedDomains]); XCTAssertEqualObjects(serverConfigKey, @"unsecured.good.com", @"Did not receive a configuration for pinned subdomain"); + XCTAssertEqualObjects(kTSKExcludeSubdomainFromParentPolicy, @"TSKExcludeSubdomainFromParentPolicy", @"Info.plist configurations use the documented key"); } - (void)testExplicitNotDisablePinningForSubdomainAdditionalDomainKeys { @@ -243,6 +244,26 @@ - (void)testExpirationDateIgnoresUserCalendar { XCTAssertEqualObjects(trustKitConfig[kTSKPinnedDomains][@"www.good.com"][kTSKExpirationDate], [gregorian dateFromComponents:components]); } +- (void)testMalformedExpirationDateIsRejected { + for (id expirationDate in @[@"12/31/2026", @"2026-31-12", @"", @20261231, [NSDate date]]) { + XCTAssertThrows( + parseTrustKitConfiguration(@{ + kTSKPinnedDomains: @ { + @"www.good.com": @ { + kTSKExpirationDate: expirationDate, + kTSKPublicKeyHashes: @[ + @"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=", + @"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + ] + } + } + }), + @"Expiration date %@ must be rejected", + expirationDate + ); + } +} + - (void)testGetConfigurationPinningDisabled { NSDictionary *trustKitConfig; trustKitConfig = parseTrustKitConfiguration(@{ diff --git a/TrustKitTests/TSKPinningValidatorTests.m b/TrustKitTests/TSKPinningValidatorTests.m index d5c64788..bce7066c 100644 --- a/TrustKitTests/TSKPinningValidatorTests.m +++ b/TrustKitTests/TSKPinningValidatorTests.m @@ -43,15 +43,6 @@ - (void)resetSubjectPublicKeyInfoDiskCache; - (void)waitForPendingSubjectPublicKeyInfoDiskWrites; @end -static BOOL AllowsAdditionalTrustAnchors = YES; // toggle in tests if needed -@interface TestPinningValidator : TSKPinningValidator -@end -@implementation TestPinningValidator -+ (BOOL)allowsAdditionalTrustAnchors { - return AllowsAdditionalTrustAnchors; -} -@end - @interface TSKPinningValidatorTests : XCTestCase @end diff --git a/TrustKitTests/TSKReporterTests.m b/TrustKitTests/TSKReporterTests.m index 3f46874f..d680b3f8 100644 --- a/TrustKitTests/TSKReporterTests.m +++ b/TrustKitTests/TSKReporterTests.m @@ -161,7 +161,7 @@ - (void)testSendReportFromValidationReport { #if !TARGET_OS_IPHONE // TEST USER-DEFINED SUCCESS // Send a notification for a successful validation and ensure no report gets sent - pinReporterMock = OCMClassMock([TSKBackgroundReporter class]); + pinReporterMock = OCMStrictClassMock([TSKBackgroundReporter class]); _trustKit.pinFailureReporter = pinReporterMock; res = [[TSKPinningValidatorResult alloc] initWithServerHostname:@"www.test.com" @@ -178,6 +178,45 @@ - (void)testSendReportFromValidationReport { #endif } +#if !TARGET_OS_IPHONE +- (void)testReportUserDefinedTrustAnchorWhenNotIgnored { + _trustKit = [[TrustKit alloc] initWithConfiguration:@{ + kTSKIgnorePinningForUserDefinedTrustAnchors: @NO, + kTSKPinnedDomains: @ { + @"www.test.com": @ { + kTSKPublicKeyHashes: @[ + @"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // Fake key + @"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=" // Fake key 2 + ] + } + } + }]; + + id pinReporterMock = OCMClassMock([TSKBackgroundReporter class]); + _trustKit.pinFailureReporter = pinReporterMock; + OCMExpect([pinReporterMock pinValidationFailedForHostname:@"www.test.com" + port:nil + certificateChain:OCMOCK_ANY + notedHostname:@"www.test.com" + reportURIs:OCMOCK_ANY + includeSubdomains:NO + enforcePinning:YES + knownPins:OCMOCK_ANY + validationResult:TSKTrustEvaluationFailedUserDefinedTrustAnchor + expirationDate:nil]); + + TSKPinningValidatorResult *res = [[TSKPinningValidatorResult alloc] initWithServerHostname:@"www.test.com" + serverTrust:_testTrust + validationResult:TSKTrustEvaluationFailedUserDefinedTrustAnchor + finalTrustDecision:TSKTrustDecisionShouldBlockConnection + validationDuration:1.0]; + [_trustKit sendValidationReport:res notedHostname:@"www.test.com" pinningPolicy:_trustKit.configuration[kTSKPinnedDomains][@"www.test.com"]]; + + [pinReporterMock verify]; + [pinReporterMock stopMocking]; +} +#endif + - (void)testReporter { // Just try a simple valid case to see if we can post this to the default report URL TSKBackgroundReporter *reporter = [[TSKBackgroundReporter alloc] initAndRateLimitReports:NO sharedContainerIdentifier:nil]; diff --git a/TrustKitTests/TSKSwizzlingTests.m b/TrustKitTests/TSKSwizzlingTests.m index ee96c84b..009176c9 100644 --- a/TrustKitTests/TSKSwizzlingTests.m +++ b/TrustKitTests/TSKSwizzlingTests.m @@ -14,6 +14,12 @@ @interface TSKSwizzlingTests : XCTestCase @end +@interface TSKAppSessionDelegate : NSObject +@end + +@implementation TSKAppSessionDelegate +@end + /* Basic tests to ensure RSSwizzle does not trigger a crash because a method we are hooking has changed */ @implementation TSKSwizzlingTests @@ -42,6 +48,12 @@ - (void)testNSURLSession { TrustKit *trustKit = [[TrustKit alloc] initWithConfiguration:trustKitConfig]; [TSKNSURLSessionDelegateProxy swizzleNSURLSessionConstructors:trustKit]; + + NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] + delegate:[[TSKAppSessionDelegate alloc] init] + delegateQueue:nil]; + XCTAssertTrue([session.delegate isKindOfClass:[TSKNSURLSessionDelegateProxy class]], @"An app delegate whose class name starts with TSK must still be proxied"); + [session invalidateAndCancel]; } - (void)testNSURLConnection { diff --git a/docs/images/linking3_dynamic.png b/docs/images/linking3_dynamic.png index 5e4fe0a7..d6de9c0e 100755 Binary files a/docs/images/linking3_dynamic.png and b/docs/images/linking3_dynamic.png differ diff --git a/scripts/install_simulator_runtime.sh b/scripts/install_simulator_runtime.sh deleted file mode 100755 index 49494bc7..00000000 --- a/scripts/install_simulator_runtime.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash - -. "$(dirname "$0")/lib.sh" - -PLATFORM="${1:?usage: $0 }" -configure_platform "$PLATFORM" - -RUNTIME="${PLATFORM_NAME% Simulator}" -OS_VERSION="$(xcrun --sdk "$SIMULATOR_SDK" --show-sdk-version)" - -# simctl can list the device as available while xcodebuild sees its runtime as (null), so ask xcodebuild -simulator_available() { - xcodebuild -showdestinations -project TrustKit.xcodeproj -scheme "$SCHEME" 2>/dev/null | awk -v destination="OS:${OS_VERSION}, name:${SIMULATOR} }" ' - /Destinations compatible with/ { compatible = 1; next } - /Destinations incompatible with/ { compatible = 0 } - compatible && index($0, destination) { found = 1 } - END { exit !found }' -} - -runtime_images() { - xcrun simctl runtime list -j | jq -r --arg platform "com.apple.platform.${SIMULATOR_SDK}" --arg version "$OS_VERSION" \ - "to_entries[] | select(.value.platformIdentifier == \$platform and .value.version == \$version) | $1" -} - -sudo --preserve-env=DEVELOPER_DIR xcodebuild -runFirstLaunch -xcrun simctl list > /dev/null - -if simulator_available; then - exit 0 -fi - -echo "${SIMULATOR} (${RUNTIME} ${OS_VERSION}) is not available; reinstalling the ${RUNTIME} ${OS_VERSION} runtime" >&2 -runtime_images '"\(.key) \(.value.kind), \(.value.state), signature \(.value.signatureState)"' >&2 - -EXPORT_DIR="$(mktemp -d)" -trap 'rm -rf "$EXPORT_DIR"' EXIT -xcodebuild -downloadPlatform "$RUNTIME" -buildVersion "$OS_VERSION" -architectureVariant arm64 -exportPath "$EXPORT_DIR" || true -IMAGE="$(find "$EXPORT_DIR" -name '*.dmg' -print -quit)" -if [[ -z "$IMAGE" ]]; then - echo "No ${RUNTIME} ${OS_VERSION} runtime image found in the export" >&2 - exit 1 -fi - -for IMAGE_ID in $(runtime_images .key); do - xcrun simctl runtime delete "$IMAGE_ID" -done -xcrun simctl runtime add "$IMAGE" - -if ! simulator_available; then - xcrun simctl runtime list >&2 - xcrun simctl list devices "${RUNTIME} ${OS_VERSION}" >&2 - echo "${SIMULATOR} (${RUNTIME} ${OS_VERSION}) is still not available" >&2 - exit 1 -fi