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.
Label 309 ha bisogno di tre tipi di chiave asimmetrica: una chiave Ed25519
che firma i record, una chiave X25519 che riceve payload sigillati classici e
una chiave ibrida X-Wing (mlkem768x25519) che riceve payload sigillati
post-quantistici. Lo standard non tratta questi elementi come tre segreti
indipendenti da conservare e gestire separatamente. Definisce un solo segreto, un
seed da 32 byte, e una regola deterministica che lo espande in tutte e tre le
coppie di chiavi.
Questa pagina specifica tale derivazione: il seed, le tre espansioni HKDF con separazione di dominio che producono la chiave privata di ciascun algoritmo, il motivo per cui i domini restano distinti, le chiavi di cifratura della chiave per slot che una PoE sigillata deriva al di sopra di esse e come le chiavi pubbliche dei destinatari e i segreti così ottenuti vengono codificati per lo scambio. Ciò che un'implementazione fa del seed al di là di questo (dove risiede, come viene sbloccato, se una stessa persona ne detiene più di uno) esula dall'ambito di questo documento. A Label 309 interessa solo che, a partire dagli stessi 32 byte, ogni implementazione conforme derivi le stesse chiavi.
Il seed
Un insieme di chiavi Label 309 ha origine da un unico valore:
| Proprietà | Valore |
|---|---|
| Lunghezza | 32 byte (256 bit) |
| Origine | Un RNG crittograficamente sicuro, oppure un qualsiasi valore da 32 byte di proprietà dell'utente |
| Ruolo | Materiale di keying in ingresso (IKM) per le tre espansioni HKDF descritte di seguito |
Il seed è una pura sorgente di entropia, non una chiave nel senso proprio di un singolo algoritmo. Non porta con sé alcuna curva, alcuna lunghezza legata a una primitiva, alcuna cerimonia di codifica. Le chiavi che un'implementazione usa di fatto vengono negoziate a ogni derivazione; il seed sopravvive alle scelte di algoritmo effettuate su di esso. Un produttore PUÒ generare il seed ex novo dal CSPRNG della piattaforma oppure importare un valore da 32 byte già esistente; in entrambi i casi esso DEVE decodificarsi in esattamente 32 byte. A livello di derivazione non viene rifiutato alcuno schema a bassa entropia: un seed composto da soli zero è un input valido, ed è proprio questo che lo rende utilizzabile come fixture riproducibile per i test di conformità.
Il seed è l'intera identità
Ogni informazione di chiave pubblica che Label 309 esprime su una parte (la chiave che attesta un record, le chiavi che ricevono un payload sigillato) è una funzione deterministica di questi 32 byte. Riproducendo il seed si riproducono tutte e tre le coppie di chiavi, byte per byte.
Codificare il seed per il backup
Poiché il seed da 32 byte è l'identità, è il valore che un utente salva, esporta e importa, e un blob nudo di 32 byte è facile da troncare o corrompere in silenzio. Label 309 ne definisce una codifica in stringa con checksum, accettata accanto all'esadecimale grezzo ovunque un seed sia preso in input.
La forma in stringa è Bech32 (BIP-173,
classico, con il limite di 90 caratteri rimosso) sotto il prefisso leggibile
l309-seed- — il trattino finale fa parte dell'HRP, perciò il separatore Bech32 rende
il prefisso visibile l309-seed-1…. La codifica restituisce la forma di
visualizzazione in MAIUSCOLO L309-SEED-1…: i segreti devono farsi notare, e la
resa in maiuscolo è visivamente distinta dalle stringhe dei destinatari age1… in
minuscolo. La forma interamente in minuscolo è una codifica altrettanto valida degli
stessi byte.
seed (32 bytes) 0000…0000 -> L309-SEED-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQLFUN82Un parser accetta due rappresentazioni e si dirama in base alla forma:
- La stringa Bech32 in un solo caso di lettere (la combinazione di maiuscole e minuscole è rifiutata secondo BIP-173), con il checksum verificato e il payload decodificato di esattamente 32 byte.
- L'esadecimale grezzo — 64 cifre esadecimali, senza distinzione tra maiuscole e
minuscole, tollerando un prefisso
0xe gli spazi circostanti o interni.
Ogni input rifiutato è mappato su un codice di errore distinto dell'API di costruzione, così che un chiamante possa distinguere un refuso da un tipo di chiave errato:
| Input | Codice di errore |
|---|---|
| Una stringa Bech32 il cui checksum fallisce (un carattere invertito, un troncamento) | SEED_STRING_BAD_CHECKSUM |
| Una stringa Bech32 che mescola maiuscole e minuscole | SEED_STRING_MIXED_CASE |
Una stringa Bech32 valida sotto un HRP diverso (ad es. un destinatario age1…) | SEED_STRING_WRONG_HRP |
| Una stringa Bech32 o esadecimale che decodifica a ≠ 32 byte | SEED_STRING_WRONG_LENGTH |
| Qualsiasi cosa che non sia né una stringa Bech32 riconosciuta né esadecimale (vuota inclusa) | SEED_STRING_UNRECOGNIZED |
Questi codici descrivono il codec della stringa del seed, che è una comodità di
gestione delle chiavi attorno alla derivazione; sono distinti dal registro dei codici
di errore del wire che un validatore strutturale emette
(Verifica). La codifica trasporta i 32 byte nudi e nient'altro
— nessuna versione, nessun parametro di derivazione — perché il significato del seed
è fissato dalle tre stringhe info qui sotto, non da come è stato trasportato.
Derivare le tre coppie di chiavi
La chiave privata di ciascun algoritmo è un'espansione HKDF-SHA-256
indipendente dello stesso seed, secondo la
RFC 5869. Le tre espansioni condividono
il materiale di keying in ingresso e il salt (assente), e differiscono per un solo
parametro: la stringa info che identifica l'algoritmo:
| Algoritmo | Stringa info | Output |
|---|---|---|
| Ed25519 | cardano-poe-ed25519-v1 | secret seed Ed25519 da 32 byte |
| X25519 | cardano-poe-x25519-v1 | secret seed X25519 da 32 byte |
mlkem768x25519 | cardano-poe-mlkem768x25519-v1 | seed della chiave di decapsulazione X-Wing da 32 byte |
La derivazione in pseudocodice:
ed25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-ed25519-v1", length = 32)
x25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-x25519-v1", length = 32)
mlkem768x25519_priv = HKDF-SHA-256(ikm = seed, salt = "", info = "cardano-poe-mlkem768x25519-v1", length = 32)Tre regole rendono questi output interoperabili tra le diverse implementazioni:
- Il salt è vuoto. Il salt di HKDF DEVE essere la stringa di byte di
lunghezza zero. Secondo la
RFC 5869 §2.2, un salt
assente viene trattato come
HashLenbyte a zero (32 byte a zero per SHA-256), così che ogni libreria conforme arrivi allo stesso passo di estrazione. - L'output è di 32 byte. Ogni espansione richiede esattamente 32 byte (un singolo blocco HKDF per SHA-256).
- Le stringhe
infosono ASCII esatto. Ogni valoreinfoDEVE essere codificato esattamente con i byte indicati: nessuno spazio attorno, nessun terminatore a zero, nessun byte-order mark, nessun newline finale. Le tre stringhe sono rispettivamente di 22, 21 e 29 byte.
I 32 byte di output sono il secret seed dell'algoritmo, non il suo scalare di curva espanso. La RFC 8032 §5.1.5 traccia questa distinzione per Ed25519: il secret seed è di 32 byte, e la libreria di firma lo espande internamente (tramite SHA-512, seguito dal clamping) nello scalare effettivo e nel prefisso di firma. Lo stesso vale per X25519, dove il clamping è applicato all'interno della primitiva secondo la RFC 7748 §5. Un'implementazione DEVE passare alla primitiva i 32 byte grezzi prodotti da HKDF e lasciare che sia la libreria a eseguire l'espansione e il clamping: non effettua né il clamping né l'espansione in anticipo. Per X-Wing, i 32 byte di output sono il seed della chiave di decapsulazione di X-Wing, da cui la coppia di chiavi completa (inclusa la chiave pubblica da 1216 byte) viene rigenerata in modo deterministico dalla generazione delle chiavi X-Wing. In ogni caso, la forma canonica da conservare e trasportare è il compatto seed da 32 byte, mai una chiave espansa.
Perché tre domini e non uno
Il parametro info di HKDF è la sua etichetta di separazione di dominio: lega
l'output espanso a uno specifico contesto applicativo, e la
RFC 5869 §3.1 ne raccomanda
vivamente l'uso quando il contesto è disponibile. Label 309 fornisce un'etichetta
distinta per ciascun algoritmo, anziché riutilizzare un'unica espansione per tutti
e tre, anche se le tre chiavi private hanno tutte una larghezza di 32 byte. Il
motivo è l'isolamento:
- I problemi restano circoscritti. Se due coppie di chiavi condividessero byte identici, una debolezza specifica di un algoritmo (un difetto nella derivazione del nonce, un side-channel su una moltiplicazione scalare) potrebbe esporre la chiave di un algoritmo del tutto estraneo. La separazione di dominio garantisce che le tre chiavi private siano funzioni indipendenti del seed, così che la compromissione di una non riveli a un attaccante nulla sulle altre.
- La migrazione resta additiva. Ogni stringa
infotermina con-v1. L'adozione di una curva diversa o di un diverso schema ibrido in una revisione futura deriva dallo stesso seed una nuova chiave-v2sotto una nuova etichetta, senza alcuna collisione con le chiavi v1 già in uso. Ciò rispecchia l'agilità rispetto agli algoritmi su cui si fonda lo stesso wire format.
La terza etichetta, cardano-poe-mlkem768x25519-v1, assegna allo schema ibrido
post-quantistico un proprio dominio, anche se il seed della sua chiave di
decapsulazione ha la stessa larghezza di 32 byte del segreto X25519 classico. Un
difetto in ML-KEM-768, in X25519 o nel combiner di X-Wing non può dunque
contaminare la chiave di cifratura classica né la chiave di firma.
Questa etichetta della chiave d'identità, cardano-poe-mlkem768x25519-v1, non porta
inoltre alcun segmento -kek-: è distinta dall'etichetta di derivazione della
KEK per record cardano-poe-kek-mlkem768x25519-v1 più sotto, così che l'espansione
seed → chiave d'identità e l'avvolgimento della chiave per slot di una PoE sigillata
non condividano mai una stringa info.
Chiavi di cifratura della chiave per slot
Le tre coppie di chiavi derivate dal seed qui sopra sono chiavi d'identità a lunga durata. Una PoE sigillata aggiunge un secondo livello di HKDF-SHA-256, questa volta per record: per ciascuno slot del destinatario il mittente deriva una nuova chiave di cifratura della chiave (KEK) da 32 byte che avvolge la chiave di cifratura del contenuto del record. La derivazione della KEK fa parte del modello delle chiavi, perciò è specificata qui; come la chiave avvolta viaggi poi nella busta è trattato in PoE sigillata.
Entrambi i KEM derivano la KEK con HKDF-SHA-256 e un info specifico del KEM, sotto
un salt a hash etichettato che lega tre valori: il materiale KEM proprio dello
slot (così che la KEK sia unica per slot), la chiave pubblica del destinatario pub_R
(così che un incapsulamento confezionato per un destinatario non possa essere reinviato
contro un altro) e il enc.nonce unico per busta (così che la KEK sia ancorata a
un'unica busta). Il segreto condiviso è l'output dell'ECDH proprio del KEM (classico) o
della decapsulazione X-Wing (ibrido), lo stesso valore da 32 byte sia che il mittente
incapsuli sia che il destinatario decapsuli, e salt e info sono identici su
entrambi i lati:
; x25519 (classico) — il salt è uno SHA-256 etichettato sulla chiave effimera e su quella del destinatario
kek_salt = SHA-256("cardano-poe-x25519-kek-salt-v1" || enc.nonce || pub_epk || pub_R) ; 32 bytes
KEK = HKDF-SHA-256(ikm = shared, ; il segreto condiviso ECDH X25519
salt = kek_salt,
info = "cardano-poe-kek-v1",
L = 32)
; mlkem768x25519 (ibrido) — stessa forma di salt etichettato sotto l'etichetta propria dell'ibrido
kek_salt = SHA-256("cardano-poe-xwing-kek-salt-v1" || enc.nonce || kem_ct || pub_R) ; 32 bytes
KEK = HKDF-SHA-256(ikm = shared, ; il segreto condiviso X-Wing
salt = kek_salt,
info = "cardano-poe-kek-mlkem768x25519-v1",
L = 32)I due salt hanno la stessa forma — SHA-256(label || enc.nonce || <materiale KEM dello slot> || pub_R) — e differiscono solo per l'etichetta specifica del KEM e per
quale materiale KEM trasportano: la chiave effimera pub_epk da 32 byte sul percorso
classico, il testo cifrato X-Wing kem_ct da 1120 byte sul percorso ibrido. Entrambi
vengono compressi attraverso un digest SHA-256 a lunghezza fissa perché gli input
ibridi sono sovradimensionati per un salt grezzo e un'unica forma uniforme tiene
allineati i due percorsi. Il binding è calcolato all'esterno del KEM, sui byte wire
dello slot stesso, perciò tratta X-Wing come un KEM a scatola nera e non fa affidamento
su alcuna proprietà dell'hashing interno del combiner. L'etichetta info distinta per
KEM garantisce inoltre che una KEK derivata sotto un KEM non possa mai coincidere con
una KEK derivata sotto l'altro a parità di segreto condiviso da 32 byte.
In entrambi i salt pub_R è la codifica wire canonica della chiave del
destinatario: esattamente la chiave pubblica X25519 da 32 byte per x25519,
esattamente la stringa di byte della chiave pubblica X-Wing fissata da 1216 byte per
mlkem768x25519. Il produttore e il destinatario DEVONO usare quella codifica
esatta e NON DEVONO sostituirla con alcun equivalente non canonico o ricodificato:
in caso contrario i due lati fornirebbero a HKDF salt diversi e deriverebbero KEK
diverse, e lo slot non si aprirebbe mai.
Ciascuna KEK e il suo prefisso del salt sono mattoni di base interni di
enc.scheme: 1: non portano alcun identificatore del wire e non sono selezionabili.
Le due etichette di prefisso del salt e le due etichette info qui sono quattro degli
undici literali di label del costrutto sigillato catalogati su
Registri degli algoritmi; un verificatore DEVE usare
ciascuna byte per byte.
Codifiche delle chiavi pubbliche dei destinatari
Il mittente di una PoE sigillata ha bisogno della chiave pubblica del destinatario in una forma testuale trasportabile, e un destinatario salva il proprio segreto nella forma corrispondente. Label 309 riutilizza le codifiche Bech32 dei destinatari adottate dall'ecosistema age, un prefisso leggibile dall'uomo (HRP) per ciascun meccanismo di incapsulamento delle chiavi registrato.
In Bech32 l'1 è il separatore tra l'HRP e la parte dati, perciò il prefisso
visibile di una stringa è il suo HRP più quell'1. L'HRP e il prefisso visibile
sono dunque distinti, e la tabella li tiene in colonne separate:
KEM (enc.kem) | Chiave pubblica | HRP della chiave pubblica | Prefisso visibile della chiave pubblica | HRP del segreto | Prefisso visibile del segreto |
|---|---|---|---|---|---|
x25519 | chiave pubblica X25519 da 32 byte | age | age1… (62 caratteri) | AGE-SECRET-KEY- | AGE-SECRET-KEY-1… |
mlkem768x25519 | chiave pubblica X-Wing da 1216 byte | age1pqc | age1pqc1… (1960 caratteri) | AGE-SECRET-KEY-PQ- | AGE-SECRET-KEY-PQ-1… |
La stringa di destinatario classica x25519 ha HRP age e la forma age v1 standard
age1…. La chiave pubblica ibrida concatena una chiave di incapsulamento ML-KEM-768
(1184 byte) con una chiave pubblica X25519 (32 byte); a 1216 byte, la sua stringa di
destinatario age1pqc1… è di 1960 caratteri.
Il segreto che un'implementazione salva e importa è, su entrambi i percorsi, il
seed da 32 byte: il seed segreto X25519 sotto AGE-SECRET-KEY-, e il seed della chiave
di decapsulazione X-Wing (il terzo output HKDF qui sopra, info = "cardano-poe-mlkem768x25519-v1") sotto AGE-SECRET-KEY-PQ-. La chiave pubblica ibrida
da 1216 byte si deriva da quel seed; è il seed compatto, mai la chiave espansa, il
segreto canonico da conservare.
Il BIP-173 limita
una stringa Bech32 a 90 caratteri, ma quel limite esiste per gli indirizzi di
pagamento digitati a mano e qui non si applica. Un'implementazione DEVE
codificare e decodificare la stringa age1pqc1… senza imporre il limite dei 90
caratteri, pur continuando ad applicare le regole di checksum e di set di caratteri
di Bech32. L'HRP distinto age1pqc impedisce al destinatario ibrido di entrare in
collisione con un qualsiasi destinatario classico age, e non è volutamente age1pq,
il prefisso più corto che una codifica nativa ML-KEM-768 + X25519 a monte rivendica
già per la stessa primitiva, così che le due codifiche dei destinatari non collidano
mai sul wire. La codifica classica rimane entro lunghezze ordinarie e viene gestita
senza modifiche.
Queste stringhe servono unicamente come comodità per individuare i destinatari. Una
chiave pubblica di un destinatario non compare mai nella busta di cifratura di
un record Label 309: una voce di enc.slots[] contiene il materiale di chiave
specifico dello slot e un valore wrap, e l'identificatore del KEM compare una sola
volta in enc.kem. Il modo in cui vengono costruiti la busta e gli slot è trattato
in PoE sigillata.
La chiave pubblica Ed25519 come kid della firma
La chiave pubblica Ed25519 non svolge alcun ruolo di destinatario; è
l'identificatore di chiave rispetto al quale un verificatore risolve una firma.
Quando un produttore firma un record, i 32 byte grezzi della chiave pubblica Ed25519
sono il kid (label 4) nell'header protetto di COSE_Sign1, secondo la
RFC 9052. Un verificatore legge quel
valore da 32 byte direttamente dalla firma on-chain (sulla blockchain) e ne verifica
il corpo del record: la chiave pubblica viaggia insieme alla firma, quindi non serve
alcuna ricerca separata per verificare la paternità. La costruzione completa della
firma, il payload firmato e le regole di verifica sono specificati in
Firme.
Scambio di chiavi fuori banda
Label 309 specifica come le chiavi pubbliche dei destinatari vengono codificate,
non come vengono individuate. Lo standard non prescrive alcuna directory, alcun
registro né alcun formato di annuncio on-chain per le chiavi dei destinatari. Chi
vuole ricevere un payload sigillato pubblica la propria stringa age1… o
age1pqc1… attraverso un qualsiasi canale di cui entrambe le parti già si fidano (una
consegna di persona, un record firmato con la propria chiave Ed25519, un record
collocato in una posizione web stabile o indirizzabile per contenuto), e il mittente
è responsabile della provenienza di qualsiasi chiave verso cui cifra.
Si tratta di un confine deliberato. La stessa proprietà che permette di verificare un record senza fidarsi di un server implica che lo scambio di chiavi non debba reintrodurre di nascosto un intermediario fidato. Un nome posto accanto a una chiave è un'attestazione di chi lo ha collocato lì, mai una rivendicazione crittografica: due parti che usano lo stesso identificativo producono comunque chiavi con byte diversi, e il verificatore confronta i byte. Associare alle chiavi nomi leggibili dall'uomo è qualcosa che un'applicazione costruita su Label 309 PUÒ offrire, ma è una funzionalità dell'applicazione, esterna al protocollo.
Pagine correlate
- Firme — come la chiave Ed25519 firma un record e come viene
verificato il
kid. - PoE sigillata — come le chiavi pubbliche X25519 e X-Wing indirizzano un payload cifrato a destinatari specifici.
- Registri degli algoritmi — gli identificatori con nome per firme, KEM, AEAD e KDF citati qui.
Registri degli algoritmi
I registri di identificatori per hash, AEAD, KEM, KDF e firme, e la regola di agilità che rende la migrazione post-quantistica additiva anziché incompatibile.
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.