Scheda firmata
Che cosa firma il browser dell’elettore, byte per byte, e come chiunque verifica quella firma a partire dai soli dati pubblicati.
Una scheda pubblicata si riduce a quattro valori: l’anello, il payload firmato, la firma ad anello e l’immagine di chiave. Appartengono al protocollo di base e si verificano senza la blockchain, che si limita a trasportarli. Questa pagina li descrive campo per campo, con le loro codifiche e la procedura di controllo di una firma.
Protocollo v1
Quattro valori per scheda.
Una scheda pubblicata è un record di quattro valori, identico sulla pagina del registro e nella blockchain. Nessuno porta una data o un ordine di arrivo.
-
ring_idUUID v4, minuscolo - L’anello contro cui la scheda è stata firmata: le sue chiavi pubbliche, e soltanto quelle, permettono di verificare la firma.
-
payloadstringa, al massimo 8.192 byte - Il payload firmato, verbatim: un documento JSON trasportato come stringa opaca, mai riserializzato.
-
signatureblsag_… - La firma ad anello collegabile sui byte esatti del payload: 32 × (1 + n) byte per un anello di n chiavi, ossia 19.232 byte con 600 chiavi.
-
key_imageki_<64 hex>_<8 hex> - L’immagine di chiave: una per chiave e per elezione. Due schede con la stessa immagine rivelano un riuso della chiave, visibile a tutti.
{
"ring_id": "7d3f1c9a-2b4e-4f6a-8c1d-5e9b0a7f3c26",
"key_image": "ki_a08a870cf43ebf3f62009c5add2194bc1626cb6b3aa2bd8a280635bd76d45861_d9f38b14",
"signature": "blsag_5522c75bbd488b5e95bd2da407d66e20c83a909b86a1451465b73bbea8db6cac…_f5f947b9",
"payload": "{\"v\":1,\"election\":\"0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21\",\"ballot\":\"Alice > Bob = Charlie\",\"weight\":1}"
}
Valori illustrativi: le forme sono corrette, ma i checksum e la firma (qui troncata) non si verificano. In un blocco votes, ciascuno di questi record è un oggetto compatto su una riga dell’array ballots; le schede vi sono pubblicate in lotti di almeno 600 (il residuo alla chiusura), ordinate per immagine di chiave. Il digest dell’anello non appartiene alla scheda ma al suo anello: la pagina di ogni scheda lo mostra per l’anello in cui è stata firmata.
La busta, byte per byte.
Un documento JSON piatto, al massimo cinque campi, nessuna proprietà aggiuntiva. Lo schema ne descrive la forma generica; alla ricezione, v, election, weight e, nel voto non anonimo, voter sono inoltre fissati ai valori attesi per l’elezione e per l’elettore.
-
vintero ≥ 1 - La versione di protocollo dell’elezione (oggi 1); deve essere uguale a quella a cui l’elezione è legata per sempre.
-
electionUUID v4, minuscolo - L’identificativo dell’elezione, che è anche il parametro election_id della firma. Mai con prefisso.
-
ballotstringa UTF-8 - L’espressione dell’elettore come stringa piatta, mai JSON annidato: una riga di classifica CEF o il marcatore /EMPTY_RANKING/. Contenuto non validato.
-
weightintero ≥ 1 - Sempre presente. Uguale al peso dell’elettore nella lista elettorale; 1 salvo che weighted_voting sia attivo.
-
voterUUID v4, minuscolo, facoltativo - Solo voto non anonimo: obbligatorio e uguale all’identificativo dell’elettore. Vietato in un’elezione anonima.
{"v":1,"election":"0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21","ballot":"Alice > Bob = Charlie","weight":1}
{"v":1,"election":"0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21","ballot":"Alice > Bob = Charlie","weight":1,"voter":"2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d"}
https://admin.stg.condorcet.vote/schemas/signed-payload.schema.json
Scarica
↓
- Costruito una volta, conservato byte per byte. Il browser produce la stringa JSON una sola volta; viene firmata, trasmessa e conservata così com’è, in una colonna binaria (bytea, mai jsonb, che PostgreSQL normalizzerebbe). Questi byte sono la scheda canonica: verifica, audit e scrutinio partono da essi.
- Nessuna forma canonica imposta. L’ordine delle chiavi e i duplicati non sono né normalizzati né rifiutati: la firma copre i byte esatti. Il client ufficiale emette JSON compatto, chiavi nell’ordine v, election, ballot, weight, voter, Unicode non codificato con escape. Il rischio residuo che due parser leggano diversamente un payload patologico è documentato e accettato.
- Al massimo 8.192 byte. Il limite si conta in byte, poiché la firma copre i byte e le etichette possono essere UTF-8 multibyte. Copre il caso peggiore documentato: una classifica completa di 30 candidati con etichette di 64 caratteri codificati su 4 byte, più la busta e l’identificativo dell’elettore, circa 7.836 byte.
- Busta validata, contenuto no. Alla ricezione il server valida la busta: JSON piatto, limite, schema, valori fissati. Il contenuto di ballot deliberatamente no: un’espressione malformata diventa una scheda nulla allo scrutinio, sempre conservata, pubblicata e verificabile.
- ballot, una riga di classifica. In pratica, una riga in sintassi di classifica CEF, forma con spazi: Alice > Bob = Charlie. I candidati a pari merito sono ordinati per code point Unicode, affinché nessun ordine possa servire da segno nascosto; /EMPTY_RANKING/ esprime la scheda bianca. Nessun parametro, quantificatore o tag CEF: un moltiplicatore (* o ^) rende la scheda nulla, non ne aumenta mai il peso.
- Il peso: firmato e autorizzato. weight è firmato dall’elettore, quindi non può essere alterato in seguito, ma l’autorità è la lista elettorale: il server rifiuta qualsiasi payload il cui peso differisca da quello dell’elettore. Un elettore, una scheda, un peso.
- voter: controllato, non provato. Nel voto non anonimo, il server verifica che voter corrisponda alla sessione. Poiché la firma ad anello nasconde il firmatario, un terzo non può provare dal solo registro che questo identificativo sia quello del firmatario: un limite documentato.
- Una versione di protocollo, nessuna versione di libreria. v è la versione di protocollo dell’elezione, fissata per sempre. Nessuna versione della libreria di firma o del modulo WebAssembly è registrata o pubblicata: una versione per scheda rivelerebbe la cronologia dei voti.
Firmata nel browser, verificata alla ricezione.
La firma avviene nel browser dell’elettore, con la libreria CryptoVote compilata in WebAssembly; vengono inviati soltanto il payload, la firma, l’immagine di chiave e l’identificativo dell’anello. Il server verifica con lo stesso nucleo Rust, nella sua versione Extism.
Lato browser
- 1 La pagina riceve l’anello assegnato (l’elenco delle sue chiavi pk_), l’identificativo dell’elezione e, nel voto non anonimo, l’identificativo dell’elettore. Calcola subito, in locale, il digest di quell’anello e lo mostra.
- 2 L’interfaccia di voto produce la riga di classifica; il browser costruisce il payload JSON, una sola volta.
- 3 La chiave segreta (distribuita dalla piattaforma, o portata dall’elettore) firma: sign_vote_str_wasm(segreto, payload, election_id, anello) restituisce la firma blsag_ e l’immagine di chiave ki_.
- 4 Il payload, la firma, l’immagine di chiave e il ring_id vengono inviati senza alcuna modifica all’API JSON della pagina di voto.
- 5 La chiave segreta resta nella memoria della pagina finché l’elettore non la lascia, il tempo di costruire la ricevuta una volta accettata la scheda. La pagina di voto applica la politica di sicurezza dei contenuti più rigorosa: nessuno script di terze parti.
Lato server, in quest’ordine
- 1 L’elezione è aperta.
- 2 L’elettore non ha ancora votato: questo controllo del registro delle presenze è la protezione principale contro il doppio voto.
- 3 La busta è valida: al massimo 8.192 byte, JSON piatto, conforme allo schema, con v, election, weight e voter uguali ai valori attesi.
- 4 L’anello indicato appartiene all’elezione, e l’immagine di chiave non vi è mai stata usata.
- 5 La firma si verifica contro le chiavi di quell’anello (circa 0,5 secondi con 600 chiavi).
- 6 Un’unica transazione atomica segna l’elettore come votante, registra l’anello in cui ha votato e inserisce la scheda, byte intatti, senza marca temporale; un vincolo di unicità su (elezione, immagine di chiave) dirime gli invii simultanei.
Il deposito passa per la sessione del link di voto: in quell’istante la piattaforma vede quale elettore deposita quale scheda. Il protocollo le accorda fiducia al momento del voto, poi la piattaforma dimentica quel legame: nessun dato collega un elettore identificato alla sua scheda conservata, e tutto ciò che pubblica si verifica senza di essa. Una scheda accettata è definitiva.
bLSAG, con immagini di chiave legate all’elezione.
Una firma ad anello prova che un membro dell’anello ha firmato, senza dire quale. Questa è collegabile: due firme della stessa chiave nella stessa elezione si riconoscono.
- Lo schema. Una variante sperimentale di bLSAG (Back’s Linkable Spontaneous Anonymous Group, la costruzione LSAG di Liu, Wei e Wong, quella di Monero), la cui sola differenza è l’immagine di chiave legata all’elezione.
- Basata su Schnorr, senza esserlo. bLSAG generalizza la sfida-risposta di Schnorr a un anello: la risposta del firmatario, s = α − c·x, è concatenata attorno a tutti i membri, così che la firma si verifica per l’intero anello senza indicarne l’autore.
- Le primitive. Curva Ristretto255 (curve25519-dalek), hash Blake2b-512. Una chiave segreta è uno scalare di 32 byte; la chiave pubblica ne deriva: pk = sk·G.
- Che cosa lega la firma. I byte esatti del payload, l’identificativo dell’elezione (election_id, normalizzato NFC dalla libreria, il che non cambia nulla per un UUID ASCII) e l’insieme dei membri dell’anello. Basta cambiare un byte, l’elezione o l’anello, e la verifica fallisce.
- L’ordine dell’anello non conta. La libreria ordina l’anello internamente, da entrambi i lati: conta solo l’insieme delle chiavi. La piattaforma lo pubblica comunque in ordine crescente.
- Congelamento per anello. L’insieme dei membri di un anello è identico dalla prima firma in poi, per sempre. Creare un anello supplementare durante il voto (anello di emergenza) non invalida nulla.
- L’anonimato è l’anello. Il verificatore non può dire quale membro abbia firmato: l’insieme di anonimato crittografico è l’anello. L’insieme pratico è più piccolo, cioè gli elettori dell’anello che hanno effettivamente votato; le chiavi di riempimento non aggiungono anonimato.
- Stato, con franchezza. La libreria CryptoVote è sperimentale e non è stata sottoposta ad audit; la sua variante con immagini di chiave legate all’elezione non proviene da un articolo pubblicato. Il suo codice è aperto (AGPL-3.0) e verificabile; è prevista una revisione indipendente prima di qualsiasi uso ad alto rischio.
Una per chiave e per elezione.
L’immagine di chiave accompagna ogni firma. Non indica il firmatario all’interno dell’anello, ma è la stessa ogni volta che la stessa chiave firma nella stessa elezione: è ciò che rende un riuso visibile a tutti.
P = x · G I_e = x · H_p(domain ‖ len(election_id) ‖ election_id ‖ P)
- Legata all’elezione. La base H_p include l’identificativo dell’elezione: una stessa chiave produce la stessa immagine in tutta l’elezione, in tutti gli anelli, e immagini senza legame da un’elezione all’altra. È la sola differenza rispetto al bLSAG standard, dove I = x · H_p(P).
- Unicità controllata. Alla ricezione, un vincolo di unicità su (elezione, immagine di chiave) respinge ogni secondo uso. Un verificatore controlla lo stesso su tutti i blocchi votes: ogni ki_ vi compare una sola volta.
- Chiave di ordinamento. Le schede sono ordinate per immagine di chiave, sul registro come in ogni blocco votes: un ordine senza rapporto con l’arrivo dei voti.
- Che cosa non rileva. Collega soltanto gli usi di una stessa chiave segreta. Un elettore che detenesse due chiavi (due distribuzioni, oppure una distribuzione e una chiave portata) produrrebbe due immagini diverse: la protezione principale resta il registro delle presenze, controllato prima della firma, con il limite pubblico «schede ≤ elettori che hanno votato».
- Una chiave di ricerca per l’elettore. Con la propria ricevuta, un elettore ritrova la sua scheda tramite l’immagine di chiave, anche prima della pubblicazione per lotti: la ricerca mirata del registro accetta questo valore ad alta entropia, impossibile da indovinare, e non consente alcuna enumerazione.
- Provare che è la propria. Se lo desidera, l’elettore può provare che un’immagine di chiave è la sua senza rivelare la chiave segreta (operazione D, la prova di voto). La prova è trasferibile e rivela il voto: è una scelta dell’elettore, mai dell’organizzatore.
Verificare l’anello che Le è stato servito.
L’anonimato di una scheda è esattamente l’anello sotto cui è stata firmata. Una piattaforma che servisse a un elettore un anello diverso anche di una sola chiave potrebbe poi distinguerne la scheda. Il digest rende rilevabile questa manovra.
- Il formato. ring_<64 hex>_<8 hex>: l’impronta canonica dell’insieme dei membri, indipendente dall’ordine, calcolata da CryptoVote (versione 1.0 e successive). Rifiuta ciò che la firma rifiuterebbe: meno di due membri, una chiave duplicata o malformata.
- Calcolato una volta, pubblicato ovunque. Il server lo calcola nella transazione che crea l’anello e non lo ricalcola mai: l’anello è congelato, quindi anche il suo digest. Compare sul registro (scheda dell’anello e pagina di ogni scheda), nel blocco rings e nel download delle chiavi dell’anello.
- Ricalcolato dall’elettore. La pagina di voto calcola il digest dall’anello ricevuto, lo mostra nella conferma e nel riepilogo e lo scrive nella ricevuta (ring_digest). Il server non le invia mai un digest da confrontare: un valore servito con l’anello potrebbe essere confezionato insieme a esso.
- Confrontato pubblicamente. Il confronto che conta è quello dell’elettore, tra il proprio digest e il valore pubblicato per tutti. Una differenza significa che ha firmato sotto un anello diverso da quello pubblicato.
- Che cosa garantisce. Il rilevamento, non la prevenzione. Il digest deriva dalle chiavi pubblicate: non aggiunge alcuna garanzia crittografica, soltanto un punto di confronto pratico. Un verificatore lo ricalcola da public_keys.
- Lo stesso calcolo da entrambi i lati. Il server (Extism) e il browser (WebAssembly) eseguono lo stesso nucleo Rust; un insieme di dati di riferimento condiviso, letto da entrambe le suite di test, garantisce che ottengano lo stesso digest nel corso degli aggiornamenti.
Stringhe con prefisso, con checksum.
Ogni artefatto crittografico circola nella forma <tag>_<corpo hex>_<checksum>. La visualizzazione non mente: prefisso e checksum fanno parte della codifica reale, e il valore si copia così com’è.
-
pk_32 byte · 64 hex - Chiave pubblica, membro di un anello. Pubblicata con gli anelli.
-
sk_32 byte · 64 hex - Chiave segreta. Mai pubblicata: compare soltanto nei file che l’elettore conserva.
-
ki_32 byte · 64 hex - Immagine di chiave di una scheda.
-
blsag_32 × (1 + n) byte - Firma ad anello, per un anello di n chiavi.
-
ring_32 byte · 64 hex - Digest dell’anello.
-
nonce_32 byte · 64 hex - Nonce di una prova di voto (operazione D).
-
own_64 byte · 128 hex - Prova di possesso di un’immagine di chiave (operazione D).
pk_6eff732bec0d28b8…11abc91fdc1802cc_480cc89f │ │ │ │ │ └ checksum: 4 byte di BLAKE3, 8 hex │ └ corpo: hex minuscolo, byte interi └ tag: pk, sk, ki, blsag, ring, nonce, own
- Un checksum che include il tag. Un digest BLAKE3 di 4 byte con il tag incluso nel calcolo: un errore di battitura, un valore troncato o un pk_ incollato dove è atteso un ki_ vengono rilevati subito.
- Una protezione, non una sicurezza. Chiunque può calcolare un checksum valido per qualsiasi byte. L’autenticità deriva unicamente dalla prova bLSAG.
- Hex nudo rifiutato. Le interfacce WebAssembly, Extism e a riga di comando di CryptoVote, come le librerie del sito, accettano soltanto la forma con prefisso; l’hex nudo è riservato all’API Rust della libreria.
- Fuori dai byte firmati. I prefissi avvolgono chiavi, immagine di chiave e firma, mai il payload: il suo campo election resta un UUID v4 nudo, e ballot non viene toccato. L’identificativo di una scheda sul registro è un UUID applicativo, non un artefatto CryptoVote.
Controllare una scheda contro il suo anello.
Sei passaggi, a partire dai soli dati pubblicati, senza chiedere nulla alla piattaforma. Valgono per una scheda letta sul registro come per una letta in un blocco votes.
- 1 Recuperare il record della scheda: ring_id, payload, signature, key_image (pagina della scheda sul registro, oppure voce dell’array ballots di un blocco votes).
- 2 Recuperare le chiavi dell’anello ring_id: il download JSON del registro (election_id, ring_id, ring_number, ring_digest, public_keys) oppure la voce corrispondente di un blocco rings.
- 3 Ricalcolare il digest da public_keys e confrontarlo con il ring_digest pubblicato; un elettore lo confronta anche con quello della propria ricevuta.
- 4 Validare il payload contro lo schema signed-payload: election uguale all’identificativo dell’elezione, v uguale alla sua versione di protocollo, voter presente se e solo se il voto è non anonimo. weight si confronta con i pesi della lista delle presenze del blocco di chiusura.
- 5 Verificare la firma sui byte esatti del payload, con l’identificativo dell’elezione come election_id, l’immagine di chiave e l’anello: il risultato è valido o non valido, nient’altro.
- 6 Controllare che l’immagine di chiave compaia una sola volta in tutta l’elezione.
$ cryptovote ring-digest --ring ring.txt
digest=ring_bacc6d4ec64d44fc13c49ff6f6316e73a77554c3ff440fff55d62dc3e8ae1838_3c527fe4
$ cryptovote verify --vote "$PAYLOAD" \
--election-id 0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21 \
--signature blsag_5522c75b…_f5f947b9 \
--key-image ki_a08a870cf43ebf3f62009c5add2194bc1626cb6b3aa2bd8a280635bd76d45861_d9f38b14 \
--ring ring.txt
valid
ring.txt contiene una chiave pk_ per riga, tratta da public_keys. $PAYLOAD è il valore del campo payload, cioè la stringa JSON decodificata, senza riserializzazione: uno spazio, un ordine delle chiavi o un escape diverso cambia i byte e fa fallire la verifica. Codice di uscita: 0 valido, 1 non valido, 2 input errato. Le stesse operazioni esistono nel modulo WebAssembly @condorcet.vote/crypto-vote (verify_vote_str_wasm, ring_digest_wasm) e nella versione Extism della libreria.
Che cosa prova una firma valida. Che il detentore di una delle chiavi di questo anello ha firmato esattamente questi byte, per questa elezione, con questa immagine di chiave. Non dice quale, non prova che il campo voter indichi il firmatario (è un controllo del server) e non giudica il contenuto di ballot, classificato come espresso, bianco o nullo allo scrutinio.
La procedura di verifica completaVedi anche
- Registro pubblico Ciò che ogni elezione pubblica, indipendentemente dal mezzo di trasmissione: definizione, anelli, registro delle presenze, schede.
- File dell'elettore I file che l'elettore può scaricare: la ricevuta di voto e la chiave privata di voto.
- Procedura di verifica Verificare un'elezione da cima a fondo: dal solo registro, oppure con la blockchain.