Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add checks that prefix counts are consistent across multiple VDAF executions #332

Merged
merged 41 commits into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
2b919c1
WIP: Add checks that prefix counts are consistent throughout multiple…
schoppmp Mar 7, 2024
bb7ef65
Add text to security considerations about heavy hitters
schoppmp Mar 7, 2024
4cdc597
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 2, 2024
0aa572f
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 2, 2024
84d00d5
Address some review comments
schoppmp Apr 2, 2024
6ed9d3a
Address review comments
schoppmp Apr 2, 2024
c9510ca
Address review comments
schoppmp Apr 2, 2024
cd65595
Add self.threshold to binder string for correlated randomness computa…
schoppmp Apr 2, 2024
d837f12
Remove threshold component from Poplar1 agg_param
schoppmp Apr 2, 2024
184451f
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 18, 2024
e5fed24
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 18, 2024
7dcb804
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 18, 2024
be3e2d2
Restrict is_valid to testing prefixes are connected and levels are in…
schoppmp Apr 18, 2024
bc8b428
Fix PoC
schoppmp Apr 18, 2024
5280b62
Fix PoC
schoppmp Apr 18, 2024
a637131
Fix formatting
schoppmp Apr 19, 2024
12bbf0e
Update draft-irtf-cfrg-vdaf.md
schoppmp Apr 24, 2024
99531a3
Update draft-irtf-cfrg-vdaf.md
schoppmp May 2, 2024
bd2db33
Update poc/vdaf_poplar1.py
schoppmp May 2, 2024
7f469bf
Rule out some ideas for streamlining preparation
cjpatton Apr 30, 2024
bd3b2a4
Revert "Update poc/vdaf_poplar1.py"
schoppmp May 2, 2024
4e8e70c
Revert "Update draft-irtf-cfrg-vdaf.md"
schoppmp May 2, 2024
15299a7
Only check prefixes are suffixes of last level if that level is > 0
schoppmp May 2, 2024
20d81c1
Address review comments
schoppmp May 3, 2024
27dddf8
Fix formatting
schoppmp May 3, 2024
f8e91c2
Fix get_ancestor call
schoppmp May 3, 2024
f1b7455
Update draft-irtf-cfrg-vdaf.md
schoppmp May 7, 2024
faf7de4
Update draft-irtf-cfrg-vdaf.md
schoppmp May 7, 2024
25f47fd
Update poc/vdaf_poplar1.py
schoppmp May 7, 2024
07b234f
Update poc/vdaf_poplar1.py
schoppmp May 7, 2024
b537eef
Update poc/vdaf_poplar1.py
schoppmp May 7, 2024
f9a3d0e
Fix privacy considerations
schoppmp May 7, 2024
a7d3c03
Fix tests for Poplar1
schoppmp May 8, 2024
ac4f098
Fix poplar1 test
schoppmp May 8, 2024
bd9eb24
Fix poplar1 test
schoppmp May 8, 2024
9af3d2f
Fix poplar1 test
schoppmp May 8, 2024
a401cca
Update draft-irtf-cfrg-vdaf.md
schoppmp May 9, 2024
05921bf
Add tests for get_ancestor
schoppmp May 13, 2024
1b07e49
Add one more test case
schoppmp May 13, 2024
43913bf
Update draft-irtf-cfrg-vdaf.md
schoppmp May 13, 2024
e8ed23f
Update draft-irtf-cfrg-vdaf.md
schoppmp May 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 41 additions & 11 deletions draft-irtf-cfrg-vdaf.md
Original file line number Diff line number Diff line change
Expand Up @@ -1180,7 +1180,7 @@ been used with the same input share.

VDAFs MUST implement the following function:

