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

  1. Receive the payload — The client sends attestation (base64), keyId (base64), the challenge identifier, and a deviceId.

  2. 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.

  3. Call verifyAttestation() — Pass the appInfo, keyId, original challenge value, and the attestation bytes. The function accepts both Uint8Array and base64 strings for challenge and attestation.

  4. Store the result — Persist publicKeyPem, signCount (always 0), and receipt for this device. You'll need the public key and counter for every future assertion.

  5. Handle errorsverifyAttestation() throws AttestationError with a typed code. 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.

Previous
Supabase Edge Functions