Guia para implementadores
Como construir uma implementação em conformidade com o Label 309 — a arquitetura em camadas recomendada, o contrato de bytes idênticos entre linguagens e os vetores de teste de conformidade que definem a interoperabilidade.
O Label 309 é um formato de transmissão e um conjunto de construções criptográficas, não um produto. Qualquer número de implementações independentes — em TypeScript, Python, Rust, Go ou em um runtime móvel nativo — pode coexistir, e um registro produzido por uma delas DEVE ser verificável por outra. Esta página é destinada à equipe que constrói uma implementação desse tipo. Ela descreve a arquitetura que mantém a superfície criptográfica auditável, o contrato exato que torna duas implementações interoperáveis e o conjunto de testes de conformidade que decide, de forma mecânica, se você o cumpriu.
Duas coisas tornam o Label 309 interoperável entre linguagens. A primeira é o determinismo: as construções estão fixadas a padrões públicos (RFC 8949 CBOR canônico, RFC 8032 Ed25519, RFC 7748 X25519, RFC 5869 HKDF, RFC 9106 Argon2id, RFC 9052 COSE), de modo que as mesmas entradas produzem os mesmos bytes em toda parte. A segunda é o conjunto de testes de conformidade: um conjunto de vetores de teste exatos a nível de byte que uma implementação ou reproduz, ou não. Conformidade é uma propriedade que se pode verificar, não uma afirmação que se faz.
A arquitetura em camadas
Uma implementação em conformidade DEVERIA separar as primitivas criptográficas da lógica de aplicação em camadas distintas, cada uma dependendo apenas da imediatamente inferior. Os nomes abaixo são papéis, não nomes de pacotes; escolha os seus.
┌─────────────────────────────────────────────────────────┐
│ application │
│ UI, routing, persistence, payments, background jobs │
├─────────────────────────────────────────────────────────┤
│ SDK │
│ service client + standalone verifier + helpers │
├─────────────────────────────────────────────────────────┤
│ wire-format library │
│ schema · structural validator · canonical-CBOR codec │
├─────────────────────────────────────────────────────────┤
│ cryptographic core │
│ hashes · KDFs · signatures · KEM · AEAD · CBOR · COSE │
│ no application or framework dependencies │
└─────────────────────────────────────────────────────────┘As fronteiras entre as camadas são estruturais, não cosméticas. Cada camada tem uma única função e uma lista curta de coisas que lhe é proibido conhecer.
O núcleo criptográfico
A camada de baixo contém apenas primitivas: funções de hash, KDFs, operações de assinatura e de KEM, a camada de conteúdo AEAD, o CBOR canônico, o COSE_Sign1, a construção de envelopamento e desenvelopamento da prova de existência selada, raízes e provas de Merkle, e as classes de erro tipadas que elas levantam. Não contém nenhuma lógica de domínio, nenhum HTTP, nenhum acesso a banco de dados e nenhuma importação de bibliotecas de UI ou de frameworks de servidor.
Esta camada DEVE permanecer livre de qualquer dependência ligada à aplicação ou ao servidor, e DEVE ser segura para uso no navegador, por três razões concretas:
- Ela roda em todo lugar. Gerar o hash de um arquivo, montar um envelope e — ponto crítico — o próprio verificador independente rodam no navegador, em workers serverless e na linha de comando com a mesma facilidade que em um servidor. Uma dependência restrita ao servidor (um driver de banco de dados, um framework de logging atrelado a um runtime, uma biblioteca de UI) quebraria esses alvos e inflaria todo consumidor que empacota o núcleo.
- Ela é a superfície de auditoria. Um revisor consegue ler um pacote feito só de primitivas de ponta a ponta, comparando-o com os RFCs. No instante em que código de aplicação se infiltra, a superfície que um revisor de segurança precisa manter em mente cresce sem limite.
- Ela é o que terceiros incorporam. Um verificador independente — alguém que não confia em nenhum serviço, apenas na cadeia — puxa esta camada e nada acima dela. Mantê-la pequena e portátil é o que torna "verifique você mesmo" algo prático.
Concretamente, o núcleo NÃO DEVE importar ORMs ou drivers de banco de dados,
frameworks de UI, frameworks de logging atrelados ao servidor, nem qualquer módulo
de aplicação. A aleatoriedade DEVE vir do CSPRNG da plataforma (getRandomValues
do Web Crypto, ou um reexport equivalente), nunca de uma fonte exclusiva do Node, de
modo que a mesma fonte rode sem alteração em um navegador.
Garanta a fronteira na CI, não na revisão de código
A regra de zero dependências se deteriora no momento em que uma importação conveniente se infiltra. Uma implementação DEVERIA rodar uma análise do grafo de dependências que percorre toda importação no núcleo e na biblioteca de formato de transmissão e faz a build falhar diante de qualquer especificador fora de uma lista de permissões por camada. Revisores esquecem; o linter não.
A biblioteca de formato de transmissão
A camada imediatamente acima é dona do próprio Label 309: o esquema do registro, o validador estrutural e o codificador e decodificador de CBOR canônico. Ela depende do núcleo criptográfico (para hashing, COSE e o codec CBOR) e de nada mais ligado à aplicação. Sua superfície é pequena e pura:
- encode — produz os bytes em CBOR canônico para um registro validado.
- decode — o inverso.
- validate — executa as verificações estruturais e semânticas do padrão sobre um registro decodificado e retorna um resultado tipado (ver Verificação).
É nesta camada que as regras de O registro vivem como código: o
conjunto fechado de chaves, a disciplina de reagrupamento de fragmentos, o invariante
items-ou-merkle, os requisitos de CBOR canônico. Assim como o núcleo, ela se
mantém livre de clientes HTTP, drivers de banco de dados e importações de frameworks.
O SDK e a aplicação
O SDK encapsula as camadas inferiores em utilitários ergonômicos — um cliente do serviço, utilitários de montagem e desbloqueio de envelopes e o verificador independente, a função que decodifica um registro, verifica sua estrutura, confere quaisquer assinaturas no nível do registro contra a chave registrada on-chain e produz um veredito usando apenas dados públicos. O verificador independente DEVE funcionar sem acesso de rede a qualquer serviço operado por um implementador; sua única entrada externa é um explorador de blockchain público escolhido pelo verificador. O SDK DEVERIA permanecer igualmente seguro para uso no navegador.
A camada de aplicação — UI, roteamento, persistência, faturamento, jobs em segundo plano — é terreno livre e não carrega nenhuma obrigação de interoperabilidade. Nada no padrão restringe como você a constrói, apenas que ela se assenta acima da superfície criptográfica verificada, em vez de penetrar nela.
O contrato de bytes idênticos
Interoperabilidade é uma propriedade dos bytes, não das intenções. Duas implementações interoperam se, e somente se, as primitivas que não têm liberdade em sua saída produzem os mesmos bytes a partir das mesmas entradas. Este é o contrato de paridade, e é o coração da conformidade.
O contrato se divide claramente em dois. As operações cuja saída é inteiramente determinada por suas entradas DEVEM ser idênticas a nível de byte entre as implementações. As operações que consomem aleatoriedade não podem ser iguais byte a byte de uma chamada para outra; para essas, o contrato é a consumibilidade cruzada — um valor produzido por uma implementação DEVE poder ser consumido por qualquer outra (um texto cifrado selado em uma linguagem é decifrado em outra).
Primitivas idênticas a nível de byte
Toda operação abaixo é uma função pura de suas entradas e DEVE emitir uma saída idêntica a nível de byte em toda implementação em conformidade:
| Primitiva | Fixada a | Saída que deve coincidir |
|---|---|---|
| Semente → par de chaves Ed25519 / X25519 | HKDF-SHA-256 com as constantes de info registradas | chaves pública e privada derivadas |
| HKDF-SHA-256 | RFC 5869 | material de chave de saída para entrada fixa |
| MAC de conjunto de slots HMAC-SHA-256 | RFC 2104 | bytes de slots_hash e da tag slots_mac para uma CEK e um conjunto de slots fixos |
| Argon2id (KDF de frase secreta) | RFC 9106 | chave derivada para (m, t, p, salt, len, password) fixos |
| SHA-256 | FIPS 180-4 | digest |
| BLAKE2b-256 | RFC 7693 | digest |
| Codificação CBOR canônica | RFC 8949 §4.2.1 | bytes codificados para entrada fixa |
| Codificação COSE_Sign1 | RFC 9052 | bytes da estrutura para cabeçalho, carga útil e assinatura fixos |
| Assinatura / verificação Ed25519 | RFC 8032 (estrito) | assinatura; veredito |
| ECDH X25519 | RFC 7748 | segredo compartilhado para escalares fixos |
| Envelopamento / desenvelopamento da prova de existência selada | Prova de existência selada | bytes por slot e MAC quando os efêmeros e a CEK são injetados |
| Raiz de Merkle + provas de inclusão | RFC 9162 §2.1.1 | raiz e provas por folha sobre uma lista ordenada de folhas |
Dois pontos merecem destaque. O Ed25519 é estrito: um verificador em
conformidade DEVE aplicar as regras de S canônico e de rejeição de pontos de
ordem baixa do RFC 8032 §5.1.7,
de modo que duas implementações concordem não só sobre as assinaturas que aceitam,
mas também sobre as que rejeitam. O Argon2id atravessa fronteiras de ecossistemas:
linguagens diferentes recorrem a bibliotecas Argon2 diferentes, mas toda biblioteca em
conformidade implementa o RFC 9106 e DEVE produzir uma saída idêntica para
parâmetros idênticos — o conjunto de parâmetros, e não a biblioteca, é o contrato.
Operações que consomem aleatoriedade
A geração de chaves, o envelopamento da prova de existência selada sob efêmeros novos por slot e a criptografia de envelopes recorrem todos a aleatoriedade nova, de modo que sua saída difere a cada chamada e não pode ser fixada a nível de byte. O contrato para essas operações é a consumibilidade cruzada: a saída produzida por uma implementação DEVE poder ser consumida por qualquer outra. Um registro selado em uma linguagem DEVE ser decifrado em outra; um par de chaves gerado em uma DEVE ser verificável e servir de destino de criptografia em outra. Os conjuntos de teste de conformidade fixam essas operações com ganchos de teste determinísticos que injetam os efêmeros — tornando o envelopamento reproduzível — e com fixtures de ida e volta que criptografam em uma linguagem e decifram na outra.
Construindo a construção da prova de existência selada
A prova de existência selada é a parte mais densa do formato de transmissão, e aquela em que um único byte errado — uma chave de mapa fora de ordem, um rótulo com um caractere a mais, uma fragmentação não canônica — produz um envelope que abre na sua própria implementação, mas em nenhuma outra. Esta seção é a lista de verificação da construção: as receitas exatas, os dados autenticados adicionais que cada AEAD cobre, o laço de tentativa de decifragem e as proteções que todo produtor e todo verificador devem aplicar. A referência da construção em Prova de existência selada é a prosa; aqui está como você a liga para deixar o gate de paridade verde. Fixe estes rascunhos externos com exatidão, pois suas internas determinam bytes que você precisa reproduzir:
chacha20-poly1305-stream64k— o formato de conteúdo — é o ChaCha20-Poly1305 (RFC 8439) no layout STREAM segmentado de 64 KiB da especificação age v1. Fixe com exatidão o tamanho do bloco (65536), o nonce por bloco de 12 bytesuint88_be(counter) ‖ final_flag, o AAD por bloco vazio e a regra do sinalizador final — eles determinam bytes que você precisa reproduzir.- X-Wing (o KEM
mlkem768x25519) é o draft-connolly-cfrg-xwing-kem-10. Trate-o como um KEM de caixa preta: a construção vincula a chave pública do destinatário e o texto cifrado à própria etapa de derivação de chave, de modo que não se apoia em nenhuma propriedade do hashing interno do combinador.XWing.EncapsulateDEVE aplicar a verificação de validade de chave pública da revisão fixada e recusar encapsular para uma chave que falhe nela; o piso de "nunca abaixo da segurança clássica do X25519" é restrito a chaves geradas validamente, e pular a verificação abdica do piso para aquele destinatário. Os vetores KEM de conformidade fixam o encapsulamento contra o draft-10, então uma divergência de revisão do rascunho aparece de imediato.
Uma CEK, dois caminhos de entrega de chave
Um registro selado criptografa o texto claro uma única vez sob uma única chave de criptografia de conteúdo (CEK) e, em seguida, entrega essa CEK por um de dois caminhos mutuamente exclusivos, distinguidos pela presença dos campos — não há tag de modo:
- caminho de slots — a CEK é envolvida de forma independente para cada
destinatário sob uma chave de criptografia de chave por slot.
enccarregaslots(ekem,slots_mac). - caminho de frase secreta — a CEK é derivada diretamente de uma frase secreta
normalizada via Argon2id.
enccarregapassphrase; não carregakem,slotsnemslots_mac.
Ambos os caminhos compartilham enc.scheme (sempre 1; rejeite qualquer outra
coisa), enc.aead (chacha20-poly1305-stream64k) e enc.nonce (24 bytes). Eles
diferem em onde mora o compromisso da chave: on-chain em slots_mac no caminho de
slots, em um cabeçalho de 32 bytes dentro do blob de texto cifrado no caminho de
frase secreta. Ambos vinculam a alegação de hash do item em sua transcrição, e ambos
selam o conteúdo no mesmo STREAM segmentado; a diferença está na entrega da chave e no
compromisso, não na camada de conteúdo.
Envoltório por slot (caminho de slots)
Escolha um KEM para o registro inteiro — nunca misture KEMs dentro de um mesmo
slots[]. Para cada um dos N destinatários, derive uma chave de criptografia de chave
por slot nova e envolva a mesma CEK sob ela com ChaCha20-Poly1305 a um nonce de
12 bytes de zeros, com o AAD definido como o literal do rótulo info daquele KEM
(nunca AAD vazio), produzindo exatamente 48 bytes (texto cifrado da CEK de 32 bytes +
tag de 16 bytes). O nonce de zeros só é seguro porque a chave de criptografia de chave
é por slot; veja a proteção de unicidade adiante.
x25519 (clássico). Par de chaves X25519 efêmero novo por slot:
priv_epk : randomBytes(32) ; fresh per slot
pub_epk : x25519_publicKey(priv_epk)
shared : x25519_sharedSecret(priv_epk, pub_R) ; reject all-zero result
kek_salt : SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R) ; 32 B
KEK : HKDF-SHA-256(ikm = shared, salt = kek_salt,
info = "cardano-poe-kek-v1", L = 32)
wrap : ChaCha20-Poly1305(key = KEK, nonce = zeros(12),
ad = "cardano-poe-kek-v1", plaintext = CEK) ; 48 B
slot : { "epk": pub_epk, "wrap": wrap }mlkem768x25519 (híbrido; X-Wing). Encapsulamento X-Wing novo por slot:
enc = XWing.Encapsulate(pub_R) ; named fields — MUST NOT consume positional order
kem_ct = enc.ct ; 1120 B
shared = enc.ss ; 32 B
kek_salt : SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R) ; 32 B
KEK : HKDF-SHA-256(ikm = shared, salt = kek_salt,
info = "cardano-poe-kek-mlkem768x25519-v1", L = 32)
wrap : ChaCha20-Poly1305(key = KEK, nonce = zeros(12),
ad = "cardano-poe-kek-mlkem768x25519-v1", plaintext = CEK)
slot : { "kem_ct": kem_ct, "wrap": wrap } ; kem_ct = single 1120-byte byte stringAmbos os salts têm uma única forma — SHA-256(label || enc.nonce || <material de KEM do slot> || pub_R) —, carregando o efêmero pub_epk de 32 bytes no caminho clássico
e o texto cifrado X-Wing kem_ct de 1120 bytes no caminho híbrido; || é a
concatenação de bytes, e cada literal de prefixo de salt é ASCII exato, sem
terminador nem prefixo de comprimento. pub_R é a chave de transmissão canônica do
destinatário (32 B para x25519, os 1216 B fixados para mlkem768x25519). O slot
híbrido não carrega um epk separado — o efêmero X25519 são os 32 bytes finais de
kem_ct — e kem_ct é uma única string de bytes CBOR de exatamente 1120 bytes:
apenas o corpo inteiro do registro é fragmentado para transporte, nunca um campo
individual.
O salt vincula três valores: o material de KEM do slot (KEK única por slot), pub_R
(frustrando o repasse por delegado confuso contra um destinatário diferente) e
enc.nonce (ancorando a KEK a um único envelope, de modo que aleatoriedade de KEM
repetida só degrade para vinculabilidade entre envelopes). Os rótulos info distintos
dão separação de domínio entre KEMs, de modo que nenhuma KEK derivada sob um KEM possa
ser igual a uma derivada sob o outro a partir de um segredo compartilhado idêntico.
Use cada um dos onze rótulos internos byte a byte — cardano-poe-kek-v1,
cardano-poe-kek-mlkem768x25519-v1, cardano-poe-x25519-kek-salt-v1,
cardano-poe-xwing-kek-salt-v1, cardano-poe-item-hashes-v1,
cardano-poe-slots-transcript-v1, cardano-poe-slots-mac-v1,
cardano-poe-passphrase-transcript-v1, cardano-poe-passphrase-mac-v1,
cardano-poe-payload-v1, cardano-poe-payload-passphrase-v1. Nenhum é jamais
serializado em trânsito; são constantes fixas, não selecionáveis por registro. Um
único byte divergente produz um slots_mac, um compromisso ou uma tag AEAD que o
produtor honesto não consegue reproduzir.
Embaralhe antes de calcular o MAC. A ordem de entrada ("destinatário principal
primeiro") é um metadado privilegiado; publicar os slots na ordem de entrada o vaza.
Embaralhe slots[] com um CSPRNG usando uma permutação Fisher-Yates sem viés — um
simples sorteio de índice u32 % m pende para resíduos baixos e precisa de amostragem
por rejeição até um índice uniforme — antes de calcular o MAC do conjunto de
slots, que vincula a ordem embaralhada presente em trânsito.
MAC do conjunto de slots: gere o hash da transcrição e depois aplique o HMAC sob a CEK
O MAC do conjunto de slots vincula o conjunto de slots inteiro, mais os campos de cabeçalho que fixam como os slots são lidos, à CEK. Construa-o em duas etapas — gere o hash de uma transcrição fechada uma vez e depois aplique o HMAC a esse hash:
hashes_hash : SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes)) ; 32 B
SLOTS_TRANSCRIPT = { ; closed 7-key map; keys are a set, not an order
"scheme": 1,
"path": "slots",
"aead": <enc.aead>, ; the content-format identifier
"kem": <enc.kem>, ; "x25519" | "mlkem768x25519"
"nonce": <enc.nonce>, ; bytes(24)
"slots": <slots>, ; the shuffled on-wire slot array
"hashes_hash": hashes_hash ; bytes(32), over this item's hashes
}
slots_hash : SHA-256("cardano-poe-slots-transcript-v1" || canonicalEncode(SLOTS_TRANSCRIPT))
HMAC_KEY : HKDF-SHA-256(ikm = CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
slots_mac : HMAC-SHA-256(key = HMAC_KEY, msg = slots_hash) ; 32 BTrês coisas determinam a paridade aqui:
- A transcrição é um mapa fechado serializado por
canonicalEncode. Sua ordem de chaves é a ordenação da RFC 8949 §4.2.1, nunca arranjada à mão. Fixarscheme,path,aead,kemenonceao lado dos slots significa que um repasse que altere qualquer campo de cabeçalho — mesmo deixando as formas dos slots válidas — mudaslots_hashe quebra o MAC. - A transcrição vincula a alegação de hash do item.
hashes_hashé um SHA-256 rotulado sobre ocanonicalEncodedo mapahashescompleto do item. Como o destinatário recalculaslots_macapenas a partir dos bytes on-chain, uma correspondência de MAC confirma que o envelope foi selado para esta exata alegação de hash — um envelope colado em um item com um mapahashesdiferente falha na etapa de correspondência on-chain, antes de qualquer busca de texto cifrado. O valor deslotsé o array embaralhado de mapas de slot em trânsito diretamente: cada campo de slot é uma única string de bytes (epk32 B,kem_ct1120 B), de modo que não há fragmentação por campo a canonicalizar. slots_hashé calculado uma vez e mantido constante ao longo do laço de tentativa de decifragem. A verificação de MAC por slot redefine a chave do HMAC a partir de cada CEK candidata, mas sempre sobre o mesmoslots_hashde 32 bytes. Gerar o hash de antemão deixa intacto o compromisso firmado com a chave CEK: ele muda a mensagem do HMAC da transcrição completa para o seu SHA-256, nada mais.
O algoritmo do MAC, sua derivação de chave e o esquema da transcrição são todos
fixados por enc.scheme = 1 e idênticos para ambos os KEMs; não há identificador de
MAC em trânsito. slots_mac tem exatamente 32 bytes e é verificado em tempo constante.
Criptografia de conteúdo: o STREAM segmentado
Criptografe o texto claro uma única vez no STREAM segmentado sob uma chave de
conteúdo derivada da CEK. A chave de conteúdo é uma folha HKDF separada da CEK — com
salt enc.nonce, sob um info específico do caminho —, de modo que a camada de
envoltório e a camada de conteúdo nunca usam os mesmos bytes como chave da mesma
primitiva:
content_key : HKDF-SHA-256(ikm = CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)
; STREAM (chacha20-poly1305-stream64k):
CHUNK_SIZE : 65536 plaintext bytes per non-final chunk
chunk nonce : uint88_be(counter) || final_flag ; 12 B; counter from 0, +1 per chunk;
; final_flag = 0x01 on the last chunk, else 0x00
per-chunk AAD : empty
ciphertext : seal(chunk_0) || seal(chunk_1) || ... || seal(chunk_final)
; each chunk sealed with ChaCha20-Poly1305 under content_key; sealed = plaintext + 16 BO AAD por bloco é vazio — e isso está correto, não é uma omissão: a chave de
conteúdo deriva da CEK, e a CEK já está comprometida com o cabeçalho completo
(incluindo hashes_hash) por slots_mac. Inverta qualquer campo de cabeçalho e o
destinatário deriva uma chave de conteúdo diferente, de modo que o fluxo não abre; um
AAD por bloco revincularia o mesmo contexto em cada bloco sem acrescentar segurança. Os
nonces de contador são seguros porque a chave de conteúdo é de uso único (uma CEK nova
com salt enc.nonce único do envelope), de modo que dois fluxos nunca compartilham um
par (key, nonce).
Construa o STREAM de modo que o truncamento seja detectável: todo bloco não final tem
exatamente 65536 bytes de texto claro, o bloco final carrega final_flag = 0x01 e de
0 a 65536 bytes (um texto claro vazio é um único bloco final de comprimento zero — uma
tag solitária de 16 bytes), e um verificador DEVE falhar (TAMPERED_CIPHERTEXT) diante
de um sinalizador final ausente, um sinalizador final em um bloco não final, dados após
o bloco final ou um bloco não final curto. Verifique a tag de cada bloco antes de
liberar seu texto claro, e trate os bytes liberados como provisórios até que o
recálculo de hash pós-decifragem passe.
O texto claro são os bytes exatos do conteúdo original; a construção não antepõe,
acrescenta nem criptografa nome de arquivo, tipo MIME, campo de tamanho ou invólucro
de metadados. O blob de texto cifrado publicado são os blocos do STREAM (no caminho de
frase secreta, precedidos pelo cabeçalho de compromisso de 32 bytes abaixo). O mapa
enc montado e a URI resultante vão para a cadeia; os bytes do texto cifrado não —
publique-os em um armazenamento endereçado por conteúdo e coloque a URI ar:// ou
ipfs:// no uris[] do item.
Caminho de frase secreta
Quando não há destinatários, derive a CEK de uma frase secreta normalizada com
Argon2id. Não há epk, não há envoltório por slot, não há MAC do conjunto de slots e
não há laço de tentativa de decifragem. O compromisso de chave que slots_mac fornece
no caminho de slots mora, em vez disso, em um cabeçalho de 32 bytes dentro do blob de
texto cifrado, anteposto antes dos blocos do STREAM:
passphrase_bytes = utf8(normalize(passphrase)) ; cardano-poe-pw-norm-v1
CEK = argon2id(passphrase_bytes, salt = enc.passphrase.salt,
params = enc.passphrase.params, L = 32)
hashes_hash = SHA-256("cardano-poe-item-hashes-v1" || canonicalEncode(item.hashes))
PASSPHRASE_TRANSCRIPT = { ; closed 6-key map; keys are a set, not an order
"scheme": 1,
"path": "passphrase",
"aead": <enc.aead>,
"nonce": <enc.nonce>, ; bytes(24)
"hashes_hash": hashes_hash, ; bytes(32), over this item's hashes
"passphrase": { ; closed sub-map
"alg": "argon2id",
"salt": enc.passphrase.salt,
"params": { "m": m, "t": t, "p": p },
"normalization": "cardano-poe-pw-norm-v1" ; scheme-fixed constant, NOT on the wire
}
}
pw_hash = SHA-256("cardano-poe-passphrase-transcript-v1" || canonicalEncode(PASSPHRASE_TRANSCRIPT))
PW_MAC_KEY = HKDF-SHA-256(ikm = CEK, salt = "", info = "cardano-poe-passphrase-mac-v1", L = 32)
commitment = HMAC-SHA-256(key = PW_MAC_KEY, msg = pw_hash) ; 32 B
content_key = HKDF-SHA-256(ikm = CEK, salt = enc.nonce, info = "cardano-poe-payload-passphrase-v1", L = 32)
ciphertext blob = commitment || STREAM chunks ; STREAM under content_keyA PASSPHRASE_TRANSCRIPT vincula os parâmetros do KDF, os campos de cabeçalho e a
alegação de hash do item ao compromisso: adulterar salt, qualquer valor de params,
nonce, aead, ou colar o envelope em uma alegação de hash diferente produz um
pw_hash diferente e a verificação do compromisso falha. O valor de "normalization"
é uma constante fixada pelo esquema alimentada na transcrição para fixar o perfil
exato sob o qual a CEK foi derivada; ele nunca é serializado em trânsito (o produtor
emite apenas { alg, salt, params }).
No lado da verificação, derive a CEK candidata, leia os 32 bytes iniciais do blob
de texto cifrado, recalcule o compromisso e compare em tempo constante antes de abrir
qualquer bloco do STREAM. Um blob com menos de 48 bytes (compromisso de 32 bytes +
STREAM mínimo de 16 bytes) é malformado (TAMPERED_CIPHERTEXT). Em caso de divergência
— frase secreta errada, salt / params / cabeçalho adulterados, ou um envelope colado
— exponha a mesma falha genérica única e não comece a transmitir; uma frase secreta
errada é indistinguível de um registro adulterado. O compromisso é deliberadamente
off-chain: um compromisso on-chain seria um oráculo gratuito de adivinhação offline para
todo registro de frase secreta, inclusive aqueles cujo texto cifrado é retido.
Aplique os pisos de parâmetros: comprimento de salt de 16 a 64 bytes; m ≥ 65536
KiB (≈ 64 MiB), t ≥ 3, p ≥ 1. Fixe a versão do Argon2 em 0x13 (19); nenhuma outra
versão é admissível sob enc.scheme: 1, e não há campo de versão em trânsito. Onde a
plataforma der suporte, os produtores DEVERIAM emitir p = 4 (o segundo perfil
recomendado da RFC 9106 §4); os
verificadores PODEM aceitar qualquer p ≥ 1, sujeito aos tetos da implantação. O
Argon2id atravessa fronteiras de ecossistemas com limpeza — o conjunto de parâmetros, e
não a biblioteca, é o contrato — de modo que um (m, t, p, salt, len, password) fixo
deve produzir uma saída idêntica a nível de byte em toda implementação. O vínculo entre
uma frase secreta e seu envelope é o compromisso dentro do texto cifrado acima; uma
frase secreta errada e um texto
cifrado adulterado ambos aparecem como uma única falha genérica.
Limite a frase secreta bruta antes da normalização e do Argon2id: rejeite qualquer
entrada maior que o MAX_PASSPHRASE_INPUT_BYTES = 4096 bytes UTF-8 de referência, de
modo que uma frase secreta patológica não possa provocar uma negação de serviço pré-KDF.
Como os limites de MAX_SLOTS e de envelope decodificado do caminho de slots, esta é
uma constante fixada pela implantação que você PODE restringir, não um campo de
transmissão.
O perfil de normalização é normativo
Duas implementações DEVEM derivar uma CEK idêntica a nível de byte a partir da
mesma frase secreta, e o único jeito de garantir isso é uma normalização fixada. O
perfil cardano-poe-pw-norm-v1, aplicado em ordem:
- Rejeite codepoints não atribuídos — uma frase secreta que contenha qualquer
codepoint não atribuído no Unicode 16.0 é rejeitada
(
ENC_PASSPHRASE_UNNORMALIZABLE) antes de qualquer normalização rodar. O Unicode garante a estabilidade da normalização apenas sobre codepoints atribuídos, de modo que isto fecha uma brecha de deriva futura e é invisível para usuários honestos. - NFKC — Forma de Normalização KC conforme a UAX #15 sob Unicode 16.0.
- Espaço em branco — defina espaço em branco como todo caractere que carrega a
propriedade Unicode
White_Spacesob Unicode 16.0; colapse toda sequência máxima em um único U+0020 SPACE. - Trim — remova o espaço em branco inicial e final.
- Rejeite vazio — se o resultado for a string vazia, rejeite
(
ENC_PASSPHRASE_EMPTY); uma frase secreta só de espaços em branco, caso contrário, fixaria o registro a uma CEK que qualquer parte pode derivar. - Codifique — UTF-8; esses bytes são a entrada de senha do Argon2id.
Fixe o Unicode em 16.0 literalmente e não o deixe flutuar: o conjunto da
propriedade White_Space, o conjunto de codepoints atribuídos e as tabelas de
mapeamento NFKC dependem todos da versão, de modo que resolver o perfil contra uma
versão Unicode diferente pode derivar uma CEK diferente a partir da mesma frase secreta
e não abrir um registro honesto. Uma revisão futura que adote uma versão Unicode mais
nova o faz sob um novo identificador de perfil, nunca reinterpretando
cardano-poe-pw-norm-v1.
Tentativa de decifragem: abra cada slot, dobre o MAC, falhe genericamente
Um destinatário detém uma chave privada de KEM e descobre seu slot tentando abrir cada
um — as chaves públicas dos destinatários não estão em trânsito. Antes de invocar
qualquer primitiva KEM ou AEAD, rode os limites de recursos e, então, as proteções
estruturais. Limite primeiro o uso de recursos do parser: rejeite um envelope cujo
tamanho decodificado exceda 65536 bytes (ENC_ENVELOPE_TOO_LARGE) ou cujo slots[]
exceda MAX_SLOTS = 1024 (ENC_SLOTS_TOO_MANY). Ambos os limites de referência ficam
muito acima do teto de ~16 KiB de metadados de transação da Cardano que restringe
qualquer registro honesto; são constantes fixadas pela implantação que você PODE
restringir, nunca campos de transmissão. Em seguida, as proteções estruturais:
scheme == 1; aead, kem registrados; nonce de 24 bytes; slots_mac de 32 bytes;
slots não vazio; segredo do destinatário de 32 bytes; cada wrap de 48 bytes; por
KEM, cada epk de exatamente 32 bytes sem kem_ct (x25519) ou cada kem_ct de
exatamente 1120 bytes sem epk (mlkem768x25519).
Rejeite o encapsulamento duplicado dentro do registro aqui, antes de qualquer
primitiva. Todos os valores de epk devem ser distintos no caminho clássico, todos
os valores de kem_ct distintos no caminho híbrido; uma duplicata levanta
ENC_SLOTS_DUPLICATE_KEM_MATERIAL. Esta é a fatia verificável pelo verificador do
invariante de unicidade-de-KEK-por-slot da qual o envoltório de nonce zero depende; o
reúso entre registros ou entre chaves é uma obrigação do produtor que nenhum verificador
consegue detectar. Esta rejeição dispara apenas em um epk / kem_ct repetido — selar
para o mesmo destinatário duas vezes com efêmeros por slot frescos é legítimo e não
a aciona (veja a regra de múltiplas correspondências abaixo). unwrap-negative carrega
o caso de epk duplicado com reúso de KEK.
Depois rode o laço, recalculando slots_hash uma vez antes dele e mantendo-o constante:
found = false
cek_conflict = false
selected_CEK = 0^32
for slot in slots: ; iterate ALL slots — no early break
; derive KEK per-KEM, as in the wrap recipe. For x25519 the all-zero shared
; secret is rejected via a secret-independent bit, not an early branch:
; kem_ok = NOT constantTimeEqual(shared, 0^32)
; KEK = ct_select(kem_ok, real_KEK, dummy_KEK) ; dummy_KEK from ikm=0^32, same salt/info
; (XWing.Decapsulate has no all-zero case; kem_ok stays true on the hybrid path.)
open_ok, candidate_CEK = ChaCha20-Poly1305_open_or_dummy(KEK, zeros(12), kem_info_label, slot.wrap)
HMAC_KEY = HKDF-SHA-256(candidate_CEK, salt = "", info = "cardano-poe-slots-mac-v1", L = 32)
mac_ok = constantTimeEqual(HMAC-SHA-256(HMAC_KEY, slots_hash), slots_mac)
ok = kem_ok AND open_ok AND mac_ok ; kem_ok folded into acceptance
first = ok AND NOT found ; first matching slot
cek_conflict = cek_conflict OR (ok AND found AND NOT constantTimeEqual(candidate_CEK, selected_CEK))
selected_CEK = ct_select(first, candidate_CEK, selected_CEK) ; constant-time
found = found OR ok
if NOT found: reject (single generic failure)
if cek_conflict: reject (single generic failure)
content_key = HKDF-SHA-256(selected_CEK, salt = enc.nonce, info = "cardano-poe-payload-v1", L = 32)
plaintext = STREAM_open(content_key, ciphertext) ; per-chunk authenticated release
if STREAM_open fails at any chunk: reject (single generic failure) ; TAMPERED_CIPHERTEXTOs pontos inegociáveis neste laço:
- Abra atomicamente; nunca libere texto em claro não verificado. Ambas as
primitivas
*_open_or_dummysão atômicas: em uma falha de tag AEAD, elas não retornam texto em claro, e o candidato retornado (a CEK envolvida ou o texto em claro do conteúdo) é um valor fictício, fixo ou pseudoaleatório, independente do texto cifrado que falhou. É isso que permite ao laço carregar umacandidate_CEKpor uma abertura de envoltório malsucedida sem jamais expor bytes não autenticados. - Dobre a verificação de todo de zeros em um bit
kem_okindependente do segredo. Calculekem_ok = NOT constantTimeEqual(shared, 0^32)para o caminhox25519, selecione a KEK em tempo constante entre a KEK real e uma KEK fictícia derivada de0^32sob o mesmo salt e o mesmo info, e incorporekem_okà aceitação (ok = kem_ok AND open_ok AND mac_ok). Não desvie cedo em um compartilhamento inválido — um slot com ECDH inválido nunca pode ser aceito, e o laço ainda faz trabalho idêntico. (XWing.Decapsulatenão tem caso todo de zeros, entãokem_oké fixo em verdadeiro no caminho híbrido.) - Dobre a verificação de
slots_macdentro do laço. Um remetente malicioso pode forjar um slot que abre sob a chave do destinatário com uma CEK escolhida pelo atacante (sem precisar conhecer chave privada alguma). Aceitar o primeiro sucesso de AEAD como "nosso" deixaria esse slot forjado eclipsar um honesto. Exigir que a CEK candidata também reproduzaslots_macsobreslots_hashfrustra a substituição, a remoção e a reordenação de slots. Nunca pule isto. - Permita múltiplas correspondências; rejeite apenas um conflito de CEK. A chave de
um destinatário PODE legitimamente corresponder a mais de um slot — selar a mesma
CEK para o mesmo destinatário em vários slots, cada um com efêmeros frescos, é
preenchimento válido da contagem de destinatários e não aciona a rejeição de
epk/kem_ctduplicado. Selecione a CEK do primeiro slot correspondente e não rejeite apenas porque mais de um slot correspondeu. A única anomalia a rejeitar são dois slots correspondentes que recuperam CEKs diferentes (comparação em tempo constante): rastreie um bitcek_conflicte exponha a única falha genérica se ele estiver definido. Isso é defesa em profundidade — sob o compromisso do conjunto de slots, uma correspondência com CEK distinta já é inviável — então ela falha de forma fechada contra uma implementação quebrada. - Itere por todos os slots dentro da passada de uma única chave privada — um número
constante de operações de slot por chave, sem interrupção antecipada — para que um
observador de temporização não consiga inferir qual slot casou. Conduza a rejeição do
todo de zeros por
kem_oke trabalho fictício, em vez de sair cedo. Um destinatário com várias chaves itera chave × slot e PODE curto-circuitar entre chaves (vazando apenas o sinal fraco de "qual chave casou"), mas precisa permanecer em tempo constante ao longo dos slots de qualquer chave — e precisa rederivar a metadepub_Rdo salt por chave, já que ambos os KEMs vinculam a própria chave pública do destinatário ao salt da KEK. Vincule esse salt à codificação de transmissão canônica da chave — exatamente a chave pública X25519 de 32 bytes, ou exatamente os 1216 bytes da chave pública X-Wing fixada — nunca uma recodificação não canônica, ou os dois lados derivam KEKs diferentes. - Apresente uma única forma de falha genérica a chamadores não confiáveis.
Internamente, você pode rastrear resultados tipados para diagnóstico local —
WRONG_RECIPIENT_KEY(nenhum slot abriu),TAMPERED_HEADER(um slot abriu, mas nenhuma CEK candidata reproduziuslots_mac),TAMPERED_CIPHERTEXT(o AEAD de conteúdo falhou depois de uma CEK ter sido recuperada e o MAC ter sido verificado) — mas um observador externo NÃO DEVE distingui-los pela forma da resposta. Quanto à temporização, o modelo é deliberadamente delimitado: um verificador PODE retornar na verificaçãoif NOT foundantes da decifragem do conteúdo, o que separa um não destinatário de um destinatário cujo texto cifrado falha ao abrir. Isso revela apenas destinatário versus não destinatário, nunca qual slot ou qualquer material de chave; uma temporização uniforme entre esses dois casos não é exigida e uma abertura de conteúdo fictícia NÃO DEVE ser obrigatória. A garantia de tempo constante que vale é a invariante entre slots acima. - Recalcule e compare o hash do texto claro após a decifragem. O mapa
hasheson-chain compromete-se com o texto claro, não com o texto cifrado, de modo que o destinatário (na camada de aplicação) precisa recalcular o digest e comparar: a entradasha2-256deve coincidir, eblake2b-256se presente. Uma divergência significa que a afirmação de hash do registro não bate com os bytes decifrados — recuse-se a agir sobre o texto claro. O validador estrutural nunca decifra.
Limite a carga útil dos dois lados
O STREAM segmentado não impõe nenhum teto criptográfico de carga útil: o contador
por bloco de 88 bits admite 2^88 blocos, e cada bloco é selado sob um par
(content_key, nonce) distinto, com folga dentro do limite de invocação única do
RFC 8439, de modo que não há risco de estouro de contador contra o qual se proteger. O
máximo que um produtor ou verificador impõe é, portanto, uma política de negação de
serviço da implantação, não uma constante de transmissão — imponha-o de forma
incremental à medida que o fluxo é escrito ou lido, e aborte antes de armazenar em
buffer uma carga útil superdimensionada. O truncamento é detectado estruturalmente pelo
sinalizador final, e não por um limite de tamanho. A mesma postura vale tanto no caminho
de slots quanto no de frase secreta.
Fixtures de conformidade da prova de existência selada
O canto da prova de existência selada no corpus é onde a maioria dos bugs entre
linguagens aparece. Conduza sua implementação por todo ele. Os fixtures positivos
fixam o envoltório determinístico e o laço de tentativa de decifragem para ambos os
KEMs — de um e de vários destinatários, com N misto, e o pior caso com várias chaves
privadas — mais o caso legítimo de um destinatário correspondendo a dois slots
(efêmeros frescos, mesma CEK, DEVE decifrar, de modo que uma implementação que
rejeite múltiplas correspondências falha aqui) e o caminho de frase secreta (cabeçalho
de compromisso mais blocos do STREAM em um único blob). Um conjunto dedicado de
layout STREAM fixa um texto claro vazio (um único bloco final de comprimento zero),
uma carga útil de um único bloco e uma carga útil de múltiplos blocos cruzando a
fronteira de 65536 bytes. KATs específicos fixam ambos os salts de KEK
(SHA-256(label ‖ enc.nonce ‖ <material de KEM> ‖ pub_R)), o hashes_hash e seu lugar
em ambas as transcrições, o encapsulamento X-Wing contra o draft-10, a extração de HKDF
com salt de comprimento zero (a convenção de salt ausente da
RFC 5869 §2.2, espelhando a
derivação da chave de slots_mac), as codificações Bech32 de destinatário/segredo e a
codificação com soma de verificação da semente de identidade.
Os fixtures negativos fixam os códigos de rejeição: um slot-sombra forjado antes de
um slot honesto (o registro DEVE ainda decifrar sob a CEK honesta); uma inversão de
cabeçalho (kem/aead/scheme) que deixa as formas dos slots válidas; uma colagem de
hashes em um item com uma alegação de hash diferente; as falhas de compromisso de
frase secreta (frase secreta errada, salt/params adulterados, cabeçalho adulterado —
todas falhando antes de qualquer bloco abrir); as rejeições de normalização de frase
secreta (uma entrada com codepoint não atribuído e uma entrada só de espaços em branco);
o segredo compartilhado X25519 todo-zero; o slot duplicado dentro do registro; e os
casos de adulteração do STREAM (tag de bloco invertida, fluxo truncado, dados
sobressalentes, bloco não final curto). Duas propriedades não têm vetor de bytes e
são afirmadas comportamentalmente: a rejeição de conflito de CEK (construir uma é
exatamente a colisão de compromisso multi-chave que o padrão presume inviável) e a
garantia de tempo constante entre slots. Reproduza cada string de bytes fixada e emita o
código exato para cada caso negativo.
Uma propriedade da prova de existência selada não tem vetor de bytes: a rejeição de conflito de CEK — dois slots correspondentes que recuperam CEKs diferentes — não pode ser construída como fixture, porque construir uma é exatamente a colisão de compromisso multi-chave que o padrão presume inviável. Fixe-a, em vez disso, com um teste comportamental em nível de implementação que afirme que seu laço de tentativa de decifragem falha de forma fechada em um conflito forçado, do mesmo modo que a propriedade de tempo constante entre slots é afirmada comportamentalmente, e não como uma string de bytes.
Conformidade e vetores de teste
Os vetores de teste normativos são o contrato de interoperabilidade. Uma implementação está em conformidade se, e somente se, reproduz toda sequência de bytes fixada no conjunto de testes a partir das mesmas entradas — e emite o código de erro tipado correto para cada fixture negativo. Não há nota parcial nem recurso: se uma comparação falha, a implementação é que está errada, nunca o vetor.
Os vetores vivem no conjunto de testes de conformidade do padrão, organizados por classe de primitiva: fixtures de registros, envelopamento e desenvelopamento da prova de existência selada, assinaturas COSE_Sign1, HKDF, derivação de semente, Argon2id e CBOR canônico. Cada um fixa entradas em hexadecimal minúsculo e as saídas esperadas. Para usá-los: alimente as entradas em sua implementação, compare cada saída nomeada byte a byte e corrija seu código diante de qualquer divergência.
Três obrigações que toda implementação deve cumprir
Reproduzir os vetores positivos. Para cada fixture de registro, devem valer as
duas metades de encode(record) == expected_cbor E a ida e volta
encode(decode(expected_cbor)) == expected_cbor. A ida e volta se generaliza para
além dos fixtures: para qualquer entrada bem formada arbitrária,
encode(decode(x)) == x. Um decodificador que perde ou reordena informação, ou um
codificador que não é canônico, quebra isso e reprova na conformidade.
Emitir os códigos de rejeição certos. Os fixtures negativos associam um registro deliberadamente malformado ao código de erro tipado exato que um validador estrutural DEVE levantar. Reproduzir os bytes de registros válidos é metade do contrato; rejeitar os inválidos com o código correto é a outra metade. Um validador que rejeita um registro ruim pela razão errada — ou que o aceita — não está em conformidade. Os fixtures negativos são a única fonte da verdade para a paridade de rejeição entre linguagens: a mesma entrada malformada DEVE levantar o mesmo código em toda implementação. O catálogo completo de códigos e seus significados está em Verificação.
Coincidir com os registros de identificadores. Os identificadores de algoritmo são cadeias de caracteres nomeadas extraídas dos registros em Registros de algoritmos. Um identificador não reconhecido DEVE revelar o código preciso de algoritmo não suportado, nunca uma aceitação silenciosa ou um panic.
Corrija a implementação, nunca o vetor
Os vetores estão fixados aos RFCs de origem e às construções determinísticas deste padrão. Quando uma comparação falha, o bug está na implementação sob teste. Editar um vetor para fazer um conjunto de testes passar converte uma falha real de interoperabilidade em uma falha latente que só vem à tona quando um registro cruza implementações na cadeia — o pior momento possível para descobri-la.
Rode a paridade a cada mudança
Uma implementação que entrega mais de uma linguagem — ou que quer provar interoperabilidade com outra — DEVERIA rodar um único job de integração contínua que faz a build de cada pacote, executa o conjunto de testes de cada linguagem contra os fixtures compartilhados, aplica a análise do grafo de dependências e verifica que o conjunto de fixtures é idêntico dos dois lados. Um fixture adicionado de um lado, mas não do outro, faz o gate falhar: as duas implementações divergiram em silêncio, e a build percebe isso antes que um registro real o faça. Os fixtures são a fonte canônica; cada linguagem mantém um espelho idêntico a nível de byte, e o gate afirma que o espelho está completo e exato.
Convenções de nomenclatura e de transmissão
Algumas convenções mantêm uma implementação legível e o formato de transmissão estável:
- Os nomes dos campos na transmissão são
snake_case—leaf_count,cose_sign1,slots_mac. Isso vale entre as linguagens: mesmo quando uma linguagem usa idiomaticamentecamelCasepara sua API em memória, o registro codificado usa chaves emsnake_case, porque as chaves fazem parte dos bytes canônicos que uma assinatura cobre. - Os identificadores são cadeias de caracteres dos registros, não enums embutidos no código. Hashes, AEADs, KEMs, KDFs e assinaturas referenciam todos identificadores nomeados; adicionar um algoritmo (um KEM pós-quântico, por exemplo) é uma entrada aditiva no registro, nunca uma quebra do formato de transmissão.
- Os nomes dos métodos espelham-se semanticamente entre as linguagens. Uma função
em uma linguagem tem uma contraparte de mesmo nome em outra (
encode_canonical_cbor↔encodeCanonicalCbor), de modo que um leitor fluente em qualquer uma delas consegue mapear uma superfície sobre a outra e raciocinar sobre paridade por simples inspeção. - Comece pelas camadas criptográficas. Erga o núcleo criptográfico e a biblioteca de formato de transmissão contra os vetores e deixe o gate de paridade verde antes de escrever uma linha de código de aplicação. O verificador independente é a menor superfície adjacente à aplicação e a próxima coisa a construir; todo o resto se assenta sobre uma camada criptográfica que você já provou correta.
Páginas relacionadas
- O registro — o formato de transmissão que o validador e o codificador implementam.
- Prova de existência selada — a referência da construção por trás das receitas de construção aqui.
- Registros de algoritmos — os identificadores nomeados que uma implementação resolve.
- Verificação — o pipeline de validação, o verificador independente e o catálogo de códigos de erro.
Modelo de segurança
O que um verificador Label 309 confia e o que não confia. A invariante de verificabilidade independente, as garantias de privacidade da PoE selada, as regras criptográficas normativas que toda implementação deve cumprir e os limites conhecidos do formato de transmissão.
Questões em aberto
O que está consolidado no formato de transmissão do Label 309 e o que ficou adiado ou voltado ao futuro — o núcleo criptográfico confirmado, as construções candidatas reservadas para uma revisão futura e o modelo de migração para alterar uma constante.