# Design: importação e rotação self-service dos certificados do BACEN (SPB)

Data: 2026-06-30
Autor: sessão Claude (handoff)
Status: proposta de design (para implementar depois; não implementado)
Relacionado: ativação do cert SPB T010 ([[monetarie-spb-hsm-t010-activation]]), memória [[monetarie-spb-bacen-cert-import-selfservice]]

## 1. Objetivo e requisito do dono

A área usuária ou piloto da reserva precisa conseguir, sozinha e 100% pela tela, importar e atualizar os certificados do BACEN usados pelo SPB. Quando o BACEN trocar o certificado dele (rotação periódica), o usuário sobe o novo arquivo pela tela e o sistema passa a usar o certificado novo automaticamente, sem nenhuma ação do time de tecnologia, sem deploy e sem reinício de serviço.

A capacidade self-service cobre dois conjuntos de certificados, ambos pela mesma tela e ambos críticos:

1. Certificados do BACEN (a contraparte): usados para cifrar o que enviamos (destino) e para verificar a assinatura do que o BACEN nos envia.
2. Os NOSSOS certificados de assinatura (a nossa identidade SPB, ex. T010): é o certificado público que faz par com a chave privada que vive no HSM. Sem gerir esse certificado e o vínculo com o HSM, não conseguimos submeter ao BACEN com a assinatura completa do HSM nem ativar uma nova identidade. A rotação do nosso certificado é, portanto, parte central desta feature (não uma fase posterior), com a diferença de que envolve o UID da chave no HSM e a ativação via GEN0006.

Motivação concreta: em 2026-06-30 a ativação do cert T010 falhou primeiro com `EGEN9907` porque os certificados do BACEN que estavam no repositório eram placeholders falsos (`CN=monetarie-spb-local`, AC code 0). A correção foi manual (importar os certificados reais T068 e T069, vincular a chave T010 no HSM, ligar o HSM e ativar via GEN0006, tudo por rpc, depois deploy). Isso não pode depender de tecnologia nem quando o BACEN rotaciona, nem quando nós rotacionamos o nosso certificado.

## 2. Estado atual (como os certificados do BACEN são carregados hoje)

Tudo vive no módulo `BacenGateway.Crypto.CertificateRegistry` (GenServer com tabela ETS `:certificate_registry`), carregado a partir de arquivos em `priv/certs` no boot:

- `load_bacen_domain_certificates/1` percorre o mapa estático `@bacen_domain_cert_files` (`SPB01 -> bacen_00038166_T068.pem`, `MES01/02/03 -> bacen_00038166_T069.pem`) e insere em ETS sob a chave `{:institution_domain, "00038166", DOMINIO}`. É o certificado de destino usado para cifrar (C14) o que enviamos ao BACEN. O header carrega `ac_cert_dest` via `CertificateAuthority.get_ac_code/1` e `serial_cert_dest` via `get_serial_padded/1`.
- `load_bacen_certificates/1` carrega todos os arquivos `bacen_*.pem` numa lista em ETS sob a chave `:bacen_certificates`. Essa lista é usada por `find_bacen_certificate_by_serial/1` para verificar a assinatura (C15) das mensagens que o BACEN nos envia (`lookup_by_serial/1`).
- `load_our_certificate/1` lê o arquivo apontado por `BACEN_CERT_PEM_FILE` como a nossa identidade de assinatura (`:our_certificate`).
- `lookup_by_serial/1` tenta, em ordem: `find_serial_index({:cert_serial, serial})` (índice explícito, hoje não populado), depois `find_bacen_certificate_by_serial` (varre `bacen_certificates()`), depois os certificados de instituições.
- `get_ac_code/1` mapeia o emissor (issuer) para o código BACEN (`@ca_map`: SERPRO=1, CERTISIGN=2, SERASA=4, VALID=6, SOLUTI=7; default 0).

Limitações para o objetivo:
- A fonte de verdade são arquivos no build (imagem). Trocar exige rebuild ou injeção manual em runtime.
- O mapa domínio para arquivo é estático no código (`@bacen_domain_cert_files`).
- Não existe reload em runtime nem persistência editável.

## 3. Visão geral da solução

