From 4ba234faa54761463623faa7e8dc07173d513a10 Mon Sep 17 00:00:00 2001 From: Phillipp Schoppmann Date: Thu, 23 Feb 2023 17:00:57 -0500 Subject: [PATCH] Add fixed-key AES specification --- draft-irtf-cfrg-vdaf.md | 201 ++++++++++++++++++++++++++++------------ poc/idpf.sage | 24 +++-- poc/idpf_poplar.sage | 57 +++++------- poc/prg.sage | 49 +++++++++- poc/vdaf_poplar1.sage | 31 ++++--- 5 files changed, 244 insertions(+), 118 deletions(-) diff --git a/draft-irtf-cfrg-vdaf.md b/draft-irtf-cfrg-vdaf.md index 5791efee..b9495b49 100644 --- a/draft-irtf-cfrg-vdaf.md +++ b/draft-irtf-cfrg-vdaf.md @@ -138,6 +138,18 @@ informative: seriesinfo: EUROCRYPT 2014 target: https://link.springer.com/chapter/10.1007/978-3-642-55220-5_35 + GKWWY20: + title: Better concrete security for half-gates garbling (in the multi-instance setting) + authors: + - ins: C. Guo + - ins: J. Katz + - ins: X. Wang + - ins: C. Weng + - ins: Y. Yu + date: 2020 + seriesinfo: CRYPTO 2020 + target: https://link.springer.com/chapter/10.1007/978-3-030-56880-1_28 + OriginTelemetry: title: "Origin Telemetry" date: 2020 @@ -1337,6 +1349,7 @@ def expand_into_vec(Prg, This section describes PrgSha3, a PRG based on the Keccak permutation of SHA-3 {{FIPS202}}. Keccak is used in the cSHAKE128 mode of operation {{SP800-185}}. +This Prg is RECOMMENDED for all use cases within VDAFs. ~~~ class PrgSha3(Prg): @@ -1363,6 +1376,57 @@ class PrgSha3(Prg): ~~~ {: title="Definition of PRG PrgSha3."} +### PrgFixedKeyAes128 {#prg-fixed-key-aes128} + +While PrgSha3 as described above can be securely used in all cases where a Prg +is needed in the VDAFs described in this document, there are some cases where +a more efficient instantiation based on fixed-key AES is possible. For now, this +is limited to the Prg used inside the Idpf {{idpf}} implementation in Poplar1 +{{idpf-poplar}}. It is NOT RECOMMENDED to use this Prg anywhere else. +See Security Considerations {{security}} for a more detailed discussion. + +~~~ +class PrgFixedKeyAes128(Prg): + # Associated parameters + SEED_SIZE = 16 + + def __init__(self, seed, custom, binder): + self.length_consumed = 0 + + # Use SHA-3 to derive a key from the binder and customization + # strings. Note that the AES key does not need to be kept + # secret from any party. However, when used with IpdfPoplar, + # we require the binder to be a random nonce. + # + # Implementation note: This step can be cached across PRG + # evaluations with many different seeds. + self.fixed_key = cSHAKE128(binder, 16, b'', custom) + self.seed = seed + + def next(self, length: Unsigned) -> Bytes: + offset = self.length_consumed % 16 + new_length = self.length_consumed + length + block_range = range( + int(self.length_consumed / 16), + int(new_length / 16) + 1) + self.length_consumed = new_length + + hashed_blocks = [ + self.hash_block(xor(self.seed, to_le_bytes(i, 16))) \ + for i in block_range + ] + return concat(hashed_blocks)[offset:offset+length] + + # The multi-instance tweakable circular correlation-robust hash function of + # [GKWWY20] (Section 4.2). + # + # Function `AES128(key, block)` is the AES-128 blockcipher. + def hash_block(self, block): + lo, hi = block[:8], block[8:] + sigma = hi + xor(hi, lo) + return xor(AES128(self.fixed_key, sigma), sigma) +~~~ + ### The Customization and Binder Strings PRGs are used to map a seed to a finite domain, e.g., a fresh seed or a vector @@ -2669,20 +2733,21 @@ either a vector of inner node field elements or leaf node field elements.) The scheme is comprised of the following algorithms: * `Idpf.gen(alpha: Unsigned, beta_inner: Vec[Vec[Idpf.FieldInner]], beta_leaf: - Vec[Idpf.FieldLeaf], rand: Bytes[Idpf.RAND_SIZE]) -> (Bytes, Vec[Bytes])` is - the randomized IDPF-key generation algorithm. (Input `rand` consists of the - random coins it consumes.) Its inputs are the index `alpha` and the values - `beta`. The value of `alpha` MUST be in range `[0, 2^BITS)`. The output is a - public part that is sent to all Aggregators and a vector of private IDPF keys, - one for each aggregator. - -* `Idpf.eval(agg_id: Unsigned, public_share: Bytes, key: Bytes, level: Unsigned, - prefixes: Vec[Unsigned]) -> Idpf.Vec` is the deterministic, stateless - IDPF-key evaluation algorithm run by each Aggregator. Its inputs are the - Aggregator's unique identifier, the public share distributed to all of the - Aggregators, the Aggregator's IDPF key, the "level" at which to evaluate the - IDPF, and the sequence of candidate prefixes. It returns the share of the - value corresponding to each candidate prefix. + Vec[Idpf.FieldLeaf], binder: Bytes, rand: Bytes[Idpf.RAND_SIZE]) -> (Bytes, + Vec[Bytes])` is the randomized IDPF-key generation algorithm. (Input `rand` + consists of the random coins it consumes.) Its inputs are the index `alpha` + the values `beta`, and a binder string. The value of `alpha` MUST be in range + `[0, 2^BITS)`. The output is a public part that is sent to all Aggregators + and a vector of private IDPF keys, one for each aggregator. + +* `Idpf.eval(agg_id: Unsigned, public_share: Bytes, key: Bytes, level: + Unsigned, prefixes: Vec[Unsigned], binder: Bytes) -> Idpf.Vec` is the + deterministic, stateless IDPF-key evaluation algorithm run by each + Aggregator. Its inputs are the Aggregator's unique identifier, the public + share distributed to all of the Aggregators, the Aggregator's IDPF key, the + "level" at which to evaluate the IDPF, the sequence of candidate prefixes, + and a binder string. It returns the share of the value corresponding to each + candidate prefix. The output type depends on the value of `level`: If `level < Idpf.BITS-1`, the output is the value for an inner node, which has type @@ -2721,22 +2786,21 @@ state across evaluations. See {{idpf-poplar}} for details. | KEY_SIZE | Size in bytes of each IDPF key | | FieldInner | Implementation of `Field` ({{field}}) used for values of inner nodes | | FieldLeaf | Implementation of `Field` used for values of leaf nodes | -| Prg | Implementation of `Prg` ({{prg}}) | {: #idpf-param title="Constants and types defined by a concrete IDPF."} ## Construction {#poplar1-construction} This section specifies `Poplar1`, an implementation of the `Vdaf` interface ({{vdaf}}). It is defined in terms of any `Idpf` ({{idpf}}) for which -`Idpf.SHARES == 2` and `Idpf.VALUE_LEN == 2`. The associated constants and types -required by the `Vdaf` interface are defined in {{poplar1-param}}. The methods -required for sharding, preparation, aggregation, and unsharding are described in -the remaining subsections. These methods make use of constants defined in -{{poplar1-const}}. +`Idpf.SHARES == 2` and `Idpf.VALUE_LEN == 2` and an implementation of `Prg` +({{prg}}). The associated constants and types required by the `Vdaf` interface +are defined in {{poplar1-param}}. The methods required for sharding, +preparation, aggregation, and unsharding are described in the remaining +subsections. These methods make use of constants defined in {{poplar1-const}}. | Parameter | Value | |:------------------|:------------------| -| `VERIFY_KEY_SIZE` | `Idpf.Prg.SEED_SIZE` | +| `VERIFY_KEY_SIZE` | `Prg.SEED_SIZE` | | `NONCE_SIZE` | `16` | | `ROUNDS` | `2` | | `SHARES` | `2` | @@ -2777,7 +2841,7 @@ follows. Function `encode_input_shares` is defined in {{poplar1-auxiliary}}. ~~~ def measurement_to_input_shares(Poplar1, measurement, nonce, rand): - l = Poplar1.Idpf.Prg.SEED_SIZE + l = Poplar1.Prg.SEED_SIZE # Split the coins into coins for IDPF key generation, # correlated randomness, and sharding. @@ -2788,8 +2852,8 @@ def measurement_to_input_shares(Poplar1, measurement, nonce, rand): corr_seed, seeds = front(2, seeds) (k_shard,), seeds = front(1, seeds) - prg = Poplar1.Idpf.Prg(k_shard, - Poplar1.custom(DST_SHARD_RAND), b'') + prg = Poplar1.Prg(k_shard, + Poplar1.custom(DST_SHARD_RAND), b'') # Construct the IDPF values for each level of the IDPF tree. # Each "data" value is 1; in addition, the Client generates @@ -2814,14 +2878,14 @@ def measurement_to_input_shares(Poplar1, measurement, nonce, rand): # used to encode shares of the `(a, b, c)` triples. # (See [BBCGGI21, Appendix C.4].) corr_offsets = vec_add( - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldInner, corr_seed[0], Poplar1.custom(DST_CORR_INNER), byte(0) + nonce, 3 * (Poplar1.Idpf.BITS-1), ), - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldInner, corr_seed[1], Poplar1.custom(DST_CORR_INNER), @@ -2830,14 +2894,14 @@ def measurement_to_input_shares(Poplar1, measurement, nonce, rand): ), ) corr_offsets += vec_add( - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldLeaf, corr_seed[0], Poplar1.custom(DST_CORR_LEAF), byte(0) + nonce, 3, ), - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldLeaf, corr_seed[1], Poplar1.custom(DST_CORR_LEAF), @@ -2915,13 +2979,13 @@ def prep_init(Poplar1, verify_key, agg_id, agg_param, # Aggregator's share of the sketch for the given level of the IDPF # tree. if level < Poplar1.Idpf.BITS - 1: - corr_prg = Poplar1.Idpf.Prg(corr_seed, + corr_prg = Poplar1.Prg(corr_seed, Poplar1.custom(DST_CORR_INNER), byte(agg_id) + nonce) # Fast-forward the PRG state to the current level. corr_prg.next_vec(Field, 3 * level) else: - corr_prg = Poplar1.Idpf.Prg(corr_seed, + corr_prg = Poplar1.Prg(corr_seed, Poplar1.custom(DST_CORR_LEAF), byte(agg_id) + nonce) (a_share, b_share, c_share) = corr_prg.next_vec(Field, 3) @@ -2930,7 +2994,7 @@ def prep_init(Poplar1, verify_key, agg_id, agg_param, # Compute the Aggregator's first round of the sketch. These are # called the "masked input values" [BBCGGI21, Appendix C.4]. - verify_rand_prg = Poplar1.Idpf.Prg(verify_key, + verify_rand_prg = Poplar1.Prg(verify_key, Poplar1.custom(DST_VERIFY_RAND), nonce + to_be_bytes(level, 2)) verify_rand = verify_rand_prg.next_vec(Field, len(prefixes)) @@ -3091,7 +3155,7 @@ def encode_input_shares(Poplar1, keys, def decode_input_share(Poplar1, encoded): l = Poplar1.Idpf.KEY_SIZE key, encoded = encoded[:l], encoded[l:] - l = Poplar1.Idpf.Prg.SEED_SIZE + l = Poplar1.Prg.SEED_SIZE corr_seed, encoded = encoded[:l], encoded[l:] l = Poplar1.Idpf.FieldInner.ENCODED_SIZE \ * 2 * (Poplar1.Idpf.BITS - 1) @@ -3156,6 +3220,11 @@ instantiating Poplar1. The scheme gets its name from the name of the protocol of The constant and type definitions required by the `Idpf` interface are given in {{idpf-poplar-param}}. +IdpfPoplar requires a PRG for deriving the output shares, as well as a variety +of other artifacts used internally. For performance reasons, we instantiate +this object using PrgFixedKeyAes128 ({{prg-fixed-key-aes128}}). See +{{prg-vs-ro}} for justification of this choice. + | Parameter | Value | |:-----------|:------------------------| | SHARES | `2` | @@ -3164,7 +3233,6 @@ The constant and type definitions required by the `Idpf` interface are given in | KEY_SIZE | `Prg.SEED_SIZE` | | FieldInner | `Field64` ({{fields}}) | | FieldLeaf | `Field255` ({{fields}}) | -| Prg | any implementation of `Prg` ({{prg}}) | {: #idpf-poplar-param title="Constants and type definitions for IdpfPoplar."} ### Key Generation @@ -3178,7 +3246,7 @@ functions `extend()`, `convert()`, and `encode_public_share()` defined in field `GF(2)`. ~~~ -def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, rand): +def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, binder, rand): if alpha >= 2^IdpfPoplar.BITS: raise ERR_INPUT # alpha too long if len(beta_inner) != IdpfPoplar.BITS - 1: @@ -3187,8 +3255,8 @@ def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, rand): raise ERR_INPUT # unexpected length for random coins init_seed = [ - rand[:IdpfPoplar.Prg.SEED_SIZE], - rand[IdpfPoplar.Prg.SEED_SIZE:], + rand[:PrgFixedKeyAes128.SEED_SIZE], + rand[PrgFixedKeyAes128.SEED_SIZE:], ] seed = init_seed.copy() @@ -3200,8 +3268,8 @@ def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, rand): lose = 1 - keep bit = Field2(keep) - (s0, t0) = IdpfPoplar.extend(seed[0]) - (s1, t1) = IdpfPoplar.extend(seed[1]) + (s0, t0) = IdpfPoplar.extend(seed[0], binder) + (s1, t1) = IdpfPoplar.extend(seed[1], binder) seed_cw = xor(s0[lose], s1[lose]) ctrl_cw = ( t0[0] + t1[0] + bit + Field2(1), @@ -3210,8 +3278,8 @@ def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, rand): x0 = xor(s0[keep], ctrl[0].conditional_select(seed_cw)) x1 = xor(s1[keep], ctrl[1].conditional_select(seed_cw)) - (seed[0], w0) = IdpfPoplar.convert(level, x0) - (seed[1], w1) = IdpfPoplar.convert(level, x1) + (seed[0], w0) = IdpfPoplar.convert(level, x0, binder) + (seed[1], w1) = IdpfPoplar.convert(level, x1, binder) ctrl[0] = t0[keep] + ctrl[0] * ctrl_cw[keep] ctrl[1] = t1[keep] + ctrl[1] * ctrl_cw[keep] @@ -3245,7 +3313,7 @@ functions `extend()`, `convert()`, and `decode_public_share()` defined in ~~~ def eval(IdpfPoplar, agg_id, public_share, init_seed, - level, prefixes): + level, prefixes, binder): if agg_id >= IdpfPoplar.SHARES: raise ERR_INPUT # invalid aggregator ID if level >= IdpfPoplar.BITS: @@ -3284,7 +3352,7 @@ def eval(IdpfPoplar, agg_id, public_share, init_seed, # complexity by caching nodes (i.e., `(seed, ctrl)` # pairs) output by previous calls to `eval_next()`. (seed, ctrl, y) = IdpfPoplar.eval_next(seed, ctrl, - correction_words[current_level], current_level, bit) + correction_words[current_level], current_level, bit, binder) out_share.append(y if agg_id == 0 else vec_neg(y)) return out_share @@ -3296,17 +3364,17 @@ def eval(IdpfPoplar, agg_id, public_share, init_seed, # discussed at the end of [BBCGGI21, Appendix C.2]. This could on # average reduce the number of AES calls by a constant factor. def eval_next(IdpfPoplar, prev_seed, prev_ctrl, - correction_word, level, bit): + correction_word, level, bit, binder): Field = IdpfPoplar.current_field(level) (seed_cw, ctrl_cw, w_cw) = correction_word - (s, t) = IdpfPoplar.extend(prev_seed) + (s, t) = IdpfPoplar.extend(prev_seed, binder) s[0] = xor(s[0], prev_ctrl.conditional_select(seed_cw)) s[1] = xor(s[1], prev_ctrl.conditional_select(seed_cw)) t[0] += ctrl_cw[0] * prev_ctrl t[1] += ctrl_cw[1] * prev_ctrl next_ctrl = t[bit] - (next_seed, y) = IdpfPoplar.convert(level, s[bit]) + (next_seed, y) = IdpfPoplar.convert(level, s[bit], binder) # Implementation note: Here we add the correction word to the # output if `next_ctrl` is set. We avoid branching on the value of # the control bit in order to reduce side channel leakage. @@ -3321,19 +3389,19 @@ def eval_next(IdpfPoplar, prev_seed, prev_ctrl, ### Auxiliary Functions {#idpf-poplar-helper-functions} ~~~ -def extend(IdpfPoplar, seed): - prg = IdpfPoplar.Prg(seed, format_custom(1, 0, 0), b'') +def extend(IdpfPoplar, seed, binder): + prg = PrgFixedKeyAes128(seed, format_custom(1, 0, 0), binder) s = [ - prg.next(IdpfPoplar.Prg.SEED_SIZE), - prg.next(IdpfPoplar.Prg.SEED_SIZE), + prg.next(PrgFixedKeyAes128.SEED_SIZE), + prg.next(PrgFixedKeyAes128.SEED_SIZE), ] b = prg.next(1)[0] t = [Field2(b & 1), Field2((b >> 1) & 1)] return (s, t) -def convert(IdpfPoplar, level, seed): - prg = IdpfPoplar.Prg(seed, format_custom(1, 0, 1), b'') - next_seed = prg.next(IdpfPoplar.Prg.SEED_SIZE) +def convert(IdpfPoplar, level, seed, binder): + prg = PrgFixedKeyAes128(seed, format_custom(1, 0, 1), binder) + next_seed = prg.next(PrgFixedKeyAes128.SEED_SIZE) Field = IdpfPoplar.current_field(level) w = prg.next_vec(Field, IdpfPoplar.VALUE_LEN) return (next_seed, w) @@ -3362,16 +3430,13 @@ def decode_public_share(IdpfPoplar, encoded): control_bits[level * 2], control_bits[level * 2 + 1], ) - l = IdpfPoplar.Prg.SEED_SIZE + l = PrgFixedKeyAes128.SEED_SIZE seed_cw, encoded = encoded[:l], encoded[l:] l = Field.ENCODED_SIZE * IdpfPoplar.VALUE_LEN encoded_w_cw, encoded = encoded[:l], encoded[l:] w_cw = Field.decode_vec(encoded_w_cw) correction_words.append((seed_cw, ctrl_cw, w_cw)) - leftover_bits = encoded_ctrl[-1] >> ( - ((IdpfPoplar.BITS + 3) % 4 + 1) * 2 - ) - if leftover_bits != 0 or len(encoded) != 0: + if len(encoded) != 0: raise ERR_DECODE return correction_words ~~~ @@ -3538,6 +3603,24 @@ differential privacy. > TODO(issue #94) Describe (or point to some description of) the central DP > mechanism for Poplar described in {{BBCGGI21}}. +## Pseudorandom Generators and random oracles {#prg-vs-ro} + +The objects we describe in {{prg}} share a common interface, which we have +called Prg. However, these are not necessarily all modeled as cryptographic +Pseudorandom Generators in the security analyses of our protocols. Instead, most +of them are modeled as random oracles. For these use cases, we want to be +conservative in our assumptions, and hence prescribe PrgSha3 as the only +RECOMMENDED Prg instantiation. + +The one exception is the PRG used in the Idpf implementation IdpfPoplar +{{idpf-poplar}}. Here, a random oracle is not needed to prove security, and +hence a construction based on fixed-key AES {{prg-fixed-key-aes128}} can be +used. However, as PrgFixedKeyAes128 has been shown to be differentiable from +a random oracle {{GKWWY20}}, it is NOT RECOMMENDED to use it anywhere else. + +> OPEN ISSUE: We may want to drop the common interface for PRGs and random +> oracles. See issue #159. + # IANA Considerations A codepoint for each (V)DAF in this document is defined in the table below. Note @@ -3565,8 +3648,8 @@ analysis of {{DPRS23}}. Thanks to Hannah Davis and Mike Rosulek, who lent their time to developing definitions and security proofs. Thanks to Henry Corrigan-Gibbs, Armando Faz-Hernández, Simon Friedberger, Tim -Geoghegan, Mariana Raykova, Jacob Rothstein, and Christopher Wood for useful -feedback on and contributions to the spec. +Geoghegan, Mariana Raykova, Jacob Rothstein, Xiao Wang, and Christopher Wood for +useful feedback on and contributions to the spec. # Test Vectors {#test-vectors} {:numbered="false"} diff --git a/poc/idpf.sage b/poc/idpf.sage index 217ea41e..44a569e5 100644 --- a/poc/idpf.sage +++ b/poc/idpf.sage @@ -43,7 +43,7 @@ class Idpf: # Generates an IDPF public share and sequence of IDPF-keys of length # `SHARES`. Value `alpha` is the input to encode. Values `beta_inner` and # `beta_leaf` are assigned to the values of the nodes on the non-zero path - # of the IDPF tree. + # of the IDPF tree. String `binder` is a binder string. # # An error is raised if integer `alpha` is larger than or equal to `2^BITS`, # any elment of `beta_inner` has length other than `VALUE_LEN`, or if @@ -53,13 +53,15 @@ class Idpf: alpha: Unsigned, beta_inner: Vec[Vec[Idpf.FieldInner]], beta_leaf: Vec[Idpf.FieldLeaf], + binder: Bytes, rand: Bytes[Idpf.RAND_SIZE]) -> (Bytes, Vec[Bytes]): raise Error('not implemented') # Evaluate an IDPF key at a given level of the tree and with the given set # of prefixes. The output is a vector where each element is a vector of # length `VALUE_LEN`. The output field is `FieldLeaf` if `level == BITS` and - # `FieldInner` otherwise. + # `FieldInner` otherwise. `binder` must match the binder string passed by + # the client to `gen`. # # Let `LSB(x, N)` denote the least significant `N` bits of positive integer # `x`. By definition, a positive integer `x` is said to be the length-`L` @@ -83,8 +85,9 @@ class Idpf: public_share: Bytes, key: Bytes, level: Unsigned, - prefixes: Vec[Unsigned]) -> Union[Vec[Vec[Idpf.FieldInner]], - Vec[Vec[Idpf.FieldLeaf]]]: + prefixes: Vec[Unsigned], + binder: Bytes) -> Union[Vec[Vec[Idpf.FieldInner]], + Vec[Vec[Idpf.FieldLeaf]]]: raise Error('not implemented') @classmethod @@ -106,12 +109,13 @@ def test_idpf(Idpf, alpha, level, prefixes): # Generate the IDPF keys. rand = gen_rand(Idpf.RAND_SIZE) - (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, rand) + binder = b'some nonce' + (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, binder, rand) out = [Idpf.current_field(level).zeros(Idpf.VALUE_LEN)] * len(prefixes) for agg_id in range(Idpf.SHARES): out_share = Idpf.eval( - agg_id, public_share, keys[agg_id], level, prefixes) + agg_id, public_share, keys[agg_id], level, prefixes, binder) for i in range(len(prefixes)): out[i] = vec_add(out[i], out_share[i]) @@ -138,7 +142,8 @@ def gen_test_vec(Idpf, alpha, test_vec_instance): beta_inner.append([Idpf.FieldInner(level)] * Idpf.VALUE_LEN) beta_leaf = [Idpf.FieldLeaf(Idpf.BITS-1)] * Idpf.VALUE_LEN rand = gen_rand(Idpf.RAND_SIZE) - (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, rand) + binder = b'some nonce' + (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, binder, rand) printable_beta_inner = [ [ str(elem.as_unsigned()) for elem in value ] for value in beta_inner @@ -172,7 +177,8 @@ def test_idpf_exhaustive(Idpf, alpha): # Generate the IDPF keys. rand = gen_rand(Idpf.RAND_SIZE) - (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, rand) + binder = b"some nonce" + (public_share, keys) = Idpf.gen(alpha, beta_inner, beta_leaf, binder, rand) # Evaluate the IDPF at every node of the tree. for level in range(Idpf.BITS): @@ -183,7 +189,7 @@ def test_idpf_exhaustive(Idpf, alpha): for agg_id in range(Idpf.SHARES): out_shares.append( Idpf.eval(agg_id, public_share, - keys[agg_id], level, prefixes)) + keys[agg_id], level, prefixes, binder)) # Check that each set of output shares for each prefix sums up to the # correct value. diff --git a/poc/idpf_poplar.sage b/poc/idpf_poplar.sage index 966e0420..7d7aa243 100644 --- a/poc/idpf_poplar.sage +++ b/poc/idpf_poplar.sage @@ -19,19 +19,17 @@ from sagelib.common import \ from sagelib.field import Field2 from sagelib.idpf import Idpf, gen_test_vec, test_idpf, test_idpf_exhaustive import sagelib.field as field -import sagelib.prg as prg +from sagelib.prg import PrgFixedKeyAes128 # An IDPF based on the construction of [BBCGI21, Section 6]. It is identical # except that the output shares may be tuples rather than single field elements. # In particular, the value of `VALUE_LEN` may be any positive integer. class IdpfPoplar(Idpf): - # Generic parameters set by a concrete instance of this IDPF. - Prg: prg.Prg = None - # Parameters required by `Vdaf`. SHARES = 2 - RAND_SIZE = None # Set by `Prg` + KEY_SIZE = PrgFixedKeyAes128.SEED_SIZE + RAND_SIZE = 2 * PrgFixedKeyAes128.SEED_SIZE FieldInner = field.Field64 FieldLeaf = field.Field255 @@ -39,7 +37,7 @@ class IdpfPoplar(Idpf): test_vec_name = 'IdpfPoplar' @classmethod - def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, rand): + def gen(IdpfPoplar, alpha, beta_inner, beta_leaf, binder, rand): if alpha >= 2^IdpfPoplar.BITS: raise ERR_INPUT # alpha too long if len(beta_inner) != IdpfPoplar.BITS - 1: @@ -48,8 +46,8 @@ class IdpfPoplar(Idpf): raise ERR_INPUT # unexpected length for random coins init_seed = [ - rand[:IdpfPoplar.Prg.SEED_SIZE], - rand[IdpfPoplar.Prg.SEED_SIZE:], + rand[:PrgFixedKeyAes128.SEED_SIZE], + rand[PrgFixedKeyAes128.SEED_SIZE:], ] seed = init_seed.copy() @@ -61,8 +59,8 @@ class IdpfPoplar(Idpf): lose = 1 - keep bit = Field2(keep) - (s0, t0) = IdpfPoplar.extend(seed[0]) - (s1, t1) = IdpfPoplar.extend(seed[1]) + (s0, t0) = IdpfPoplar.extend(seed[0], binder) + (s1, t1) = IdpfPoplar.extend(seed[1], binder) seed_cw = xor(s0[lose], s1[lose]) ctrl_cw = ( t0[0] + t1[0] + bit + Field2(1), @@ -71,8 +69,8 @@ class IdpfPoplar(Idpf): x0 = xor(s0[keep], ctrl[0].conditional_select(seed_cw)) x1 = xor(s1[keep], ctrl[1].conditional_select(seed_cw)) - (seed[0], w0) = IdpfPoplar.convert(level, x0) - (seed[1], w1) = IdpfPoplar.convert(level, x1) + (seed[0], w0) = IdpfPoplar.convert(level, x0, binder) + (seed[1], w1) = IdpfPoplar.convert(level, x1, binder) ctrl[0] = t0[keep] + ctrl[0] * ctrl_cw[keep] ctrl[1] = t1[keep] + ctrl[1] * ctrl_cw[keep] @@ -96,7 +94,7 @@ class IdpfPoplar(Idpf): @classmethod def eval(IdpfPoplar, agg_id, public_share, init_seed, - level, prefixes): + level, prefixes, binder): if agg_id >= IdpfPoplar.SHARES: raise ERR_INPUT # invalid aggregator ID if level >= IdpfPoplar.BITS: @@ -135,7 +133,7 @@ class IdpfPoplar(Idpf): # complexity by caching nodes (i.e., `(seed, ctrl)` # pairs) output by previous calls to `eval_next()`. (seed, ctrl, y) = IdpfPoplar.eval_next(seed, ctrl, - correction_words[current_level], current_level, bit) + correction_words[current_level], current_level, bit, binder) out_share.append(y if agg_id == 0 else vec_neg(y)) return out_share @@ -148,17 +146,17 @@ class IdpfPoplar(Idpf): # average reduce the number of AES calls by a constant factor. @classmethod def eval_next(IdpfPoplar, prev_seed, prev_ctrl, - correction_word, level, bit): + correction_word, level, bit, binder): Field = IdpfPoplar.current_field(level) (seed_cw, ctrl_cw, w_cw) = correction_word - (s, t) = IdpfPoplar.extend(prev_seed) + (s, t) = IdpfPoplar.extend(prev_seed, binder) s[0] = xor(s[0], prev_ctrl.conditional_select(seed_cw)) s[1] = xor(s[1], prev_ctrl.conditional_select(seed_cw)) t[0] += ctrl_cw[0] * prev_ctrl t[1] += ctrl_cw[1] * prev_ctrl next_ctrl = t[bit] - (next_seed, y) = IdpfPoplar.convert(level, s[bit]) + (next_seed, y) = IdpfPoplar.convert(level, s[bit], binder) # Implementation note: Here we add the correction word to the # output if `next_ctrl` is set. We avoid branching on the value of # the control bit in order to reduce side channel leakage. @@ -169,20 +167,20 @@ class IdpfPoplar(Idpf): return (next_seed, next_ctrl, y) @classmethod - def extend(IdpfPoplar, seed): - prg = IdpfPoplar.Prg(seed, format_custom(1, 0, 0), b'') + def extend(IdpfPoplar, seed, binder): + prg = PrgFixedKeyAes128(seed, format_custom(1, 0, 0), binder) s = [ - prg.next(IdpfPoplar.Prg.SEED_SIZE), - prg.next(IdpfPoplar.Prg.SEED_SIZE), + prg.next(PrgFixedKeyAes128.SEED_SIZE), + prg.next(PrgFixedKeyAes128.SEED_SIZE), ] b = prg.next(1)[0] t = [Field2(b & 1), Field2((b >> 1) & 1)] return (s, t) @classmethod - def convert(IdpfPoplar, level, seed): - prg = IdpfPoplar.Prg(seed, format_custom(1, 0, 1), b'') - next_seed = prg.next(IdpfPoplar.Prg.SEED_SIZE) + def convert(IdpfPoplar, level, seed, binder): + prg = PrgFixedKeyAes128(seed, format_custom(1, 0, 1), binder) + next_seed = prg.next(PrgFixedKeyAes128.SEED_SIZE) Field = IdpfPoplar.current_field(level) w = prg.next_vec(Field, IdpfPoplar.VALUE_LEN) return (next_seed, w) @@ -213,7 +211,7 @@ class IdpfPoplar(Idpf): control_bits[level * 2], control_bits[level * 2 + 1], ) - l = IdpfPoplar.Prg.SEED_SIZE + l = PrgFixedKeyAes128.SEED_SIZE seed_cw, encoded = encoded[:l], encoded[l:] l = Field.ENCODED_SIZE * IdpfPoplar.VALUE_LEN encoded_w_cw, encoded = encoded[:l], encoded[l:] @@ -223,14 +221,6 @@ class IdpfPoplar(Idpf): raise ERR_DECODE return correction_words - @classmethod - def with_prg(IdpfPoplar, ThePrg): - class IdpfPoplarWithPrg(IdpfPoplar): - Prg = ThePrg - KEY_SIZE = ThePrg.SEED_SIZE - RAND_SIZE = 2*ThePrg.SEED_SIZE - return IdpfPoplarWithPrg - @classmethod def with_bits(IdpfPoplar, bits: Unsigned): if bits == 0: @@ -272,7 +262,6 @@ def unpack_bits(packed_bits: Bytes, length: Unsigned) -> Vec[Field2]: if __name__ == '__main__': cls = IdpfPoplar \ - .with_prg(prg.PrgSha3) \ .with_value_len(2) if TEST_VECTOR: gen_test_vec(cls.with_bits(10), 0, 0) diff --git a/poc/prg.sage b/poc/prg.sage index ed1acabd..0192915c 100644 --- a/poc/prg.sage +++ b/poc/prg.sage @@ -8,7 +8,7 @@ from Cryptodome.Util.number import bytes_to_long from sagelib.common import TEST_VECTOR, VERSION, Bytes, Error, Unsigned, \ format_custom, zeros, from_le_bytes, gen_rand, \ next_power_of_2, print_wrapped_line, to_be_bytes, \ - to_le_bytes + to_le_bytes, xor, concat # The base class for PRGs. class Prg: @@ -99,6 +99,51 @@ class PrgSha3(Prg): def next(self, length: Unsigned) -> Bytes: return self.shake.read(length) +# PRG based on a circular collision-resistant hash function from fixed-key AES. +class PrgFixedKeyAes128(Prg): + # Associated parameters + SEED_SIZE = 16 + + def __init__(self, seed, custom, binder): + self.length_consumed = 0 + + # Use SHA-3 to derive a key from the binder and customization + # strings. Note that the AES key does not need to be kept + # secret from any party. However, when used with IpdfPoplar, + # we require the binder to be a random nonce. + # + # Implementation note: This step can be cached across PRG + # evaluations with many different seeds. + shake = cSHAKE128.new(custom=custom) + shake.update(binder) + fixed_key = shake.read(16) + self.cipher = AES.new(fixed_key, AES.MODE_ECB) + # Save seed to be used in `next`. + self.seed = seed + + def next(self, length: Unsigned) -> Bytes: + offset = self.length_consumed % 16 + new_length = self.length_consumed + length + block_range = range( + int(self.length_consumed / 16), + int(new_length / 16) + 1) + self.length_consumed = new_length + + hashed_blocks = [ + self.hash_block(xor(self.seed, to_le_bytes(i, 16))) \ + for i in block_range + ] + return concat(hashed_blocks)[offset:offset+length] + + # The multi-instance tweakable circular correlation-robust hash function of + # [GKWWY20] (Section 4.2). + def hash_block(self, block): + lo, hi = block[:8], block[8:] + sigma = hi + xor(hi, lo) + return xor(self.cipher.encrypt(sigma), sigma) + + + ## # TESTS # @@ -143,7 +188,7 @@ if __name__ == '__main__': ) assert expanded_vec[-1] == Field64(13681157193520586550) - for cls in (PrgAes128, PrgSha3): + for cls in (PrgAes128, PrgSha3, PrgFixedKeyAes128): test_prg(cls, Field128, 23) if TEST_VECTOR: diff --git a/poc/vdaf_poplar1.sage b/poc/vdaf_poplar1.sage index 139e9048..ef8db508 100644 --- a/poc/vdaf_poplar1.sage +++ b/poc/vdaf_poplar1.sage @@ -30,6 +30,7 @@ DST_VERIFY_RAND = 4 class Poplar1(Vdaf): # Types provided by a concrete instadce of `Poplar1`. Idpf = idpf.Idpf + Prg = prg.Prg # Parameters required by `Vdaf`. ID = 0x00001000 @@ -55,7 +56,7 @@ class Poplar1(Vdaf): @classmethod def measurement_to_input_shares(Poplar1, measurement, nonce, rand): - l = Poplar1.Idpf.Prg.SEED_SIZE + l = Poplar1.Prg.SEED_SIZE # Split the coins into coins for IDPF key generation, # correlated randomness, and sharding. @@ -66,7 +67,7 @@ class Poplar1(Vdaf): corr_seed, seeds = front(2, seeds) (k_shard,), seeds = front(1, seeds) - prg = Poplar1.Idpf.Prg(k_shard, + prg = Poplar1.Prg(k_shard, Poplar1.custom(DST_SHARD_RAND), b'') # Construct the IDPF values for each level of the IDPF tree. @@ -85,6 +86,7 @@ class Poplar1(Vdaf): (public_share, keys) = Poplar1.Idpf.gen(measurement, beta_inner, beta_leaf, + nonce, idpf_rand) # Generate correlated randomness used by the Aggregators to @@ -92,14 +94,14 @@ class Poplar1(Vdaf): # used to encode shares of the `(a, b, c)` triples. # (See [BBCGGI21, Appendix C.4].) corr_offsets = vec_add( - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldInner, corr_seed[0], Poplar1.custom(DST_CORR_INNER), byte(0) + nonce, 3 * (Poplar1.Idpf.BITS-1), ), - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldInner, corr_seed[1], Poplar1.custom(DST_CORR_INNER), @@ -108,14 +110,14 @@ class Poplar1(Vdaf): ), ) corr_offsets += vec_add( - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldLeaf, corr_seed[0], Poplar1.custom(DST_CORR_LEAF), byte(0) + nonce, 3, ), - Poplar1.Idpf.Prg.expand_into_vec( + Poplar1.Prg.expand_into_vec( Poplar1.Idpf.FieldLeaf, corr_seed[1], Poplar1.custom(DST_CORR_LEAF), @@ -174,19 +176,19 @@ class Poplar1(Vdaf): # Evaluate the IDPF key at the given set of prefixes. value = Poplar1.Idpf.eval( - agg_id, public_share, key, level, prefixes) + agg_id, public_share, key, level, prefixes, nonce) # Get shares of the correlated randomness for computing the # Aggregator's share of the sketch for the given level of the IDPF # tree. if level < Poplar1.Idpf.BITS - 1: - corr_prg = Poplar1.Idpf.Prg(corr_seed, + corr_prg = Poplar1.Prg(corr_seed, Poplar1.custom(DST_CORR_INNER), byte(agg_id) + nonce) # Fast-forward the PRG state to the current level. corr_prg.next_vec(Field, 3 * level) else: - corr_prg = Poplar1.Idpf.Prg(corr_seed, + corr_prg = Poplar1.Prg(corr_seed, Poplar1.custom(DST_CORR_LEAF), byte(agg_id) + nonce) (a_share, b_share, c_share) = corr_prg.next_vec(Field, 3) @@ -195,7 +197,7 @@ class Poplar1(Vdaf): # Compute the Aggregator's first round of the sketch. These are # called the "masked input values" [BBCGGI21, Appendix C.4]. - verify_rand_prg = Poplar1.Idpf.Prg(verify_key, + verify_rand_prg = Poplar1.Prg(verify_key, Poplar1.custom(DST_VERIFY_RAND), nonce + to_be_bytes(level, 2)) verify_rand = verify_rand_prg.next_vec(Field, len(prefixes)) @@ -311,7 +313,7 @@ class Poplar1(Vdaf): def decode_input_share(Poplar1, encoded): l = Poplar1.Idpf.KEY_SIZE key, encoded = encoded[:l], encoded[l:] - l = Poplar1.Idpf.Prg.SEED_SIZE + l = Poplar1.Prg.SEED_SIZE corr_seed, encoded = encoded[:l], encoded[l:] l = Poplar1.Idpf.FieldInner.ENCODED_SIZE \ * 2 * (Poplar1.Idpf.BITS - 1) @@ -362,13 +364,14 @@ class Poplar1(Vdaf): @classmethod def with_bits(Poplar1, bits: Unsigned): TheIdpf = idpf_poplar.IdpfPoplar \ - .with_prg(prg.PrgSha3) \ .with_value_len(2) \ .with_bits(bits) + ThePrg = prg.PrgSha3 class Poplar1WithBits(Poplar1): Idpf = TheIdpf - VERIFY_KEY_SIZE = TheIdpf.Prg.SEED_SIZE - RAND_SIZE = 3*TheIdpf.Prg.SEED_SIZE + TheIdpf.RAND_SIZE + Prg = ThePrg + VERIFY_KEY_SIZE = ThePrg.SEED_SIZE + RAND_SIZE = 3*ThePrg.SEED_SIZE + TheIdpf.RAND_SIZE test_vec_name = 'Poplar1' return Poplar1WithBits