ciphertext_commitment_equality.rs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. //! The ciphertext-commitment equality sigma proof system.
  2. //!
  3. //! A ciphertext-commitment equality proof is defined with respect to a twisted ElGamal ciphertext
  4. //! and a Pedersen commitment. The proof certifies that a given ciphertext and a commitment pair
  5. //! encrypts/encodes the same message. To generate the proof, a prover must provide the decryption
  6. //! key for the first ciphertext and the Pedersen opening for the commitment.
  7. //!
  8. //! The protocol guarantees computational soundness (by the hardness of discrete log) and perfect
  9. //! zero-knowledge in the random oracle model.
  10. #[cfg(target_arch = "wasm32")]
  11. use wasm_bindgen::prelude::*;
  12. #[cfg(not(target_os = "solana"))]
  13. use {
  14. crate::{
  15. encryption::{
  16. elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
  17. pedersen::{PedersenCommitment, PedersenOpening, G, H},
  18. },
  19. sigma_proofs::{canonical_scalar_from_optional_slice, ristretto_point_from_optional_slice},
  20. UNIT_LEN,
  21. },
  22. curve25519_dalek::traits::MultiscalarMul,
  23. rand::rngs::OsRng,
  24. zeroize::Zeroize,
  25. };
  26. use {
  27. crate::{
  28. sigma_proofs::errors::{EqualityProofVerificationError, SigmaProofVerificationError},
  29. transcript::TranscriptProtocol,
  30. },
  31. curve25519_dalek::{
  32. ristretto::{CompressedRistretto, RistrettoPoint},
  33. scalar::Scalar,
  34. traits::{IsIdentity, VartimeMultiscalarMul},
  35. },
  36. merlin::Transcript,
  37. };
  38. /// Byte length of a ciphertext-commitment equality proof.
  39. const CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN: usize = UNIT_LEN * 6;
  40. /// Equality proof.
  41. ///
  42. /// Contains all the elliptic curve and scalar components that make up the sigma protocol.
  43. #[cfg_attr(target_arch = "wasm32", wasm_bindgen)]
  44. #[allow(non_snake_case)]
  45. #[derive(Clone)]
  46. pub struct CiphertextCommitmentEqualityProof {
  47. Y_0: CompressedRistretto,
  48. Y_1: CompressedRistretto,
  49. Y_2: CompressedRistretto,
  50. z_s: Scalar,
  51. z_x: Scalar,
  52. z_r: Scalar,
  53. }
  54. #[allow(non_snake_case)]
  55. #[cfg(not(target_os = "solana"))]
  56. impl CiphertextCommitmentEqualityProof {
  57. /// Creates a ciphertext-commitment equality proof.
  58. ///
  59. /// The function does *not* hash the public key, ciphertext, or commitment into the transcript.
  60. /// For security, the caller (the main protocol) should hash these public components prior to
  61. /// invoking this constructor.
  62. ///
  63. /// This function is randomized. It uses `OsRng` internally to generate random scalars.
  64. ///
  65. /// Note that the proof constructor does not take the actual Pedersen commitment as input; it
  66. /// takes the associated Pedersen opening instead.
  67. ///
  68. /// * `keypair` - The ElGamal keypair associated with the first to be proved
  69. /// * `ciphertext` - The main ElGamal ciphertext to be proved
  70. /// * `opening` - The opening associated with the main Pedersen commitment to be proved
  71. /// * `amount` - The message associated with the ElGamal ciphertext and Pedersen commitment
  72. /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
  73. pub fn new(
  74. keypair: &ElGamalKeypair,
  75. ciphertext: &ElGamalCiphertext,
  76. opening: &PedersenOpening,
  77. amount: u64,
  78. transcript: &mut Transcript,
  79. ) -> Self {
  80. transcript.ciphertext_commitment_equality_proof_domain_separator();
  81. // extract the relevant scalar and Ristretto points from the inputs
  82. let P = keypair.pubkey().get_point();
  83. let D = ciphertext.handle.get_point();
  84. let s = keypair.secret().get_scalar();
  85. let x = Scalar::from(amount);
  86. let r = opening.get_scalar();
  87. // generate random masking factors that also serves as nonces
  88. let mut y_s = Scalar::random(&mut OsRng);
  89. let mut y_x = Scalar::random(&mut OsRng);
  90. let mut y_r = Scalar::random(&mut OsRng);
  91. let Y_0 = (&y_s * P).compress();
  92. let Y_1 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_s], vec![&G, D]).compress();
  93. let Y_2 = RistrettoPoint::multiscalar_mul(vec![&y_x, &y_r], vec![&G, &(*H)]).compress();
  94. // record masking factors in the transcript
  95. transcript.append_point(b"Y_0", &Y_0);
  96. transcript.append_point(b"Y_1", &Y_1);
  97. transcript.append_point(b"Y_2", &Y_2);
  98. let c = transcript.challenge_scalar(b"c");
  99. transcript.challenge_scalar(b"w");
  100. // compute the masked values
  101. let z_s = &(&c * s) + &y_s;
  102. let z_x = &(&c * &x) + &y_x;
  103. let z_r = &(&c * r) + &y_r;
  104. // zeroize random scalars
  105. y_s.zeroize();
  106. y_x.zeroize();
  107. y_r.zeroize();
  108. CiphertextCommitmentEqualityProof {
  109. Y_0,
  110. Y_1,
  111. Y_2,
  112. z_s,
  113. z_x,
  114. z_r,
  115. }
  116. }
  117. /// Verifies a ciphertext-commitment equality proof.
  118. ///
  119. /// * `pubkey` - The ElGamal pubkey associated with the ciphertext to be proved
  120. /// * `ciphertext` - The main ElGamal ciphertext to be proved
  121. /// * `commitment` - The main Pedersen commitment to be proved
  122. /// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
  123. pub fn verify(
  124. self,
  125. pubkey: &ElGamalPubkey,
  126. ciphertext: &ElGamalCiphertext,
  127. commitment: &PedersenCommitment,
  128. transcript: &mut Transcript,
  129. ) -> Result<(), EqualityProofVerificationError> {
  130. transcript.ciphertext_commitment_equality_proof_domain_separator();
  131. // extract the relevant scalar and Ristretto points from the inputs
  132. let P = pubkey.get_point();
  133. let C_ciphertext = ciphertext.commitment.get_point();
  134. let D = ciphertext.handle.get_point();
  135. let C_commitment = commitment.get_point();
  136. // include Y_0, Y_1, Y_2 to transcript and extract challenges
  137. transcript.validate_and_append_point(b"Y_0", &self.Y_0)?;
  138. transcript.validate_and_append_point(b"Y_1", &self.Y_1)?;
  139. transcript.validate_and_append_point(b"Y_2", &self.Y_2)?;
  140. let c = transcript.challenge_scalar(b"c");
  141. transcript.append_scalar(b"z_s", &self.z_s);
  142. transcript.append_scalar(b"z_x", &self.z_x);
  143. transcript.append_scalar(b"z_r", &self.z_r);
  144. let w = transcript.challenge_scalar(b"w"); // w used for batch verification
  145. let ww = &w * &w;
  146. let w_negated = -&w;
  147. let ww_negated = -&ww;
  148. // check that the required algebraic condition holds
  149. let Y_0 = self
  150. .Y_0
  151. .decompress()
  152. .ok_or(SigmaProofVerificationError::Deserialization)?;
  153. let Y_1 = self
  154. .Y_1
  155. .decompress()
  156. .ok_or(SigmaProofVerificationError::Deserialization)?;
  157. let Y_2 = self
  158. .Y_2
  159. .decompress()
  160. .ok_or(SigmaProofVerificationError::Deserialization)?;
  161. let check = RistrettoPoint::vartime_multiscalar_mul(
  162. vec![
  163. &self.z_s, // z_s
  164. &(-&c), // -c
  165. &(-&Scalar::ONE), // -identity
  166. &(&w * &self.z_x), // w * z_x
  167. &(&w * &self.z_s), // w * z_s
  168. &(&w_negated * &c), // -w * c
  169. &w_negated, // -w
  170. &(&ww * &self.z_x), // ww * z_x
  171. &(&ww * &self.z_r), // ww * z_r
  172. &(&ww_negated * &c), // -ww * c
  173. &ww_negated, // -ww
  174. ],
  175. vec![
  176. P, // P
  177. &(*H), // H
  178. &Y_0, // Y_0
  179. &G, // G
  180. D, // D
  181. C_ciphertext, // C_ciphertext
  182. &Y_1, // Y_1
  183. &G, // G
  184. &(*H), // H
  185. C_commitment, // C_commitment
  186. &Y_2, // Y_2
  187. ],
  188. );
  189. if check.is_identity() {
  190. Ok(())
  191. } else {
  192. Err(SigmaProofVerificationError::AlgebraicRelation.into())
  193. }
  194. }
  195. pub fn to_bytes(&self) -> [u8; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN] {
  196. let mut buf = [0_u8; CIPHERTEXT_COMMITMENT_EQUALITY_PROOF_LEN];
  197. let mut chunks = buf.chunks_mut(UNIT_LEN);
  198. chunks.next().unwrap().copy_from_slice(self.Y_0.as_bytes());
  199. chunks.next().unwrap().copy_from_slice(self.Y_1.as_bytes());
  200. chunks.next().unwrap().copy_from_slice(self.Y_2.as_bytes());
  201. chunks.next().unwrap().copy_from_slice(self.z_s.as_bytes());
  202. chunks.next().unwrap().copy_from_slice(self.z_x.as_bytes());
  203. chunks.next().unwrap().copy_from_slice(self.z_r.as_bytes());
  204. buf
  205. }
  206. pub fn from_bytes(bytes: &[u8]) -> Result<Self, EqualityProofVerificationError> {
  207. let mut chunks = bytes.chunks(UNIT_LEN);
  208. let Y_0 = ristretto_point_from_optional_slice(chunks.next())?;
  209. let Y_1 = ristretto_point_from_optional_slice(chunks.next())?;
  210. let Y_2 = ristretto_point_from_optional_slice(chunks.next())?;
  211. let z_s = canonical_scalar_from_optional_slice(chunks.next())?;
  212. let z_x = canonical_scalar_from_optional_slice(chunks.next())?;
  213. let z_r = canonical_scalar_from_optional_slice(chunks.next())?;
  214. Ok(CiphertextCommitmentEqualityProof {
  215. Y_0,
  216. Y_1,
  217. Y_2,
  218. z_s,
  219. z_x,
  220. z_r,
  221. })
  222. }
  223. }
  224. #[cfg(test)]
  225. mod test {
  226. use {
  227. super::*,
  228. crate::{
  229. encryption::{
  230. elgamal::ElGamalSecretKey,
  231. pedersen::Pedersen,
  232. pod::{
  233. elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
  234. pedersen::PodPedersenCommitment,
  235. },
  236. },
  237. sigma_proofs::pod::PodCiphertextCommitmentEqualityProof,
  238. },
  239. std::str::FromStr,
  240. };
  241. #[test]
  242. fn test_ciphertext_commitment_equality_proof_correctness() {
  243. // success case
  244. let keypair = ElGamalKeypair::new_rand();
  245. let message: u64 = 55;
  246. let ciphertext = keypair.pubkey().encrypt(message);
  247. let (commitment, opening) = Pedersen::new(message);
  248. let mut prover_transcript = Transcript::new(b"Test");
  249. let mut verifier_transcript = Transcript::new(b"Test");
  250. let proof = CiphertextCommitmentEqualityProof::new(
  251. &keypair,
  252. &ciphertext,
  253. &opening,
  254. message,
  255. &mut prover_transcript,
  256. );
  257. proof
  258. .verify(
  259. keypair.pubkey(),
  260. &ciphertext,
  261. &commitment,
  262. &mut verifier_transcript,
  263. )
  264. .unwrap();
  265. // fail case: encrypted and committed messages are different
  266. let keypair = ElGamalKeypair::new_rand();
  267. let encrypted_message: u64 = 55;
  268. let committed_message: u64 = 77;
  269. let ciphertext = keypair.pubkey().encrypt(encrypted_message);
  270. let (commitment, opening) = Pedersen::new(committed_message);
  271. let mut prover_transcript = Transcript::new(b"Test");
  272. let mut verifier_transcript = Transcript::new(b"Test");
  273. let proof = CiphertextCommitmentEqualityProof::new(
  274. &keypair,
  275. &ciphertext,
  276. &opening,
  277. encrypted_message,
  278. &mut prover_transcript,
  279. );
  280. assert!(proof
  281. .verify(
  282. keypair.pubkey(),
  283. &ciphertext,
  284. &commitment,
  285. &mut verifier_transcript
  286. )
  287. .is_err());
  288. }
  289. #[test]
  290. fn test_ciphertext_commitment_equality_proof_edge_cases() {
  291. // if ElGamal public key zero (public key is invalid), then the proof should always reject
  292. let public = ElGamalPubkey::try_from([0u8; 32].as_slice()).unwrap();
  293. let secret = ElGamalSecretKey::new_rand();
  294. let elgamal_keypair = ElGamalKeypair::new_for_tests(public, secret);
  295. let message: u64 = 55;
  296. let ciphertext = elgamal_keypair.pubkey().encrypt(message);
  297. let (commitment, opening) = Pedersen::new(message);
  298. let mut prover_transcript = Transcript::new(b"Test");
  299. let mut verifier_transcript = Transcript::new(b"Test");
  300. let proof = CiphertextCommitmentEqualityProof::new(
  301. &elgamal_keypair,
  302. &ciphertext,
  303. &opening,
  304. message,
  305. &mut prover_transcript,
  306. );
  307. assert!(proof
  308. .verify(
  309. elgamal_keypair.pubkey(),
  310. &ciphertext,
  311. &commitment,
  312. &mut verifier_transcript
  313. )
  314. .is_err());
  315. // if ciphertext is all-zero (valid commitment of 0) and commitment is also all-zero, then
  316. // the proof should still accept
  317. let elgamal_keypair = ElGamalKeypair::new_rand();
  318. let message: u64 = 0;
  319. let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
  320. let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
  321. let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
  322. let mut prover_transcript = Transcript::new(b"Test");
  323. let mut verifier_transcript = Transcript::new(b"Test");
  324. let proof = CiphertextCommitmentEqualityProof::new(
  325. &elgamal_keypair,
  326. &ciphertext,
  327. &opening,
  328. message,
  329. &mut prover_transcript,
  330. );
  331. proof
  332. .verify(
  333. elgamal_keypair.pubkey(),
  334. &ciphertext,
  335. &commitment,
  336. &mut verifier_transcript,
  337. )
  338. .unwrap();
  339. // if commitment is all-zero and the ciphertext is a correct encryption of 0, then the
  340. // proof should still accept
  341. let elgamal_keypair = ElGamalKeypair::new_rand();
  342. let message: u64 = 0;
  343. let ciphertext = elgamal_keypair.pubkey().encrypt(message);
  344. let commitment = PedersenCommitment::from_bytes(&[0u8; 32]).unwrap();
  345. let opening = PedersenOpening::from_bytes(&[0u8; 32]).unwrap();
  346. let mut prover_transcript = Transcript::new(b"Test");
  347. let mut verifier_transcript = Transcript::new(b"Test");
  348. let proof = CiphertextCommitmentEqualityProof::new(
  349. &elgamal_keypair,
  350. &ciphertext,
  351. &opening,
  352. message,
  353. &mut prover_transcript,
  354. );
  355. proof
  356. .verify(
  357. elgamal_keypair.pubkey(),
  358. &ciphertext,
  359. &commitment,
  360. &mut verifier_transcript,
  361. )
  362. .unwrap();
  363. // if ciphertext is all zero and commitment correctly encodes 0, then the proof should
  364. // still accept
  365. let elgamal_keypair = ElGamalKeypair::new_rand();
  366. let message: u64 = 0;
  367. let ciphertext = ElGamalCiphertext::from_bytes(&[0u8; 64]).unwrap();
  368. let (commitment, opening) = Pedersen::new(message);
  369. let mut prover_transcript = Transcript::new(b"Test");
  370. let mut verifier_transcript = Transcript::new(b"Test");
  371. let proof = CiphertextCommitmentEqualityProof::new(
  372. &elgamal_keypair,
  373. &ciphertext,
  374. &opening,
  375. message,
  376. &mut prover_transcript,
  377. );
  378. proof
  379. .verify(
  380. elgamal_keypair.pubkey(),
  381. &ciphertext,
  382. &commitment,
  383. &mut verifier_transcript,
  384. )
  385. .unwrap();
  386. }
  387. #[test]
  388. fn test_ciphertext_commitment_equality_proof_string() {
  389. let pubkey_str = "JNa7rRrDm35laU7f8HPds1PmHoZEPSHFK/M+aTtEhAk=";
  390. let pod_pubkey = PodElGamalPubkey::from_str(pubkey_str).unwrap();
  391. let pubkey: ElGamalPubkey = pod_pubkey.try_into().unwrap();
  392. let ciphertext_str = "RAXnbQ/DPRlYAWmD+iHRNqMDv7oQcPgQ7OejRzj4bxVy2qOJNziqqDOC7VP3iTW1+z/jckW4smA3EUF7i/r8Rw==";
  393. let pod_ciphertext = PodElGamalCiphertext::from_str(ciphertext_str).unwrap();
  394. let ciphertext: ElGamalCiphertext = pod_ciphertext.try_into().unwrap();
  395. let commitment_str = "ngPTYvbY9P5l6aOfr7bLQiI+0HZsw8GBgiumdW3tNzw=";
  396. let pod_commitment = PodPedersenCommitment::from_str(commitment_str).unwrap();
  397. let commitment: PedersenCommitment = pod_commitment.try_into().unwrap();
  398. let proof_str = "cCZySLxB2XJdGyDvckVBm2OWiXqf7Jf54IFoDuLJ4G+ySj+lh5DbaDMHDhuozQC9tDWtk2mFITuaXOc5Zw3nZ2oEvVYpqv5hN+k5dx9k8/nZKabUCkZwx310z7x4fE4Np5SY9PYia1hkrq9AWq0b3v97XvW1+XCSSxuflvBk5wsdaQQ+ZgcmPnKWKjHfRwmU2k5iVgYzs2VmvZa5E3OWBoM/M2yFNvukY+FCC2YMnspO0c4lNBr/vDFQuHdW0OgJ";
  399. let pod_proof = PodCiphertextCommitmentEqualityProof::from_str(proof_str).unwrap();
  400. let proof: CiphertextCommitmentEqualityProof = pod_proof.try_into().unwrap();
  401. let mut verifier_transcript = Transcript::new(b"Test");
  402. proof
  403. .verify(&pubkey, &ciphertext, &commitment, &mut verifier_transcript)
  404. .unwrap();
  405. }
  406. }