Vada al contenuto
Condorcet.vote Alpha
← Documentazione tecnica

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

Anatomia

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_id UUID v4, minuscolo
L’anello contro cui la scheda è stata firmata: le sue chiavi pubbliche, e soltanto quelle, permettono di verificare la firma.
payload stringa, al massimo 8.192 byte
Il payload firmato, verbatim: un documento JSON trasportato come stringa opaca, mai riserializzato.
signature blsag_…
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_image ki_<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.
Scheda pubblicata · valori illustrativi
{
  "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.

Payload firmato

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.

v intero ≥ 1
La versione di protocollo dell’elezione (oggi 1); deve essere uguale a quella a cui l’elezione è legata per sempre.
election UUID v4, minuscolo
L’identificativo dell’elezione, che è anche il parametro election_id della firma. Mai con prefisso.
ballot stringa 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.
weight intero ≥ 1
Sempre presente. Uguale al peso dell’elettore nella lista elettorale; 1 salvo che weighted_voting sia attivo.
voter UUID v4, minuscolo, facoltativo
Solo voto non anonimo: obbligatorio e uguale all’identificativo dell’elettore. Vietato in un’elezione anonima.
Elezione anonima
{"v":1,"election":"0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21","ballot":"Alice > Bob = Charlie","weight":1}
Voto non anonimo
{"v":1,"election":"0c2e7e2a-5df3-4c0e-9d2b-3f6a8f0c9e21","ballot":"Alice > Bob = Charlie","weight":1,"voter":"2a3b4c5d-6e7f-4a8b-9c0d-1e2f3a4b5c6d"}
signed-payload.schema.json Il JSON Schema (draft 2020-12) della busta: lo stesso file usato dal server, servito all’URL canonico scritto nel suo $id. 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.
Firma e deposito

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. 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. 2 L’interfaccia di voto produce la riga di classifica; il browser costruisce il payload JSON, una sola volta.
  3. 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. 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. 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. 1 L’elezione è aperta.
  2. 2 L’elettore non ha ancora votato: questo controllo del registro delle presenze è la protezione principale contro il doppio voto.
  3. 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. 4 L’anello indicato appartiene all’elezione, e l’immagine di chiave non vi è mai stata usata.
  5. 5 La firma si verifica contro le chiavi di quell’anello (circa 0,5 secondi con 600 chiavi).
  6. 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.

Firma ad anello

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.
Immagine di chiave

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.

Definizione · x chiave segreta, G generatore, H_p hash verso la curva
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.
Digest dell’anello

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.
Codifiche

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).
Lettura di un valore · valore illustrativo
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.
Verifica

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. 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. 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. 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. 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. 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. 6 Controllare che l’immagine di chiave compaia una sola volta in tutta l’elezione.
Con la riga di comando cryptovote · valori illustrativi
$ 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 completa