Trustvice è in beta limitata. Alla registrazione o all’accesso devi inserire la password da beta tester che hai ricevuto oppure richiederne una a preview@trustvice.com. Grazie.
Dentro il formato delle ricevute: JSON canonico, Ed25519 e chiavi pubblicate
Un giro campo per campo della busta delle ricevute Trustvice: perché firmiamo una forma canonica RFC 8785 invece dei byte in transito, come gli ID delle chiavi derivano dalle chiavi stesse e cosa promette il set di chiavi pubblicato.
Di The Trustvice team··6 min lettura
Una ricevuta ha un solo compito: essere controllata, da chiunque, tra anni, con codice che non abbiamo scritto noi. Questo requisito ha guidato ogni decisione del formato. Questo post percorre la busta, la canonicalizzazione, la firma e il set di chiavi, e spiega le scelte dietro ciascuno.
Le ricevute in questo formato condividono una busta; cambia solo il body da un tipo di ricevuta all’altro. Le prove firmate come un Proof-of-Erasure usano una busta gemella, con subject e claims al posto di body, con la stessa canonicalizzazione, la stessa firma e lo stesso set di chiavi descritti sotto.
Sempre "1". La versione del formato della ricevuta, non una versione dell’API.
receiptId
Un ULID di 26 caratteri, ordinabile per ora di creazione.
issuedAt
Istante ISO 8601 in UTC con millisecondi, impostato da Trustvice alla firma.
type
Il tipo di ricevuta. body.type è sempre uguale.
signatureKeyId
Il kid della chiave di firma nel set pubblicato: 22 caratteri base64url.
body
I fatti specifici del tipo.
signature
La firma Ed25519, base64url senza padding: 86 caratteri.
Dal modo in cui la firma è costruita seguono due proprietà, ed entrambe contano per chi scrive un verificatore.
Ogni membro tranne signature è firmato. Questo include qualsiasi membro aggiunto da una versione futura. Un verificatore non deve scartare i membri che non riconosce prima del controllo, o le ricevute valide falliranno. I nuovi campi opzionali sono additivi all’interno della versione 1, quindi chi legge dovrebbe ignorare ciò che non conosce, ma tenerlo quando verifica.
La ricevuta non nomina il tuo workspace o account. Il workspace è l’ambito in cui elenchi e recuperi le ricevute, ma non è scritto nella busta firmata. Una ricevuta è una dichiarazione autonoma su un fatto, non la prova di chi l’ha chiesta.
Un ID ricevuta è un ULID: 26 caratteri in base32 di Crockford. I primi dieci caratteri sono un timestamp a 48 bit in millisecondi e i restanti sedici sono casuali. Gli ID si ordinano quindi per ora di creazione.
L’ora dentro l’ID è generata a pochi millisecondi di distanza da issuedAt. Considera issuedAt l’ora firmata e l’ID un identificatore.
Una firma copre byte, non significato. JSON ha molte grafie a livello di byte per lo stesso valore: chiavi in qualsiasi ordine, spazi facoltativi, 4 o 4.0, "é" o "\u00e9". Se firmassimo i byte che ci capita di inviare, un proxy che reindenta la risposta, o una libreria che riordina le chiavi quando la salvi, romperebbe una ricevuta perfettamente valida.
La firma copre quindi una forma canonica, definita da RFC 8785, il JSON Canonicalization Scheme (JCS). Ogni implementazione corretta trasforma lo stesso valore analizzato negli stessi byte. Per una ricevuta, le regole si riducono a cinque:
Le chiavi sono ordinate a ogni livello di annidamento, per unità di codice UTF-16.
Nessuno spazio tra i token.
I numeri usano la conversione da numero a stringa di ECMAScript: gli interi si stampano senza punto decimale né esponente. I body delle ricevute contengono solo interi.
Le stringhe usano l’escape JSON minimo; i caratteri non ASCII sono emessi come UTF-8 grezzo, mai come \uXXXX, e la barra non è soggetta a escape.
Gli array mantengono il loro ordine, e i valori null e false sono conservati, non scartati.
Il messaggio firmato è la forma JCS della busta senza il membro signature, codificata in UTF-8. Dal lato della firma, i valori che non si possono rappresentare in modo identico ovunque, come NaN, infiniti, undefined o bigint, sono rifiutati invece che convertiti.
La conseguenza pratica per te: puoi formattare una ricevuta, salvarla in un database che riordina le chiavi o passarla in qualsiasi tool JSON che conservi i valori, e si verifica ancora. Non puoi cambiare un valore, rimuovere un membro null o arrotondare un numero. Sono esattamente le modifiche che un verificatore deve intercettare.
La firma è Ed25519 puro come specificato in RFC 8032: niente pre-hash e nessuna stringa di contesto. L’abbiamo scelto perché è piccolo, veloce, deterministico e implementato nelle librerie standard che le persone hanno già. Node 20 e successivi, i browser attuali, Deno, Bun e Cloudflare Workers possono verificarlo con il solo WebCrypto; Python con il pacchetto cryptography; OpenSSL 3 dalla shell. Un verificatore per una ricevuta Trustvice non dovrebbe mai aver bisogno di una libreria Trustvice.
Una firma è di 64 byte, cioè gli 86 caratteri base64url nella busta. Una chiave pubblica è di 32 byte.
Trustvice custodisce le chiavi private negli HSM di AWS KMS, una chiave per regione dei dati, da cui non escono mai. Il firmatario invia i byte canonici a KMS e riceve in cambio una firma; può firmare ma non esportare una chiave.
Il signatureKeyId di una ricevuta corrisponde al kid di una chiave nel set pubblicato. Quel kid non è un’etichetta scelta da noi. È calcolato dalla chiave:
text
kid = base64url( first 16 bytes of SHA-256( the 32 raw bytes of x ) )
senza padding, il che dà 22 caratteri. Ne seguono due conseguenze utili.
Primo, una voce del set di chiavi è autoverificabile: chiunque può ricalcolare il kid da x, e una voce in cui non corrispondono non è valida. In JavaScript:
Secondo, poiché il signatureKeyId sta dentro i byte firmati, una ricevuta non può essere ripuntata a una chiave diversa senza invalidarla. E poiché il kid deriva da x, un x cambiato per un kid che hai già non può essere legittimo.
Le chiavi pubbliche sono un JSON Web Key Set su https://trustvice.com/.well-known/receipt-keys.json, che reindirizza con HTTP 308 a https://trustvice.com/api/v1/receipts/keys. È servito come JSON, memorizzabile in cache per un’ora, e con Access-Control-Allow-Origin: *, così una pagina sul tuo dominio può scaricarlo e verificare le ricevute nel browser.
Per verificare ti servono solo kid, kty, crv e x. Il set può contenere più chiavi, e le chiavi ritirate restano pubblicate. Le ricevute di un workspace possono portare ID di chiave diversi nel tempo, quindi cerca sempre la chiave per ogni ricevuta.
Il set è costruito da configurazione pubblicata, quindi la verifica non dipende mai da una chiamata ad AWS. Vi compaiono solo campi pubblici.
Di default un verificatore si fida di ciò che l’endpoint delle chiavi restituisce nel momento in cui lo interroga, il che è forte solo quanto quella connessione TLS. Per prove che contano, fissa il set: scaricalo una volta da una rete di cui ti fidi, ricalcola ogni kid dal suo x, salva le coppie in un posto che cambia solo con una revisione, e verifica con quel file. Aggiornalo a intervalli regolari e rivedi le nuove chiavi prima di aggiungerle.
Conserva il set di chiavi che hai usato accanto a ogni ricevuta che tieni come prova. Una ricevuta più il set di chiavi con cui è stata verificata si verifica da sola.
Una ricevuta nomina la chiave che l’ha firmata, quindi aggiungere una chiave al set non tocca mai le ricevute precedenti. Oggi non c’è rotazione automatica e le chiavi non hanno scadenza. La rotazione è una cerimonia manuale, fuori banda, e il set non porta alcun marcatore di chiave ritirata o revocata. La rotazione è additiva: una chiave ritirata resta pubblicata perché le ricevute che ha firmato restino verificabili. È una policy, non qualcosa che l’endpoint delle chiavi impone: un motivo in più per tenere una tua copia fissata.
Non c’è nemmeno una lista di revoca. Se una chiave di firma venisse compromessa, il rimedio sarebbe smettere di pubblicarla, il che farebbe fallire con unknown_key ogni ricevuta firmata con essa, comprese quelle autentiche. Tenere una tua copia del set di chiavi del momento in cui hai accettato una ricevuta, e registrare la ricevuta in un sistema che controlli tu, mantiene comunque utilizzabili le tue prove.
Incolla una qualsiasi ricevuta Trustvice nel verificatore. Controlla la firma nel tuo browser con le nostre chiavi pubblicate, senza bisogno di un account.