@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.1

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 (284) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +0 -10
  3. package/docs/facts.json +1 -1
  4. package/manifest.json +285 -285
  5. package/package.json +3 -3
  6. package/pipeline/lib/redact.mjs +3 -2
  7. package/pipeline/scripts/_notices.mjs +1 -1
  8. package/pipeline/scripts/gen-skills-index.mjs +13 -1
  9. package/pipeline/scripts/pre-commit-check.sh +4 -0
  10. package/pipeline/skills/.skill-manifest.json +69 -69
  11. package/pipeline/skills/shared/README.md +70 -70
  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/app-clips/SKILL.md +260 -160
  16. package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
  17. package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
  18. package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
  19. package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
  20. package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
  21. package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
  22. package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
  23. package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
  24. package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
  25. package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
  26. package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
  27. package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
  28. package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
  29. package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
  30. package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
  31. package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
  32. package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
  33. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
  34. package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
  35. package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
  36. package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
  37. package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
  38. package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
  39. package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
  40. package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
  41. package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
  42. package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
  43. package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
  44. package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
  45. package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
  46. package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
  47. package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
  48. package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
  49. package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
  50. package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
  51. package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
  52. package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
  53. package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
  54. package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
  55. package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
  56. package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
  57. package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
  58. package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
  59. package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
  60. package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
  61. package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
  62. package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
  63. package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
  64. package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
  65. package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
  66. package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
  67. package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
  68. package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
  69. package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
  70. package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
  71. package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
  72. package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
  73. package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
  74. package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
  75. package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
  76. package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
  77. package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
  78. package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
  79. package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
  80. package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
  81. package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
  82. package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
  83. package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
  84. package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
  85. package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
  86. package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
  87. package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
  88. package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
  89. package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
  90. package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
  91. package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
  92. package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
  93. package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
  94. package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
  95. package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
  96. package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
  97. package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
  98. package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
  99. package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
  100. package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
  101. package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
  102. package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
  103. package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
  104. package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
  105. package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
  106. package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
  107. package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
  108. package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
  109. package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
  110. package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
  111. package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
  112. package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
  113. package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
  114. package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
  115. package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
  116. package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
  117. package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
  118. package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
  119. package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
  120. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
  121. package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
  122. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
  123. package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
  124. package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
  125. package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
  126. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
  127. package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
  128. package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
  129. package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
  130. package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
  131. package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
  132. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
  133. package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
  134. package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
  135. package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
  136. package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
  137. package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
  138. package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
  139. package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
  140. package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
  141. package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
  142. package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
  143. package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
  144. package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
  145. package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
  146. package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
  147. package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
  148. package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
  149. package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
  150. package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
  151. package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
  152. package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
  153. package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
  154. package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
  155. package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
  156. package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
  157. package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
  158. package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
  159. package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
  160. package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
  161. package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
  162. package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
  163. package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
  164. package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
  165. package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
  166. package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
  167. package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
  168. package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
  169. package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
  170. package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
  171. package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
  172. package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
  173. package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
  174. package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
  175. package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
  176. package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
  177. package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
  178. package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
  179. package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
  180. package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
  181. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
  182. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
  183. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
  184. package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
  185. package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
  186. package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
  187. package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
  188. package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
  189. package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
  190. package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
  191. package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
  192. package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
  193. package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
  194. package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
  195. package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
  196. package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
  197. package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
  198. package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
  199. package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
  200. package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
  201. package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
  202. package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
  203. package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
  204. package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
  205. package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
  206. package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
  207. package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
  208. package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
  209. package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
  210. package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
  211. package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
  212. package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
  213. package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
  214. package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
  215. package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
  216. package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
  217. package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
  218. package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
  219. package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
  220. package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
  221. package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
  222. package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
  223. package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
  224. package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
  225. package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
  226. package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
  227. package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
  228. package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
  229. package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
  230. package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
  231. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
  232. package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
  233. package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
  234. package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
  235. package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
  236. package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
  237. package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
  238. package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
  239. package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
  240. package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
  241. package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
  242. package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
  243. package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
  244. package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
  245. package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
  246. package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
  247. package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
  248. package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
  249. package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
  250. package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
  251. package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
  252. package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
  253. package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
  254. package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
  255. package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
  256. package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
  257. package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
  258. package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
  259. package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
  260. package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
  261. package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
  262. package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
  263. package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
  264. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
  265. package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
  266. package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
  267. package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
  268. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
  269. package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
  270. package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
  271. package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
  272. package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
  273. package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
  274. package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
  275. package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
  276. package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
  277. package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
  278. package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
  279. package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
  280. package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
  281. package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
  282. package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
  283. package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
  284. package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
@@ -1,566 +1,506 @@
1
- # Secure Enclave: Hardware-Backed Key Operations for iOS & macOS
1
+ # Secure Enclave
2
2
 
3
- > Scope: Secure Enclave capabilities, constraints, and integration patterns for key generation, persistence, biometric gating, and testability on Apple platforms.
3
+ The Secure Enclave is a security coprocessor kept apart from the main
4
+ processor. It creates keys, stores them and uses them without the private
5
+ material ever reaching the Application Processor. It has shipped since the
6
+ iPhone 5s in 2013.
4
7
 
5
- **The Secure Enclave (SE) is Apple's dedicated security coprocessor - a physically isolated chip that generates, stores, and operates on cryptographic keys in silicon that never exposes private key material to the application processor.** Every modern Apple device since iPhone 5s (2013) contains this hardware, but developers routinely misuse it because of subtle API behaviors, simulator traps, and fundamental architectural constraints that AI code generators consistently get wrong. This reference covers CryptoKit's `SecureEnclave` module (iOS 13+), the legacy Security framework path, iOS 26 post-quantum additions, correct and incorrect code patterns, persistence, testing strategies, and the hardware limitations you must design around.
8
+ This file covers the CryptoKit `SecureEnclave` namespace (iOS 13+), the older
9
+ Security framework route, the post-quantum additions in iOS 26, how to persist
10
+ keys, how to test, and what the hardware cannot do.
6
11
 
7
- Primary sources: Apple Platform Security Guide (Secure Enclave chapter), Apple Developer Documentation for CryptoKit `SecureEnclave` types, WWDC 2019 Session 709 "Cryptography and Your Apps," WWDC 2025 "Get ahead with quantum-secure cryptography," Apple DTS documentation "Protecting keys with the Secure Enclave," and "Storing CryptoKit Keys in the Keychain."
8
-
9
- ---
12
+ Sources: the enclave chapter of the Apple Platform Security Guide; CryptoKit's
13
+ `SecureEnclave` reference pages; WWDC 2019 session 709 (on cryptography in
14
+ apps); the WWDC 2025 session on quantum-secure cryptography; and two Apple
15
+ developer articles, one on protecting keys with the enclave and one on keeping
16
+ CryptoKit keys in the keychain.
10
17
 
11
18
  ## Contents
12
19
 
