@mmerterden/multi-agent-pipeline 20.7.0 → 20.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/LICENSE +0 -10
- package/docs/facts.json +1 -1
- package/manifest.json +266 -267
- package/package.json +2 -2
- package/pipeline/scripts/_notices.mjs +1 -1
- package/pipeline/skills/.skill-manifest.json +68 -68
- package/pipeline/skills/shared/README.md +70 -70
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +373 -381
- package/pipeline/skills/shared/external/alarmkit/evals/evals.json +23 -18
- package/pipeline/skills/shared/external/alarmkit/references/alarmkit-patterns.md +328 -378
- package/pipeline/skills/shared/external/app-clips/SKILL.md +260 -160
- package/pipeline/skills/shared/external/app-clips/evals/evals.json +27 -27
- package/pipeline/skills/shared/external/app-clips/references/data-handoff-notifications-location.md +150 -83
- package/pipeline/skills/shared/external/app-clips/references/routing-and-experiences.md +135 -83
- package/pipeline/skills/shared/external/app-clips/references/size-capabilities-and-promotion.md +143 -85
- package/pipeline/skills/shared/external/app-intents/SKILL.md +302 -304
- package/pipeline/skills/shared/external/app-intents/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-intents/references/appintents-advanced.md +594 -894
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +339 -277
- package/pipeline/skills/shared/external/app-store-optimization/evals/evals.json +27 -23
- package/pipeline/skills/shared/external/app-store-optimization/references/keyword-research-methodology.md +105 -122
- package/pipeline/skills/shared/external/app-store-optimization/references/product-page-variants.md +143 -166
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +307 -326
- package/pipeline/skills/shared/external/app-store-review/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/app-store-review/references/privacy-manifest.md +105 -67
- package/pipeline/skills/shared/external/app-store-review/references/review-checklists.md +114 -101
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +333 -360
- package/pipeline/skills/shared/external/apple-on-device-ai/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-conversion.md +215 -322
- package/pipeline/skills/shared/external/apple-on-device-ai/references/coreml-optimization.md +161 -256
- package/pipeline/skills/shared/external/apple-on-device-ai/references/foundation-models.md +277 -387
- package/pipeline/skills/shared/external/apple-on-device-ai/references/mlx-swift.md +196 -210
- package/pipeline/skills/shared/external/authentication/SKILL.md +265 -381
- package/pipeline/skills/shared/external/authentication/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/authentication/references/keychain-biometric.md +133 -178
- package/pipeline/skills/shared/external/authentication/references/passkeys.md +111 -147
- package/pipeline/skills/shared/external/avkit/SKILL.md +267 -364
- package/pipeline/skills/shared/external/avkit/evals/evals.json +26 -26
- package/pipeline/skills/shared/external/avkit/references/avkit-patterns.md +375 -493
- package/pipeline/skills/shared/external/background-processing/SKILL.md +270 -382
- package/pipeline/skills/shared/external/background-processing/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/background-processing/references/background-task-patterns.md +169 -317
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +290 -371
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/callkit-voip/references/callkit-patterns.md +175 -343
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +292 -381
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +33 -30
- package/pipeline/skills/shared/external/cloudkit-sync/references/cloudkit-patterns.md +227 -355
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +197 -346
- package/pipeline/skills/shared/external/contacts-framework/evals/evals.json +19 -21
- package/pipeline/skills/shared/external/contacts-framework/references/contacts-patterns.md +169 -308
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +226 -376
- package/pipeline/skills/shared/external/core-bluetooth/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/core-bluetooth/references/ble-patterns.md +257 -337
- package/pipeline/skills/shared/external/core-data/SKILL.md +292 -368
- package/pipeline/skills/shared/external/core-data/evals/evals.json +30 -27
- package/pipeline/skills/shared/external/core-motion/SKILL.md +235 -324
- package/pipeline/skills/shared/external/core-motion/evals/evals.json +31 -27
- package/pipeline/skills/shared/external/core-motion/references/motion-patterns.md +210 -310
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +292 -366
- package/pipeline/skills/shared/external/core-nfc/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/core-nfc/references/nfc-patterns.md +233 -329
- package/pipeline/skills/shared/external/coreml/SKILL.md +323 -367
- package/pipeline/skills/shared/external/coreml/evals/evals.json +24 -21
- package/pipeline/skills/shared/external/coreml/references/coreml-swift-integration.md +562 -565
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +253 -394
- package/pipeline/skills/shared/external/cryptokit/evals/evals.json +20 -18
- package/pipeline/skills/shared/external/cryptokit/references/cryptokit-patterns.md +299 -488
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +270 -323
- package/pipeline/skills/shared/external/debugging-instruments/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/debugging-instruments/references/instruments-guide.md +167 -315
- package/pipeline/skills/shared/external/debugging-instruments/references/lldb-patterns.md +140 -193
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +230 -353
- package/pipeline/skills/shared/external/device-integrity/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/device-integrity/references/device-integrity-patterns.md +159 -197
- package/pipeline/skills/shared/external/energykit/SKILL.md +225 -392
- package/pipeline/skills/shared/external/energykit/evals/evals.json +29 -28
- package/pipeline/skills/shared/external/energykit/references/energykit-patterns.md +174 -470
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +261 -383
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +25 -22
- package/pipeline/skills/shared/external/eventkit-calendar/references/eventkit-patterns.md +165 -268
- package/pipeline/skills/shared/external/healthkit/SKILL.md +252 -303
- package/pipeline/skills/shared/external/healthkit/evals/evals.json +24 -23
- package/pipeline/skills/shared/external/healthkit/references/healthkit-patterns.md +369 -523
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +233 -348
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +27 -22
- package/pipeline/skills/shared/external/homekit-matter/references/matter-commissioning.md +199 -305
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +368 -340
- package/pipeline/skills/shared/external/ios-accessibility/evals/evals.json +28 -27
- package/pipeline/skills/shared/external/ios-accessibility/references/a11y-patterns.md +314 -260
- package/pipeline/skills/shared/external/ios-accessibility/references/media-accessibility.md +97 -67
- package/pipeline/skills/shared/external/ios-accessibility/references/nutrition-labels.md +165 -101
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +258 -371
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +23 -23
- package/pipeline/skills/shared/external/ios-localization/references/formatstyle-locale.md +283 -491
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +313 -440
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +265 -341
- package/pipeline/skills/shared/external/ios-networking/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/ios-networking/references/background-websocket.md +425 -652
- package/pipeline/skills/shared/external/ios-networking/references/file-storage-patterns.md +143 -285
- package/pipeline/skills/shared/external/ios-networking/references/lightweight-clients.md +93 -53
- package/pipeline/skills/shared/external/ios-networking/references/network-framework.md +231 -456
- package/pipeline/skills/shared/external/ios-networking/references/urlsession-patterns.md +517 -784
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +265 -393
- package/pipeline/skills/shared/external/ios-simulator/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/ios-simulator/references/simctl-commands.md +177 -270
- package/pipeline/skills/shared/external/live-activities/SKILL.md +318 -360
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/live-activities/references/activitykit-patterns.md +478 -710
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +295 -267
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +28 -24
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-corelocation-patterns.md +378 -532
- package/pipeline/skills/shared/external/mapkit-location/references/mapkit-patterns.md +397 -499
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +165 -348
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +26 -23
- package/pipeline/skills/shared/external/metrickit-diagnostics/references/metrickit-patterns.md +123 -130
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +189 -315
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +22 -21
- package/pipeline/skills/shared/external/musickit-audio/references/musickit-patterns.md +181 -270
- package/pipeline/skills/shared/external/natural-language/SKILL.md +188 -340
- package/pipeline/skills/shared/external/natural-language/evals/evals.json +21 -21
- package/pipeline/skills/shared/external/natural-language/references/translation-patterns.md +171 -225
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +258 -392
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +30 -29
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +164 -231
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +312 -344
- package/pipeline/skills/shared/external/pdfkit/evals/evals.json +19 -19
- package/pipeline/skills/shared/external/pdfkit/references/pdfkit-patterns.md +413 -624
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +242 -358
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +25 -21
- package/pipeline/skills/shared/external/pencilkit-drawing/references/pencilkit-patterns.md +161 -226
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +282 -400
- package/pipeline/skills/shared/external/permissionkit/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/permissionkit/references/permissionkit-patterns.md +237 -350
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +276 -325
- package/pipeline/skills/shared/external/photos-camera-media/references/av-playback.md +299 -545
- package/pipeline/skills/shared/external/photos-camera-media/references/camera-capture.md +344 -588
- package/pipeline/skills/shared/external/photos-camera-media/references/image-loading-caching.md +316 -660
- package/pipeline/skills/shared/external/photos-camera-media/references/photokit-patterns.md +270 -416
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +312 -340
- package/pipeline/skills/shared/external/push-notifications/evals/evals.json +27 -26
- package/pipeline/skills/shared/external/push-notifications/references/notification-patterns.md +328 -485
- package/pipeline/skills/shared/external/push-notifications/references/rich-notifications.md +327 -560
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +218 -410
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/realitykit-ar/references/realitykit-patterns.md +221 -348
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +222 -393
- package/pipeline/skills/shared/external/shareplay-activities/evals/evals.json +23 -24
- package/pipeline/skills/shared/external/shareplay-activities/references/shareplay-patterns.md +280 -420
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +217 -421
- package/pipeline/skills/shared/external/speech-recognition/evals/evals.json +23 -26
- package/pipeline/skills/shared/external/speech-recognition/references/speechanalyzer-patterns.md +133 -125
- package/pipeline/skills/shared/external/storekit/SKILL.md +228 -204
- package/pipeline/skills/shared/external/storekit/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/storekit/references/app-review-guidelines.md +98 -109
- package/pipeline/skills/shared/external/storekit/references/core-patterns.md +298 -242
- package/pipeline/skills/shared/external/storekit/references/storekit-advanced.md +356 -649
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +274 -399
- package/pipeline/skills/shared/external/swift-api-design-guidelines/evals/evals.json +22 -24
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/argument-labels-and-parameters.md +107 -108
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/conventions-and-special-rules.md +93 -165
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/naming-and-clarity.md +99 -137
- package/pipeline/skills/shared/external/swift-api-design-guidelines/references/side-effects-and-mutating-pairs.md +77 -120
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +334 -350
- package/pipeline/skills/shared/external/swift-architecture/evals/evals.json +22 -22
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +208 -394
- package/pipeline/skills/shared/external/swift-charts/evals/evals.json +27 -30
- package/pipeline/skills/shared/external/swift-charts/references/charts-patterns.md +351 -762
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +339 -343
- package/pipeline/skills/shared/external/swift-codable/evals/evals.json +20 -20
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +303 -351
- package/pipeline/skills/shared/external/swift-concurrency/evals/evals.json +27 -24
- package/pipeline/skills/shared/external/swift-concurrency/references/approachable-concurrency.md +65 -80
- package/pipeline/skills/shared/external/swift-concurrency/references/async-algorithms.md +48 -84
- package/pipeline/skills/shared/external/swift-concurrency/references/bridging-interop.md +134 -79
- package/pipeline/skills/shared/external/swift-concurrency/references/concurrency-patterns.md +145 -167
- package/pipeline/skills/shared/external/swift-concurrency/references/diagnostics.md +62 -50
- package/pipeline/skills/shared/external/swift-concurrency/references/swiftui-concurrency.md +92 -121
- package/pipeline/skills/shared/external/swift-concurrency/references/synchronization-primitives.md +177 -241
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +258 -234
- package/pipeline/skills/shared/external/swift-language/SKILL.md +342 -382
- package/pipeline/skills/shared/external/swift-language/evals/evals.json +24 -27
- package/pipeline/skills/shared/external/swift-language/references/swift-attributes-interop.md +79 -56
- package/pipeline/skills/shared/external/swift-language/references/swift-patterns-extended.md +297 -340
- package/pipeline/skills/shared/external/swift-security/SKILL.md +180 -161
- package/pipeline/skills/shared/external/swift-security/evals/evals.json +25 -25
- package/pipeline/skills/shared/external/swift-security/references/biometric-authentication.md +314 -469
- package/pipeline/skills/shared/external/swift-security/references/certificate-trust.md +408 -476
- package/pipeline/skills/shared/external/swift-security/references/common-anti-patterns.md +260 -530
- package/pipeline/skills/shared/external/swift-security/references/compliance-owasp-mapping.md +270 -477
- package/pipeline/skills/shared/external/swift-security/references/credential-storage-patterns.md +573 -571
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-public-key.md +370 -441
- package/pipeline/skills/shared/external/swift-security/references/cryptokit-symmetric.md +332 -433
- package/pipeline/skills/shared/external/swift-security/references/keychain-access-control.md +346 -468
- package/pipeline/skills/shared/external/swift-security/references/keychain-fundamentals.md +352 -472
- package/pipeline/skills/shared/external/swift-security/references/keychain-item-classes.md +431 -432
- package/pipeline/skills/shared/external/swift-security/references/keychain-sharing.md +328 -425
- package/pipeline/skills/shared/external/swift-security/references/migration-legacy-stores.md +341 -579
- package/pipeline/skills/shared/external/swift-security/references/secure-enclave.md +396 -457
- package/pipeline/skills/shared/external/swift-security/references/testing-security-code.md +354 -614
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +188 -175
- package/pipeline/skills/shared/external/swift-testing/evals/evals.json +26 -24
- package/pipeline/skills/shared/external/swift-testing/references/testing-advanced.md +80 -84
- package/pipeline/skills/shared/external/swift-testing/references/testing-patterns.md +317 -433
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +392 -256
- package/pipeline/skills/shared/external/swiftdata/evals/evals.json +24 -24
- package/pipeline/skills/shared/external/swiftdata/references/core-data-coexistence.md +206 -402
- package/pipeline/skills/shared/external/swiftdata/references/indexing.md +59 -52
- package/pipeline/skills/shared/external/swiftdata/references/predicate-pitfalls.md +57 -33
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-advanced.md +354 -747
- package/pipeline/skills/shared/external/swiftdata/references/swiftdata-queries.md +300 -508
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +175 -226
- package/pipeline/skills/shared/external/swiftlint/references/adoption-and-configuration.md +141 -208
- package/pipeline/skills/shared/external/swiftlint/references/custom-rules-and-analyze.md +100 -109
- package/pipeline/skills/shared/external/swiftlint/references/plugins-run-scripts-and-integrations.md +159 -179
- package/pipeline/skills/shared/external/swiftlint/references/rule-reference.md +383 -18
- package/pipeline/skills/shared/external/swiftlint/references/rules-suppressions-and-baselines.md +143 -229
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +283 -366
- package/pipeline/skills/shared/external/swiftui-animation/references/animation-advanced.md +396 -608
- package/pipeline/skills/shared/external/swiftui-animation/references/core-animation-bridge.md +336 -385
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +239 -349
- package/pipeline/skills/shared/external/swiftui-gestures/references/gesture-patterns.md +228 -310
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +260 -249
- package/pipeline/skills/shared/external/swiftui-layout-components/references/form.md +92 -74
- package/pipeline/skills/shared/external/swiftui-layout-components/references/grids.md +112 -177
- package/pipeline/skills/shared/external/swiftui-layout-components/references/list.md +61 -64
- package/pipeline/skills/shared/external/swiftui-layout-components/references/scrollview.md +94 -134
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +193 -225
- package/pipeline/skills/shared/external/swiftui-liquid-glass/references/liquid-glass.md +173 -327
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +193 -168
- package/pipeline/skills/shared/external/swiftui-navigation/references/deeplinks.md +127 -150
- package/pipeline/skills/shared/external/swiftui-navigation/references/navigationstack.md +132 -133
- package/pipeline/skills/shared/external/swiftui-navigation/references/sheets.md +152 -117
- package/pipeline/skills/shared/external/swiftui-navigation/references/tabview.md +106 -140
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +316 -252
- package/pipeline/skills/shared/external/swiftui-patterns/references/architecture-patterns.md +341 -332
- package/pipeline/skills/shared/external/swiftui-patterns/references/deprecated-migration.md +547 -854
- package/pipeline/skills/shared/external/swiftui-patterns/references/design-polish.md +485 -537
- package/pipeline/skills/shared/external/swiftui-patterns/references/platform-and-sharing.md +417 -499
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +213 -376
- package/pipeline/skills/shared/external/swiftui-performance/references/demystify-swiftui-performance-wwdc23.md +86 -175
- package/pipeline/skills/shared/external/swiftui-performance/references/optimizing-swiftui-performance-instruments.md +89 -195
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-hangs-in-your-app.md +95 -182
- package/pipeline/skills/shared/external/swiftui-performance/references/understanding-improving-swiftui-performance.md +71 -149
- package/pipeline/skills/shared/external/swiftui-performance/references/wwdc-session-sources.md +21 -27
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +303 -295
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/hosting-migration.md +204 -387
- package/pipeline/skills/shared/external/swiftui-uikit-interop/references/representable-recipes.md +469 -683
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +140 -186
- package/pipeline/skills/shared/external/swiftui-webkit/references/loading-and-observation.md +75 -86
- package/pipeline/skills/shared/external/swiftui-webkit/references/local-content-and-custom-schemes.md +63 -60
- package/pipeline/skills/shared/external/swiftui-webkit/references/migration-and-fallbacks.md +69 -137
- package/pipeline/skills/shared/external/swiftui-webkit/references/navigation-and-javascript.md +95 -67
- package/pipeline/skills/shared/external/tipkit/SKILL.md +220 -335
- package/pipeline/skills/shared/external/tipkit/references/tipkit-patterns.md +356 -494
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +260 -375
- package/pipeline/skills/shared/external/vision-framework/references/vision-requests.md +393 -515
- package/pipeline/skills/shared/external/vision-framework/references/visionkit-scanner.md +363 -539
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +152 -310
- package/pipeline/skills/shared/external/weatherkit/references/weatherkit-patterns.md +288 -407
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +216 -288
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +414 -719
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +0 -39
|
@@ -1,439 +1,316 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: device-integrity
|
|
3
|
-
description: "
|
|
3
|
+
description: "DeviceCheck and App Attest: DCDevice tokens and per-device bits, DCAppAttestService key generation, attestation and assertions. Use when adding fraud prevention, detecting compromised devices, validating app authenticity with Apple's servers, protecting sensitive endpoints with attested requests, or designing backend device verification. Not for sign-in, OAuth, JWT sessions, TLS or pinning."
|
|
4
4
|
metadata:
|
|
5
|
-
source:
|
|
5
|
+
source: multi-agent-pipeline
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Device Integrity
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
simple flags (e.g., "claimed promo offer"). App Attest uses Secure Enclave keys
|
|
13
|
-
and Apple attestation to cryptographically prove app legitimacy on sensitive
|
|
14
|
-
requests.
|
|
10
|
+
Two frameworks let a backend decide whether a request really originates from
|
|
11
|
+
real Apple hardware running an unmodified build of your app:
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
- **DeviceCheck** (`DCDevice`) hands your server a token it can trade with Apple
|
|
14
|
+
for two developer-defined bits per device. Good for simple flags such as
|
|
15
|
+
"this phone already redeemed the trial".
|
|
16
|
+
- **App Attest** (`DCAppAttestService`) creates a hardware key inside the Secure
|
|
17
|
+
Enclave, has Apple certify it, and then signs chosen requests with it. Good
|
|
18
|
+
for cryptographic proof on sensitive calls.
|
|
17
19
|
|
|
18
|
-
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
- [App Attest Attestation Flow](#app-attest-attestation-flow)
|
|
22
|
-
- [App Attest Assertion Flow](#app-attest-assertion-flow)
|
|
23
|
-
- [Server Verification Guidance](#server-verification-guidance)
|
|
24
|
-
- [Error Handling](#error-handling)
|
|
25
|
-
- [Common Patterns](#common-patterns)
|
|
26
|
-
- [Common Mistakes](#common-mistakes)
|
|
27
|
-
- [Review Checklist](#review-checklist)
|
|
28
|
-
- [References](#references)
|
|
20
|
+
Server algorithms, retries, rejected-key recovery, an integration outline and
|
|
21
|
+
rollout advice live in the [patterns reference](references/device-integrity-patterns.md).
|
|
22
|
+
The `IntegrityError` enum used below is defined there too.
|
|
29
23
|
|
|
30
|
-
##
|
|
24
|
+
## DeviceCheck tokens (`DCDevice`, iOS 11+)
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
Apple's servers to read or set two per-device bits. Available on iOS 11+.
|
|
37
|
-
|
|
38
|
-
### Token Generation
|
|
26
|
+
`DCDevice` produces an ephemeral token that identifies the device to Apple.
|
|
27
|
+
Each token is single-use: create a fresh one for every server operation and
|
|
28
|
+
never cache it. The server forwards the token to Apple to read or change the
|
|
29
|
+
two bits.
|
|
39
30
|
|
|
40
31
|
```swift
|
|
41
32
|
import DeviceCheck
|
|
42
33
|
|
|
43
|
-
func
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
func sendJSON(_ fields: [String: String], to url: URL) async throws -> Bool {
|
|
35
|
+
var req = URLRequest(url: url)
|
|
36
|
+
req.httpMethod = "POST"
|
|
37
|
+
req.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
38
|
+
let payload = try JSONEncoder().encode(fields)
|
|
39
|
+
let (_, reply) = try await URLSession.shared.upload(for: req, from: payload)
|
|
40
|
+
return (reply as? HTTPURLResponse).map { $0.statusCode == 200 } ?? false
|
|
49
41
|
}
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
### Sending the Token to Your Server
|
|
53
42
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
let
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
request.httpMethod = "POST"
|
|
60
|
-
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
61
|
-
request.httpBody = try JSONEncoder().encode(["device_token": tokenString])
|
|
62
|
-
|
|
63
|
-
let (_, response) = try await URLSession.shared.data(for: request)
|
|
64
|
-
guard let httpResponse = response as? HTTPURLResponse,
|
|
65
|
-
httpResponse.statusCode == 200 else {
|
|
66
|
-
throw DeviceIntegrityError.serverVerificationFailed
|
|
67
|
-
}
|
|
43
|
+
func claimTrial(at endpoint: URL) async throws {
|
|
44
|
+
guard DCDevice.current.isSupported else { throw IntegrityError.deviceCheckUnsupported }
|
|
45
|
+
let token = try await DCDevice.current.generateToken() // new token every call
|
|
46
|
+
let accepted = try await sendJSON(["device_token": token.base64EncodedString()], to: endpoint)
|
|
47
|
+
if !accepted { throw IntegrityError.serverVerificationFailed }
|
|
68
48
|
}
|
|
69
49
|
```
|
|
70
50
|
|
|
71
|
-
###
|
|
72
|
-
|
|
73
|
-
Your server uses the device token to call Apple's DeviceCheck API endpoints:
|
|
74
|
-
|
|
75
|
-
| Endpoint | Purpose |
|
|
76
|
-
|----------|---------|
|
|
77
|
-
| `https://api.devicecheck.apple.com/v1/query_two_bits` | Read the two bits for a device |
|
|
78
|
-
| `https://api.devicecheck.apple.com/v1/update_two_bits` | Set the two bits for a device |
|
|
79
|
-
| `https://api.devicecheck.apple.com/v1/validate_device_token` | Validate a device token without reading bits |
|
|
51
|
+
### What the server does
|
|
80
52
|
|
|
81
|
-
|
|
82
|
-
portal, creating a signed JWT for each request.
|
|
53
|
+
All three calls live under `/v1/` on the DeviceCheck host:
|
|
83
54
|
|
|
84
|
-
|
|
85
|
-
|
|
55
|
+
| Path | Purpose |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `query_two_bits` | Returns the current bit values |
|
|
58
|
+
| `update_two_bits` | Changes one or both bits |
|
|
59
|
+
| `validate_device_token` | Confirms a token is real, bits untouched |
|
|
86
60
|
|
|
87
|
-
|
|
61
|
+
The server signs a JWT for each call with a DeviceCheck private key created in
|
|
62
|
+
the Developer portal. Hosts: `api.devicecheck.apple.com` for production traffic,
|
|
63
|
+
and `api.development.devicecheck.apple.com` only while testing.
|
|
88
64
|
|
|
89
|
-
|
|
90
|
-
they mean. Common uses:
|
|
65
|
+
### The two bits
|
|
91
66
|
|
|
92
|
-
|
|
93
|
-
|
|
67
|
+
Apple keeps two Booleans per device for each developer team, and you decide
|
|
68
|
+
what they mean. For example bit 0 might record "promo redeemed" and bit 1
|
|
69
|
+
"flagged for abuse". They survive deleting and reinstalling the app; only
|
|
70
|
+
your server can reset them through the API.
|
|
94
71
|
|
|
95
|
-
|
|
96
|
-
server API.
|
|
72
|
+
## App Attest (`DCAppAttestService`, iOS 14+)
|
|
97
73
|
|
|
98
|
-
|
|
74
|
+
App Attest ties a specific app instance on a specific device to a Secure
|
|
75
|
+
Enclave key. It runs in three phases:
|
|
99
76
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
77
|
+
1. **Key generation** - the Secure Enclave makes a key pair.
|
|
78
|
+
2. **Attestation** - Apple vouches that this key sits on real hardware inside
|
|
79
|
+
your app.
|
|
80
|
+
3. **Assertion** - sign chosen requests with the attested key.
|
|
104
81
|
|
|
105
|
-
|
|
106
|
-
1. **Key generation** -- create a key pair in the Secure Enclave.
|
|
107
|
-
2. **Attestation** -- Apple certifies the key belongs to a genuine Apple device running your app.
|
|
108
|
-
3. **Assertion** -- sign server requests with the attested key to prove ongoing legitimacy.
|
|
109
|
-
|
|
110
|
-
### Checking Support
|
|
82
|
+
### Support check
|
|
111
83
|
|
|
112
84
|
```swift
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
guard attestService.isSupported else {
|
|
118
|
-
// Fall back to DCDevice token or other risk assessment.
|
|
119
|
-
// App Attest is not available on simulators or all device models.
|
|
120
|
-
return
|
|
85
|
+
let attest = DCAppAttestService.shared
|
|
86
|
+
if !attest.isSupported {
|
|
87
|
+
// Fall back to a DCDevice token or another risk signal.
|
|
121
88
|
}
|
|
122
89
|
```
|
|
123
90
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
91
|
+
The service is unavailable in the Simulator and on some hardware. For app
|
|
92
|
+
extensions only Action, extensible single sign-on and watchOS extensions are
|
|
93
|
+
supported; treat any other extension type as unsupported even when
|
|
94
|
+
`isSupported` returns true.
|
|
127
95
|
|
|
128
|
-
|
|
96
|
+
### Key generation
|
|
129
97
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
attestation. If server verification fails, discard the `keyId` and generate a
|
|
98
|
+
Make one key for each signed-in account on a given device. Its private half never leaves
|
|
99
|
+
the Secure Enclave and the returned `keyId` is your only handle to it, so store
|
|
100
|
+
it and reuse it for that account on that device. Never share a key between
|
|
101
|
+
users, and avoid regenerating: every new key moves Apple's per-device key
|
|
102
|
+
count, which feeds risk metrics. Treat a `keyId` as usable only once your
|
|
103
|
+
server accepts its attestation; if verification fails, discard it and make a
|
|
137
104
|
new key before retrying.
|
|
138
105
|
|
|
139
106
|
```swift
|
|
140
107
|
import DeviceCheck
|
|
108
|
+
import Foundation
|
|
109
|
+
import Security
|
|
141
110
|
|
|
142
|
-
actor
|
|
111
|
+
actor AttestKeyStore {
|
|
143
112
|
private let service = DCAppAttestService.shared
|
|
144
|
-
private
|
|
113
|
+
private let accountID: String
|
|
114
|
+
var keyID: String?
|
|
145
115
|
|
|
146
|
-
|
|
147
|
-
func generateKeyIfNeeded() async throws -> String {
|
|
148
|
-
if let existingKeyId = loadKeyIdFromKeychain() {
|
|
149
|
-
self.keyId = existingKeyId
|
|
150
|
-
return existingKeyId
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let newKeyId = try await service.generateKey()
|
|
154
|
-
saveKeyIdToKeychain(newKeyId)
|
|
155
|
-
self.keyId = newKeyId
|
|
156
|
-
return newKeyId
|
|
157
|
-
}
|
|
116
|
+
init(accountID: String) { self.accountID = accountID }
|
|
158
117
|
|
|
159
|
-
|
|
118
|
+
private var keychainAccount: String { "attest-key.\(accountID)" }
|
|
160
119
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
let query: [String: Any] = [
|
|
120
|
+
var baseQuery: [String: Any] {
|
|
121
|
+
[
|
|
164
122
|
kSecClass as String: kSecClassGenericPassword,
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
kSecValueData as String: data,
|
|
168
|
-
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
123
|
+
kSecAttrService as String: Bundle.main.bundleIdentifier ?? "attest",
|
|
124
|
+
kSecAttrAccount as String: keychainAccount,
|
|
169
125
|
]
|
|
170
|
-
SecItemDelete(query as CFDictionary) // Remove old if exists
|
|
171
|
-
SecItemAdd(query as CFDictionary, nil)
|
|
172
126
|
}
|
|
173
127
|
|
|
174
|
-
|
|
175
|
-
let
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
kSecReturnData as String: true,
|
|
180
|
-
kSecMatchLimit as String: kSecMatchLimitOne
|
|
181
|
-
]
|
|
182
|
-
var result: AnyObject?
|
|
183
|
-
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
|
184
|
-
guard status == errSecSuccess, let data = result as? Data else { return nil }
|
|
185
|
-
return String(data: data, encoding: .utf8)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
**Important:** Generate the key once per user account on a device, persist that
|
|
191
|
-
account/device `keyId`, and keep the key count low. Generating unnecessary keys
|
|
192
|
-
pollutes App Attest risk metrics.
|
|
193
|
-
|
|
194
|
-
## App Attest Attestation Flow
|
|
195
|
-
|
|
196
|
-
Attestation proves that the key was generated on a genuine Apple device running
|
|
197
|
-
a legitimate instance of your app. You perform attestation once per key, then
|
|
198
|
-
store the verified public key and receipt on your server. The app stores the
|
|
199
|
-
`keyId` for future assertions after the server accepts the attestation.
|
|
200
|
-
|
|
201
|
-
### Client-Side Attestation
|
|
202
|
-
|
|
203
|
-
```swift
|
|
204
|
-
import DeviceCheck
|
|
205
|
-
import CryptoKit
|
|
206
|
-
|
|
207
|
-
extension AppAttestManager {
|
|
208
|
-
/// Attest the key with Apple. Send the attestation object to your server.
|
|
209
|
-
func attestKey() async throws -> Data {
|
|
210
|
-
guard let keyId else {
|
|
211
|
-
throw DeviceIntegrityError.keyNotGenerated
|
|
128
|
+
func generateKeyIfNeeded() async throws -> String {
|
|
129
|
+
if let keyID { return keyID }
|
|
130
|
+
if let stored = loadKeyID() {
|
|
131
|
+
keyID = stored
|
|
132
|
+
return stored
|
|
212
133
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
// 2. Hash the challenge (Apple requires a SHA-256 hash)
|
|
218
|
-
let challengeHash = Data(SHA256.hash(data: challenge))
|
|
219
|
-
|
|
220
|
-
// 3. Ask Apple to attest the key
|
|
221
|
-
let attestation = try await service.attestKey(keyId, clientDataHash: challengeHash)
|
|
222
|
-
|
|
223
|
-
// 4. Send the attestation object to your server for verification
|
|
224
|
-
try await sendAttestationToServer(
|
|
225
|
-
keyId: keyId,
|
|
226
|
-
attestation: attestation,
|
|
227
|
-
challenge: challenge
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
return attestation
|
|
134
|
+
let fresh = try await service.generateKey()
|
|
135
|
+
try saveKeyID(fresh)
|
|
136
|
+
keyID = fresh
|
|
137
|
+
return fresh
|
|
231
138
|
}
|
|
232
139
|
|
|
233
|
-
private func
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
140
|
+
private func saveKeyID(_ value: String) throws {
|
|
141
|
+
SecItemDelete(baseQuery as CFDictionary)
|
|
142
|
+
var item = baseQuery
|
|
143
|
+
item[kSecValueData as String] = Data(value.utf8)
|
|
144
|
+
item[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
|
|
145
|
+
let status = SecItemAdd(item as CFDictionary, nil)
|
|
146
|
+
guard status == errSecSuccess else { throw IntegrityError.keyNotGenerated }
|
|
237
147
|
}
|
|
238
148
|
|
|
239
|
-
private func
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
let payload: [String: String] = [
|
|
249
|
-
"key_id": keyId,
|
|
250
|
-
"attestation": attestation.base64EncodedString(),
|
|
251
|
-
"challenge": challenge.base64EncodedString()
|
|
252
|
-
]
|
|
253
|
-
request.httpBody = try JSONEncoder().encode(payload)
|
|
254
|
-
|
|
255
|
-
let (_, response) = try await URLSession.shared.data(for: request)
|
|
256
|
-
guard let httpResponse = response as? HTTPURLResponse,
|
|
257
|
-
httpResponse.statusCode == 200 else {
|
|
258
|
-
throw DeviceIntegrityError.attestationVerificationFailed
|
|
259
|
-
}
|
|
149
|
+
private func loadKeyID() -> String? {
|
|
150
|
+
var query = baseQuery
|
|
151
|
+
query[kSecReturnData as String] = true
|
|
152
|
+
query[kSecMatchLimit as String] = kSecMatchLimitOne
|
|
153
|
+
var found: CFTypeRef?
|
|
154
|
+
let status = SecItemCopyMatching(query as CFDictionary, &found)
|
|
155
|
+
guard status == errSecSuccess, let bytes = found as? Data else { return nil }
|
|
156
|
+
return String(data: bytes, encoding: .utf8)
|
|
260
157
|
}
|
|
261
158
|
}
|
|
262
159
|
```
|
|
263
160
|
|
|
264
|
-
|
|
161
|
+
Save, load and delete all build on the same `baseQuery`, so the account
|
|
162
|
+
attribute always carries the account suffix. The delete path in the
|
|
163
|
+
[rejected-key section](references/device-integrity-patterns.md#rejected-keys)
|
|
164
|
+
reuses it for that reason.
|
|
265
165
|
|
|
266
|
-
|
|
267
|
-
chain against Apple's App Attest root CA, checks Apple's nonce calculation, and
|
|
268
|
-
stores the verified public key and receipt for future assertion verification.
|
|
269
|
-
The attestation nonce is not `SHA256(challenge)` alone; it is
|
|
270
|
-
`SHA256(authData || SHA256(challenge))` and is compared with the credential
|
|
271
|
-
certificate extension `1.2.840.113635.100.8.2`. See
|
|
272
|
-
[references/device-integrity-patterns.md](references/device-integrity-patterns.md)
|
|
273
|
-
for the full server verification flow.
|
|
166
|
+
### Attestation
|
|
274
167
|
|
|
275
|
-
|
|
168
|
+
Attestation proves the key was created on a real device inside your genuine
|
|
169
|
+
app. Do it once per key. The server records the public key and receipt it verified;
|
|
170
|
+
the app keeps using the `keyId` for assertions once the server has accepted it.
|
|
276
171
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
server-issued, one-time challenge to prevent replay.
|
|
280
|
-
|
|
281
|
-
### Client-Side Assertion
|
|
172
|
+
Client order: ask the server for a one-time challenge, hash it with SHA-256,
|
|
173
|
+
call `attestKey(_:clientDataHash:)`, and upload the result.
|
|
282
174
|
|
|
283
175
|
```swift
|
|
284
|
-
import DeviceCheck
|
|
285
176
|
import CryptoKit
|
|
286
177
|
|
|
287
|
-
extension
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
throw DeviceIntegrityError.keyNotGenerated
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
let clientDataHash = Data(SHA256.hash(data: clientData))
|
|
178
|
+
extension AttestKeyStore {
|
|
179
|
+
func attest(challenge: Data, uploadTo url: URL) async throws {
|
|
180
|
+
let keyID = try await generateKeyIfNeeded()
|
|
181
|
+
let hash = Data(SHA256.hash(data: challenge))
|
|
182
|
+
let attestation = try await DCAppAttestService.shared.attestKey(keyID, clientDataHash: hash)
|
|
296
183
|
|
|
297
|
-
|
|
184
|
+
let accepted = try await sendJSON([
|
|
185
|
+
"key_id": keyID,
|
|
186
|
+
"attestation": attestation.base64EncodedString(),
|
|
187
|
+
"challenge": challenge.base64EncodedString(),
|
|
188
|
+
], to: url)
|
|
189
|
+
guard accepted else { throw IntegrityError.attestationVerificationFailed }
|
|
298
190
|
}
|
|
299
191
|
}
|
|
300
192
|
```
|
|
301
193
|
|
|
302
|
-
|
|
194
|
+
On the server: parse the CBOR object, walk its certificates up to Apple's
|
|
195
|
+
App Attest root, check the nonce, then persist the public key and receipt. The
|
|
196
|
+
nonce is **not** `SHA256(challenge)`. It is `SHA256(authData || SHA256(challenge))`,
|
|
197
|
+
and it has to match the octet string stored under OID `1.2.840.113635.100.8.2`
|
|
198
|
+
in the leaf certificate.
|
|
199
|
+
|
|
200
|
+
### Assertion
|
|
201
|
+
|
|
202
|
+
An assertion signs one sensitive request and embeds a server-issued, one-time
|
|
203
|
+
challenge so a captured request cannot be replayed. Call
|
|
204
|
+
`generateAssertion(_:clientDataHash:)` with the SHA-256 of encoded client data
|
|
205
|
+
that holds the challenge plus request context.
|
|
303
206
|
|
|
304
207
|
```swift
|
|
305
|
-
struct
|
|
208
|
+
struct SignedRequestContext: Codable {
|
|
306
209
|
let challenge: String
|
|
307
210
|
let method: String
|
|
308
211
|
let path: String
|
|
309
|
-
let
|
|
212
|
+
let bodyDigest: String
|
|
310
213
|
}
|
|
311
214
|
|
|
312
|
-
extension
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
let clientData = try JSONEncoder().encode(
|
|
322
|
-
AppAttestClientData(
|
|
323
|
-
challenge: challenge,
|
|
324
|
-
method: method,
|
|
325
|
-
path: url.path,
|
|
326
|
-
bodySHA256: bodyHash
|
|
327
|
-
)
|
|
215
|
+
extension AttestKeyStore {
|
|
216
|
+
func signed(_ original: URLRequest, body: Data, challenge: Data) async throws -> URLRequest {
|
|
217
|
+
var request = original
|
|
218
|
+
let keyID = try await generateKeyIfNeeded()
|
|
219
|
+
let context = SignedRequestContext(
|
|
220
|
+
challenge: challenge.base64EncodedString(),
|
|
221
|
+
method: request.httpMethod ?? "GET",
|
|
222
|
+
path: request.url?.path ?? "/",
|
|
223
|
+
bodyDigest: SHA256.hash(data: body).map { String(format: "%02x", $0) }.joined()
|
|
328
224
|
)
|
|
329
|
-
let
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
225
|
+
let encoded = try JSONEncoder().encode(context)
|
|
226
|
+
let assertion = try await DCAppAttestService.shared.generateAssertion(
|
|
227
|
+
keyID, clientDataHash: Data(SHA256.hash(data: encoded))
|
|
228
|
+
)
|
|
229
|
+
let headers = [
|
|
230
|
+
"X-App-Attest-Assertion": assertion.base64EncodedString(),
|
|
231
|
+
"X-App-Attest-Client-Data": encoded.base64EncodedString(),
|
|
232
|
+
]
|
|
233
|
+
headers.forEach { request.addValue($0.value, forHTTPHeaderField: $0.key) }
|
|
336
234
|
request.httpBody = body
|
|
337
|
-
|
|
338
|
-
return try await URLSession.shared.data(for: request)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
private func fetchAssertionChallenge() async throws -> String {
|
|
342
|
-
let url = serverURL.appending(path: "assert/challenge")
|
|
343
|
-
let (data, _) = try await URLSession.shared.data(from: url)
|
|
344
|
-
return String(decoding: data, as: UTF8.self)
|
|
235
|
+
return request
|
|
345
236
|
}
|
|
346
237
|
}
|
|
347
238
|
```
|
|
348
239
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
counter,
|
|
353
|
-
the
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
## Review Checklist
|
|
418
|
-
|
|
419
|
-
- [ ] `DCDevice` tokens generated per server operation and never cached for reuse
|
|
420
|
-
- [ ] `DCAppAttestService.isSupported` checked before use; unsupported devices and extension types have a fallback
|
|
421
|
-
- [ ] Key generated once per user account on each device and `keyId` persisted only for that app account/device
|
|
422
|
-
- [ ] Attestation performed once per key; server stores verified public key and receipt
|
|
423
|
-
- [ ] Server validates attestation certificate chain, App ID hash, environment `aaguid`, credential ID, and nonce `SHA256(authData || SHA256(challenge))`
|
|
424
|
-
- [ ] Assertions include one-time challenge plus request context; server verifies signature, RP ID, counter, challenge, and request binding
|
|
425
|
-
- [ ] Protected endpoints still enforce normal user authentication and entitlement authorization after App Attest passes
|
|
426
|
-
- [ ] `DCError` cases handled: `.serverUnavailable` retries attestation with the same key/hash; bad keys are discarded and regenerated
|
|
427
|
-
- [ ] App Attest environment entitlement and sandbox/production server routing are consistent
|
|
428
|
-
- [ ] Gradual rollout considered; feature flag in place for enabling/disabling
|
|
240
|
+
Fetch a new assertion challenge from your challenge endpoint for every request.
|
|
241
|
+
|
|
242
|
+
Server side: decode the CBOR assertion, inspect authenticator data and its
|
|
243
|
+
counter, hash the client data it received to get `clientDataHash` again, check
|
|
244
|
+
that the stored public key signed `SHA256(authenticatorData || clientDataHash)`,
|
|
245
|
+
and make sure the challenge and the request context both line up.
|
|
246
|
+
|
|
247
|
+
## Security boundaries
|
|
248
|
+
|
|
249
|
+
App Attest says "this request came from a genuine instance of the app". It
|
|
250
|
+
says nothing about who the user is. Sign-in, OAuth or JWT sessions, how API
|
|
251
|
+
tokens are designed, paid-feature and subscription checks, TLS and certificate
|
|
252
|
+
pinning all remain separate jobs. Keep enforcing authentication and authorization after the
|
|
253
|
+
attestation check passes. Hand-offs:
|
|
254
|
+
|
|
255
|
+
- Apple ID sign-in, passkeys, OAuth flows, credential state: `authentication`
|
|
256
|
+
- Keychain policy, Secure Enclave, pinning, storage hardening: `swift-security`
|
|
257
|
+
- Hashing and signing primitives: `cryptokit`
|
|
258
|
+
- Transport, request pipelines, TLS configuration: `ios-networking`
|
|
259
|
+
|
|
260
|
+
## Errors
|
|
261
|
+
|
|
262
|
+
| `DCError.Code` | Meaning | Action |
|
|
263
|
+
|---|---|---|
|
|
264
|
+
| `.serverUnavailable` | Apple's service is temporarily unreachable | Retry with exponential backoff |
|
|
265
|
+
| `.invalidKey` | Key already attested, assertion with an unattested key, or key rejected | Find out which, then recover |
|
|
266
|
+
| `.featureUnsupported` | Device or OS lacks App Attest | Use `DCDevice` tokens instead |
|
|
267
|
+
| `.invalidInput` | Malformed `clientDataHash` or `keyId` | Fix the caller |
|
|
268
|
+
|
|
269
|
+
When `attestKey` reports `.serverUnavailable`, try again later passing the
|
|
270
|
+
**same** `keyId` and the **same** `clientDataHash`. Any other attestation
|
|
271
|
+
failure means the key identifier is spent: drop it and make a fresh key first.
|
|
272
|
+
|
|
273
|
+
## Environment entitlement
|
|
274
|
+
|
|
275
|
+
The App Attest environment entitlement
|
|
276
|
+
(`com.apple.developer.devicecheck.appattest-environment`) takes `development`
|
|
277
|
+
for testing or `production` for release. Without it, development builds talk
|
|
278
|
+
to the App Attest sandbox. TestFlight, App Store and Enterprise Program builds
|
|
279
|
+
ignore the value and always use production.
|
|
280
|
+
|
|
281
|
+
## Common mistakes
|
|
282
|
+
|
|
283
|
+
- Generating a key on every launch instead of once per account per device.
|
|
284
|
+
- Caching or reusing a `DCDevice` token.
|
|
285
|
+
- No fallback path for unsupported devices or extension types.
|
|
286
|
+
- Trusting attestation on the client. Only the server can verify it.
|
|
287
|
+
- Signing just the raw body. Include the one-time challenge and request context.
|
|
288
|
+
- Checking the nonce as `SHA256(challenge)`.
|
|
289
|
+
- No replay defense. The backend has to accept each challenge once and keep the per-key counter moving forward.
|
|
290
|
+
- Mixing environments. Sandbox keys and receipts fail against production and the reverse.
|
|
291
|
+
- Regenerating on `invalidKey` without knowing why. Establish whether the key
|
|
292
|
+
was already attested, never attested, or rejected, and only replace it when
|
|
293
|
+
its state is known to be bad.
|
|
294
|
+
|
|
295
|
+
## Review checklist
|
|
296
|
+
|
|
297
|
+
- [ ] A new `DCDevice` token per operation; none cached
|
|
298
|
+
- [ ] `isSupported` checked, with a fallback for devices and extension types that lack App Attest
|
|
299
|
+
- [ ] One key per account per device; `keyId` stored only for that account on that device
|
|
300
|
+
- [ ] Keychain save, load and delete all use the same account-scoped query
|
|
301
|
+
- [ ] One attestation per key; server keeps the public key and receipt
|
|
302
|
+
- [ ] Attestation verifier covers: certificates, App ID hash, `aaguid` for the environment, credential ID, and a nonce of `SHA256(authData || SHA256(challenge))`
|
|
303
|
+
- [ ] Each assertion signs a challenge plus request context; the backend checks the signature, RP ID, counter, the challenge and how it binds to the request
|
|
304
|
+
- [ ] Protected endpoints still require user authentication and authorization
|
|
305
|
+
- [ ] `DCError` handled: `.serverUnavailable` retried with same key and hash, bad keys discarded and regenerated
|
|
306
|
+
- [ ] Environment entitlement agrees with sandbox or production server routing
|
|
307
|
+
- [ ] Rollout gated behind a feature flag
|
|
429
308
|
|
|
430
309
|
## References
|
|
431
310
|
|
|
432
|
-
-
|
|
433
|
-
- [DeviceCheck
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
- [Attestation Object Validation Guide](https://sosumi.ai/documentation/devicecheck/attestation-object-validation-guide)
|
|
439
|
-
- [App Attest Environment](https://sosumi.ai/documentation/bundleresources/entitlements/com.apple.developer.devicecheck.appattest-environment)
|
|
311
|
+
- [Patterns reference](references/device-integrity-patterns.md) - server verification steps, architecture, retries, rejected keys, rollout
|
|
312
|
+
- Apple documentation: [DeviceCheck](https://developer.apple.com/documentation/devicecheck),
|
|
313
|
+
[DCDevice](https://developer.apple.com/documentation/devicecheck/dcdevice),
|
|
314
|
+
[DCAppAttestService](https://developer.apple.com/documentation/devicecheck/dcappattestservice),
|
|
315
|
+
plus the DeviceCheck articles on app integrity, server-side validation,
|
|
316
|
+
attestation objects and the environment entitlement (see the patterns reference)
|