Draft: draft-pda-protocol

Commentary: I strongly recommend against using a biased PRG ( even though the bias is small ) . There are two easy way to solve this : Rejection sampling : Accept the -byte integer if it is strictly smaller than the prime . To increase the probability of accepting a number , you may want to zero the most significant bits ( e.g. , if the prime is in ( 2 ^ 125 , 2 ^ 126 ) , zero the two most significant bits of your 16 - byte randomness , then do rejection sampling ) . This is the approach currently followed in and . The distribution is exactly the uniform distribution modulo the prime . Flooding / Extra Bits : Reduce a -byte integer modulo the prime , which gives a distribution close to the uniform distribution modulo the prime when l ' is large enough . In practice however , we may want to set , which would be quite wasteful . Both approaches are recommended for example when generating keys for EC cryptography , see Sections B.4.1 and B.4.2 of . 
 My vote is for generating more bits than are necessary , where the amount of extra bits is a function of the system security parameter . The draft captures this quite nicely , I think . ( Okay , I 'm biased . ) Somewhat of a bikeshed , but : is generating 8 extra bytes of randomness really a big deal ? 
 Good question . I actually do n't think it 's a bikeshed , because the main difference with hash - to - curve and EC key generation is that , in Prio , there are # bins random values to generate , per user , and the extra bits needs to be generated for every random value . For example , if p is a 32 - bit prime , means that you are generating 12 bytes to obtain one 32 - bit random integer via a modular reduction of a 96 - bit integer . That 's 3 times the amount of bytes used to represent the result and the code needs to perform modular arithmetic with numbers that do not fit in a ( but fits in a ) . With rejection sampling instead , in expectation you generate less than 24 = 8 bytes , and you only need to perform comparisons over , albeit the execution time may be varying . 
 Yeah , I get that it 's excessively wasteful ( relatively speaking ) . What it not clear to me is if it 's actually wasteful in absolute terms . Either way , we can certainly be precise in the minimum number of extra bits needed . We should just do that ! 
 I wrote a small to compare the performance of the two PRGs approaches above for different vector sizes and the cost of an elliptic curve Diffie - Hellman . You can run it as follows Disclaimer : It may not be the most optimized implementations I think it is valuable to keep this issue open and experiment more comprehensively before choosing the definitive approach , because : the PRG is likely to become the bottleneck compared to the ECDH , henceforth it will be important to optimize it ; rejection sampling is likely giving a 2 - 5x speedup compared to drawing extra bits . On my macbook air , I obtain the following timings Run on ( 4 X 1700 MHz CPU s ) CPU Caches : L1 Data 32 KiB ( x2 ) L1 Instruction 32 KiB ( x2 ) L2 Unified 256 KiB ( x2 ) L3 Unified 4096 KiB ( x1 ) Load Average : 6.86 , 6.66 , 5.96 
 Benchmark Time CPU Iterations 
 BMExtra32Bits4293918721/8 438 ns 432 ns BMExtra32Bits4293918721/64 895 ns 879 ns BMExtra32Bits4293918721/512 3236 ns 3206 ns BMExtra32Bits4293918721/4096 ns ns BMExtra32Bits4293918721/ ns ns 3485 BMExtra96Bits4293918721/8 556 ns 554 ns BMExtra96Bits4293918721/64 916 ns 901 ns BMExtra96Bits4293918721/512 4930 ns 4899 ns  ns ns BMExtra96Bits4293918721/ ns ns 2073 BMExtra64Bits15564440312192434177/8 611 ns 574 ns BMExtra64Bits15564440312192434177/64 916 ns 908 ns BMExtra64Bits15564440312192434177/512 5047 ns 4998 ns BMExtra64Bits15564440312192434177/4096 ns ns BMExtra64Bits15564440312192434177/ ns ns 2062 BMRejectionSampling4293918721/8 389 ns 387 ns BMRejectionSampling4293918721/64 577 ns 574 ns BMRejectionSampling4293918721/512 1417 ns 1400 ns BMRejectionSampling4293918721/4096 8928 ns 8879 ns BMRejectionSampling4293918721/ ns ns 8604 BMRejectionSampling15564440312192434177/8 497 ns 491 ns BMRejectionSampling15564440312192434177/64 806 ns 800 ns BMRejectionSampling15564440312192434177/512 2780 ns 2750 ns BMRejectionSampling15564440312192434177/4096 ns ns BMRejectionSampling15564440312192434177/ ns ns 4005 BMPrg/8 121 ns 120 ns BMPrg/64 140 ns 128 ns BMPrg/512 212 ns 211 ns BMPrg/4096 1609 ns 1595 ns BMPrg/ ns ns BMECDH 298 ns 286 ns In particular , I looked at where is the number of extra bits , and looked at two moduli from the documentation , of respectively 32 and 64 bits . The value after the is the size of the output vector . 
 Additionally , I would like to point out that the analysis in the hash - to - curve draft is for generating a single random value . If the approach of extra bits is chosen , when generating random values , the statistical distance also depends on , and henceforth more than bits should be used for a statistical distance of for the whole vector . ( In which case , you may actually want to consider different methods to quantify the closeness of the distributions , such as ) . 
 It seems like rejection sampling is clearly the better method , based on the above benchmarks . It 's also fairly straightforward to implement . I would favor updating the text to require this method . 
 I 'm working on a PR to close this issue ( we 'll go with rejection sampling ) . I ca n't confirm all of the behavior of libprio - rs and libprio - cc that you pointed to . Specifically , neither library appears to clear the two most significant bits , as you suggested they do . Here 's the pertinent code in the latter : Can you confirm that the bits are n't being cleared ? 
 In libprio - rs ( at least initially ) and libprio - cc , the prime is hardcoded to a 32 - bit prime , so you actually should not clear the most significant bits . More generally , if p is a k - bit prime in a word of l bits , you will want to clear the most significant bits . for k = 32 , l=32 , you want to clear 0 bits ; for k = 126 , l=128 , you want to clear 2 bits ( example I gave in the first comment ) . 
 Ah , yes . That makes sense . Thanks !
