fix: handle empty or malformed logs during message processing#21192
fix: handle empty or malformed logs during message processing#21192nchamo wants to merge 5 commits intomerge-train/fairiesfrom
Conversation
| /// order to perform validation, store results, etc. For example, messages containing notes require knowledge of note | ||
| /// hashes and the first nullifier in order to find the note's nonce. | ||
| #[derive(Deserialize, Eq)] | ||
| #[derive(Deserialize, Eq, Serialize)] |
There was a problem hiding this comment.
Needed for the do_sync_state test
| #[derive(Deserialize, Eq)] | ||
| #[derive(Deserialize, Eq, Serialize)] | ||
| pub(crate) struct PendingTaggedLog { | ||
| pub log: BoundedVec<Field, PRIVATE_LOG_SIZE_IN_FIELDS>, |
There was a problem hiding this comment.
Needed for the do_sync_state test
8d1b7b8 to
ba3b667
Compare
| recipient: AztecAddress, | ||
| ) -> Option<BoundedVec<Field, MESSAGE_PLAINTEXT_LEN>> { | ||
| let eph_pk_x = ciphertext.get(0); | ||
| // Extract the ephemeral public key x-coordinate and masked fields, returning None for empty ciphertext. |
There was a problem hiding this comment.
Since Noir lacks early return, I though the best way to make this clear as possible was to fully embrace Option. Open to other suggestions though
There was a problem hiding this comment.
It's a bit annoying that we end up in nested-hell, but it is the cleanest way of handling this I think.
| } | ||
|
|
||
| #[test] | ||
| unconstrained fn decrypt_returns_none_on_empty_ciphertext() { |
There was a problem hiding this comment.
All these scenarios used to cause a panic
| for j in 0..32 { | ||
| let next_byte = bytes.get(i * 32 + j); | ||
| field = field * 256 + next_byte as Field; | ||
| try_fields_from_bytes(bytes).expect(f"Value does not fit in field") |
There was a problem hiding this comment.
I'm now using try_fields_from_bytes to avoid the repeated logic, but let me know if you think the two should coexist (maybe as a form of optimization?)
There was a problem hiding this comment.
But expect can fail, no? So if the bytes cannot be put into fields we'll panic. Is this not an issue?
There was a problem hiding this comment.
Sorry I wasn't clear. Both functions can co-exists. I didn't delete fields_from_bytes because it was public and maybe someone users are using it
My comment tried (and failed) to imply that having fields_from_bytes use try_fields_from_bytes might not be the most optimized solution, since we do things a little differently to handle the Option instead of panicking. It should be negligible, but I wanted to double check
noir-projects/aztec-nr/aztec/src/utils/conversion/fields_to_bytes.nr
Outdated
Show resolved
Hide resolved
| capsules::store(contract_address, base_slot, 1 as Field); | ||
| capsules::store( | ||
| contract_address, | ||
| base_slot + 1, | ||
| PendingTaggedLog { log: BoundedVec::new(), context: std::mem::zeroed() }, | ||
| ); | ||
|
|
||
| let logs: CapsuleArray<PendingTaggedLog> = CapsuleArray::at(contract_address, base_slot); | ||
| assert_eq(logs.len(), 1); |
There was a problem hiding this comment.
Instead of doing things this way, just create the capsule array and push into it.
| let no_handler: Option<CustomMessageHandler<()>> = Option::none(); | ||
| do_sync_state(contract_address, dummy_compute_nhnn, no_handler); |
There was a problem hiding this comment.
| let no_handler: Option<CustomMessageHandler<()>> = Option::none(); | |
| do_sync_state(contract_address, dummy_compute_nhnn, no_handler); | |
| do_sync_state(contract_address, dummy_compute_nhnn, Option::none()); |
This way you don't need to write the type.
There was a problem hiding this comment.
That didn't work for me. I think Noir needs me to specify what the type for CustomMessageHandler is, even if Option is empty. It can infer it
| use crate::protocol::address::AztecAddress; | ||
|
|
||
| #[test] | ||
| unconstrained fn do_sync_state_can_handle_empty_logs() { |
There was a problem hiding this comment.
| unconstrained fn do_sync_state_can_handle_empty_logs() { | |
| unconstrained fn do_sync_state_does_not_panic_on_empty_logs() { |
| recipient: AztecAddress, | ||
| ) -> Option<BoundedVec<Field, MESSAGE_PLAINTEXT_LEN>> { | ||
| let eph_pk_x = ciphertext.get(0); | ||
| // Extract the ephemeral public key x-coordinate and masked fields, returning None for empty ciphertext. |
There was a problem hiding this comment.
It's a bit annoying that we end up in nested-hell, but it is the cleanest way of handling this I think.
| ) -> Option<BoundedVec<Field, MESSAGE_PLAINTEXT_LEN>> { | ||
| let eph_pk_x = ciphertext.get(0); | ||
| // Extract the ephemeral public key x-coordinate and masked fields, returning None for empty ciphertext. | ||
| split_ciphertext(ciphertext).and_then(|(eph_pk_x, masked_fields)| { |
There was a problem hiding this comment.
What if this were a lambda?
|ciphertext| {
if ciphertext.len() > 0 {
Option::some(
(ciphertext.get(0), array::subbvec(ciphertext, EPH_PK_X_SIZE_IN_FIELDS)),
)
} else {
Option::none()
}
}.and_then(|(eph_pk_x, masked_fields)| {| header_bytes[0] = 0xFF; | ||
| header_bytes[1] = 0xFF; |
There was a problem hiding this comment.
Better to have the len be MESSAGE_PLAINTEXT_SIZE_IN_BYTES+1, so that we make sure we're handling the edge case correctly.
| for j in 0..32 { | ||
| let next_byte = bytes.get(i * 32 + j); | ||
| field = field * 256 + next_byte as Field; | ||
| try_fields_from_bytes(bytes).expect(f"Value does not fit in field") |
There was a problem hiding this comment.
But expect can fail, no? So if the bytes cannot be put into fields we'll panic. Is this not an issue?
| ok = true; | ||
| /// Non-panicking version of `fields_from_bytes`. Returns `Option::none()` if the input | ||
| /// length is not a multiple of 32 or if any 32-byte chunk exceeds the field modulus. | ||
| pub fn try_fields_from_bytes<let N: u32>(bytes: BoundedVec<u8, N>) -> Option<BoundedVec<Field, N / 32>> { |
There was a problem hiding this comment.
| pub fn try_fields_from_bytes<let N: u32>(bytes: BoundedVec<u8, N>) -> Option<BoundedVec<Field, N / 32>> { | |
| fn try_fields_from_bytes<let N: u32>(bytes: BoundedVec<u8, N>) -> Option<BoundedVec<Field, N / 32>> { |
Let's keep API surface small for now.
| if bytes.len() % 32 == 0 { | ||
| let mut fields = BoundedVec::new(); | ||
| let p = std::field::modulus_be_bytes(); | ||
| let mut valid = true; |
There was a problem hiding this comment.
Might read easier if extracting this body into a fn. Typically helps with nested options etc.
| // 0xFF repeated 32 times is larger than the BN254 field modulus. | ||
| let input = BoundedVec::<_, 32>::from_array([0xFF as u8; 32]); |
There was a problem hiding this comment.
Better to test with p directly to check the edge case.
|
Since Nico went through this and the feedback was not yet addressed here I don't think my review is necessary at this point. @nchamo please request review again from me if you would like me to chime in. |
Problem
AES128::decryptinaes128.nrcurrently panics at 5 points when given malformed ciphertext or wrong-key data. During message discovery (do_sync_state), a panic indecryptcrashes the entire sync process rather than gracefully skipping the unprocessable log.Additionally,
do_sync_stateitself panics when it encounters empty logs, since it unconditionally indexes into the log without checking its length first.Panic points in
decryptciphertext.get(0)panics when theBoundedVecis empty.header_plaintext.get(0)/.get(1)panic when the AES decrypt oracle returns fewer than 2 bytes (e.g. wrong-key PKCS#7 stripping produces an empty result).BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length)panics when the 2-byte header decodes to a length exceedingMESSAGE_PLAINTEXT_SIZE_IN_BYTES(e.g. 65535 from corrupted data).fields_from_bytesassertsbytes.len() % 32 == 0, panicking when the decrypted body has a non-aligned length (e.g. 33 bytes).fields_from_bytesasserts each 32-byte chunk fits within the BN254 field modulus, panicking when decrypted bytes exceed it (e.g.0xFFrepeated 32 times).Panic in
do_sync_statedo_sync_stateprocesses every pending tagged log without validating its size. It would panic whenpending_tagged_log.log.get(0)was called on empty logsImportant for reviewers
I recommend using "hide whitespace" config when reviewing file changes, since it would make the changes easier to understand
Fixes F-356
Fixes F-191