Mover a fonte de verdade dos certificados do BACEN de arquivos no build para uma tabela no banco, com uma tela de administração que faz upload, validação, ativação e histórico. No salvar, além de persistir no banco, atualizar imediatamente o ETS do `CertificateRegistry` (pickup automático, sem restart). No boot, o `CertificateRegistry` passa a carregar do banco (com fallback para os arquivos atuais, para não quebrar quem ainda não migrou).

Resultado: o usuário sobe o certificado novo pela tela, o sistema valida, ativa e já começa a usar, e isso sobrevive a restart porque está no banco.

## 4. Arquitetura e componentes

1. Tabela nova `bacen_domain_certificates` (banco do SPB) com versionamento e flag de ativo.
2. Endpoints admin no `bacen_gateway` (upload, listar, pré-visualizar, ativar, desativar, histórico).
3. Camada de domínio `BacenGateway.Crypto.BacenCertStore` que encapsula persistência, validação e atualização do ETS.
4. Ajuste no `CertificateRegistry`:
   - No boot, `load_bacen_domain_certificates` e `load_bacen_certificates` passam a ler do `BacenCertStore` (banco) e, se vazio, caem no fallback de arquivos atual.
   - Nova função pública `refresh_bacen_certificates/0` que recarrega o ETS a partir do banco. Chamada após cada import/ativação.
5. Tela no front admin do SPB: "Certificados do BACEN".

Princípio de segregação: o certificado do BACEN é público; nunca há chave privada do BACEN. A nossa chave privada continua no HSM, fora do escopo desta feature.

## 5. Modelo de dados

Tabela `bacen_domain_certificates`:

| coluna | tipo | descrição |
|---|---|---|
| id | uuid | PK |
| ispb | varchar(8) | sempre `00038166` (BACEN), deixado explícito para futuro |
| domain | varchar(10) | `SPB01`, `MES01`, `MES02`, `MES03`, etc. |
| serial_hex | varchar(64) | serial do certificado, normalizado (upper, sem zeros à esquerda) |
| subject_cn | varchar | ex. `BANCO CENTRAL DO BRASIL T068` |
| issuer_cn | varchar | ex. `Autoridade Certificadora do SERPRO Final SSL` |
| ac_code | smallint | código da AC derivado do issuer (1=SERPRO, 6=VALID, ...) |
| not_before | timestamptz | validade início |
| not_after | timestamptz | validade fim |
| fingerprint_sha256 | varchar(64) | impressão digital do DER |
| pem | text | o certificado em PEM (público) |
| active | boolean | se é o ativo para o (ispb, domain) |
| imported_by | varchar | usuário que importou (do JWT) |
| imported_at | timestamptz | quando |
| replaced_serial | varchar(64) null | serial do anterior (para rollback e auditoria) |

Índices: único parcial `(ispb, domain) where active`, índice em `serial_hex`. Histórico = todas as linhas (ativas e inativas) por (ispb, domain).

## 6. API backend (admin, com JWT + escopo admin)

- `POST /api/admin/bacen-certs/preview` — recebe o conteúdo do `.cer` (PEM ou DER, multipart ou base64) e o domínio. Não persiste. Retorna o parse validado: subject, issuer, ac_code, serial, validade, fingerprint, e a lista de validações (ok ou erro). Permite o usuário conferir antes de salvar.
- `POST /api/admin/bacen-certs` — importa de fato: valida, persiste como nova linha ativa para (ispb, domain), marca a anterior como inativa (guardando `replaced_serial`), chama `CertificateRegistry.refresh_bacen_certificates/0`. Retorna o registro criado.
- `GET /api/admin/bacen-certs` — lista os ativos por domínio (e opcionalmente o histórico).
- `GET /api/admin/bacen-certs/:id` — detalhe de um registro.
- `POST /api/admin/bacen-certs/:id/activate` — reativa um registro antigo (rollback explícito) e desativa o atual.
- `POST /api/admin/bacen-certs/:id/deactivate` — desativa (uso raro; normalmente troca-se por outro ativo).

Todas as rotas dentro do bloco admin do router, com o mesmo plug de auth já usado pelos endpoints admin existentes.

## 7. Tela do front (admin SPB)

