Skip to content

Instantly share code, notes, and snippets.

@ruescasd
Created October 29, 2025 11:24
Show Gist options
  • Select an option

  • Save ruescasd/4996a7de9e624c855740e400fc4b642d to your computer and use it in GitHub Desktop.

Select an option

Save ruescasd/4996a7de9e624c855740e400fc4b642d to your computer and use it in GitHub Desktop.
use crypto::context::Context;
use crypto::context::RistrettoCtx as RCtx;
use crypto::cryptosystem::elgamal::{Ciphertext, KeyPair};
use crypto::traits::groups::GroupElement;
fn main() {
let keypair: KeyPair<RCtx> = KeyPair::generate();
// voter submission
// encrypt vote
let message = [RCtx::random_element(); 1];
let r = [RCtx::random_scalar(); 1];
let ciphertext = keypair.encrypt_with_r(&message, &r);
// compute cv1
let cv1 = [RCtx::random_element(); 1];
// note that we are encrypting cv1 with the same randomness r as the vote, which means
// their first values must be equal
let ecv1 = keypair.encrypt_with_r(&cv1, &r);
// check that the first element of the original ciphertext matches the first element of ecv1
assert_eq!(ciphertext.u()[0], ecv1.u()[0]);
// ea tagging
// computes cv2
let cv2 = [RCtx::random_element(); 1];
let ecv2 = keypair.encrypt_with_r(&cv2, &r);
// election ends, tally filtering
// ea multiplies ecv1 and ecv2:
// e(cv) = e(cv1) * e(cv2) = e(cv1 * cv2)
let ecv = ecv1.0.mul(&ecv2.0);
// ea combines ciphertext and cv
let combined_u = [ciphertext.u()[0], ecv[0][0]];
let combined_v = [ciphertext.v()[0], ecv[1][0]];
// resulting in a single ciphertext
// (e(message), e(cv1 * cv2))
// ==>
// // e(message, cv1 * cv2)
let combined: Ciphertext<RCtx, 2> = Ciphertext::new(combined_u, combined_v);
// shuffle
// (ballot, cv) pairs are shuffled as a single ciphertext
let r_n = [RCtx::random_scalar(); 2];
let shuffled = combined.re_encrypt(&r_n, &keypair.pkey.y);
// in a real election _only the cv values are decrypted_ at the filtering stage,
// by splitting the shuffled outputs
// e(message, cv1 * cv2)
// into
// (e(message), e(cv1 * cv2))
// and decrypting only the second element of each pair
let decrypted = keypair.decrypt(&shuffled);
// to deniably cancel
// * voter reveals cv1 (this can be checked with randomness used to encrypt cv1, showing the ea that
// it is attached to _a_ ballot)
// * ea reveals cv2 (this can be checked with randomness used to encrypt cv2, showing the voter that it is
// attached to _their_ ballot)
// then, voter submits cv = cv1 * cv2, eg in a box
let cv = cv1[0].mul(&cv2[0]);
// the cv = cv1 * cv2 values extracted from the box are used to
// select ballots for cancelling
// ballots with attached cancel value = cv1 * cv2 are filtered out, by matching with the decrypted
// second element of a (enc(ballot), enc(cv)) pair, since
// cv1 * cv2 = dec(enc(cv1 * cv2)) = dec(e(cv1) * e(cv2)) = dec(enc(cv))
// note that
// * the ea cannot cancel a ballot without the voter's value cv1
// * the voter can verify that cv1 * cv2 is used to cancel a ballot, and
// the ea cannot silently refuse to cancel a ballot, as they must publish cv1 * cv2.
// * the coercer cannot detect whether the voter's ballot was cancelled, even if they know cv1, since they do not know cv2,
// moreover, if the coercer demands the value cv2 from the voter, they cannot distinguish a voter that did
// not cancel, from one that did and lied
assert_eq!(decrypted[1], cv);
// first element must match original message
assert_eq!(decrypted[0], message[0]);
/*
Note regarding a denied cancel value attack
* If your coercer denies your cancel value from you,
it is still possible for the trustees to decrypt your
cancel value, and follow the normal deniable cancelling procedure.
This trades a small amount of correctness, as in theory,
the trustees could collude to cancel votes without the
voter's consent.
* The most frequent coercer scenarios occurs
with a spouse or some other close relation, who will usually
not be qualified to set up a voting system that denies the cancel
value from the target.
*/
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment