App Attest keys and assertions, or a DeviceCheck token, on iOS
One.DeviceAttestation proves to your server that requests come from a
genuine app on a genuine device. App Attest mints a per-installation key,
attests it once, and signs later assertions with a counter your server
checks. DeviceCheck mints a token your server trades for two persistent
device bits. Check availability before starting either flow: both depend
on the device and app registration.
On iOS it drives DCAppAttestService and DCDevice from DeviceCheck.
On Android and web availability reads false for both capabilities, and
every key, token, attestation, and assertion call rejects.
import { One } from 'one'
async function hashChallenge(challenge: string) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(challenge)) return btoa(String.fromCharCode(...new Uint8Array(digest)))}
const { appAttest, deviceCheck } = One.DeviceAttestation.getAvailability()
if (appAttest) { const keyId = await One.DeviceAttestation.generateKey() const attestation = await One.DeviceAttestation.attestKey(keyId, await hashChallenge('enroll')) const assertion = await One.DeviceAttestation.generateAssertion(keyId, await hashChallenge('login')) console.log(keyId, attestation, assertion)}
if (deviceCheck) { const token = await One.DeviceAttestation.generateDeviceToken() console.log(token)}Availability is synchronous. Save the key id per account and install; your server verifies the attestation certificate, app id, challenge, and later assertion signatures and counters.
Both hash arguments are the standard base64 encoding of exactly 32 SHA-256 bytes. Hash unique client data that includes a server challenge. Reuse one key identifier for later assertions; minting a key per request defeats the per-installation identity. The client returns Apple’s data as base64 and verifies nothing: a DeviceCheck token needs Apple’s server API, since the client can neither read nor change the two bits itself.
Register the app ID for the services in the Apple Developer account. App Attest uses Apple’s development environment for a development build unless the entitlement says otherwise, and development keys never work in production. App Store and TestFlight distribution use production regardless of a development entitlement.
Invalid identifiers and noncanonical or non-32-byte hashes reject with an input error. Unsupported hardware or app contexts reject as unavailable. Apple operation failures carry one code per verb: key generation, attestation, assertion, or token.
| Methods | Type | Description |
|---|---|---|
getAvailability() => DeviceAttestationAvailabilityWhether App Attest and DeviceCheck run here. | () => DeviceAttestationAvailability | Whether App Attest and DeviceCheck run here. |
generateKey() => Promise<string>Mint a key. Resolves with its id. | () => Promise<string> | Mint a key. Resolves with its id. |
attestKey(keyId: string, clientDataHashBase64: string) => Promise<string>Attest a key over a hashed challenge. | (keyId: string, clientDataHashBase64: string) => Promise<string> | Attest a key over a hashed challenge. |
generateAssertion(keyId: string, clientDataHashBase64: string) => Promise<string>Sign a hashed challenge with a key. | (keyId: string, clientDataHashBase64: string) => Promise<string> | Sign a hashed challenge with a key. |
generateDeviceToken() => Promise<string>Mint a DeviceCheck token. | () => Promise<string> | Mint a DeviceCheck token. |
| Availability | Type | Description |
|---|---|---|
appAttestbooleanWhether App Attest keys run here. | boolean | Whether App Attest keys run here. |
deviceCheckbooleanWhether DeviceCheck tokens run here. | boolean | Whether DeviceCheck tokens run here. |
| iOS | Android | Web | |
|---|---|---|---|
| App Attest | DeviceCheck | Rejects | Rejects |
| DeviceCheck | DeviceCheck | Rejects | Rejects |
| Available | Per device | Never | Never |
import { One } from 'one'
function toBase64(bytes: Uint8Array<ArrayBuffer>) { return btoa(String.fromCharCode(...bytes))}
async function hashChallenge(challenge: string) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(challenge)) return toBase64(new Uint8Array(digest))}
async function attestInstall(challenge: string) { if (!One.DeviceAttestation.getAvailability().appAttest) return null const keyId = await One.DeviceAttestation.generateKey() const attestation = await One.DeviceAttestation.attestKey(keyId, await hashChallenge(challenge)) return { keyId, challenge, attestation }}The challenge travels to the server alongside the key id and attestation, so verification ties all three together. Persist the key id; the next launch asserts with it instead of minting again.
import { One } from 'one'
async function authorizedFetch(keyId: string, challenge: string, url: string) { const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(challenge)) const hash = btoa(String.fromCharCode(...new Uint8Array(digest))) const assertion = await One.DeviceAttestation.generateAssertion(keyId, hash) return fetch(url, { headers: { 'x-attestation': assertion, 'x-challenge': challenge }, })}Each sensitive request carries a fresh assertion over a fresh server challenge. The server checks the signature and the monotonically rising counter before honoring the call.
import { One } from 'one'
async function deviceBits() { if (!One.DeviceAttestation.getAvailability().deviceCheck) return null const token = await One.DeviceAttestation.generateDeviceToken() const response = await fetch('https://example.com/device-bits', { method: 'POST', body: JSON.stringify({ token }), }) return (await response.json()) as { bit0: boolean; bit1: boolean }}The token is opaque on the client; only the server, through Apple’s API, reads or flips the two bits. The client just ferries the token and consumes the verdict.
Edit this page on GitHub.