Skip to content

feat: mask all ciphertext fields with Poseidon2-derived values#21009

Merged
nchamo merged 8 commits intomerge-train/fairiesfrom
feat/mask-all-ciphertext-fields
Mar 4, 2026
Merged

feat: mask all ciphertext fields with Poseidon2-derived values#21009
nchamo merged 8 commits intomerge-train/fairiesfrom
feat/mask-all-ciphertext-fields

Conversation

@nchamo
Copy link
Contributor

@nchamo nchamo commented Mar 2, 2026

Why we are doing this

AES-encrypted logs pack byte data into fields at 31 bytes per field. This means no field ever contains a value larger than 2^248, while other log types might use full field values (up to ~2^254). An external observer can exploit this to identify which logs are AES-encrypted just by checking whether all field values fit in 248 bits, breaking the indistinguishability goal.

The same problem applies to trailing padding fields. Previously these were filled with random 31-byte values (via unconstrained oracles), which also never exceeded 2^248.

Our fix

Content fields (those carrying actual packed message bytes) are masked by adding a Poseidon2-derived field element keyed on the ECDH shared secret and field index:

  mask_i = poseidon2_hash([shared_secret.x, shared_secret.y], DOM_SEP + i)
  ciphertext[1 + i] = packed_field[i] + mask_i

Padding fields (trailing fields that fill the ciphertext to a fixed 15-field size) use random() instead, since their values are irrelevant to the recipient and discarded during decryption.

The result: all 15 output fields (except the ephemeral public key) appear as uniformly random Field values to any observer without knowledge of the shared secret.

Migration

get_random_bytes has been removed from aztec::utils::random. Replace with direct calls to the random oracle from aztec::oracle::random.

Fixes F-369
Fixes #12749

