1#[cfg(all(feature = "alloc", not(feature = "std")))]
2use alloc::vec::Vec;
3
4#[cfg(feature = "std")]
5use std::vec::Vec;
6
7use core::iter;
8
9use curve25519_dalek::constants;
10use curve25519_dalek::ristretto::RistrettoPoint;
11use curve25519_dalek::scalar::Scalar;
12use curve25519_dalek::traits::VartimeMultiscalarMul;
13use digest::generic_array::typenum::U64;
14use digest::Digest;
15use rand::{CryptoRng, Rng, SeedableRng};
16use rand_chacha::ChaChaRng;
17
18use crate::errors::{InternalError, TokenError};
19use crate::oprf::*;
20
21pub const DLEQ_PROOF_LENGTH: usize = 64;
23
24#[allow(non_snake_case)]
26#[derive(Debug, Clone)]
27pub struct DLEQProof {
28 pub(crate) c: Scalar,
31 pub(crate) s: Scalar,
34}
35
36#[cfg(any(test, feature = "base64"))]
37impl_base64!(DLEQProof);
38
39#[cfg(feature = "serde")]
40impl_serde!(DLEQProof);
41
42#[allow(non_snake_case)]
43impl DLEQProof {
44 fn _new<D, T>(rng: &mut T, P: RistrettoPoint, Q: RistrettoPoint, k: &SigningKey) -> Self
46 where
47 D: Digest<OutputSize = U64> + Default,
48 T: Rng + CryptoRng,
49 {
50 let t = Scalar::random(rng);
51
52 let A = t * constants::RISTRETTO_BASEPOINT_POINT;
53 let B = t * P;
54
55 let mut h = D::default();
56
57 let X = constants::RISTRETTO_BASEPOINT_COMPRESSED;
58 let Y = k.public_key.0;
59 let P = P.compress();
60 let Q = Q.compress();
61 let A = A.compress();
62 let B = B.compress();
63
64 h.update(X.as_bytes());
65 h.update(Y.as_bytes());
66 h.update(P.as_bytes());
67 h.update(Q.as_bytes());
68 h.update(A.as_bytes());
69 h.update(B.as_bytes());
70
71 let c = Scalar::from_hash(h);
72
73 let s = t - c * k.k;
74
75 DLEQProof { c, s }
76 }
77
78 pub fn new<D, T>(
80 rng: &mut T,
81 blinded_token: &BlindedToken,
82 signed_token: &SignedToken,
83 k: &SigningKey,
84 ) -> Result<Self, TokenError>
85 where
86 D: Digest<OutputSize = U64> + Default,
87 T: Rng + CryptoRng,
88 {
89 Ok(Self::_new::<D, T>(
90 rng,
91 blinded_token
92 .0
93 .decompress()
94 .ok_or(TokenError(InternalError::PointDecompressionError))?,
95 signed_token
96 .0
97 .decompress()
98 .ok_or(TokenError(InternalError::PointDecompressionError))?,
99 k,
100 ))
101 }
102
103 fn _verify<D>(
105 &self,
106 P: RistrettoPoint,
107 Q: RistrettoPoint,
108 public_key: &PublicKey,
109 ) -> Result<(), TokenError>
110 where
111 D: Digest<OutputSize = U64> + Default,
112 {
113 let X = constants::RISTRETTO_BASEPOINT_COMPRESSED;
114 let Y = public_key.0;
115
116 let A = (self.s * constants::RISTRETTO_BASEPOINT_POINT)
117 + (self.c
118 * Y.decompress()
119 .ok_or(TokenError(InternalError::PointDecompressionError))?);
120 let B = (self.s * P) + (self.c * Q);
121
122 let A = A.compress();
123 let B = B.compress();
124 let P = P.compress();
125 let Q = Q.compress();
126
127 let mut h = D::default();
128
129 h.update(X.as_bytes());
130 h.update(Y.as_bytes());
131 h.update(P.as_bytes());
132 h.update(Q.as_bytes());
133 h.update(A.as_bytes());
134 h.update(B.as_bytes());
135
136 let c = Scalar::from_hash(h);
137
138 if c == self.c {
139 Ok(())
140 } else {
141 Err(TokenError(InternalError::VerifyError))
142 }
143 }
144
145 pub fn verify<D>(
147 &self,
148 blinded_token: &BlindedToken,
149 signed_token: &SignedToken,
150 public_key: &PublicKey,
151 ) -> Result<(), TokenError>
152 where
153 D: Digest<OutputSize = U64> + Default,
154 {
155 self._verify::<D>(
156 blinded_token
157 .0
158 .decompress()
159 .ok_or(TokenError(InternalError::PointDecompressionError))?,
160 signed_token
161 .0
162 .decompress()
163 .ok_or(TokenError(InternalError::PointDecompressionError))?,
164 public_key,
165 )
166 }
167}
168
169impl DLEQProof {
170 pub fn to_bytes(&self) -> [u8; DLEQ_PROOF_LENGTH] {
172 let mut proof_bytes: [u8; DLEQ_PROOF_LENGTH] = [0u8; DLEQ_PROOF_LENGTH];
173
174 proof_bytes[..32].copy_from_slice(&self.c.to_bytes());
175 proof_bytes[32..].copy_from_slice(&self.s.to_bytes());
176 proof_bytes
177 }
178
179 fn bytes_length_error() -> TokenError {
180 TokenError(InternalError::BytesLengthError {
181 name: "DLEQProof",
182 length: DLEQ_PROOF_LENGTH,
183 })
184 }
185
186 pub fn from_bytes(bytes: &[u8]) -> Result<DLEQProof, TokenError> {
188 if bytes.len() != DLEQ_PROOF_LENGTH {
189 return Err(DLEQProof::bytes_length_error());
190 }
191
192 let mut c_bits: [u8; 32] = [0u8; 32];
193 let mut s_bits: [u8; 32] = [0u8; 32];
194
195 c_bits.copy_from_slice(&bytes[..32]);
196 s_bits.copy_from_slice(&bytes[32..]);
197
198 let c = Option::from(Scalar::from_canonical_bytes(c_bits))
199 .ok_or(TokenError(InternalError::ScalarFormatError))?;
200 let s = Option::from(Scalar::from_canonical_bytes(s_bits))
201 .ok_or(TokenError(InternalError::ScalarFormatError))?;
202
203 Ok(DLEQProof { c, s })
204 }
205}
206
207#[allow(non_snake_case)]
210#[derive(Debug, Clone)]
211pub struct BatchDLEQProof(DLEQProof);
212
213#[cfg(any(test, feature = "base64"))]
214impl_base64!(BatchDLEQProof);
215
216#[cfg(feature = "serde")]
217impl_serde!(BatchDLEQProof);
218
219#[allow(non_snake_case)]
220impl BatchDLEQProof {
221 fn calculate_composites<D>(
222 blinded_tokens: &[BlindedToken],
223 signed_tokens: &[SignedToken],
224 public_key: &PublicKey,
225 ) -> Result<(RistrettoPoint, RistrettoPoint), TokenError>
226 where
227 D: Digest<OutputSize = U64> + Default,
228 {
229 if blinded_tokens.len() != signed_tokens.len() {
230 return Err(TokenError(InternalError::LengthMismatchError));
231 }
232
233 let mut h = D::default();
234
235 h.update(constants::RISTRETTO_BASEPOINT_COMPRESSED.as_bytes());
236 h.update(public_key.0.as_bytes());
237
238 for (Pi, Qi) in blinded_tokens.iter().zip(signed_tokens.iter()) {
239 h.update(Pi.0.as_bytes());
240 h.update(Qi.0.as_bytes());
241 }
242
243 let result = h.finalize();
244
245 let mut seed: [u8; 32] = [0u8; 32];
246 seed.copy_from_slice(&result[..32]);
247
248 let mut prng: ChaChaRng = SeedableRng::from_seed(seed);
249 let c_m: Vec<Scalar> = iter::repeat_with(|| Scalar::random(&mut prng))
250 .take(blinded_tokens.len())
251 .collect();
252
253 let M = RistrettoPoint::optional_multiscalar_mul(
254 &c_m,
255 blinded_tokens.iter().map(|Pi| Pi.0.decompress()),
256 )
257 .ok_or(TokenError(InternalError::PointDecompressionError))?;
258
259 let Z = RistrettoPoint::optional_multiscalar_mul(
260 &c_m,
261 signed_tokens.iter().map(|Qi| Qi.0.decompress()),
262 )
263 .ok_or(TokenError(InternalError::PointDecompressionError))?;
264
265 Ok((M, Z))
266 }
267
268 pub fn new<D, T>(
270 rng: &mut T,
271 blinded_tokens: &[BlindedToken],
272 signed_tokens: &[SignedToken],
273 signing_key: &SigningKey,
274 ) -> Result<Self, TokenError>
275 where
276 D: Digest<OutputSize = U64> + Default,
277 T: Rng + CryptoRng,
278 {
279 let (M, Z) = BatchDLEQProof::calculate_composites::<D>(
280 blinded_tokens,
281 signed_tokens,
282 &signing_key.public_key,
283 )?;
284 Ok(BatchDLEQProof(DLEQProof::_new::<D, T>(
285 rng,
286 M,
287 Z,
288 signing_key,
289 )))
290 }
291
292 pub fn verify<D>(
294 &self,
295 blinded_tokens: &[BlindedToken],
296 signed_tokens: &[SignedToken],
297 public_key: &PublicKey,
298 ) -> Result<(), TokenError>
299 where
300 D: Digest<OutputSize = U64> + Default,
301 {
302 let (M, Z) =
303 BatchDLEQProof::calculate_composites::<D>(blinded_tokens, signed_tokens, public_key)?;
304
305 self.0._verify::<D>(M, Z, public_key)
306 }
307
308 pub fn verify_and_unblind<'a, D, I>(
310 &self,
311 tokens: I,
312 blinded_tokens: &[BlindedToken],
313 signed_tokens: &[SignedToken],
314 public_key: &PublicKey,
315 ) -> Result<Vec<UnblindedToken>, TokenError>
316 where
317 D: Digest<OutputSize = U64> + Default,
318 I: IntoIterator<Item = &'a Token>,
319 {
320 self.verify::<D>(blinded_tokens, signed_tokens, public_key)?;
321
322 let unblinded_tokens: Result<Vec<UnblindedToken>, TokenError> = tokens
323 .into_iter()
324 .zip(signed_tokens.iter())
325 .map(|(token, signed_token)| token.unblind(signed_token))
326 .collect();
327 unblinded_tokens.and_then(|unblinded_tokens| {
328 if unblinded_tokens.len() != signed_tokens.len() {
329 return Err(TokenError(InternalError::LengthMismatchError));
330 }
331 Ok(unblinded_tokens)
332 })
333 }
334}
335
336impl BatchDLEQProof {
337 pub fn to_bytes(&self) -> [u8; DLEQ_PROOF_LENGTH] {
339 self.0.to_bytes()
340 }
341
342 #[cfg(feature = "serde")]
343 fn bytes_length_error() -> TokenError {
344 DLEQProof::bytes_length_error()
345 }
346
347 pub fn from_bytes(bytes: &[u8]) -> Result<BatchDLEQProof, TokenError> {
349 DLEQProof::from_bytes(bytes).map(BatchDLEQProof)
350 }
351}
352
353#[cfg(test)]
354mod tests {
355 use base64::{engine::Engine as _, prelude::BASE64_STANDARD};
356 use curve25519_dalek::ristretto::CompressedRistretto;
357 use rand::rngs::OsRng;
358 use sha2::Sha512;
359
360 use super::*;
361 use crate::oprf::Token;
362
363 #[test]
364 #[allow(non_snake_case)]
365 fn works() {
366 let mut rng = OsRng;
367
368 let key1 = SigningKey::random(&mut rng);
369 let key2 = SigningKey::random(&mut rng);
370
371 let P = RistrettoPoint::random(&mut rng);
372 let Q = key1.k * P;
373
374 let proof = DLEQProof::_new::<Sha512, _>(&mut rng, P, Q, &key1);
375
376 assert!(proof._verify::<Sha512>(P, Q, &key1.public_key).is_ok());
377
378 let P = RistrettoPoint::random(&mut rng);
379 let Q = key2.k * P;
380
381 let proof = DLEQProof::_new::<Sha512, _>(&mut rng, P, Q, &key1);
382
383 assert!(proof._verify::<Sha512>(P, Q, &key1.public_key).is_err());
384 }
385
386 #[allow(non_snake_case)]
387 #[test]
388 fn vector_tests() {
389 let vectors = [
391("SlPD+7xZlw7l+Fr4E4dd/8E6kEouU65+ZfoN6m5iyQE=", "nGajOcg0T5IvwyBstdroFKWUwBd90yNcJU2cQJpluAg=", "COY3iHwUTGQZ9q2bycTgMwgHcLZc+VqICT9kNoeNnTc=", "VLYFn6ZhrZpSHcRS2ffzbtYod1r6gWRc54hVFzXW+1s=", "4hl2AKMk2ueO7f67sHGol531jPu4DfDtDPrkFS10DgVj6RSbG13zWMt+KjpwVtAUdEMkJuYv8b0NPaBZrMlaAg=="),
392("rBKvQjzCywrH+WAHjvVpB4P59cy1A0CCcYjeUioWdA0=", "fEgJRiE5cPsSkdQR+gM+EeGbAja5GYenhQPRuirUGHM=", "Hnvv9L/So4IvS2FPpzvRJL3pZ9ow01IHri3qefZ673Y=", "mszTCP66IXeEPOV7fvVR7bLj3NwxweLxH8wq3XQCTDc=", "u9DkAnPlj21cKR0CHhuGFbi419mRl0fnB5/s6VhDVABwLm1C7zLpiCwRzFHyEwkIBam/2Nt+/ZsuZL9B5cZpDQ=="),
393("vu+HsyYOp5bteH7HKTl22hV/pgqGg1Xo3LIDW56ASQY=", "0EnIefouYrEhIxV06aSvEb0wlADk9pPt2IRJAPkBPAA=", "dpF+MlDg5pNOH0C53DhdWAnsqLz6haKZx/6M+wfzsz4=", "dgvVTGJZTUiGQgRZqcKq26a0ERoNGPNtMJ2Hromjox4=", "7NoWAv8wvYJoNZtPmRK1L+xpbR2NDOpbnh/8ccGiOABfxGf6f/BmQjVDZQiGbYXnyQjzM5NfcjiAP/shOK5lCg=="),
394("tviSLm/W8oFds67y9lMs990fjh08hQNV17/4V2bmOQY=", "5ufRlCvVKvXp1yuxxS7Jvw9LSwQUl6Q/MlT6HY2l1Hc=", "zOVEbK4KQ1GBW97YUVNguoN+NntwtGi1t+EeioMusXY=", "lH2gNbwqSC1nYYxT3I7fNQagTsD4OvSbzwrSCpanQkQ=", "NJF9U3TWiCWMd6Qh/vA90F/2N6udsXbTvifNxf0rzgbhInoEvYDi5jZAZUQEi7x7mmP8iFq7+ukoOroy6/8jCw=="),
395("Ge3prZ2jJSoh1A3ZvrSfaSA1kDziGW2I+Gmh6jniaAs=", "2nNCd5YN9H5EYlOL9/kmLYNBMkaLwnG3wjyd7jw2QAY=", "YHdAzlpSTAMy3mB+F4mPwlyVl+V9Yt4f3cDPNJpWdns=", "gEnqgXg3FDaCQFayTXrIfpbZ2n0P6FD/95LuMsdIfFk=", "Fj2/YunbQs5XxSyLxl/fC4dAfRlErGurTtHHSfGKyQTzrLZrO7VghmGFQaMAXZ+jg+6v99YL6FWj1Y/5WFt2Aw=="),
396 ];
397 for (k, Y, P, Q_b64, dleq_b64) in vectors {
398 let server_key = SigningKey::decode_base64(k).unwrap();
399
400 assert_eq!(server_key.public_key.encode_base64(), Y);
401
402 let P_bytes = BASE64_STANDARD.decode(P).unwrap();
403 let mut P_bits: [u8; 32] = [0u8; 32];
404 P_bits.copy_from_slice(&P_bytes[..32]);
405 let P = CompressedRistretto(P_bits).decompress().unwrap();
406
407 let Q = P * server_key.k;
408
409 assert_eq!(BASE64_STANDARD.encode(&Q.compress().to_bytes()[..]), Q_b64);
410
411 let seed: [u8; 32] = [0u8; 32];
412 let mut prng: ChaChaRng = SeedableRng::from_seed(seed);
413
414 let dleq = DLEQProof::_new::<Sha512, _>(&mut prng, P, Q, &server_key);
415 assert_eq!(dleq.encode_base64(), dleq_b64);
416
417 assert!(dleq._verify::<Sha512>(P, Q, &server_key.public_key).is_ok());
418 }
419 }
420
421 #[allow(non_snake_case)]
422 #[test]
423 fn batch_vector_tests() {
424 let vectors = [
426("8n06lJH/lkGHsPVPehyAwYA4BX97JbFmDmJzPO1oIQE=", "4KbMr7/9/ZxNG2Baixy/3MFFOCzFFfJLjsUAoetKOjo=", "xGUA5fGnGgvg1zzC3mWeq7K2jqVBfJwJ2uJ6upqPxgA=,LHTaaZi7YqIsuqJ1P+HV8vYIwHFKePF+xton737J3SE=,8MFJE9IYULJT+LsiI3h+kDLc4rmd809utaDo1xic3GQ=,HOMibeqOkyJsN+N5eRJrQkAT+G32L6eOhU+1/Drsbks=,PGLb/Ael7MnFYaS6YjCR/lRIkKONoAsEKuJ5SRvJMwg=", "fgOAdXLxUs/pfT3ustR9mBbUyIbcx1Nis0zWuIz8TS8=,Lh0ekCvZ0cWhDTTxUQV3frSb2+hW9mDWunYWGv/foTA=,EvA36hZe6DD9WOU/F7rnJ8kgXyUgDZ908Li4F1IqAko=,gtbw9qu9qpp1nYEejzzhsWkMyOM8kbvOOMBTdArj82E=,ypADHr5ETijWYAQTw9f6J7OSIk24S1JwBVrgrIsAAE8=", "jhS4wBez3EdGoGEW4ENMW0UpZHn1aiJa9WUWp5RJHyk=", "xJfJMAnGI0ZrEuJcuBaa7cZ84HCe62f9eWIQE2JeiUU=", "X9bPqHlbzd+PzBCTMoZMdF8joNsbZkODlPABwhgD7w3XLP6J8BQP80bfgagB3pXWwa0Uhd1NquBhOL5uCNy9DA=="),
427("iE4eMIMtnV1rgsSMpuE9rfT486QnmuyNAsDB8JSwkgI=", "MIo6R2WZkAylW7XeUOwcYClUOpXj9wZ14Tr6wYY7FDs=", "flw+gZoFAcOlEPmGqr3LliVkshxK5XfBNlow5X+clSs=,roiJVY4XvfsBh6iGSzz9OM/40Nx4VrG9SriyifVOc0A=,UMnZMVFyE9keC0PSx2KSZ2Y8DEM8tT6/XGQAV6AXGBg=,dPcHlc7Q3X/9F9uKd4T2bCk9uOANGBoh72N4tyn/6Xg=,pHS/yXXFw+wYP8FOAqKFCyzuI8552dyKoaEVcA9PXnw=", "sL8mkkfISTi+0cvfZLaB+bb0zbp9vSo3WEt8Q/t5VR0=,quVLKurPfGk/waJnPXniatNV0byGYL9R04PI0Bap5H0=,/naRAvDSk/6QwHC5dFuGKqCwB6Jy+r30EqL2+5y06XI=,Ms7MKju+/YdXOaMjYRouki5VhPA3O6Ng8Hw4DDRdAxU=,6A2sYddm/4xRSJkTpUDrPUuK1Cy0OCUpUthlTgjEQWM=", "an0BjUmpwb/SON3OS/Unoj4+AJ0I7+27WS83LFKtKk4=", "rIv5UFDK/jGS2jy/tHV4vKMXdr8bDMjJt6JFOksb7gQ=", "LR8s0d+woWFwCJpdDek4cWt6d0lbiGoEZMmxa+rILggX0zKz5/T2QQr4qQJAXSWnkySjIwVGtydn+sha8nJJBw=="),
428("CDATdJXtmEMd+a8MiNyBzJfml/tlFeUt+7fqfSdvGgI=", "oCjGp1LXIuALA14oUuDzMvXxZQqNmA9zQvpW7gj8gQc=", "uqzF6Yud/TcOb0Qqh6W5pVXDVTfccpd6vDFKl6cko2s=,1JHJSOeBP4YZJU8vxBQJky++2CCnSXR7AiVYhWIcxhc=,xlt2BWmGn2HHNQRMouv/4CZTjbA5OzAW/OUHy/xMRwc=,jneVhZ2uILeea08oFNHrsMrySZ5NpGZ2rIT8pM2IPxM=,KFtx8ceLvtuL2+dRPm7bDZ5FdrL0f/TRNNkIzMtttl4=", "mF7kNktgoP9OHSA8mgm6t+Vmx9bhO79TG0AjYNjbz24=,7IbBiJX/cH4//616F8UCgo2Lzh3RATwRlrtLl2GmjlM=,YBl7hRaHTwQ19HwtbrVcRTBQ3xhpDrb6EjzS80LO2wQ=,Rs6W8kUZCBam3BZKyvC/Zc7S3bVIWEBXzNVwKsSk/Us=,ylT/xqEBYjWDWfZd0lH9w08/c55SsA9KSgAOtNz48HE=", "AjQpqYvy6onAHHyD8ZtsVGYW/Y84bwvusSy3MEYyQng=", "wESOv7FzkbDc3NJraYWj58aeFRXbt0VAEb1PgLsJ73g=", "hDDZcM6e+X9UV8ZtV9QwQ7w4PeHb+A1ug7WfsKiAXQEHk7P4LIrXSH2AvI09NGUG642xKweGXmkvELD3Q7yxAQ=="),
429("siv+BM3AvP8Jv1aL4MFhMs9Xa6jxUNhFXpTWDfGrZQQ=", "XgFOlHEz5zm5dtx6ptYIXNg1NsJ/3vAq+cf/9eBkbxI=", "dsaMl4/9FcOFtaW3l65y1Z9ETJR36aTcXPMp+w4HGUY=,aH2q1HiReMA/Ney2NNZCgl+5GKK9xrxVwdC+THq9pFY=,uGRqS51VD7DuK0gSpMb3owRld57W6DqOyZpygXJVpmI=,Bvv+lqtCg39SD1H218rPZdQTmYPe2HD3QScntqw1oFA=,9IHWUyv/SCwZ4WKEGi58+bQ5nHsaDBXCku2vOzGvgUY=", "4lPV/OyNjVy4VTUvaROxCuq4ryfegkt7jt5IhrX9THo=,4EAmV5Mv3a/IQFsfVlaFxErNc96Ns980FT4yLlCdoxA=,bgGM37uBMLdRRAd1cu/4Iq+FzFwzRFLVhqp2uGFnPQc=,rCn4OuWnV5tTsgcPJAYRSqfONZf9k/92fwzWHtUxxh8=,lHCDzazDlU0w735u7OQmJM96WGeaNFILanawmC9EwFE=", "jotItTWLW/kpDeh8KJQtNqM7ON0YibEJ7R8VnMHP7Cs=", "Pu49xb3Ixn+Dfg6s+wgjyoPy7ickB5lM7/MxQVdpaUI=", "N6vDmGbYZ0aa9S2JqWSYppiX1AV33QDXGc8FHaF0aQ9y6Hp68UEkI2x5AJQ3URqS+5/x1AuucMH0AOMcLNqODg=="),
430("jTTf/D0gicaG++cQJ1X4qYaOqk4YPo0p6Mo2B95kJAg=", "LCtewONuYTXljy+oK73/m7CON/vr/e1r4aDaVE3xDnc=", "GsomH3aBo6qBHaNGzZZ/pNOviBTbZrUfpthgYU5jAmU=,wHvznjZDA9L8dgQGEj7wf1/QGxunE5/WYdxUpQX4Umc=,ZMxkDV7epUgmpix38jBfWv42VeMQefypY56dnysikWA=,aEy+/J0AFmjYGTjcv5y942fleEk/0rwqlD+kXSn0vCM=,QJMHfIYUraWdJKzenROtgLyjU9MrDtDDwFfDNIcjCHA=", "aGUXY5bBYoDmw4x1muwHzKp1w2sITQMeDfsyxv9EUDc=,qh0wfC/wAdclIJ79R+IIpPJLJM5aBe/i5i54dQOn3Vk=,zCi7XaHyO/b9SfN2AYuJcC60zqnIorkXMjjbixhEKxk=,XlBf7DyH5FtcUyr9Gfnj8i3cnKKPtWGHXm/LpIQX4gM=,wHtZg2i40wdxQvHehZEunTHiODSuEMv8suwFaqynmTQ=", "osUDqpps33Jw0k7vEHFCAk+iywlE7YrXrX5RfqC0olA=", "LnD9wlZrDo3v6dw56owm6NazoBLKwtqMPWdtxSRVWxc=", "KVEotBgIaz5Rymqpy4paroHGQyD/80FdvLCrONxDzQUWgNZxZ6aiCJ2VxIGP+6+86FZXS1sXGgs3dwft/VMCCw=="),
431 ];
432 for (k, Y, P, Q, M_b64, Z_b64, dleq_b64) in vectors {
433 let server_key = SigningKey::decode_base64(k).unwrap();
434
435 assert_eq!(server_key.public_key.encode_base64(), Y);
436
437 let P: Vec<&str> = P.split(',').collect();
438 let Q: Vec<&str> = Q.split(',').collect();
439
440 let P: Vec<BlindedToken> = P
441 .iter()
442 .map(|P_i| BlindedToken::decode_base64(P_i).unwrap())
443 .collect();
444
445 let Q: Vec<SignedToken> = P
446 .iter()
447 .zip(Q.into_iter())
448 .map(|(P_i, Q_i_b64)| {
449 let Q_i = server_key.sign(P_i).unwrap();
450 assert_eq!(Q_i.encode_base64(), Q_i_b64);
451 Q_i
452 })
453 .collect();
454
455 let (M, Z) =
456 BatchDLEQProof::calculate_composites::<Sha512>(&P, &Q, &server_key.public_key)
457 .unwrap();
458
459 assert_eq!(BASE64_STANDARD.encode(&M.compress().to_bytes()[..]), M_b64);
460 assert_eq!(BASE64_STANDARD.encode(&Z.compress().to_bytes()[..]), Z_b64);
461
462 let seed: [u8; 32] = [0u8; 32];
463 let mut prng: ChaChaRng = SeedableRng::from_seed(seed);
464
465 let batch_proof =
466 BatchDLEQProof::new::<Sha512, _>(&mut prng, &P, &Q, &server_key).unwrap();
467 assert_eq!(batch_proof.encode_base64(), dleq_b64);
468
469 assert!(batch_proof
470 .verify::<Sha512>(&P, &Q, &server_key.public_key)
471 .is_ok());
472 }
473 }
474
475 #[test]
476 #[allow(non_snake_case)]
477 fn batch_works() {
478 use std::vec::Vec;
479
480 let mut rng = OsRng;
481
482 let key = SigningKey::random(&mut rng);
483
484 let blinded_tokens = vec![Token::random::<Sha512, _>(&mut rng).blind()];
485 let signed_tokens: Vec<SignedToken> = blinded_tokens
486 .iter()
487 .filter_map(|t| key.sign(t).ok())
488 .collect();
489
490 let batch_proof =
491 BatchDLEQProof::new::<Sha512, _>(&mut rng, &blinded_tokens, &signed_tokens, &key)
492 .unwrap();
493
494 assert!(batch_proof
495 .verify::<Sha512>(&blinded_tokens, &signed_tokens, &key.public_key)
496 .is_ok());
497 }
498}