Guides
Verifying attestations
A complete edge function that receives an attestation from your app and verifies it.
The edge function
import { createClient } from 'jsr:@supabase/supabase-js@2'
import {
verifyAttestation,
AttestationError,
AttestationErrorCode,
} from '@bradford-tech/supabase-integrity-attest'
const appInfo = {
appId: Deno.env.get('APP_ID')!,
developmentEnv: Deno.env.get('ENVIRONMENT') !== 'production',
}
Deno.serve(async (req: Request) => {
const { attestation, keyId, challenge, deviceId } = await req.json()
// 1. Look up the challenge from your server-side store
const supabase = createClient(
Deno.env.get('SUPABASE_URL')!,
Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!,
)
const { data: stored } = await supabase
.from('challenges')
.select('value')
.eq('id', challenge)
.single()
if (!stored) {
return new Response(JSON.stringify({ error: 'Invalid challenge' }), {
status: 400,
})
}
// 2. Delete the challenge (single-use)
await supabase.from('challenges').delete().eq('id', challenge)
// 3. Verify the attestation
try {
const result = await verifyAttestation(
appInfo,
keyId,
stored.value, // The original challenge bytes/string
attestation, // Base64-encoded CBOR attestation object
)
// 4. Store the device's public key and receipt
await supabase.from('devices').upsert({
device_id: deviceId,
public_key_pem: result.publicKeyPem,
sign_count: result.signCount,
receipt: result.receipt,
})
return new Response(JSON.stringify({ success: true }), { status: 200 })
} catch (error) {
if (error instanceof AttestationError) {
return new Response(
JSON.stringify({ error: error.message, code: error.code }),
{ status: 401 },
)
}
throw error
}
})
Step by step
Receive the payload — The client sends
attestation(base64),keyId(base64), thechallengeidentifier, and adeviceId.Validate the challenge — Look up the challenge from your server-side store. It must exist, be single-use, and not be expired. Delete it immediately after retrieval.
Call
verifyAttestation()— Pass theappInfo,keyId, original challenge value, and the attestation bytes. The function accepts bothUint8Arrayand base64 strings forchallengeandattestation.Store the result — Persist
publicKeyPem,signCount(always 0), andreceiptfor this device. You'll need the public key and counter for every future assertion.Handle errors —
verifyAttestation()throwsAttestationErrorwith a typedcode. See Types & error codes for the full list.
Challenge management is critical
Never accept a challenge that wasn't generated by your server. Never reuse a challenge. If you skip challenge validation, an attacker can replay a previously captured attestation.