@nchamo nchamo changed the base branch from next to merge-train/fairies March 2, 2026 17:21
@nchamo nchamo self-assigned this Mar 2, 2026
@nchamo nchamo added the ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure label Mar 2, 2026
unconstrained fn derive_field_mask(shared_secret: Point, index: u32) -> Field {
poseidon2_hash_with_separator(
[shared_secret.x, shared_secret.y],
DOM_SEP__CIPHERTEXT_FIELD_MASK + index,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are adding the index to the separator instead of the inputs to be consistent with extract_many_close_to_uniformly_random_256_bits_from_ecdh_shared_secret_using_poseidon2_unsafe (another function in this file that does something similar)

@nchamo nchamo requested a review from nventuro March 4, 2026 13:44
…ask-all-ciphertext-fields

# Conflicts:
#	docs/docs-developers/docs/resources/migration_notes.md
Copy link
Contributor

@nventuro nventuro left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost there! Feel free to merge once you've addressed my last comments. Great work!

let mask = unsafe { derive_field_mask(ciphertext_shared_secret, i as u32) };
ciphertext[1 + i] += mask;
// Pad with random fields so that padding is indistinguishable from masked data fields.
for i in (1 + message_bytes_as_fields.len())..MESSAGE_CIPHERTEXT_LEN {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is why let mut offset was useful 🫠

for i in (1 + message_bytes_as_fields.len())..MESSAGE_CIPHERTEXT_LEN {
// Safety: we assume that the sender wants for the message to be private - a malicious one could simply
// reveal its contents publicly. It is therefore fine to trust the sender to provide random padding.
ciphertext[i] = unsafe { random() };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See how scary it seems that here we do ciphertext[i] but above we did [i + 1]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wasn't that scared to be honest 😅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

``
for i in 0..len
ciphertext[offset + i]


😭 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was literally like that before my PR 😅


/// Removes the Poseidon2-derived mask from a ciphertext field. Returns the unmasked value if it fits in 31 bytes
/// (a content field), or zero if it doesn't (random padding).
fn unmask_field(shared_secret: Point, index: u32, masked: Field) -> Field {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this unconstrained (so that we dont accidentally call it in constranined fns) and have it return Option<Field>, and then explicitly above do unmask_field(...).unwrap_or(0), instead of hiding how we handle this here. Handling the failure to unmask above by explaining that those are fields that we'll probably ignore anyway is easier to follow/audit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, that is much better

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

embrace monads

@nchamo nchamo enabled auto-merge (squash) March 4, 2026 20:50
@nchamo nchamo merged commit 80ecd98 into merge-train/fairies Mar 4, 2026
16 of 20 checks passed
@nchamo nchamo deleted the feat/mask-all-ciphertext-fields branch March 4, 2026 21:08
@AztecBot
Copy link
Collaborator

AztecBot commented Mar 4, 2026

❌ Failed to cherry-pick to v4 due to conflicts. Dispatching ClaudeBox to resolve. View backport run.

github-merge-queue bot pushed a commit that referenced this pull request Mar 5, 2026
BEGIN_COMMIT_OVERRIDE
chore: remove prefund env var in non local networks (#21095)
feat: aztec new supporting multiple contract crates (#21007)
feat!: Expose offchain effects when simulating/sending txs (#20563)
chore: exclude auto-generated dirs from VS Code search (#20881)
feat: improve oracle name prefixes (#21101)
fix(pxe): correct contract class log DA gas metering from +2 to +1
(#21102)
chore: remove stale aes comments (#21133)
chore: add warning on invalid recipients (#21134)
feat: mask ciphertext fields with Poseidon2-derived values (#21009)
END_COMMIT_OVERRIDE
ludamad added a commit that referenced this pull request Mar 10, 2026
BEGIN_COMMIT_OVERRIDE
chore: chonk proof compression poc (#20645)
feat: Update L1 to L2 message APIs (#20913)
fix: adapt chonk proof compression for v4 Translator layout (#21067)
fix: omit bigint priceBumpPercentage from IPC config in testbench worker
(#21086)
feat: standby mode for prover broker (#21098)
fix(p2p): remove default block handler in favor of block handler
(#21105)
chore: prepare barretenberg-rs for crates.io publishing (#20496)
feat: reenable function selectors + additional validation in public
setup allowlist (backport #20909, #21122) (#21129)
chore: remove stale aes comments (#21133)
chore: remove auto-tag job (#21127)
feat: calldata length validation of public setup function allowlist
(#21139)
feat: run AVM NAPI simulations on dedicated threads instead of libuv
pool (#21138)
feat: Remove non-protocol contracts from public setup allowlist (#21154)
feat!: Expose offchain effects when simulating/sending txs (backport
#20563) (#21110)
chore: bump minor version (#21171)
chore: backport #21161 (tally slashing pruning improvements) to v4
(#21166)
chore: More updated Alpha configuration (backport #21155) (#21165)
fix(p2p): report most severe failure in runValidations (#21185)
feat: add ergonomic conversions for Noir's `Option<T>` (#21107)
docs: clarifying Noir fields vs struct fields in event metadata (#21172)
fix: bump lighthouse consensus client v7.1.0 -> v8.0.1 (#21170)
fix: update dependencies (#20997)
chore: New alpha-net environment (#20800) (#21202)
chore: code decuplication + refactor (public setup allowlist) (#21200)
feat: mask all ciphertext fields with Poseidon2-derived values (backport
#21009) (#21140)
chore: disable sponsored FPC in testnet (#21235)
feat!: exposing pub event pagination on wallet (#21197)
refactor(pxe): narrow tryGetPublicKeysAndPartialAddress return type
(backport #21208) (#21236)
feat: orchestrator enqueues via serial queue (#21247)
feat: rollup mana limit gas validation (#21219)
chore: deploy SPONSORED_FPC in test networks (#21254)
fix(sequencer): fix log when not enough txs (#21297)
END_COMMIT_OVERRIDE

---------

Co-authored-by: ledwards2225 <ledwards2225@users.noreply.github.com>
Co-authored-by: PhilWindle <PhilWindle@users.noreply.github.com>
Co-authored-by: ludamad <adam.domurad@gmail.com>
Co-authored-by: mrzeszutko <mrzeszutko@users.noreply.github.com>
Co-authored-by: spalladino <spalladino@users.noreply.github.com>
Co-authored-by: johnathan79717 <johnathan79717@users.noreply.github.com>
Co-authored-by: nventuro <nventuro@users.noreply.github.com>
Co-authored-by: alexghr <alexghr@users.noreply.github.com>
Co-authored-by: AztecBot <AztecBot@users.noreply.github.com>
Co-authored-by: Martin Verzilli <martin@aztec-labs.com>
Co-authored-by: PhilWindle <60546371+PhilWindle@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: mverzilli <mverzilli@users.noreply.github.com>
Co-authored-by: benesjan <benesjan@users.noreply.github.com>
Co-authored-by: danielntmd <danielntmd@users.noreply.github.com>
Co-authored-by: deffrian <deffrian@users.noreply.github.com>
Co-authored-by: benesjan <janbenes1234@gmail.com>
ludamad added a commit that referenced this pull request Mar 11, 2026
BEGIN_COMMIT_OVERRIDE
chore: chonk proof compression poc (#20645)
feat: Update L1 to L2 message APIs (#20913)
fix: adapt chonk proof compression for v4 Translator layout (#21067)
fix: omit bigint priceBumpPercentage from IPC config in testbench worker
(#21086)
feat: standby mode for prover broker (#21098)
fix(p2p): remove default block handler in favor of block handler
(#21105)
chore: prepare barretenberg-rs for crates.io publishing (#20496)
feat: reenable function selectors + additional validation in public
setup allowlist (backport #20909, #21122) (#21129)
chore: remove stale aes comments (#21133)
chore: remove auto-tag job (#21127)
feat: calldata length validation of public setup function allowlist
(#21139)
feat: run AVM NAPI simulations on dedicated threads instead of libuv
pool (#21138)
feat: Remove non-protocol contracts from public setup allowlist (#21154)
feat!: Expose offchain effects when simulating/sending txs (backport
#20563) (#21110)
chore: bump minor version (#21171)
chore: backport #21161 (tally slashing pruning improvements) to v4
(#21166)
chore: More updated Alpha configuration (backport #21155) (#21165)
fix(p2p): report most severe failure in runValidations (#21185)
feat: add ergonomic conversions for Noir's `Option<T>` (#21107)
docs: clarifying Noir fields vs struct fields in event metadata (#21172)
fix: bump lighthouse consensus client v7.1.0 -> v8.0.1 (#21170)
fix: update dependencies (#20997)
chore: New alpha-net environment (#20800) (#21202)
chore: code decuplication + refactor (public setup allowlist) (#21200)
feat: mask all ciphertext fields with Poseidon2-derived values (backport
#21009) (#21140)
chore: disable sponsored FPC in testnet (#21235)
feat!: exposing pub event pagination on wallet (#21197)
refactor(pxe): narrow tryGetPublicKeysAndPartialAddress return type
(backport #21208) (#21236)
feat: orchestrator enqueues via serial queue (#21247)
feat: rollup mana limit gas validation (#21219)
chore: deploy SPONSORED_FPC in test networks (#21254)
fix(sequencer): fix log when not enough txs (#21297)
fix: Simulate gas in n tps test. Set min txs per block to 1 (backport
#21312) (#21329)
fix(log): do not log validation error if unregistered handler (#21111)
fix(node): fix index misalignment in findLeavesIndexes (#21327)
fix: limit parallel blocks in prover to max AVM parallel simulations
(#21320)
fix: use native sha256 to speed up proving job id generation (#21292)
fix(validator): wait for l1 sync before processing block proposals
(#21336)
fix(txpool): cap priority fee with max fees when computing priority
(#21279)
chore: reduce severity of errors due to HA node not acquiring signature
(#21311)
fix: (A-643) add buffer to maxFeePerBlobGas for gas estimation and fix
bump loop truncation (#21323)
END_COMMIT_OVERRIDE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-to-v4 ci-no-fail-fast Sets NO_FAIL_FAST in the CI so the run is not aborted on the first failure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants