@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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +285 -285
- package/package.json +3 -3
- package/pipeline/lib/redact.mjs +3 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/scripts/gen-skills-index.mjs +13 -1
- package/pipeline/scripts/pre-commit-check.sh +4 -0
- package/pipeline/skills/.skill-manifest.json +69 -69
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +345 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +107 -121
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +145 -165
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +306 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +335 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +277 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +135 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +274 -384
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +173 -321
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +228 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +302 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +236 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/example-mapping.json +1 -1
- package/pipeline/skills/shared/external/localization-reuse-map/reference/format-and-output.md +8 -7
- package/pipeline/skills/shared/external/localization-reuse-map/reference/publish-and-snapshot.md +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/_shared.py +100 -0
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-artifact.py +45 -26
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/build-spreadsheet.py +14 -16
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-annotations.py +12 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/fetch-legacy-labels.py +2 -1
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/publish-confluence.py +6 -5
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/render-overlay.py +44 -18
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-legacy-values.py +5 -2
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +10 -11
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/scan-screen-keys.py +4 -33
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/verify-map.py +12 -59
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +297 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/skill-creator/template.md +7 -1
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +302 -241
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +304 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +183 -162
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +411 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +375 -491
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +397 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +191 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +81 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +394 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +201 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +134 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +111 -138
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +160 -315
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +224 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +416 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,566 +1,506 @@
|
|
|
1
|
-
# Secure Enclave
|
|
1
|
+
# Secure Enclave
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
14
|
-
- [
|
|
15
|
-
- [CryptoKit SecureEnclave
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [
|
|
20
|
-
- [
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
- **No
|
|
58
|
-
|
|
59
|
-
- **
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
CryptoKit
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
109
|
-
let
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
func registrationPayload(for key: SecureEnclave.P256.Signing.PrivateKey) -> Data {
|
|
123
|
+
key.publicKey.derRepresentation
|
|
124
|
+
}
|
|
122
125
|
```
|
|
123
126
|
|
|
124
|
-
|
|
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
|
-
|
|
133
|
+
### Key agreement, then symmetric encryption
|
|
127
134
|
|
|
128
135
|
```swift
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
Store the blob as a generic password with `WhenUnlockedThisDeviceOnly`, saving
|
|
165
|
+
with add, then update on duplicate:
|
|
164
166
|
|
|
165
167
|
```swift
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
]
|
|
181
|
-
|
|
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
|
-
|
|
189
|
-
let
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
guard
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
&
|
|
234
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
//
|
|
264
|
-
let
|
|
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
|
-
|
|
255
|
+
Choosing the flag:
|
|
273
256
|
|
|
274
|
-
-
|
|
275
|
-
|
|
276
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
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
|
|
291
|
+
return key
|
|
314
292
|
}
|
|
315
293
|
```
|
|
316
294
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
301
|
+
## iOS 26: Post-Quantum Types in the Enclave
|
|
346
302
|
|
|
347
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
var
|
|
461
|
-
var
|
|
462
|
-
var
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
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
|
|
414
|
+
return SoftwareSigner()
|
|
482
415
|
#else
|
|
483
|
-
|
|
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
|
-
|
|
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
|
-
|
|
509
|
-
|
|
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
|
|
436
|
+
func testEnclaveSignatureVerifies() throws {
|
|
513
437
|
#if targetEnvironment(simulator)
|
|
514
|
-
throw XCTSkip("Secure Enclave
|
|
438
|
+
throw XCTSkip("No Secure Enclave in the Simulator")
|
|
515
439
|
#else
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
let
|
|
520
|
-
let
|
|
521
|
-
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
GitHub Actions macOS runners
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
## Summary
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
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).
|