13
- - [What the Secure Enclave actually is](#what-the-secure-enclave-actually-is)
14
- - [Hardware limitations you must design around](#hardware-limitations-you-must-design-around)
15
- - [CryptoKit SecureEnclave API (iOS 13+)](#cryptokit-secureenclave-api-ios-13)
16
- - [Creating signing keys](#creating-signing-keys)
17
- - [Signing and verification](#signing-and-verification)
18
- - [Key agreement (ECDH) with HKDF derivation](#key-agreement-ecdh-with-hkdf-derivation)
19
- - [Persisting SE keys via dataRepresentation](#persisting-se-keys-via-datarepresentation)
20
- - [Biometric-gated SE keys with SecAccessControl](#biometric-gated-se-keys-with-secaccesscontrol)
21
- - [Legacy Security framework approach (iOS 10+)](#legacy-security-framework-approach-ios-10)
22
- - [iOS 26: Post-quantum cryptography in the Secure Enclave](#ios-26-post-quantum-cryptography-in-the-secure-enclave)
23
- - [When to use SE versus software keys](#when-to-use-se-versus-software-keys)
24
- - [Six correctness traps AI generators get wrong](#six-correctness-traps-ai-generators-get-wrong)
25
- - [1. Not checking isAvailable (and the simulator double-trap)](#1-not-checking-isavailable-and-the-simulator-double-trap)
26
- - [2. Attempting to import external keys](#2-attempting-to-import-external-keys)
27
- - [3. Attempting AES/symmetric encryption directly](#3-attempting-aessymmetric-encryption-directly)
28
- - [4. Assuming SE keys can be backed up or transferred](#4-assuming-se-keys-can-be-backed-up-or-transferred)
29
- - [5. Using legacy Security framework when CryptoKit is available](#5-using-legacy-security-framework-when-cryptokit-is-available)
30
- - [6. Omitting .privateKeyUsage in access control](#6-omitting-privatekeyusage-in-access-control)
31
- - [Testing and CI/CD strategies](#testing-and-cicd-strategies)
32
- - [Protocol-based abstraction for testable SE code](#protocol-based-abstraction-for-testable-se-code)
33
- - [Factory with SE → software fallback](#factory-with-se-software-fallback)
34
- - [XCTest patterns](#xctest-patterns)
35
- - [CI/CD reality](#cicd-reality)
36
- - [Operational guidance: rotation, migration, and incident response](#operational-guidance-rotation-migration-and-incident-response)
37
- - [Conclusion](#conclusion)
38
- - [Summary Checklist](#summary-checklist)
39
-
40
- ## What the Secure Enclave actually is
41
-
42
- The SE is a dedicated security subsystem embedded in Apple's SoC. It has its own secure boot chain, hardware-backed key handling, and isolated execution boundary. Apps interact with it only through system frameworks such as CryptoKit and Security.
43
-
44
- The SE's core guarantee: **private key material generated inside the Secure Enclave never leaves its hardware boundary.** When your code "uses" an SE key, it sends a request through the mailbox, the SE performs the cryptographic operation internally, and only the result (a signature, a shared secret) comes back. There is no API, debug interface, or JTAG path to extract the raw key.
45
-
46
- Each SoC has a Unique ID (UID) permanently fused into silicon at manufacturing. This UID is inaccessible to any software (including Apple's) and serves as the root cryptographic key from which all SE keys derive. This is what makes keys irrevocably device-bound.
47
-
48
- **Devices with Secure Enclave:** All iPhones from iPhone 5s onward (A7+ chips), all iPads from iPad Air onward, Apple Watch Series 1+, Apple TV HD (4th gen) onward, HomePod, all Macs with T1/T2/M-series chips, and Apple Vision Pro. Intel Macs without T1 or T2 chips (pre-2016 MacBook Pro, pre-2018 MacBook Air, pre-2020 iMac except iMac Pro) do **not** have a Secure Enclave.
49
-
50
- ---
51
-
52
- ## Hardware limitations you must design around
53
-
54
- These constraints are architectural, not bugs - they are fundamental to the SE's security model:
55
-
56
- - **P-256 only for classical EC** - No P-384, P-521, Curve25519, or secp256k1. CryptoKit has no `SecureEnclave.P384` or `SecureEnclave.Curve25519` types. iOS 26 adds lattice-based post-quantum algorithms (ML-KEM, ML-DSA), not additional curves.
57
- - **No symmetric key operations** - The internal AES engine handles Data Protection and FileVault but is **not exposed as a developer API**. There is no `SecureEnclave.AES`.
58
- - **No key export** - `SecKeyCopyExternalRepresentation()` on an SE private key fails. The `dataRepresentation` property returns an encrypted opaque blob, not raw key material.
59
- - **No key import** - Keys must be generated inside the SE. `init(dataRepresentation:)` accepts only the opaque blob from a previously created SE key - not arbitrary key material. There is no `init(rawRepresentation:)` on SE key types.
60
- - **Device-bound** - Keys are tied to the device's UID fused at manufacturing. They do not survive factory resets, cannot be backed up to iCloud, cannot sync via iCloud Keychain, and cannot be transferred to a replacement device.
61
- - **Limited storage** - Treat SE key storage as scarce and reserve it for high-value keys. For bulk data, use an SE-protected root key to derive or unwrap software symmetric keys.
62
- - **Performance overhead** - Each operation requires an interrupt-driven round-trip to the isolated coprocessor. The SE is not suitable for high-frequency operations (thousands of signatures per second). Batch signing or bulk encryption should use SE-derived symmetric keys instead.
63
-
64
- ---
65
-
66
- ## CryptoKit SecureEnclave API (iOS 13+)
67
-
68
- CryptoKit's `SecureEnclave` module is the primary API for new code. It wraps the lower-level Security framework with Swift-native types and a curated surface that makes misuse harder.
69
-
70
- Two operation families are supported: **signing** (`SecureEnclave.P256.Signing`) and **key agreement** (`SecureEnclave.P256.KeyAgreement`).
71
-
72
- ### Creating signing keys
20
+ - [What It Is](#what-it-is)
21
+ - [What It Cannot Do](#what-it-cannot-do)
22
+ - [CryptoKit SecureEnclave (iOS 13+)](#cryptokit-secureenclave-ios-13)
23
+ - [Keeping a Key Across Launches](#keeping-a-key-across-launches)
24
+ - [Biometric-Gated Enclave Keys](#biometric-gated-enclave-keys)
25
+ - [Security Framework Route (iOS 10+)](#security-framework-route-ios-10)
26
+ - [iOS 26: Post-Quantum Types in the Enclave](#ios-26-post-quantum-types-in-the-enclave)
27
+ - [Enclave or Software Key?](#enclave-or-software-key)
28
+ - [Six Traps](#six-traps)
29
+ - [Testing and CI](#testing-and-ci)
30
+ - [Operating Enclave Keys](#operating-enclave-keys)
31
+ - [Summary](#summary)
32
+
33
+ ## What It Is
34
+
35
+ - It boots through its own secure boot chain and runs isolated. Apps reach it
36
+ only through CryptoKit or the Security framework.
37
+ - Using a key means posting a request to the enclave's mailbox. Only the
38
+ output, such as a signature or an agreed secret, travels back; the key stays. No API, debugger or
39
+ JTAG path exports it.
40
+ - Each chip has a UID fused in at manufacture that no software can read, not
41
+ even Apple's. Every enclave key descends from it, which is why enclave keys
42
+ are tied to one device.
43
+
44
+ Hardware with an enclave: iPhone 5s and later (A7+), iPad Air and later, Apple
45
+ Watch Series 1 and later, Apple TV HD (4th generation) and later, HomePod,
46
+ Macs with a T1, T2 or Apple silicon chip, and Apple Vision Pro.
47
+
48
+ Hardware without one: Intel Macs with neither T1 nor T2, such as MacBook Pro
49
+ models before 2016, MacBook Air before 2018, and iMac before 2020 (the iMac Pro
50
+ has a T2).
51
+
52
+ ## What It Cannot Do
53
+
54
+ - **One classical curve.** Only P-256. Curve25519, secp256k1, P-521 and P-384
55
+ are all absent, and CryptoKit has neither a `SecureEnclave.Curve25519` nor a
56
+ `SecureEnclave.P384` type.
57
+ iOS 26 adds lattice-based algorithms, not more curves.
58
+ - **No symmetric crypto for apps.** The enclave's AES engine serves Data
59
+ Protection and FileVault and is not exposed. There is no
60
+ `SecureEnclave.AES`.
61
+ - **No export.** `SecKeyCopyExternalRepresentation()` fails on an enclave
62
+ private key. `dataRepresentation` is an encrypted, opaque blob, not key
63
+ material.
64
+ - **No import.** `init(dataRepresentation:)` accepts only a blob produced by an
65
+ earlier enclave key. Enclave types have no `init(rawRepresentation:)`.
66
+ - **Bound to the device.** A factory reset destroys the keys. They are not in
67
+ iCloud backups, do not sync through iCloud Keychain, and cannot move to a new
68
+ device.
69
+ - **Limited room.** Reserve it for keys that matter. For bulk data, keep one
70
+ enclave root key from which software symmetric keys are derived or unwrapped.
71
+ - **Slow per operation.** Every call is an interrupt-driven round trip, so
72
+ thousands of signatures per second are out of reach. High-volume work uses
73
+ symmetric keys derived from an enclave key.
74
+
75
+ ## CryptoKit SecureEnclave (iOS 13+)
76
+
77
+ For new code this is the API to use. It has two families,
78
+ `SecureEnclave.P256.Signing` and `SecureEnclave.P256.KeyAgreement`.
79
+
80
+ ### Creating a key, and the Simulator
81
+
82
+ The Simulator has no Secure Enclave. Decide Simulator behaviour at compile time
83
+ with `#if targetEnvironment(simulator)`, and treat `SecureEnclave.isAvailable`
84
+ as the runtime check on real hardware. Do not base Simulator behaviour on the
85
+ value `isAvailable` happens to report there; key creation in the Simulator is
86
+ not a supported path, and the compile-time branch keeps tests and previews
87
+ predictable.
73
88
 
74
89
  ```swift
75
- // ✅ CORRECT: Robust availability check + key creation
76
90
  import CryptoKit
77
91
 
78
- func createSigningKey() throws -> SecureEnclave.P256.Signing.PrivateKey {
92
+ enum EnclaveKeyError: Error {
93
+ case simulator
94
+ case noEnclave
95
+ }
96
+
97
+ func newDeviceSigningKey() throws -> SecureEnclave.P256.Signing.PrivateKey {
79
98
  #if targetEnvironment(simulator)
80
- throw SecureEnclaveError.notAvailable
99
+ throw EnclaveKeyError.simulator
81
100
  #else
82
- guard SecureEnclave.isAvailable else {
83
- throw SecureEnclaveError.notAvailable
84
- }
101
+ guard SecureEnclave.isAvailable else { throw EnclaveKeyError.noEnclave }
85
102
  return try SecureEnclave.P256.Signing.PrivateKey()
86
103
  #endif
87
104
  }
88
-
89
- enum SecureEnclaveError: Error {
90
- case notAvailable
91
- case keyCreationFailed(underlying: Error)
92
- }
93
105
  ```
94
106
 
95
- The `#if targetEnvironment(simulator)` compile-time guard is essential. **`SecureEnclave.isAvailable` can return `true` on the Simulator** when the host Mac has SE hardware (T2/M-series), but actual key generation fails at runtime. This behavior varies across Xcode versions - some return `false` consistently, others reflect the host's hardware. The compile-time check eliminates the ambiguity entirely.
96
-
97
107
  ```swift
98
- // ❌ INCORRECT: No availability check - crashes on simulator and old devices
108
+ // Wrong: no guard. On the Simulator or on a Mac without an enclave this fails
109
+ // at runtime instead of taking a planned fallback.
99
110
  let key = try SecureEnclave.P256.Signing.PrivateKey()
100
- // Simulator: error -25293 or EXC_BAD_ACCESS depending on Xcode version
101
111
  ```
102
112
 
103
- ### Signing and verification
104
-
105
- Once you have an SE key, signing is straightforward. The SE performs ECDSA internally and returns a standard `P256.Signing.ECDSASignature`. Verification uses the public key - a regular `P256.Signing.PublicKey` that can be freely exported and used anywhere:
113
+ ### Signing
106
114
 
107
115
  ```swift
108
- // ✅ Sign with SE key, verify with public key
109
- let privateKey = try SecureEnclave.P256.Signing.PrivateKey()
110
- let message = "Transfer $500 to Alice".data(using: .utf8)!
111
-
112
- // Signing happens inside the Secure Enclave hardware
113
- let signature = try privateKey.signature(for: message)
114
-
115
- // Public key is standard P256 - works anywhere, export as DER for servers
116
- let publicKey = privateKey.publicKey
117
- let isValid = publicKey.isValidSignature(signature, for: message) // true
116
+ func signReceipt(_ receipt: Data, with key: SecureEnclave.P256.Signing.PrivateKey) throws -> (wire: Data, compact: Data) {
117
+ let signature = try key.signature(for: receipt)
118
+ precondition(key.publicKey.isValidSignature(signature, for: receipt))
119
+ return (signature.derRepresentation, signature.rawRepresentation)
120
+ }
118
121
 
119
- let derSignature = signature.derRepresentation // For wire format
120
- let rawSignature = signature.rawRepresentation // For compact storage
121
- let publicDER = publicKey.derRepresentation // Register with backend
122
+ func registrationPayload(for key: SecureEnclave.P256.Signing.PrivateKey) -> Data {
123
+ key.publicKey.derRepresentation
124
+ }
122
125
  ```
123
126
 
124
- ### Key agreement (ECDH) with HKDF derivation
127
+ The enclave computes the ECDSA signature and hands back a
128
+ `P256.Signing.ECDSASignature`. Send `derRepresentation` over the wire, or
129
+ `rawRepresentation` when a compact 64-byte form is expected. The public key is
130
+ an ordinary `P256.Signing.PublicKey`, free to export; register its
131
+ DER form with the server.
125
132
 
126
- `SecureEnclave.P256.KeyAgreement.PrivateKey` performs Elliptic Curve Diffie-Hellman inside the SE. The resulting `SharedSecret` is then derived into a usable symmetric key via HKDF - this is the **only correct path to symmetric encryption** when starting from an SE key:
133
+ ### Key agreement, then symmetric encryption
127
134
 
128
135
  ```swift
129
- // ✅ CORRECT: ECDH key agreement → HKDF → AES-GCM
130
- let localKey = try SecureEnclave.P256.KeyAgreement.PrivateKey()
131
- let localPublicKey = localKey.publicKey // Send to peer
132
-
133
- // Received from peer (decoded from DER or raw bytes)
134
- let peerPublicKey: P256.KeyAgreement.PublicKey = // ...
135
-
136
- // ECDH happens inside the Secure Enclave
137
- let sharedSecret = try localKey.sharedSecretFromKeyAgreement(with: peerPublicKey)
138
-
139
- // Derive a 256-bit AES key using HKDF-SHA256
140
- let symmetricKey = sharedSecret.hkdfDerivedSymmetricKey(
141
- using: SHA256.self,
142
- salt: "com.myapp.v1.salt".data(using: .utf8)!,
143
- sharedInfo: "encryption-key".data(using: .utf8)!,
144
- outputByteCount: 32
145
- )
146
-
147
- // Now use the derived software key for AES-GCM encryption
148
- let sealedBox = try AES.GCM.seal(plaintext, using: symmetricKey)
149
- ```
150
-
151
- ```swift
152
- // ❌ WRONG: There is no SE symmetric API
153
- // SecureEnclave.AES.GCM.seal(data, using: seKey) - DOES NOT EXIST
154
- // AES.GCM.seal(data, using: seSigningKey) - TYPE MISMATCH (needs SymmetricKey)
136
+ func sealForPeer(_ message: Data,
137
+ mine: SecureEnclave.P256.KeyAgreement.PrivateKey,
138
+ peer: P256.KeyAgreement.PublicKey,
139
+ context: Data) throws -> AES.GCM.SealedBox {
140
+ let secret = try mine.sharedSecretFromKeyAgreement(with: peer)
141
+ let key = secret.hkdfDerivedSymmetricKey(using: SHA256.self,
142
+ salt: Data("example.chat.v1".utf8),
143
+ sharedInfo: context,
144
+ outputByteCount: 32)
145
+ return try AES.GCM.seal(message, using: key)
146
+ }
155
147
  ```
156
148
 
157
- > The ECDH + HKDF pattern is covered in full - including curve selection, `info` parameter guidance, and output key length - in `cryptokit-public-key.md` section Key Agreement with HKDF Derivation.
149
+ A key created with `SecureEnclave.P256.KeyAgreement.PrivateKey()` agrees on a
150
+ secret with a `P256.KeyAgreement.PublicKey`, HKDF turns that secret into a
151
+ symmetric key, and `AES.GCM.seal` does the encryption. This chain is the only
152
+ correct way to get symmetric encryption out of an enclave key.
153
+ `SecureEnclave.AES.GCM` does not exist, and handing a signing key to
154
+ `AES.GCM.seal` does not type-check. More on HKDF and AES-GCM in
155
+ [cryptokit-symmetric.md](cryptokit-symmetric.md) and
156
+ [cryptokit-public-key.md](cryptokit-public-key.md).
158
157
 
159
- ---
158
+ ## Keeping a Key Across Launches
160
159
 
161
- ## Persisting SE keys via dataRepresentation
160
+ An enclave key object disappears when the app quits unless you save its
161
+ `dataRepresentation`. That blob is not the private key; only the same enclave
162
+ on the same device can turn it back into a usable key.
162
163
 
163
- CryptoKit SE keys are **ephemeral by default** - if you don't persist the `dataRepresentation`, the key reference is lost when the app terminates. The `dataRepresentation` property returns an opaque encrypted blob that only the same Secure Enclave on the same device can use to reconstruct the key. It is emphatically **not** the raw private key.
164
+ Store the blob as a generic password with `WhenUnlockedThisDeviceOnly`, saving
165
+ with add, then update on duplicate:
164
166
 
165
167
  ```swift
166
- // ✅ Persist SE key to keychain and retrieve later
167
- import CryptoKit
168
- import Security
169
-
170
- // --- Store ---
171
- let privateKey = try SecureEnclave.P256.Signing.PrivateKey()
172
- let keyBlob: Data = privateKey.dataRepresentation // Encrypted, device-bound
173
-
174
- let storeQuery: [String: Any] = [
175
- kSecClass as String: kSecClassGenericPassword,
176
- kSecAttrAccount as String: "com.myapp.signing-key",
177
- kSecValueData as String: keyBlob,
178
- kSecAttrAccessible as String:
179
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly
180
- ]
181
- // Delete-then-add pattern to handle existing items
182
- SecItemDelete(storeQuery as CFDictionary)
183
- let status = SecItemAdd(storeQuery as CFDictionary, nil)
184
- guard status == errSecSuccess else {
185
- throw NSError(domain: NSOSStatusErrorDomain, code: Int(status))
168
+ func persist(_ key: SecureEnclave.P256.Signing.PrivateKey, account: String) throws {
169
+ let match: [String: Any] = [
170
+ kSecClass as String: kSecClassGenericPassword,
171
+ kSecAttrService as String: "com.example.device-key",
172
+ kSecAttrAccount as String: account,
173
+ kSecUseDataProtectionKeychain as String: true
174
+ ]
175
+ var insert = match
176
+ insert[kSecValueData as String] = key.dataRepresentation
177
+ insert[kSecAttrAccessible as String] = kSecAttrAccessibleWhenUnlockedThisDeviceOnly
178
+ let added = SecItemAdd(insert as CFDictionary, nil)
179
+ if added == errSecSuccess { return }
180
+ guard added == errSecDuplicateItem else { throw KeychainError(status: added) }
181
+ let changed = SecItemUpdate(match as CFDictionary,
182
+ [kSecValueData as String: key.dataRepresentation] as CFDictionary)
183
+ guard changed == errSecSuccess else { throw KeychainError(status: changed) }
186
184
  }
187
185
 
188
- // --- Retrieve ---
189
- let fetchQuery: [String: Any] = [
190
- kSecClass as String: kSecClassGenericPassword,
191
- kSecAttrAccount as String: "com.myapp.signing-key",
192
- kSecReturnData as String: true,
193
- kSecMatchLimit as String: kSecMatchLimitOne
194
- ]
195
- var item: CFTypeRef?
196
- let fetchStatus = SecItemCopyMatching(fetchQuery as CFDictionary, &item)
197
- guard fetchStatus == errSecSuccess, let storedBlob = item as? Data else {
198
- throw NSError(domain: NSOSStatusErrorDomain, code: Int(fetchStatus))
186
+ func restore(account: String) throws -> SecureEnclave.P256.Signing.PrivateKey? {
187
+ let query: [String: Any] = [
188
+ kSecClass as String: kSecClassGenericPassword,
189
+ kSecAttrService as String: "com.example.device-key",
190
+ kSecAttrAccount as String: account,
191
+ kSecReturnData as String: true,
192
+ kSecMatchLimit as String: kSecMatchLimitOne,
193
+ kSecUseDataProtectionKeychain as String: true
194
+ ]
195
+ var out: CFTypeRef?
196
+ let status = SecItemCopyMatching(query as CFDictionary, &out)
197
+ if status == errSecItemNotFound { return nil }
198
+ guard status == errSecSuccess, let blob = out as? Data else { throw KeychainError(status: status) }
199
+ return try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: blob)
199
200
  }
200
-
201
- let restoredKey = try SecureEnclave.P256.Signing.PrivateKey(
202
- dataRepresentation: storedBlob
203
- )
204
- // restoredKey is fully functional - operations route to the same SE key
205
201
  ```
206
202
 
207
- On macOS, add `kSecUseDataProtectionKeychain: true` to target the modern data protection keychain rather than the legacy file-based keychain. On iOS/tvOS/watchOS this flag is redundant but harmless.
208
-
209
- **Always use `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`** for SE key blobs - the key is device-bound anyway, and syncable accessibility levels would store the blob on iCloud servers where it is useless (the SE that can decrypt it isn't there).
203
+ - `kSecUseDataProtectionKeychain` is what makes this work on macOS; other
204
+ platforms ignore it.
205
+ - Always use `WhenUnlockedThisDeviceOnly` for these blobs. A class that syncs or
206
+ migrates would copy a blob to devices whose enclave cannot open it.
207
+ - `KeychainError` is the wrapper from
208
+ [keychain-fundamentals.md](keychain-fundamentals.md).
210
209
 
211
- ---
210
+ ## Biometric-Gated Enclave Keys
212
211
 
213
- ## Biometric-gated SE keys with SecAccessControl
212
+ The enclave evaluates the access control itself, so the policy holds even if
213
+ the OS is compromised. The flags always include `.privateKeyUsage`; without it
214
+ the key is created but every signing attempt fails.
214
215
 
215
- The most security-critical SE pattern combines hardware key isolation with biometric authentication. The SE evaluates access control policies internally, making them tamper-proof even against OS-level compromises.
216
+ Face ID prompts need `NSFaceIDUsageDescription` in `Info.plist`.
216
217
 
217
218
  ```swift
218
- // ✅ Complete biometric-gated SE key creation (iOS 13+)
219
- import CryptoKit
220
219
  import LocalAuthentication
221
- import Security
222
-
223
- func createBiometricKey() throws -> SecureEnclave.P256.Signing.PrivateKey {
224
- guard SecureEnclave.isAvailable else {
225
- throw SecureEnclaveError.notAvailable
226
- }
227
220
 
228
- var error: Unmanaged<CFError>?
229
- guard let accessControl = SecAccessControlCreateWithFlags(
230
- nil,
231
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
232
- [.privateKeyUsage, .biometryCurrentSet],
233
- &error
234
- ) else {
235
- throw error!.takeRetainedValue() as Error
221
+ func newApprovalKey() throws -> (key: SecureEnclave.P256.Signing.PrivateKey, context: LAContext) {
222
+ guard SecureEnclave.isAvailable else { throw EnclaveKeyError.noEnclave }
223
+ var cfError: Unmanaged<CFError>?
224
+ guard let acl = SecAccessControlCreateWithFlags(
225
+ nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
226
+ [.privateKeyUsage, .biometryCurrentSet], &cfError) else {
227
+ throw (cfError?.takeRetainedValue() as Error?) ?? EnclaveKeyError.noEnclave
236
228
  }
237
-
238
- let context = LAContext()
239
- context.localizedReason = "Authenticate to create signing key"
240
- context.touchIDAuthenticationAllowableReuseDuration = 10
241
-
242
- return try SecureEnclave.P256.Signing.PrivateKey(
243
- compactRepresentable: true,
244
- accessControl: accessControl,
245
- authenticationContext: context
246
- )
229
+ let ctx = LAContext()
230
+ ctx.localizedReason = "Approve the transfer"
231
+ ctx.touchIDAuthenticationAllowableReuseDuration = 10
232
+ let key = try SecureEnclave.P256.Signing.PrivateKey(compactRepresentable: true,
233
+ accessControl: acl,
234
+ authenticationContext: ctx)
235
+ return (key, ctx)
247
236
  }
248
237
 
249
- // Later: reconstruct and use (biometric prompt appears)
250
- func signWithBiometricKey(storedBlob: Data, data: Data) throws -> Data {
251
- let context = LAContext()
252
- context.localizedReason = "Authenticate to sign transaction"
253
-
254
- let key = try SecureEnclave.P256.Signing.PrivateKey(
255
- dataRepresentation: storedBlob,
256
- authenticationContext: context
257
- )
258
- return try key.signature(for: data).derRepresentation
238
+ func approve(_ payload: Data, blob: Data) throws -> Data {
239
+ let ctx = LAContext()
240
+ ctx.localizedReason = "Approve the transfer"
241
+ let key = try SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: blob,
242
+ authenticationContext: ctx)
243
+ return try key.signature(for: payload).derRepresentation
259
244
  }
260
245
  ```
261
246
 
247
+ Calling `signature(for:)` on the restored key shows the biometric prompt.
248
+
262
249
  ```swift
263
- // ❌ Omitting .privateKeyUsage causes signing to fail
264
- let badControl = SecAccessControlCreateWithFlags(
265
- nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
266
- .biometryCurrentSet, // Missing .privateKeyUsage!
267
- nil
268
- )!
269
- // Key creation succeeds, but signing operations will fail
250
+ // Wrong: .privateKeyUsage missing. Creation succeeds, signing fails.
251
+ let brokenACL = SecAccessControlCreateWithFlags(
252
+ nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, nil)
270
253
  ```
271
254
 
272
- **Access control flag selection:**
255
+ Choosing the flag:
273
256
 
274
- - **`.biometryCurrentSet`** - Strongest. Key is permanently invalidated when the user re-enrolls biometrics (adds a new fingerprint, re-registers Face ID). Best for banking/healthcare. Requires re-keying logic when invalidation occurs.
275
- - **`.biometryAny`** - Key survives biometric re-enrollment. Good balance of security and convenience for most apps.
276
- - **`.userPresence`** - Accepts biometric or device passcode. Most flexible; use when you just need proof that a human is present.
257
+ - `.biometryCurrentSet`: strongest; the key becomes unusable when enrollment
258
+ changes, so plan for re-keying. Banking and healthcare.
259
+ - `.biometryAny`: keeps working after re-enrollment.
260
+ - `.userPresence`: biometrics or the passcode; proof that a person is there.
277
261
 
278
- **Critical operational note:** If you use `.biometryCurrentSet` and the user changes their enrolled biometrics, the key becomes **permanently unusable**. Your app must detect `errSecItemNotFound` or authentication errors, explain to the user why re-authentication is needed, and generate a fresh key with server-side re-enrollment. (See `biometric-authentication.md` for full LAContext integration patterns.)
262
+ The invalidation caused by `.biometryCurrentSet` is permanent. Watch for
263
+ `errSecItemNotFound` or authentication errors, tell the user what happened,
264
+ create a new key, and register it with the server again.
265
+ [biometric-authentication.md](biometric-authentication.md) covers the
266
+ LocalAuthentication side.
279
267
 
280
- ---
281
-
282
- ## Legacy Security framework approach (iOS 10+)
283
-
284
- Before CryptoKit, SE keys were created via `SecKeyCreateRandomKey` with `kSecAttrTokenIDSecureEnclave`. This still works and is necessary when targeting pre-iOS 13 or working with certificate-based identity operations:
268
+ ## Security Framework Route (iOS 10+)
285
269
 
286
270
  ```swift
287
- // Legacy approach - functional but verbose; prefer CryptoKit for new code
288
- import Security
289
-
290
- func legacyCreateSEKey(tag: String) throws -> SecKey {
291
- let access = SecAccessControlCreateWithFlags(
292
- kCFAllocatorDefault,
293
- kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
294
- [.privateKeyUsage, .biometryCurrentSet],
295
- nil
296
- )!
297
-
298
- let attributes: NSDictionary = [
299
- kSecAttrKeyType: kSecAttrKeyTypeECSECPrimeRandom,
300
- kSecAttrKeySizeInBits: 256,
301
- kSecAttrTokenID: kSecAttrTokenIDSecureEnclave,
302
- kSecPrivateKeyAttrs: [
303
- kSecAttrIsPermanent: true,
304
- kSecAttrApplicationTag: tag.data(using: .utf8)!,
305
- kSecAttrAccessControl: access
271
+ func legacyEnclaveKey(tag: Data) throws -> SecKey {
272
+ var cfError: Unmanaged<CFError>?
273
+ guard let acl = SecAccessControlCreateWithFlags(
274
+ nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
275
+ [.privateKeyUsage, .biometryCurrentSet], &cfError) else {
276
+ throw (cfError?.takeRetainedValue() as Error?) ?? EnclaveKeyError.noEnclave
277
+ }
278
+ let spec: [String: Any] = [
279
+ kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
280
+ kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
281
+ kSecAttrKeySizeInBits as String: 256,
282
+ kSecPrivateKeyAttrs as String: [
283
+ kSecAttrIsPermanent as String: true,
284
+ kSecAttrApplicationTag as String: tag,
285
+ kSecAttrAccessControl as String: acl
306
286
  ]
307
287
  ]
308
-
309
- var error: Unmanaged<CFError>?
310
- guard let privateKey = SecKeyCreateRandomKey(attributes, &error) else {
311
- throw error!.takeRetainedValue() as Error
288
+ guard let key = SecKeyCreateRandomKey(spec as CFDictionary, &cfError) else {
289
+ throw (cfError?.takeRetainedValue() as Error?) ?? EnclaveKeyError.noEnclave
312
290
  }
313
- return privateKey
291
+ return key
314
292
  }
315
293
  ```
316
294
 
317
- CryptoKit is preferred for new code because it provides compile-time type safety (distinct types per algorithm/operation), automatic memory zeroing, Swift-native error handling, and a curated API surface. The Security framework remains necessary for certificate management (`SecTrust`), RSA keys, or existing keychain items stored via the older API. (See `certificate-trust.md` for SecTrust patterns.)
318
-
319
- ---
320
-
321
- ## iOS 26: Post-quantum cryptography in the Secure Enclave
322
-
323
- WWDC 2025 session "Get ahead with quantum-secure cryptography" announced the most significant expansion of the SE's developer-facing capabilities since its 2013 introduction. Starting with **iOS 26, macOS 26, and all 2025 platform releases**, four new algorithm families are available:
324
-
325
- - **`SecureEnclave.MLKEM768`** and **`SecureEnclave.MLKEM1024`** - Post-quantum key encapsulation (FIPS 203). Hardware-isolated ML-KEM operations for quantum-resistant key exchange.
326
- - **`SecureEnclave.MLDSA65`** and **`SecureEnclave.MLDSA87`** - Post-quantum digital signatures (FIPS 204). Hardware-isolated ML-DSA signing resistant to quantum attacks.
327
-
328
- These are **hardware-backed**, not software-only. Apple confirmed SE support explicitly. The implementations are formally verified as functionally equivalent to their FIPS specifications.
329
-
330
- **Quantum-secure TLS by default:** `URLSession` and `Network.framework` automatically upgrade to quantum-secure TLS 1.3 using X-Wing (ML-KEM768 + X25519) in iOS 26. System services including CloudKit, Push Notifications, and Private Relay already use it. For most developers, no code changes are needed.
331
-
332
- **Custom end-to-end encryption:** Apple recommends hybrid constructions that combine post-quantum and classical algorithms. The `XWingMLKEM768X25519` type provides a hybrid KEM ciphersuite. For application-level encryption, use `SecureEnclave.MLKEM768.PrivateKey` to encapsulate/decapsulate shared secrets within the hardware boundary.
333
-
334
- **API evolution timeline:**
335
-
336
- | Release | SE Developer Additions |
337
- | ----------------------- | -------------------------------------------------------------------------------------------------- |
338
- | **iOS 13** (2019) | CryptoKit introduced: `SecureEnclave.P256.Signing`, `.P256.KeyAgreement`, `.isAvailable` |
339
- | **iOS 14** (2020) | No SE changes. Added HKDF, PEM/DER format support |
340
- | **iOS 15-16** (2021-22) | No SE changes |
341
- | **iOS 17** (2023) | No SE changes. HPKE added (software-only). iMessage PQ3 shipped in 17.4 |
342
- | **iOS 18** (2024) | No SE changes |
343
- | **iOS 26** (2025) | **Major expansion**: `.MLKEM768`, `.MLKEM1024`, `.MLDSA65`, `.MLDSA87`. Quantum-secure TLS default |
295
+ Reach for this only when the target predates iOS 13 or the work involves
296
+ certificates and identities. Otherwise CryptoKit is better: one type per
297
+ algorithm, memory zeroed automatically, Swift errors, and a small, curated
298
+ surface. The Security framework is still required for `SecTrust`, RSA, and
299
+ items that already exist in the keychain.
344
300
 
345
- The SE's classical elliptic curve support remains **P-256 only** - the expansion is entirely into lattice-based post-quantum algorithms.
301
+ ## iOS 26: Post-Quantum Types in the Enclave
346
302
 
347
- > For the full post-quantum algorithm catalog - including software-only types, X-Wing hybrid KEM construction, key/signature size trade-offs, HPKE integration patterns, and hybrid classical+PQ signing - see `cryptokit-public-key.md` section Post-Quantum Cryptography (iOS 26+). This section covers the hardware-backed SE variants specifically.
303
+ iOS 26, macOS 26 and the other 2025 platform releases add hardware-backed
304
+ lattice algorithms to the enclave:
348
305
 
349
- ---
306
+ - `SecureEnclave.MLKEM768` and `SecureEnclave.MLKEM1024`: key encapsulation per
307
+ FIPS 203;
308
+ - `SecureEnclave.MLDSA65` and `SecureEnclave.MLDSA87`: signatures per FIPS 204.
350
309
 
351
- ## When to use SE versus software keys
352
-
353
- **Use the Secure Enclave for:** root signing keys, device attestation, transaction authorization, biometric-gated authentication, and any scenario where proving key possession on a specific physical device matters. The non-exportability guarantee is the core value - an attacker who compromises the application processor still cannot extract the private key.
354
-
355
- **Use standard keychain (software keys) for:** session tokens, API keys, symmetric encryption keys, keys requiring algorithms beyond P-256 (RSA, P-384, Ed25519), keys that must sync via iCloud Keychain, keys that need to survive device replacement, and high-throughput operations requiring thousands of operations per second.
356
-
357
- **Common effective pattern:** Store a master asymmetric key in the SE and use ECDH to derive or wrap symmetric keys for bulk encryption. The SE protects the root of trust; derived keys handle the high-throughput work.
358
-
359
- The anti-pattern is reaching for the SE for every secret. The P-256 constraint, performance overhead, and device-binding mean SE keys should protect the most critical operations, not replace the standard keychain. (See `credential-storage-patterns.md` for token lifecycle patterns.)
360
-
361
- ---
362
-
363
- ## Six correctness traps AI generators get wrong
364
-
365
- These patterns appear routinely in LLM-generated code. Each reflects a misunderstanding of the SE's hardware architecture.
366
-
367
- ### 1. Not checking isAvailable (and the simulator double-trap)
368
-
369
- The minimal check is `SecureEnclave.isAvailable`, but this alone is insufficient on the simulator. The robust pattern combines compile-time and runtime checks:
310
+ Apple states that these implementations were formally verified against the FIPS
311
+ specifications.
370
312
 
371
313
  ```swift
372
- // ✅ Robust availability check - safe everywhere
373
- var canUseSecureEnclave: Bool {
374
- #if targetEnvironment(simulator)
375
- return false
376
- #else
377
- return SecureEnclave.isAvailable
378
- #endif
314
+ @available(iOS 26.0, macOS 26.0, *)
315
+ func pqRoundTrip() throws -> Bool {
316
+ let recipient = try SecureEnclave.MLKEM768.PrivateKey()
317
+ let sent = try recipient.publicKey.encapsulate()
318
+ let received = try recipient.decapsulate(sent.encapsulated)
319
+ return received == sent.sharedSecret
379
320
  }
380
321
  ```
381
322
 
382
- ### 2. Attempting to import external keys
383
-
384
- There is no `init(rawRepresentation:)` on SE key types. `init(dataRepresentation:)` accepts only the opaque blob from a previously created SE key:
385
-
386
- ```swift
387
- // ❌ IMPOSSIBLE: Cannot import an existing key into the Secure Enclave
388
- let externalKey = P256.Signing.PrivateKey()
389
- let rawBytes = externalKey.rawRepresentation
390
- // SecureEnclave.P256.Signing.PrivateKey(rawRepresentation:) DOES NOT EXIST
391
- // SecureEnclave.P256.Signing.PrivateKey(dataRepresentation: rawBytes) WILL THROW
392
-
393
- // ✅ Keys MUST be generated inside the SE
394
- let seKey = try SecureEnclave.P256.Signing.PrivateKey()
395
- ```
396
-
397
- ### 3. Attempting AES/symmetric encryption directly
398
-
399
- The SE's internal AES engine is not exposed to developers. Use ECDH → HKDF → AES-GCM instead (see Key agreement section above).
400
-
401
- ### 4. Assuming SE keys can be backed up or transferred
402
-
403
- SE keys are device-bound. Server-side architectures **must** register the device's public key and support re-keying when a user changes devices. Design re-enrollment flows from day one.
404
-
405
- ### 5. Using legacy Security framework when CryptoKit is available
406
-
407
- `SecKeyCreateRandomKey` + `kSecAttrTokenIDSecureEnclave` still works, but CryptoKit eliminates ~20 lines of dictionary-based C-style code and provides compile-time type safety. Use the legacy API only for pre-iOS 13 targets or certificate operations.
408
-
409
- ### 6. Omitting .privateKeyUsage in access control
410
-
411
- SE keys created with `SecAccessControl` for biometric gating **must** include `.privateKeyUsage`. Without it, key creation succeeds but signing operations fail silently on some configurations. Always combine: `[.privateKeyUsage, .biometryCurrentSet]`.
412
-
413
- ---
414
-
415
- ## Testing and CI/CD strategies
416
-
417
- ### Protocol-based abstraction for testable SE code
418
-
419
- Since SE operations fail on simulators and most CI environments, abstract cryptographic operations behind a protocol with SE, software, and mock implementations:
323
+ Transport needs no work: in iOS 26, `URLSession` and `Network.framework`
324
+ negotiate quantum-secure TLS 1.3 on their own, offering the hybrid
325
+ `X25519MLKEM768` key-exchange group, and CloudKit, push notifications and
326
+ iCloud Private Relay already use it. For an app's own end-to-end encryption,
327
+ prefer a hybrid of post-quantum and classical algorithms, such as the CryptoKit
328
+ X-Wing KEM (`XWingMLKEM768X25519`) in HPKE; an
329
+ `SecureEnclave.MLKEM768.PrivateKey` performs encapsulation and decapsulation
330
+ inside the hardware. The full CryptoKit catalog is in
331
+ [cryptokit-public-key.md](cryptokit-public-key.md).
332
+
333
+ | Release | Enclave-related change |
334
+ | --- | --- |
335
+ | iOS 13 | CryptoKit ships with enclave P-256 signing and key agreement and `SecureEnclave.isAvailable` |
336
+ | iOS 14 | nothing for the enclave (HKDF as a type, PEM and DER key formats arrive) |
337
+ | iOS 15, 16 | nothing |
338
+ | iOS 17 | nothing (HPKE is software only; iMessage PQ3 arrives in 17.4) |
339
+ | iOS 18 | nothing |
340
+ | iOS 26 | enclave ML-KEM and ML-DSA; post-quantum TLS by default |
341
+
342
+ The only classical curve remains P-256.
343
+
344
+ ## Enclave or Software Key?
345
+
346
+ The enclave suits root signing keys, your own scheme of device attestation,
347
+ transaction approval, biometric-gated authentication, and proving that a
348
+ request came from one particular device.
349
+
350
+ Software keys kept in the keychain suit API keys, session tokens, symmetric
351
+ keys, algorithms the enclave lacks (Ed25519, RSA, P-384), keys required to sync or
352
+ survive a device change, and high-throughput operations.
353
+
354
+ The pattern that works: one asymmetric master key in the enclave, with ECDH
355
+ deriving or wrapping the symmetric keys that do the bulk encryption. Putting
356
+ every secret in the enclave is an anti-pattern.
357
+
358
+ ## Six Traps
359
+
360
+ 1. **Availability.** Combine the compile-time Simulator branch with
361
+ `SecureEnclave.isAvailable`.
362
+ 2. **Import.** Enclave types have no `init(rawRepresentation:)`, and
363
+ `init(dataRepresentation:)` given raw software key bytes throws. Keys are
364
+ generated inside the enclave.
365
+ 3. **AES.** Not available. Go through ECDH, HKDF, then AES-GCM.
366
+ 4. **Backup.** None. From the first release, the server records each device's
367
+ public key and supports re-keying when the device changes.
368
+ 5. **The legacy route.** `SecKeyCreateRandomKey` takes about twenty lines of
369
+ dictionary setup; keep it for pre-iOS 13 targets and certificate work.
370
+ 6. **`.privateKeyUsage`.** Any access control on an enclave key includes it,
371
+ for example `[.privateKeyUsage, .biometryCurrentSet]`; leave it out and
372
+ signing fails even though creation succeeded.
373
+
374
+ ## Testing and CI
375
+
376
+ Hide the key behind a protocol so tests do not need hardware:
420
377
 
421
378
  ```swift
422
- // ✅ Protocol abstraction for testable SE-dependent code
423
- import CryptoKit
424
- import Foundation
425
-
426
- protocol SigningKeyProvider {
427
- var publicKeyData: Data { get throws }
428
- func sign(_ data: Data) throws -> Data
379
+ protocol DeviceSigner {
380
+ var publicKeyDER: Data { get throws }
381
+ func sign(_ payload: Data) throws -> Data
429
382
  }
430
383
 
431
- // Production: Secure Enclave implementation
432
- final class SESigningKey: SigningKeyProvider {
433
- private let key: SecureEnclave.P256.Signing.PrivateKey
434
-
435
- init() throws { self.key = try SecureEnclave.P256.Signing.PrivateKey() }
436
- init(dataRepresentation: Data) throws {
437
- self.key = try SecureEnclave.P256.Signing.PrivateKey(
438
- dataRepresentation: dataRepresentation)
439
- }
440
-
441
- var publicKeyData: Data { get throws { key.publicKey.derRepresentation } }
442
- func sign(_ data: Data) throws -> Data {
443
- try key.signature(for: data).derRepresentation
384
+ struct EnclaveSigner: DeviceSigner {
385
+ let key: SecureEnclave.P256.Signing.PrivateKey
386
+ init(blob: Data? = nil) throws {
387
+ key = try blob.map { try .init(dataRepresentation: $0) } ?? .init()
444
388
  }
389
+ var publicKeyDER: Data { key.publicKey.derRepresentation }
390
+ func sign(_ payload: Data) throws -> Data { try key.signature(for: payload).derRepresentation }
445
391
  }
446
392
 
447
- // Fallback: Software P256 (same curve, same signature format)
448
- final class SoftwareSigningKey: SigningKeyProvider {
449
- private let key: P256.Signing.PrivateKey
450
-
451
- init() { self.key = P256.Signing.PrivateKey() }
452
- var publicKeyData: Data { get throws { key.publicKey.derRepresentation } }
453
- func sign(_ data: Data) throws -> Data {
454
- try key.signature(for: data).derRepresentation
455
- }
393
+ struct SoftwareSigner: DeviceSigner {
394
+ let key = P256.Signing.PrivateKey()
395
+ var publicKeyDER: Data { key.publicKey.derRepresentation }
396
+ func sign(_ payload: Data) throws -> Data { try key.signature(for: payload).derRepresentation }
456
397
  }
457
398
 
458
- // Test: Mock implementation
459
- final class MockSigningKey: SigningKeyProvider {
460
- var publicKeyDataToReturn = Data()
461
- var signatureToReturn = Data()
462
- var shouldThrow = false
463
- var signCallCount = 0
464
-
465
- var publicKeyData: Data { get throws { publicKeyDataToReturn } }
466
- func sign(_ data: Data) throws -> Data {
467
- signCallCount += 1
468
- if shouldThrow { throw NSError(domain: "Mock", code: -1) }
469
- return signatureToReturn
399
+ final class StubSigner: DeviceSigner {
400
+ var cannedSignature = Data([0x30, 0x01])
401
+ var shouldFail = false
402
+ private(set) var signCount = 0
403
+ var publicKeyDER: Data { Data([0x04]) }
404
+ func sign(_ payload: Data) throws -> Data {
405
+ signCount += 1
406
+ if shouldFail { throw EnclaveKeyError.noEnclave }
407
+ return cannedSignature
470
408
  }
471
409
  }
472
- ```
473
410
 
474
- ### Factory with SE → software fallback
475
-
476
- ```swift
477
- // ✅ Runtime factory - SE when available, software otherwise
478
- struct SigningKeyFactory {
479
- static func create() throws -> SigningKeyProvider {
411
+ enum SignerFactory {
412
+ static func make() throws -> any DeviceSigner {
480
413
  #if targetEnvironment(simulator)
481
- return SoftwareSigningKey()
414
+ return SoftwareSigner()
482
415
  #else
483
- if SecureEnclave.isAvailable {
484
- return try SESigningKey()
485
- }
486
- return SoftwareSigningKey()
416
+ return SecureEnclave.isAvailable ? try EnclaveSigner() : SoftwareSigner()
487
417
  #endif
488
418
  }
489
419
  }
490
420
  ```
491
421
 
492
- Both SE and software implementations produce **identical P256 ECDSA signatures** - verification code works the same regardless of which implementation created the key.
493
-
494
- ### XCTest patterns
422
+ Enclave and software P-256 keys produce signatures in the same format, so the
423
+ server's verification code does not care which one signed.
495
424
 
496
425
  ```swift
497
426
  import XCTest
498
- @testable import MyApp
499
-
500
- final class AuthServiceTests: XCTestCase {
501
- func testSignChallenge() throws {
502
- let mock = MockSigningKey()
503
- mock.signatureToReturn = Data([0xDE, 0xAD])
504
- let service = AuthService(signingKey: mock)
505
-
506
- let result = try service.signChallenge(Data("test".utf8))
507
427
 
508
- XCTAssertEqual(mock.signCallCount, 1)
509
- XCTAssertEqual(result, Data([0xDE, 0xAD]))
428
+ final class DeviceSignerTests: XCTestCase {
429
+ func testStubCountsCalls() throws {
430
+ let stub = StubSigner()
431
+ let out = try stub.sign(Data("x".utf8))
432
+ XCTAssertEqual(out, stub.cannedSignature)
433
+ XCTAssertEqual(stub.signCount, 1)
510
434
  }
511
435
 
512
- func testRealSEKey() throws {
436
+ func testEnclaveSignatureVerifies() throws {
513
437
  #if targetEnvironment(simulator)
514
- throw XCTSkip("Secure Enclave not available on Simulator")
438
+ throw XCTSkip("No Secure Enclave in the Simulator")
515
439
  #else
516
- guard SecureEnclave.isAvailable else {
517
- throw XCTSkip("Secure Enclave not available on this hardware")
518
- }
519
- let key = try SESigningKey()
520
- let signature = try key.sign(Data("test".utf8))
521
- XCTAssertFalse(signature.isEmpty)
440
+ try XCTSkipUnless(SecureEnclave.isAvailable, "Device has no Secure Enclave")
441
+ let signer = try EnclaveSigner()
442
+ let message = Data("ping".utf8)
443
+ let der = try signer.sign(message)
444
+ let pub = try P256.Signing.PublicKey(derRepresentation: signer.publicKeyDER)
445
+ XCTAssertTrue(pub.isValidSignature(try .init(derRepresentation: der), for: message))
522
446
  #endif
523
447
  }
524
448
  }
525
449
  ```
526
450
 
527
- ### CI/CD reality
528
-
529
- GitHub Actions macOS runners (both arm64 and Intel) run in VMs where the **Secure Enclave is not accessible** - the Apple Virtualization Framework does not pass through SE access to guest VMs. Self-hosted runners on physical Mac hardware (Mac mini M-series, MacBook Pro with T2) do have SE access. Xcode Cloud runs on Apple silicon but SE availability depends on the specific cloud configuration.
530
-
531
- **Practical CI approach:** Run unit tests with mocks on CI; run SE integration tests only on physical device test farms or self-hosted runners; tag SE-specific tests with `XCTSkip` guards for conditional execution. (See `testing-security-code.md` for comprehensive CI/CD patterns.)
532
-
533
- ---
534
-
535
- ## Operational guidance: rotation, migration, and incident response
536
-
537
- Treat SE keys as **ephemeral, device-bound artifacts** rather than permanent user identities:
538
-
539
- - **Device replacement:** When a user gets a new device, SE keys from the old device are gone. Your app must detect a missing key (keychain blob absent or `dataRepresentation` fails to reconstruct) and trigger a re-enrollment flow: generate a new SE key, register its public key with your backend, and invalidate the old public key.
540
- - **Biometric re-enrollment:** If using `.biometryCurrentSet`, adding a new fingerprint or resetting Face ID permanently invalidates the key. Catch the error, explain to the user why they need to re-authenticate, and provision a fresh key.
541
- - **Key rotation:** Periodic rotation of SE keys follows the same re-enrollment pattern. Generate a new key, register the new public key with the server, sign a transition token with the old key (if still valid), and delete the old key blob from the keychain.
542
- - **Incident response:** If a device is compromised at the OS level, SE keys remain protected (the SE operates independently). However, if the physical device is in an attacker's possession and they know the passcode, they can authenticate to the SE. Remote wipe via MDM or Find My destroys the UID-derived key hierarchy, rendering all SE keys permanently unrecoverable.
543
-
544
- ---
545
-
546
- ## Conclusion
547
-
548
- The Secure Enclave's developer surface was remarkably stable from iOS 13 to iOS 25 - `SecureEnclave.P256` was the entire API. iOS 26 broke open the boundary with post-quantum ML-KEM and ML-DSA, the first algorithm expansion in the SE's 12-year history. The practical insight is that **correct SE usage is more about what you don't do** (don't skip availability checks, don't try to import keys, don't assume portability, don't use the SE for symmetric encryption) than complex API choreography. The CryptoKit API is deliberately minimal and hard to misuse, which is its greatest strength.
549
-
550
- For new projects, the recommended architecture is: protocol-based abstraction around signing and key agreement; SE implementation as primary with software P256 fallback; `dataRepresentation` persisted in the keychain as `kSecClassGenericPassword` with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`; biometric access control for high-value keys; server-side public key registration with re-keying support for device replacement; and `XCTSkip`-guarded integration tests on physical hardware.
551
-
552
- ---
553
-
554
- ## Summary Checklist
555
-
556
- 1. **Availability guard** - Always combine `#if targetEnvironment(simulator)` (compile-time) with `SecureEnclave.isAvailable` (runtime) before any SE key creation. Never assume SE is present.
557
- 2. **No key import** - SE keys must be generated inside the hardware. `init(dataRepresentation:)` reconstructs existing SE keys only - it cannot import external key material.
558
- 3. **No symmetric encryption** - The SE does not expose AES to developers. Use ECDH → HKDF → `AES.GCM` for encryption workflows starting from an SE key.
559
- 4. **Device-bound design** - SE keys cannot be backed up, synced, or transferred. Build server-side re-enrollment flows for device replacement from day one.
560
- 5. **Persist dataRepresentation** - Store the opaque encrypted blob in the keychain as `kSecClassGenericPassword` with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. Without persistence, keys are lost on app termination.
561
- 6. **Include .privateKeyUsage** - When creating `SecAccessControl` for biometric-gated SE keys, always include `.privateKeyUsage` alongside the biometric flag. Omitting it causes signing to fail silently.
562
- 7. **Handle biometric invalidation** - `.biometryCurrentSet` keys are permanently invalidated on biometric re-enrollment. Detect the error and trigger re-keying with server notification.
563
- 8. **Protocol abstraction** - Abstract SE operations behind a protocol with SE, software, and mock implementations for testability. Both SE and software P256 produce identical signature formats.
564
- 9. **CryptoKit over Security framework** - Use `SecureEnclave.P256.Signing.PrivateKey` (CryptoKit) instead of `SecKeyCreateRandomKey` + `kSecAttrTokenIDSecureEnclave` for new code. Reserve the Security framework for certificates and pre-iOS 13 targets.
565
- 10. **iOS 26 post-quantum** - `SecureEnclave.MLKEM768/1024` and `.MLDSA65/87` are hardware-backed. For custom E2E encryption, adopt hybrid constructions (classical + PQC). `URLSession` TLS upgrades automatically.
566
- 11. **CI/CD skip guards** - Use `XCTSkip` for SE-specific tests in CI. GitHub Actions VMs do not have SE access. Run SE integration tests only on physical hardware or device farms.
451
+ CI facts:
452
+
453
+ - GitHub Actions macOS runners, Apple silicon and Intel alike, are virtual
454
+ machines, and Apple's Virtualization framework does not pass the enclave
455
+ through.
456
+ - Self-hosted physical Macs (an Apple silicon Mac mini, a T2 MacBook Pro) have
457
+ one. Whether Xcode Cloud jobs can use it depends on the configuration.
458
+ - Run mocks in CI, run enclave integration tests on a device farm or
459
+ self-hosted hardware, and mark those tests with `XCTSkip` guards.
460
+
461
+ More in [testing-security-code.md](testing-security-code.md).
462
+
463
+ ## Operating Enclave Keys
464
+
465
+ - Treat enclave keys as device-bound and replaceable, not as a permanent
466
+ identity.
467
+ - **New device:** notice the missing blob or the failed restore, create a key,
468
+ register the new public key, and revoke the old one on the server.
469
+ - **Re-enrollment under `.biometryCurrentSet`:** catch the error, explain it,
470
+ and provision a fresh key.
471
+ - **Rotation:** create the new key, register it, sign a hand-over token with
472
+ the old key while it still works, then delete the old blob.
473
+ - **Incidents:** a compromised OS still cannot read enclave keys, but someone
474
+ holding the device and knowing the passcode can authenticate. Wiping the device
475
+ remotely, through MDM or Find My, erases the key hierarchy rooted in the UID
476
+ and every key with it.
477
+
478
+ ## Summary
479
+
480
+ From iOS 13 until iOS 26 the whole enclave surface was
481
+ `SecureEnclave.P256`. A sound architecture puts the key behind a protocol, uses
482
+ the enclave where present with a software P-256 fallback, stores the
483
+ `dataRepresentation` as a `WhenUnlockedThisDeviceOnly` generic password, adds a
484
+ biometric ACL for high-value keys, registers public keys with the server and
485
+ supports re-keying, and guards device-only tests with `XCTSkip`.
486
+
487
+ Checklist:
488
+
489
+ - Compile-time and runtime availability checks.
490
+ - No attempt to import a key.
491
+ - No symmetric encryption inside the enclave; ECDH, then HKDF, then `AES.GCM`.
492
+ - Device-bound design with server-side re-enrollment.
493
+ - The `dataRepresentation` stored as a `WhenUnlockedThisDeviceOnly` generic
494
+ password, saved with add, then update.
495
+ - `.privateKeyUsage` alongside any biometric flags, and
496
+ `NSFaceIDUsageDescription` in `Info.plist`.
497
+ - `.biometryCurrentSet` invalidation handled by re-keying and telling the
498
+ server.
499
+ - A protocol with enclave, software and mock implementations.
500
+ - CryptoKit rather than the Security framework for new code.
501
+ - iOS 26 post-quantum enclave types are hardware-backed; custom end-to-end
502
+ schemes use a hybrid; `URLSession` TLS upgrades by itself.
503
+ - Enclave tests skip in CI, since GitHub Actions VMs have no enclave.
504
+
505
+ Related: [certificate-trust.md](certificate-trust.md),
506
+ [credential-storage-patterns.md](credential-storage-patterns.md).