Questa è una traduzione a scopo informativo. Fa fede la versione inglese, che è quella normativa. Leggi la versione inglese

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
Lunghezza32 byte (256 bit)
OrigineUn RNG crittograficamente sicuro, oppure un qualsiasi valore da 32 byte di proprietà dell'utente
RuoloMateriale 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-1QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQLFUN82

Un 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 0x e 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:

InputCodice 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 minuscoleSEED_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 byteSEED_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:

AlgoritmoStringa infoOutput
Ed25519cardano-poe-ed25519-v1secret seed Ed25519 da 32 byte
X25519cardano-poe-x25519-v1secret seed X25519 da 32 byte
mlkem768x25519cardano-poe-mlkem768x25519-v1seed 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:

  1. 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 HashLen byte a zero (32 byte a zero per SHA-256), così che ogni libreria conforme arrivi allo stesso passo di estrazione.
  2. L'output è di 32 byte. Ogni espansione richiede esattamente 32 byte (un singolo blocco HKDF per SHA-256).
  3. Le stringhe info sono ASCII esatto. Ogni valore info DEVE 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 info termina 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 -v2 sotto 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 pubblicaHRP della chiave pubblicaPrefisso visibile della chiave pubblicaHRP del segretoPrefisso visibile del segreto
x25519chiave pubblica X25519 da 32 byteageage1… (62 caratteri)AGE-SECRET-KEY-AGE-SECRET-KEY-1…
mlkem768x25519chiave pubblica X-Wing da 1216 byteage1pqcage1pqc1… (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.