|
Sim. A forma mais correta é não criptografar o Event Store inteiro com uma chave única. Vamos criptografar cada evento, ou cada pequeno lote de eventos, com uma chave de dados própria, e o Linus Salamander atua como Enclave Vault/KMS interno que cria, protege, versiona e autoriza o uso dessas chaves. |
|
|
|
A arquitetura fica assim: |
|
|
|
Agent |
|
├── EventSourcingSidecar |
|
│ ├── recebe evento em claro apenas em memória |
|
│ ├── normaliza evento |
|
│ ├── calcula hashes/cadeia de integridade |
|
│ └── grava evento cifrado no Local Event Store |
|
│ |
|
├── CryptoSidecar |
|
│ ├── pede chave ao Linus Salamander |
|
│ ├── cifra payload |
|
│ ├── remove chave da memória |
|
│ └── decifra somente quando autorizado |
|
│ |
|
└── Linus Salamander Enclave Vault |
|
├── Master Keys / KEKs |
|
├── políticas de acesso |
|
├── rotação de chaves |
|
├── auditoria de decrypt |
|
└── unwrap temporário de Data Keys |
|
|
|
O padrão que devemos usar é envelope encryption: o dado é cifrado com uma Data Encryption Key, e essa chave de dados é cifrada por uma chave superior, geralmente chamada de Key Encryption Key. Esse modelo é recomendado/consagrado em KMSs modernos; a AWS descreve envelope encryption exatamente como cifrar dados com uma data key e depois cifrar essa data key com outra chave, enquanto o NIST SP 800-57 é a referência geral para ciclo de vida e gerenciamento de chaves criptográficas. |
|
|
|
A estrutura do evento gravado deveria ser algo assim: |
|
|
|
{ |
|
"event_id": "evt_01...", |
|
"agent_id": "UserDomainAgent_...", |
|
"stream_id": "user:123", |
|
"event_type": "UserContactRequested", |
|
"event_version": 1, |
|
|
|
"metadata_plain": { |
|
"schema_version": "1.0", |
|
"created_at": "2026-06-28T16:56:00-03:00", |
|
"tenant_id": "itarare", |
|
"agent_type": "LawyerWhatsAppAgent", |
|
"key_id": "lsm:kek:tenant:itarare:v3", |
|
"dek_id": "lsm:dek:event:evt_01..." |
|
}, |
|
|
|
"crypto": { |
|
"alg": "XChaCha20-Poly1305", |
|
"nonce": "...", |
|
"encrypted_dek": "...", |
|
"aad_hash": "...", |
|
"key_version": 3 |
|
}, |
|
|
|
"integrity": { |
|
"payload_hash": "...", |
|
"previous_event_hash": "...", |
|
"event_hash": "...", |
|
"signature": "..." |
|
}, |
|
|
|
"payload_ciphertext": "..." |
|
} |
|
|
|
Eu deixaria metadados mínimos em claro e o payload cifrado. O Event Store precisa conseguir ordenar, filtrar por stream, aplicar replay técnico e fazer retenção sem abrir dados sensíveis. Então campos como event_id, stream_id, agent_id, event_type, created_at, key_id, schema_version podem ficar visíveis. Já dados pessoais, conteúdo de conversa, documentos, valores, nomes, CPF, resumo do atendimento e anexos devem ir para payload_ciphertext. |
|
|
|
A regra seria: |
|
|
|
Metadados operacionais: claro, mínimos e não sensíveis. |
|
Payload de domínio: sempre cifrado. |
|
Anexos/documentos: sempre cifrados separadamente. |
|
Chaves: nunca ficam no Event Store. |
|
|
|
O fluxo de escrita: |
|
|
|
1. Agent gera evento de domínio. |
|
2. EventSourcingSidecar normaliza o evento. |
|
3. CryptoSidecar pede uma DEK ao Linus Salamander. |
|
4. Linus Salamander gera ou libera uma DEK temporária. |
|
5. CryptoSidecar cifra o payload. |
|
6. CryptoSidecar apaga a DEK em claro da memória. |
|
7. EventSourcingSidecar calcula hash do ciphertext + metadados + hash anterior. |
|
8. Event Store grava somente o evento cifrado. |
|
|
|
O fluxo de leitura/replay: |
|
|
|
1. Agent pede replay de uma stream. |
|
2. EventSourcingSidecar lê eventos cifrados. |
|
3. Para cada evento, valida hash chain e assinatura. |
|
4. CryptoSidecar pede autorização ao Linus Salamander. |
|
5. Linus Salamander verifica policy, agent_id, tenant_id, purpose e escopo. |
|
6. Se autorizado, libera unwrap temporário da DEK. |
|
7. CryptoSidecar decifra em memória. |
|
8. Agent recebe o payload reconstruído. |
|
9. Chave e payload temporário são descartados após uso. |
|
|
|
A parte importante: o Linus Salamander não deve virar um banco de dados de eventos. Ele deve guardar ou derivar as chaves, controlar acesso, registrar auditoria e permitir rotação/revogação. A AWS KMS documenta esse modelo: a data key é usada fora do KMS para cifrar dados, removida da memória depois do uso, e a versão cifrada da data key pode ficar armazenada junto do dado cifrado. |
|
|
|
Eu faria três níveis de chave: |
|
|
|
Root Key |
|
└── Tenant KEK |
|
└── Agent KEK |
|
└── Event DEK |
|
|
|
Ou seja: |
|
|
|
Linus Salamander Root Key |
|
↓ |
|
Cidade/Tenant Key: itarare |
|
↓ |
|
Agent Key: LawyerWhatsAppAgent |
|
↓ |
|
Event/Data Key: evento individual ou lote pequeno |
|
|
|
Isso te dá isolamento por cidade, por profissional, por agent e por evento. Se um advogado sair da plataforma, você pode revogar ou congelar o acesso daquele escopo sem quebrar toda a base da cidade. |
|
|
|
Para o Local EventSource de cada Agent, eu usaria: |
|
|
|
DEK por evento sensível |
|
ou |
|
DEK por stream curta |
|
ou |
|
DEK por janela temporal |
|
|
|
Minha recomendação: |
|
|
|
Eventos normais: DEK por stream + rotação diária/semanal. |
|
Eventos sensíveis: DEK por evento. |
|
Documentos/anexos: DEK por arquivo. |
|
|
|
Para advogado, documentos e conversas podem conter dados pessoais e informações sensíveis. Então eu não colocaria tudo na mesma chave. A divisão segura fica: |
|
|
|
ContactRequested → DEK por stream |
|
AppointmentRequested → DEK por stream |
|
DocumentUploaded → DEK por arquivo |
|
LegalDocumentExtracted → DEK por evento |
|
ClientConversation → DEK por conversa ou por janela |
|
|
|
O algoritmo simétrico: eu usaria XChaCha20-Poly1305 ou AES-256-GCM. Se você quer simplicidade, segurança e nonce grande, XChaCha20-Poly1305 é bom. Se quiser mais padrão corporativo e aceleração via hardware, AES-256-GCM é mais aceito. Em ambos, usar AEAD, porque você precisa de criptografia autenticada: confidencialidade + integridade do payload. |
|
|
|
O AAD é essencial. Ele prende criptograficamente o payload aos metadados. Exemplo: |
|
|
|
AAD = |
|
tenant_id |
|
agent_id |
|
stream_id |
|
event_id |
|
event_type |
|
event_version |
|
created_at |
|
previous_event_hash |
|
|
|
Assim, se alguém copiar o ciphertext de um evento para outro stream, ou tentar trocar metadados, a decriptação falha. |
|
|
|
A cadeia de integridade deve ser feita sobre o evento cifrado, não só sobre o payload aberto: |
|
|
|
event_hash = HASH( |
|
event_id |
|
stream_id |
|
event_type |
|
metadata_plain |
|
crypto_metadata |
|
payload_ciphertext |
|
previous_event_hash |
|
) |
|
|
|
Isso mantém o Event Store auditável sem precisar abrir o conteúdo. Event stores/event sourcing usam naturalmente um log append-only, e a Azure descreve o padrão Event Sourcing como uma sequência de eventos registrada em append-only store. Para auditoria criptográfica, a ideia de cadeia de hashes em event store também já aparece em implementações como EventSourcingDB, onde cada evento contém hash do predecessor para formar uma cadeia verificável. |
|
|
|
A política do Linus Salamander precisa receber um pedido assim: |
|
|
|
{ |
|
"operation": "decrypt", |
|
"purpose": "replay_for_agent", |
|
"tenant_id": "itarare", |
|
"agent_id": "LawyerWhatsAppAgent", |
|
"stream_id": "lawyer:123:contact:456", |
|
"event_id": "evt_01...", |
|
"key_id": "lsm:kek:tenant:itarare:agent:lawyer-whatsapp:v3", |
|
"requested_by": "LawyerDomainAgent", |
|
"session_proof": "DPoP/mTLS/passkey-bound-proof" |
|
} |
|
|
|
E o Linus Salamander só libera se a política bater: |
|
|
|
- agent autorizado; |
|
- tenant correto; |
|
- chave ativa; |
|
- sessão válida; |
|
- purpose permitido; |
|
- escopo permitido; |
|
- limite de tempo válido; |
|
- auditoria registrada; |
|
- não houve revogação. |
|
|
|
O ponto mais forte da sua arquitetura é: o Event Store local pode ser comprometido e mesmo assim o atacante só terá ciphertext + metadados mínimos + hashes. Sem Linus Salamander, ele não reconstrói o conteúdo. |
|
|
|
Para LGPD e direito ao esquecimento, Event Sourcing é delicado porque eventos são append-only. A solução prática é crypto-shredding: você mantém o evento por auditoria, mas destrói/revoga a chave que permite abrir o dado pessoal. Assim o log continua íntegro, mas o conteúdo sensível fica irrecuperável. Para arquiteturas event-driven, retenção e privacidade precisam ser pensadas explicitamente, porque event stores preservam histórico e não funcionam como CRUD comum. |
|
|
|
Então a regra seria: |
|
|
|
Nunca apagar evento por padrão. |
|
Apagar ou revogar a chave quando o dado pessoal precisar deixar de ser acessível. |
|
Adicionar evento de redaction/tombstone. |
|
Manter hash, metadados mínimos e prova de que existiu. |
|
|
|
Exemplo: |
|
|
|
ClientDocumentUploaded |
|
ClientDocumentExtracted |
|
ClientRequestedDeletion |
|
ClientDataCryptoShredded |
|
|
|
Depois do ClientDataCryptoShredded, o replay sabe que aquele dado foi destruído criptograficamente e não tenta reconstruir o payload. |
|
|
|
A fronteira final: |
|
|
|
Event Store: |
|
- guarda eventos cifrados |
|
- guarda hash chain |
|
- guarda metadados mínimos |
|
- guarda encrypted_dek |
|
- não guarda chave aberta |
|
|
|
Linus Salamander: |
|
- guarda/deriva KEKs |
|
- controla unwrap de DEKs |
|
- audita decrypt |
|
- aplica policies |
|
- rotaciona chaves |
|
- revoga acesso |
|
|
|
CryptoSidecar: |
|
- cifra/decifra localmente |
|
- usa chaves temporárias |
|
- apaga segredo da memória |
|
|
|
EventSourcingSidecar: |
|
- append-only |
|
- replay |
|
- snapshot |
|
- hash chain |
|
- projeções |
|
|
|
Minha recomendação de design para o seu padrão: |
|
|
|
Agent-local Encrypted Event Sourcing |
|
com Linus Salamander como Enclave Vault |
|
|
|
Ou em fórmula: |
|
|
|
EncryptedEvent(E) := |
|
AEAD_Encrypt( |
|
DEK_event, |
|
payload(E), |
|
AAD = agent_id ⊕ stream_id ⊕ event_id ⊕ event_type ⊕ previous_event_hash |
|
) |
|
|
|
StoredEvent(E) := |
|
metadata_plain(E) |
|
⊕ encrypted_dek(DEK_event, KEK_agent) |
|
⊕ ciphertext(E) |
|
⊕ hash_chain(E) |
|
⊕ signature(E) |
|
|
|
E o contrato arquitetural: |
|
|
|
Nenhum Agent grava evento em claro. |
|
Nenhum Event Store possui chave de decriptação. |
|
Nenhuma chave de evento vive além da operação. |
|
Nenhum replay sensível ocorre sem autorização do Linus Salamander. |
|
Nenhum payload é confiável sem validação AEAD + hash chain. |
|
|
|
Isso encaixa perfeitamente com seu modelo d |
|
e Agent + CryptoSidecar + Local |
|
EventSourceSidecar + Linus Salamander |
|
Enclave Vault. |
|
|
|
|
|
Quando o agente reinicia, ele não deve guardar chave em disco para continuar. Ele deve passar por um fluxo de re-hydration autorizada: lê os últimos eventos cifrados, prova sua identidade para o Linus Salamander, recebe autorização temporária para abrir só o necessário, reconstrói o estado e apaga as chaves da memória. |
|
|
|
O fluxo correto é este: |
|
|
|
Agent reinicia |
|
↓ |
|
Supervisor sobe primeiro |
|
↓ |
|
Agent prova identidade |
|
↓ |
|
CryptoSidecar abre sessão com Linus Salamander |
|
↓ |
|
EventSourcingSidecar lê últimos eventos cifrados |
|
↓ |
|
CryptoSidecar pede unwrap das chaves necessárias |
|
↓ |
|
Eventos são descriptografados em memória |
|
↓ |
|
Estado local/projeção é reconstruído |
|
↓ |
|
Chaves temporárias são descartadas |
|
↓ |
|
Agent volta a operar |
|
|
|
A regra central: |
|
|
|
O agente pode recuperar estado. |
|
Mas ele nunca “possui” a chave permanentemente. |
|
|
|
Na prática, você precisa de três coisas: snapshot cifrado, replay incremental e sessão curta de decrypt. |
|
|
|
O ideal é não descriptografar todos os eventos desde o começo sempre que reiniciar. O Agent mantém snapshots locais cifrados. |
|
|
|
Exemplo: |
|
|
|
event-001 |
|
event-002 |
|
event-003 |
|
... |
|
event-100 |
|
snapshot-100 |
|
event-101 |
|
event-102 |
|
event-103 |
|
|
|
Quando reinicia, ele faz: |
|
|
|
1. Abre o snapshot-100. |
|
2. Reaplica apenas event-101, event-102, event-103. |
|
3. Reconstrói o estado atual. |
|
|
|
Isso evita custo alto, reduz decrypt e melhora performance. |
|
|
|
A estrutura ficaria assim: |
|
|
|
LocalQBEQStore |
|
├── events/ |
|
│ ├── event-001.enc |
|
│ ├── event-002.enc |
|
│ └── event-103.enc |
|
│ |
|
├── snapshots/ |
|
│ └── snapshot-100.enc |
|
│ |
|
└── projections/ |
|
└── current_state.enc |
|
|
|
Mas atenção: snapshot também é dado sensível, então ele também precisa estar cifrado. |
|
|
|
A sequência de boot do Agent deveria ser formalizada: |
|
|
|
AgentBootIntent |
|
↓ |
|
Supervisor validates process identity |
|
↓ |
|
CryptoSidecar requests session from Linus Salamander |
|
↓ |
|
Linus Salamander validates: |
|
- agent_id |
|
- tenant_id |
|
- machine identity |
|
- mTLS certificate |
|
- DPoP proof |
|
- signed boot nonce |
|
- policy |
|
- key version |
|
↓ |
|
Linus Salamander grants short-lived decrypt session |
|
↓ |
|
EventSourcingSidecar rehydrates state |
|
↓ |
|
Agent enters READY state |
|
|
|
Eu modelaria isso como um evento também: |
|
|
|
AgentRestarted |
|
AgentRehydrationRequested |
|
AgentRehydrationAuthorized |
|
AgentStateRebuilt |
|
AgentReady |
|
|
|
Mas esses eventos de boot não devem expor segredo. Eles só registram auditoria. |
|
|
|
O pedido para o Linus Salamander seria algo assim: |
|
|
|
{ |
|
"operation": "rehydrate", |
|
"tenant_id": "itarare", |
|
"agent_id": "LawyerWhatsAppAgent:lawyer_123", |
|
"stream_scope": [ |
|
"lawyer_123:appointments", |
|
"lawyer_123:contacts", |
|
"lawyer_123:documents" |
|
], |
|
"from_snapshot": "snapshot_100", |
|
"events_after": 100, |
|
"purpose": "agent_restart_recovery", |
|
"session_ttl_seconds": 60, |
|
"proof": { |
|
"mtls": "...", |
|
"dpop": "...", |
|
"boot_nonce_signature": "..." |
|
} |
|
} |
|
|
|
O Linus Salamander não deve devolver “a chave mestra”. Ele deve devolver uma dessas opções: |
|
|
|
Opção A: DEKs temporariamente unwrapped para aquele escopo. |
|
Opção B: capability token que permite unwrap por evento durante 60 segundos. |
|
Opção C: decrypt streaming dentro do enclave, retornando plaintext só ao CryptoSidecar autorizado. |
|
|
|
Para o seu caso, eu escolheria a Opção B: |
|
|
|
Linus Salamander emite uma Rehydration Capability curta. |
|
|
|
Exemplo: |
|
|
|
{ |
|
"capability_id": "rehydrate_cap_01", |
|
"agent_id": "LawyerWhatsAppAgent:lawyer_123", |
|
"allowed_operation": "decrypt", |
|
"allowed_streams": [ |
|
"lawyer_123:appointments", |
|
"lawyer_123:contacts" |
|
], |
|
"max_events": 200, |
|
"expires_in_seconds": 60, |
|
"single_use": true |
|
} |
|
|
|
Aí o CryptoSidecar usa essa capability para abrir as chaves dos últimos eventos. Depois expira. |
|
|
|
O contrato de segurança fica: |
|
|
|
- capability curta; |
|
- escopo mínimo; |
|
- uso único ou limitado; |
|
- máximo de eventos; |
|
- purpose obrigatório; |
|
- auditoria obrigatória; |
|
- sem chave persistida; |
|
- sem decrypt fora do CryptoSidecar. |
|
|
|
Para evitar pedir chave evento por evento e ficar lento, você pode usar janelas de rehydration. |
|
|
|
Exemplo: |
|
|
|
snapshot key → abre snapshot atual |
|
stream key window → abre eventos dos últimos 15 minutos ou últimos N eventos |
|
document keys → só abre documentos se realmente precisar |
|
|
|
Então o agente não precisa abrir tudo: |
|
|
|
Atendente jurídico: |
|
- precisa abrir agenda recente; |
|
- precisa abrir contatos pendentes; |
|
- não precisa abrir todos os documentos antigos; |
|
- não precisa abrir conversas encerradas de meses atrás. |
|
|
|
Isso é importante. O Agent só deve descriptografar o working set, não o histórico inteiro. |
|
|
|
Eu dividiria o estado em três camadas: |
|
|
|
1. Hot State |
|
Estado necessário para continuar funcionando agora. |
|
Ex.: contatos pendentes, agenda do dia, últimos atendimentos. |
|
|
|
2. Warm State |
|
Estado acessado sob demanda. |
|
Ex.: histórico dos últimos 90 dias. |
|
|
|
3. Cold State |
|
Arquivo/auditoria. |
|
Ex.: documentos antigos, eventos encerrados, atendimentos arquivados. |
|
|
|
No boot, só abre o Hot State. |
|
|
|
AgentRestart |
|
↓ |
|
Decrypt Hot Snapshot |
|
↓ |
|
Replay Hot Events |
|
↓ |
|
Agent Ready |
|
|
|
O restante só abre quando necessário. |
|
|
|
A fórmula: |
|
|
|
Rehydrate(Agent) := |
|
Decrypt(snapshot_hot) |
|
⊕ Replay(events_after_snapshot) |
|
⊕ LazyDecrypt(warm/cold streams only when requested) |
|
|
|
Snapshots também devem ter hash chain: |
|
|
|
{ |
|
"snapshot_id": "snapshot_100", |
|
"agent_id": "LawyerWhatsAppAgent:lawyer_123", |
|
"covers_until_event": 100, |
|
"state_ciphertext": "...", |
|
"encrypted_dek": "...", |
|
"state_hash": "...", |
|
"last_event_hash": "...", |
|
"created_at": "..." |
|
} |
|
|
|
Na hora de restaurar: |
|
|
|
1. Verifica hash do snapshot. |
|
2. Verifica se snapshot aponta para o último event_hash conhecido. |
|
3. Descriptografa snapshot. |
|
4. Reaplica eventos posteriores. |
|
5. Confere se a projeção final bate. |
|
|
|
Se algo falhar: |
|
|
|
- snapshot corrompido; |
|
- hash chain inválido; |
|
- key version revogada; |
|
- evento faltando; |
|
- decrypt negado; |
|
|
|
O Agent não deve subir em modo normal. Ele sobe em modo degradado: |
|
|
|
AgentState = DEGRADED_RECOVERY |
|
|
|
E dispara: |
|
|
|
AgentRehydrationFailed |
|
|
|
Aí o Supervisor decide: |
|
|
|
- tentar snapshot anterior; |
|
- pedir reautorização; |
|
- sincronizar com backup remoto; |
|
- chamar humano; |
|
- isolar Agent. |
|
|
|
Para seu padrão, eu criaria estes estados canônicos: |
|
|
|
BOOTING |
|
AUTHENTICATING |
|
REHYDRATING |
|
REPLAYING |
|
READY |
|
DEGRADED |
|
SEALED |
|
FAILED |
|
|
|
SEALED é importante. Significa: |
|
|
|
O Agent está vivo, mas não recebeu autorização para descriptografar seu estado. |
|
|
|
Nesse modo ele pode responder: |
|
|
|
O atendimento está temporariamente indisponível para recuperação segura. Tente novamente em instantes ou aguarde retorno humano. |
|
|
|
Mas ele não inventa estado. |
|
|
|
Para o Event Store local, o processo fica assim: |
|
|
|
onAgentStart(): |
|
supervisor.start() |
|
identity = supervisor.proveAgentIdentity() |
|
cap = linusSalamander.requestRehydrationCapability(identity, scope) |
|
|
|
snapshot = localStore.loadLatestHotSnapshot(agent_id) |
|
plaintextState = cryptoSidecar.decrypt(snapshot, cap) |
|
|
|
events = localStore.loadEventsAfter(snapshot.last_event_id) |
|
for event in events: |
|
assert verifyHashChain(event) |
|
plaintextEvent = cryptoSidecar.decrypt(event, cap) |
|
plaintextState = apply(plaintextState, plaintextEvent) |
|
|
|
cryptoSidecar.zeroize() |
|
agent.setState(plaintextState) |
|
agent.ready() |
|
|
|
Em pseudocódigo mais próximo da arquitetura: |
|
|
|
async function rehydrateAgent(agentId: AgentId) { |
|
const identityProof = await Supervisor.proveIdentity(agentId) |
|
|
|
const capability = await LinusSalamander.requestCapability({ |
|
operation: "rehydrate", |
|
agentId, |
|
purpose: "agent_restart_recovery", |
|
ttlSeconds: 60, |
|
maxEvents: 200, |
|
proof: identityProof |
|
}) |
|
|
|
const snapshot = await LocalQBEQStore.loadLatestSnapshot(agentId, { |
|
scope: "hot" |
|
}) |
|
|
|
let state = await CryptoSidecar.decryptSnapshot(snapshot, capability) |
|
|
|
const events = await LocalQBEQStore.loadEventsAfter({ |
|
agentId, |
|
afterEventId: snapshot.lastEventId |
|
}) |
|
|
|
for (const encryptedEvent of events) { |
|
EventSourcingSidecar.verifyHashChain(encryptedEvent) |
|
|
|
const event = await CryptoSidecar.decryptEvent( |
|
encryptedEvent, |
|
capability |
|
) |
|
|
|
state = EventSourcingSidecar.apply(state, event) |
|
} |
|
|
|
await CryptoSidecar.zeroizeSession(capability) |
|
|
|
return state |
|
} |
|
|
|
A parte mais importante: state em claro deve existir só em memória e, mesmo assim, com cuidado. Se possível: |
|
|
|
- memória travada; |
|
- tempo curto; |
|
- sem swap; |
|
- zeroize após uso; |
|
- processo isolado; |
|
- permissões mínimas; |
|
- logs sem payload; |
|
- crash dump desativado para dados sensíveis. |
|
|
|
O erro que você deve evitar é este: |
|
|
|
Agent reiniciou |
|
↓ |
|
lê uma chave local salva |
|
↓ |
|
descriptografa tudo sozinho |
|
|
|
Isso quebra o conceito do Linus Salamander como Enclave Vault. |
|
|
|
O certo: |
|
|
|
Agent reiniciou |
|
↓ |
|
prova identidade |
|
↓ |
|
recebe autorização temporária |
|
↓ |
|
abre só o necessário |
|
↓ |
|
continua |
|
|
|
A decisão de design que eu adotaria: |
|
|
|
O Agent não recupera chaves. |
|
O Agent recupera capacidades temporárias de decriptação. |
|
|
|
Isso deixa a arquitetura mais forte. |
|
|
|
Nome do fluxo: |
|
|
|
Secure Agent Rehydration |
|
|
|
Ou no seu padrão: |
|
|
|
Linus Salamander Rehydration Protocol |
|
|
|
Contrato final: |
|
|
|
AgentRestart(E) := |
|
Proof(agent_identity) |
|
⊕ Capability(LinusSalamander, scope, ttl, purpose) |
|
⊕ Decrypt(hot_snapshot) |
|
⊕ Replay(encrypted_events_after_snapshot) |
|
⊕ Zeroize(session_keys) |
|
⊕ AgentReady |
|
|
|
E a regra canônica: |
|
|
|
Um Agent só pode continuar após reinício se conseguir reconstruir seu estado por replay autorizado, usando capability temporária emitida pelo Linus Salamander, sem persistir chaves locais e sem descriptografar mais dados do que o necessário. |
|
|
|
|
|
|
|
Sim, você está certo. A versão correta do seu stack não é “mTLS clássico + DPoP opcional”. No seu padrão, o mTLS precisa ser híbrido/pós-quântico, e ML-KEM + DPoP entram como comportamentos LinearAutoDestroy. |
|
|
|
A correção fica assim: |
|
|
|
Transporte Agent↔Agent: |
|
- TLS 1.3 |
|
- mTLS com certificado curto por Agent |
|
- Hybrid Key Exchange: X25519 + ML-KEM |
|
- DPoP obrigatório por request/intenção |
|
- ML-KEM LinearAutoDestroy |
|
- DPoP LinearAutoDestroy |
|
|
|
O ML-KEM é o padrão NIST FIPS 203 para key encapsulation pós-quântica, com parâmetros ML-KEM-512, ML-KEM-768 e ML-KEM-1024. Para o seu runtime, eu usaria ML-KEM-768 como padrão seguro/equilibrado. |
|
|
|
O DPoP é definido no RFC 9449 como mecanismo de proof-of-possession no nível da aplicação: o cliente prova posse de uma chave privada por request usando um JWT no header DPoP, reduzindo replay de tokens. |
|
|
|
A nova tabela canônica fica: |
|
|
|
Camada Algoritmo Uso |
|
|
|
Transporte TLS 1.3 Canal seguro base |
|
Autenticação mútua mTLS X.509 curto Agent A e Agent B se autenticam |
|
Key exchange clássico X25519 Segredo clássico efêmero |
|
Key exchange pós-quântico ML-KEM-768 Segredo resistente a quantum |
|
Combiner HKDF-SHA256 Combina X25519 + ML-KEM |
|
Canal TLS AES-256-GCM ou ChaCha20-Poly1305 Cifra do transporte |
|
Proof-of-possession DPoP Ed25519 ou ES256 Prova por request/intenção |
|
Assinatura Agent Ed25519 ou ML-DSA futuramente Autoria |
|
Payload XChaCha20-Poly1305 Cifra da mensagem |
|
Event Store Envelope Encryption DEK por evento/stream |
|
Vault Linus Salamander KEK, policy, certificados, capabilities |
|
|
|
|
|
A fórmula do handshake fica: |
|
|
|
AgentMTLSHandshake(A, B) := |
|
TLS1.3 |
|
⊕ mTLS(A_cert, B_cert) |
|
⊕ X25519_ECDHE(A, B) |
|
⊕ ML-KEM-768(A, B) |
|
⊕ HKDF-SHA256(X25519_secret ∥ MLKEM_secret) |
|
⊕ LinearAutoDestroy(X25519_secret) |
|
⊕ LinearAutoDestroy(MLKEM_secret) |
|
⊕ ChannelReady |
|
|
|
O ponto importante: X25519 continua, mas não sozinho. Ele entra em modo híbrido com ML-KEM. |
|
|
|
Por quê? |
|
|
|
X25519 protege contra atacantes clássicos atuais. |
|
ML-KEM protege contra harvest-now-decrypt-later. |
|
HKDF combina os dois segredos. |
|
Se um dos dois continuar seguro, a sessão continua protegida. |
|
|
|
O IETF tem trabalho específico para construção de hybrid key exchange em TLS 1.3, motivado pela transição para criptografia pós-quântica. |
|
|
|
Agora, sobre o LinearAutoDestroy: no seu modelo, ML-KEM e DPoP não são só algoritmos. Eles viram Semantic Behavior Types obrigatórios. |
|
|
|
ML-KEM como LinearAutoDestroy |
|
|
|
O segredo gerado pelo ML-KEM não pode sobreviver depois da derivação da chave de sessão. |
|
|
|
MLKEMSharedSecret : LinearAutoDestroy<Secret> |
|
|
|
Uso: |
|
|
|
1. Agent B possui ML-KEM public key. |
|
2. Agent A encapsula segredo para Agent B. |
|
3. Agent A obtém: |
|
- ciphertext_kem |
|
- shared_secret_mlkem |
|
4. Agent B decapsula ciphertext_kem. |
|
5. Ambos têm shared_secret_mlkem. |
|
6. HKDF combina shared_secret_mlkem + X25519_secret. |
|
7. shared_secret_mlkem é destruído imediatamente. |
|
|
|
Contrato: |
|
|
|
ML-KEM secret só pode ser usado uma vez: |
|
- input para HKDF; |
|
- nunca logar; |
|
- nunca persistir; |
|
- nunca serializar; |
|
- nunca reutilizar; |
|
- destruir após derivação. |
|
|
|
Em pseudo-tipo: |
|
|
|
type MLKEMSharedSecret = |
|
LinearAutoDestroy<SecretBytes, "ml-kem-shared-secret"> |
|
|
|
type HybridSessionKey = |
|
DerivedKey<"tls-agent-session"> |
|
|
|
Uso: |
|
|
|
const mlkemSecret = MLKEM.decapsulate(ciphertext, privateKey) |
|
// mlkemSecret: LinearAutoDestroy |
|
|
|
const x25519Secret = X25519.derive(ephemeralPrivate, peerPublic) |
|
// x25519Secret: LinearAutoDestroy |
|
|
|
const sessionKey = HKDF_SHA256.derive({ |
|
ikm: concat( |
|
mlkemSecret.readOnce(), |
|
x25519Secret.readOnce() |
|
), |
|
salt: handshakeTranscriptHash, |
|
info: "allascode-agent-mtls-hybrid-v1" |
|
}) |
|
|
|
mlkemSecret.destroy() |
|
x25519Secret.destroy() |
|
|
|
A regra semântica: |
|
|
|
Depois de HKDF, o segredo ML-KEM deixa de existir. |
|
|
|
DPoP como LinearAutoDestroy |
|
|
|
O DPoP também deve ser linear, porque cada prova deve valer para uma request/intenção, não para várias. |
|
|
|
DPoPProof : LinearAutoDestroy<JWTProof> |
|
|
|
Fluxo: |
|
|
|
1. Agent A vai chamar Agent B. |
|
2. Agent A cria DPoP proof para: |
|
- método; |
|
- URL/canal; |
|
- intent; |
|
- nonce; |
|
- timestamp; |
|
- body_hash; |
|
- message_id. |
|
3. Agent A assina a prova. |
|
4. Agent B valida. |
|
5. Agent B registra jti como usado. |
|
6. A prova é destruída. |
|
7. Reuso do mesmo jti = replay bloqueado. |
|
|
|
Payload DPoP: |
|
|
|
{ |
|
"typ": "dpop+jwt", |
|
"alg": "EdDSA", |
|
"jwk": "public-key-or-thumbprint", |
|
"htm": "POST", |
|
"htu": "agent://itarare/lawyer-123/LawyerAgendaAgent", |
|
"iat": 1782677105, |
|
"jti": "proof_01HX...", |
|
"ath": "access-token-hash", |
|
"intent": "AppointmentRequested", |
|
"body_hash": "sha256..." |
|
} |
|
|
|
Contrato: |
|
|
|
DPoP proof só pode ser consumido uma vez: |
|
- TTL curto; |
|
- jti único; |
|
- preso ao destino; |
|
- preso ao método; |
|
- preso ao intent; |
|
- preso ao hash do payload; |
|
- destruído após validação. |
|
|
|
Em pseudo-tipo: |
|
|
|
type DPoPProof = |
|
LinearAutoDestroy<SignedJWT, "dpop-proof"> |
|
|
|
function createDPoP(intent, payloadHash): DPoPProof { |
|
return signOnce({ |
|
htm: "POST", |
|
htu: intent.targetAgentUri, |
|
intent: intent.name, |
|
body_hash: payloadHash, |
|
jti: randomId(), |
|
iat: now() |
|
}) |
|
} |
|
|
|
mTLS final com ML-KEM + DPoP |
|
|
|
O fluxo completo Agent A → Agent B: |
|
|
|
1. Agent A pede certificado curto ao Linus Salamander. |
|
2. Agent B também tem certificado curto. |
|
3. Agent A abre conexão TLS 1.3 mTLS com Agent B. |
|
4. Handshake usa X25519 + ML-KEM-768. |
|
5. HKDF deriva chave de sessão híbrida. |
|
6. X25519_secret é destruído. |
|
7. MLKEM_secret é destruído. |
|
8. Agent A monta payload. |
|
9. Payload é cifrado com XChaCha20-Poly1305. |
|
10. Agent A cria DPoP proof linear para aquela intenção. |
|
11. Agent A assina envelope. |
|
12. Agent B valida mTLS, policy, DPoP, assinatura e AEAD. |
|
13. Agent B registra jti usado. |
|
14. Agent B grava evento cifrado no Event Store. |
|
15. DPoP proof é destruído. |
|
|
|
Fórmula canônica: |
|
|
|
SecureAgentCall(A → B) := |
|
HybridMTLS( |
|
TLS1.3, |
|
mTLS(A_cert, B_cert), |
|
X25519, |
|
ML-KEM-768::LinearAutoDestroy |
|
) |
|
⊕ PolicyCheck(A, B, Intent) |
|
⊕ PayloadEncrypt(XChaCha20-Poly1305) |
|
⊕ DPoP(Intent, PayloadHash)::LinearAutoDestroy |
|
⊕ Signature(Ed25519) |
|
⊕ EventStoreAppend(EncryptedEvent) |
|
|
|
Agora, o papel do Linus Salamander: |
|
|
|
Linus Salamander: |
|
- emite certificado curto por Agent; |
|
- publica trust bundle; |
|
- guarda policy Agent↔Agent; |
|
- registra chave pública DPoP do Agent; |
|
- emite capability curta; |
|
- valida rotação; |
|
- revoga agentes; |
|
- protege KEKs do Event Store; |
|
- audita decrypt, sign, cert issue e replay. |
|
|
|
Eu definiria três tipos de identidade por Agent: |
|
|
|
1. mTLS Identity |
|
Usada para canal. |
|
|
|
2. DPoP Identity |
|
Usada para provar posse por request/intenção. |
|
|
|
3. Event Signing Identity |
|
Usada para assinar eventos persistidos. |
|
|
|
Elas podem derivar de uma raiz do Agent, mas eu prefiro separar: |
|
|
|
agent_mtls_key |
|
agent_dpop_key |
|
agent_event_signing_key |
|
|
|
Porque o comprometimento de uma não quebra tudo. |
|
|
|
Stack corrigida |
|
|
|
Transporte: |
|
- TLS 1.3 |
|
- mTLS com certificados X.509 curtos por Agent |
|
- Hybrid KEX: X25519 + ML-KEM-768 |
|
- HKDF-SHA256 como combiner |
|
- AES-256-GCM ou ChaCha20-Poly1305 no canal TLS |
|
|
|
LinearAutoDestroy no transporte: |
|
- X25519 ephemeral secret |
|
- ML-KEM shared secret |
|
- Hybrid handshake secret |
|
- Session resumption secret, se existir |
|
|
|
Payload Agent↔Agent: |
|
- XChaCha20-Poly1305 para cifrar mensagens |
|
- Ed25519 para assinar envelope |
|
- X25519 opcional para payload E2E quando passar por broker |
|
- HKDF-SHA256 para derivar message keys |
|
- BLAKE3 ou SHA-256 para hash de payload/evento |
|
|
|
Event Store: |
|
- XChaCha20-Poly1305 ou AES-256-GCM |
|
- Envelope Encryption |
|
- DEK por evento/stream |
|
- KEK protegida pelo Linus Salamander |
|
- Hash chain com SHA-256 ou BLAKE3 |
|
|
|
Proof-of-possession: |
|
- DPoP obrigatório |
|
- Assinatura Ed25519 ou ES256 |
|
- DPoPProof como LinearAutoDestroy |
|
- jti single-use |
|
- TTL curto |
|
- body_hash obrigatório |
|
- intent obrigatório |
|
|
|
A frase correta da arquitetura: |
|
|
|
Entre agentes, o canal é protegido por TLS 1.3 mTLS híbrido com X25519 + ML-KEM-768, ambos efêmeros, e toda chamada carrega uma prova DPoP LinearAutoDestroy vinculada ao intent, ao destino e ao hash do payload. |
|
|
|
E o contrato mais forte: |
|
|
|
Nenhum Agent fala sem mTLS. |
|
Nenhum mTLS é apenas clássico. |
|
Nenhum ML-KEM secret sobrevive ao HKDF. |
|
Nenhum DPoP pode ser reutilizado. |
|
Nenhum payload sensível depende só do transporte. |
|
Nenhum evento entra no Event Store sem assinatura, hash chain e criptografia. |
|
|
|
Essa é a versão alinhada com o que vocês já tinham definido: ML-KEM + DPoP como QuarkBehavior/LinearAutoDestroy dentro do mTLS Agent↔Agent. |
|
|
|
|
|
|
|
|
|
|