Tela "Certificados do BACEN":
- Tabela com os certificados ativos por domínio: Domínio, CN, AC, serial, validade (com alerta visual se perto de expirar), fingerprint, importado por/quando.
- Botão "Importar certificado": upload do `.cer` (aceitar PEM e DER), seletor de domínio (SPB01, MES01, MES02, MES03), botão "Pré-visualizar".
- Pré-visualização: mostra o resultado do `preview` (subject, issuer, AC, serial, validade) e bloqueia o "Salvar" se houver erro de validação.
- Botão "Salvar e ativar": chama o import; ao concluir, recarrega a tabela e mostra confirmação.
- Histórico por domínio com a opção de "Reativar" (rollback) um certificado anterior.
- Sem dado mockado; tudo vem da API.

## 8. Pickup automático sem restart (ponto crítico)

Este é o requisito central. O `CertificateRegistry` ganha:
- `refresh_bacen_certificates/0` (público): lê do `BacenCertStore` todos os ativos, e reinsere no ETS:
  - `{:institution_domain, "00038166", DOMINIO}` por domínio (substitui o de destino);
  - `:bacen_certificates` (lista de PEMs ativos, para `find_bacen_certificate_by_serial`);
  - opcionalmente popula `{:cert_serial, serial}` (índice por serial) para acelerar `lookup_by_serial`.
- O endpoint de import chama `refresh_bacen_certificates/0` logo após o commit no banco. Como o ETS é compartilhado no nó, a próxima mensagem (in ou out) já usa o certificado novo. Zero restart.
- Se o serviço escalar para mais de uma instância no futuro, propagar o refresh via NATS (evento `spb.bacen_cert.updated`) para todos os nós recarregarem. Hoje o spb-api é instância única, então o refresh local basta; deixar o gancho de NATS previsto.

No boot, `load_bacen_domain_certificates` e `load_bacen_certificates` passam a chamar `refresh_bacen_certificates/0` (lendo do banco). Fallback: se o banco estiver vazio para um domínio, usa o arquivo de `priv/certs` correspondente (comportamento atual), para uma migração suave.

## 9. Validações e segurança

No `preview` e no import, rejeitar e explicar o motivo se:
- O arquivo não for um X.509 válido (PEM ou DER).
- O subject não contiver `BANCO CENTRAL DO BRASIL` e `OU=ISPB-00038166` (garante que é mesmo um certificado do BACEN para o nosso ISPB de contraparte).
- O issuer não mapear para uma AC reconhecida (ac_code = 0 é bloqueado, pois foi exatamente a causa do EGEN9907).
- O certificado estiver expirado (`not_after` no passado) ou ainda não válido (`not_before` no futuro).
- Já existir um ativo idêntico (mesmo serial e domínio) — nesse caso, no-op informativo.
- O domínio não estiver na lista permitida (SPB01, MES01, MES02, MES03; configurável).

Segurança: rota admin com JWT; nunca aceitar nem armazenar chave privada (validar que o arquivo é só certificado, sem bloco `PRIVATE KEY`); registrar quem importou.

## 10. Auditoria e rollback

- Toda importação grava `imported_by`, `imported_at` e `replaced_serial`.
- O histórico mantém todas as versões (ativas e inativas) por (ispb, domain).
- Rollback = reativar uma linha anterior (`/activate`), que desativa a atual e chama o refresh. Útil se o BACEN voltar atrás ou se uma importação errada for feita.
- Logar em `audit_logs` cada import/ativação/desativação.

## 11. Passos de implementação (fases)

1. Migração: criar a tabela `bacen_domain_certificates` e seed inicial com os certificados reais atuais (T068 para SPB01, T069 para MES01/02/03) extraídos dos arquivos de `priv/certs` já commitados.
2. `BacenCertStore`: parse e validação de certificado (reutilizar `CertificateAuthority` para ac_code e serial), CRUD com versionamento, e a função de montar o estado para o ETS.
3. `CertificateRegistry.refresh_bacen_certificates/0` + ajuste do boot para ler do banco com fallback para arquivo.
4. Controller admin + rotas + plug de auth.
5. Tela do front admin (upload, preview, lista, histórico, rollback) sem mock.
6. Testes: parse/validação (inclui rejeição de AC 0 e de expirado), import que troca o ativo e atualiza o ETS, verify de assinatura inbound usando o certificado importado, rollback. Teste de regressão garantindo que `lookup_by_serial` acha o certificado recém-importado.
7. (Opcional) Evento NATS `spb.bacen_cert.updated` para multi-instância.

## 12. Critérios de aceite

- Um usuário admin importa um `.cer` real do BACEN pela tela, sem ajuda de tecnologia.
- Imediatamente após salvar (sem restart), uma mensagem outbound passa a cifrar para o novo certificado (`ac_cert_dest`/`serial_cert_dest` corretos) e uma mensagem inbound do BACEN passa a ter a assinatura verificada contra o novo certificado.
- Após um restart do spb-api, o certificado importado continua valendo (veio do banco).
- Importar um certificado inválido (expirado, AC 0, ou que não seja do BACEN) é bloqueado com mensagem clara.
- Histórico e rollback funcionam.

## 13. Riscos e decisões em aberto

- Escopo: esta feature cobre os certificados do BACEN (seções 5 a 12) E os nossos certificados de assinatura com ativação via GEN0006 (seção 14). As duas trilhas compartilham a mesma tela, a mesma persistência em banco e o mesmo mecanismo de pickup sem restart; o que muda é o fluxo específico de cada uma.
- Multi-instância: hoje instância única; o gancho de NATS fica previsto mas não obrigatório na primeira entrega.
- Migração: manter o fallback de arquivo durante a transição evita big-bang; remover o fallback depois que o banco for a fonte única.

## 14. Certificados próprios de assinatura (identidade SPB) + ativação GEN0006

Esta trilha é tão central quanto a do BACEN: é o nosso certificado, pareado com a chave privada no HSM, que produz a assinatura completa que submetemos ao BACEN. Reaproveita toda a infra (banco, tela, pickup sem restart, auditoria), com um fluxo próprio.

### 14.1 O que muda em relação ao certificado do BACEN
- O certificado do BACEN é só importado e usado. O nosso, além de importado, precisa de duas coisas a mais:
  1. O vínculo com a chave no HSM. A chave privada nunca está no arquivo nem no banco; gerimos o certificado público mais os UIDs da chave no HSM (privada e pública) e o vHSM.
  2. A ativação no BACEN via mensagem GEN0006, assinada pela própria chave nova (no HSM). Antes de ativado, o certificado não vale como identidade.
- Por ser a nossa identidade (`:our_certificate` + UID de assinatura), a troca tem que ser atômica e reversível.

### 14.2 Como está cabeado hoje
- `:our_certificate` vem do arquivo `BACEN_CERT_PEM_FILE`. A chave: `RTM_HSM_PRIVATE_KEY_UID` / `RTM_HSM_PUBLIC_KEY_UID` (env, vHSM `RTM_HSM_VHSM`), usada por `RtmHsm.sign` (assinatura C15) e `RtmHsm.decrypt_private` (decifragem C14 inbound).
- A validação do par cert+chave foi feita por round-trip: assinar um nonce com o UID privado no HSM e verificar a assinatura contra a chave pública do certificado.
- A ativação foi GEN0006 (`CodCertifrAtv` = código da AC, ex. VALID = 6; `CertifAtv` = serial em 32 hex com zeros à esquerda), assinada pelo HSM, enviada pelo pipeline existente; confirmação = GEN0006R1.

### 14.3 Modelo de dados: tabela `our_signing_certificates`

| coluna | tipo | descrição |
|---|---|---|
| id | uuid | PK |
| ispb | varchar(8) | `46026562` (Monetarie) |
| label | varchar | rótulo, ex. `T010` |
| serial_hex | varchar(64) | serial normalizado |
| subject_cn / issuer_cn | varchar | ex. `MONETARIE ... T010` / `AC VALID SPB v5` |
| ac_code | smallint | derivado do issuer (VALID = 6, etc.) |
| not_before / not_after | timestamptz | validade |
| fingerprint_sha256 | varchar(64) | impressão digital do DER |
| pem | text | certificado público (sem chave privada) |
| hsm_private_key_uid | varchar | UID da chave privada no HSM (ex. `V/YVgxtsO+m2jJyNIOgc`) |
| hsm_public_key_uid | varchar | UID da chave pública (ex. `T4GGZ4edSga/jRacj2mb`) |
| hsm_vhsm | varchar | vHSM (ex. `60042`) |
| status | varchar | `imported` / `verified` / `activating` / `active` / `inactive` / `failed` |
| activation_message_id | uuid null | a GEN0006 que ativou |
| activation_response | text null | resumo do GEN0006R1 ou GEN0006E |
| activated_at | timestamptz null | quando ativou |
| imported_by / imported_at | varchar / timestamptz | quem e quando |
| replaced_serial | varchar(64) null | serial do anterior (rollback/auditoria) |

