Firme
L'array opzionale `sigs` a livello di record — una COSE_Sign1 distaccata sull'intero corpo del record, il suo payload firmato con separazione di dominio, le due modalità di trasporto della chiave del firmatario e la verifica Ed25519 rigorosa.
Un record Label 309 PUÒ contenere una o più firme di paternità in un array
sigs opzionale di primo livello. Ogni voce è una
COSE_Sign1 (RFC 9052) distaccata sul
corpo del record, che attesta come una determinata chiave garantisca per quel
record. La paternità è sempre opzionale: lo standard non richiede mai una
firma, e un record privo del campo sigs è una prova di esistenza (Proof of
Existence) completa e pienamente verificabile.
Una firma è additiva: aggiunge "e questa chiave ne garantisce" alla rivendicazione temporale, senza mai sostituirla. L'hash del contenuto è la rivendicazione primaria; la firma è un metadato su chi sta dietro a tale rivendicazione. È fondamentale capire che una firma che il verificatore non è in grado di controllare (un algoritmo non supportato, una chiave non risolvibile) non invalida mai la rivendicazione sul contenuto o sull'orario. Le firme falliscono in modo morbido; l'esistenza no.
Questa pagina definisce che cosa copre una firma, i byte esatti che vengono
firmati, le due modalità con cui viene trasportata la chiave pubblica del
firmatario e la verifica rigorosa svolta da un verificatore pubblico. La chiave
Ed25519 in sé è definita nella pagina Chiavi; il campo sigs
on-wire — dove cose_sign1 e cose_key sono ciascuno un'unica stringa di byte CBOR —
è definito nella pagina Il record.
Che cosa copre una firma
Una singola voce sigs[i] attesta l'intero corpo del record, in modo
uniforme. Non esiste granularità di firma per singolo item, per singolo URI o
per singolo campo: una firma si impegna su ogni item, ogni URI di storage, ogni
busta di cifratura, il puntatore supersedes se presente, e ogni chiave di
estensione che il record contiene. Un relay non può aggiungere, rimuovere o
riscrivere nessuno di questi elementi a posteriori senza invalidare la firma.
Il corpo firmato è la mappa del record con il campo sigs rimosso:
remove_keys(record_map, ["sigs"]), qui indicato come record_body. L'array
sigs è escluso da ciò che ogni voce firma perché una firma non può coprire se
stessa, e perché ogni firmatario si impegna solo sulla rivendicazione, non
sull'elenco dei co-firmatari. Concretamente, ogni voce firma {v, items?, merkle?, supersedes?, crit?, <extensions?>} (gli stessi byte di record_body
per tutte le voci) ma nessuna voce firma le altre voci in sigs. Un firmatario
attesta quindi che il corpo che ha firmato è il corpo a cui è vincolata ogni
altra voce; nessun firmatario attesta quali altri firmatari abbiano co-firmato.
L'ambito della firma è il corpo del record, non la transazione
Una firma verificata dimostra che una chiave ha prodotto una firma sul corpo del record. Non dimostra che la stessa chiave abbia inviato la transazione che lo trasporta, ne abbia pagato la commissione o ne abbia scelto il block time. Un corpo di record identico PUÒ essere ripubblicato da chiunque in una transazione successiva: questa è una portabilità del record intenzionale. Presenta una firma verificata come "firmato da <chiave>", mai come "<chiave> ha inviato questo" o "pubblicato da <chiave> alle <ora>".
Il payload firmato
Ogni voce trasporta una COSE_Sign1 distaccata, quindi il campo payload di COSE è vuoto e i byte effettivamente firmati vengono ricostruiti dal verificatore a partire dal record on-chain (sulla blockchain). Il firmatario calcola:
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 è serializzato come CBOR canonico secondo
RFC 8949 §4.2.1, la stessa
codifica deterministica usata dall'intero record. È proprio il determinismo a
rendere una firma interoperabile: due implementazioni che codificano lo stesso
corpo logico producono record_body_bytes identici byte per byte, così che una
firma prodotta da una si verifica con l'altra.
Il prefisso di separazione di dominio
to_sign è la stringa UTF-8 di 25 byte cardano-poe-record-sig-v1 anteposta a
record_body_bytes. Il prefisso lega la firma al suo ruolo in Label 309 e
previene il replay tra protocolli diversi. Un futuro schema di metadati Cardano
che avesse per caso la stessa forma CBOR del corpo (stesse chiavi, stessi tipi)
non potrebbe riutilizzare contro se stesso una firma Label 309: il suo to_sign
porterebbe un prefisso diverso, o nessuno, quindi la sequenza di byte firmati
differirebbe e la firma fallirebbe. Le implementazioni DEVONO inserire questa
sequenza letterale di byte esattamente come byte iniziali di to_sign; firmare
solo il CBOR canonico nudo, senza prefisso, non è conforme.
Perché external_aad è vuoto
Label 309 colloca il separatore di dominio dentro to_sign, non in
external_aad di COSE. Lo slot external_aad (Sig_structure[2]) è sempre
la stringa di byte vuota h''. Si tratta di una deviazione deliberata dal
consueto schema COSE, che inserisce una stringa di dominio in external_aad, e la
ragione è l'interoperabilità con i wallet:
CIP-30
signData, il percorso standard di firma tramite wallet su Cardano, stabilisce
che non si usi alcun external_aad e non offre a una dApp alcun modo per
fornirne uno. Un external_aad non vuoto farebbe fallire ogni firma prodotta da
un wallet. Inserire il prefisso nel payload preserva la medesima proprietà
anti-replay mantenendo identici, byte per byte, i byte prodotti dal wallet e
quelli ricalcolati dal verificatore.
La Sig_structure
Sig_structure è l'array di firma COSE_Sign1 a 4 elementi definito in
RFC 9052 §4.4:
| Slot | Valore | Note |
|---|---|---|
[0] | "Signature1" | Identificatore di contesto COSE fisso, emesso come stringa di testo CBOR completa (11 byte), mai come UTF-8 nudo. |
[1] | protected | I byte dell'header protetto del firmatario, incapsulati in bstr e in CBOR canonico, usati verbatim e mai ri-canonicalizzati dal verificatore. |
[2] | external_aad | Sempre h'' (bstr di lunghezza zero). |
[3] | to_sign | Il prefisso di 25 byte concatenato con record_body_bytes. |
La COSE_Sign1 pubblicata trasporta il proprio campo payload (COSE_Sign1[2]) come
CBOR null (0xF6): la forma distaccata. Un payload allegato, inclusa una
stringa di byte di lunghezza zero, viene rifiutato. Distaccare il payload è ciò
che ancora i byte firmati al corpo del record che il verificatore ricalcola in
modo indipendente; una forma allegata permetterebbe a un produttore di firmare
byte presi a prestito che non hanno alcuna relazione con le rivendicazioni
on-chain.
Modalità hashed degli hardware wallet
CIP-30 / CIP-8
definiscono un flag opzionale "hashed": true nell'header non protetto, che un co-firmatario
hardware con risorse limitate può impostare. Quando è presente e vale true, Sig_structure[3] è
il digest Blake2b-224(to_sign) di 28 byte invece di to_sign stesso; gli altri tre slot restano
invariati. Un verificatore DEVE ispezionare l'header non protetto ed eseguire questa
sostituzione prima della verifica Ed25519 rigorosa. I produttori software e SDK NON DOVREBBERO
impostarlo: non risparmia byte on-wire e complica i percorsi di codice del verificatore.
Algoritmo di firma
L'unico algoritmo di firma in v1 è EdDSA su Ed25519
(RFC 8032), identificato da COSE
alg = -8 (RFC 9053 §2.2),
che risiede nell'header protetto della COSE_Sign1. La baseline obbligatoria di un
verificatore v1 è {-8}; può inoltre accettare -19 (Ed25519, fully-specified)
e verificare entrambi i codepoint sulla stessa primitiva Ed25519. Il registro è
estensibile: le revisioni future aggiungono firme post-quantistiche in modo
additivo, mai come modifica incompatibile.
Risoluzione della chiave del firmatario
Un verificatore pubblico deve risolvere la chiave pubblica del firmatario senza contattare alcun servizio, quindi ogni firma porta con sé la propria chiave, o un riferimento inequivocabile ad essa interno alla firma, on-chain. In v1 esistono esattamente due modalità di trasporto, e sono mutuamente esclusive all'interno di una singola voce: una voce che le usa entrambe è un errore strutturale.
Modalità 1 — firma con identità (kid interno alla firma)
La chiave pubblica Ed25519 grezza di 32 byte è collocata all'etichetta di header
COSE 4 (kid,
RFC 9052 §3.1) dentro
l'header protetto della COSE_Sign1. La voce non porta alcun campo cose_key.
Per convenzione di Label 309, un kid nell'header protetto lungo esattamente
32 byte è la chiave pubblica, non un puntatore opaco a una chiave da cercare
fuori banda. La lunghezza di 32 byte è un discriminante inequivocabile: le chiavi
pubbliche Ed25519 sono sempre di 32 byte. Collocare la chiave nell'header protetto
(non in quello non protetto) la lega alla firma; un avversario che la riscrivesse
invaliderebbe la verifica.
Questa convenzione è una deviazione deliberata e documentata dalla lettura di
kid come identificatore opaco prevista in RFC 9052; è ciò che rende la modalità
con identità indipendente dai servizi, senza richiedere alcuna directory di
chiavi. Il modello delle chiavi è definito nella pagina Chiavi.
Modalità 2 — firma con wallet (cose_key inline)
Una firma CIP-30 signData restituisce la chiave pubblica del firmatario come
blob cbor<COSE_Key> separato, non all'interno della COSE_Sign1. Un produttore
che inserisce una firma di questo tipo in un record DEVE collocare quella
COSE_Key nella stessa voce sigs[i] sotto la chiave cose_key, come un'unica
stringa di byte CBOR. Il verificatore la decodifica come COSE_Key e legge la chiave
pubblica Ed25519 dall'etichetta -2. La COSE_Key DEVE descrivere solo la metà pubblica
(kty = OKP (1), crv = Ed25519 (6), la x di 32 byte all'etichetta -2) e
NON DEVE contenere materiale di chiave privata (etichetta -4 e simili);
pubblicare uno scalare privato su un registro permanente è una fuga di chiave
irreversibile.
Mutua esclusione
Le due modalità sono esclusive a livello di wire. Una voce porta o un kid
di 32 byte nell'header protetto e nessun cose_key (modalità 1), oppure
un campo cose_key e nessun kid di 32 byte nell'header protetto
(modalità 2), mai entrambi. Una voce che porta entrambi viene rifiutata; il
verificatore non deve mai disambiguare in fase di verifica. La risoluzione è
quindi una discriminazione a livello di wire, non una precedenza per priorità:
| Modalità | Condizione | Chiave del firmatario |
|---|---|---|
| 1 | kid protetto di 32 byte, nessun cose_key | Il valore kid di 32 byte, usato direttamente. |
| 2 | cose_key presente, nessun kid di 32 byte | La chiave Ed25519 all'etichetta COSE_Key -2. |
Un kid trasportato solo nell'header non protetto non è una modalità di
risoluzione ammessa: si trova al di fuori della busta firmata, quindi un relay
potrebbe riscriverlo senza invalidare la firma. Un verificatore DEVE ignorare
i valori kid dell'header non protetto ai fini della risoluzione. Se nessuna
modalità consentita produce una chiave Ed25519 di 32 byte, la voce è segnalata
come non risolta e non contribuisce ad alcuna rivendicazione di paternità.
Verifica
Un verificatore pubblico controlla ogni sigs[i] in modo indipendente, in
quest'ordine:
- Decodifica. Analizza la stringa di byte
sigs[i].cose_sign1come una COSE_Sign1. Il campo payload DEVE esserenull(distaccato); qualsiasi payload non null o non vuoto è malformato. - Algoritmo. Leggi l'
algdell'header protetto. Se è al di fuori dell'insieme supportato dal verificatore, la voce è non supportata (vedi sotto), non un errore sul record. - Risolvi la chiave. Applica la discriminazione modalità 1 / modalità 2 di cui sopra per ottenere la chiave pubblica Ed25519 di 32 byte. Se nessuna modalità ne produce una, la voce è non risolta.
- Ricostruisci e verifica. Ricostruisci
to_signeSig_structure = ["Signature1", protected, h'', to_sign], codificalo in CBOR canonico e verifica la firma con Ed25519 rigoroso. (Sostituisci primaBlake2b-224(to_sign)ato_signse l'header non protetto porta"hashed": true.) - Binding del wallet (solo modalità 2). Ricalcola lo stake address dalla
chiave risolta e confrontalo byte per byte con l'
addressdell'header protetto; una discrepanza fa fallire il binding anche se la firma Ed25519 in sé è stata verificata. Questo controllo, presente solo nella modalità 2, è ciò che consente a una UI di presentare un record come legato a un wallet; le voci di modalità 1 lo saltano.
Ed25519 rigoroso
La verifica segue le regole rigorose di RFC 8032 §5.1.7: esiste esattamente una risposta accettabile per ogni data combinazione di chiave, messaggio e firma:
- Le codifiche non canoniche di
Ro dello scalare di firmaS(in particolare qualsiasiS ≥ ℓ, l'ordine del gruppo) DEVONO essere rifiutate. - Le chiavi pubbliche e i valori
Rdi ordine piccolo / sottogruppo piccolo / con componente di torsione DEVONO essere rifiutati. - L'equazione di verifica con cofattore (la forma in stile ZIP-215, adatta alla verifica in batch) NON DEVE essere sostituita all'equazione rigorosa.
È il rigore a rendere il verdetto riproducibile tra implementazioni diverse: un verificatore con cofattore accetterebbe firme che uno rigoroso rifiuta, quindi due verificatori conformi non sarebbero d'accordo. Le implementazioni devono scegliere una libreria, o una modalità di libreria, che esegua una verifica rigorosa e senza cofattore.
Semantica del verdetto
Le firme sono additive, quindi una firma non verificabile è segnalata sulla voce,
non promossa a un fallimento a livello di record. Ogni sigs[i] si risolve in uno
di questi esiti tipizzati per voce; il catalogo completo degli errori e le regole
del verdetto a livello di record si trovano nella pagina
Verifica:
| Esito | Significato |
|---|---|
| verificata | Ed25519 rigoroso (e, per la modalità 2, il binding dell'address) ha avuto successo. |
| firma non supportata | L'alg dell'header protetto è al di fuori dell'insieme del verificatore. Informativo, mai un errore. |
| chiave del firmatario non risolta | Nessuna modalità consentita produce una chiave pubblica Ed25519 di 32 byte. |
| firma non valida | Ed25519 rigoroso ha restituito false sulla Sig_structure ricostruita. |
| address del wallet non corrispondente | Modalità 2: la firma è stata verificata, ma lo stake address ricalcolato ≠ quello dichiarato. |
Una firma non supportata non invalida mai la prova
Un algoritmo di firma non riconosciuto o non supportato produce un esito tipizzato di firma non
supportata con gravità informativa. La rivendicazione sul contenuto e sull'orario (l'impegno
hashes on-chain) è strutturalmente valida indipendentemente da quali algoritmi di firma un
verificatore implementi. Un record che porta solo firme con algoritmi futuri si presenta comunque
come una prova di esistenza valida, con ciascuna di queste voci contrassegnata come non
supportata. Le firme sono additive; l'esistenza non dipende da esse.
Pagine correlate
- Chiavi — la chiave di firma Ed25519, la sua derivazione e la
chiave pubblica di 32 byte trasportata nel
kiddella modalità 1. - Il record — il campo
sigsdi primo livello, la mappa chiusasig-entry(cose_sign1/cose_keyciascuno un'unica stringa di byte) e il trasporto sull'intero corpo. - Verifica — i codici di esito per ogni voce, le regole del verdetto a livello di record e l'intera pipeline di validazione.
Chiavi
Il modello delle chiavi di Label 309, un unico seed da 32 byte, tre coppie di chiavi (una per algoritmo) derivate da esso tramite HKDF-SHA-256 con separazione di dominio, le chiavi di cifratura della chiave per slot che una PoE sigillata deriva al di sopra, e come le chiavi pubbliche dei destinatari e i segreti vengono codificati.
Sealed PoE
La busta di cifratura di Label 309. Come un mittente sigilla un contenuto verso una o più chiavi di destinatario, mentre la blockchain trasporta soltanto l'hash del testo in chiaro e gli slot di chiave incapsulati, mai il testo in chiaro e mai i destinatari.