Skip to content

mpc-tls: size GHASH preprocessing to the configured record length - #1174

Open
SipengXie2024 wants to merge 2 commits into
tlsnotary:mainfrom
SipengXie2024:zkrelay/cap-aware-ghash
Open

mpc-tls: size GHASH preprocessing to the configured record length#1174
SipengXie2024 wants to merge 2 commits into
tlsnotary:mainfrom
SipengXie2024:zkrelay/cap-aware-ghash

Conversation

@SipengXie2024

Copy link
Copy Markdown

GHASH preprocessing allocates 1026 exponents unconditionally, which is exactly one maximum-size TLS record: 1024 ciphertext blocks plus an AAD block and a length block. A connection that provisions far less still pays for a full record in both directions.

Ghash::alloc now takes a max_power that MpcGhash stores, and setup, compute_shares and compute all read that stored value. The AES-GCM caller derives it from the allocation it already receives:

pub(crate) fn powers_for_len(len: usize) -> usize {
    (len.div_ceil(16) + 2).min(MAX_POWER).next_multiple_of(2)
}

The clamp preserves existing behaviour exactly: any caller provisioning at least one maximum-size record still allocates 1026. Rounding up to an even value is required because odd powers are converted in pairs.

All four sites had to change together. compute derives its block offset from shares.len():

let offset = shares.len() - blocks.len();

That subtraction is only safe while the block_count > MAX_POWER guard and the share vector share one bound. Parameterising alloc alone would let it wrap in release builds, skip past the end of the iterator, and fold an empty zip into Gf2_128::zero() — a wrong tag share with no error. alloc also now rejects bounds it cannot honour (odd, below 2, above MAX_POWER).

Tests added:

  • bounded_shares_match_the_unbounded_prefix — for bounds 2, 4, 56 and 1026, the bounded share vector equals the corresponding prefix of the full one.
  • powers_for_len_covers_the_input_and_clamps — the bound always covers ceil(len/16) + 2, stays even, and clamps.
  • alloc_rejects_bounds_it_cannot_honor.
  • bounded_ghash_accepts_its_limit_and_rejects_one_block_more — a four-power instance reproduces the reference GHASH tag at its limit and errors one byte past it, in both roles.

Measured on a downstream application with an 855-byte sent allocation and a 64-byte online receive allocation: 4,123,934 fewer bytes per session (−14.1%), split almost evenly between the two directions, with latency unchanged.

The branch also contains a second, independent commit adding an optional prover-supplied hash commitment blinder; happy to split it into its own PR if you prefer to review them separately.

GHASH preprocessing always allocated 1026 exponents, which is exactly one
maximum-size TLS record: 1024 ciphertext blocks plus an AAD block and a
length block. A connection that provisions far less still paid for a full
record in both directions.

Ghash::alloc now takes a max_power that MpcGhash stores, and setup,
compute_shares and compute all read that stored value. The AES-GCM caller
derives it from the allocation it already receives via powers_for_len,
which clamps to the old bound so any caller provisioning at least one
maximum-size record is unchanged, and rounds up to an even value because
odd powers are converted in pairs.

All four sites had to change together: compute derives its block offset
from shares.len(), and that subtraction is only safe while the length
guard and the share vector share one bound. Parameterising alloc alone
would let it wrap in release builds and fold an empty iterator into a zero
tag share with no error. alloc now also rejects bounds it cannot honour.

Tests: bounded shares equal the unbounded prefix for several bounds; the
derived bound always covers the input, stays even and clamps; invalid
bounds are rejected; and a four-power instance reproduces the reference
GHASH tag at its limit while erroring one byte past it, in both roles.

Measured on a downstream TLSNotary application with an 855-byte sent
allocation and a 64-byte online receive allocation: 4,123,934 fewer bytes
per session (-14.1%), split almost evenly between the two directions.
prove_hash sampled the blinder itself, so a prover could not know the
commitment value until the proving phase and could not start work that
depends on it any earlier.

ProveConfig now carries optional prover-local blinders, and prove_hash
uses a supplied one when present. The field is serde(skip) and absent
from ProveRequest: the verifier still learns commitment values only from
the proving computation, so a prover that supplies a blinder gains
nothing and the verifier assumes nothing new. Blinder::new lets a caller
construct one from its own randomness.

Callers remain responsible for using a cryptographically secure source
and one blinder per commitment; omitting it keeps the previous sampling
behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant