@coinbase/cdp-app-attest 0.0.94 → 0.0.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/README.md +0 -678
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coinbase/cdp-app-attest",
3
- "version": "0.0.94",
3
+ "version": "0.0.95",
4
4
  "type": "module",
5
5
  "description": "CDP native module for iOS App Attest and Android Play Integrity",
6
6
  "files": [
package/README.md DELETED
@@ -1,678 +0,0 @@
1
- # @coinbase/cdp-app-attest
2
-
3
- CDP native module for iOS App Attest, providing device attestation capabilities to verify that requests come from legitimate instances of your app running on genuine Apple devices.
4
-
5
- ## Features
6
-
7
- - ✅ **Simplified API** - Automatic key management, no manual key ID tracking
8
- - ✅ **Secure** - Keys stored in iOS Keychain, private keys in Secure Enclave
9
- - ✅ **iOS 14+** - Uses Apple's DCAppAttestService
10
- - ✅ Assertion-based security for sensitive operations
11
-
12
- ## Platform Support
13
-
14
- - **iOS**: iOS 14.0+ on physical devices (uses DCAppAttestService)
15
- - ⚠️ Not supported on iOS Simulator
16
- - ⚠️ Not supported in Expo Go (requires custom dev build)
17
- - **Android**: Play Integrity support is planned but not available in this release
18
-
19
- ## Installation
20
-
21
- ```bash
22
- npx expo install @coinbase/cdp-app-attest
23
- ```
24
-
25
- ## Setup
26
-
27
- ### iOS Configuration
28
-
29
- 1. Open your project in Xcode:
30
- ```bash
31
- open ios/YourApp.xcworkspace
32
- ```
33
-
34
- 2. Add the App Attest capability:
35
- - Select your app target
36
- - Go to **Signing & Capabilities**
37
- - Click **+ Capability**
38
- - Add **App Attest**
39
-
40
- That's it! No additional configuration needed.
41
-
42
- ## Understanding Attestation vs Assertion
43
-
44
- iOS App Attest uses two different operations:
45
-
46
- ### **Attestation** (One-Time Registration)
47
- - **What:** Generates a cryptographic key and proves it's in your device's Secure Enclave
48
- - **When:** Once per app installation (before making sensitive requests)
49
- - **Signed by:** Apple
50
- - **Flow:**
51
- 1. Generate server challenge
52
- 2. Call `attest(challenge)` - creates key + attestation object
53
- 3. Send attestation to backend `/attestation/register` endpoint
54
- 4. Backend verifies with Apple and stores your device's public key
55
- - **Result:** Your device's public key is registered with the backend
56
-
57
- ### **Assertion** (Per-Request Proof)
58
- - **What:** Cryptographically signed proof that a request comes from your attested device
59
- - **When:** Every sensitive API request (sign transactions, export keys, etc.)
60
- - **Signed by:** Your device's Secure Enclave using the private key
61
- - **Flow:**
62
- 1. Call `createAssertion(clientData)` - signs request data
63
- 2. Add assertion to request headers: `X-App-Attestation-Assertion` + `X-App-Attestation-Key-ID`
64
- 3. Backend validates signature using stored public key
65
- - **Result:** Backend confirms request is from genuine, attested device
66
-
67
- **Think of it like:**
68
- - **Attestation** = Getting a passport (register your identity once)
69
- - **Assertion** = Signing documents with your passport (prove it's you for each transaction)
70
-
71
- ## Usage
72
-
73
- ### Basic Example
74
-
75
- ```typescript
76
- import * as AppAttest from '@coinbase/cdp-app-attest';
77
-
78
- // 1. Check if device supports attestation
79
- const supported = await AppAttest.isSupported();
80
- console.log('Attestation supported:', supported);
81
-
82
- if (supported) {
83
- // ONE-TIME: Attest the app installation
84
- // Get a challenge from your server (32+ bytes, base64-encoded)
85
- const challenge = await yourBackend.getChallenge();
86
-
87
- // Attest with the challenge (generates key automatically)
88
- const result = await AppAttest.attest(challenge);
89
- // Result: { ios: { keyId: "...", attestation: "...", bundleId: "..." } }
90
-
91
- // Send attestation to backend for verification and public key storage
92
- await yourBackend.exchangeAttestation({
93
- challenge: challenge,
94
- ...result.ios // Send iOS attestation data
95
- });
96
- console.log('Device attested and public key registered!');
97
-
98
- // ONGOING: Generate assertions for sensitive operations
99
- const clientData = btoa('POST||/v2/wallets/sign-transaction');
100
- const assertion = await AppAttest.createAssertion(clientData);
101
- // Result: { ios: { assertion: "...", keyId: "..." } }
102
-
103
- // Include assertion in request headers
104
- await fetch('https://api.example.com/v2/wallets/sign-transaction', {
105
- method: 'POST',
106
- headers: {
107
- 'X-App-Attestation-Assertion': assertion.ios.assertion,
108
- 'X-App-Attestation-Key-ID': assertion.ios.keyId,
109
- 'Content-Type': 'application/json',
110
- },
111
- body: JSON.stringify({ /* transaction data */ }),
112
- });
113
- }
114
- ```
115
-
116
- ### Complete Attestation Flow
117
-
118
- ```typescript
119
- import * as AppAttest from '@coinbase/cdp-app-attest';
120
-
121
- async function setupAttestation() {
122
- // Check support
123
- const supported = await AppAttest.isSupported();
124
- if (!supported) {
125
- console.log('App Attest not supported on this device');
126
- return false;
127
- }
128
-
129
- try {
130
- // ONE-TIME: Attest the app instance
131
- // This generates a key (stored in Keychain) and creates an attestation
132
- // object signed by Apple that proves this is a legitimate installation
133
- const challenge = await yourBackend.getChallenge();
134
- const result = await AppAttest.attest(challenge);
135
-
136
- // result: { ios: { keyId: "...", attestation: "...", bundleId: "..." } }
137
-
138
- // Send to backend for verification and public key storage
139
- await yourBackend.exchangeAttestation({
140
- challenge: challenge,
141
- keyId: result.ios.keyId,
142
- attestation: result.ios.attestation,
143
- bundleId: result.ios.bundleId,
144
- });
145
-
146
- console.log('✅ Device attested and public key registered!');
147
- return true;
148
- } catch (error) {
149
- console.error('❌ Attestation failed:', error);
150
- return false;
151
- }
152
- }
153
-
154
- async function makeAttestedRequest(method: string, path: string, body: any) {
155
- try {
156
- // ONGOING: Generate assertion for each sensitive request
157
- // Client data should match backend expectation (e.g., "METHOD||PATH")
158
- const clientData = btoa(`${method}||${path}`);
159
- const result = await AppAttest.createAssertion(clientData);
160
-
161
- // result: { ios: { assertion: "...", keyId: "..." } }
162
-
163
- // Include assertion in request headers
164
- const response = await fetch(`https://api.example.com${path}`, {
165
- method: method,
166
- headers: {
167
- 'X-App-Attestation-Assertion': result.ios.assertion,
168
- 'X-App-Attestation-Key-ID': result.ios.keyId,
169
- 'Content-Type': 'application/json',
170
- },
171
- body: JSON.stringify(body),
172
- });
173
-
174
- if (!response.ok) {
175
- throw new Error(`Request failed: ${response.status}`);
176
- }
177
-
178
- return await response.json();
179
- } catch (error) {
180
- console.error('❌ Attested request failed:', error);
181
- throw error;
182
- }
183
- }
184
- ```
185
-
186
- ## When to Re-Attest
187
-
188
- You need to re-attest when:
189
-
190
- - **App is installed for the first time** - No key exists yet
191
- - **Backend reports "key not found"** - Stored public key expired or was lost
192
- - **App is reinstalled** - Keys are automatically cleared
193
- - **Key is manually cleared** - After calling `clearAttestation()` for testing/debugging
194
-
195
- You do **NOT** need to re-attest:
196
-
197
- - **On every app launch** - Key persists in Keychain across launches
198
- - **After app updates** - Key survives app updates
199
- - **After device restart** - Key is stored in Keychain (persistent storage)
200
- - **For every request** - Use `createAssertion()` for ongoing requests
201
-
202
- ## API Reference
203
-
204
- ### `isSupported(): Promise<boolean>`
205
-
206
- Check if device attestation is supported on this device.
207
-
208
- **Returns:** `Promise<boolean>` - `true` if App Attest is available
209
-
210
- **Example:**
211
- ```typescript
212
- const supported = await AppAttest.isSupported();
213
- if (!supported) {
214
- // Fallback to non-attested flow
215
- }
216
- ```
217
-
218
- ---
219
-
220
- ### `attest(challenge: string): Promise<AttestationResult>`
221
-
222
- Attest the app instance with a server-provided challenge.
223
-
224
- This method automatically:
225
- - Generates a new cryptographic key on first use (stored in iOS Keychain)
226
- - Reuses the cached key on subsequent calls
227
- - Creates an attestation object signed by Apple
228
-
229
- **⚠️ Important:** Each key can only be attested **once**. Subsequent calls with the same key will fail. After successful attestation, use `createAssertion()` for ongoing requests.
230
-
231
- **Parameters:**
232
- - `challenge` (string) - Base64-encoded challenge from your server (minimum 32 bytes)
233
-
234
- **Returns:** `Promise<AttestationResult>`
235
- ```typescript
236
- interface AttestationResult {
237
- ios: {
238
- keyId: string; // Key identifier (UUID)
239
- attestation: string; // Base64-encoded attestation object
240
- bundleId: string; // Bundle identifier (e.g., "com.example.app")
241
- };
242
- }
243
- ```
244
-
245
- **Response Format:**
246
- ```typescript
247
- {
248
- ios: {
249
- keyId: "abc123def456...",
250
- attestation: "b2F1dGhEYXRh...",
251
- bundleId: "com.example.app"
252
- }
253
- }
254
- ```
255
-
256
- **Throws:**
257
- - `Error` if device doesn't support App Attest
258
- - `Error` if key was already attested (error code 2)
259
- - `Error` if challenge is invalid
260
-
261
- **Example:**
262
- ```typescript
263
- try {
264
- const challenge = await getServerChallenge();
265
- const result = await AppAttest.attest(challenge);
266
-
267
- // Send to backend for verification and public key storage
268
- await sendToBackend({
269
- challenge: challenge,
270
- keyId: result.ios.keyId,
271
- attestation: result.ios.attestation,
272
- bundleId: result.ios.bundleId,
273
- });
274
- } catch (error) {
275
- console.error('Attestation failed:', error);
276
- }
277
- ```
278
-
279
- ---
280
-
281
- ### `createAssertion(clientData: string): Promise<AssertionResult>`
282
-
283
- Generate a cryptographic assertion for an API request.
284
-
285
- Must call `attest()` at least once before using this method to ensure a key has been generated and attested.
286
-
287
- **Parameters:**
288
- - `clientData` (string) - Base64-encoded data to sign (typically request payload hash)
289
-
290
- **Returns:** `Promise<AssertionResult>`
291
- ```typescript
292
- interface AssertionResult {
293
- ios: {
294
- assertion: string; // Base64-encoded assertion
295
- keyId: string; // Key identifier (same as from attest())
296
- };
297
- }
298
- ```
299
-
300
- **Response Format:**
301
- ```typescript
302
- {
303
- ios: {
304
- assertion: "YXNzZXJ0aW9u...",
305
- keyId: "abc123def456..."
306
- }
307
- }
308
- ```
309
-
310
- **Throws:**
311
- - `Error` if no attested key exists (call `attest()` first)
312
- - `Error` if device doesn't support App Attest
313
-
314
- **Example:**
315
- ```typescript
316
- // Generate assertion for sensitive request
317
- const clientData = btoa('POST||/v2/wallets/sign-transaction');
318
- const result = await AppAttest.createAssertion(clientData);
319
-
320
- // Include assertion in request headers
321
- await fetch('https://api.example.com/v2/wallets/sign-transaction', {
322
- method: 'POST',
323
- headers: {
324
- 'X-App-Attestation-Assertion': result.ios.assertion,
325
- 'X-App-Attestation-Key-ID': result.ios.keyId,
326
- 'Content-Type': 'application/json',
327
- },
328
- body: JSON.stringify({ /* transaction data */ }),
329
- });
330
- ```
331
-
332
- ---
333
-
334
- ### `confirmRegistration(keyId: string): Promise<void>`
335
-
336
- Mark attestation as successfully registered with your backend.
337
-
338
- Call this only after your backend confirms the attestation exchange succeeded.
339
- This stores a local flag so your app can avoid re-attesting unnecessarily.
340
-
341
- **Parameters:**
342
- - `keyId` (string) - The key ID that was registered with the backend
343
-
344
- **Returns:** `Promise<void>`
345
-
346
- **Example:**
347
- ```typescript
348
- const attestation = await AppAttest.attest(challenge);
349
- await backend.exchangeAttestation(attestation.ios);
350
- await AppAttest.confirmRegistration(attestation.ios.keyId);
351
- ```
352
-
353
- ---
354
-
355
- ### `getRegisteredKeyId(): Promise<string | null>`
356
-
357
- Check whether this device's attestation has been confirmed as registered
358
- with your backend.
359
-
360
- Returns the stored key ID if registration was confirmed, otherwise `null`.
361
-
362
- **Returns:** `Promise<string | null>`
363
-
364
- **Example:**
365
- ```typescript
366
- const registeredKeyId = await AppAttest.getRegisteredKeyId();
367
- if (registeredKeyId) {
368
- console.log('Already registered:', registeredKeyId);
369
- } else {
370
- // Perform attestation flow
371
- }
372
- ```
373
-
374
- ---
375
-
376
- ### `clearAttestation(): Promise<void>`
377
-
378
- Clears all attestation state, allowing re-attestation with a new key.
379
-
380
- **When to use:**
381
- - Backend returns "Attestation key not found" (public key expired or lost)
382
- - Backend's stored public key has expired
383
- - Testing/debugging scenarios
384
-
385
- **Important:** After clearing attestation, you MUST call `attest()` again to generate a new key and perform fresh attestation. Until then, `createAssertion()` will fail.
386
-
387
- This clears both:
388
- - Swift's internal keyId (used for generating assertions)
389
- - SDK's registration confirmation flag (indicates backend has the public key)
390
-
391
- **Returns:** `Promise<void>`
392
-
393
- **Throws:**
394
- - `Error` if the native module doesn't support clearing
395
-
396
- **Example:**
397
- ```typescript
398
- // Handle "key not found" error from backend
399
- try {
400
- const assertion = await AppAttest.createAssertion(clientData);
401
- await api.sendTransaction({ assertion });
402
- } catch (error) {
403
- if (error.message.includes('Attestation key not found')) {
404
- console.log('Public key not found on backend - re-attesting');
405
-
406
- // Clear the old key
407
- await AppAttest.clearAttestation();
408
-
409
- // Re-attest with new key
410
- const challenge = await getServerChallenge();
411
- const attestation = await AppAttest.attest(challenge);
412
- await sendToBackend({ challenge, ...attestation });
413
-
414
- // Retry the original operation
415
- const newAssertion = await AppAttest.createAssertion(clientData);
416
- await api.sendTransaction({ assertion: newAssertion });
417
- }
418
- }
419
-
420
- // Explicit re-attestation flow
421
- async function forceReAttestation() {
422
- console.log('Clearing existing key and re-attesting...');
423
-
424
- // 1. Clear the old key
425
- await AppAttest.clearAttestation();
426
-
427
- // 2. Get new challenge
428
- const challenge = await getServerChallenge();
429
-
430
- // 3. Attest (generates new key)
431
- const attestation = await AppAttest.attest(challenge);
432
-
433
- // 4. Exchange with backend
434
- await sendToBackend({ challenge, ...attestation });
435
-
436
- console.log('Re-attestation complete!');
437
- }
438
- ```
439
-
440
- ## Key Management
441
-
442
- ### Understanding the Key Architecture
443
-
444
- App Attest uses a cryptographic key pair with three components:
445
-
446
- | Component | Location | Purpose | Who Accesses |
447
- |-----------|----------|---------|--------------|
448
- | **Key ID** | iOS Keychain (in app) | Identifier to reference the key | Your app |
449
- | **Private Key** | Secure Enclave (hardware) | Signs attestations/assertions | iOS only (never extractable) |
450
- | **Public Key** | Backend server | Verifies signatures | Backend |
451
-
452
- **Important:** The private key never leaves the device and can't be extracted, even by your app. iOS uses it internally to create signatures.
453
-
454
- ### Automatic Key Storage
455
-
456
- Keys are automatically managed:
457
- - ✅ Generated on first `attest()` call
458
- - ✅ Key ID stored in iOS Keychain (secure, persistent)
459
- - ✅ Private key stored in Secure Enclave (hardware-backed, never extractable)
460
- - ✅ Key ID reused for subsequent `createAssertion()` calls
461
- - ✅ Persisted across app launches and updates
462
- - ❌ **Not** persisted across app reinstallation (by design)
463
-
464
- ### What Each Component Needs
465
-
466
- **Your App (SDK):**
467
- - Stores: Key ID only (in iOS Keychain)
468
- - Never has access to: Private key (locked in Secure Enclave)
469
- - Uses: Key ID to call `createAssertion()`
470
-
471
- **Backend:**
472
- - Stores: Public key (extracted from attestation object)
473
- - Never has access to: Private key
474
- - Uses: Public key to verify assertion signatures
475
-
476
- **iOS Secure Enclave:**
477
- - Stores: Private key (hardware-backed, never extractable)
478
- - Used by: iOS internally to sign attestations and assertions
479
- - Never exposed to: The app or the backend
480
-
481
- ### Key Lifecycle
482
-
483
- ```
484
- App Install
485
-
486
- First attest() call → Generates key pair
487
-
488
- ├─ Key ID → Stored in Keychain (accessible to app)
489
- ├─ Private Key → Stored in Secure Enclave (never extractable)
490
- └─ Public Key → Sent to backend in attestation (verified & stored)
491
-
492
- createAssertion() calls → iOS uses private key to sign (app provides Key ID)
493
-
494
- App Update → Key survives ✅
495
-
496
- App Reinstall → Key is lost ❌ (start over)
497
- ```
498
-
499
- ## Error Handling
500
-
501
- ### Common Errors
502
-
503
- **Error Code 2 (DCAppAttestErrorInvalidKey)**
504
- ```typescript
505
- // Key was already attested
506
- try {
507
- await AppAttest.attest(challenge);
508
- } catch (error) {
509
- if (error.message.includes('error 2')) {
510
- // Option 1: Use createAssertion() for ongoing requests
511
- console.log('Key already attested, use assertions for ongoing requests');
512
- const assertion = await AppAttest.createAssertion(clientData);
513
-
514
- // Option 2: If you need to re-attest (e.g., backend lost public key),
515
- // clear the key first
516
- await AppAttest.clearAttestation();
517
- const newAttestation = await AppAttest.attest(newChallenge);
518
- }
519
- }
520
- ```
521
-
522
- **Not Supported**
523
- ```typescript
524
- const supported = await AppAttest.isSupported();
525
- if (!supported) {
526
- // Device doesn't support App Attest
527
- // - iOS version < 14.0
528
- // - Running on simulator
529
- // - Running in Expo Go
530
- }
531
- ```
532
-
533
- **No Key Found**
534
- ```typescript
535
- try {
536
- await AppAttest.createAssertion(data);
537
- } catch (error) {
538
- if (error.message.includes('No attestation key found')) {
539
- // Need to call attest() first
540
- await AppAttest.attest(challenge);
541
- }
542
- }
543
- ```
544
-
545
- ## Testing
546
-
547
- ### Development Builds
548
-
549
- App Attest works in development builds but uses Apple's **development environment**. Be aware:
550
- - ✅ Works on physical devices
551
- - ❌ Does NOT work on simulators
552
- - ⚠️ TestFlight uses a different environment than production
553
- - ⚠️ Development attestations can't be verified in production
554
-
555
- ### Resetting Keys for Testing
556
-
557
- For testing, you may need to reset keys between test runs. Use the public `clearAttestation()` method:
558
-
559
- ```typescript
560
- import * as AppAttest from '@coinbase/cdp-app-attest';
561
-
562
- // Clear the key to allow re-attestation
563
- await AppAttest.clearAttestation();
564
- console.log('Key cleared - can attest again');
565
-
566
- // Now you can attest with a new key
567
- const challenge = await getServerChallenge();
568
- const attestation = await AppAttest.attest(challenge);
569
- ```
570
-
571
- **Note:** `clearAttestation()` is now part of the public API and can be used in production for legitimate re-attestation scenarios (e.g., when backend's public key expires).
572
-
573
- ## Architecture Details
574
-
575
- ### Key Storage: Keychain vs UserDefaults
576
-
577
- **What we store:** Key ID only (a UUID-like identifier string)
578
-
579
- **Where we store it:** iOS Keychain
580
-
581
- **Why Keychain instead of UserDefaults?**
582
- - ✅ Best practice for persistent identifiers
583
- - ✅ More secure by default
584
- - ✅ Survives app updates (but not reinstalls, by design)
585
- - ✅ Professional implementation standard
586
-
587
- **Could we use UserDefaults?** Yes! Apple's documentation says to store the Key ID in "persistent storage, for example by writing it to a file." Both Keychain and UserDefaults qualify as persistent storage.
588
-
589
- **Is the Key ID a secret?** No. It's just an identifier. Even if exposed, an attacker can't use it without the physical device's Secure Enclave containing the private key.
590
-
591
- ### What The Backend Must Store
592
-
593
- During attestation verification, the backend **must** extract and store:
594
-
595
- 1. **Public Key** - From the attestation's credCert (x5c certificate)
596
- 2. **Counter** - Starts at 0, increments with each assertion (prevents replay attacks)
597
- 3. **Project/App Association** - Link this key to your app/project
598
-
599
- Without storing the public key and counter, the backend can't verify assertions later!
600
-
601
- ```javascript
602
- // Backend stores per attested key:
603
- {
604
- projectId: "project123",
605
- keyId: "abc123def456...", // From iOS attestation
606
- publicKey: "MFkwEwYHKoZI...", // Extracted from attestation credCert
607
- counter: 5, // Updated with each assertion to prevent replay
608
- createdAt: "2026-02-05T10:30:00Z",
609
- expiresAt: "2026-05-05T10:30:00Z"
610
- }
611
- ```
612
-
613
- **Counter validation is critical:**
614
- - Each assertion includes a counter value
615
- - Backend MUST verify: `assertion.counter > stored.counter`
616
- - Backend MUST update stored counter after successful validation
617
- - Without this, attackers can replay old assertions
618
-
619
- ## Backend Verification
620
-
621
- The backend must verify both attestations and assertions. See Apple's documentation:
622
- - [Validating Apps That Connect to Your Server](https://developer.apple.com/documentation/devicecheck/validating-apps-that-connect-to-your-server)
623
- - [Assessing Fraud Risk](https://developer.apple.com/documentation/devicecheck/assessing-fraud-risk)
624
-
625
- ## Important Notes
626
-
627
- ### Device Requirements
628
- - ✅ Physical iOS device (iPhone, iPad)
629
- - ✅ iOS 14.0 or later
630
- - ❌ iOS Simulator (not supported by Apple)
631
- - ❌ Expo Go (requires custom dev build)
632
-
633
- ### Production Considerations
634
- - Keys are tied to the app installation
635
- - Each key can only be attested **once**
636
- - Keys don't survive app reinstallation
637
- - TestFlight uses different attestation environment
638
- - Key generation is automatic (handled by the SDK when calling `attest()`)
639
-
640
- ### Security Best Practices
641
- - Always verify attestations on your backend
642
- - Use unique, one-time challenges
643
- - Challenges should be at least 32 bytes
644
- - Store attested key IDs server-side
645
- - Monitor backend logs for suspicious patterns (same user attesting multiple times)
646
-
647
- ## Troubleshooting
648
-
649
- **"App Attest is not supported on this device"**
650
- - Check iOS version (requires 14.0+)
651
- - Ensure using physical device (not simulator)
652
- - Verify App Attest capability is enabled in Xcode
653
-
654
- **"Failed to attest key: error 2"**
655
- - Key was already attested
656
- - Use `createAssertion()` for ongoing requests
657
- - To re-attest: call `clearAttestation()` first, then `attest()` with a new challenge
658
-
659
- **"No attestation key found"**
660
- - Call `attest()` before `createAssertion()`
661
- - Key may have been lost (app reinstall)
662
- - Start attestation flow from beginning
663
-
664
- **"Attestation key not found" (from backend)**
665
- - Backend's public key expired or was lost
666
- - Clear the key: `await AppAttest.clearAttestation()`
667
- - Re-attest: `await AppAttest.attest(newChallenge)`
668
- - Send new attestation to backend for verification
669
-
670
- ## Related Documentation
671
-
672
- - [Apple's App Attest Guide](https://developer.apple.com/documentation/devicecheck/establishing-your-app-s-integrity)
673
- - [Expo Modules Documentation](https://docs.expo.dev/modules/)
674
- - [@coinbase/cdp-core Integration Guide](../core/README.md)
675
-
676
- ## License
677
-
678
- Apache-2.0