@mmerterden/multi-agent-pipeline 20.6.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 (326) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +328 -329
  5. package/package.json +2 -2
  6. package/pipeline/multi-agent-refs/features/design-conformance.md +62 -64
  7. package/pipeline/scripts/_notices.mjs +12 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +1 -1
  9. package/pipeline/skills/.skill-manifest.json +106 -106
  10. package/pipeline/skills/shared/README.md +71 -71
  11. package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +1 -0
  12. package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
  13. package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
  14. package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
  15. package/pipeline/skills/shared/external/android-architecture/SKILL.md +2 -0
  16. package/pipeline/skills/shared/external/android-performance/SKILL.md +2 -0
  17. package/pipeline/skills/shared/external/android-security/SKILL.md +2 -0
  18. package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
  19. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  20. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  21. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  22. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  23. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  24. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  25. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  26. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
  27. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  28. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
  29. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
  30. package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
  31. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  32. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  33. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  34. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
  35. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  37. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  38. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  39. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  40. package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
  41. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  42. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
  43. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  44. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  45. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  46. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  47. package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
  48. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  49. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
  50. package/pipeline/skills/shared/external/backlog/BACKLOG.md +1 -1
  51. package/pipeline/skills/shared/external/backlog/SKILL.md +56 -33
  52. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  53. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  54. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  55. package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +1 -0
  56. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  57. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  58. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  59. package/pipeline/skills/shared/external/compose-components/SKILL.md +2 -0
  60. package/pipeline/skills/shared/external/compose-navigation/SKILL.md +3 -2
  61. package/pipeline/skills/shared/external/compose-testing/SKILL.md +2 -0
  62. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  63. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  64. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  65. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
  66. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  67. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  68. package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
  69. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  70. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  71. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  72. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  73. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  74. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  75. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  76. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  77. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  78. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  79. package/pipeline/skills/shared/external/council/SKILL.md +1 -0
  80. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  81. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  82. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  83. package/pipeline/skills/shared/external/css-modern/SKILL.md +1 -0
  84. package/pipeline/skills/shared/external/database-patterns/SKILL.md +1 -0
  85. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  86. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  87. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  88. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  89. package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
  90. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  91. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  92. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  93. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  94. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  95. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  96. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  97. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  98. package/pipeline/skills/shared/external/evidence-github/SKILL.md +2 -0
  99. package/pipeline/skills/shared/external/evidence-registry/SKILL.md +2 -0
  100. package/pipeline/skills/shared/external/gradle-kotlin-dsl/SKILL.md +2 -0
  101. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  102. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  103. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  104. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  105. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  106. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  107. package/pipeline/skills/shared/external/html-semantic/SKILL.md +1 -0
  108. package/pipeline/skills/shared/external/humanizer/SKILL.md +1 -0
  109. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  110. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  111. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  112. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  113. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  114. package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +1 -0
  115. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  116. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  117. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  118. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  119. package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +1 -0
  120. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  121. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  122. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  123. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  124. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  125. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  126. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  127. package/pipeline/skills/shared/external/ios-security/SKILL.md +2 -0
  128. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  129. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  130. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  131. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  132. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  133. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  134. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +91 -283
  135. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +119 -151
  136. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +60 -90
  137. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +119 -156
  138. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +726 -787
  139. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +253 -288
  140. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +243 -304
  141. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +88 -104
  142. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +181 -235
  143. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-key-shots.py +198 -263
  144. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +461 -466
  145. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +145 -151
  146. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +123 -141
  147. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +146 -157
  148. package/pipeline/skills/shared/external/localization-reuse-map/scripts/snapshot-resources.sh +22 -19
  149. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +156 -140
  150. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
  151. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  152. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  153. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  154. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  155. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  156. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  157. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  158. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  159. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  160. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  161. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  162. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  163. package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +1 -0
  164. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  165. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  166. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  167. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  168. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  169. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  170. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  171. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  172. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  173. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  174. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  175. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  176. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  177. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  178. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  179. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  180. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  181. package/pipeline/skills/shared/external/play-store-review/SKILL.md +2 -0
  182. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  183. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  184. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  185. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  186. package/pipeline/skills/shared/external/python-patterns/SKILL.md +1 -0
  187. package/pipeline/skills/shared/external/react-best-practices/SKILL.md +1 -0
  188. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  189. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  190. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  191. package/pipeline/skills/shared/external/rest-api-design/SKILL.md +1 -0
  192. package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +2 -0
  193. package/pipeline/skills/shared/external/room-database/SKILL.md +2 -0
  194. package/pipeline/skills/shared/external/search-first/SKILL.md +1 -0
  195. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  196. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  197. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  198. package/pipeline/skills/shared/external/signal-community/SKILL.md +2 -0
  199. package/pipeline/skills/shared/external/skill-creator/SKILL.md +80 -41
  200. package/pipeline/skills/shared/external/skill-creator/audit.md +63 -59
  201. package/pipeline/skills/shared/external/skill-creator/checklist.md +28 -20
  202. package/pipeline/skills/shared/external/skill-creator/examples.md +40 -40
  203. package/pipeline/skills/shared/external/skill-creator/label-check.md +48 -36
  204. package/pipeline/skills/shared/external/skill-creator/scripts/audit-panel.js +91 -100
  205. package/pipeline/skills/shared/external/skill-creator/template.md +51 -39
  206. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  207. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  208. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  209. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  210. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  211. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  212. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
  213. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  214. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  215. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  216. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  217. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  218. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  219. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  220. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  221. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  222. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  223. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  224. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  225. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  226. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  227. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
  228. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  229. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  230. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  231. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  232. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  233. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  234. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  235. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  236. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  237. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  238. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  239. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  240. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  241. package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
  242. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  243. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  244. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
  245. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  246. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  247. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  248. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  249. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  250. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  251. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
  252. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  253. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  254. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  255. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
  256. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  257. package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
  258. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  259. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
  260. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  261. package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
  262. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  263. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  264. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  265. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  266. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  267. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  268. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  269. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  270. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  271. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  272. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  273. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  274. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  275. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  276. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  277. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  278. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  279. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  280. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  281. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  282. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  283. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  284. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  285. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  286. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
  287. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  288. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
  289. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  290. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
  291. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  292. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  293. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  294. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  295. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  296. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  297. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  298. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  299. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  300. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  301. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  302. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  303. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  304. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  305. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  306. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  307. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  308. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  309. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  310. package/pipeline/skills/shared/external/tailwind-css/SKILL.md +1 -0
  311. package/pipeline/skills/shared/external/testing-backend/SKILL.md +1 -0
  312. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  313. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  314. package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +1 -0
  315. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  316. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  317. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  318. package/pipeline/skills/shared/external/vue-composition/SKILL.md +1 -0
  319. package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
  320. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  321. package/pipeline/skills/shared/external/web-accessibility/SKILL.md +1 -0
  322. package/pipeline/skills/shared/external/web-performance/SKILL.md +1 -0
  323. package/pipeline/skills/shared/external/web-testing/SKILL.md +1 -0
  324. package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
  325. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
  326. 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.