TryMellon
Navigation

Attestation policies

Enforce which authenticators can register — by AAGUID, certification level, or software/hardware class. Built for regulated industries where "any passkey" is not acceptable.

Attestation policies

WebAuthn accepts any authenticator by default: YubiKeys, phones, password managers, browser-synced credentials. For most consumer apps that’s the right answer. For regulated ones it isn’t.

TryMellon’s attestation policy engine lets you define, per tenant, which authenticators can register. You can require hardware-backed keys only, pin specific models by AAGUID, block software authenticators, or set a minimum FIDO certification level. Policies run in either audit mode (log mismatches, accept all) or block mode (reject non-compliant registrations).

This is how you tell a private banking compliance team “only YubiKey 5 Series and Feitian K9” — and enforce it cryptographically, not on the honor system.


When to use it

SituationPolicy configuration
Private banking / custodyallowed_aaguids = [YubiKey 5 Series, Feitian K9 family] · enforcement_mode: block
Regulated fintech (EU DORA, PCI)min_certification_level: L2 · block_software_auth: true · enforcement_mode: block
Enterprise workforcerequire_known_aaguids: true · blocklist consumer password managers
Consumer app ramping upenforcement_mode: audit first, promote to block once telemetry confirms coverage
Rolling out a new authenticator modelAdd to allowed_aaguids in audit mode; watch metrics; promote

If you don’t need any of this, do nothing. The default is “accept any authenticator” — which is the right default for most SaaS.


How it works

Every WebAuthn registration produces an attestation statement — a cryptographic claim from the authenticator about its own identity: its AAGUID (Authenticator Attestation GUID, a UUID identifying the make/model), its certification level, and (for hardware devices) a chain rooted in the vendor’s CA.

TryMellon validates that chain against the FIDO MDS (Metadata Service), then evaluates it against your policy:

Registration ceremony


  Parse attestation statement


  Resolve AAGUID → MDS metadata


  ┌─────────────────────────────┐
  │ Policy evaluation           │
  │  • allowed_aaguids?         │
  │  • blocked_aaguids?         │
  │  • min_certification_level? │
  │  • block_software_auth?     │
  │  • require_known_aaguids?   │
  └─────────────────────────────┘

   ┌────┴────┐
   │         │
audit      block
log+accept  log+reject

Results are logged to your tenant audit log with the AAGUID, the matched/failed rule, and the evaluation mode — whether or not you block. Compliance gets a reviewable trail either way.


Policy shape

A tenant has exactly one policy. Updating it replaces the previous one.

FieldTypeRequiredDescription
tenant_idstring (UUID)yesYour tenant ID.
allowed_aaguidsstring[] (UUIDs)noIf set, only these AAGUIDs can register.
blocked_aaguidsstring[] (UUIDs)noThese AAGUIDs are always rejected (even if also in allowed).
min_certification_level"L1" | "L2" | "L3" | "L3plus"noMinimum FIDO certification level — see FIDO Alliance levels.
block_software_authbooleanyesReject software/virtual authenticators (e.g. browser-synced passkeys without hardware attestation).
require_known_aaguidsbooleanyesReject authenticators whose AAGUID isn’t in the FIDO MDS.
enforcement_mode"audit" | "block"yesaudit = log only · block = reject non-compliant.

Rule precedence: blocked_aaguids > allowed_aaguids > per-flag checks (block_software_auth, require_known_aaguids) > min_certification_level.


Quick start

1. Set a policy (S2S)

POST https://api.trymellonauth.com/v1/attestation/policy
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json

{
  "tenant_id": "ten_01HXYZ…",
  "allowed_aaguids": [
    "ee882879-721c-4913-9775-3dfcce97072a",   // YubiKey 5 Series
    "b92c3f9a-c014-4056-887f-140a2501163b"    // YubiKey 5Ci
  ],
  "blocked_aaguids": [],
  "min_certification_level": "L2",
  "block_software_auth": true,
  "require_known_aaguids": true,
  "enforcement_mode": "audit"
}

Start in audit mode. Watch the logs. Promote to block once you’re confident your users have compliant devices.

2. Read the current policy

GET https://api.trymellonauth.com/v1/attestation/policy?tenant_id=ten_01HXYZ…
Authorization: Basic <base64(client_id:client_secret)>

Response:

{
  "ok": true,
  "data": {
    "policy": {
      "id": "pol_…",
      "tenant_id": "ten_…",
      "allowed_aaguids": ["…"],
      "blocked_aaguids": [],
      "min_certification_level": "L2",
      "block_software_auth": true,
      "require_known_aaguids": true,
      "enforcement_mode": "audit",
      "created_at": "2026-04-17T10:00:00.000Z",
      "updated_at": "2026-04-17T10:00:00.000Z"
    }
  }
}

If no policy is set, policy is null.

3. Evaluate a credential ad-hoc

Useful for dashboards and compliance reports — check an existing credential against the current policy without running a full registration ceremony.

POST https://api.trymellonauth.com/v1/attestation/evaluate
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/json

{
  "tenant_id": "ten_…",
  "user_id": "user_…",
  "aaguid": "ee882879-721c-4913-9775-3dfcce97072a"
}

Pass "aaguid": null to evaluate an authenticator that didn’t provide attestation.


API reference

POST /v1/attestation/policy

Create or replace the tenant’s policy.

  • Auth: Basic Auth (S2S).
  • Body: see Policy shape.
  • Returns: 200 OK + the saved policy.

GET /v1/attestation/policy

Read the tenant’s current policy.

  • Auth: Basic Auth (S2S).
  • Query: tenant_id (UUID).
  • Returns: { policy: Policy | null }.

POST /v1/attestation/evaluate

Evaluate an AAGUID against the active policy without persisting.

  • Auth: Basic Auth (S2S).
  • Body: { tenant_id, user_id, aaguid }.
  • Returns: { passed: boolean, failed_rule?: string, level?: string }.

All three endpoints require the Attestation Policy Engine to be wired at deploy time. Self-hosted deployments without the engine return 501 Not Implemented.


Audit & telemetry

Every evaluation writes an audit entry:

{
  "event_type": "attestation.evaluated",
  "tenant_id": "ten_…",
  "user_id": "user_…",
  "aaguid": "ee882879-…",
  "enforcement_mode": "audit",
  "outcome": "pass",
  "failed_rule": null,
  "ts": "2026-04-17T10:15:00.000Z"
}

Filter for outcome: "fail" to find non-compliant registrations during audit-mode ramp-up. Use the audit log API — see Webhooks, audit & privacy.


Rollout playbook

  1. Inventory first. Query existing credentials via the admin API. Distribution by AAGUID tells you which devices are already in the wild.
  2. Audit mode. Set your policy with enforcement_mode: audit. Run for 7–14 days.
  3. Review logs. Group by failed_rule. If >5% of registrations would be blocked, iterate: expand allowed_aaguids, relax min_certification_level, communicate with users who need new devices.
  4. Promote to block. Flip enforcement_mode: block. Existing credentials are not retroactively rejected; only new registrations are evaluated.
  5. Monitor. Watch registration success rate and support volume for the next week. If anomalies spike, flip back to audit, investigate, re-promote.

Error codes

HTTPCodeCause
400invalid_aaguidA UUID in allowed_aaguids/blocked_aaguids is malformed.
400invalid_enforcement_modeNot "audit" or "block".
401unauthorizedBad Basic Auth.
403forbiddenApp lacks the attestation capability.
501not_implementedAttestation engine not wired in this deployment.

Registration failures due to policy in block mode return:

HTTPCodeCause
403attestation_blocked_aaguidAuthenticator AAGUID is in blocked_aaguids.
403attestation_aaguid_not_allowedallowed_aaguids is set and AAGUID is not in it.
403attestation_software_auth_blockedblock_software_auth: true and the credential has no hardware attestation.
403attestation_unknown_aaguidrequire_known_aaguids: true and AAGUID is not in FIDO MDS.
403attestation_certification_level_below_minimumAuthenticator’s level is below min_certification_level.