### 14.4 Ciclo de vida (estados)
1. `imported`: upload do certificado público mais os UIDs do HSM (privada e pública) e o vHSM. Valida o certificado (subject Monetarie ISPB-46026562, AC reconhecida com ac_code diferente de 0, dentro da validade, sem bloco de chave privada).
2. `verified`: round-trip no HSM. O backend manda o HSM assinar um nonce com o `hsm_private_key_uid` e verifica a assinatura contra a chave pública do certificado. Só passa se bater. Isso prova, antes de qualquer envio ao BACEN, que a chave no HSM é a do certificado.
3. `activating`: monta o GEN0006 (`CodCertifrAtv` = ac_code, `CertifAtv` = serial em 32 hex, ambos derivados do próprio certificado, nunca digitados), assina pelo HSM usando o UID deste certificado, envia pelo pipeline e aguarda a resposta.
4. `active`: ao receber o GEN0006R1 confirmado, promove de forma atômica: `:our_certificate` passa a ser este certificado e o UID ativo de assinatura e decifragem passa a ser o deste certificado. O anterior vira `inactive` (mantido para rollback). Em GEN0006E, vai para `failed` com o motivo do BACEN.
5. rollback: reativar um anterior (`/rollback`) restaura `:our_certificate` mais o UID; se o anterior ainda estiver ativo no BACEN, não precisa de novo GEN0006.

### 14.5 Transição com chave dupla (cuidado crítico, foi o ponto delicado em 2026-06-30)
Durante `activating`, o BACEN ainda cifra o inbound para o certificado ATUALMENTE ativo. O novo só passa a valer depois do GEN0006R1. Então:
- A assinatura do GEN0006 de ativação usa o UID do certificado NOVO (prova de posse).
- A decifragem do inbound e as demais assinaturas de tráfego normal continuam com o certificado ATIVO até a promoção.
- Só na promoção o UID ativo vira o do novo certificado (aí o BACEN passa a cifrar para ele).
Implementação: o `RtmHsm` deve receber o UID por chamada (ou separar um "UID de ativação" temporário do "UID ativo" global), em vez de um único UID global usado para tudo. Sem isso, ligar o novo UID antes da promoção quebra a decifragem do inbound (foi exatamente o que aconteceu e exigiu contornar à mão).

### 14.6 API
- `POST /api/admin/our-certs/preview` — valida o certificado e, se informados, os UIDs.
- `POST /api/admin/our-certs` — importa (status `imported`); guarda certificado mais UIDs e vHSM.
- `POST /api/admin/our-certs/:id/verify-hsm` — round-trip no HSM, leva a `verified`.
- `POST /api/admin/our-certs/:id/activate` — dispara o GEN0006 (status `activating`); ao confirmar, `active`.
- `GET /api/admin/our-certs`, `/:id`, `/:id/activation-status` — listar, detalhe e acompanhar a ativação (mostra a GEN0006 e a GEN0006R1/E).
- `POST /api/admin/our-certs/:id/rollback` — restaura um anterior.

### 14.7 Tela
Aba "Nossos certificados (assinatura)": importar (certificado mais UIDs do HSM e vHSM), botão "Verificar par no HSM", botão "Ativar" que dispara o GEN0006 e mostra o andamento até o GEN0006R1, histórico e rollback. Exibe status, serial, AC, validade e o resultado da ativação. Sem dado mockado.

### 14.8 Validações específicas
- Certificado: subject Monetarie (ISPB-46026562), AC reconhecida (ac_code diferente de 0), dentro da validade, só certificado (sem chave privada no arquivo).
- HSM: o round-trip tem que passar antes de permitir a ativação. Nunca enviar GEN0006 sem o `verified`.
- Idempotência: não reativar o que já está `active`; `CodCertifrAtv` e `CertifAtv` derivados do próprio certificado.

### 14.9 Pickup e permanência
Igual à trilha do BACEN: ao promover, atualiza o ETS (`:our_certificate` mais o UID ativo) sem restart e persiste no banco; no boot, `:our_certificate` e o UID ativo vêm do banco (registro `active`), com fallback para `BACEN_CERT_PEM_FILE` e as envs `RTM_HSM_*` durante a migração.
