Assinaturas
O array opcional `sigs`, no nível do registro — um COSE_Sign1 destacado sobre todo o corpo do registro, sua carga útil assinada com separação de domínio, os dois caminhos para a chave do signatário e a verificação estrita de Ed25519.
Um registro Label 309 PODE conter uma ou mais assinaturas de autoria em um
array sigs opcional, no nível superior. Cada entrada é um
COSE_Sign1 (RFC 9052) destacado sobre o
corpo do registro, atestando que alguma chave responde por aquele registro. A
autoria é sempre opcional: o padrão nunca exige uma assinatura, e um registro
sem o campo sigs é uma prova de existência (PoE) completa e plenamente
verificável.
Uma assinatura é aditiva: ela acrescenta "e esta chave responde por isto" à afirmação de carimbo temporal, nunca a substitui. O hash do conteúdo é a afirmação principal; uma assinatura são metadados sobre quem está por trás dessa afirmação. O ponto essencial: uma assinatura que o verificador não consegue checar — um algoritmo não suportado, uma chave que não se resolve — nunca invalida a afirmação de conteúdo ou de carimbo temporal. As assinaturas falham de forma branda; a existência, não.
Esta página define o que uma assinatura cobre, os bytes exatos que são assinados,
as duas formas de transportar a chave pública do signatário e a verificação
estrita que um verificador público realiza. A própria chave Ed25519 é definida em
Chaves; o campo sigs no formato de transmissão — em que cose_sign1
e cose_key são, cada um, uma única string de bytes CBOR — é definido em
O registro.
O que uma assinatura cobre
Uma única entrada sigs[i] atesta todo o corpo do registro, de modo uniforme.
Não há granularidade de assinatura por item, por URI ou por campo: uma assinatura
compromete todos os itens, todos os URIs de armazenamento, todos os envelopes de
criptografia, o ponteiro supersedes, se presente, e cada chave de extensão que o
registro carrega. Um relé não pode adicionar, remover ou reescrever nenhum desses
elementos depois do fato sem invalidar a assinatura.
O corpo assinado é o mapa do registro com o campo sigs removido —
remove_keys(record_map, ["sigs"]), aqui denotado record_body. O array sigs
fica de fora do que cada entrada assina porque uma assinatura não pode cobrir a si
mesma e porque cada signatário compromete apenas a afirmação, não a lista de
cossignatários. Concretamente, toda entrada assina {v, items?, merkle?, supersedes?, crit?, <extensions?>} — os mesmos bytes de record_body para todas
as entradas — mas nenhuma entrada assina as demais entradas em sigs. Um
signatário, portanto, atesta que o corpo que assinou é o corpo ao qual todas as
outras entradas estão vinculadas; nenhum signatário atesta quais outros
signatários cossinaram.
O escopo da assinatura é o corpo do registro, não a transação
Uma assinatura verificada prova que uma chave produziu uma assinatura sobre o corpo do registro. Ela não prova que a mesma chave submeteu a transação que o carrega, pagou sua taxa ou escolheu seu horário do bloco. Um corpo de registro idêntico PODE ser republicado por qualquer parte em uma transação posterior — isso é portabilidade de registro intencional. Apresente uma assinatura verificada como "assinado por <chave>", nunca como "<chave> submeteu isto" ou "publicado por <chave> em <horário>".
A carga útil assinada
Cada entrada carrega um COSE_Sign1 destacado, de modo que o campo de carga útil do COSE fica vazio e os bytes efetivamente assinados são reconstruídos pelo verificador a partir do registro on-chain. O signatário calcula:
record_body = remove_keys(record_map, ["sigs"])
record_body_bytes = canonical_cbor(record_body)
SIG_DOMAIN_RECORD = utf8("cardano-poe-record-sig-v1") ; 25 bytes
to_sign = SIG_DOMAIN_RECORD || record_body_bytes ; concatenation
Sig_structure = [ "Signature1", protected, h'', to_sign ]
signature = Sign(canonical_cbor(Sig_structure), signer_key)record_body é serializado como CBOR canônico conforme
RFC 8949 §4.2.1 — a mesma
codificação determinística usada por todo o registro. O determinismo é o que torna uma
assinatura interoperável: duas implementações que codificam o mesmo corpo lógico
produzem record_body_bytes idênticos byte a byte, de modo que uma assinatura
produzida por uma é verificada sob a outra.
O prefixo de separação de domínio
to_sign é a string UTF-8 de 25 bytes cardano-poe-record-sig-v1 anteposta a
record_body_bytes. O prefixo vincula a assinatura ao seu papel no Label 309 e
impede a reutilização entre protocolos. Um futuro esquema de metadados Cardano que
por acaso compartilhasse a forma CBOR do corpo (mesmas chaves, mesmos tipos) não
poderia reutilizar uma assinatura Label 309 contra si mesmo: seu to_sign
carregaria um prefixo diferente, ou nenhum, de modo que a sequência de bytes
assinados seria distinta e a assinatura falharia. As implementações DEVEM
embutir essa sequência literal de bytes como os bytes iniciais de to_sign
exatamente; assinar apenas o CBOR canônico nu, sem prefixo, não é conforme.
Por que external_aad é vazio
O Label 309 coloca o separador de domínio dentro de to_sign, e não no
external_aad do COSE. O campo external_aad (Sig_structure[2]) é sempre a
string de bytes vazia h''. Esse é um afastamento deliberado do padrão usual do
COSE de colocar uma string de domínio em external_aad, e o motivo é a
interoperabilidade com carteiras: o
CIP-30
signData — o caminho padrão de assinatura por carteira no Cardano — estipula que
nenhum external_aad seja usado e não dá a uma dApp nenhuma forma de fornecer um.
Um external_aad não vazio faria toda assinatura produzida por carteira falhar.
Embutir o prefixo na carga útil preserva a mesma propriedade contra reutilização
e, ao mesmo tempo, mantém os bytes produzidos pela carteira e recomputados pelo
verificador iguais byte a byte.
A Sig_structure
Sig_structure é o array de assinatura COSE_Sign1 de 4 elementos do
RFC 9052 §4.4:
| Slot | Valor | Notas |
|---|---|---|
[0] | "Signature1" | Identificador de contexto COSE fixo, emitido como a string de texto CBOR completa (11 bytes), nunca o UTF-8 nu. |
[1] | protected | Os bytes do cabeçalho protegido do signatário, em CBOR canônico e envolvidos em bstr, usados literalmente — nunca recanonicalizados pelo verificador. |
[2] | external_aad | Sempre h'' (bstr de comprimento zero). |
[3] | to_sign | O prefixo de 25 bytes concatenado com record_body_bytes. |
O COSE_Sign1 publicado carrega seu campo de carga útil (COSE_Sign1[2]) como o
CBOR null (0xF6) — a forma destacada. Uma carga útil anexada, inclusive uma
string de bytes de comprimento zero, é rejeitada. Destacar a carga útil é o que
fixa os bytes assinados ao corpo do registro que o verificador recomputa de forma
independente; uma forma anexada permitiria que um produtor assinasse bytes
emprestados, sem relação alguma com as afirmações on-chain.
Modo com hash de carteira de hardware
O CIP-30 / CIP-8
definem uma flag opcional "hashed": true de cabeçalho não protegido que um cossignatário de
hardware com recursos limitados PODE definir. Quando presente e verdadeira, Sig_structure[3] é o
digest Blake2b-224(to_sign) de 28 bytes em vez do próprio to_sign; os outros três slots
permanecem inalterados. Um verificador DEVE inspecionar o cabeçalho não protegido e realizar
essa substituição antes da verificação estrita de Ed25519. Produtores de software e SDK NÃO
DEVERIAM defini-la — ela não economiza bytes na transmissão e complica os caminhos de código do
verificador.
Algoritmo de assinatura
O único algoritmo de assinatura na v1 é EdDSA sobre Ed25519
(RFC 8032), identificado pelo COSE
alg = -8 (RFC 9053 §2.2),
que reside no cabeçalho protegido do COSE_Sign1. A linha de base obrigatória de um
verificador v1 é {-8}; ele PODE aceitar adicionalmente -19 (Ed25519,
totalmente especificado) e verificar ambos os codepoints sob a mesma primitiva
Ed25519. O catálogo de algoritmos é extensível — revisões futuras adicionam
assinaturas pós-quânticas de forma aditiva, nunca como uma mudança incompatível.
Resolução da chave do signatário
Um verificador público deve resolver a chave pública do signatário sem contatar serviço algum, de modo que toda assinatura carrega sua chave, ou uma referência inequívoca a ela dentro da própria assinatura, on-chain. Há exatamente duas formas de transporte na v1, e elas são mutuamente exclusivas dentro de uma única entrada — uma entrada que use ambas é um erro estrutural.
Caminho 1 — assinatura por identidade (kid na própria assinatura)
A chave pública Ed25519 bruta de 32 bytes é colocada no rótulo de cabeçalho COSE
4 (kid, RFC 9052 §3.1)
dentro do cabeçalho protegido do COSE_Sign1. A entrada não carrega nenhum campo
cose_key. Pela convenção do Label 309, um kid de cabeçalho protegido com
exatamente 32 bytes é a chave pública — não um ponteiro opaco para uma chave
buscada fora de banda. O comprimento de 32 bytes é um discriminador inequívoco: as
chaves públicas Ed25519 têm sempre 32 bytes. Colocar a chave no cabeçalho protegido
(e não no não protegido) a vincula à assinatura; um adversário que a reescrevesse
invalidaria a verificação.
Essa convenção é um desvio deliberado e documentado da leitura de kid como
identificador opaco na RFC 9052; é o que torna o caminho de identidade independente
de serviços, sem nenhum diretório de chaves necessário. O modelo de chaves é
definido em Chaves.
Caminho 2 — assinatura por carteira (cose_key embutido)
Uma assinatura signData do CIP-30 retorna a chave pública do signatário como um
blob cbor<COSE_Key> separado, não dentro do COSE_Sign1. Um produtor que encadeie
tal assinatura em um registro DEVE colocar essa COSE_Key na mesma entrada
sigs[i], sob a chave cose_key, como uma única string de bytes CBOR. O verificador
a decodifica como uma COSE_Key e lê a chave pública Ed25519 no rótulo -2. A COSE_Key
DEVE
descrever apenas a metade pública — kty = OKP (1), crv = Ed25519 (6), o x de
32 bytes no rótulo -2 — e NÃO DEVE carregar material de chave privada (rótulo
-4 e similares); publicar um escalar privado em um livro-razão permanente é um
vazamento de chave irreversível.
Exclusão mútua
Os dois caminhos são exclusivos no nível da transmissão. Uma entrada carrega ou
um kid de cabeçalho protegido de 32 bytes e nenhum cose_key (caminho 1),
ou um campo cose_key e nenhum kid de cabeçalho protegido de 32 bytes
(caminho 2) — nunca ambos. Uma entrada que carregue ambos é rejeitada; um
verificador nunca precisa desambiguar no momento da verificação. A resolução é,
portanto, uma distinção no nível da transmissão, e não uma precedência ordenada:
| Caminho | Condição | Chave do signatário |
|---|---|---|
| 1 | kid protegido de 32 bytes, sem cose_key | O valor de kid de 32 bytes, usado diretamente. |
| 2 | cose_key presente, sem kid de 32 bytes | A chave Ed25519 no rótulo -2 da COSE_Key. |
Um kid carregado apenas no cabeçalho não protegido não é um caminho de
resolução sancionado: ele fica fora do envelope assinado, de modo que um relé
poderia reescrevê-lo sem invalidar a assinatura. Um verificador DEVE ignorar
valores de kid de cabeçalho não protegido na resolução. Se nenhum caminho
permitido produzir uma chave Ed25519 de 32 bytes, a entrada é reportada como não
resolvida e não contribui com nenhuma afirmação de autoria.
Verificação
Um verificador público checa cada sigs[i] de forma independente, nesta ordem:
- Decodificar. Faça o parse da string de bytes
sigs[i].cose_sign1como um COSE_Sign1. O campo de carga útil DEVE sernull(destacado); qualquer carga útil não nula ou não vazia é malformada. - Algoritmo. Leia o
algdo cabeçalho protegido. Se estiver fora do conjunto suportado pelo verificador, a entrada é não suportada (veja abaixo) — e não um erro no registro. - Resolver a chave. Aplique a distinção entre caminho 1 e caminho 2 acima para obter a chave pública Ed25519 de 32 bytes. Se nenhum caminho produzir uma, a entrada é não resolvida.
- Reconstruir e verificar. Reconstrua
to_signeSig_structure = ["Signature1", protected, h'', to_sign], codifique-o em CBOR canônico e verifique a assinatura com Ed25519 estrito. (Substituato_signporBlake2b-224(to_sign)primeiro, se o cabeçalho não protegido carregar"hashed": true.) - Vínculo com a carteira (apenas caminho 2). Recompute o endereço de stake a
partir da chave resolvida e compare-o byte a byte com o
addressdo cabeçalho protegido; uma divergência faz o vínculo falhar, ainda que a própria assinatura Ed25519 tenha sido verificada. Essa checagem, exclusiva do caminho 2, é o que permite a uma interface apresentar um registro como vinculado a uma carteira; as entradas do caminho 1 a ignoram.
Ed25519 estrito
A verificação segue as regras estritas da RFC 8032 §5.1.7 — há exatamente uma resposta aceitável para qualquer chave, mensagem e assinatura dadas:
- Codificações não canônicas de
Rou do escalar de assinaturaS(em especial qualquerS ≥ ℓ, a ordem do grupo) DEVEM ser rejeitadas. - Chaves públicas e valores de
Rde ordem pequena, de subgrupo pequeno ou com componente de torção DEVEM ser rejeitados. - A equação de verificação com cofator (a forma ZIP-215 / favorável a lotes) NÃO DEVE ser substituída pela equação estrita.
O rigor é o que torna o veredicto reproduzível entre implementações: um verificador com cofator aceitaria assinaturas que um estrito rejeita, de modo que dois verificadores conformes discordariam. As implementações devem escolher uma biblioteca — ou um modo de biblioteca — que realize verificação estrita, sem cofator.
Semântica do veredicto
As assinaturas são aditivas, portanto uma assinatura não verificável é reportada na
entrada, não promovida a uma falha no nível do registro. Cada sigs[i] resolve-se
em um destes resultados tipados por entrada; o catálogo completo de erros e as
regras do veredicto no nível do registro estão em
Verificação:
| Resultado | Significado |
|---|---|
| verificada | O Ed25519 estrito (e, para o caminho 2, o vínculo de endereço) passou. |
| assinatura não suportada | O alg do cabeçalho protegido está fora do conjunto do verificador. Informativo, nunca um erro. |
| chave do signatário não resolvida | Nenhum caminho permitido produz uma chave pública Ed25519 de 32 bytes. |
| assinatura inválida | O Ed25519 estrito retornou false sobre a Sig_structure reconstruída. |
| endereço de carteira divergente | Caminho 2: a assinatura foi verificada, mas o endereço de stake recomputado ≠ o reivindicado. |
Uma assinatura não suportada nunca invalida a prova
Um algoritmo de assinatura não reconhecido ou não suportado produz um resultado tipado de
assinatura não suportada com severidade informativa. A afirmação de conteúdo e de carimbo
temporal — o compromisso hashes on-chain — é estruturalmente válida, independentemente dos
algoritmos de assinatura que um verificador implementa. Um registro que carrega apenas assinaturas
de algoritmos futuros ainda se apresenta como uma prova de existência válida, com cada entrada
dessas marcada como não suportada. As assinaturas são aditivas; a existência não depende delas.
Páginas relacionadas
- Chaves — a chave de assinatura Ed25519, sua derivação e a
chave pública de 32 bytes carregada no
kiddo caminho 1. - O registro — o campo
sigsno nível superior, o mapasig-entryfechado (cose_sign1/cose_key, cada um uma única string de bytes) e o transporte sobre o corpo inteiro. - Verificação — os códigos de resultado por entrada, as regras do veredicto no nível do registro e o pipeline completo de validação.
Chaves
O modelo de chaves do Label 309 — uma semente de 32 bytes, três pares de chaves derivados dela por HKDF-SHA-256 com separação de domínio, as chaves de criptografia de chave por slot que uma PoE selada deriva sobre elas, e como as chaves públicas e os segredos dos destinatários são codificados.
PoE selada
O envelope de criptografia do Label 309 — como um remetente sela o conteúdo para uma ou mais chaves de destinatários, enquanto a cadeia carrega apenas o hash do texto claro e os slots de chave protegidos, nunca o texto claro e nunca os destinatários.