# Deep audit — mensagens, assinaturas, certificados e fluxos PIX (2026-06-25)

Auditoria profunda 100% EMPÍRICA pedida pelo dono ("sem inferir nada"): cada item foi
validado contra o BACEN homolog VIVO, os certificados reais e as mensagens reais (replay
do JetStream / envio + leitura da resposta). Nenhuma conclusão por exclusão; nenhum erro
atribuído ao BACEN sem prova de que o nosso lado já estava correto.

Estado: tudo DEPLOYADO (pix-api task-def `:41`, imagem `sha256:f79bc103...`). Branch
`fix/pix-camt060-verifier-spi-msg-audit`.

## Resumo dos defeitos NOSSOS achados e corrigidos (todos provados ao vivo)

Saída (assinatura/envio):
1. `sign_spi` não removia whitespace → BACEN recusava a assinatura (admi.002 "Assinatura
   no XML não confere"). Fix: `strip_blanks` (igual ao `sign_dict`).
2. camt.060 no canal errado → BACEN só aceita no PRIMARY (CPM); no secondary = "Schema
   desconhecido / canal incorreto". Fix: `get_balance` força `:primary`.
3. camt.060 `<Document>` sem xmlns → BACEN isola o Document p/ identificar o schema e
   sem xmlns próprio dá "Schema desconhecido". Fix: Document com xmlns explícito.

Entrada (verificação/parse) — a camt.053 chegava e era DESCARTADA silenciosamente:
4. `verify_spi` digest da KeyInfo: a KeyInfo perde o `xmlns:ds` ao ser extraída de dentro
   de `<ds:Signature>` → exc-c14n diverge. Fix: `ensure_ds_namespace`.
5. `verify_spi` SignedInfo: mesma perda de `xmlns:ds` na canonicalização da assinatura.
   Fix: `ensure_ds_namespace` generalizado p/ qualquer `ds:*` apex.
6. `verify_spi` SignatureValue: o BACEN quebra o base64 em linhas com `&#13;` (referência
   de caractere XML) + whitespace → `Base.decode64` estrito falha. Fix:
   `sanitize_signature_value` remove a entidade INTEIRA + whitespace.
7. `verify_dict` digest da KeyInfo: mesmo bug do item 4, mas o `verify_digests_dict` não
   tinha sido corrigido (diferença de indentação no replace anterior). Fix aplicado.
8. camt.053 `confirmed` balance: a camt.053 de saldo (Bal SADP/SABK, sem CLBD) cravava
   `confirmed=closing_balance=0` (CLBD defaultava p/ Decimal 0, truthy) → discrepância
   falsa. Fix: parser CLBD default `nil`; handler usa SADP+SABK como confirmado na Conta PI.

## Fase 1 — Certificados (empírico, DB vivo + openssl)

| Cert | ISPB | Serial | Uso | keyUsage / EKU | Validade |
|------|------|--------|-----|----------------|----------|
| CPIA (nosso) | 46026562 | 7B9AB4FA…/38253… | assinatura | DigitalSignature, NonRepudiation, KeyEncipherment / TLS Client Auth | 2025-11-26 .. 2026-11-26 |
| CPIC (nosso) | 46026562 | 7B9AB4FA…/38253… (MESMO cert) | conexão mTLS | idem | idem |
| PEERPIA (BCB) | 00038166 | D391F362…/65477… | verificar inbound | DigitalSignature… | 2026-01-29 .. 2027-01-29 |

- **CPIA e CPIC são o MESMO certificado físico** (PEM idêntico, md5 `3986a271`). Não é
  defeito: o cert tem `Digital Signature` (XMLDSig) E `TLS Web Client Authentication`
  (mTLS), então serve corretamente para os dois usos. Um único cert PI ICP-Brasil.
- **BACEN assina TODO o inbound com um único serial `65477…` (PEERPIA T517)** — verificado
  em admi.002, camt.014, camt.053, pibr.002 e nos não-classificados. Nós TEMOS esse cert.
  Nenhum cert do BACEN faltando.
- Chave privada de assinatura: vem do `CertificateManager.get_private_key` (Secrets
  Manager). `get_signing_key("46026562")` retorna RSAPrivateKey + serial 38253. OK.

## Fase 2 — Assinaturas de SAÍDA (16 tipos)

Para cada tipo que enviamos: `build → sign_spi → verify_spi`. Como o `verify_spi` agora bate
byte-a-byte com o BACEN (provado na camt.053 real), `{:ok, :valid}` aqui significa que o
BACEN aceitaria. **TODOS os 16 deram `{:ok, :valid}`:** pacs.008, pacs.002, pacs.004,
camt.029, camt.055, camt.060, pain.009, pain.011, pain.012, pain.013, admi.004, reda.014,
reda.022, reda.031, trck.002, pibr.001.

## Fase 3 — Assinaturas de ENTRADA (mensagens reais do BACEN)

Replay do JetStream `MONETARIE_SPI` + `verify_spi` deployado. Todos os tipos REAIS recebidos
verificam `{:ok, :valid}`: **camt.053, pacs.002, pibr.002, admi.002, camt.014**. Os demais
(pacs.004, camt.054/055/029/052/025, admi.004, pain.014/012, reda.016) ainda não chegaram no
homolog (sem amostra real), mas usam o mesmo `verify_spi` + cert PEERPIA.

## Fase 4 — camt.053 → saldo (campo a campo)

A camt.053 real tem 2 `<Bal>`: `SADP=9160142073.16` (disponível) e `SABK=0.00` (bloqueado).
Materialização final (provada no DB): `available=9160142073.16, blocked=0.00,
confirmed=9160142073.16, source=camt.053`, **sem aviso de discrepância**.

## Fase 5 — DICT

`sign_dict` + `verify_dict` (após o fix item 7): KeyInfo e root digests batem. **NOTA de
design a revisar:** `verify_dict` NÃO tem callers em produção — o DICT inbound (respostas do
BACEN) hoje NÃO é verificado por assinatura (só o SPI é). O GetEntry funciona (200) porque a
resposta não passa por verificação de assinatura. Decisão do dono se quer ligar a verificação.

## Fase 6 — Fluxos (end-to-end, ao vivo)

- **camt.060 → camt.053**: `get_balance` → BACEN aceita → camt.053 recebida + verificada +
  saldo materializado (Conta PI R$9.160.142.073,16). OK.
- **pacs.008 → pacs.002**: pacs.008 enviado → BACEN responde pacs.002 → processada SEM
  "XMLDSig rejected" (antes era descartada). Timeline fecha. OK.

## Notas abertas (não são bloqueios, registradas honestamente)

1. **DICT inbound sem verificação de assinatura** (verify_dict sem callers) — ver Fase 5.
2. **Handshake TLS transiente no deploy**: na janela de cada deploy (min=0), a task nova
   conecta o ICOM mTLS enquanto a antiga drena, batendo no teto de 6 conexões do BACEN →
   `tls_alert handshake_failure` por ~1-2min → recupera ao conectar. 94 falhas em
   01:44-01:46 (deploy `:40`), zero depois. É mecânica de deploy, não estado estável.
3. Tipos inbound ainda não recebidos no homolog não têm amostra real para teste (Fase 3).
