@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,339 +1,274 @@
|
|
|
1
|
-
# Certificate Trust
|
|
1
|
+
# Certificate Trust, Pinning and Client Certificates
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
What is here: the Security types for certificates, trust, identities and policies;
|
|
4
|
+
pinning, whether by leaf, by issuing CA, by public-key digest or declaratively through
|
|
5
|
+
`NSPinnedDomains`; building your own trust policy; mutual TLS; the points where App
|
|
6
|
+
Transport Security and trust evaluation overlap; and running a pin set in production.
|
|
7
|
+
It applies from iOS 12 through iOS 18 and macOS 10.14 through 15.
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
What is not here: transport concerns other than checking TLS certificates, managing
|
|
10
|
+
certificates on your servers, and ATS as a configuration topic (the `ios-networking`
|
|
11
|
+
skill owns that).
|
|
8
12
|
|
|
9
13
|
## Contents
|
|
10
14
|
|
|
11
|
-
- [
|
|
12
|
-
- [
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- [
|
|
18
|
-
- [
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| `SecCertificate` | X.509 certificate (DER-encoded) | `SecCertificateCreateWithData`, `SecCertificateCopyKey` (iOS 12+), `SecCertificateCopyData`, `SecCertificateCopySubjectSummary` |
|
|
44
|
-
| `SecTrust` | Trust evaluation context for a certificate chain against policies | `SecTrustCreateWithCertificates`, `SecTrustEvaluateWithError` (iOS 12+), `SecTrustEvaluateAsyncWithError` (iOS 13+) |
|
|
45
|
-
| `SecIdentity` | Private key + certificate pair for client authentication | Extracted via `SecPKCS12Import`; used with `URLCredential(identity:certificates:persistence:)` |
|
|
46
|
-
| `SecPolicy` | Validation policy (SSL hostname check, revocation) | `SecPolicyCreateSSL`, `SecPolicyCreateRevocation` |
|
|
47
|
-
|
|
48
|
-
---
|
|
49
|
-
|
|
50
|
-
## Trust Evaluation APIs
|
|
51
|
-
|
|
52
|
-
Three trust evaluation functions exist. Only two are current.
|
|
53
|
-
|
|
54
|
-
### SecTrustEvaluateAsyncWithError - recommended async API (iOS 13+)
|
|
15
|
+
- [The four types](#the-four-types)
|
|
16
|
+
- [Evaluating trust](#evaluating-trust)
|
|
17
|
+
- [Custom policies and anchors](#custom-policies-and-anchors)
|
|
18
|
+
- [Four ways to pin](#four-ways-to-pin)
|
|
19
|
+
- [Certificates and identities](#certificates-and-identities)
|
|
20
|
+
- [Mistakes to reject in review](#mistakes-to-reject-in-review)
|
|
21
|
+
- [Backup pins and rotation](#backup-pins-and-rotation)
|
|
22
|
+
- [Where ATS meets trust](#where-ats-meets-trust)
|
|
23
|
+
- [API timeline](#api-timeline)
|
|
24
|
+
- [Threads and performance](#threads-and-performance)
|
|
25
|
+
- [CI guardrails](#ci-guardrails)
|
|
26
|
+
- [Related and sources](#related-and-sources)
|
|
27
|
+
- [Checklist](#checklist)
|
|
28
|
+
|
|
29
|
+
## The four types
|
|
30
|
+
|
|
31
|
+
| Type | Role | Calls you will use |
|
|
32
|
+
| --- | --- | --- |
|
|
33
|
+
| `SecCertificate` | One X.509 certificate, DER bytes | create from data, copy the data back, copy a subject summary, and from iOS 12 `SecCertificateCopyKey` |
|
|
34
|
+
| `SecTrust` | Certificates plus the policies they are judged by | `SecTrustCreateWithCertificates`; evaluation with `SecTrustEvaluateWithError` (12+) or the async variant (13+) |
|
|
35
|
+
| `SecIdentity` | Certificate with its private key, for client authentication | Comes out of `SecPKCS12Import`; goes into `URLCredential(identity:certificates:persistence:)` |
|
|
36
|
+
| `SecPolicy` | Which rules apply | `SecPolicyCreateSSL`, `SecPolicyCreateRevocation` |
|
|
37
|
+
|
|
38
|
+
## Evaluating trust
|
|
39
|
+
|
|
40
|
+
`SecTrustEvaluateAsyncWithError` takes the trust object, a dispatch queue and a
|
|
41
|
+
callback. The callback receives the trust, a success flag and an optional `CFError`;
|
|
42
|
+
the function itself returns an `OSStatus` saying whether evaluation could start.
|
|
43
|
+
|
|
44
|
+
When a cached verdict exists, the callback may run before the call returns. Because
|
|
45
|
+
evaluation may fetch intermediates and query revocation servers, start it from a
|
|
46
|
+
background queue:
|
|
55
47
|
|
|
56
48
|
```swift
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
_ queue: dispatch_queue_t,
|
|
60
|
-
_ result: @escaping (SecTrust, Bool, CFError?) -> Void
|
|
61
|
-
) -> OSStatus
|
|
62
|
-
```
|
|
49
|
+
import Foundation
|
|
50
|
+
import Security
|
|
63
51
|
|
|
64
|
-
|
|
52
|
+
struct TrustFailure: Error {
|
|
53
|
+
let status: OSStatus
|
|
54
|
+
let underlying: CFError?
|
|
55
|
+
}
|
|
65
56
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
completion(result, error as Error?)
|
|
57
|
+
func checkTrustOffMain(_ trust: SecTrust, then report: @escaping @Sendable (TrustFailure?) -> Void) {
|
|
58
|
+
let worker = DispatchQueue(label: "tls.trust", qos: .userInitiated)
|
|
59
|
+
nonisolated(unsafe) let handedOver = trust
|
|
60
|
+
worker.async {
|
|
61
|
+
let started = SecTrustEvaluateAsyncWithError(handedOver, worker) { _, ok, cfError in
|
|
62
|
+
report(ok ? nil : TrustFailure(status: errSecNotTrusted, underlying: cfError))
|
|
73
63
|
}
|
|
74
|
-
if
|
|
75
|
-
|
|
64
|
+
if started != errSecSuccess {
|
|
65
|
+
report(TrustFailure(status: started, underlying: nil))
|
|
76
66
|
}
|
|
77
67
|
}
|
|
78
68
|
}
|
|
79
69
|
```
|
|
80
70
|
|
|
81
|
-
|
|
71
|
+
`SecTrust` is not `Sendable`, so moving it onto the worker queue is marked
|
|
72
|
+
`nonisolated(unsafe)`: the caller promises not to touch the trust while it is
|
|
73
|
+
being evaluated. `report` is `@Sendable` because it runs on that queue.
|
|
74
|
+
|
|
75
|
+
Apple has not added async/await versions of these calls as of iOS 18. A continuation
|
|
76
|
+
bridges the gap:
|
|
82
77
|
|
|
83
78
|
```swift
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
queue.async {
|
|
89
|
-
let status = SecTrustEvaluateAsyncWithError(trust, queue) { _, result, error in
|
|
90
|
-
if result {
|
|
91
|
-
continuation.resume(returning: true)
|
|
92
|
-
} else {
|
|
93
|
-
continuation.resume(throwing: error! as Error)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
if status != errSecSuccess {
|
|
97
|
-
continuation.resume(throwing: NSError(
|
|
98
|
-
domain: NSOSStatusErrorDomain, code: Int(status)))
|
|
99
|
-
}
|
|
79
|
+
func verifiedTrust(_ trust: SecTrust) async throws {
|
|
80
|
+
let _: Void = try await withCheckedThrowingContinuation { cont in
|
|
81
|
+
checkTrustOffMain(trust) { failure in
|
|
82
|
+
if let failure { cont.resume(throwing: failure) } else { cont.resume() }
|
|
100
83
|
}
|
|
101
84
|
}
|
|
102
85
|
}
|
|
103
86
|
```
|
|
104
87
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
**Not deprecated.** Valid inside `URLSessionDelegate` callbacks (already off main thread). Apple's warning: do not call from the main run loop - it may require network access.
|
|
112
|
-
|
|
113
|
-
### SecTrustEvaluate - deprecated since iOS 13
|
|
114
|
-
|
|
115
|
-
```swift
|
|
116
|
-
// ❌ DEPRECATED: Returns opaque SecTrustResultType without error context
|
|
117
|
-
func SecTrustEvaluate(_ trust: SecTrust,
|
|
118
|
-
_ result: UnsafeMutablePointer<SecTrustResultType>) -> OSStatus
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
Returns a `SecTrustResultType` enum requiring manual interpretation. Replaced by `SecTrustEvaluateWithError`. **AI generators frequently produce this pattern - reject on sight.**
|
|
122
|
-
|
|
123
|
-
### SecTrustResultType reference
|
|
124
|
-
|
|
125
|
-
For code that must inspect results after evaluation via `SecTrustGetTrustResult`:
|
|
126
|
-
|
|
127
|
-
| Result | Meaning | Action |
|
|
128
|
-
| -------------------------- | -------------------------------------------- | --------------------------------- |
|
|
129
|
-
| `.unspecified` | Chain validates to implicitly trusted anchor | **Proceed** - most common success |
|
|
130
|
-
| `.proceed` | User explicitly chose to trust this cert | **Proceed** |
|
|
131
|
-
| `.deny` | User explicitly marked cert as untrusted | **Reject** - never override |
|
|
132
|
-
| `.recoverableTrustFailure` | Failed but recovery possible | Inspect, possibly reconfigure |
|
|
133
|
-
| `.fatalTrustFailure` | Fundamental certificate defect | **Reject** |
|
|
134
|
-
| `.otherError` | Non-trust error (revoked, OS error) | **Reject** |
|
|
135
|
-
| `.invalid` | No evaluation performed yet | Call evaluation first |
|
|
88
|
+
The blocking form, `SecTrustEvaluateWithError`, which returns a `Bool` and fills an
|
|
89
|
+
optional `CFError` out-parameter, arrived in iOS 12 and remains supported. Calling it
|
|
90
|
+
from a `URLSessionDelegate` challenge handler is fine because that handler is already
|
|
91
|
+
off the main thread. Keep it away from the main run loop: it may go to the network.
|
|
136
92
|
|
|
137
|
-
|
|
93
|
+
`SecTrustEvaluate(_:_:)`, the old call that returns a `SecTrustResultType`, has been
|
|
94
|
+
deprecated since iOS 13. Reject code that uses it.
|
|
138
95
|
|
|
139
|
-
|
|
96
|
+
If you need the detailed verdict, read it with `SecTrustGetTrustResult` after
|
|
97
|
+
evaluating:
|
|
140
98
|
|
|
141
|
-
|
|
99
|
+
| `SecTrustResultType` | Meaning | Action |
|
|
100
|
+
| --- | --- | --- |
|
|
101
|
+
| `.unspecified` | Chains to an implicitly trusted anchor; the usual success | Proceed |
|
|
102
|
+
| `.proceed` | User explicitly trusted it | Proceed |
|
|
103
|
+
| `.deny` | User explicitly distrusted it | Reject; never override |
|
|
104
|
+
| `.recoverableTrustFailure` | Failed, but settings could change the result | Inspect, reconfigure or reject |
|
|
105
|
+
| `.fatalTrustFailure` | Structurally invalid | Reject |
|
|
106
|
+
| `.otherError` | Revoked, or an OS error | Reject |
|
|
107
|
+
| `.invalid` | Not evaluated yet | Evaluate first |
|
|
142
108
|
|
|
143
|
-
|
|
144
|
-
// ✅ CORRECT: SSL policy with hostname verification
|
|
145
|
-
let policy = SecPolicyCreateSSL(true, "api.example.com" as CFString)
|
|
146
|
-
// true = server evaluation; hostname enables SNI matching
|
|
147
|
-
|
|
148
|
-
var trust: SecTrust?
|
|
149
|
-
SecTrustCreateWithCertificates(certificateChain as CFTypeRef, policy, &trust)
|
|
150
|
-
```
|
|
109
|
+
Only `.unspecified` and `.proceed` count as trusted.
|
|
151
110
|
|
|
152
|
-
|
|
153
|
-
// ✅ CORRECT: Custom anchor while preserving system trust store
|
|
154
|
-
SecTrustSetAnchorCertificates(trust, [customRootCA] as CFArray)
|
|
155
|
-
SecTrustSetAnchorCertificatesOnly(trust, false) // false = ALSO trust system anchors
|
|
156
|
-
```
|
|
111
|
+
## Custom policies and anchors
|
|
157
112
|
|
|
158
113
|
```swift
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
114
|
+
func makeTrust(chain: [SecCertificate], host: String,
|
|
115
|
+
privateRoot: SecCertificate) -> SecTrust? {
|
|
116
|
+
let policy = SecPolicyCreateSSL(true, host as CFString)
|
|
117
|
+
var trust: SecTrust?
|
|
118
|
+
guard SecTrustCreateWithCertificates(chain as CFArray, policy, &trust) == errSecSuccess,
|
|
119
|
+
let trust else { return nil }
|
|
120
|
+
|
|
121
|
+
SecTrustSetAnchorCertificates(trust, [privateRoot] as CFArray)
|
|
122
|
+
SecTrustSetAnchorCertificatesOnly(trust, false)
|
|
123
|
+
return trust
|
|
124
|
+
}
|
|
163
125
|
```
|
|
164
126
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
let policy = SecPolicyCreateSSL(true, nil)
|
|
168
|
-
// Any valid certificate for ANY domain now passes - MITM vector
|
|
169
|
-
```
|
|
127
|
+
`SecPolicyCreateSSL(true, host)` evaluates a server certificate and checks it against
|
|
128
|
+
the hostname. Two traps:
|
|
170
129
|
|
|
171
|
-
|
|
130
|
+
- Setting anchor certificates without `SecTrustSetAnchorCertificatesOnly(trust, false)`
|
|
131
|
+
quietly turns off every system root. Pass `false` unless you really mean to trust
|
|
132
|
+
only your own CA.
|
|
133
|
+
- `SecPolicyCreateSSL(true, nil)` skips hostname checking. Any valid certificate for any
|
|
134
|
+
domain passes, which is a man-in-the-middle opening.
|
|
172
135
|
|
|
173
|
-
## Four
|
|
136
|
+
## Four ways to pin
|
|
174
137
|
|
|
175
|
-
### Leaf certificate
|
|
138
|
+
### 1. Leaf certificate (do not ship)
|
|
176
139
|
|
|
177
|
-
|
|
140
|
+
Server certificates last from 90 days (common with free ACME CAs) up to 398 days (the
|
|
141
|
+
CA/Browser Forum maximum). Every renewal changes the bytes, the pin stops matching, and
|
|
142
|
+
nobody can connect until a fixed build clears review and reaches their phone.
|
|
178
143
|
|
|
179
144
|
```swift
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
let serverCertData = SecCertificateCopyData(serverCert) as Data
|
|
187
|
-
let localCertData = // loaded from bundle .cer file
|
|
188
|
-
|
|
189
|
-
if serverCertData == localCertData {
|
|
190
|
-
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
|
191
|
-
} else {
|
|
192
|
-
// WILL fire when the certificate renews, locking out all users
|
|
193
|
-
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
145
|
+
// Anti-pattern: exact leaf comparison
|
|
146
|
+
func leafMatches(_ trust: SecTrust, bundled: Data) -> Bool {
|
|
147
|
+
let certs = (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? []
|
|
148
|
+
return certs.first.map { SecCertificateCopyData($0) as Data } == bundled
|
|
194
149
|
}
|
|
195
150
|
```
|
|
196
151
|
|
|
197
|
-
|
|
152
|
+
Only consider this if you own the entire certificate lifecycle and can push new pins
|
|
153
|
+
without app review.
|
|
198
154
|
|
|
199
|
-
### Intermediate CA
|
|
155
|
+
### 2. Intermediate CA
|
|
200
156
|
|
|
201
|
-
|
|
157
|
+
Compare each certificate in the chain with a bundled intermediate. Intermediates are
|
|
158
|
+
valid for 5-10 years, so leaf renewals pass. The trade-off: any certificate that CA
|
|
159
|
+
issues for your hostname passes too, so a compromise of that CA is in scope.
|
|
202
160
|
|
|
203
161
|
```swift
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
let pinnedIntermediateData = // load intermediate CA .cer from bundle
|
|
211
|
-
|
|
212
|
-
for cert in chain {
|
|
213
|
-
let certData = SecCertificateCopyData(cert) as Data
|
|
214
|
-
if certData == pinnedIntermediateData {
|
|
215
|
-
completionHandler(.useCredential, URLCredential(trust: serverTrust))
|
|
216
|
-
return
|
|
217
|
-
}
|
|
162
|
+
func chainContainsIntermediate(_ trust: SecTrust, pinned: Data) -> Bool {
|
|
163
|
+
let certs = (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? []
|
|
164
|
+
return certs.dropFirst().contains { SecCertificateCopyData($0) as Data == pinned }
|
|
218
165
|
}
|
|
219
|
-
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
220
166
|
```
|
|
221
167
|
|
|
222
|
-
|
|
168
|
+
### 3. SPKI hash (recommended in code)
|
|
223
169
|
|
|
224
|
-
|
|
170
|
+
The hash of the SubjectPublicKeyInfo stays the same across renewals as long as the key
|
|
171
|
+
pair does, which makes it the programmatic approach to choose.
|
|
225
172
|
|
|
226
|
-
|
|
173
|
+
One detail breaks most hand-written implementations: what
|
|
174
|
+
`SecKeyCopyExternalRepresentation` gives back is only the key material, lacking the
|
|
175
|
+
ASN.1 prefix that turns it into a SubjectPublicKeyInfo. Put the algorithm's prefix in
|
|
176
|
+
front before hashing; pins produced with OpenSSL are computed over the full structure
|
|
177
|
+
and will otherwise never match.
|
|
227
178
|
|
|
228
|
-
|
|
179
|
+
```swift
|
|
180
|
+
import CryptoKit
|
|
229
181
|
|
|
230
|
-
|
|
182
|
+
final class PinnedSessionDelegate: NSObject, URLSessionDelegate {
|
|
183
|
+
private let acceptedPins: Set<String>
|
|
231
184
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
class SPKIPinningDelegate: NSObject, URLSessionDelegate {
|
|
235
|
-
|
|
236
|
-
// ASN.1 headers for reconstructing SPKI from raw key data
|
|
237
|
-
private static let rsa2048Header: [UInt8] = [
|
|
238
|
-
0x30, 0x82, 0x01, 0x22, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86,
|
|
239
|
-
0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00, 0x03, 0x82, 0x01, 0x0f, 0x00
|
|
240
|
-
]
|
|
241
|
-
private static let ecP256Header: [UInt8] = [
|
|
242
|
-
0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x02,
|
|
243
|
-
0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x03,
|
|
244
|
-
0x42, 0x00
|
|
245
|
-
]
|
|
246
|
-
|
|
247
|
-
private let pinnedHashes: Set<String> // Base64(SHA256(SPKI))
|
|
248
|
-
|
|
249
|
-
init(pinnedHashes: Set<String>) {
|
|
250
|
-
self.pinnedHashes = pinnedHashes
|
|
185
|
+
init(acceptedPins: Set<String>) {
|
|
186
|
+
self.acceptedPins = acceptedPins
|
|
251
187
|
}
|
|
252
188
|
|
|
253
|
-
func urlSession(
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
let serverTrust =
|
|
260
|
-
|
|
261
|
-
return
|
|
189
|
+
func urlSession(
|
|
190
|
+
_ session: URLSession,
|
|
191
|
+
didReceive challenge: URLAuthenticationChallenge
|
|
192
|
+
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
|
193
|
+
let space = challenge.protectionSpace
|
|
194
|
+
guard space.authenticationMethod == NSURLAuthenticationMethodServerTrust,
|
|
195
|
+
let serverTrust = space.serverTrust else {
|
|
196
|
+
return (.performDefaultHandling, nil)
|
|
262
197
|
}
|
|
263
|
-
|
|
264
|
-
// Step 1: ALWAYS validate the chain via system trust first
|
|
265
198
|
guard SecTrustEvaluateWithError(serverTrust, nil) else {
|
|
266
|
-
|
|
267
|
-
return
|
|
199
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
268
200
|
}
|
|
269
201
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return
|
|
202
|
+
let presented = (SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate]) ?? []
|
|
203
|
+
let pinsSeen = presented.compactMap(SPKIPin.base64SHA256(of:))
|
|
204
|
+
guard pinsSeen.contains(where: acceptedPins.contains) else {
|
|
205
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
275
206
|
}
|
|
207
|
+
return (.useCredential, URLCredential(trust: serverTrust))
|
|
208
|
+
}
|
|
209
|
+
}
|
|
276
210
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
211
|
+
enum SPKIPin {
|
|
212
|
+
private static let rsa2048Prefix = bytes("30820122300d06092a864886f70d01010105000382010f00")
|
|
213
|
+
private static let p256Prefix = bytes("3059301306072a8648ce3d020106082a8648ce3d030107034200")
|
|
214
|
+
|
|
215
|
+
private static func bytes(_ hex: String) -> Data {
|
|
216
|
+
var out = Data()
|
|
217
|
+
var index = hex.startIndex
|
|
218
|
+
while index < hex.endIndex {
|
|
219
|
+
let next = hex.index(index, offsetBy: 2)
|
|
220
|
+
out.append(UInt8(hex[index..<next], radix: 16) ?? 0)
|
|
221
|
+
index = next
|
|
282
222
|
}
|
|
283
|
-
|
|
223
|
+
return out
|
|
284
224
|
}
|
|
285
225
|
|
|
286
|
-
|
|
287
|
-
guard let
|
|
288
|
-
let
|
|
289
|
-
let
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
let
|
|
294
|
-
switch (
|
|
295
|
-
case (kSecAttrKeyTypeRSA
|
|
296
|
-
case (
|
|
297
|
-
// Add RSA-4096 header for production use
|
|
298
|
-
return nil
|
|
299
|
-
case (kSecAttrKeyTypeECSECPrimeRandom as String, 256):
|
|
300
|
-
header = Self.ecP256Header
|
|
226
|
+
static func base64SHA256(of certificate: SecCertificate) -> String? {
|
|
227
|
+
guard let key = SecCertificateCopyKey(certificate),
|
|
228
|
+
let keyBytes = SecKeyCopyExternalRepresentation(key, nil) as Data?,
|
|
229
|
+
let info = SecKeyCopyAttributes(key) as? [String: Any] else { return nil }
|
|
230
|
+
|
|
231
|
+
let algorithm = info[kSecAttrKeyType as String] as? String
|
|
232
|
+
let size = info[kSecAttrKeySizeInBits as String] as? Int
|
|
233
|
+
let prefix: Data
|
|
234
|
+
switch (algorithm, size) {
|
|
235
|
+
case (String(kSecAttrKeyTypeRSA), 2048): prefix = rsa2048Prefix
|
|
236
|
+
case (String(kSecAttrKeyTypeECSECPrimeRandom), 256): prefix = p256Prefix
|
|
301
237
|
default: return nil
|
|
302
238
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
spki.append(keyData)
|
|
306
|
-
|
|
307
|
-
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
|
308
|
-
spki.withUnsafeBytes {
|
|
309
|
-
_ = CC_SHA256($0.baseAddress, CC_LONG(spki.count), &hash)
|
|
310
|
-
}
|
|
311
|
-
return Data(hash).base64EncodedString()
|
|
239
|
+
let spki = prefix + keyBytes
|
|
240
|
+
return Data(SHA256.hash(data: spki)).base64EncodedString()
|
|
312
241
|
}
|
|
313
242
|
}
|
|
314
243
|
```
|
|
315
244
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
openssl
|
|
245
|
+
The order matters: first the system evaluation must pass, then the chain is walked and
|
|
246
|
+
Base64(SHA-256(header + key)) is compared for each certificate; any match accepts with
|
|
247
|
+
`URLCredential(trust:)`, no match cancels. Challenges other than server trust get
|
|
248
|
+
default handling. The RSA-2048 header is 24 bytes starting `30 82 01 22`, the P-256
|
|
249
|
+
header 26 bytes starting `30 59 30 13`. Other key types (RSA-4096, P-384) need their own
|
|
250
|
+
headers; the sample refuses them rather than guessing. Older code often hashes with
|
|
251
|
+
CommonCrypto's `CC_SHA256`; CryptoKit's `SHA256` does the same job.
|
|
252
|
+
|
|
253
|
+
Generating a pin from a certificate file:
|
|
254
|
+
|
|
255
|
+
```sh
|
|
256
|
+
openssl x509 -in server.pem -noout -pubkey \
|
|
257
|
+
| openssl pkey -pubin -outform der \
|
|
258
|
+
| openssl dgst -sha256 -binary \
|
|
259
|
+
| openssl enc -base64
|
|
329
260
|
```
|
|
330
261
|
|
|
331
|
-
|
|
262
|
+
From a live server, start the pipe with
|
|
263
|
+
`echo | openssl s_client -servername api.example.com -connect api.example.com:443`
|
|
264
|
+
and feed its output through `openssl x509 -noout -pubkey` before the steps above.
|
|
332
265
|
|
|
333
|
-
|
|
266
|
+
### 4. NSPinnedDomains (iOS 14+)
|
|
267
|
+
|
|
268
|
+
Apple's recommended, declarative approach. `URLSession` enforces it through ATS, and it
|
|
269
|
+
is based on SPKI hashes.
|
|
334
270
|
|
|
335
271
|
```xml
|
|
336
|
-
<!-- ✅ CORRECT: CA identity pinning with backup pin via NSPinnedDomains -->
|
|
337
272
|
<key>NSAppTransportSecurity</key>
|
|
338
273
|
<dict>
|
|
339
274
|
<key>NSPinnedDomains</key>
|
|
@@ -346,12 +281,11 @@ Apple's recommended approach. Enforced automatically by `URLSession` via ATS. Us
|
|
|
346
281
|
<array>
|
|
347
282
|
<dict>
|
|
348
283
|
<key>SPKI-SHA256-BASE64</key>
|
|
349
|
-
<string>
|
|
284
|
+
<string>r/mIkG3eEpVdm+u/ko/cwxzOMo1bk4TyHIlByibiA5E=</string>
|
|
350
285
|
</dict>
|
|
351
286
|
<dict>
|
|
352
|
-
<!-- Backup CA from a different provider -->
|
|
353
287
|
<key>SPKI-SHA256-BASE64</key>
|
|
354
|
-
<string>
|
|
288
|
+
<string>YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=</string>
|
|
355
289
|
</dict>
|
|
356
290
|
</array>
|
|
357
291
|
</dict>
|
|
@@ -359,253 +293,251 @@ Apple's recommended approach. Enforced automatically by `URLSession` via ATS. Us
|
|
|
359
293
|
</dict>
|
|
360
294
|
```
|
|
361
295
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
- **`NSPinnedCAIdentities`** - matches any intermediate or root in the chain (logical OR within array)
|
|
365
|
-
- **`NSPinnedLeafIdentities`** - matches the leaf certificate only
|
|
366
|
-
- **`NSIncludesSubdomains`** - covers first-level subdomains when `true`
|
|
367
|
-
|
|
368
|
-
If **both** `NSPinnedCAIdentities` and `NSPinnedLeafIdentities` are specified, ATS requires a match in **each** category (AND between categories, OR within each).
|
|
296
|
+
The two entries above stand for a primary CA and a backup CA from a different
|
|
297
|
+
provider.
|
|
369
298
|
|
|
370
|
-
|
|
299
|
+
- `NSPinnedCAIdentities` is checked against every issuer above the leaf, intermediates
|
|
300
|
+
and root alike; a single hit among the listed hashes satisfies it.
|
|
301
|
+
- `NSPinnedLeafIdentities` matches only the leaf.
|
|
302
|
+
- `NSIncludesSubdomains` extends the rule to first-level subdomains.
|
|
303
|
+
- List both kinds and both must be satisfied: each list needs its own hit (the lists
|
|
304
|
+
combine with AND, entries inside one list with OR).
|
|
305
|
+
- Works for `URLSession` and, after fixes, `WKWebView` from iOS 16. Does not apply to
|
|
306
|
+
`SFSafariViewController`. Pins are visible to anyone reading `Info.plist`, and
|
|
307
|
+
changing them needs an app update.
|
|
371
308
|
|
|
372
|
-
###
|
|
309
|
+
### Which to use
|
|
373
310
|
|
|
374
|
-
|
|
|
375
|
-
|
|
|
376
|
-
| Leaf
|
|
377
|
-
| Intermediate CA
|
|
378
|
-
| SPKI
|
|
379
|
-
| NSPinnedDomains
|
|
311
|
+
| Approach | When |
|
|
312
|
+
| --- | --- |
|
|
313
|
+
| Leaf | Never in production |
|
|
314
|
+
| Intermediate CA | Apps that use one CA |
|
|
315
|
+
| SPKI in code | Pin sets that change at runtime, or custom decision logic |
|
|
316
|
+
| `NSPinnedDomains` | Default for most apps |
|
|
380
317
|
|
|
381
|
-
|
|
318
|
+
SPKI-based options (in code or `NSPinnedDomains`) only need updating when the key
|
|
319
|
+
itself rotates.
|
|
382
320
|
|
|
383
|
-
##
|
|
321
|
+
## Certificates and identities
|
|
384
322
|
|
|
385
|
-
|
|
323
|
+
Loading a bundled DER certificate:
|
|
386
324
|
|
|
387
325
|
```swift
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
326
|
+
struct BundledCertificate {
|
|
327
|
+
let certificate: SecCertificate
|
|
328
|
+
let subject: String?
|
|
329
|
+
let publicKey: SecKey?
|
|
330
|
+
let der: Data
|
|
393
331
|
}
|
|
394
332
|
|
|
395
|
-
|
|
396
|
-
let
|
|
397
|
-
let
|
|
333
|
+
func loadBundledCertificate(named name: String) -> BundledCertificate? {
|
|
334
|
+
guard let folder = Bundle.main.resourceURL,
|
|
335
|
+
let der = try? Data(contentsOf: folder.appendingPathComponent("\(name).cer")),
|
|
336
|
+
let cert = SecCertificateCreateWithData(nil, der as CFData) else { return nil }
|
|
337
|
+
|
|
338
|
+
return BundledCertificate(
|
|
339
|
+
certificate: cert,
|
|
340
|
+
subject: SecCertificateCopySubjectSummary(cert) as String?,
|
|
341
|
+
publicKey: SecCertificateCopyKey(cert),
|
|
342
|
+
der: SecCertificateCopyData(cert) as Data
|
|
343
|
+
)
|
|
344
|
+
}
|
|
398
345
|
```
|
|
399
346
|
|
|
400
|
-
`SecCertificateCreateWithData` accepts
|
|
347
|
+
`SecCertificateCreateWithData` accepts DER only. For PEM, remove the
|
|
348
|
+
`-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` lines and Base64-decode
|
|
349
|
+
what remains.
|
|
401
350
|
|
|
402
|
-
|
|
351
|
+
Importing a client identity from PKCS#12:
|
|
403
352
|
|
|
404
353
|
```swift
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
guard status == errSecSuccess
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
let
|
|
415
|
-
throw
|
|
354
|
+
enum IdentityImportError: Error { case status(OSStatus), empty }
|
|
355
|
+
|
|
356
|
+
func importClientIdentity(p12: Data, passphrase: String) throws -> SecIdentity {
|
|
357
|
+
let options = [kSecImportExportPassphrase as String: passphrase] as CFDictionary
|
|
358
|
+
var items: CFArray?
|
|
359
|
+
let status = SecPKCS12Import(p12 as CFData, options, &items)
|
|
360
|
+
guard status == errSecSuccess else { throw IdentityImportError.status(status) }
|
|
361
|
+
|
|
362
|
+
guard let entries = items as? [[String: Any]],
|
|
363
|
+
let value = entries.first?[kSecImportItemIdentity as String] else {
|
|
364
|
+
throw IdentityImportError.empty
|
|
416
365
|
}
|
|
417
|
-
|
|
366
|
+
let ref = value as CFTypeRef
|
|
367
|
+
guard CFGetTypeID(ref) == SecIdentityGetTypeID() else { throw IdentityImportError.empty }
|
|
368
|
+
return ref as! SecIdentity
|
|
418
369
|
}
|
|
419
370
|
```
|
|
420
371
|
|
|
421
|
-
|
|
372
|
+
Conditional casts to Core Foundation types always succeed at compile time, so the
|
|
373
|
+
type-ID check is what actually verifies the value before the cast.
|
|
422
374
|
|
|
423
|
-
|
|
424
|
-
- **`kSecImportItemCertChain`** (`[SecCertificate]`) - full certificate chain
|
|
425
|
-
- **`kSecImportItemTrust`** (`SecTrust`) - pre-configured trust object
|
|
426
|
-
- **`kSecImportItemKeyID`** (`Data`) - typically SHA-1 hash of public key
|
|
375
|
+
Each import result dictionary holds:
|
|
427
376
|
|
|
428
|
-
|
|
377
|
+
| Key | Value |
|
|
378
|
+
| --- | --- |
|
|
379
|
+
| `kSecImportItemIdentity` | `SecIdentity` |
|
|
380
|
+
| `kSecImportItemCertChain` | `[SecCertificate]` |
|
|
381
|
+
| `kSecImportItemTrust` | `SecTrust` |
|
|
382
|
+
| `kSecImportItemKeyID` | `Data`, usually the SHA-1 of the public key |
|
|
429
383
|
|
|
430
|
-
|
|
384
|
+
Never ship the PKCS#12 passphrase in the app. Ask the user, or keep it in the
|
|
385
|
+
Keychain.
|
|
386
|
+
|
|
387
|
+
Mutual TLS in a session delegate:
|
|
431
388
|
|
|
432
389
|
```swift
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
private let
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
self.
|
|
440
|
-
self.certChain = certChain
|
|
390
|
+
final class MutualTLSDelegate: NSObject, URLSessionDelegate, @unchecked Sendable {
|
|
391
|
+
private let clientIdentity: SecIdentity
|
|
392
|
+
private let issuerChain: [SecCertificate]
|
|
393
|
+
|
|
394
|
+
init(clientIdentity: SecIdentity, issuerChain: [SecCertificate]) {
|
|
395
|
+
self.clientIdentity = clientIdentity
|
|
396
|
+
self.issuerChain = issuerChain
|
|
441
397
|
}
|
|
442
398
|
|
|
443
|
-
func urlSession(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
let
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
SecTrustEvaluateWithError(trust, nil) else {
|
|
459
|
-
completionHandler(.cancelAuthenticationChallenge, nil)
|
|
460
|
-
return
|
|
399
|
+
func urlSession(
|
|
400
|
+
_ session: URLSession,
|
|
401
|
+
didReceive challenge: URLAuthenticationChallenge
|
|
402
|
+
) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
|
|
403
|
+
let method = challenge.protectionSpace.authenticationMethod
|
|
404
|
+
if method == NSURLAuthenticationMethodClientCertificate {
|
|
405
|
+
let proof = URLCredential(identity: clientIdentity,
|
|
406
|
+
certificates: issuerChain,
|
|
407
|
+
persistence: .forSession)
|
|
408
|
+
return (.useCredential, proof)
|
|
409
|
+
}
|
|
410
|
+
if method == NSURLAuthenticationMethodServerTrust {
|
|
411
|
+
if let serverTrust = challenge.protectionSpace.serverTrust,
|
|
412
|
+
SecTrustEvaluateWithError(serverTrust, nil) {
|
|
413
|
+
return (.useCredential, URLCredential(trust: serverTrust))
|
|
461
414
|
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
default:
|
|
465
|
-
completionHandler(.performDefaultHandling, nil)
|
|
415
|
+
return (.cancelAuthenticationChallenge, nil)
|
|
466
416
|
}
|
|
417
|
+
return (.performDefaultHandling, nil)
|
|
467
418
|
}
|
|
468
419
|
}
|
|
469
420
|
```
|
|
470
421
|
|
|
471
|
-
|
|
422
|
+
`URLSessionDelegate` requires `Sendable`, and `SecIdentity` is not marked
|
|
423
|
+
`Sendable`. The class holds only `let` properties, so `@unchecked Sendable` states
|
|
424
|
+
a guarantee the compiler cannot check.
|
|
425
|
+
|
|
426
|
+
Client certificate challenges are delivered at the session level
|
|
427
|
+
(`URLSessionDelegate`), not per task. Apps cannot use certificates installed system-wide
|
|
428
|
+
by MDM; only identities in the app's own sandbox are available.
|
|
472
429
|
|
|
473
|
-
|
|
430
|
+
Walking the chain across OS versions:
|
|
474
431
|
|
|
475
432
|
```swift
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
return SecTrustCopyCertificateChain(trust) as? [SecCertificate] ?? []
|
|
480
|
-
} else {
|
|
481
|
-
return (0..<SecTrustGetCertificateCount(trust)).compactMap {
|
|
482
|
-
SecTrustGetCertificateAtIndex(trust, $0)
|
|
483
|
-
}
|
|
433
|
+
func certificates(in trust: SecTrust) -> [SecCertificate] {
|
|
434
|
+
guard #unavailable(iOS 15, macOS 12) else {
|
|
435
|
+
return (SecTrustCopyCertificateChain(trust) as? [SecCertificate]) ?? []
|
|
484
436
|
}
|
|
437
|
+
let total = SecTrustGetCertificateCount(trust)
|
|
438
|
+
return (0..<total).compactMap { SecTrustGetCertificateAtIndex(trust, $0) }
|
|
485
439
|
}
|
|
486
440
|
```
|
|
487
441
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
|
494
|
-
|
|
|
495
|
-
|
|
|
496
|
-
| `
|
|
497
|
-
|
|
|
498
|
-
|
|
|
499
|
-
|
|
|
500
|
-
|
|
|
501
|
-
|
|
|
502
|
-
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
-
|
|
582
|
-
-
|
|
583
|
-
-
|
|
584
|
-
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
- **WWDC 2017 Session 709** - "Your Apps and Evolving Network Security Standards" (ATS, CT, pinning guidance)
|
|
591
|
-
- **Apple Developer Documentation** - "Evaluating a Trust and Parsing the Result", `SecTrustEvaluateAsyncWithError`, `NSPinnedDomains`
|
|
592
|
-
- **Apple Platform Security Guide** - Revocation infrastructure, Certificate Transparency
|
|
593
|
-
- **Apple News Article** - "Identity Pinning: How to configure server certificates for your app"
|
|
594
|
-
- **OWASP Pinning Cheat Sheet** - Strategy recommendations, backup pin guidance
|
|
595
|
-
- **OWASP MASTG** - Certificate pinning test cases
|
|
596
|
-
|
|
597
|
-
---
|
|
598
|
-
|
|
599
|
-
## Summary Checklist
|
|
600
|
-
|
|
601
|
-
1. **Trust evaluation uses modern API** - `SecTrustEvaluateWithError` (sync) or `SecTrustEvaluateAsyncWithError` (async); no deprecated `SecTrustEvaluate`
|
|
602
|
-
2. **Trust evaluation runs off main thread** - background dispatch queue for async; URLSession delegate callbacks already off-main for sync
|
|
603
|
-
3. **Pinning strategy avoids leaf certificates** - use SPKI hash pinning, intermediate CA pinning, or `NSPinnedDomains`; never pin raw leaf certificate bytes in production
|
|
604
|
-
4. **At least two pins configured** - primary + backup from different CA or pre-generated backup key pair
|
|
605
|
-
5. **System trust evaluated before pin checks** - always call `SecTrustEvaluateWithError` first, then compare SPKI hashes; never skip chain validation
|
|
606
|
-
6. **SPKI hashing includes ASN.1 header** - prepend correct algorithm-specific header before SHA-256 hashing raw key bytes from `SecKeyCopyExternalRepresentation`
|
|
607
|
-
7. **Custom anchors preserve system trust** - `SecTrustSetAnchorCertificates` paired with `SecTrustSetAnchorCertificatesOnly(_, false)` unless intentionally restricting
|
|
608
|
-
8. **SSL policy binds hostname** - `SecPolicyCreateSSL` always receives actual expected hostname, never `nil`
|
|
609
|
-
9. **ATS not globally disabled** - no `NSAllowsArbitraryLoads: true` in production; use targeted exceptions (`NSAllowsLocalNetworking`, per-domain exceptions)
|
|
610
|
-
10. **Chain inspection uses current APIs** - `SecTrustCopyCertificateChain` (iOS 15+) with fallback to `SecTrustGetCertificateAtIndex` for older targets; `SecCertificateCopyKey` not `SecTrustCopyPublicKey`
|
|
611
|
-
11. **Client certificate passwords not bundled** - PKCS#12 passwords prompted at runtime or stored in Keychain, never hardcoded or embedded in app bundle
|
|
442
|
+
Storing certificates and identities in the Keychain is covered in
|
|
443
|
+
[keychain-item-classes.md](keychain-item-classes.md).
|
|
444
|
+
|
|
445
|
+
## Mistakes to reject in review
|
|
446
|
+
|
|
447
|
+
| Mistake | Fix |
|
|
448
|
+
| --- | --- |
|
|
449
|
+
| Result-code `SecTrustEvaluate`, gone since iOS 13 | The error-returning sync call, or its async sibling |
|
|
450
|
+
| `NSAllowsArbitraryLoads: true` globally; opens MITM and draws App Review questions | `NSAllowsLocalNetworking` for development, narrow per-domain exceptions in production |
|
|
451
|
+
| Custom anchors without `SecTrustSetAnchorCertificatesOnly(_, false)` | Always pair them |
|
|
452
|
+
| `SecPolicyCreateSSL` with a nil hostname | Pass the real hostname |
|
|
453
|
+
| Pin check without system evaluation, so expired or revoked certificates pass | Evaluate first, then pin |
|
|
454
|
+
| Index-based chain access, retired in iOS 15 | Copy the whole chain as an array; keep the index path only for older systems |
|
|
455
|
+
| Copying the public key from the trust the pre-iOS 14 way | Take it from the certificate, or use `SecTrustCopyKey` |
|
|
456
|
+
| SPKI hash computed without the ASN.1 header | Prepend the header; otherwise nothing matches |
|
|
457
|
+
| Trust evaluation on the main queue | Background queue; the UI freezes otherwise |
|
|
458
|
+
|
|
459
|
+
## Backup pins and rotation
|
|
460
|
+
|
|
461
|
+
- Always pin at least two keys. With a single pin, a revocation, a CA compromise or an
|
|
462
|
+
unplanned key change takes networking down.
|
|
463
|
+
- Backup strategy: generate a spare key pair ahead of time and pin its SPKI hash
|
|
464
|
+
without deploying any certificate for it. If the primary is compromised, get a
|
|
465
|
+
certificate issued for the spare key.
|
|
466
|
+
- If every pin fails: show a clear verification error, fall back to offline or cached
|
|
467
|
+
content, never offer a bypass, and log the event. Recovery is an App Store update or
|
|
468
|
+
an updated MDM configuration profile.
|
|
469
|
+
- OWASP's guidance is that pinning pays off only for teams that own the server end as
|
|
470
|
+
well as the app, have a safe channel for shipping new pins, and have rehearsed key
|
|
471
|
+
rotation. Certificate Transparency (enforced since
|
|
472
|
+
iOS 12.1.1) together with Apple's revocation checking protects well without the
|
|
473
|
+
operational risk of pinning.
|
|
474
|
+
|
|
475
|
+
## Where ATS meets trust
|
|
476
|
+
|
|
477
|
+
- ATS requires, for every `URLSession` connection: TLS 1.2 or later, RSA keys of at
|
|
478
|
+
least 2048 bits or ECC of at least 256 bits, SHA-256 or stronger certificate
|
|
479
|
+
signatures, AES-128 or AES-256, and forward secrecy.
|
|
480
|
+
- From iOS 17, ATS also requires HTTPS when connecting to a bare IP address.
|
|
481
|
+
- Keys that draw App Review scrutiny: `NSAllowsArbitraryLoads`,
|
|
482
|
+
`NSAllowsArbitraryLoadsForMedia`, `NSAllowsArbitraryLoadsInWebContent`,
|
|
483
|
+
`NSExceptionAllowsInsecureHTTPLoads`, `NSExceptionMinimumTLSVersion`.
|
|
484
|
+
- Diagnose on a Mac with `nscurl --ats-diagnostics https://api.example.com`.
|
|
485
|
+
|
|
486
|
+
## API timeline
|
|
487
|
+
|
|
488
|
+
| Release | Change |
|
|
489
|
+
| --- | --- |
|
|
490
|
+
| iOS 12 / macOS 10.14 | `SecTrustEvaluateWithError`; Certificate Transparency enforced (12.1.1) |
|
|
491
|
+
| iOS 13 / macOS 10.15 | `SecTrustEvaluateAsyncWithError`; `SecTrustEvaluate` deprecated |
|
|
492
|
+
| iOS 14 / macOS 11 | `NSPinnedDomains`; `SecTrustCopyKey` replaces `SecTrustCopyPublicKey` |
|
|
493
|
+
| iOS 15 / macOS 12 | `SecTrustCopyCertificateChain` replaces `SecTrustGetCertificateAtIndex` and `SecTrustGetCertificateCount` |
|
|
494
|
+
| iOS 17 / macOS 14 | Connections to literal IPs fall under ATS; TLS 1.3 for EAP |
|
|
495
|
+
| iOS 18 / macOS 15 | Trust API unchanged; the Swift 6 language mode makes the closure-based Security calls harder to adopt |
|
|
496
|
+
|
|
497
|
+
## Threads and performance
|
|
498
|
+
|
|
499
|
+
- Do not use one `SecTrust` from several threads at once. Separate instances can be
|
|
500
|
+
evaluated in parallel.
|
|
501
|
+
- On iOS the whole family of certificate, key and trust functions may be called from
|
|
502
|
+
any thread, including reentrantly.
|
|
503
|
+
- On macOS, evaluation can wait on user interaction such as a keychain unlock dialog;
|
|
504
|
+
always evaluate in the background.
|
|
505
|
+
- None of the trust, certificate or key reference types conforms to `Sendable`. In
|
|
506
|
+
Swift 6 mode, confine them to an actor or wrap them in an `@unchecked Sendable` box whose use you control.
|
|
507
|
+
|
|
508
|
+
## CI guardrails
|
|
509
|
+
|
|
510
|
+
- Fail the build when a production `Info.plist` sets `NSAllowsArbitraryLoads` to true.
|
|
511
|
+
- Block any SSL policy created without a hostname outside test targets.
|
|
512
|
+
- Require at least two SPKI hashes for every `NSPinnedDomains` entry.
|
|
513
|
+
- Flag `SecTrustEvaluate(`, `SecTrustGetCertificateAtIndex(` and
|
|
514
|
+
`SecTrustCopyPublicKey(`.
|
|
515
|
+
- Exercise pinning through a certificate rotation in staging.
|
|
516
|
+
|
|
517
|
+
## Related and sources
|
|
518
|
+
|
|
519
|
+
[keychain-item-classes.md](keychain-item-classes.md) for storing certificates,
|
|
520
|
+
identities and PKCS#12 material; [keychain-fundamentals.md](keychain-fundamentals.md);
|
|
521
|
+
[cryptokit-public-key.md](cryptokit-public-key.md) for PEM/DER and curve choice for
|
|
522
|
+
client keys. OWASP M5 (insecure communication) and MASVS-NETWORK are mapped in
|
|
523
|
+
[compliance-owasp-mapping.md](compliance-owasp-mapping.md).
|
|
524
|
+
|
|
525
|
+
Sources: WWDC17 session 701 on network security standards; Apple's documentation
|
|
526
|
+
pages on trust evaluation and result parsing, on `SecTrustEvaluateAsyncWithError` and
|
|
527
|
+
on `NSPinnedDomains`; the Apple Platform Security Guide; the Apple developer news post
|
|
528
|
+
on identity pinning; the OWASP pinning cheat sheet; OWASP MASTG.
|
|
529
|
+
|
|
530
|
+
## Checklist
|
|
531
|
+
|
|
532
|
+
- [ ] Only current evaluation APIs.
|
|
533
|
+
- [ ] Evaluation runs off the main thread.
|
|
534
|
+
- [ ] No leaf pinning; SPKI, intermediate CA or `NSPinnedDomains`.
|
|
535
|
+
- [ ] At least two pins (a second CA or a pre-generated backup key).
|
|
536
|
+
- [ ] System trust evaluated before pins are checked.
|
|
537
|
+
- [ ] SPKI hashes include the ASN.1 header.
|
|
538
|
+
- [ ] Custom anchors keep system roots unless restriction is intended.
|
|
539
|
+
- [ ] SSL policies always carry the hostname.
|
|
540
|
+
- [ ] ATS not disabled globally; only targeted exceptions.
|
|
541
|
+
- [ ] `SecTrustCopyCertificateChain` (iOS 15+) with a fallback; `SecCertificateCopyKey`
|
|
542
|
+
instead of `SecTrustCopyPublicKey`.
|
|
543
|
+
- [ ] No PKCS#12 passphrases in the bundle.
|