Bulletin signé
Ce que le navigateur du votant signe, à l'octet près, et comment n'importe qui vérifie cette signature à partir des seules données publiées.
Un bulletin publié tient en quatre valeurs : l'anneau, la charge utile signée, la signature en anneau et l'image de clé. Elles relèvent du protocole de base et se vérifient sans la blockchain, qui ne fait que les transporter. Cette page les décrit champ par champ, avec leurs encodages et la procédure de contrôle d'une signature.
Protocole v1
Quatre valeurs par bulletin.
Un bulletin publié est un enregistrement de quatre valeurs, le même sur la page du registre et dans la blockchain. Aucune ne porte de date ni d'ordre d'arrivée.
-
ring_idUUID v4, minuscules - L'anneau contre lequel le bulletin a été signé : ses clés publiques, et elles seules, permettent de vérifier la signature.
-
payloadchaîne, 8 192 octets au plus - La charge utile signée, telle quelle : un document JSON transporté comme une chaîne opaque, jamais re-sérialisé.
-
signatureblsag_… - La signature en anneau chaînable sur les octets exacts de la charge utile : 32 × (1 + n) octets pour un anneau de n clés, soit 19 232 octets à 600 clés.
-
key_imageki_<64 hexa>_<8 hexa> - L'image de clé : une par clé et par élection. Deux bulletins portant la même image trahissent un double usage de la clé, visible de tous.
{
"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}"
}
Valeurs d'illustration : les formes sont correctes, mais les sommes de contrôle et la signature (tronquée ici) ne se vérifient pas. Dans un bloc votes, chacun de ces enregistrements est un objet compact sur une ligne du tableau ballots ; les bulletins y sont publiés par lots d'au moins 600 (le reliquat à la clôture), triés par image de clé. Le condensé d'anneau n'appartient pas au bulletin mais à son anneau : la page de chaque bulletin l'affiche pour l'anneau où il a été signé.
L'enveloppe, à l'octet près.
Un document JSON plat, cinq champs au plus, aucune propriété additionnelle. Le schéma décrit sa forme générique ; à la réception, v, election, weight et, en vote non anonyme, voter sont en outre fixés aux valeurs attendues pour l'élection et le votant.
-
ventier ≥ 1 - La version de protocole de l'élection (1 aujourd'hui) ; elle doit être égale à celle à laquelle l'élection est liée pour toujours.
-
electionUUID v4, minuscules - L'identifiant de l'élection, qui est aussi le paramètre election_id de la signature. Jamais préfixé.
-
ballotchaîne UTF-8 - L'expression du votant, une chaîne plate, jamais du JSON imbriqué : une ligne de classement CEF ou la sentinelle /EMPTY_RANKING/. Contenu non validé.
-
weightentier ≥ 1 - Toujours présent. Égal au poids du votant sur la liste électorale ; 1 sauf si weighted_voting est activé.
-
voterUUID v4, minuscules, optionnel - Vote non anonyme seulement : obligatoire et égal à l'identifiant du votant. Interdit dans une élection anonyme.
{"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
Télécharger
↓
- Construite une fois, stockée à l'octet. Le navigateur produit la chaîne JSON une seule fois ; elle est signée, transmise et stockée telle quelle, en colonne binaire (bytea, jamais jsonb, que PostgreSQL normaliserait). Ces octets sont le bulletin canonique : vérification, audit et dépouillement partent d'eux.
- Aucune forme canonique imposée. L'ordre des clés et les doublons ne sont ni normalisés ni rejetés : la signature couvre les octets exacts. Le client officiel émet un JSON compact, clés dans l'ordre v, election, ballot, weight, voter, Unicode non échappé. Le risque résiduel que deux analyseurs lisent différemment une charge utile pathologique est documenté et assumé.
- 8 192 octets au plus. Le plafond se compte en octets, puisque la signature porte sur les octets et que les étiquettes peuvent être en UTF-8 multi-octets. Il couvre le pire cas documenté : un classement complet de 30 candidats aux étiquettes de 64 caractères codés sur 4 octets, plus l'enveloppe et l'identifiant de votant, soit environ 7 836 octets.
- Enveloppe validée, contenu non. À la réception, le serveur valide l'enveloppe : JSON plat, plafond, schéma, valeurs imposées. Le contenu de ballot ne l'est délibérément pas : une expression malformée devient un bulletin nul au dépouillement, toujours stocké, publié et vérifiable.
- ballot, une ligne de classement. En pratique, une ligne en syntaxe de classement CEF, forme avec espaces : Alice > Bob = Charlie. Les ex æquo sont triés par point de code Unicode, pour qu'aucun ordre ne serve de marque cachée ; /EMPTY_RANKING/ exprime le vote blanc. Ni paramètre, ni quantificateur, ni étiquette CEF : un multiplicateur (* ou ^) rend le bulletin nul, il n'en gonfle jamais le poids.
- Le poids, signé et autorisé. weight est signé par le votant, donc impossible à modifier après coup, mais c'est la liste électorale qui fait autorité : le serveur rejette toute charge utile dont le poids diffère de celui du votant. Un votant, un bulletin, un poids.
- voter, contrôlé mais non prouvé. En vote non anonyme, le serveur vérifie que voter correspond à la session. La signature en anneau masquant le signataire, un tiers ne peut pas prouver à partir du seul registre que cet identifiant est bien celui du signataire : c'est une limite documentée.
- Une version de protocole, aucune version de bibliothèque. v est la version de protocole de l'élection, fixée pour toujours. Aucune version de la bibliothèque de signature ni du module WebAssembly n'est enregistrée ou publiée : une version par bulletin trahirait la chronologie des votes.
Signé dans le navigateur, vérifié à la réception.
La signature se fait dans le navigateur du votant, avec la bibliothèque CryptoVote compilée en WebAssembly ; seuls la charge utile, la signature, l'image de clé et l'identifiant d'anneau sont envoyés. Le serveur vérifie avec le même cœur Rust, dans sa version Extism.
Côté navigateur
- 1 La page reçoit l'anneau attribué (la liste de ses clés pk_), l'identifiant de l'élection et, en vote non anonyme, l'identifiant du votant. Elle calcule aussitôt, localement, le condensé de cet anneau et l'affiche.
- 2 L'interface de vote produit la ligne de classement ; le navigateur construit la charge utile JSON, une seule fois.
- 3 La clé secrète (distribuée par la plateforme, ou apportée par le votant) signe : sign_vote_str_wasm(secret, charge utile, election_id, anneau) renvoie la signature blsag_ et l'image de clé ki_.
- 4 La charge utile, la signature, l'image de clé et le ring_id partent sans aucune modification vers l'API JSON de la page de vote.
- 5 La clé secrète reste en mémoire de la page jusqu'à ce que le votant la quitte, le temps de construire le reçu une fois le bulletin accepté. La page de vote applique la politique de sécurité de contenu la plus stricte : aucun script tiers.
Côté serveur, dans cet ordre
- 1 L'élection est ouverte.
- 2 Le votant n'a pas encore voté : ce contrôle du registre d'émargement est la protection principale contre le double vote.
- 3 L'enveloppe est valide : 8 192 octets au plus, JSON plat, conforme au schéma, avec v, election, weight et voter égaux aux valeurs attendues.
- 4 L'anneau désigné appartient à l'élection, et l'image de clé n'y a encore jamais servi.
- 5 La signature se vérifie contre les clés de cet anneau (de l'ordre de 0,5 seconde à 600 clés).
- 6 Une transaction atomique marque le votant comme ayant voté, enregistre l'anneau où il a voté et insère le bulletin, octets intacts, sans horodatage ; une contrainte d'unicité sur (élection, image de clé) départage les soumissions simultanées.
Le dépôt passe par la session du lien de vote : à cet instant, la plateforme voit quel votant dépose quel bulletin. Le protocole lui fait confiance au moment du vote, puis elle oublie ce lien : aucune donnée ne relie un votant identifié à son bulletin stocké, et tout ce qu'elle publie se vérifie sans elle. Un bulletin accepté est définitif.
bLSAG, avec des images de clé liées à l'élection.
Une signature en anneau prouve qu'un membre de l'anneau a signé, sans dire lequel. Celle-ci est chaînable : deux signatures de la même clé dans la même élection se reconnaissent.
- Le schéma. Une variante expérimentale de bLSAG (Back's Linkable Spontaneous Anonymous Group, la construction LSAG de Liu, Wei et Wong, celle de Monero), dont la seule différence est l'image de clé limitée à l'élection.
- Fondée sur Schnorr, sans en être une. bLSAG généralise le défi-réponse de Schnorr à un anneau : la réponse du signataire, s = α − c·x, est chaînée autour de tous les membres, si bien que la signature se vérifie pour l'anneau entier sans désigner son auteur.
- Les primitives. Courbe Ristretto255 (curve25519-dalek), hachage Blake2b-512. Une clé secrète est un scalaire de 32 octets ; la clé publique s'en déduit : pk = sk·G.
- Ce que la signature lie. Les octets exacts de la charge utile, l'identifiant de l'élection (election_id, normalisé NFC par la bibliothèque, ce qui ne change rien à un UUID ASCII) et l'ensemble des membres de l'anneau. Un octet changé, une autre élection ou un autre anneau, et la vérification échoue.
- L'ordre de l'anneau ne compte pas. La bibliothèque trie l'anneau en interne, des deux côtés : seul l'ensemble des clés importe. La plateforme le publie de toute façon en ordre croissant.
- Gel par anneau. L'ensemble des membres d'un anneau est identique dès la première signature, et pour toujours. Créer un anneau supplémentaire en cours de vote (anneau d'urgence) n'invalide rien.
- L'anonymat, c'est l'anneau. Le vérificateur ne peut pas dire quel membre a signé : l'ensemble d'anonymat cryptographique est l'anneau. L'ensemble pratique est plus petit, ce sont les votants de l'anneau qui ont effectivement voté ; les clés de remplissage n'ajoutent pas d'anonymat.
- Statut, honnêtement. La bibliothèque CryptoVote est expérimentale et n'a pas été auditée ; sa variante à images de clé liées à l'élection ne provient pas d'un article publié. Son code est ouvert (AGPL-3.0) et auditable ; une revue indépendante est prévue avant tout usage à fort enjeu.
Une par clé et par élection.
L'image de clé accompagne chaque signature. Elle ne désigne pas le signataire au sein de l'anneau, mais elle est la même chaque fois que la même clé signe dans la même élection : c'est ce qui rend un double usage visible de tous.
P = x · G I_e = x · H_p(domain ‖ len(election_id) ‖ election_id ‖ P)
- Liée à l'élection. La base H_p inclut l'identifiant de l'élection : une même clé produit la même image dans toute l'élection, tous anneaux confondus, et des images sans lien d'une élection à l'autre. C'est la seule différence avec le bLSAG standard, où I = x · H_p(P).
- Unicité contrôlée. À la réception, une contrainte d'unicité sur (élection, image de clé) rejette tout second usage. Un vérificateur contrôle la même chose sur l'ensemble des blocs votes : chaque ki_ n'y apparaît qu'une fois.
- Clé de tri. Les bulletins sont triés par image de clé, sur le registre comme dans chaque bloc votes : un ordre sans rapport avec l'arrivée des votes.
- Ce qu'elle ne détecte pas. Elle ne relie que les usages d'une même clé secrète. Un votant qui détiendrait deux clés (deux distributions, ou une distribution et une clé apportée) produirait deux images différentes : la protection principale reste le registre d'émargement, contrôlé avant la signature, avec la borne publique « bulletins ≤ votants ayant voté ».
- Une clé d'accès pour le votant. Avec son reçu, un votant retrouve son bulletin par son image de clé, avant même sa publication par lot : la recherche ciblée du registre accepte cette valeur à haute entropie, impossible à deviner, et ne permet aucune énumération.
- Prouver qu'elle est la sienne. S'il le choisit, le votant peut prouver qu'une image de clé est la sienne sans révéler sa clé secrète (opération D, la preuve de vote). Cette preuve est transférable et révèle le vote : c'est le choix du votant, jamais celui de l'organisateur.
Vérifier l'anneau qu'on vous a servi.
L'anonymat d'un bulletin, c'est exactement l'anneau sous lequel il a été signé. Une plateforme qui servirait à un votant un anneau différent d'une seule clé pourrait ensuite distinguer son bulletin. Le condensé rend cette manœuvre détectable.
- Le format. ring_<64 hexa>_<8 hexa> : l'empreinte canonique de l'ensemble des membres, indépendante de l'ordre, calculée par CryptoVote (version 1.0 et suivantes). Elle refuse ce que la signature refuserait : moins de deux membres, une clé dupliquée ou malformée.
- Calculé une fois, publié partout. Le serveur le calcule dans la transaction qui crée l'anneau et ne le recalcule jamais : l'anneau est gelé, son condensé aussi. Il figure sur le registre (fiche de l'anneau et page de chaque bulletin), dans le bloc rings et dans le téléchargement des clés de l'anneau.
- Recalculé par le votant. La page de vote calcule le condensé à partir de l'anneau qu'elle a reçu, l'affiche dans la confirmation et le récapitulatif, et l'écrit dans le reçu (ring_digest). Le serveur ne lui envoie jamais de condensé à comparer : une valeur servie avec l'anneau pourrait être falsifiée avec lui.
- Comparé publiquement. La comparaison qui compte est celle du votant, entre son condensé et la valeur publiée pour tous. Une différence signifie qu'il a signé sous un autre anneau que l'anneau publié.
- Ce qu'il garantit. La détection, pas la prévention. Le condensé se déduit des clés publiées : il n'ajoute aucune garantie cryptographique, seulement un point de comparaison pratique. Un vérificateur le recalcule à partir de public_keys.
- Le même calcul des deux côtés. Le serveur (Extism) et le navigateur (WebAssembly) exécutent le même cœur Rust ; un jeu de données de référence, lu par les deux suites de tests, garantit qu'ils obtiennent le même condensé au fil des mises à jour.
Des chaînes préfixées, avec somme de contrôle.
Chaque artefact cryptographique circule sous la forme <étiquette>_<corps hexa>_<somme de contrôle>. L'affichage ne ment pas : préfixe et somme de contrôle font partie de l'encodage réel, la valeur se copie telle quelle.
-
pk_32 octets · 64 hexa - Clé publique, membre d'un anneau. Publiée avec les anneaux.
-
sk_32 octets · 64 hexa - Clé secrète. Jamais publiée : elle ne figure que dans les fichiers que garde le votant.
-
ki_32 octets · 64 hexa - Image de clé d'un bulletin.
-
blsag_32 × (1 + n) octets - Signature en anneau, pour un anneau de n clés.
-
ring_32 octets · 64 hexa - Condensé d'anneau.
-
nonce_32 octets · 64 hexa - Nonce d'une preuve de vote (opération D).
-
own_64 octets · 128 hexa - Preuve de possession d'une image de clé (opération D).
pk_6eff732bec0d28b8…11abc91fdc1802cc_480cc89f │ │ │ │ │ └ somme de contrôle : 4 octets BLAKE3, 8 hexa │ └ corps : hexa minuscule, octets entiers └ étiquette : pk, sk, ki, blsag, ring, nonce, own
- Somme de contrôle avec l'étiquette. 4 octets d'empreinte BLAKE3, étiquette incluse dans le calcul : une faute de frappe, une valeur tronquée ou un pk_ collé là où un ki_ est attendu sont détectés aussitôt.
- Un garde-fou, pas une sécurité. N'importe qui peut calculer une somme de contrôle valide pour n'importe quels octets. L'authenticité vient uniquement de la preuve bLSAG.
- Hexa nu refusé. Les interfaces WebAssembly, Extism et en ligne de commande de CryptoVote, comme les bibliothèques du site, n'acceptent que la forme préfixée ; l'hexa nu est réservé à l'API Rust de la bibliothèque.
- Hors des octets signés. Les préfixes enveloppent clés, image de clé et signature, jamais la charge utile : son champ election reste un UUID v4 nu, et ballot n'est pas touché. L'identifiant d'un bulletin sur le registre est un UUID applicatif, pas un artefact CryptoVote.
Contrôler un bulletin contre son anneau.
Six étapes, à partir des seules données publiées, sans rien demander à la plateforme. Elles valent pour un bulletin lu sur le registre comme pour un bulletin lu dans un bloc votes.
- 1 Récupérer l'enregistrement du bulletin : ring_id, payload, signature, key_image (page du bulletin sur le registre, ou entrée du tableau ballots d'un bloc votes).
- 2 Récupérer les clés de l'anneau ring_id : le téléchargement JSON du registre (election_id, ring_id, ring_number, ring_digest, public_keys) ou l'entrée correspondante d'un bloc rings.
- 3 Recalculer le condensé à partir de public_keys et le comparer au ring_digest publié ; un votant le compare aussi à celui de son reçu.
- 4 Valider la charge utile contre le schéma signed-payload : election égal à l'identifiant de l'élection, v égal à sa version de protocole, voter présent si et seulement si le vote est non anonyme. weight se recoupe avec les poids de la liste d'émargement du bloc de clôture.
- 5 Vérifier la signature sur les octets exacts de la charge utile, avec l'identifiant de l'élection comme election_id, l'image de clé et l'anneau : le résultat est valide ou invalide, rien d'autre.
- 6 Contrôler que l'image de clé n'apparaît qu'une fois dans toute l'élection.
$ 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 contient une clé pk_ par ligne, tirée de public_keys. $PAYLOAD est la valeur du champ payload, c'est-à-dire la chaîne JSON décodée, sans re-sérialisation : un espace, un ordre de clés ou un échappement différent change les octets et fait échouer la vérification. Code de sortie : 0 valide, 1 invalide, 2 entrée incorrecte. Les mêmes opérations existent dans le module WebAssembly @condorcet.vote/crypto-vote (verify_vote_str_wasm, ring_digest_wasm) et dans la version Extism de la bibliothèque.
Ce que prouve une signature valide. Qu'un détenteur de l'une des clés de cet anneau a signé exactement ces octets, pour cette élection, sous cette image de clé. Elle ne dit pas lequel, ne prouve pas que le champ voter désigne le signataire (c'est un contrôle du serveur) et ne juge pas le contenu de ballot, classé exprimé, blanc ou nul au dépouillement.
La procédure de vérification complèteVoir aussi
- Registre public Ce que chaque élection publie, indépendamment de son transport : définition, anneaux, émargement, bulletins.
- Fichiers du votant Les fichiers que le votant peut télécharger : le reçu de vote et la clé privée de vote.
- Procédure de vérification Vérifier une élection de bout en bout : à partir du registre seul, ou avec la blockchain.