* `Vdaf.is_valid(agg_param: AggParam, previous_agg_params: set[AggParam]) ->
* `Vdaf.is_valid(agg_param: AggParam, previous_agg_params: list[AggParam]) ->
Bool`: Checks if the `agg_param` is compatible with all elements of
`previous_agg_params`.

Expand Down Expand Up @@ -2573,7 +2573,7 @@ Every input share MUST only be used once, regardless of the aggregation
parameters used.

~~~
def is_valid(agg_param, previous_agg_params):
def is_valid(Prio3, agg_param, previous_agg_params):
return len(previous_agg_params) == 0
~~~
{: #prio3-validity-scope title="Validity of aggregation parameters for Prio3."}
Expand Down Expand Up @@ -3975,15 +3975,36 @@ def prep_shares_to_prep(Poplar1, agg_param, prep_shares):
Aggregation parameters are valid for a given input share if no aggregation
parameter with the same level has been used with the same input share before.
The whole preparation phase MUST NOT be run more than once for a given
combination of input share and level.
combination of input share and level. This function checks that levels are
increasing between calls, and also enforces that the prefixes at each level are
suffixes of the previous level's prefixes.

~~~
def is_valid(agg_param, previous_agg_params):
(level, _) = agg_param
return all(
level != other_level
for (other_level, _) in previous_agg_params
)
def get_ancestor(Poplar1, input, this_level, last_level):
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
# Helper function to determine the prefix of `input` at `last_level`.
return input >> (this_level - last_level)

def is_valid(Poplar1, agg_param, previous_agg_params):
# Exit early if this is the first time.
if len(previous_agg_params) < 1:
return True

(level, prefixes) = agg_param
(last_level, last_prefixes) = previous_agg_params[-1]
# The empty prefix 0 is always there.
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
last_prefixes_set = set(list(last_prefixes) + [0])
schoppmp marked this conversation as resolved.
Show resolved Hide resolved

# Check that level increased.
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
if level <= last_level
return False

# Check that prefixes are suffixes of the last level's prefixes.
for prefix in prefixes:
last_prefix = get_ancestor(prefix, level, last_level)
if last_prefix not in last_prefixes_set:
return False # Current prefix not a suffix of last level's prefixes.
schoppmp marked this conversation as resolved.
Show resolved Hide resolved

return True
~~~
{: #poplar1-validity-scope title="Validity of aggregation parameters for
Poplar1."}
Expand All @@ -3995,6 +4016,8 @@ Aggregation involves simply adding up the output shares.
~~~
def aggregate(Poplar1, agg_param, out_shares):
(level, prefixes) = agg_param
if sum(prefix_counts) > len(out_shares):
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
return ERR_VERIFY
Field = Poplar1.Idpf.current_field(level)
agg_share = Field.zeros(len(prefixes))
for out_share in out_shares:
Expand Down Expand Up @@ -4603,8 +4626,15 @@ perhaps unlikely) for a large set of non-heavy-hitter values to share a common
prefix, which would be leaked by a prefix tree with a sufficiently small
threshold.

The only known, general-purpose approach to mitigating this leakage is via
differential privacy.
A malicious adversary controlling the Collector and one of the Aggregators can
further turn arbitrary non-heavy prefixes into heavy ones by tampering with the
IDPF output at any position. While our construction ensures that the sum of all
nodes evaluated at a level cannot exceed the value at the parent nodes, this
still may allow an adversary to discover individual non-heavy strings.
schoppmp marked this conversation as resolved.
Show resolved Hide resolved

The only practical, general-purpose approach to mitigating these leakages is via
differential privacy, which is RECOMMENDED for all protocols using Poplar1 for
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
heavy-hitter type applications.

> TODO(issue #94) Describe (or point to some description of) the central DP
> mechanism for Poplar described in {{BBCGGI21}}.
Expand Down
12 changes: 9 additions & 3 deletions poc/tests/test_vdaf_poplar1.py
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,15 @@ def test_is_valid(self):
# Test `is_valid` returns False on repeated levels, and True otherwise.
cls = Poplar1.with_bits(256)
agg_params = [(0, ()), (1, (0,)), (1, (0, 1))]
assert cls.is_valid(agg_params[0], set([]))
assert cls.is_valid(agg_params[1], set(agg_params[:1]))
assert not cls.is_valid(agg_params[2], set(agg_params[:2]))
assert cls.is_valid(agg_params[0], list([]))
assert cls.is_valid(agg_params[1], list(agg_params[:1]))
assert not cls.is_valid(agg_params[2], list(agg_params[:2]))

# Test `is_valid` accepts level jumps.
agg_params = [(0, ()), (2, (0, 1)), (3, (0, 5))]
assert cls.is_valid(agg_params[1], list(agg_params[:1]))
# Test `is_valid` rejects unconnected prefixes.
assert not cls.is_valid(agg_params[2], list(agg_params[:2]))

def test_aggregation_parameter_encoding(self):
# Test aggregation parameter encoding.
Expand Down
39 changes: 31 additions & 8 deletions poc/vdaf_poplar1.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,16 +151,39 @@ def shard(Poplar1, measurement, nonce, rand):
input_shares = list(zip(keys, corr_seed, corr_inner, corr_leaf))
return (public_share, input_shares)

def is_valid(agg_param, previous_agg_params):
@classmethod
def get_ancestor(Poplar1, input, this_level, last_level):
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
"""
Checks if the level of `agg_param` appears anywhere in
`previous_agg_params`. Returns `False` if it does, and `True` otherwise.
Helper function to determine the prefix of `input` at `last_level`.
"""
(level, _) = agg_param
return all(
level != other_level
for (other_level, _) in previous_agg_params
)
return input >> (this_level - last_level)

@classmethod
def is_valid(Poplar1, agg_param, previous_agg_params):
"""
Checks that levels are increasing between calls, and also enforces that
the prefixes at each level are suffixes of the previous level's
prefixes.
"""
if len(previous_agg_params) < 1:
return True

(level, prefixes) = agg_param
(last_level, last_prefixes) = previous_agg_params[-1]
# The empty prefix 0 is always there.
schoppmp marked this conversation as resolved.
Show resolved Hide resolved
last_prefixes_set = set(list(last_prefixes) + [0])
schoppmp marked this conversation as resolved.
Show resolved Hide resolved

# Check that level increased.
if level <= last_level:
return False

# Check that prefixes are suffixes of the last level's prefixes.
for (i, prefix) in enumerate(prefixes):
last_prefix = Poplar1.get_ancestor(prefix, level, last_level)
if last_prefix not in last_prefixes_set:
# Current prefix not a suffix of last level's prefixes.
return False
return True

@classmethod
def prep_init(Poplar1, verify_key, agg_id, agg_param,
Expand Down
Loading