|
| 1 | +# App Attestation (GUI client) |
| 2 | + |
| 3 | +Post-implementation architecture for **application-level** attestation in **edge-react-gui**. The info server issues challenges, verifies platform attestations, mints tokens, and gates signing endpoints; see **edge-info-server** `docs/APP_ATTESTATION.md` for the server side. |
| 4 | + |
| 5 | +## Goals |
| 6 | + |
| 7 | +- On supported physical devices, prove this install is the genuine Edge app (`co.edgesecure.app`). |
| 8 | +- Keep a short-lived attestation JWT cached in JS and attach it to info-server signing calls that need it. |
| 9 | +- Never block app boot: attestation is best-effort. If unsupported, offline, or slow, gated plugins simply omit the header and the **info server** decides (403 when that provider requires attestation). |
| 10 | + |
| 11 | +## High-level flow |
| 12 | + |
| 13 | +```mermaid |
| 14 | +sequenceDiagram |
| 15 | + participant Boot as app.ts |
| 16 | + participant Net as initInfoServer |
| 17 | + participant Eng as util/attestation.ts |
| 18 | + participant Nat as EdgeAttestation native |
| 19 | + participant Info as edge-info-server |
| 20 | + participant Plugin as simplex / banxa plugin |
| 21 | +
|
| 22 | + Boot->>Net: initInfoServer() |
| 23 | + Net->>Eng: initAttestation() |
| 24 | + Eng->>Info: GET /v1/attest/challenge |
| 25 | + Info-->>Eng: challenge |
| 26 | + Eng->>Nat: getAttestation(challenge) |
| 27 | + Nat-->>Eng: keyId+attestation or certChain |
| 28 | + Eng->>Info: POST /v1/attest/apple|android |
| 29 | + Info-->>Eng: { token, expires, assuranceLevel } |
| 30 | + Note over Eng: cache token; refresh 2 min before expiry |
| 31 | +
|
| 32 | + Plugin->>Eng: getAttestationToken() |
| 33 | + Eng-->>Plugin: JWT or undefined |
| 34 | + Plugin->>Info: POST jwtSign|createHmac<br/>x-attestation-token? (only if JWT) |
| 35 | +``` |
| 36 | + |
| 37 | +Server endpoints and Couch gating policy: **edge-info-server** `docs/APP_ATTESTATION.md`. |
| 38 | + |
| 39 | +## Boot wiring |
| 40 | + |
| 41 | +1. [`src/app.ts`](../src/app.ts) calls `initInfoServer()` during startup. |
| 42 | +2. [`src/util/network.ts`](../src/util/network.ts) `initInfoServer()` pings info servers and calls **`initAttestation()`** (does not await the handshake). |
| 43 | +3. [`src/util/attestation.ts`](../src/util/attestation.ts) runs the background engine. |
| 44 | + |
| 45 | +**`fetchInfo` has no attestation logic.** It only fans out to `INFO_SERVER` / production info hosts. Plugins attach `x-attestation-token` themselves. |
| 46 | + |
| 47 | +### Local testing target |
| 48 | + |
| 49 | +Optional `env.json` / `ENV.INFO_SERVER` (see `envConfig.ts`) overrides the default `https://info1.edge.app` / `info2.edge.app` list so devices can hit a LAN info server (e.g. `["http://10.x.x.x:8008"]`). Android may also use `adb reverse tcp:8008 tcp:8008`. |
| 50 | + |
| 51 | +## JS attestation engine (`src/util/attestation.ts`) |
| 52 | + |
| 53 | +| API | Behavior | |
| 54 | +| --- | --- | |
| 55 | +| `initAttestation()` | If no live token, start a non-blocking handshake | |
| 56 | +| `getAttestationToken()` | Return cached JWT if live; else start handshake, wait ≤ **3s**, then return JWT or `undefined` | |
| 57 | +| Refresh | On success, `setTimeout` to re-handshake **2 minutes before** `expires` | |
| 58 | +| Clock skew | Token treated expired within **5s** of `expires` | |
| 59 | + |
| 60 | +Handshake steps: |
| 61 | + |
| 62 | +1. `GET v1/attest/challenge` via `fetchInfo` |
| 63 | +2. Native `EdgeAttestation.getAttestation(challenge)` |
| 64 | +3. `POST v1/attest/apple` (iOS) or `v1/attest/android` (Android) |
| 65 | +4. Cache `{ token, expires }` from the JSON body (`expires` is epoch **milliseconds**) |
| 66 | + |
| 67 | +Failures are logged (`console.warn`) and never thrown to boot. |
| 68 | + |
| 69 | +## Native modules |
| 70 | + |
| 71 | +### iOS — App Attest |
| 72 | + |
| 73 | +| File | Role | |
| 74 | +| --- | --- | |
| 75 | +| `ios/edge/EdgeAttestation.swift` | `DCAppAttestService`: `isSupported`, `generateKey` + `attestKey` | |
| 76 | +| `ios/edge/EdgeAttestation.m` | React Native bridge | |
| 77 | +| `ios/edge/edge.entitlements` | `com.apple.developer.devicecheck.appattest-environment` = **production** (all configs) | |
| 78 | +| `scripts/addAttestationIosFiles.js` | Adds Swift/ObjC sources to the Xcode project | |
| 79 | + |
| 80 | +**Key lifecycle:** a **fresh** App Attest key is generated on **every** handshake. An App Attest key can only be attested once; Keychain persist + assertion refresh are not implemented yet. |
| 81 | + |
| 82 | +Returns `{ keyId, attestation (base64 CBOR), bundleId }`. Simulator: `isSupported` is false → no token. |
| 83 | + |
| 84 | +Production entitlement → info server maps AAGUID to **`secureElement`**. |
| 85 | + |
| 86 | +### Android — Keystore attestation |
| 87 | + |
| 88 | +| File | Role | |
| 89 | +| --- | --- | |
| 90 | +| `android/.../EdgeAttestationModule.kt` | Keystore EC key with `setAttestationChallenge` | |
| 91 | +| `android/.../EdgeAttestationPackage.kt` | RN package | |
| 92 | +| `MainApplication.kt` | Registers the package | |
| 93 | + |
| 94 | +**StrongBox first**, TEE fallback on `StrongBoxUnavailableException`. Returns `{ certChain: base64 DER[] }`. Requires API 24+. Uses only platform Keystore APIs (**no Play Integrity**). |
| 95 | + |
| 96 | +Info server maps StrongBox → `secureElement`, TEE → `hardware`, debug-keystore digest → `debug`. |
| 97 | + |
| 98 | +## Where tokens are attached |
| 99 | + |
| 100 | +Call sites await `getAttestationToken()` and set the header only when non-null: |
| 101 | + |
| 102 | +| File | Info-server path | |
| 103 | +| --- | --- | |
| 104 | +| `src/plugins/gui/providers/simplexProvider.ts` | `v1/jwtSign/simplex` (quote + approve) | |
| 105 | +| `src/plugins/ramps/simplex/simplexRampPlugin.ts` | `v1/jwtSign/...` | |
| 106 | +| `src/plugins/gui/providers/banxaProvider.ts` | `v1/createHmac/...` | |
| 107 | +| `src/plugins/ramps/banxa/banxaRampPlugin.ts` | `v1/createHmac/...` | |
| 108 | + |
| 109 | +Pattern: |
| 110 | + |
| 111 | +```ts |
| 112 | +const attestationToken = await getAttestationToken() |
| 113 | +await fetchInfo(`v1/createHmac/${hmacUser}`, { |
| 114 | + method: 'POST', |
| 115 | + headers: { |
| 116 | + 'Content-Type': 'application/json', |
| 117 | + ...(attestationToken != null |
| 118 | + ? { 'x-attestation-token': attestationToken } |
| 119 | + : {}) |
| 120 | + }, |
| 121 | + body |
| 122 | +}) |
| 123 | +``` |
| 124 | + |
| 125 | +Whether a missing/invalid token fails the quote is decided by the info server’s Couch `attestationLevel` for that provider (string / `null` = ungated; e.g. `"hardware"` = required). See **edge-info-server** `docs/APP_ATTESTATION.md`. |
| 126 | + |
| 127 | +## Device requirements |
| 128 | + |
| 129 | +| Platform | Notes | |
| 130 | +| --- | --- | |
| 131 | +| iOS | Physical device required (no Secure Enclave on simulator). Network needed for Apple’s attest servers. | |
| 132 | +| Android | Emulator can exercise the API path at `debug`/`software` assurance; production-gated providers need a real TEE/StrongBox device (e.g. Pixel 10 → `secureElement`). | |
| 133 | + |
| 134 | +## Source map |
| 135 | + |
| 136 | +| Path | Role | |
| 137 | +| --- | --- | |
| 138 | +| `src/util/attestation.ts` | Background engine | |
| 139 | +| `src/util/network.ts` | `fetchInfo`, `initInfoServer` → `initAttestation` | |
| 140 | +| `src/app.ts` | Boot → `initInfoServer` | |
| 141 | +| `ios/edge/EdgeAttestation.*` | App Attest native | |
| 142 | +| `android/.../EdgeAttestation*.kt` | Keystore native | |
| 143 | +| `scripts/extractSigningCert.ts` | Helper for Android allow-list digests | |
| 144 | + |
| 145 | +## Related server |
| 146 | + |
| 147 | +The GUI depends on these info-server endpoints: |
| 148 | + |
| 149 | +- `GET /v1/attest/challenge` |
| 150 | +- `POST /v1/attest/apple` / `POST /v1/attest/android` |
| 151 | +- `POST /v1/jwtSign/:provider` / `POST /v1/createHmac/:provider` (optional `x-attestation-token`) |
| 152 | +- (optional) `GET /v1/attest/jwks` for other services verifying tokens |
| 153 | + |
| 154 | +Full contracts, Couch allow-lists, Redis challenges, and per-provider gating: **edge-info-server** `docs/APP_ATTESTATION.md`. |
0 commit comments