@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.
Files changed (264) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +266 -267
  5. package/package.json +2 -2
  6. package/pipeline/scripts/_notices.mjs +1 -1
  7. package/pipeline/skills/.skill-manifest.json +68 -68
  8. package/pipeline/skills/shared/README.md +70 -70
  9. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  10. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  11. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  12. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  13. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  14. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  15. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  16. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  17. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  18. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  19. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  20. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  21. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  22. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  23. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  24. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  25. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  26. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  27. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  28. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  29. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  30. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  31. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  32. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  34. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  35. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  36. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  37. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  38. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  39. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  40. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  41. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  42. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  43. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  44. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  45. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  46. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  47. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  48. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  49. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  50. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  51. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  52. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  53. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  54. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  55. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  56. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  57. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  58. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  59. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  60. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  61. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  62. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  63. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  64. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  65. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  66. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  67. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  68. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  69. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  70. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  71. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  72. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  73. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  74. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  75. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  76. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  77. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  78. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  79. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  80. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  81. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  82. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  83. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  84. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  85. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  86. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  87. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  88. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  89. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  90. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  91. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  92. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  93. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  94. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  95. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  96. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  97. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  98. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  99. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  100. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  101. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  102. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  103. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  104. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  105. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  106. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  107. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  108. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  109. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  111. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  112. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  113. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  114. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  115. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  116. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  117. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  118. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  119. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  120. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  121. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  122. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  123. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  124. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  125. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  126. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  127. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  128. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  129. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  130. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  131. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  132. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  133. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  134. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  135. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  136. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  137. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  138. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  139. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  140. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  141. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  142. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  143. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  144. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  145. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  146. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  147. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  148. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  149. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  150. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  151. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  152. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  153. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  154. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  155. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  156. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  157. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  158. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  159. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  160. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  161. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  162. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  163. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  164. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  165. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  166. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  167. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  168. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  169. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  170. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  171. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  172. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  173. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  174. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  175. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  176. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  177. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  178. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  179. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  180. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  181. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  182. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  183. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  184. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  185. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  186. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  187. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  188. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  189. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  190. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  191. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  192. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  193. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  194. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  195. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  196. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  197. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  198. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  199. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  200. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  201. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  202. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  203. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  204. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  205. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  206. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  207. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  208. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  209. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  210. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  211. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  212. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  213. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  214. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  215. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  216. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  217. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  218. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  219. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  220. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  221. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  222. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  223. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  224. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  225. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  226. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  227. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  228. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  229. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  230. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  231. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  232. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  233. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  234. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  235. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  236. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  237. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  238. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  239. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  240. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  241. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  242. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  243. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  244. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  245. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  246. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  247. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  248. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  249. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  250. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  251. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  252. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  253. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  254. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  255. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  256. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  257. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  258. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  259. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  260. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  261. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  262. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  263. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  264. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,339 +1,274 @@
1
- # Certificate Trust Evaluation & Pinning
1
+ # Certificate Trust, Pinning and Client Certificates
2
2
 
3
- > **Scope**: SecCertificate, SecTrust evaluation, SecIdentity, certificate pinning strategies (leaf / intermediate CA / SPKI hash / NSPinnedDomains), custom trust policies, client certificate authentication (mTLS), ATS interaction, and operational pin management. iOS 12+ through iOS 18, macOS 10.14+ through macOS 15.
4
- >
5
- > **Out of scope**: Network-layer encryption beyond TLS certificate handling, server-side certificate management, App Transport Security as a standalone topic (covered briefly where it intersects pinning).
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
- - [Core Security Types](#core-security-types)
12
- - [Trust Evaluation APIs](#trust-evaluation-apis)
13
- - [SecTrustEvaluateAsyncWithError - recommended async API (iOS 13+)](#sectrustevaluateasyncwitherror-recommended-async-api-ios-13)
14
- - [SecTrustEvaluateWithError - synchronous, still current (iOS 12+)](#sectrustevaluatewitherror-synchronous-still-current-ios-12)
15
- - [SecTrustEvaluate - deprecated since iOS 13](#sectrustevaluate-deprecated-since-ios-13)
16
- - [SecTrustResultType reference](#sectrustresulttype-reference)
17
- - [Custom Trust Policy Configuration](#custom-trust-policy-configuration)
18
- - [Four Pinning Strategies](#four-pinning-strategies)
19
- - [Leaf certificate pinning - breaks on every renewal](#leaf-certificate-pinning-breaks-on-every-renewal)
20
- - [Intermediate CA pinning - 5-10 year validity window](#intermediate-ca-pinning-510-year-validity-window)
21
- - [SPKI hash pinning - survives renewal with same key pair](#spki-hash-pinning-survives-renewal-with-same-key-pair)
22
- - [NSPinnedDomains - declarative pinning, zero code (iOS 14+)](#nspinneddomains-declarative-pinning-zero-code-ios-14)
23
- - [Pinning Strategy Decision Matrix](#pinning-strategy-decision-matrix)
24
- - [SecCertificate and SecIdentity](#seccertificate-and-secidentity)
25
- - [Creating certificates from DER data](#creating-certificates-from-der-data)
26
- - [Importing PKCS#12 for client certificate authentication](#importing-pkcs12-for-client-certificate-authentication)
27
- - [Client certificate authentication in URLSession](#client-certificate-authentication-in-urlsession)
28
- - [Certificate chain inspection (backward-compatible)](#certificate-chain-inspection-backward-compatible)
29
- - [Anti-Patterns AI Code Generators Produce](#anti-patterns-ai-code-generators-produce)
30
- - [Backup Pins, Rotation, and Graceful Degradation](#backup-pins-rotation-and-graceful-degradation)
31
- - [ATS Interaction Points](#ats-interaction-points)
32
- - [API Deprecation Timeline](#api-deprecation-timeline)
33
- - [Thread Safety and Performance](#thread-safety-and-performance)
34
- - [CI/CD Guardrails](#cicd-guardrails)
35
- - [Cross-References](#cross-references)
36
- - [WWDC and Reference Citations](#wwdc-and-reference-citations)
37
- - [Summary Checklist](#summary-checklist)
38
-
39
- ## Core Security Types
40
-
41
- | Type | Purpose | Key Operations |
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
- func SecTrustEvaluateAsyncWithError(
58
- _ trust: SecTrust,
59
- _ queue: dispatch_queue_t,
60
- _ result: @escaping (SecTrust, Bool, CFError?) -> Void
61
- ) -> OSStatus
62
- ```
49
+ import Foundation
50
+ import Security
63
51
 
64
- The callback receives a Boolean result and optional error. The callback may fire synchronously if the trust object has a cached result. Always dispatch on a **background queue** - evaluation may perform network access for intermediate certificate fetching or revocation checks.
52
+ struct TrustFailure: Error {
53
+ let status: OSStatus
54
+ let underlying: CFError?
55
+ }
65
56
 
66
- ```swift
67
- // ✅ CORRECT: Async trust evaluation with proper error handling
68
- func evaluateTrust(_ trust: SecTrust, completion: @escaping (Bool, Error?) -> Void) {
69
- let queue = DispatchQueue.global(qos: .userInitiated)
70
- queue.async {
71
- let status = SecTrustEvaluateAsyncWithError(trust, queue) { _, result, error in
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 status != errSecSuccess {
75
- completion(false, NSError(domain: NSOSStatusErrorDomain, code: Int(status)))
64
+ if started != errSecSuccess {
65
+ report(TrustFailure(status: started, underlying: nil))
76
66
  }
77
67
  }
78
68
  }
79
69
  ```
80
70
 
81
- Apple has **not** added native async/await wrappers to the Security framework through iOS 18. Wrap manually:
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
- // ✅ CORRECT: Swift concurrency wrapper
85
- func evaluateTrust(_ trust: SecTrust) async throws -> Bool {
86
- try await withCheckedThrowingContinuation { continuation in
87
- let queue = DispatchQueue.global(qos: .userInitiated)
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
- ### SecTrustEvaluateWithError - synchronous, still current (iOS 12+)
106
-
107
- ```swift
108
- func SecTrustEvaluateWithError(_ trust: SecTrust, _ error: UnsafeMutablePointer<CFError?>?) -> Bool
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
- Modern `SecTrustEvaluateWithError` collapses this to a Boolean. Treat only `.unspecified` and `.proceed` as success.
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
- ## Custom Trust Policy Configuration
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
- ```swift
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
- ```swift
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
- // ❌ INCORRECT: Missing SecTrustSetAnchorCertificatesOnly
160
- SecTrustSetAnchorCertificates(trust, [customRootCA] as CFArray)
161
- // Without SecTrustSetAnchorCertificatesOnly(trust, false), ALL system anchors
162
- // are silently disabled - only your custom CA is trusted!
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
- ```swift
166
- // ❌ INCORRECT: nil hostname disables hostname verification entirely
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 Pinning Strategies
136
+ ## Four ways to pin
174
137
 
175
- ### Leaf certificate pinning - breaks on every renewal
138
+ ### 1. Leaf certificate (do not ship)
176
139
 
177
- Commercial TLS certificates expire every 90 days (Let's Encrypt) to 398 days (CA/Browser Forum maximum). When the server renews, the certificate bytes change (new serial, validity dates, signature) and the pin breaks. Users are locked out until an App Store update ships.
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
- // ❌ DANGEROUS: Leaf pinning that breaks on every certificate renewal
181
- guard let chain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate],
182
- let serverCert = chain.first else {
183
- completionHandler(.cancelAuthenticationChallenge, nil)
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
- **Verdict**: never use in production unless you control the full certificate lifecycle AND can update pins without App Store review.
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 pinning - 5-10 year validity window
155
+ ### 2. Intermediate CA
200
156
 
201
- Pin an intermediate CA certificate. Any leaf issued by that CA passes the check. The server can freely renew its leaf certificate.
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
- // ✅ CORRECT: Intermediate CA pinning (resilient to leaf renewal)
205
- guard let chain = SecTrustCopyCertificateChain(serverTrust) as? [SecCertificate] else {
206
- completionHandler(.cancelAuthenticationChallenge, nil)
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
- **Tradeoff**: trusts any certificate from that CA, not just yours. If the CA is compromised, a same-CA certificate could impersonate your server.
168
+ ### 3. SPKI hash (recommended in code)
223
169
 
224
- ### SPKI hash pinning - survives renewal with same key pair
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
- Hashes the SubjectPublicKeyInfo (SPKI) structure. When certificates renew **with the same key pair**, the SPKI stays identical. This is the **recommended programmatic approach**.
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
- **Critical correctness issue**: `SecKeyCopyExternalRepresentation` returns raw key bytes **without** the ASN.1 SPKI header. You must prepend the correct header before hashing. Omitting this produces incorrect hashes that won't match pins generated via OpenSSL.
179
+ ```swift
180
+ import CryptoKit
229
181
 
230
- The code below uses current APIs with proper SPKI construction. Do not hash raw key bytes directly; they lack the ASN.1 SPKI header expected by common pin-generation workflows.
182
+ final class PinnedSessionDelegate: NSObject, URLSessionDelegate {
183
+ private let acceptedPins: Set<String>
231
184
 
232
- ```swift
233
- // ✅ CORRECT: SPKI hash pinning with ASN.1 header and modern APIs
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(_ session: URLSession,
254
- didReceive challenge: URLAuthenticationChallenge,
255
- completionHandler: @escaping (URLSession.AuthChallengeDisposition,
256
- URLCredential?) -> Void) {
257
- guard challenge.protectionSpace.authenticationMethod
258
- == NSURLAuthenticationMethodServerTrust,
259
- let serverTrust = challenge.protectionSpace.serverTrust else {
260
- completionHandler(.performDefaultHandling, nil)
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
- completionHandler(.cancelAuthenticationChallenge, nil)
267
- return
199
+ return (.cancelAuthenticationChallenge, nil)
268
200
  }
269
201
 
270
- // Step 2: Walk chain and check SPKI hashes
271
- guard let chain = SecTrustCopyCertificateChain(serverTrust)
272
- as? [SecCertificate] else {
273
- completionHandler(.cancelAuthenticationChallenge, nil)
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
- for cert in chain {
278
- if let hash = spkiHash(for: cert), pinnedHashes.contains(hash) {
279
- completionHandler(.useCredential, URLCredential(trust: serverTrust))
280
- return
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
- completionHandler(.cancelAuthenticationChallenge, nil)
223
+ return out
284
224
  }
285
225
 
286
- private func spkiHash(for certificate: SecCertificate) -> String? {
287
- guard let publicKey = SecCertificateCopyKey(certificate),
288
- let keyData = SecKeyCopyExternalRepresentation(publicKey, nil) as Data?,
289
- let attrs = SecKeyCopyAttributes(publicKey) as? [CFString: Any],
290
- let keyType = attrs[kSecAttrKeyType] as? String,
291
- let keySize = attrs[kSecAttrKeySizeInBits] as? Int else { return nil }
292
-
293
- let header: [UInt8]
294
- switch (keyType, keySize) {
295
- case (kSecAttrKeyTypeRSA as String, 2048): header = Self.rsa2048Header
296
- case (kSecAttrKeyTypeRSA as String, 4096):
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
- var spki = Data(header)
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
- Generate expected SPKI hashes from the command line:
317
-
318
- ```bash
319
- # From a PEM certificate file:
320
- openssl x509 -in cert.pem -noout -pubkey | \
321
- openssl pkey -pubin -outform der | \
322
- openssl dgst -sha256 -binary | openssl enc -base64
323
-
324
- # From a live server:
325
- openssl s_client -connect api.example.com:443 </dev/null 2>/dev/null | \
326
- openssl x509 -pubkey -noout | \
327
- openssl pkey -pubin -outform der | \
328
- openssl dgst -sha256 -binary | openssl enc -base64
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
- ### NSPinnedDomains - declarative pinning, zero code (iOS 14+)
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
- Apple's recommended approach. Enforced automatically by `URLSession` via ATS. Uses SPKI hashes.
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>PrimaryCA_SPKI_Hash_Base64==</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>BackupCA_SPKI_Hash_Base64==</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
- Available keys per pinned domain:
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
- **Limitations**: works with `URLSession` and `WKWebView` (iOS 16+ after earlier bugs were fixed). Does not work with `SFSafariViewController`. Pins are visible in `Info.plist` and cannot be updated without an app update.
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
- ### Pinning Strategy Decision Matrix
309
+ ### Which to use
373
310
 
374
- | Strategy | Resilience | Specificity | Update Frequency | Best For |
375
- | ---------------- | --------------------------------- | -------------------------- | -------------------- | -------------------------------- |
376
- | Leaf certificate | ❌ Breaks every 90-398 days | Highest - exact cert match | Every renewal | Never in production |
377
- | Intermediate CA | ✅ 5-10 years | Medium - all certs from CA | Rarely | Single-CA-provider apps |
378
- | SPKI hash (code) | ✅ Survives renewal with same key | High - specific key | Only on key rotation | Dynamic pinsets, custom logic |
379
- | NSPinnedDomains | ✅ Survives renewal with same key | High - SPKI-based | Only on key rotation | **Default choice for most apps** |
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
- ## SecCertificate and SecIdentity
321
+ ## Certificates and identities
384
322
 
385
- ### Creating certificates from DER data
323
+ Loading a bundled DER certificate:
386
324
 
387
325
  ```swift
388
- // ✅ CORRECT: Load .cer from app bundle
389
- guard let certURL = Bundle.main.url(forResource: "server", withExtension: "cer"),
390
- let certData = try? Data(contentsOf: certURL),
391
- let certificate = SecCertificateCreateWithData(nil, certData as CFData) else {
392
- fatalError("Failed to load certificate")
326
+ struct BundledCertificate {
327
+ let certificate: SecCertificate
328
+ let subject: String?
329
+ let publicKey: SecKey?
330
+ let der: Data
393
331
  }
394
332
 
395
- let summary = SecCertificateCopySubjectSummary(certificate) as String?
396
- let publicKey = SecCertificateCopyKey(certificate) // iOS 12+
397
- let derBytes = SecCertificateCopyData(certificate) as Data // Round-trip to DER
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 **DER-encoded** data only - not PEM. For PEM files, strip the `-----BEGIN CERTIFICATE-----` header/footer and Base64-decode.
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
- ### Importing PKCS#12 for client certificate authentication
351
+ Importing a client identity from PKCS#12:
403
352
 
404
353
  ```swift
405
- // ✅ CORRECT: Import .p12 and extract SecIdentity
406
- func importIdentity(from p12Data: Data, password: String) throws -> SecIdentity {
407
- let options: [String: Any] = [kSecImportExportPassphrase as String: password]
408
- var rawItems: CFArray?
409
- let status = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &rawItems)
410
-
411
- guard status == errSecSuccess,
412
- let items = rawItems as? [[String: Any]],
413
- let firstItem = items.first,
414
- let identity = firstItem[kSecImportItemIdentity as String] as? SecIdentity else {
415
- throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
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
- return identity
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
- Result dictionary keys from `SecPKCS12Import`:
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
- - **`kSecImportItemIdentity`** (`SecIdentity`) - private key + certificate pair
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
- **Never bundle passwords with your app.** Prompt the user or read from the Keychain.
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
- ### Client certificate authentication in URLSession
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
- // ✅ CORRECT: Mutual TLS delegate handling both server trust and client cert
434
- class MutualTLSDelegate: NSObject, URLSessionDelegate {
435
- private let identity: SecIdentity
436
- private let certChain: [SecCertificate]?
437
-
438
- init(identity: SecIdentity, certChain: [SecCertificate]? = nil) {
439
- self.identity = identity
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(_ session: URLSession,
444
- didReceive challenge: URLAuthenticationChallenge,
445
- completionHandler: @escaping (URLSession.AuthChallengeDisposition,
446
- URLCredential?) -> Void) {
447
- switch challenge.protectionSpace.authenticationMethod {
448
- case NSURLAuthenticationMethodClientCertificate:
449
- let credential = URLCredential(
450
- identity: identity,
451
- certificates: certChain,
452
- persistence: .forSession
453
- )
454
- completionHandler(.useCredential, credential)
455
-
456
- case NSURLAuthenticationMethodServerTrust:
457
- guard let trust = challenge.protectionSpace.serverTrust,
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
- completionHandler(.useCredential, URLCredential(trust: trust))
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
- Client certificate challenges are **session-wide** (`URLSessionDelegate`), not task-specific. Apps must manage certificates within their sandbox - they cannot access system-wide certificates installed via MDM.
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
- ### Certificate chain inspection (backward-compatible)
430
+ Walking the chain across OS versions:
474
431
 
475
432
  ```swift
476
- // ✅ CORRECT: Backward-compatible chain inspection
477
- func certificateChain(from trust: SecTrust) -> [SecCertificate] {
478
- if #available(iOS 15.0, macOS 12.0, *) {
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
- ## Anti-Patterns AI Code Generators Produce
491
-
492
- | Anti-Pattern | Risk | Correct Replacement |
493
- | ----------------------------------------------------------------------------- | ----------------------------------------------- | --------------------------------------------------------------------- |
494
- | Using deprecated `SecTrustEvaluate` | No error context, deprecated iOS 13 | `SecTrustEvaluateWithError` or `SecTrustEvaluateAsyncWithError` |
495
- | Disabling ATS globally | Enables trivial MITM, triggers App Store review | `NSAllowsLocalNetworking` for dev; targeted exceptions for production |
496
- | `SecTrustSetAnchorCertificates` without `SetAnchorCertificatesOnly(_, false)` | Silently disables all system anchors | Always pair both calls |
497
- | `SecPolicyCreateSSL` with `nil` hostname | Disables hostname verification - MITM vector | Always pass the actual expected hostname |
498
- | Skipping system trust eval before pin checks | Expired/revoked certs pass pin checks | Always `SecTrustEvaluateWithError` first, then check pins |
499
- | Using `SecTrustGetCertificateAtIndex` | Deprecated iOS 15 | `SecTrustCopyCertificateChain` (with backward-compat fallback) |
500
- | Using `SecTrustCopyPublicKey` | Deprecated iOS 14 | `SecCertificateCopyKey` or `SecTrustCopyKey` |
501
- | SPKI hashing without ASN.1 header | Produces wrong hash, pins never match | Prepend correct ASN.1 SPKI header before SHA-256 |
502
- | Evaluating trust on `.main` queue | UI freezes during network-dependent checks | Always use background dispatch queue |
503
-
504
- ```xml
505
- <!-- ❌ DANGEROUS: Never ship this -->
506
- <key>NSAppTransportSecurity</key>
507
- <dict>
508
- <key>NSAllowsArbitraryLoads</key>
509
- <true/>
510
- </dict>
511
-
512
- <!-- ✅ CORRECT: Local networking only for development -->
513
- <key>NSAppTransportSecurity</key>
514
- <dict>
515
- <key>NSAllowsLocalNetworking</key>
516
- <true/>
517
- </dict>
518
- ```
519
-
520
- ---
521
-
522
- ## Backup Pins, Rotation, and Graceful Degradation
523
-
524
- **Always include at least two pins.** A single pin means any certificate revocation, CA compromise, or unplanned key rotation bricks your app's networking.
525
-
526
- **Backup strategy**: pre-generate a backup key pair, compute its SPKI hash, include it as a pin - without deploying the corresponding certificate. If the primary key is compromised, issue a certificate for the backup key server-side. The app already trusts it.
527
-
528
- **When all pins fail**: display a clear error that server credentials could not be verified, switch to offline/cached mode, **never allow the user to bypass the pin**, log for diagnostics. Recovery requires an App Store update (consumer apps) or MDM profile update (managed deployments).
529
-
530
- **OWASP's current nuanced position**: pinning should only be done when you control both client and server, can update the pinset securely, and have a clear rotation strategy. Certificate Transparency (enforced on Apple platforms since iOS 12.1.1) plus Apple's revocation infrastructure provides substantial protection without pinning's operational risk.
531
-
532
- ---
533
-
534
- ## ATS Interaction Points
535
-
536
- ATS enforces TLS 1.2+, 2048-bit RSA or 256-bit ECC keys, SHA-256+ hashing, AES-128/256, and forward secrecy on all `URLSession` connections.
537
-
538
- **iOS 17 change**: ATS now requires HTTPS for connections to bare IP addresses (not just domain names).
539
-
540
- Keys that trigger additional App Store review: `NSAllowsArbitraryLoads`, `NSAllowsArbitraryLoadsForMedia`, `NSAllowsArbitraryLoadsInWebContent`, `NSExceptionAllowsInsecureHTTPLoads`, `NSExceptionMinimumTLSVersion`.
541
-
542
- Use `nscurl --ats-diagnostics https://your-server.com` on macOS to diagnose ATS compatibility.
543
-
544
- ---
545
-
546
- ## API Deprecation Timeline
547
-
548
- | OS Version | Year | Key Changes |
549
- | -------------------- | ---- | -------------------------------------------------------------------------------------- |
550
- | iOS 12 / macOS 10.14 | 2018 | `SecTrustEvaluateWithError` introduced; Certificate Transparency enforced (iOS 12.1.1) |
551
- | iOS 13 / macOS 10.15 | 2019 | `SecTrustEvaluateAsyncWithError` introduced; `SecTrustEvaluate` deprecated |
552
- | iOS 14 / macOS 11 | 2020 | **`NSPinnedDomains`** introduced; `SecTrustCopyKey` replaces `SecTrustCopyPublicKey` |
553
- | iOS 15 / macOS 12 | 2021 | **`SecTrustCopyCertificateChain`** replaces `SecTrustGetCertificateAtIndex`/`Count` |
554
- | iOS 17 / macOS 14 | 2023 | ATS enforced for IP addresses; EAP-TLS 1.3 support |
555
- | iOS 18 / macOS 15 | 2024 | Swift 6 strict concurrency affects callback-based Security code; no new SecTrust APIs |
556
-
557
- ---
558
-
559
- ## Thread Safety and Performance
560
-
561
- - SecTrust objects are thread-safe only **across different instances**. Never access the same `SecTrust` from multiple threads.
562
- - Different `SecTrust` objects can be evaluated concurrently on different threads.
563
- - On iOS, all Certificate/Key/Trust Services functions are thread-safe and reentrant.
564
- - On macOS, trust evaluation can **block on user interaction** (keychain unlock dialogs) - always evaluate on background threads.
565
- - `SecTrust`, `SecCertificate`, and `SecKey` are **not** marked `Sendable`. With Swift 6 strict concurrency, use `@unchecked Sendable` wrappers or explicit actor isolation.
566
-
567
- ---
568
-
569
- ## CI/CD Guardrails
570
-
571
- - **Fail builds** if `NSAllowsArbitraryLoads` is `true` in production `Info.plist`.
572
- - **Validate** that `SecPolicyCreateSSL` is never called with a `nil` hostname in production code paths.
573
- - **Enforce** that any `NSPinnedDomains` entry contains at least two SPKI hashes (backup pin requirement).
574
- - **Scan** for deprecated APIs: `SecTrustEvaluate(`, `SecTrustGetCertificateAtIndex(`, `SecTrustCopyPublicKey(`.
575
- - **Test** pinning with certificate rotation in staging before production deployment.
576
-
577
- ---
578
-
579
- ## Cross-References
580
-
581
- - `keychain-item-classes.md` - `kSecClassCertificate` and `kSecClassIdentity` storage, PKCS#12 import patterns
582
- - `keychain-fundamentals.md` - SecItem CRUD patterns for certificate and identity persistence
583
- - `cryptokit-public-key.md` - PEM/DER key interoperability, curve selection for client certificates
584
- - `compliance-owasp-mapping.md` - M5 (Insecure Communication) trust evaluation requirements
585
-
586
- ---
587
-
588
- ## WWDC and Reference Citations
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.