Paragraph (version A):
To generate an aggregator's share, the client runs (c[i], k[i]) :=
Encaps(pk[i]) and sends c[i] to the aggregator.  To compute its
share, the aggregator would run k[i] := Decaps(sk[i], c[i]) and
compute its share as {x:i} = PRG(k[i], n).
Paragraph (version B):
To generate an aggregator's share, the client runs (c[i], k[i]) :=
Encaps(pk[i]) and sends c[i] to the aggregator.  To compute its
share, the aggregator would run k[i] := Decaps(sk[i], c[i]) and
compute its share as {x:i} = PRNG(k[i], n).
Paragraph (version A):
x := PRG(k, n) denotes generation of a vector of n elements of K.
Paragraph (version A):
This can be instantiated using a standard stream cipher, e.g..,
ChaCha20 as follows.  Interpret k as the cipher key, and using a
fixed nonce, generate l*n bytes of output, where l is the number of
bytes needed to encode an element of K, then map each chunk of l
bytes to an element of K by interpreting the chunk as an l-byte
integer and reducing it modulo the prime modulus.
Paragraph (version B):
x := PRNG(k, n) denotes generation of a vector of n elements of K.
Paragraph (version B):
This can be instantiated using a standard stream cipher, e.g., AES-
CTR, as follows.  Interpret the seed k as the key and IV for
generating the AES-CTR key stream.  Proceed by rejection sampling, as
follows.  Let m be the number of bits needed to encode an element of
K.  Generate the next m bits of key stream and interpret the bytes as
an integer x, clearing the most significant m - l bits, where l is
the bit-length of the modulus p.  If x < p, then output x.
Otherwise, generate the next m bits of key stream and try again.
Repeat this process indefinitely until a suitable output is found.