# Aprovação de ato com passkey

<https://unifokal.com/docs/modulos/passkey>

## Aprovação de ato com passkey

O módulo `passkey` faz o titular aprovar um **ato** seu (uma transferência, uma troca de chave Pix, uma alteração de cadastro) com a passkey que ele vinculou à conta dele na sua base. O navegador mostra o pedido do sistema operacional, o titular confirma com a biometria ou o PIN do aparelho, e a assinatura cobre o **resumo do ato** que você enviou na criação da sessão. Não há selfie nem documento nesse caminho: quem responde é a chave.

! **Em breve.** A venda deste módulo está pausada: ele aparece na [tabela de preços](https://unifokal.com/precos) com o selo "Em breve", e o `create` de flow o recusa até a abertura. O que está descrito abaixo é o contrato que o backend já emite, para você planejar a integração.

**O vínculo nasce de prova, nunca de pedido.** Você liga o vínculo no flow de onboarding (`passkey_bind`), o titular cria a chave durante a sessão, e ela só passa a valer quando a verificação aprova pelo caminho automático. Com rosto e prova de vida aprovados, a chave nasce com garantia de `identidade`; com prova de vida só, nasce com garantia de `presenca`. Aprovação manual na fila de revisão não vincula chave nenhuma, e a sessão de aprovação de ato também não pode ser aprovada nem reprocessada pelo painel.

**O ato vai na criação da sessão, no campo `act`.** O backend canoniza o ato, calcula o `act_digest` (SHA-256 da forma canônica) e guarda o resumo cifrado para mostrar ao titular. O desafio que o aparelho assina é derivado desse digest, então a assinatura não serve para outro ato. A sessão de ato vale 10 minutos, e o titular pode recusar o ato na própria tela, o que gera o evento `act.rejected`.

```
// passkey no check_details: o ato foi aprovado pela chave vinculada
{ "module": "passkey", "passed": true, "outcome": "approved", "score": 100,
  "data": { "passkey_id": "spk_01J9ZC6Q8R3M2V7K4T5N1B0XHD",
            "bound_by": "cadastro",          // cadastro | presenca | chave_existente | reprova
            "assurance": "identidade",       // identidade | presenca
            "backup_eligible": false, "backup_state": false,
            "user_verified": true,
            "act_digest": "b64url-do-sha256-do-ato-canonico",
            "act_kind": "pix_transfer",
            "evidence": { "format": "unifokal/passkey-assertion@1", "...": "..." },
            "model_version": "passkey-v1" } }

// passkey: a assinatura foi conferida e reprovou (veredito sobre a credencial)
{ "module": "passkey", "passed": false, "outcome": "failed", "score": 0,
  "data": { "passkey_id": "spk_01J9ZC6Q8R3M2V7K4T5N1B0XHD", "bound_by": "cadastro",
            "assurance": "identidade", "backup_eligible": null, "backup_state": null,
            "user_verified": null, "act_digest": null, "act_kind": null, "evidence": null,
            "reason": "passkey_assertion_invalid",
            "model_version": "passkey-v1" } }

// passkey no sandbox: a cerimônia é simulada e o bloco diz isso
{ "module": "passkey", "passed": true, "outcome": "approved", "score": 100,
  "data": { "passkey_id": "spk_01SANDBOXEXEMPLO0000000000", "bound_by": "cadastro",
            "assurance": "identidade", "backup_eligible": true, "backup_state": true,
            "user_verified": true, "act_digest": "b64url-do-sha256-do-ato-canonico",
            "act_kind": "pagamento", "simulated": true,
            "evidence": { "format": "unifokal/passkey-assertion@1", "simulated": true, "...": "..." },
            "model_version": "passkey-v1" } }
```

Os motivos em `data.reason`: `passkey_assertion_invalid` (a assinatura não confere), `passkey_backup_flag_changed` (a chave mudou de estado de cópia desde o vínculo), `passkey_not_device_bound` (a política pedia chave presa ao aparelho), `passkey_counter_regressed` (o contador do autenticador voltou, vai para revisão) e `passkey_not_completed` (o titular não concluiu, sem cobrança).

**A evidência é conferível sem nos consultar.** O bloco `evidence` traz o RP ID, a origem, a chave pública em JWK, os dados do autenticador, o `client_data_json`, a assinatura, o nonce e o ato canônico. O desafio assinado é `SHA-256("unifokal/passkey-act/v1" || nonce || act_digest)`, e a assinatura cobre `authenticator_data || SHA-256(client_data_json)`, como em qualquer asserção WebAuthn. Guarde a evidência junto do ato: ela prova, anos depois, o que o titular aprovou.

Os eventos de webhook do módulo são `passkey.bound` (a chave passou a valer), `passkey.revoked` (revogada pelo painel ou pelo apagamento do titular) e `act.rejected` (o titular recusou o ato). A revogação pelo painel exige a confirmação do segundo fator de quem opera.
