From 1bb4d40251ee8617cd72efdc42b535d15a8a516d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 30 Apr 2026 14:17:41 -0700 Subject: [PATCH 01/22] feat(native-rust): message header inline structures (EDK, encryption context) serialization and tests --- esdk/src/message/encrypted_data_keys.rs | 198 +++++++ esdk/src/message/encryption_context.rs | 101 ++++ esdk/src/message/serializable_types.rs | 106 ++++ esdk/src/message/serialize_functions.rs | 192 +++++++ esdk/tests/test_encrypted_data_keys.rs | 661 ++++++++++++++++++++++ esdk/tests/test_encryption_context_aad.rs | 230 ++++++++ 6 files changed, 1488 insertions(+) create mode 100644 esdk/src/message/encrypted_data_keys.rs create mode 100644 esdk/src/message/encryption_context.rs create mode 100644 esdk/src/message/serializable_types.rs create mode 100644 esdk/src/message/serialize_functions.rs create mode 100644 esdk/tests/test_encrypted_data_keys.rs create mode 100644 esdk/tests/test_encryption_context_aad.rs diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs new file mode 100644 index 000000000..2ddd00c07 --- /dev/null +++ b/esdk/src/message/encrypted_data_keys.rs @@ -0,0 +1,198 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Encrypted data key serialization and deserialization. + +use super::serialize_functions::{read_seq_u16, read_str_u16, read_u16, write_bytes, write_u16}; +use super::{Error, ser_err}; +use crate::types::{SafeRead, SafeWrite}; +use aws_mpl_legacy::EncryptedDataKey; + +//= specification/client-apis/encrypt.md#v1-header +//# - MUST serialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). +//= specification/client-apis/encrypt.md#v2-header +//# - MUST serialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). +pub(crate) fn write_edks(w: &mut dyn SafeWrite, edks: &[EncryptedDataKey]) -> Result<(), Error> { + //= specification/data-format/message-header.md#encrypted-data-keys + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + let Ok(edk_count) = u16::try_from(edks.len()) else { + return ser_err("Count too large for UInt16"); + }; + + //= specification/data-format/message-header.md#encrypted-data-key-count + //# The length of the serialized encrypted data key count MUST be 2 bytes. + // + //= specification/data-format/message-header.md#encrypted-data-key-count + //# The encrypted data key count MUST be interpreted as a UInt16. + write_u16(w, edk_count)?; + + for edk in edks { + write_edk(w, edk)?; + } + Ok(()) +} + +pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result<(), Error> { + //= specification/data-format/message-header.md#encrypted-data-key-entries + //# Each Encrypted Data Key Entry MUST consist of, in order, + //# Key Provider ID Length, + //# Key Provider ID, + //# Key Provider Information Length, + //# Key Provider Information, + //# Encrypted Data Key Length, + //# and Encrypted Data Key. + let kp_id_bytes = edk.key_provider_id.as_bytes(); + + //= specification/data-format/message-header.md#key-provider-id-length + //# The key provider ID length MUST be interpreted as a UInt16. + let Ok(kp_id_len) = u16::try_from(kp_id_bytes.len()) else { + return ser_err("Key provider ID length too long for 16 bits"); + }; + + //= specification/data-format/message-header.md#key-provider-id-length + //# The length of the serialized key provider ID length field MUST be 2 bytes. + write_u16(w, kp_id_len)?; + + //= specification/data-format/message-header.md#key-provider-id + //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. + //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. + // + //= specification/data-format/message-header.md#key-provider-id + //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. + write_bytes(w, kp_id_bytes)?; + + //= specification/data-format/message-header.md#key-provider-information-length + //# The key provider information length MUST be interpreted as a UInt16. + let Ok(kp_info_len) = u16::try_from(edk.key_provider_info.len()) else { + return ser_err("Key provider info length too long for 16 bits"); + }; + + //= specification/data-format/message-header.md#key-provider-information-length + //# The length of the serialized key provider information length field MUST be 2 bytes. + write_u16(w, kp_info_len)?; + + //= specification/data-format/message-header.md#key-provider-information + //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. + //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. + // + //= specification/data-format/message-header.md#key-provider-information + //# The key provider information MUST be interpreted as bytes. + write_bytes(w, &edk.key_provider_info)?; + + //= specification/data-format/message-header.md#encrypted-data-key-length + //# The encrypted data key length MUST be interpreted as a UInt16. + let Ok(edk_len) = u16::try_from(edk.ciphertext.len()) else { + return ser_err("Encrypted data key length too long for 16 bits"); + }; + + //= specification/data-format/message-header.md#encrypted-data-key-length + //# The length of the serialized encrypted data key length field MUST be 2 bytes. + write_u16(w, edk_len)?; + + //= specification/data-format/message-header.md#encrypted-data-key + //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. + //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. + // + //= specification/data-format/message-header.md#encrypted-data-key + //# The encrypted data key MUST be interpreted as bytes. + write_bytes(w, &edk.ciphertext) +} + +//= specification/client-apis/decrypt.md#v1-header-deserialization +//# - MUST deserialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). +//= specification/client-apis/decrypt.md#v2-header-deserialization +//# - MUST deserialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). +pub(crate) fn read_edks( + r: &mut dyn SafeRead, + max_edks: Option, + raw: &mut dyn SafeWrite, +) -> Result, Error> { + //= specification/data-format/message-header.md#encrypted-data-keys + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + // + //= specification/data-format/message-header.md#encrypted-data-key-count + //# The length of the serialized encrypted data key count MUST be 2 bytes. + // + //= specification/data-format/message-header.md#encrypted-data-key-count + //# The encrypted data key count MUST be interpreted as a UInt16. + let count = read_u16(r, raw)?; + + if let Some(max_edks) = max_edks + && usize::from(count) > max_edks.get() + { + //= specification/client-apis/decrypt.md#v2-header-deserialization + //# If the number of [encrypted data keys](../framework/structures.md#encrypted-data-keys) + //# deserialized from the [message header](../data-format/message-header.md) + //# is greater than the [maximum number of encrypted data keys](client.md#maximum-number-of-encrypted-data-keys) configured in the [client](client.md), + //# then as soon as that can be determined during deserializing + //# decrypt MUST process no more bytes and yield an error. + return ser_err("Ciphertext encrypted data keys exceed maximum encrypted data keys limit"); + } + + let mut edks = Vec::with_capacity(usize::from(count)); + for _ in 0..count { + edks.push(read_edk(r, raw)?); + } + Ok(edks) +} + +pub(crate) fn read_edk( + r: &mut dyn SafeRead, + raw: &mut dyn SafeWrite, +) -> Result { + //= specification/data-format/message-header.md#encrypted-data-key-entries + //# Each Encrypted Data Key Entry MUST consist of, in order, + //# Key Provider ID Length, + //# Key Provider ID, + //# Key Provider Information Length, + //# Key Provider Information, + //# Encrypted Data Key Length, + //# and Encrypted Data Key. + // + //= specification/data-format/message-header.md#key-provider-id-length + //# The key provider ID length MUST be interpreted as a UInt16. + // + //= specification/data-format/message-header.md#key-provider-id-length + //# The length of the serialized key provider ID length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-id + //= reason=read_str_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. + //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. + // + //= specification/data-format/message-header.md#key-provider-id + //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. + let provider_id = read_str_u16(r, raw)?; + + //= specification/data-format/message-header.md#key-provider-information-length + //# The key provider information length MUST be interpreted as a UInt16. + // + //= specification/data-format/message-header.md#key-provider-information-length + //# The length of the serialized key provider information length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-information + //= reason=read_seq_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. + //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. + // + //= specification/data-format/message-header.md#key-provider-information + //# The key provider information MUST be interpreted as bytes. + let provider_info = read_seq_u16(r, raw)?; + + //= specification/data-format/message-header.md#encrypted-data-key-length + //# The encrypted data key length MUST be interpreted as a UInt16. + // + //= specification/data-format/message-header.md#encrypted-data-key-length + //# The length of the serialized encrypted data key length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#encrypted-data-key + //= reason=read_seq_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. + //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. + // + //= specification/data-format/message-header.md#encrypted-data-key + //# The encrypted data key MUST be interpreted as bytes. + let ciphertext = read_seq_u16(r, raw)?; + + Ok(EncryptedDataKey::new(provider_id, provider_info, ciphertext)) +} diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs new file mode 100644 index 000000000..f7bcde0bf --- /dev/null +++ b/esdk/src/message/encryption_context.rs @@ -0,0 +1,101 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Encryption context serialization for message header and AAD. + +use super::serializable_types::ESDKCanonicalEncryptionContext; +use super::serialize_functions::{read_str_u16, read_u16, write_bytes, write_u16}; +use super::{Error, ser_err}; +use crate::types::{SafeRead, SafeWrite}; + +pub(crate) fn read_canonical_ec( + r: &mut dyn SafeRead, + raw: &mut dyn SafeWrite, +) -> Result { + let bytes = usize::from(read_u16(r, raw)?); + if bytes == 0 { + return Ok(Vec::new()); + } + let count = usize::from(read_u16(r, raw)?); + let mut result: ESDKCanonicalEncryptionContext = Vec::with_capacity(count); + for _ in 0..count { + let key = read_str_u16(r, raw)?; + let value = read_str_u16(r, raw)?; + result.push((key, value)); + } + Ok(result) +} + +pub(crate) fn write_empty_ec_or_write_aad( + w: &mut dyn SafeWrite, + data: &ESDKCanonicalEncryptionContext, +) -> Result<(), Error> { + if data.is_empty() { + //= specification/data-format/message-header.md#key-value-pairs + //# When the [encryption context](../framework/structures.md#encryption-context) is empty, + //# this field MUST NOT be included in the [AAD](#aad). + Ok(()) + } else { + write_aad(w, data) + } +} + +fn get_length(data: &ESDKCanonicalEncryptionContext) -> usize { + let mut length = 0; + for pair in data { + length += 4 + pair.0.len() + pair.1.len(); + } + length +} + +pub(crate) fn write_aad_section( + w: &mut dyn SafeWrite, + data: &ESDKCanonicalEncryptionContext, +) -> Result<(), Error> { + //= specification/data-format/message-header.md#aad + //# The AAD MUST consist of, in order, + //# Key Value Pairs Length, + //# and Key Value Pairs. + if data.is_empty() { + //= specification/data-format/message-header.md#key-value-pairs-length + //# When the [encryption context](../framework/structures.md#encryption-context) is empty, the value of this field MUST be 0. + write_u16(w, 0)?; + return Ok(()); + } + let bytes = get_length(data); + + //= specification/data-format/message-header.md#key-value-pairs-length + //# The length of the serialized key value pairs length field MUST be 2 bytes. + + //= specification/data-format/message-header.md#key-value-pairs-length + //# The key value pairs length MUST be interpreted as a UInt16. + let Ok(bytes_u16) = u16::try_from(bytes) else { + return ser_err("value too large for u16"); + }; + write_u16(w, bytes_u16)?; + write_aad(w, data) +} + +pub(crate) fn write_aad( + w: &mut dyn SafeWrite, + data: &ESDKCanonicalEncryptionContext, +) -> Result<(), Error> { + let Ok(data_len) = u16::try_from(data.len()) else { + return ser_err("value too large for u16"); + }; + write_u16(w, data_len)?; + for pair in data { + //= specification/data-format/message-header.md#key-value-pairs + //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). + let Ok(key_len) = u16::try_from(pair.0.len()) else { + return ser_err("value too large for u16"); + }; + write_u16(w, key_len)?; + write_bytes(w, pair.0.as_bytes())?; + let Ok(val_len) = u16::try_from(pair.1.len()) else { + return ser_err("value too large for u16"); + }; + write_u16(w, val_len)?; + write_bytes(w, pair.1.as_bytes())?; + } + Ok(()) +} diff --git a/esdk/src/message/serializable_types.rs b/esdk/src/message/serializable_types.rs new file mode 100644 index 000000000..5432fceb5 --- /dev/null +++ b/esdk/src/message/serializable_types.rs @@ -0,0 +1,106 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Type aliases and helper functions for message serialization. + +use crate::types::EncryptionContext; +use aws_mpl_legacy::EncryptedDataKey; +use aws_mpl_legacy::suites::AlgorithmSuite; + +pub(crate) type ESDKEncryptionContext = EncryptionContext; +pub(crate) type ESDKEncryptionContextPair = (String, String); +pub(crate) type ESDKCanonicalEncryptionContext = Vec; + +const ESDK_CANONICAL_ENCRYPTION_CONTEXT_MAX_LENGTH: u64 = u16::MAX as u64 - 2; + +pub(crate) const fn get_iv_length(a: &AlgorithmSuite) -> u8 { + match a.encrypt { + aws_mpl_legacy::suites::Encrypt::AesGcm(_e) => 12, + _ => 0, + } +} + +pub(crate) const fn get_tag_length(a: &AlgorithmSuite) -> u8 { + match a.encrypt { + aws_mpl_legacy::suites::Encrypt::AesGcm(_e) => 16, + _ => 0, + } +} + +pub(crate) const fn get_encrypt_key_length(a: &AlgorithmSuite) -> u8 { + match a.encrypt { + aws_mpl_legacy::suites::Encrypt::AesGcm(e) => e.key_len(), + _ => 0, + } +} + +// Length properties of the Encryption Context. +// The Encryption Context has a complex relationship with length. +// Each key or value MUST be less than Uint16, +// However the entire thing MUST also serialize to less than Uint16. +// In practice, this means than the longest value, +// given a key of 1 bytes is Uint16-2-2-1. +// e.g. +// 2 for the key length +// 1 for the key data +// 2 for the value length +// Uint16-2-2-1 for the value data + +pub(crate) fn length(encryption_context: &ESDKEncryptionContext) -> u64 { + let mut length: usize = 0; + for (key, value) in encryption_context { + length += 2 + key.len() + 2 + value.len(); + } + length as u64 +} + +pub(crate) fn to_canonical_pairs( + encryption_context: ESDKEncryptionContext, +) -> ESDKCanonicalEncryptionContext { + let mut pairs: Vec<(String, String)> = encryption_context.into_iter().collect(); + pairs.sort_by(|a, b| a.0.cmp(&b.0)); + pairs +} + +pub(crate) fn from_canonical_pairs(pairs: ESDKCanonicalEncryptionContext) -> ESDKEncryptionContext { + let mut map: ESDKEncryptionContext = ESDKEncryptionContext::new(); + for (key, value) in pairs { + map.insert(key, value); + } + map +} + +pub(crate) fn is_esdk_encryption_context(ec: &EncryptionContext) -> bool { + if ec.len() >= usize::from(u16::MAX) { + return false; + } + if length(ec) >= ESDK_CANONICAL_ENCRYPTION_CONTEXT_MAX_LENGTH { + return false; + } + for (key, value) in ec { + if key.len() >= usize::from(u16::MAX) { + return false; + } + if value.len() >= usize::from(u16::MAX) { + return false; + } + } + true +} + +pub(crate) fn is_esdk_encrypted_data_key(edk: &EncryptedDataKey) -> bool { + u16::try_from(edk.key_provider_id.len()).is_ok() + && u16::try_from(edk.key_provider_info.len()).is_ok() + && u16::try_from(edk.ciphertext.len()).is_ok() +} + +pub(crate) fn is_esdk_encrypted_data_keys(edks: &[EncryptedDataKey]) -> bool { + if edks.len() >= usize::from(u16::MAX) { + return false; + } + for edk in edks { + if !is_esdk_encrypted_data_key(edk) { + return false; + } + } + true +} diff --git a/esdk/src/message/serialize_functions.rs b/esdk/src/message/serialize_functions.rs new file mode 100644 index 000000000..51d3e70a3 --- /dev/null +++ b/esdk/src/message/serialize_functions.rs @@ -0,0 +1,192 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Low-level byte read/write primitives for message serialization. + +use super::{Error, ser_err}; +use crate::error::ErrorKind; +use crate::types::{SafeRead, SafeWrite}; +use std::backtrace::Backtrace; +use std::sync::Arc; + +#[track_caller] +fn ser_io(e: std::io::Error) -> Error { + match e.kind() { + std::io::ErrorKind::UnexpectedEof => Error { + kind: ErrorKind::SerializationError, + message: "Unexpected end of data".into(), + cause: Some(Arc::new(e)), + backtrace: Arc::new(Backtrace::capture()), + }, + _ => Error { + kind: ErrorKind::SerializationError, + message: "IO Error".into(), + cause: Some(Arc::new(e)), + backtrace: Arc::new(Backtrace::capture()), + }, + } +} + +pub(crate) fn read_up_to(this: &mut dyn SafeRead, buf: &mut [u8]) -> Result { + let mut curr: usize = 0; + loop { + match this.read(&mut buf[curr..]) { + Ok(0) => { + return Ok(curr); + } + Ok(n) => { + curr += n; + if curr == buf.len() { + return Ok(curr); + } + } + // Err(ref e) if e.is_interrupted() => {} + Err(e) => return Err(ser_io(e)), + } + } +} + +pub(crate) fn read_up_to_peek( + this: &mut dyn SafeRead, + buf: &mut [u8], + first: Option, +) -> Result { + if buf.is_empty() { + return Ok(0); + } + match first { + Some(f) => { + buf[0] = f; + match read_up_to(this, &mut buf[1..]) { + Ok(n) => Ok(n + 1), + Err(e) => Err(e), + } + } + None => read_up_to(this, buf), + } +} + +#[track_caller] +fn ser_utf8(item: std::string::FromUtf8Error) -> Error { + Error { + kind: ErrorKind::SerializationError, + message: "UTF8 Decode Error".into(), + cause: Some(Arc::new(item)), + backtrace: Arc::new(Backtrace::capture()), + } +} + +pub(crate) fn write_bytes(w: &mut dyn SafeWrite, data: &[u8]) -> Result<(), Error> { + w.write_all(data).map_err(ser_io)?; + Ok(()) +} + +pub(crate) fn write_u8(w: &mut dyn SafeWrite, data: u8) -> Result<(), Error> { + write_bytes(w, &[data]) +} +pub(crate) fn write_u16(w: &mut dyn SafeWrite, data: u16) -> Result<(), Error> { + write_bytes(w, &data.to_be_bytes()) +} +pub(crate) fn write_u32(w: &mut dyn SafeWrite, data: u32) -> Result<(), Error> { + write_bytes(w, &data.to_be_bytes()) +} + +pub(crate) fn read_bytes( + r: &mut dyn SafeRead, + buf: &mut [u8], + raw: &mut dyn SafeWrite, +) -> Result<(), Error> { + r.read_exact(buf).map_err(ser_io)?; + write_bytes(raw, buf) +} + +pub(crate) fn read_vec( + r: &mut dyn SafeRead, + length: usize, + raw: &mut dyn SafeWrite, +) -> Result, Error> { + let mut result = vec![0; length]; + read_bytes(r, &mut result, raw)?; + Ok(result) +} + +pub(crate) fn read_u8(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { + let mut result = [0u8; 1]; + read_bytes(r, &mut result, raw)?; + Ok(result[0]) +} + +pub(crate) fn read_opt_u8(r: &mut dyn SafeRead) -> Result, Error> { + let mut result = [0u8; 1]; + match r.read_exact(&mut result) { + Ok(()) => Ok(Some(result[0])), + Err(e) => match e.kind() { + std::io::ErrorKind::UnexpectedEof => Ok(None), + _ => Err(ser_io(e)), + }, + } +} + +pub(crate) fn read_u16(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { + let mut result = [0u8; 2]; + read_bytes(r, &mut result, raw)?; + Ok(u16::from_be_bytes(result)) +} +pub(crate) fn read_u32(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { + let mut result = [0u8; 4]; + read_bytes(r, &mut result, raw)?; + Ok(u32::from_be_bytes(result)) +} +pub(crate) fn read_u64(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { + let mut result = [0u8; 8]; + read_bytes(r, &mut result, raw)?; + Ok(u64::from_be_bytes(result)) +} + +pub(crate) fn read_seq_u16( + r: &mut dyn SafeRead, + raw: &mut dyn SafeWrite, +) -> Result, Error> { + let len = read_u16(r, raw)?; + read_vec(r, usize::from(len), raw) +} + +pub(crate) fn read_seq_u32_bounded( + r: &mut dyn SafeRead, + bound: u32, + msg: &str, + data: &mut Vec, + raw: &mut dyn SafeWrite, +) -> Result<(), Error> { + let len = read_u32(r, raw)?; + if len > bound { + return ser_err(msg); + } + let Ok(len_usize) = usize::try_from(len) else { + return ser_err("length too large for platform"); + }; + data.resize(len_usize, 0); + read_bytes(r, &mut data[..], raw) +} + +pub(crate) fn read_seq_u64_bounded( + r: &mut dyn SafeRead, + bound: u64, + msg: &str, + raw: &mut dyn SafeWrite, +) -> Result, Error> { + let len = read_u64(r, raw)?; + if len > bound { + return ser_err(msg); + } + let Ok(len_usize) = usize::try_from(len) else { + return ser_err("length too large for platform"); + }; + read_vec(r, len_usize, raw) +} + +pub(crate) fn read_str_u16(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { + let len = read_u16(r, raw)?; + let result = read_vec(r, usize::from(len), raw)?; + let result = String::from_utf8(result).map_err(ser_utf8)?; + Ok(result) +} diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs new file mode 100644 index 000000000..3deb47157 --- /dev/null +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -0,0 +1,661 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tests for the Encrypted Data Keys sections of specification/data-format/message-header.md + +mod fixtures; +mod test_helpers; + +use aws_esdk::*; +use aws_mpl_legacy::commitment::EsdkCommitmentPolicy; +use fixtures::*; +use test_helpers::*; + +#[tokio::test(flavor = "multi_thread")] +async fn test_encrypted_data_keys_ordering() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + //= specification/data-format/message-header.md#encrypted-data-keys + //= type=test + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + let ct = encrypt_with_version(b"ordering test", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + // Count field comes first, then entries follow immediately + assert_eq!(parsed.edk_count, 1); + assert_eq!(parsed.edks.len(), 1); + // The entries start at edk_count_offset + 2 (right after the 2-byte count) + let entries_start = parsed.edk_count_offset + 2; + // First entry's provider_id_len is at entries_start + let first_pid_len = u16::from_be_bytes([ct[entries_start], ct[entries_start + 1]]); + assert_eq!( + first_pid_len, parsed.edks[0].provider_id_len, + "{version:?}: EDK entries must immediately follow the count field" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_field_is_2_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"count 2 bytes", version, keyring.clone()).await; + let offset = skip_to_edk_section(&ct, version); + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //# The length of the serialized encrypted data key count MUST be 2 bytes. + // The count occupies exactly bytes [offset] and [offset+1] + let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); + assert_eq!( + count, 1, + "{version:?}: single keyring produces exactly 1 EDK" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_interpreted_as_uint16() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"uint16 count", version, mk.clone()).await; + let offset = skip_to_edk_section(&ct, version); + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //# The encrypted data key count MUST be interpreted as a UInt16. + // Big-endian UInt16: high byte should be 0, low byte should be 2 + assert_eq!( + ct[offset], 0x00, + "{version:?}: high byte of UInt16 count must be 0 for small counts" + ); + assert_eq!( + ct[offset + 1], + 0x02, + "{version:?}: low byte of UInt16 count must be 2 for two keyrings" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_zero_rejected_on_decrypt() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let mut ct = encrypt_with_version(b"zero count", version, keyring.clone()).await; + let offset = skip_to_edk_section(&ct, version); + // Tamper: set count to 0 + ct[offset] = 0x00; + ct[offset + 1] = 0x00; + let mut dec = + DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), keyring.clone()); + if let Version::V1 = version { + dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //= reason=Tampering the count to 0 and verifying decrypt rejects it proves the >0 constraint is enforced on the deserialization path. + //# This value MUST be greater than 0. + let err = decrypt(&dec) + .await + .expect_err(&format!("{version:?}: decrypt must reject EDK count of 0")); + // Setting count=0 corrupts the header structure: either the count=0 check fires, or + // subsequent bytes are misinterpreted by the parser. Both outcomes are valid structural + // rejections — assert this is a SerializationError (parse/structural failure), not a + // generic unrelated failure. + assert!( + matches!(err.kind, aws_esdk::ErrorKind::SerializationError) + || err.message.to_lowercase().contains("empty") + || err.message.to_lowercase().contains("encrypted data key"), + "{version:?}: error must indicate a structural/EDK failure, got: {} ({:?})", + err.message, err.kind + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_max_enforcement_encrypt() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //= reason=max_encrypted_data_keys on encrypt enforces the upper bound on EDK count before serialization. + //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. + let mut input = + EncryptInput::with_legacy_keyring(b"max edk encrypt", EncryptionContext::new(), mk); + input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); + let err = encrypt(&input).await.expect_err("encrypt must fail when EDK count exceeds max"); + assert!( + err.message.contains("exceed") && err.message.contains("maximum"), + "error must indicate EDK count exceeds maximum, got: {} ({:?})", + err.message, err.kind + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_max_enforcement_decrypt() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + let ct = encrypt_with_version(b"max edk decrypt", Version::V2, mk.clone()).await; + let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), mk); + dec.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //= reason=max_encrypted_data_keys on decrypt enforces the upper bound when deserializing the header. + //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. + let err = decrypt(&dec).await.expect_err("decrypt must fail when EDK count exceeds max"); + assert!( + err.message.contains("exceed") && err.message.contains("maximum"), + "error must indicate EDK count exceeds maximum, got: {} ({:?})", + err.message, err.kind + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_at_max_succeeds() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //= reason=Setting max equal to actual count verifies the less-than-or-equal semantics. + //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. + let mut input = + EncryptInput::with_legacy_keyring(b"at max", EncryptionContext::new(), mk); + input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(2).unwrap()); + assert!( + encrypt(&input).await.is_ok(), + "encrypt must succeed when EDK count equals max" + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_entry_field_order() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"entry order", version, keyring.clone()).await; + let edk_start = skip_to_edk_section(&ct, version) + 2; // skip count + let mut pos = edk_start; + + //= specification/data-format/message-header.md#encrypted-data-key-entries + //= type=test + //# Each Encrypted Data Key Entry MUST consist of, in order, + //# Key Provider ID Length, + //# Key Provider ID, + //# Key Provider Information Length, + //# Key Provider Information, + //# Encrypted Data Key Length, + //# and Encrypted Data Key. + + // 1. Key Provider ID Length (2 bytes) + let pid_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]); + pos += 2; + assert!(pid_len > 0, "{version:?}: provider ID length must be positive"); + + // 2. Key Provider ID (pid_len bytes) + let pid = &ct[pos..pos + pid_len as usize]; + let pid_str = std::str::from_utf8(pid).expect("provider ID must be valid UTF-8"); + let (expected_ns, _) = namespace_and_name(0); + assert_eq!( + pid_str, expected_ns, + "{version:?}: provider ID must match keyring namespace" + ); + pos += pid_len as usize; + + // 3. Key Provider Information Length (2 bytes) + let pinfo_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]); + pos += 2; + + // 4. Key Provider Information (pinfo_len bytes) + let _pinfo = &ct[pos..pos + pinfo_len as usize]; + pos += pinfo_len as usize; + + // 5. Encrypted Data Key Length (2 bytes) + let edk_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]); + pos += 2; + assert!( + edk_len > 0, + "{version:?}: encrypted data key length must be positive" + ); + + // 6. Encrypted Data Key (edk_len bytes) + let _edk = &ct[pos..pos + edk_len as usize]; + pos += edk_len as usize; + + // Verify we consumed exactly one entry and the position matches the parser + let parsed = parse_edk_section(&ct, version); + assert_eq!( + pos, parsed.end_offset, + "{version:?}: manual walk must match parser end offset" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_count_matches_entries() { + let generator = aes_keyring(0).await; + let c1 = aes_keyring(1).await; + let c2 = aes_keyring(2).await; + let mk = multi_keyring(generator, vec![c1, c2]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"3 edks", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#encrypted-data-keys + //= type=test + //= reason=Using a multi-keyring with 3 keyrings verifies the serialized count matches the number of entries that follow, covering the "Count, then Entries" structure. + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + assert_eq!( + parsed.edk_count, 3, + "{version:?}: multi-keyring with 3 keyrings must produce 3 EDKs" + ); + assert_eq!( + parsed.edks.len(), + 3, + "{version:?}: parsed entries must match count" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_entries_preserve_keyring_order() { + let generator = aes_keyring(0).await; + let c1 = aes_keyring(1).await; + let c2 = aes_keyring(2).await; + let mk = multi_keyring(generator, vec![c1, c2]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"order check", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#encrypted-data-keys + //= type=test + //= reason=Verifying that EDK provider IDs appear in generator-then-children order proves entries are serialized in the order they appear in the encryption materials, exercising the "Entries" component of the Count+Entries structure. + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + for (i, edk) in parsed.edks.iter().enumerate() { + let pid_str = std::str::from_utf8(&edk.provider_id).unwrap(); + let (expected_ns, _) = namespace_and_name(i as u8); + assert_eq!( + pid_str, expected_ns, + "{version:?}: EDK {i} provider ID must match keyring {i} namespace" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_id_length_is_2_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pid len 2 bytes", version, keyring.clone()).await; + let edk_start = skip_to_edk_section(&ct, version) + 2; + + //= specification/data-format/message-header.md#key-provider-id-length + //= type=test + //# The length of the serialized key provider ID length field MUST be 2 bytes. + // The first 2 bytes of the entry are the provider ID length field + let pid_len_bytes = &ct[edk_start..edk_start + 2]; + assert_eq!( + pid_len_bytes.len(), + 2, + "{version:?}: key provider ID length field must be exactly 2 bytes" + ); + let pid_len = u16::from_be_bytes([pid_len_bytes[0], pid_len_bytes[1]]); + let (expected_ns, _) = namespace_and_name(0); + assert_eq!( + pid_len as usize, + expected_ns.len(), + "{version:?}: provider ID length must equal the namespace string length" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_id_length_interpreted_as_uint16() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pid len uint16", version, keyring.clone()).await; + let edk_start = skip_to_edk_section(&ct, version) + 2; + let (expected_ns, _) = namespace_and_name(0); + let expected_len = expected_ns.len() as u16; + + //= specification/data-format/message-header.md#key-provider-id-length + //= type=test + //# The key provider ID length MUST be interpreted as a UInt16. + // Verify big-endian UInt16 encoding + assert_eq!( + ct[edk_start], + (expected_len >> 8) as u8, + "{version:?}: high byte of UInt16 provider ID length" + ); + assert_eq!( + ct[edk_start + 1], + (expected_len & 0xFF) as u8, + "{version:?}: low byte of UInt16 provider ID length" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_id_length_matches_field() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pid len match", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#key-provider-id + //= type=test + //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. + for edk in &parsed.edks { + assert_eq!( + edk.provider_id.len(), + edk.provider_id_len as usize, + "{version:?}: provider ID byte length must equal the provider ID length field" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_id_is_utf8() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pid utf8", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#key-provider-id + //= type=test + //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. + for (i, edk) in parsed.edks.iter().enumerate() { + let pid_str = + std::str::from_utf8(&edk.provider_id).expect("provider ID must be valid UTF-8"); + let (expected_ns, _) = namespace_and_name(i as u8); + assert_eq!( + pid_str, expected_ns, + "{version:?}: provider ID must be the keyring namespace as UTF-8" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_info_length_is_2_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pinfo len 2 bytes", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + // The provider info length was parsed as 2 bytes by our parser. + // Verify it's consistent by checking the raw bytes at the expected offset. + let edk_start = parsed.edk_count_offset + 2; + let pid_len = parsed.edks[0].provider_id_len as usize; + let pinfo_len_offset = edk_start + 2 + pid_len; // skip pid_len field + pid bytes + + //= specification/data-format/message-header.md#key-provider-information-length + //= type=test + //# The length of the serialized key provider information length field MUST be 2 bytes. + let pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); + assert_eq!( + pinfo_len, parsed.edks[0].provider_info_len, + "{version:?}: provider info length field must be 2 bytes wide" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_info_length_interpreted_as_uint16() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pinfo len uint16", version, keyring.clone()).await; + let edk_start = skip_to_edk_section(&ct, version) + 2; + // Walk the wire bytes: provider_id_len (2) + provider_id (pid_len) → then provider_info_len at that offset + let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); + let pinfo_len_offset = edk_start + 2 + pid_len as usize; + let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; + + //= specification/data-format/message-header.md#key-provider-information-length + //= type=test + //# The key provider information length MUST be interpreted as a UInt16. + // Decoding the length directly from the wire bytes as big-endian UInt16 must match + // the parser's interpretation and the actual provider info byte length. + assert_eq!( + wire_pinfo_len as usize, + edk.provider_info.len(), + "{version:?}: big-endian UInt16 read from wire bytes must equal actual provider info length" + ); + assert_eq!( + wire_pinfo_len, edk.provider_info_len, + "{version:?}: wire UInt16 must match parser-interpreted UInt16" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_info_length_matches_field() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pinfo len match", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#key-provider-information + //= type=test + //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. + for edk in &parsed.edks { + assert_eq!( + edk.provider_info.len(), + edk.provider_info_len as usize, + "{version:?}: provider info byte length must equal the provider info length field" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_key_provider_info_interpreted_as_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"pinfo bytes", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; + // Provider info for raw AES keyring starts with the key name + let (_, expected_name) = namespace_and_name(0); + + //= specification/data-format/message-header.md#key-provider-information + //= type=test + //# The key provider information MUST be interpreted as bytes. + assert!( + edk.provider_info.starts_with(expected_name.as_bytes()), + "{version:?}: provider info must start with the known key name" + ); + // Round-trip proves the bytes are correctly interpreted + let mut dec = + DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), keyring.clone()); + if let Version::V1 = version { + dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + let result = decrypt(&dec).await.unwrap(); + assert_eq!(result.plaintext, b"pinfo bytes"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_length_field_is_2_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"edk len 2 bytes", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; + // Walk to the EDK length field offset manually + let edk_start = parsed.edk_count_offset + 2; + let edk_len_offset = edk_start + + 2 + edk.provider_id_len as usize // pid_len field + pid bytes + + 2 + edk.provider_info_len as usize; // pinfo_len field + pinfo bytes + + //= specification/data-format/message-header.md#encrypted-data-key-length + //= type=test + //# The length of the serialized encrypted data key length field MUST be 2 bytes. + let edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); + assert_eq!( + edk_len, edk.edk_len, + "{version:?}: encrypted data key length field must be exactly 2 bytes" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_length_interpreted_as_uint16() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"edk len uint16", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; + + //= specification/data-format/message-header.md#encrypted-data-key-length + //= type=test + //# The encrypted data key length MUST be interpreted as a UInt16. + // The UInt16 value must match the actual encrypted data key byte length + assert_eq!( + edk.edk_len as usize, + edk.edk.len(), + "{version:?}: UInt16 EDK length must match actual EDK bytes" + ); + // For AES-GCM wrapping, the EDK is non-trivially sized (IV + ciphertext + tag) + assert!( + edk.edk_len > 0, + "{version:?}: encrypted data key must have positive length" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_length_matches_field() { + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"edk len match", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + + //= specification/data-format/message-header.md#encrypted-data-key + //= type=test + //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. + for (i, edk) in parsed.edks.iter().enumerate() { + assert_eq!( + edk.edk.len(), + edk.edk_len as usize, + "{version:?}: EDK {i}: encrypted data key byte length must equal the EDK length field" + ); + } + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_interpreted_as_bytes() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"edk as bytes", version, keyring.clone()).await; + let mut dec = + DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), keyring.clone()); + if let Version::V1 = version { + dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + + //= specification/data-format/message-header.md#encrypted-data-key + //= type=test + //# The encrypted data key MUST be interpreted as bytes. + let result = decrypt(&dec).await.unwrap(); + assert_eq!( + result.plaintext, b"edk as bytes", + "{version:?}: round-trip proves EDK bytes are correctly interpreted" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_edk_bytes_are_nonempty_ciphertext() { + let keyring = aes_keyring(0).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"edk nonempty", version, keyring.clone()).await; + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; + + //= specification/data-format/message-header.md#encrypted-data-key + //= type=test + //= reason=Verifying the EDK contains non-zero bytes proves it holds actual encrypted key material, not a placeholder. + //# The encrypted data key MUST be interpreted as bytes. + assert!( + !edk.edk.is_empty(), + "{version:?}: encrypted data key must not be empty" + ); + // AES-GCM wrapping produces at minimum IV (12 bytes) + tag (16 bytes) = 28 bytes + assert!( + edk.edk.len() >= 28, + "{version:?}: EDK must be at least 28 bytes (AES-GCM IV + tag), got {}", + edk.edk.len() + ); + // The EDK should contain actual ciphertext (not all zeros) + assert!( + edk.edk.iter().any(|&b| b != 0), + "{version:?}: encrypted data key must contain non-zero bytes (actual ciphertext)" + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_multi_keyring_round_trip_each_child() { + let generator = aes_keyring(0).await; + let c1 = aes_keyring(1).await; + let c2 = aes_keyring(2).await; + let mk = multi_keyring(generator.clone(), vec![c1.clone(), c2.clone()]).await; + + for version in VERSIONS { + let ct = encrypt_with_version(b"multi rt", version, mk.clone()).await; + + // Each individual keyring should be able to decrypt + for kr in [generator.clone(), c1.clone(), c2.clone()] { + let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), kr); + if let Version::V1 = version { + dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + let result = decrypt(&dec).await.unwrap(); + assert_eq!(result.plaintext, b"multi rt"); + } + } +} diff --git a/esdk/tests/test_encryption_context_aad.rs b/esdk/tests/test_encryption_context_aad.rs new file mode 100644 index 000000000..8d5b127c3 --- /dev/null +++ b/esdk/tests/test_encryption_context_aad.rs @@ -0,0 +1,230 @@ +// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Tests for specification/data-format/message-header.md#aad, +//! #key-value-pairs-length, and #key-value-pairs + +mod fixtures; +mod test_helpers; + +use aws_esdk::*; +use aws_mpl_legacy::commitment::EsdkCommitmentPolicy; +use aws_mpl_legacy::suites::EsdkAlgorithmSuiteId; +use fixtures::*; +use test_helpers::*; + +/// V1 header AAD offset: Version(1) + Type(1) + AlgSuiteID(2) + MessageID(16) = 20. +const V1_AAD_OFFSET: usize = 20; +/// V2 header AAD offset: Version(1) + AlgSuiteID(2) + MessageID(32) = 35. +const V2_AAD_OFFSET: usize = 35; + +fn aad_offset(version: Version) -> usize { + match version { + Version::V1 => V1_AAD_OFFSET, + Version::V2 => V2_AAD_OFFSET, + } +} + +/// Encrypt with a non-signing suite (so the header EC matches what we provide — +/// signing suites add `aws-crypto-public-key` to the EC). V1 uses forbid-commit policy. +async fn encrypt_no_sign(pt: &[u8], ec: EncryptionContext, version: Version) -> Vec { + let keyring = test_keyring().await; + let mut input = EncryptInput::with_legacy_keyring(pt, ec, keyring); + match version { + Version::V1 => { + input.algorithm_suite_id = + Some(EsdkAlgorithmSuiteId::AlgAes256GcmIv12Tag16HkdfSha256); + input.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + Version::V2 => { + input.algorithm_suite_id = Some(EsdkAlgorithmSuiteId::AlgAes256GcmHkdfSha512CommitKey); + } + } + encrypt(&input).await.unwrap().ciphertext +} + +async fn decrypt_roundtrip(ct: &[u8], version: Version) -> Vec { + let keyring = test_keyring().await; + let mut dec_input = + DecryptInput::with_legacy_keyring(ct, EncryptionContext::new(), keyring); + if let Version::V1 = version { + dec_input.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; + } + decrypt(&dec_input).await.unwrap().plaintext +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_serialization_order() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::AB); + let pt = b"aad serialization order"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#aad + //= type=test + //# The AAD MUST consist of, in order, + //# Key Value Pairs Length, + //# and Key Value Pairs. + + // KVP Length field comes first at the AAD offset. + let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; + assert!(kvp_len > 0, "{version:?}: non-empty EC must have non-zero KVP length"); + // KVP data follows immediately after the 2-byte length field (count is first). + let kvp_count_offset = off + 2; + let kvp_count = + u16::from_be_bytes([ct[kvp_count_offset], ct[kvp_count_offset + 1]]) as usize; + assert_eq!(kvp_count, 2, "{version:?}: AB encryption context has 2 key-value pairs"); + + // Round-trip proves the ordering is correct end-to-end. + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_key_value_pairs_length_field_size() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::A); + let pt = b"kvp length field size"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#key-value-pairs-length + //= type=test + //# The length of the serialized key value pairs length field MUST be 2 bytes. + + // The KVP length field occupies exactly 2 bytes at [off..off+2]. + let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; + // For "A" (keyA=valA): key_len(2) + key(4) + val_len(2) + val(4) = 12 bytes of pair data. + assert_eq!(kvp_len, 12, "{version:?}: KVP length for single pair keyA=valA must be 12"); + + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_key_value_pairs_length_uint16() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::A); + let pt = b"kvp length uint16"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#key-value-pairs-length + //= type=test + //# The key value pairs length MUST be interpreted as a UInt16. + + // Read the 2 bytes as big-endian u16 and verify the value. + let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]); + // keyA=valA: key_len(2) + key(4) + val_len(2) + val(4) = 12. + assert_eq!(kvp_len, 12, "{version:?}: UInt16 KVP length for keyA=valA must be 12"); + + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_empty_encryption_context_length_zero() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::Empty); + let pt = b"empty ec length zero"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#key-value-pairs-length + //= type=test + //# When the [encryption context](../framework/structures.md#encryption-context) is empty, the value of this field MUST be 0. + + // The 2 bytes at the AAD offset must be [0x00, 0x00]. + assert_eq!(ct[off], 0x00, "{version:?}: empty EC KVP length high byte must be 0"); + assert_eq!( + ct[off + 1], + 0x00, + "{version:?}: empty EC KVP length low byte must be 0" + ); + + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_key_value_pairs_serialization() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::AB); + let pt = b"kvp serialization"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#key-value-pairs + //= type=test + //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). + + // Parse the KVP section: after 2-byte length, 2-byte count, then pairs. + let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; + assert!(kvp_len > 0, "{version:?}: non-empty KVP length"); + let mut pos = off + 2; + let count = u16::from_be_bytes([ct[pos], ct[pos + 1]]) as usize; + assert_eq!(count, 2, "{version:?}: AB has 2 pairs"); + pos += 2; + + // Pairs must be sorted by key: keyA < keyB. + let key1_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]) as usize; + pos += 2; + let key1 = std::str::from_utf8(&ct[pos..pos + key1_len]).unwrap(); + pos += key1_len; + let val1_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]) as usize; + pos += 2; + let val1 = std::str::from_utf8(&ct[pos..pos + val1_len]).unwrap(); + pos += val1_len; + + let key2_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]) as usize; + pos += 2; + let key2 = std::str::from_utf8(&ct[pos..pos + key2_len]).unwrap(); + pos += key2_len; + let val2_len = u16::from_be_bytes([ct[pos], ct[pos + 1]]) as usize; + pos += 2; + let val2 = std::str::from_utf8(&ct[pos..pos + val2_len]).unwrap(); + + assert_eq!(key1, "keyA", "{version:?}: first key in sorted order"); + assert_eq!(val1, "valA", "{version:?}: first value"); + assert_eq!(key2, "keyB", "{version:?}: second key in sorted order"); + assert_eq!(val2, "valB", "{version:?}: second value"); + + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_aad_empty_encryption_context_no_kvp_field() { + for version in VERSIONS { + let ec = small_encryption_context(SmallEncryptionContextVariation::Empty); + let pt = b"empty ec no kvp"; + let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let off = aad_offset(version); + + //= specification/data-format/message-header.md#key-value-pairs + //= type=test + //# When the [encryption context](../framework/structures.md#encryption-context) is empty, + //# this field MUST NOT be included in the [AAD](#aad). + + // KVP Length is 0, and the next field (EDK count) starts immediately after. + let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]); + assert_eq!(kvp_len, 0, "{version:?}: empty EC must have KVP length 0"); + // The bytes right after the 2-byte KVP Length field are the EDK count (not KVP data). + let edk_count_offset = off + 2; + let edk_count = + u16::from_be_bytes([ct[edk_count_offset], ct[edk_count_offset + 1]]); + assert!( + edk_count >= 1, + "{version:?}: EDK count must be at least 1, proving no KVP field between AAD length and EDKs" + ); + + let pt_out = decrypt_roundtrip(&ct, version).await; + assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + } +} From 400204045618f3fb86826c0f4e1991d6dc949f63 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:06:06 -0700 Subject: [PATCH 02/22] docs(native-rust): sync EDK section headers from unreviewed --- esdk/src/message/encrypted_data_keys.rs | 31 +++++++++++++++++++++---- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index 2ddd00c07..6f7ef36c7 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -16,17 +16,21 @@ pub(crate) fn write_edks(w: &mut dyn SafeWrite, edks: &[EncryptedDataKey]) -> Re //# The Encrypted Data Keys MUST consist of, in order, //# Encrypted Data Key Count, //# and Encrypted Data Key Entries. - let Ok(edk_count) = u16::try_from(edks.len()) else { - return ser_err("Count too large for UInt16"); - }; + + // Encrypted Data Key Count //= specification/data-format/message-header.md#encrypted-data-key-count //# The length of the serialized encrypted data key count MUST be 2 bytes. // //= specification/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. + let Ok(edk_count) = u16::try_from(edks.len()) else { + return ser_err("Count too large for UInt16"); + }; write_u16(w, edk_count)?; + // Encrypted Data Key Entries + for edk in edks { write_edk(w, edk)?; } @@ -42,6 +46,9 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# Key Provider Information, //# Encrypted Data Key Length, //# and Encrypted Data Key. + + // Key Provider ID Length and Key Provider ID + let kp_id_bytes = edk.key_provider_id.as_bytes(); //= specification/data-format/message-header.md#key-provider-id-length @@ -62,6 +69,8 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. write_bytes(w, kp_id_bytes)?; + // Key Provider Information Length and Key Provider Information + //= specification/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. let Ok(kp_info_len) = u16::try_from(edk.key_provider_info.len()) else { @@ -80,6 +89,8 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The key provider information MUST be interpreted as bytes. write_bytes(w, &edk.key_provider_info)?; + // Encrypted Data Key Length and Encrypted Data Key + //= specification/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. let Ok(edk_len) = u16::try_from(edk.ciphertext.len()) else { @@ -112,7 +123,9 @@ pub(crate) fn read_edks( //# The Encrypted Data Keys MUST consist of, in order, //# Encrypted Data Key Count, //# and Encrypted Data Key Entries. - // + + // Encrypted Data Key Count + //= specification/data-format/message-header.md#encrypted-data-key-count //# The length of the serialized encrypted data key count MUST be 2 bytes. // @@ -132,6 +145,8 @@ pub(crate) fn read_edks( return ser_err("Ciphertext encrypted data keys exceed maximum encrypted data keys limit"); } + // Encrypted Data Key Entries + let mut edks = Vec::with_capacity(usize::from(count)); for _ in 0..count { edks.push(read_edk(r, raw)?); @@ -151,7 +166,9 @@ pub(crate) fn read_edk( //# Key Provider Information, //# Encrypted Data Key Length, //# and Encrypted Data Key. - // + + // Key Provider ID Length and Key Provider ID + //= specification/data-format/message-header.md#key-provider-id-length //# The key provider ID length MUST be interpreted as a UInt16. // @@ -166,6 +183,8 @@ pub(crate) fn read_edk( //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. let provider_id = read_str_u16(r, raw)?; + // Key Provider Information Length and Key Provider Information + //= specification/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. // @@ -180,6 +199,8 @@ pub(crate) fn read_edk( //# The key provider information MUST be interpreted as bytes. let provider_info = read_seq_u16(r, raw)?; + // Encrypted Data Key Length and Encrypted Data Key + //= specification/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. // From 20129c4e01ee0dcf059d29ac97246fc239f7f78b Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:09:11 -0700 Subject: [PATCH 03/22] docs(native-rust): sync split write_edk section headers from unreviewed --- esdk/src/message/encrypted_data_keys.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index 6f7ef36c7..43fd468d1 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -47,7 +47,7 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# Encrypted Data Key Length, //# and Encrypted Data Key. - // Key Provider ID Length and Key Provider ID + // Key Provider ID Length let kp_id_bytes = edk.key_provider_id.as_bytes(); @@ -61,6 +61,8 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The length of the serialized key provider ID length field MUST be 2 bytes. write_u16(w, kp_id_len)?; + // Key Provider ID + //= specification/data-format/message-header.md#key-provider-id //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. @@ -69,7 +71,7 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. write_bytes(w, kp_id_bytes)?; - // Key Provider Information Length and Key Provider Information + // Key Provider Information Length //= specification/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. @@ -81,6 +83,8 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The length of the serialized key provider information length field MUST be 2 bytes. write_u16(w, kp_info_len)?; + // Key Provider Information + //= specification/data-format/message-header.md#key-provider-information //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. @@ -89,7 +93,7 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The key provider information MUST be interpreted as bytes. write_bytes(w, &edk.key_provider_info)?; - // Encrypted Data Key Length and Encrypted Data Key + // Encrypted Data Key Length //= specification/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. @@ -101,6 +105,8 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The length of the serialized encrypted data key length field MUST be 2 bytes. write_u16(w, edk_len)?; + // Encrypted Data Key + //= specification/data-format/message-header.md#encrypted-data-key //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. @@ -110,10 +116,6 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result write_bytes(w, &edk.ciphertext) } -//= specification/client-apis/decrypt.md#v1-header-deserialization -//# - MUST deserialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). -//= specification/client-apis/decrypt.md#v2-header-deserialization -//# - MUST deserialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). pub(crate) fn read_edks( r: &mut dyn SafeRead, max_edks: Option, From a1934159630f9ef6c233df212e08bc743ec7e433 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:12:59 -0700 Subject: [PATCH 04/22] docs(native-rust): sync encryption_context docs from unreviewed --- esdk/src/message/encryption_context.rs | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index f7bcde0bf..d81cba7de 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -1,20 +1,42 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + //! Encryption context serialization for message header and AAD. +//! +//! An encryption context is a canonicalized (sorted, deduplicated) list of +//! UTF-8 key-value pairs. It is serialized in two closely related forms: +//! +//! - The header's AAD field, which wraps the key-value pairs in an outer +//! `Key Value Pairs Length` (UInt16). See [`write_aad_section`] and +//! [`read_canonical_ec`]. +//! - A bare "canonical" byte stream with no outer length, used as input to +//! signatures and as AAD for AES-GCM. See [`write_aad`] and +//! [`write_empty_ec_or_write_aad`]. use super::serializable_types::ESDKCanonicalEncryptionContext; use super::serialize_functions::{read_str_u16, read_u16, write_bytes, write_u16}; use super::{Error, ser_err}; use crate::types::{SafeRead, SafeWrite}; +/// Read the header's AAD encryption context sub-section and return the +/// canonical (key, value) pairs. +/// +/// Reads `Key Value Pairs Length` (UInt16). A length of 0 means the +/// encryption context is empty and nothing further is consumed. Otherwise +/// reads `Key Value Pair Count` (UInt16) followed by that many (key, value) +/// UTF-8 string pairs. pub(crate) fn read_canonical_ec( r: &mut dyn SafeRead, raw: &mut dyn SafeWrite, ) -> Result { + // Key Value Pairs Length. When zero, the key-value-pairs sub-field is + // absent entirely and we're done. let bytes = usize::from(read_u16(r, raw)?); if bytes == 0 { return Ok(Vec::new()); } + + // Key Value Pair Count, then `count` UTF-8 (key, value) pairs. let count = usize::from(read_u16(r, raw)?); let mut result: ESDKCanonicalEncryptionContext = Vec::with_capacity(count); for _ in 0..count { @@ -25,6 +47,11 @@ pub(crate) fn read_canonical_ec( Ok(result) } +/// Write the canonical encryption context bytes (no outer length prefix) used +/// for signing and as AES-GCM AAD, or write nothing when the context is empty. +/// +/// When the encryption context is empty, the spec requires this field to be +/// omitted entirely (not written as a zero-length field). pub(crate) fn write_empty_ec_or_write_aad( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, @@ -39,14 +66,24 @@ pub(crate) fn write_empty_ec_or_write_aad( } } +/// Serialized length of the canonical key-value-pairs body, in bytes. +/// +/// Each pair contributes two UInt16 length fields (4 bytes total) plus the +/// UTF-8 bytes of the key and value. fn get_length(data: &ESDKCanonicalEncryptionContext) -> usize { let mut length = 0; for pair in data { + // 2 bytes key length + 2 bytes value length + key bytes + value bytes. length += 4 + pair.0.len() + pair.1.len(); } length } +/// Write the header's AAD encryption context sub-section: `Key Value Pairs +/// Length` (UInt16) followed by the canonical key-value-pairs body. +/// +/// When the encryption context is empty the length field is written as 0 and +/// the key-value-pairs body is omitted. pub(crate) fn write_aad_section( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, @@ -61,6 +98,9 @@ pub(crate) fn write_aad_section( write_u16(w, 0)?; return Ok(()); } + + // Key Value Pairs Length: total size in bytes of the key-value-pairs body + // that `write_aad` will emit below. let bytes = get_length(data); //= specification/data-format/message-header.md#key-value-pairs-length @@ -72,25 +112,39 @@ pub(crate) fn write_aad_section( return ser_err("value too large for u16"); }; write_u16(w, bytes_u16)?; + + // Key Value Pairs body. write_aad(w, data) } +/// Write the canonical key-value-pairs body with no outer length prefix: +/// `Key Value Pair Count` (UInt16) followed by that many (key, value) pairs. +/// +/// Each pair is `Key Length` (UInt16), key UTF-8 bytes, `Value Length` +/// (UInt16), value UTF-8 bytes. Callers use this directly for signature input +/// and AES-GCM AAD, or via [`write_aad_section`] when writing the header AAD. pub(crate) fn write_aad( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, ) -> Result<(), Error> { + // Key Value Pair Count. let Ok(data_len) = u16::try_from(data.len()) else { return ser_err("value too large for u16"); }; write_u16(w, data_len)?; + for pair in data { //= specification/data-format/message-header.md#key-value-pairs //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). + + // Key: length (UInt16) then UTF-8 bytes. let Ok(key_len) = u16::try_from(pair.0.len()) else { return ser_err("value too large for u16"); }; write_u16(w, key_len)?; write_bytes(w, pair.0.as_bytes())?; + + // Value: length (UInt16) then UTF-8 bytes. let Ok(val_len) = u16::try_from(pair.1.len()) else { return ser_err("value too large for u16"); }; From 1dd8de6d00c64ac3592abc1de6162394a658fdfd Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:21:04 -0700 Subject: [PATCH 05/22] test(native-rust): sync encryption_context_aad helper refactor from unreviewed --- esdk/tests/test_encryption_context_aad.rs | 95 ++++++++++++----------- 1 file changed, 48 insertions(+), 47 deletions(-) diff --git a/esdk/tests/test_encryption_context_aad.rs b/esdk/tests/test_encryption_context_aad.rs index 8d5b127c3..c73196211 100644 --- a/esdk/tests/test_encryption_context_aad.rs +++ b/esdk/tests/test_encryption_context_aad.rs @@ -8,8 +8,6 @@ mod fixtures; mod test_helpers; use aws_esdk::*; -use aws_mpl_legacy::commitment::EsdkCommitmentPolicy; -use aws_mpl_legacy::suites::EsdkAlgorithmSuiteId; use fixtures::*; use test_helpers::*; @@ -25,32 +23,18 @@ fn aad_offset(version: Version) -> usize { } } -/// Encrypt with a non-signing suite (so the header EC matches what we provide — -/// signing suites add `aws-crypto-public-key` to the EC). V1 uses forbid-commit policy. -async fn encrypt_no_sign(pt: &[u8], ec: EncryptionContext, version: Version) -> Vec { - let keyring = test_keyring().await; - let mut input = EncryptInput::with_legacy_keyring(pt, ec, keyring); - match version { - Version::V1 => { - input.algorithm_suite_id = - Some(EsdkAlgorithmSuiteId::AlgAes256GcmIv12Tag16HkdfSha256); - input.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; - } - Version::V2 => { - input.algorithm_suite_id = Some(EsdkAlgorithmSuiteId::AlgAes256GcmHkdfSha512CommitKey); - } - } - encrypt(&input).await.unwrap().ciphertext -} - -async fn decrypt_roundtrip(ct: &[u8], version: Version) -> Vec { - let keyring = test_keyring().await; - let mut dec_input = - DecryptInput::with_legacy_keyring(ct, EncryptionContext::new(), keyring); - if let Version::V1 = version { - dec_input.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; +/// Assert that every (key, value) pair in `expected` is present in `actual`. +/// Used to verify the encryption context survives the round trip intact, +/// while ignoring any keys the SDK may add (e.g. `aws-crypto-public-key` +/// for signing suites — not used by these tests, but the check is defensive). +fn assert_ec_contains(actual: &EncryptionContext, expected: &EncryptionContext, version: Version) { + for (k, v) in expected { + assert_eq!( + actual.get(k), + Some(v), + "{version:?}: decrypted EC missing or mismatched for key {k:?}" + ); } - decrypt(&dec_input).await.unwrap().plaintext } #[tokio::test(flavor = "multi_thread")] @@ -58,7 +42,7 @@ async fn test_aad_serialization_order() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::AB); let pt = b"aad serialization order"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#aad @@ -67,7 +51,7 @@ async fn test_aad_serialization_order() { //# Key Value Pairs Length, //# and Key Value Pairs. - // KVP Length field comes first at the AAD offset. + // Primary assertion: the encrypt path lays out KVP Length first, followed by KVP data. let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; assert!(kvp_len > 0, "{version:?}: non-empty EC must have non-zero KVP length"); // KVP data follows immediately after the 2-byte length field (count is first). @@ -76,9 +60,10 @@ async fn test_aad_serialization_order() { u16::from_be_bytes([ct[kvp_count_offset], ct[kvp_count_offset + 1]]) as usize; assert_eq!(kvp_count, 2, "{version:?}: AB encryption context has 2 key-value pairs"); - // Round-trip proves the ordering is correct end-to-end. - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: the decrypt path recovers the same encryption context, which is + // only possible if the on-wire ordering agreed with the spec on both sides. + let dec = decrypt_with_version(&ct, version).await; + assert_ec_contains(&dec.encryption_context, &ec, version); } } @@ -87,7 +72,7 @@ async fn test_aad_key_value_pairs_length_field_size() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::A); let pt = b"kvp length field size"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#key-value-pairs-length @@ -99,8 +84,10 @@ async fn test_aad_key_value_pairs_length_field_size() { // For "A" (keyA=valA): key_len(2) + key(4) + val_len(2) + val(4) = 12 bytes of pair data. assert_eq!(kvp_len, 12, "{version:?}: KVP length for single pair keyA=valA must be 12"); - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: the decrypted EC matches what we encrypted, confirming the 2-byte + // length field was parsed correctly on the decrypt side too. + let dec = decrypt_with_version(&ct, version).await; + assert_ec_contains(&dec.encryption_context, &ec, version); } } @@ -109,7 +96,7 @@ async fn test_aad_key_value_pairs_length_uint16() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::A); let pt = b"kvp length uint16"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#key-value-pairs-length @@ -121,8 +108,10 @@ async fn test_aad_key_value_pairs_length_uint16() { // keyA=valA: key_len(2) + key(4) + val_len(2) + val(4) = 12. assert_eq!(kvp_len, 12, "{version:?}: UInt16 KVP length for keyA=valA must be 12"); - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: the decrypted EC round-trips, confirming both sides agree that + // the field is a big-endian UInt16. + let dec = decrypt_with_version(&ct, version).await; + assert_ec_contains(&dec.encryption_context, &ec, version); } } @@ -131,7 +120,7 @@ async fn test_aad_empty_encryption_context_length_zero() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::Empty); let pt = b"empty ec length zero"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#key-value-pairs-length @@ -146,8 +135,14 @@ async fn test_aad_empty_encryption_context_length_zero() { "{version:?}: empty EC KVP length low byte must be 0" ); - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: decrypt returns an empty encryption context (non-signing suite + // means the SDK added no entries of its own). + let dec = decrypt_with_version(&ct, version).await; + assert!( + dec.encryption_context.is_empty(), + "{version:?}: decrypted EC must be empty, got {:?}", + dec.encryption_context + ); } } @@ -156,7 +151,7 @@ async fn test_aad_key_value_pairs_serialization() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::AB); let pt = b"kvp serialization"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#key-value-pairs @@ -194,8 +189,9 @@ async fn test_aad_key_value_pairs_serialization() { assert_eq!(key2, "keyB", "{version:?}: second key in sorted order"); assert_eq!(val2, "valB", "{version:?}: second value"); - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: decrypted EC contains the same key/value pairs we encrypted. + let dec = decrypt_with_version(&ct, version).await; + assert_ec_contains(&dec.encryption_context, &ec, version); } } @@ -204,7 +200,7 @@ async fn test_aad_empty_encryption_context_no_kvp_field() { for version in VERSIONS { let ec = small_encryption_context(SmallEncryptionContextVariation::Empty); let pt = b"empty ec no kvp"; - let ct = encrypt_no_sign(pt, ec.clone(), version).await; + let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); //= specification/data-format/message-header.md#key-value-pairs @@ -224,7 +220,12 @@ async fn test_aad_empty_encryption_context_no_kvp_field() { "{version:?}: EDK count must be at least 1, proving no KVP field between AAD length and EDKs" ); - let pt_out = decrypt_roundtrip(&ct, version).await; - assert_eq!(pt_out, pt, "{version:?}: round-trip plaintext mismatch"); + // Cross-check: decrypt recovers an empty encryption context. + let dec = decrypt_with_version(&ct, version).await; + assert!( + dec.encryption_context.is_empty(), + "{version:?}: decrypted EC must be empty, got {:?}", + dec.encryption_context + ); } } From 17dda4e60bfb0470d796e18fe552ca989f948f62 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:24:41 -0700 Subject: [PATCH 06/22] test(native-rust): sync EDK ordering section pattern from unreviewed --- esdk/tests/test_encrypted_data_keys.rs | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index 3deb47157..34beb5cd7 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -22,18 +22,26 @@ async fn test_encrypted_data_keys_ordering() { //# Encrypted Data Key Count, //# and Encrypted Data Key Entries. let ct = encrypt_with_version(b"ordering test", version, keyring.clone()).await; + let edk_section_start = skip_to_edk_section(&ct, version); let parsed = parse_edk_section(&ct, version); - // Count field comes first, then entries follow immediately - assert_eq!(parsed.edk_count, 1); - assert_eq!(parsed.edks.len(), 1); - // The entries start at edk_count_offset + 2 (right after the 2-byte count) - let entries_start = parsed.edk_count_offset + 2; - // First entry's provider_id_len is at entries_start + + // 1. Encrypted Data Key Count (2 bytes) + let count = u16::from_be_bytes([ct[edk_section_start], ct[edk_section_start + 1]]); + assert_eq!( + count, 1, + "{version:?}: count field (first 2 bytes of the EDK section) must equal 1" + ); + assert_eq!(parsed.edk_count, count); + + // 2. Encrypted Data Key Entries (immediately after the count) + let entries_start = edk_section_start + 2; + // The entry begins with its Key Provider ID Length; the count is not repeated. let first_pid_len = u16::from_be_bytes([ct[entries_start], ct[entries_start + 1]]); assert_eq!( first_pid_len, parsed.edks[0].provider_id_len, "{version:?}: EDK entries must immediately follow the count field" ); + assert_eq!(parsed.edks.len(), count as usize); } } From 0912758d0385c08457d5d1ad45ea0e0e81ddc119 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:32:29 -0700 Subject: [PATCH 07/22] sync(native-rust): dedupe EDK tests, trim encryption_context comments, implications on source --- esdk/src/message/encrypted_data_keys.rs | 2 + esdk/src/message/encryption_context.rs | 60 +--- esdk/tests/test_encrypted_data_keys.rs | 348 ++++++------------------ 3 files changed, 96 insertions(+), 314 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index 43fd468d1..d1fd194c3 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -112,6 +112,7 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. // //= specification/data-format/message-header.md#encrypted-data-key + //= type=implication //# The encrypted data key MUST be interpreted as bytes. write_bytes(w, &edk.ciphertext) } @@ -214,6 +215,7 @@ pub(crate) fn read_edk( //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. // //= specification/data-format/message-header.md#encrypted-data-key + //= type=implication //# The encrypted data key MUST be interpreted as bytes. let ciphertext = read_seq_u16(r, raw)?; diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index d81cba7de..e921185f7 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -2,41 +2,24 @@ // SPDX-License-Identifier: Apache-2.0 //! Encryption context serialization for message header and AAD. -//! -//! An encryption context is a canonicalized (sorted, deduplicated) list of -//! UTF-8 key-value pairs. It is serialized in two closely related forms: -//! -//! - The header's AAD field, which wraps the key-value pairs in an outer -//! `Key Value Pairs Length` (UInt16). See [`write_aad_section`] and -//! [`read_canonical_ec`]. -//! - A bare "canonical" byte stream with no outer length, used as input to -//! signatures and as AAD for AES-GCM. See [`write_aad`] and -//! [`write_empty_ec_or_write_aad`]. use super::serializable_types::ESDKCanonicalEncryptionContext; use super::serialize_functions::{read_str_u16, read_u16, write_bytes, write_u16}; use super::{Error, ser_err}; use crate::types::{SafeRead, SafeWrite}; -/// Read the header's AAD encryption context sub-section and return the -/// canonical (key, value) pairs. -/// -/// Reads `Key Value Pairs Length` (UInt16). A length of 0 means the -/// encryption context is empty and nothing further is consumed. Otherwise -/// reads `Key Value Pair Count` (UInt16) followed by that many (key, value) -/// UTF-8 string pairs. +/// Read the header's AAD encryption context sub-section. pub(crate) fn read_canonical_ec( r: &mut dyn SafeRead, raw: &mut dyn SafeWrite, ) -> Result { - // Key Value Pairs Length. When zero, the key-value-pairs sub-field is - // absent entirely and we're done. + // Empty EC: length 0, no further bytes. let bytes = usize::from(read_u16(r, raw)?); if bytes == 0 { return Ok(Vec::new()); } - // Key Value Pair Count, then `count` UTF-8 (key, value) pairs. + // Count, then `count` (key, value) pairs. let count = usize::from(read_u16(r, raw)?); let mut result: ESDKCanonicalEncryptionContext = Vec::with_capacity(count); for _ in 0..count { @@ -47,11 +30,7 @@ pub(crate) fn read_canonical_ec( Ok(result) } -/// Write the canonical encryption context bytes (no outer length prefix) used -/// for signing and as AES-GCM AAD, or write nothing when the context is empty. -/// -/// When the encryption context is empty, the spec requires this field to be -/// omitted entirely (not written as a zero-length field). +/// Write canonical EC bytes for signing/AES-GCM AAD; empty EC writes nothing. pub(crate) fn write_empty_ec_or_write_aad( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, @@ -66,24 +45,17 @@ pub(crate) fn write_empty_ec_or_write_aad( } } -/// Serialized length of the canonical key-value-pairs body, in bytes. -/// -/// Each pair contributes two UInt16 length fields (4 bytes total) plus the -/// UTF-8 bytes of the key and value. +/// Serialized length of the key-value-pairs body in bytes. fn get_length(data: &ESDKCanonicalEncryptionContext) -> usize { let mut length = 0; for pair in data { - // 2 bytes key length + 2 bytes value length + key bytes + value bytes. + // 2 (key len) + 2 (val len) + key bytes + val bytes. length += 4 + pair.0.len() + pair.1.len(); } length } -/// Write the header's AAD encryption context sub-section: `Key Value Pairs -/// Length` (UInt16) followed by the canonical key-value-pairs body. -/// -/// When the encryption context is empty the length field is written as 0 and -/// the key-value-pairs body is omitted. +/// Write the header's AAD EC sub-section: length + key-value pairs. pub(crate) fn write_aad_section( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, @@ -99,8 +71,7 @@ pub(crate) fn write_aad_section( return Ok(()); } - // Key Value Pairs Length: total size in bytes of the key-value-pairs body - // that `write_aad` will emit below. + // Key Value Pairs Length. let bytes = get_length(data); //= specification/data-format/message-header.md#key-value-pairs-length @@ -113,21 +84,16 @@ pub(crate) fn write_aad_section( }; write_u16(w, bytes_u16)?; - // Key Value Pairs body. + // Key Value Pairs. write_aad(w, data) } -/// Write the canonical key-value-pairs body with no outer length prefix: -/// `Key Value Pair Count` (UInt16) followed by that many (key, value) pairs. -/// -/// Each pair is `Key Length` (UInt16), key UTF-8 bytes, `Value Length` -/// (UInt16), value UTF-8 bytes. Callers use this directly for signature input -/// and AES-GCM AAD, or via [`write_aad_section`] when writing the header AAD. +/// Write the key-value-pairs body: count, then (key, value) pairs. pub(crate) fn write_aad( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, ) -> Result<(), Error> { - // Key Value Pair Count. + // Count. let Ok(data_len) = u16::try_from(data.len()) else { return ser_err("value too large for u16"); }; @@ -137,14 +103,14 @@ pub(crate) fn write_aad( //= specification/data-format/message-header.md#key-value-pairs //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). - // Key: length (UInt16) then UTF-8 bytes. + // Key: length + UTF-8 bytes. let Ok(key_len) = u16::try_from(pair.0.len()) else { return ser_err("value too large for u16"); }; write_u16(w, key_len)?; write_bytes(w, pair.0.as_bytes())?; - // Value: length (UInt16) then UTF-8 bytes. + // Value: length + UTF-8 bytes. let Ok(val_len) = u16::try_from(pair.1.len()) else { return ser_err("value too large for u16"); }; diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index 34beb5cd7..16ee17aa3 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -13,81 +13,71 @@ use test_helpers::*; #[tokio::test(flavor = "multi_thread")] async fn test_encrypted_data_keys_ordering() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - //= specification/data-format/message-header.md#encrypted-data-keys - //= type=test - //# The Encrypted Data Keys MUST consist of, in order, - //# Encrypted Data Key Count, - //# and Encrypted Data Key Entries. - let ct = encrypt_with_version(b"ordering test", version, keyring.clone()).await; - let edk_section_start = skip_to_edk_section(&ct, version); - let parsed = parse_edk_section(&ct, version); - - // 1. Encrypted Data Key Count (2 bytes) - let count = u16::from_be_bytes([ct[edk_section_start], ct[edk_section_start + 1]]); - assert_eq!( - count, 1, - "{version:?}: count field (first 2 bytes of the EDK section) must equal 1" - ); - assert_eq!(parsed.edk_count, count); - - // 2. Encrypted Data Key Entries (immediately after the count) - let entries_start = edk_section_start + 2; - // The entry begins with its Key Provider ID Length; the count is not repeated. - let first_pid_len = u16::from_be_bytes([ct[entries_start], ct[entries_start + 1]]); - assert_eq!( - first_pid_len, parsed.edks[0].provider_id_len, - "{version:?}: EDK entries must immediately follow the count field" - ); - assert_eq!(parsed.edks.len(), count as usize); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_field_is_2_bytes() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"count 2 bytes", version, keyring.clone()).await; - let offset = skip_to_edk_section(&ct, version); - - //= specification/data-format/message-header.md#encrypted-data-key-count - //= type=test - //# The length of the serialized encrypted data key count MUST be 2 bytes. - // The count occupies exactly bytes [offset] and [offset+1] - let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); - assert_eq!( - count, 1, - "{version:?}: single keyring produces exactly 1 EDK" - ); + let single = aes_keyring(0).await; + let generator = aes_keyring(0).await; + let c1 = aes_keyring(1).await; + let c2 = aes_keyring(2).await; + let triple = multi_keyring(generator, vec![c1, c2]).await; + + // Cover both the single-EDK case and the multi-EDK case so that the + // "Count, then Entries" structure is exercised with the count field set + // to both 1 and 3. + for (label, keyring, expected_count) in [ + ("single", single, 1u16), + ("triple", triple, 3u16), + ] { + for version in VERSIONS { + //= specification/data-format/message-header.md#encrypted-data-keys + //= type=test + //# The Encrypted Data Keys MUST consist of, in order, + //# Encrypted Data Key Count, + //# and Encrypted Data Key Entries. + let ct = encrypt_with_version(b"ordering test", version, keyring.clone()).await; + let edk_section_start = skip_to_edk_section(&ct, version); + let parsed = parse_edk_section(&ct, version); + + // 1. Encrypted Data Key Count (2 bytes) + let count = u16::from_be_bytes([ct[edk_section_start], ct[edk_section_start + 1]]); + assert_eq!(count, expected_count, "{label} {version:?}: count field value"); + assert_eq!(parsed.edk_count, count); + + // 2. Encrypted Data Key Entries (immediately after the count) + let entries_start = edk_section_start + 2; + let first_pid_len = u16::from_be_bytes([ct[entries_start], ct[entries_start + 1]]); + assert_eq!( + first_pid_len, parsed.edks[0].provider_id_len, + "{label} {version:?}: EDK entries must immediately follow the count field" + ); + assert_eq!( + parsed.edks.len(), + count as usize, + "{label} {version:?}: parsed entries must match count" + ); + } } } #[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_interpreted_as_uint16() { +async fn test_edk_count_is_big_endian_uint16() { let generator = aes_keyring(0).await; let child = aes_keyring(1).await; let mk = multi_keyring(generator, vec![child]).await; for version in VERSIONS { - let ct = encrypt_with_version(b"uint16 count", version, mk.clone()).await; + let ct = encrypt_with_version(b"count uint16", version, mk.clone()).await; let offset = skip_to_edk_section(&ct, version); //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test + //# The length of the serialized encrypted data key count MUST be 2 bytes. + // + //= specification/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. - // Big-endian UInt16: high byte should be 0, low byte should be 2 - assert_eq!( - ct[offset], 0x00, - "{version:?}: high byte of UInt16 count must be 0 for small counts" - ); - assert_eq!( - ct[offset + 1], - 0x02, - "{version:?}: low byte of UInt16 count must be 2 for two keyrings" - ); + // 2 keyrings → count bytes must be [0x00, 0x02] (big-endian UInt16 for value 2). + let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); + assert_eq!(count, 2, "{version:?}: big-endian UInt16 count for 2 keyrings"); + assert_eq!(ct[offset], 0x00, "{version:?}: high byte"); + assert_eq!(ct[offset + 1], 0x02, "{version:?}: low byte"); } } @@ -98,7 +88,7 @@ async fn test_edk_count_zero_rejected_on_decrypt() { for version in VERSIONS { let mut ct = encrypt_with_version(b"zero count", version, keyring.clone()).await; let offset = skip_to_edk_section(&ct, version); - // Tamper: set count to 0 + // Tamper: set count to 0. ct[offset] = 0x00; ct[offset + 1] = 0x00; let mut dec = @@ -114,15 +104,9 @@ async fn test_edk_count_zero_rejected_on_decrypt() { let err = decrypt(&dec) .await .expect_err(&format!("{version:?}: decrypt must reject EDK count of 0")); - // Setting count=0 corrupts the header structure: either the count=0 check fires, or - // subsequent bytes are misinterpreted by the parser. Both outcomes are valid structural - // rejections — assert this is a SerializationError (parse/structural failure), not a - // generic unrelated failure. assert!( - matches!(err.kind, aws_esdk::ErrorKind::SerializationError) - || err.message.to_lowercase().contains("empty") - || err.message.to_lowercase().contains("encrypted data key"), - "{version:?}: error must indicate a structural/EDK failure, got: {} ({:?})", + matches!(err.kind, aws_esdk::ErrorKind::SerializationError), + "{version:?}: expected SerializationError, got: {} ({:?})", err.message, err.kind ); } @@ -244,7 +228,7 @@ async fn test_edk_entry_field_order() { let _edk = &ct[pos..pos + edk_len as usize]; pos += edk_len as usize; - // Verify we consumed exactly one entry and the position matches the parser + // Verify we consumed exactly one entry and the position matches the parser. let parsed = parse_edk_section(&ct, version); assert_eq!( pos, parsed.end_offset, @@ -253,35 +237,6 @@ async fn test_edk_entry_field_order() { } } -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_matches_entries() { - let generator = aes_keyring(0).await; - let c1 = aes_keyring(1).await; - let c2 = aes_keyring(2).await; - let mk = multi_keyring(generator, vec![c1, c2]).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"3 edks", version, mk.clone()).await; - let parsed = parse_edk_section(&ct, version); - - //= specification/data-format/message-header.md#encrypted-data-keys - //= type=test - //= reason=Using a multi-keyring with 3 keyrings verifies the serialized count matches the number of entries that follow, covering the "Count, then Entries" structure. - //# The Encrypted Data Keys MUST consist of, in order, - //# Encrypted Data Key Count, - //# and Encrypted Data Key Entries. - assert_eq!( - parsed.edk_count, 3, - "{version:?}: multi-keyring with 3 keyrings must produce 3 EDKs" - ); - assert_eq!( - parsed.edks.len(), - 3, - "{version:?}: parsed entries must match count" - ); - } -} - #[tokio::test(flavor = "multi_thread")] async fn test_edk_entries_preserve_keyring_order() { let generator = aes_keyring(0).await; @@ -311,35 +266,7 @@ async fn test_edk_entries_preserve_keyring_order() { } #[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_id_length_is_2_bytes() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"pid len 2 bytes", version, keyring.clone()).await; - let edk_start = skip_to_edk_section(&ct, version) + 2; - - //= specification/data-format/message-header.md#key-provider-id-length - //= type=test - //# The length of the serialized key provider ID length field MUST be 2 bytes. - // The first 2 bytes of the entry are the provider ID length field - let pid_len_bytes = &ct[edk_start..edk_start + 2]; - assert_eq!( - pid_len_bytes.len(), - 2, - "{version:?}: key provider ID length field must be exactly 2 bytes" - ); - let pid_len = u16::from_be_bytes([pid_len_bytes[0], pid_len_bytes[1]]); - let (expected_ns, _) = namespace_and_name(0); - assert_eq!( - pid_len as usize, - expected_ns.len(), - "{version:?}: provider ID length must equal the namespace string length" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_id_length_interpreted_as_uint16() { +async fn test_key_provider_id_length_is_big_endian_uint16() { let keyring = aes_keyring(0).await; for version in VERSIONS { @@ -350,17 +277,15 @@ async fn test_key_provider_id_length_interpreted_as_uint16() { //= specification/data-format/message-header.md#key-provider-id-length //= type=test + //# The length of the serialized key provider ID length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-id-length //# The key provider ID length MUST be interpreted as a UInt16. - // Verify big-endian UInt16 encoding - assert_eq!( - ct[edk_start], - (expected_len >> 8) as u8, - "{version:?}: high byte of UInt16 provider ID length" - ); + // The first 2 bytes of the entry are the big-endian UInt16 provider ID length. + let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); assert_eq!( - ct[edk_start + 1], - (expected_len & 0xFF) as u8, - "{version:?}: low byte of UInt16 provider ID length" + pid_len, expected_len, + "{version:?}: big-endian UInt16 provider ID length must match namespace length" ); } } @@ -412,56 +337,29 @@ async fn test_key_provider_id_is_utf8() { } #[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_info_length_is_2_bytes() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"pinfo len 2 bytes", version, keyring.clone()).await; - let parsed = parse_edk_section(&ct, version); - // The provider info length was parsed as 2 bytes by our parser. - // Verify it's consistent by checking the raw bytes at the expected offset. - let edk_start = parsed.edk_count_offset + 2; - let pid_len = parsed.edks[0].provider_id_len as usize; - let pinfo_len_offset = edk_start + 2 + pid_len; // skip pid_len field + pid bytes - - //= specification/data-format/message-header.md#key-provider-information-length - //= type=test - //# The length of the serialized key provider information length field MUST be 2 bytes. - let pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); - assert_eq!( - pinfo_len, parsed.edks[0].provider_info_len, - "{version:?}: provider info length field must be 2 bytes wide" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_info_length_interpreted_as_uint16() { +async fn test_key_provider_info_length_is_big_endian_uint16() { let keyring = aes_keyring(0).await; for version in VERSIONS { let ct = encrypt_with_version(b"pinfo len uint16", version, keyring.clone()).await; let edk_start = skip_to_edk_section(&ct, version) + 2; - // Walk the wire bytes: provider_id_len (2) + provider_id (pid_len) → then provider_info_len at that offset + // Walk to the provider_info_len offset: pid_len field (2) + pid bytes. let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); let pinfo_len_offset = edk_start + 2 + pid_len as usize; - let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); let parsed = parse_edk_section(&ct, version); let edk = &parsed.edks[0]; //= specification/data-format/message-header.md#key-provider-information-length //= type=test + //# The length of the serialized key provider information length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. - // Decoding the length directly from the wire bytes as big-endian UInt16 must match - // the parser's interpretation and the actual provider info byte length. + let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); assert_eq!( wire_pinfo_len as usize, edk.provider_info.len(), - "{version:?}: big-endian UInt16 read from wire bytes must equal actual provider info length" - ); - assert_eq!( - wire_pinfo_len, edk.provider_info_len, - "{version:?}: wire UInt16 must match parser-interpreted UInt16" + "{version:?}: big-endian UInt16 provider info length must match actual byte length" ); } } @@ -497,7 +395,7 @@ async fn test_key_provider_info_interpreted_as_bytes() { let ct = encrypt_with_version(b"pinfo bytes", version, keyring.clone()).await; let parsed = parse_edk_section(&ct, version); let edk = &parsed.edks[0]; - // Provider info for raw AES keyring starts with the key name + // Provider info for raw AES keyring starts with the key name. let (_, expected_name) = namespace_and_name(0); //= specification/data-format/message-header.md#key-provider-information @@ -507,64 +405,34 @@ async fn test_key_provider_info_interpreted_as_bytes() { edk.provider_info.starts_with(expected_name.as_bytes()), "{version:?}: provider info must start with the known key name" ); - // Round-trip proves the bytes are correctly interpreted - let mut dec = - DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), keyring.clone()); - if let Version::V1 = version { - dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; - } - let result = decrypt(&dec).await.unwrap(); - assert_eq!(result.plaintext, b"pinfo bytes"); } } #[tokio::test(flavor = "multi_thread")] -async fn test_edk_length_field_is_2_bytes() { +async fn test_edk_length_is_big_endian_uint16() { let keyring = aes_keyring(0).await; for version in VERSIONS { - let ct = encrypt_with_version(b"edk len 2 bytes", version, keyring.clone()).await; + let ct = encrypt_with_version(b"edk len uint16", version, keyring.clone()).await; let parsed = parse_edk_section(&ct, version); let edk = &parsed.edks[0]; - // Walk to the EDK length field offset manually + // Walk to the EDK length field. let edk_start = parsed.edk_count_offset + 2; let edk_len_offset = edk_start - + 2 + edk.provider_id_len as usize // pid_len field + pid bytes - + 2 + edk.provider_info_len as usize; // pinfo_len field + pinfo bytes + + 2 + edk.provider_id_len as usize + + 2 + edk.provider_info_len as usize; //= specification/data-format/message-header.md#encrypted-data-key-length //= type=test //# The length of the serialized encrypted data key length field MUST be 2 bytes. - let edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); - assert_eq!( - edk_len, edk.edk_len, - "{version:?}: encrypted data key length field must be exactly 2 bytes" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_length_interpreted_as_uint16() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"edk len uint16", version, keyring.clone()).await; - let parsed = parse_edk_section(&ct, version); - let edk = &parsed.edks[0]; - + // //= specification/data-format/message-header.md#encrypted-data-key-length - //= type=test //# The encrypted data key length MUST be interpreted as a UInt16. - // The UInt16 value must match the actual encrypted data key byte length + let wire_edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); assert_eq!( - edk.edk_len as usize, + wire_edk_len as usize, edk.edk.len(), - "{version:?}: UInt16 EDK length must match actual EDK bytes" - ); - // For AES-GCM wrapping, the EDK is non-trivially sized (IV + ciphertext + tag) - assert!( - edk.edk_len > 0, - "{version:?}: encrypted data key must have positive length" + "{version:?}: big-endian UInt16 EDK length must match actual EDK byte length" ); } } @@ -592,60 +460,6 @@ async fn test_edk_length_matches_field() { } } -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_interpreted_as_bytes() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"edk as bytes", version, keyring.clone()).await; - let mut dec = - DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), keyring.clone()); - if let Version::V1 = version { - dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; - } - - //= specification/data-format/message-header.md#encrypted-data-key - //= type=test - //# The encrypted data key MUST be interpreted as bytes. - let result = decrypt(&dec).await.unwrap(); - assert_eq!( - result.plaintext, b"edk as bytes", - "{version:?}: round-trip proves EDK bytes are correctly interpreted" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_bytes_are_nonempty_ciphertext() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"edk nonempty", version, keyring.clone()).await; - let parsed = parse_edk_section(&ct, version); - let edk = &parsed.edks[0]; - - //= specification/data-format/message-header.md#encrypted-data-key - //= type=test - //= reason=Verifying the EDK contains non-zero bytes proves it holds actual encrypted key material, not a placeholder. - //# The encrypted data key MUST be interpreted as bytes. - assert!( - !edk.edk.is_empty(), - "{version:?}: encrypted data key must not be empty" - ); - // AES-GCM wrapping produces at minimum IV (12 bytes) + tag (16 bytes) = 28 bytes - assert!( - edk.edk.len() >= 28, - "{version:?}: EDK must be at least 28 bytes (AES-GCM IV + tag), got {}", - edk.edk.len() - ); - // The EDK should contain actual ciphertext (not all zeros) - assert!( - edk.edk.iter().any(|&b| b != 0), - "{version:?}: encrypted data key must contain non-zero bytes (actual ciphertext)" - ); - } -} - #[tokio::test(flavor = "multi_thread")] async fn test_multi_keyring_round_trip_each_child() { let generator = aes_keyring(0).await; @@ -656,7 +470,7 @@ async fn test_multi_keyring_round_trip_each_child() { for version in VERSIONS { let ct = encrypt_with_version(b"multi rt", version, mk.clone()).await; - // Each individual keyring should be able to decrypt + // Each individual keyring must be able to decrypt. for kr in [generator.clone(), c1.clone(), c2.clone()] { let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), kr); if let Version::V1 = version { From b448f048e7000681e76ac18355f81163b80c9f56 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:52:22 -0700 Subject: [PATCH 08/22] docs(native-rust): sync serializable_types/serialize_functions comments from unreviewed --- esdk/src/message/serializable_types.rs | 12 ++++++++++++ esdk/src/message/serialize_functions.rs | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/esdk/src/message/serializable_types.rs b/esdk/src/message/serializable_types.rs index 5432fceb5..34dc346a4 100644 --- a/esdk/src/message/serializable_types.rs +++ b/esdk/src/message/serializable_types.rs @@ -1,15 +1,21 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + //! Type aliases and helper functions for message serialization. use crate::types::EncryptionContext; use aws_mpl_legacy::EncryptedDataKey; use aws_mpl_legacy::suites::AlgorithmSuite; +/// Unordered encryption context from the public API. pub(crate) type ESDKEncryptionContext = EncryptionContext; pub(crate) type ESDKEncryptionContextPair = (String, String); +/// Sorted-by-key encryption context used for on-wire serialization. pub(crate) type ESDKCanonicalEncryptionContext = Vec; +/// Max total key-value-pairs body length: the outer UInt16 length field (2 bytes) +/// plus the count UInt16 (2 bytes) must together still fit in a UInt16, leaving +/// u16::MAX - 2 for the payload. const ESDK_CANONICAL_ENCRYPTION_CONTEXT_MAX_LENGTH: u64 = u16::MAX as u64 - 2; pub(crate) const fn get_iv_length(a: &AlgorithmSuite) -> u8 { @@ -45,6 +51,7 @@ pub(crate) const fn get_encrypt_key_length(a: &AlgorithmSuite) -> u8 { // 2 for the value length // Uint16-2-2-1 for the value data +/// Serialized byte length of the key-value-pairs body (no outer length prefix). pub(crate) fn length(encryption_context: &ESDKEncryptionContext) -> u64 { let mut length: usize = 0; for (key, value) in encryption_context { @@ -53,6 +60,7 @@ pub(crate) fn length(encryption_context: &ESDKEncryptionContext) -> u64 { length as u64 } +/// Sort by key to produce the canonical on-wire ordering. pub(crate) fn to_canonical_pairs( encryption_context: ESDKEncryptionContext, ) -> ESDKCanonicalEncryptionContext { @@ -69,6 +77,8 @@ pub(crate) fn from_canonical_pairs(pairs: ESDKCanonicalEncryptionContext) -> ESD map } +/// True iff `ec` fits the on-wire encoding: pair count, each key/value length, +/// and total serialized length all fit in a UInt16. pub(crate) fn is_esdk_encryption_context(ec: &EncryptionContext) -> bool { if ec.len() >= usize::from(u16::MAX) { return false; @@ -87,12 +97,14 @@ pub(crate) fn is_esdk_encryption_context(ec: &EncryptionContext) -> bool { true } +/// True iff every EDK field length fits in a UInt16. pub(crate) fn is_esdk_encrypted_data_key(edk: &EncryptedDataKey) -> bool { u16::try_from(edk.key_provider_id.len()).is_ok() && u16::try_from(edk.key_provider_info.len()).is_ok() && u16::try_from(edk.ciphertext.len()).is_ok() } +/// True iff the EDK count and each entry fit in UInt16. pub(crate) fn is_esdk_encrypted_data_keys(edks: &[EncryptedDataKey]) -> bool { if edks.len() >= usize::from(u16::MAX) { return false; diff --git a/esdk/src/message/serialize_functions.rs b/esdk/src/message/serialize_functions.rs index 51d3e70a3..3268f90e0 100644 --- a/esdk/src/message/serialize_functions.rs +++ b/esdk/src/message/serialize_functions.rs @@ -1,6 +1,10 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 //! Low-level byte read/write primitives for message serialization. +//! +//! The `raw` SafeWrite parameter on every `read_*` function tees the consumed +//! bytes into a mirror buffer so callers can reconstruct the exact raw header +//! bytes used for authentication and signing. use super::{Error, ser_err}; use crate::error::ErrorKind; @@ -26,6 +30,7 @@ fn ser_io(e: std::io::Error) -> Error { } } +/// Read up to `buf.len()` bytes; returns the number actually read (may be < len on EOF). pub(crate) fn read_up_to(this: &mut dyn SafeRead, buf: &mut [u8]) -> Result { let mut curr: usize = 0; loop { @@ -45,6 +50,7 @@ pub(crate) fn read_up_to(this: &mut dyn SafeRead, buf: &mut [u8]) -> Result Result<(), Erro Ok(()) } +// Big-endian fixed-width writers. pub(crate) fn write_u8(w: &mut dyn SafeWrite, data: u8) -> Result<(), Error> { write_bytes(w, &[data]) } @@ -90,6 +97,7 @@ pub(crate) fn write_u32(w: &mut dyn SafeWrite, data: u32) -> Result<(), Error> { write_bytes(w, &data.to_be_bytes()) } +/// Read exactly `buf.len()` bytes and mirror them into `raw`. pub(crate) fn read_bytes( r: &mut dyn SafeRead, buf: &mut [u8], @@ -99,6 +107,7 @@ pub(crate) fn read_bytes( write_bytes(raw, buf) } +/// Read exactly `length` bytes into a fresh Vec. pub(crate) fn read_vec( r: &mut dyn SafeRead, length: usize, @@ -109,12 +118,15 @@ pub(crate) fn read_vec( Ok(result) } +// Big-endian fixed-width readers. Each mirrors the consumed bytes into `raw`. pub(crate) fn read_u8(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { let mut result = [0u8; 1]; read_bytes(r, &mut result, raw)?; Ok(result[0]) } +/// Read one byte, returning `Ok(None)` on clean EOF. Does NOT mirror into a +/// raw buffer (used for streaming peek). pub(crate) fn read_opt_u8(r: &mut dyn SafeRead) -> Result, Error> { let mut result = [0u8; 1]; match r.read_exact(&mut result) { @@ -142,6 +154,7 @@ pub(crate) fn read_u64(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result< Ok(u64::from_be_bytes(result)) } +/// Read a UInt16 length prefix followed by that many bytes. pub(crate) fn read_seq_u16( r: &mut dyn SafeRead, raw: &mut dyn SafeWrite, @@ -150,6 +163,8 @@ pub(crate) fn read_seq_u16( read_vec(r, usize::from(len), raw) } +/// Read a UInt32 length prefix followed by that many bytes into `data`, +/// rejecting lengths above `bound`. pub(crate) fn read_seq_u32_bounded( r: &mut dyn SafeRead, bound: u32, @@ -168,6 +183,8 @@ pub(crate) fn read_seq_u32_bounded( read_bytes(r, &mut data[..], raw) } +/// Read a UInt64 length prefix followed by that many bytes, rejecting lengths +/// above `bound`. pub(crate) fn read_seq_u64_bounded( r: &mut dyn SafeRead, bound: u64, @@ -184,6 +201,7 @@ pub(crate) fn read_seq_u64_bounded( read_vec(r, len_usize, raw) } +/// Read a UInt16-prefixed UTF-8 string. pub(crate) fn read_str_u16(r: &mut dyn SafeRead, raw: &mut dyn SafeWrite) -> Result { let len = read_u16(r, raw)?; let result = read_vec(r, usize::from(len), raw)?; From 72d8818317d25e0cc91e56135fa6959f5a6df25d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:53:10 -0700 Subject: [PATCH 09/22] test(native-rust): sync AAD order section pattern from unreviewed --- esdk/tests/test_encryption_context_aad.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esdk/tests/test_encryption_context_aad.rs b/esdk/tests/test_encryption_context_aad.rs index c73196211..6c5373dde 100644 --- a/esdk/tests/test_encryption_context_aad.rs +++ b/esdk/tests/test_encryption_context_aad.rs @@ -51,10 +51,11 @@ async fn test_aad_serialization_order() { //# Key Value Pairs Length, //# and Key Value Pairs. - // Primary assertion: the encrypt path lays out KVP Length first, followed by KVP data. + // 1. Key Value Pairs Length (2 bytes at the AAD offset) let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; assert!(kvp_len > 0, "{version:?}: non-empty EC must have non-zero KVP length"); - // KVP data follows immediately after the 2-byte length field (count is first). + + // 2. Key Value Pairs (immediately follow the length field) let kvp_count_offset = off + 2; let kvp_count = u16::from_be_bytes([ct[kvp_count_offset], ct[kvp_count_offset + 1]]) as usize; From 1cec139b959775b77000121cd8ab5ceb4b2b1e74 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 10:58:59 -0700 Subject: [PATCH 10/22] test(native-rust): sync EDK annotation-above-assert placement from unreviewed --- esdk/tests/test_encrypted_data_keys.rs | 32 +++++++++++++++----------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index 16ee17aa3..4344b830f 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -67,14 +67,15 @@ async fn test_edk_count_is_big_endian_uint16() { let ct = encrypt_with_version(b"count uint16", version, mk.clone()).await; let offset = skip_to_edk_section(&ct, version); + // 2 keyrings → count bytes must be [0x00, 0x02] (big-endian UInt16 for value 2). + let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); + //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test //# The length of the serialized encrypted data key count MUST be 2 bytes. // //= specification/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. - // 2 keyrings → count bytes must be [0x00, 0x02] (big-endian UInt16 for value 2). - let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); assert_eq!(count, 2, "{version:?}: big-endian UInt16 count for 2 keyrings"); assert_eq!(ct[offset], 0x00, "{version:?}: high byte"); assert_eq!(ct[offset + 1], 0x02, "{version:?}: low byte"); @@ -118,14 +119,15 @@ async fn test_edk_count_max_enforcement_encrypt() { let child = aes_keyring(1).await; let mk = multi_keyring(generator, vec![child]).await; - //= specification/data-format/message-header.md#encrypted-data-key-count - //= type=test - //= reason=max_encrypted_data_keys on encrypt enforces the upper bound on EDK count before serialization. - //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. let mut input = EncryptInput::with_legacy_keyring(b"max edk encrypt", EncryptionContext::new(), mk); input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); let err = encrypt(&input).await.expect_err("encrypt must fail when EDK count exceeds max"); + + //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test + //= reason=max_encrypted_data_keys on encrypt enforces the upper bound on EDK count before serialization. + //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. assert!( err.message.contains("exceed") && err.message.contains("maximum"), "error must indicate EDK count exceeds maximum, got: {} ({:?})", @@ -142,12 +144,12 @@ async fn test_edk_count_max_enforcement_decrypt() { let ct = encrypt_with_version(b"max edk decrypt", Version::V2, mk.clone()).await; let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), mk); dec.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); + let err = decrypt(&dec).await.expect_err("decrypt must fail when EDK count exceeds max"); //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test //= reason=max_encrypted_data_keys on decrypt enforces the upper bound when deserializing the header. //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. - let err = decrypt(&dec).await.expect_err("decrypt must fail when EDK count exceeds max"); assert!( err.message.contains("exceed") && err.message.contains("maximum"), "error must indicate EDK count exceeds maximum, got: {} ({:?})", @@ -161,13 +163,14 @@ async fn test_edk_count_at_max_succeeds() { let child = aes_keyring(1).await; let mk = multi_keyring(generator, vec![child]).await; + let mut input = + EncryptInput::with_legacy_keyring(b"at max", EncryptionContext::new(), mk); + input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(2).unwrap()); + //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test //= reason=Setting max equal to actual count verifies the less-than-or-equal semantics. //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. - let mut input = - EncryptInput::with_legacy_keyring(b"at max", EncryptionContext::new(), mk); - input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(2).unwrap()); assert!( encrypt(&input).await.is_ok(), "encrypt must succeed when EDK count equals max" @@ -275,14 +278,15 @@ async fn test_key_provider_id_length_is_big_endian_uint16() { let (expected_ns, _) = namespace_and_name(0); let expected_len = expected_ns.len() as u16; + // The first 2 bytes of the entry are the big-endian UInt16 provider ID length. + let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); + //= specification/data-format/message-header.md#key-provider-id-length //= type=test //# The length of the serialized key provider ID length field MUST be 2 bytes. // //= specification/data-format/message-header.md#key-provider-id-length //# The key provider ID length MUST be interpreted as a UInt16. - // The first 2 bytes of the entry are the big-endian UInt16 provider ID length. - let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); assert_eq!( pid_len, expected_len, "{version:?}: big-endian UInt16 provider ID length must match namespace length" @@ -348,6 +352,7 @@ async fn test_key_provider_info_length_is_big_endian_uint16() { let pinfo_len_offset = edk_start + 2 + pid_len as usize; let parsed = parse_edk_section(&ct, version); let edk = &parsed.edks[0]; + let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); //= specification/data-format/message-header.md#key-provider-information-length //= type=test @@ -355,7 +360,6 @@ async fn test_key_provider_info_length_is_big_endian_uint16() { // //= specification/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. - let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); assert_eq!( wire_pinfo_len as usize, edk.provider_info.len(), @@ -421,6 +425,7 @@ async fn test_edk_length_is_big_endian_uint16() { let edk_len_offset = edk_start + 2 + edk.provider_id_len as usize + 2 + edk.provider_info_len as usize; + let wire_edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); //= specification/data-format/message-header.md#encrypted-data-key-length //= type=test @@ -428,7 +433,6 @@ async fn test_edk_length_is_big_endian_uint16() { // //= specification/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. - let wire_edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); assert_eq!( wire_edk_len as usize, edk.edk.len(), From afc18583f254c3a848f0e0d3c63866e79a9d6df2 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Fri, 1 May 2026 11:23:12 -0700 Subject: [PATCH 11/22] test(native-rust): sync EDK test dedup from unreviewed --- esdk/tests/test_encrypted_data_keys.rs | 309 ++++++++----------------- 1 file changed, 97 insertions(+), 212 deletions(-) diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index 4344b830f..1146cfe04 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -58,17 +58,28 @@ async fn test_encrypted_data_keys_ordering() { } #[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_is_big_endian_uint16() { +async fn test_edk_section_length_fields_are_big_endian_uint16() { let generator = aes_keyring(0).await; let child = aes_keyring(1).await; let mk = multi_keyring(generator, vec![child]).await; + let (expected_ns, _) = namespace_and_name(0); + let expected_pid_len = expected_ns.len() as u16; for version in VERSIONS { - let ct = encrypt_with_version(b"count uint16", version, mk.clone()).await; - let offset = skip_to_edk_section(&ct, version); + let ct = encrypt_with_version(b"length fields uint16", version, mk.clone()).await; + let parsed = parse_edk_section(&ct, version); + let edk = &parsed.edks[0]; - // 2 keyrings → count bytes must be [0x00, 0x02] (big-endian UInt16 for value 2). - let count = u16::from_be_bytes([ct[offset], ct[offset + 1]]); + // Decode each length field directly from the wire as a big-endian UInt16. + let entries_start = parsed.edk_count_offset + 2; + let pid_len_offset = entries_start; + let pinfo_len_offset = entries_start + 2 + edk.provider_id_len as usize; + let edk_len_offset = pinfo_len_offset + 2 + edk.provider_info_len as usize; + + let count_wire = u16::from_be_bytes([ct[parsed.edk_count_offset], ct[parsed.edk_count_offset + 1]]); + let pid_len_wire = u16::from_be_bytes([ct[pid_len_offset], ct[pid_len_offset + 1]]); + let pinfo_len_wire = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); + let edk_len_wire = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test @@ -76,9 +87,38 @@ async fn test_edk_count_is_big_endian_uint16() { // //= specification/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. - assert_eq!(count, 2, "{version:?}: big-endian UInt16 count for 2 keyrings"); - assert_eq!(ct[offset], 0x00, "{version:?}: high byte"); - assert_eq!(ct[offset + 1], 0x02, "{version:?}: low byte"); + // + //= specification/data-format/message-header.md#key-provider-id-length + //= type=test + //# The length of the serialized key provider ID length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-id-length + //# The key provider ID length MUST be interpreted as a UInt16. + // + //= specification/data-format/message-header.md#key-provider-information-length + //= type=test + //# The length of the serialized key provider information length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#key-provider-information-length + //# The key provider information length MUST be interpreted as a UInt16. + // + //= specification/data-format/message-header.md#encrypted-data-key-length + //= type=test + //# The length of the serialized encrypted data key length field MUST be 2 bytes. + // + //= specification/data-format/message-header.md#encrypted-data-key-length + //# The encrypted data key length MUST be interpreted as a UInt16. + // Count: 2 keyrings → UInt16 value 2 ([0x00, 0x02]). + assert_eq!(count_wire, 2, "{version:?}: EDK count UInt16"); + assert_eq!(ct[parsed.edk_count_offset], 0x00, "{version:?}: count high byte"); + assert_eq!(ct[parsed.edk_count_offset + 1], 0x02, "{version:?}: count low byte"); + // Provider ID length: equals the known keyring namespace byte length. + assert_eq!(pid_len_wire, expected_pid_len, "{version:?}: provider ID length UInt16"); + // Info and EDK lengths: positive, non-tautological lower bounds. + // Raw AES keyring stores IV in provider_info; the ciphertext field is + // wrapped data key (32 bytes) + GCM tag (16 bytes) = 48. + assert!(pinfo_len_wire > 0, "{version:?}: provider info length UInt16 must be positive"); + assert_eq!(edk_len_wire, 48, "{version:?}: EDK ciphertext length must be 48 (wrapped 32B key + 16B tag)"); } } @@ -114,65 +154,43 @@ async fn test_edk_count_zero_rejected_on_decrypt() { } #[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_max_enforcement_encrypt() { +async fn test_edk_count_max_enforcement() { let generator = aes_keyring(0).await; let child = aes_keyring(1).await; let mk = multi_keyring(generator, vec![child]).await; - let mut input = - EncryptInput::with_legacy_keyring(b"max edk encrypt", EncryptionContext::new(), mk); - input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); - let err = encrypt(&input).await.expect_err("encrypt must fail when EDK count exceeds max"); + let expect_exceed = |err: &aws_esdk::Error| { + assert!( + err.message.contains("exceed") && err.message.contains("maximum"), + "error must indicate EDK count exceeds maximum, got: {} ({:?})", + err.message, err.kind + ); + }; //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test - //= reason=max_encrypted_data_keys on encrypt enforces the upper bound on EDK count before serialization. //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. - assert!( - err.message.contains("exceed") && err.message.contains("maximum"), - "error must indicate EDK count exceeds maximum, got: {} ({:?})", - err.message, err.kind - ); -} -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_max_enforcement_decrypt() { - let generator = aes_keyring(0).await; - let child = aes_keyring(1).await; - let mk = multi_keyring(generator, vec![child]).await; - - let ct = encrypt_with_version(b"max edk decrypt", Version::V2, mk.clone()).await; - let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), mk); - dec.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); - let err = decrypt(&dec).await.expect_err("decrypt must fail when EDK count exceeds max"); - - //= specification/data-format/message-header.md#encrypted-data-key-count - //= type=test - //= reason=max_encrypted_data_keys on decrypt enforces the upper bound when deserializing the header. - //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. - assert!( - err.message.contains("exceed") && err.message.contains("maximum"), - "error must indicate EDK count exceeds maximum, got: {} ({:?})", - err.message, err.kind + // Encrypt with 2 EDKs and max=1 → error. + let mut enc_over = EncryptInput::with_legacy_keyring( + b"max edk encrypt", + EncryptionContext::new(), + mk.clone(), ); -} + enc_over.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); + expect_exceed(&encrypt(&enc_over).await.expect_err("encrypt must fail when EDK count exceeds max")); -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_count_at_max_succeeds() { - let generator = aes_keyring(0).await; - let child = aes_keyring(1).await; - let mk = multi_keyring(generator, vec![child]).await; - - let mut input = - EncryptInput::with_legacy_keyring(b"at max", EncryptionContext::new(), mk); - input.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(2).unwrap()); + // Decrypt a 2-EDK message with max=1 → error. + let ct = encrypt_with_version(b"max edk decrypt", Version::V2, mk.clone()).await; + let mut dec_over = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), mk.clone()); + dec_over.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(1).unwrap()); + expect_exceed(&decrypt(&dec_over).await.expect_err("decrypt must fail when EDK count exceeds max")); - //= specification/data-format/message-header.md#encrypted-data-key-count - //= type=test - //= reason=Setting max equal to actual count verifies the less-than-or-equal semantics. - //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. + // Encrypt with 2 EDKs and max=2 → ok (the "equal to" side of ≤). + let mut enc_at = EncryptInput::with_legacy_keyring(b"at max", EncryptionContext::new(), mk); + enc_at.max_encrypted_data_keys = Some(std::num::NonZeroUsize::new(2).unwrap()); assert!( - encrypt(&input).await.is_ok(), + encrypt(&enc_at).await.is_ok(), "encrypt must succeed when EDK count equals max" ); } @@ -269,47 +287,39 @@ async fn test_edk_entries_preserve_keyring_order() { } #[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_id_length_is_big_endian_uint16() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"pid len uint16", version, keyring.clone()).await; - let edk_start = skip_to_edk_section(&ct, version) + 2; - let (expected_ns, _) = namespace_and_name(0); - let expected_len = expected_ns.len() as u16; - - // The first 2 bytes of the entry are the big-endian UInt16 provider ID length. - let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); - - //= specification/data-format/message-header.md#key-provider-id-length - //= type=test - //# The length of the serialized key provider ID length field MUST be 2 bytes. - // - //= specification/data-format/message-header.md#key-provider-id-length - //# The key provider ID length MUST be interpreted as a UInt16. - assert_eq!( - pid_len, expected_len, - "{version:?}: big-endian UInt16 provider ID length must match namespace length" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_id_length_matches_field() { - let keyring = aes_keyring(0).await; +async fn test_edk_entry_lengths_match_fields() { + // Multi-keyring so multiple entries are checked per run. + let generator = aes_keyring(0).await; + let child = aes_keyring(1).await; + let mk = multi_keyring(generator, vec![child]).await; for version in VERSIONS { - let ct = encrypt_with_version(b"pid len match", version, keyring.clone()).await; + let ct = encrypt_with_version(b"entry lengths match", version, mk.clone()).await; let parsed = parse_edk_section(&ct, version); //= specification/data-format/message-header.md#key-provider-id //= type=test //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. - for edk in &parsed.edks { + // + //= specification/data-format/message-header.md#key-provider-information + //= type=test + //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. + // + //= specification/data-format/message-header.md#encrypted-data-key + //= type=test + //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. + for (i, edk) in parsed.edks.iter().enumerate() { + assert_eq!( + edk.provider_id.len(), edk.provider_id_len as usize, + "{version:?}: EDK {i}: provider ID byte length must equal the provider ID length field" + ); assert_eq!( - edk.provider_id.len(), - edk.provider_id_len as usize, - "{version:?}: provider ID byte length must equal the provider ID length field" + edk.provider_info.len(), edk.provider_info_len as usize, + "{version:?}: EDK {i}: provider info byte length must equal the provider info length field" + ); + assert_eq!( + edk.edk.len(), edk.edk_len as usize, + "{version:?}: EDK {i}: encrypted data key byte length must equal the EDK length field" ); } } @@ -340,57 +350,6 @@ async fn test_key_provider_id_is_utf8() { } } -#[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_info_length_is_big_endian_uint16() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"pinfo len uint16", version, keyring.clone()).await; - let edk_start = skip_to_edk_section(&ct, version) + 2; - // Walk to the provider_info_len offset: pid_len field (2) + pid bytes. - let pid_len = u16::from_be_bytes([ct[edk_start], ct[edk_start + 1]]); - let pinfo_len_offset = edk_start + 2 + pid_len as usize; - let parsed = parse_edk_section(&ct, version); - let edk = &parsed.edks[0]; - let wire_pinfo_len = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); - - //= specification/data-format/message-header.md#key-provider-information-length - //= type=test - //# The length of the serialized key provider information length field MUST be 2 bytes. - // - //= specification/data-format/message-header.md#key-provider-information-length - //# The key provider information length MUST be interpreted as a UInt16. - assert_eq!( - wire_pinfo_len as usize, - edk.provider_info.len(), - "{version:?}: big-endian UInt16 provider info length must match actual byte length" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_key_provider_info_length_matches_field() { - let generator = aes_keyring(0).await; - let child = aes_keyring(1).await; - let mk = multi_keyring(generator, vec![child]).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"pinfo len match", version, mk.clone()).await; - let parsed = parse_edk_section(&ct, version); - - //= specification/data-format/message-header.md#key-provider-information - //= type=test - //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. - for edk in &parsed.edks { - assert_eq!( - edk.provider_info.len(), - edk.provider_info_len as usize, - "{version:?}: provider info byte length must equal the provider info length field" - ); - } - } -} - #[tokio::test(flavor = "multi_thread")] async fn test_key_provider_info_interpreted_as_bytes() { let keyring = aes_keyring(0).await; @@ -411,77 +370,3 @@ async fn test_key_provider_info_interpreted_as_bytes() { ); } } - -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_length_is_big_endian_uint16() { - let keyring = aes_keyring(0).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"edk len uint16", version, keyring.clone()).await; - let parsed = parse_edk_section(&ct, version); - let edk = &parsed.edks[0]; - // Walk to the EDK length field. - let edk_start = parsed.edk_count_offset + 2; - let edk_len_offset = edk_start - + 2 + edk.provider_id_len as usize - + 2 + edk.provider_info_len as usize; - let wire_edk_len = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); - - //= specification/data-format/message-header.md#encrypted-data-key-length - //= type=test - //# The length of the serialized encrypted data key length field MUST be 2 bytes. - // - //= specification/data-format/message-header.md#encrypted-data-key-length - //# The encrypted data key length MUST be interpreted as a UInt16. - assert_eq!( - wire_edk_len as usize, - edk.edk.len(), - "{version:?}: big-endian UInt16 EDK length must match actual EDK byte length" - ); - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_edk_length_matches_field() { - let generator = aes_keyring(0).await; - let child = aes_keyring(1).await; - let mk = multi_keyring(generator, vec![child]).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"edk len match", version, mk.clone()).await; - let parsed = parse_edk_section(&ct, version); - - //= specification/data-format/message-header.md#encrypted-data-key - //= type=test - //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. - for (i, edk) in parsed.edks.iter().enumerate() { - assert_eq!( - edk.edk.len(), - edk.edk_len as usize, - "{version:?}: EDK {i}: encrypted data key byte length must equal the EDK length field" - ); - } - } -} - -#[tokio::test(flavor = "multi_thread")] -async fn test_multi_keyring_round_trip_each_child() { - let generator = aes_keyring(0).await; - let c1 = aes_keyring(1).await; - let c2 = aes_keyring(2).await; - let mk = multi_keyring(generator.clone(), vec![c1.clone(), c2.clone()]).await; - - for version in VERSIONS { - let ct = encrypt_with_version(b"multi rt", version, mk.clone()).await; - - // Each individual keyring must be able to decrypt. - for kr in [generator.clone(), c1.clone(), c2.clone()] { - let mut dec = DecryptInput::with_legacy_keyring(&ct, EncryptionContext::new(), kr); - if let Version::V1 = version { - dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; - } - let result = decrypt(&dec).await.unwrap(); - assert_eq!(result.plaintext, b"multi rt"); - } - } -} From 1694cb0c0f75678896e319843605fbca0550d9ac Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 4 May 2026 09:24:55 -0700 Subject: [PATCH 12/22] test(native-rust): sync split EDK spec citations from unreviewed --- esdk/tests/test_encrypted_data_keys.rs | 55 ++++++++++++++------------ 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index 1146cfe04..cd2beab99 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -81,44 +81,49 @@ async fn test_edk_section_length_fields_are_big_endian_uint16() { let pinfo_len_wire = u16::from_be_bytes([ct[pinfo_len_offset], ct[pinfo_len_offset + 1]]); let edk_len_wire = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); + // EDK count: 2 keyrings → UInt16 value 2 ([0x00, 0x02]). //= specification/data-format/message-header.md#encrypted-data-key-count //= type=test //# The length of the serialized encrypted data key count MUST be 2 bytes. // //= specification/data-format/message-header.md#encrypted-data-key-count + //= type=test //# The encrypted data key count MUST be interpreted as a UInt16. - // + assert_eq!(count_wire, 2, "{version:?}: EDK count UInt16 value"); + assert_eq!(ct[parsed.edk_count_offset], 0x00, "{version:?}: EDK count high byte"); + assert_eq!(ct[parsed.edk_count_offset + 1], 0x02, "{version:?}: EDK count low byte"); + + // Key provider ID length: the UInt16 at this offset equals the known keyring namespace byte length. //= specification/data-format/message-header.md#key-provider-id-length //= type=test //# The length of the serialized key provider ID length field MUST be 2 bytes. // //= specification/data-format/message-header.md#key-provider-id-length + //= type=test //# The key provider ID length MUST be interpreted as a UInt16. - // + assert_eq!(pid_len_wire, expected_pid_len, "{version:?}: provider ID length UInt16 value"); + + // Key provider information length: the UInt16 at this offset must be positive for a raw AES keyring + // (which packs key name + bit length + IV length + IV into provider info). //= specification/data-format/message-header.md#key-provider-information-length //= type=test //# The length of the serialized key provider information length field MUST be 2 bytes. // //= specification/data-format/message-header.md#key-provider-information-length + //= type=test //# The key provider information length MUST be interpreted as a UInt16. - // + assert!(pinfo_len_wire > 0, "{version:?}: provider info length UInt16 must be positive"); + + // Encrypted data key length: raw AES keyring stores IV in provider_info; the ciphertext field is + // wrapped data key (32 bytes) + GCM tag (16 bytes) = 48. //= specification/data-format/message-header.md#encrypted-data-key-length //= type=test //# The length of the serialized encrypted data key length field MUST be 2 bytes. // //= specification/data-format/message-header.md#encrypted-data-key-length + //= type=test //# The encrypted data key length MUST be interpreted as a UInt16. - // Count: 2 keyrings → UInt16 value 2 ([0x00, 0x02]). - assert_eq!(count_wire, 2, "{version:?}: EDK count UInt16"); - assert_eq!(ct[parsed.edk_count_offset], 0x00, "{version:?}: count high byte"); - assert_eq!(ct[parsed.edk_count_offset + 1], 0x02, "{version:?}: count low byte"); - // Provider ID length: equals the known keyring namespace byte length. - assert_eq!(pid_len_wire, expected_pid_len, "{version:?}: provider ID length UInt16"); - // Info and EDK lengths: positive, non-tautological lower bounds. - // Raw AES keyring stores IV in provider_info; the ciphertext field is - // wrapped data key (32 bytes) + GCM tag (16 bytes) = 48. - assert!(pinfo_len_wire > 0, "{version:?}: provider info length UInt16 must be positive"); - assert_eq!(edk_len_wire, 48, "{version:?}: EDK ciphertext length must be 48 (wrapped 32B key + 16B tag)"); + assert_eq!(edk_len_wire, 48, "{version:?}: EDK ciphertext length UInt16 value (wrapped 32B key + 16B tag)"); } } @@ -297,26 +302,26 @@ async fn test_edk_entry_lengths_match_fields() { let ct = encrypt_with_version(b"entry lengths match", version, mk.clone()).await; let parsed = parse_edk_section(&ct, version); - //= specification/data-format/message-header.md#key-provider-id - //= type=test - //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. - // - //= specification/data-format/message-header.md#key-provider-information - //= type=test - //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. - // - //= specification/data-format/message-header.md#encrypted-data-key - //= type=test - //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. for (i, edk) in parsed.edks.iter().enumerate() { + //= specification/data-format/message-header.md#key-provider-id + //= type=test + //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. assert_eq!( edk.provider_id.len(), edk.provider_id_len as usize, "{version:?}: EDK {i}: provider ID byte length must equal the provider ID length field" ); + + //= specification/data-format/message-header.md#key-provider-information + //= type=test + //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. assert_eq!( edk.provider_info.len(), edk.provider_info_len as usize, "{version:?}: EDK {i}: provider info byte length must equal the provider info length field" ); + + //= specification/data-format/message-header.md#encrypted-data-key + //= type=test + //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. assert_eq!( edk.edk.len(), edk.edk_len as usize, "{version:?}: EDK {i}: encrypted data key byte length must equal the EDK length field" From 97e221f7ccb9a506eb00f410017a78fc5ab97bfb Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 4 May 2026 11:27:17 -0700 Subject: [PATCH 13/22] fix(native-rust): sync AAD length convention + defensive EDK checks from unreviewed --- esdk/src/message/encrypted_data_keys.rs | 84 +++++++++++++---------- esdk/src/message/encryption_context.rs | 48 +++++++++---- esdk/src/message/serializable_types.rs | 10 +-- esdk/tests/test_encrypted_data_keys.rs | 38 +++++----- esdk/tests/test_encryption_context_aad.rs | 28 ++++---- 5 files changed, 120 insertions(+), 88 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index d1fd194c3..508e8fb87 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -1,5 +1,6 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 + //! Encrypted data key serialization and deserialization. use super::serialize_functions::{read_seq_u16, read_str_u16, read_u16, write_bytes, write_u16}; @@ -7,22 +8,25 @@ use super::{Error, ser_err}; use crate::types::{SafeRead, SafeWrite}; use aws_mpl_legacy::EncryptedDataKey; -//= specification/client-apis/encrypt.md#v1-header -//# - MUST serialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). -//= specification/client-apis/encrypt.md#v2-header -//# - MUST serialize the [Encrypted Data Keys](../data-format/message-header.md#encrypted-data-keys). + pub(crate) fn write_edks(w: &mut dyn SafeWrite, edks: &[EncryptedDataKey]) -> Result<(), Error> { - //= specification/data-format/message-header.md#encrypted-data-keys + //= spec/data-format/message-header.md#encrypted-data-keys //# The Encrypted Data Keys MUST consist of, in order, //# Encrypted Data Key Count, //# and Encrypted Data Key Entries. // Encrypted Data Key Count - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count + //# This value MUST be greater than 0. + if edks.is_empty() { + return ser_err("Cannot serialize empty encrypted data keys list"); + } + + //= spec/data-format/message-header.md#encrypted-data-key-count //# The length of the serialized encrypted data key count MUST be 2 bytes. // - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. let Ok(edk_count) = u16::try_from(edks.len()) else { return ser_err("Count too large for UInt16"); @@ -38,7 +42,7 @@ pub(crate) fn write_edks(w: &mut dyn SafeWrite, edks: &[EncryptedDataKey]) -> Re } pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result<(), Error> { - //= specification/data-format/message-header.md#encrypted-data-key-entries + //= spec/data-format/message-header.md#encrypted-data-key-entries //# Each Encrypted Data Key Entry MUST consist of, in order, //# Key Provider ID Length, //# Key Provider ID, @@ -51,67 +55,67 @@ pub(crate) fn write_edk(w: &mut dyn SafeWrite, edk: &EncryptedDataKey) -> Result let kp_id_bytes = edk.key_provider_id.as_bytes(); - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //# The key provider ID length MUST be interpreted as a UInt16. let Ok(kp_id_len) = u16::try_from(kp_id_bytes.len()) else { return ser_err("Key provider ID length too long for 16 bits"); }; - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //# The length of the serialized key provider ID length field MUST be 2 bytes. write_u16(w, kp_id_len)?; // Key Provider ID - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. // - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. write_bytes(w, kp_id_bytes)?; // Key Provider Information Length - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. let Ok(kp_info_len) = u16::try_from(edk.key_provider_info.len()) else { return ser_err("Key provider info length too long for 16 bits"); }; - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //# The length of the serialized key provider information length field MUST be 2 bytes. write_u16(w, kp_info_len)?; // Key Provider Information - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. // - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //# The key provider information MUST be interpreted as bytes. write_bytes(w, &edk.key_provider_info)?; // Encrypted Data Key Length - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. let Ok(edk_len) = u16::try_from(edk.ciphertext.len()) else { return ser_err("Encrypted data key length too long for 16 bits"); }; - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //# The length of the serialized encrypted data key length field MUST be 2 bytes. write_u16(w, edk_len)?; // Encrypted Data Key - //= specification/data-format/message-header.md#encrypted-data-key + //= spec/data-format/message-header.md#encrypted-data-key //= reason=The length field is derived from the same byte slice that is serialized, so they are equal by construction. //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. // - //= specification/data-format/message-header.md#encrypted-data-key + //= spec/data-format/message-header.md#encrypted-data-key //= type=implication //# The encrypted data key MUST be interpreted as bytes. write_bytes(w, &edk.ciphertext) @@ -122,24 +126,30 @@ pub(crate) fn read_edks( max_edks: Option, raw: &mut dyn SafeWrite, ) -> Result, Error> { - //= specification/data-format/message-header.md#encrypted-data-keys + //= spec/data-format/message-header.md#encrypted-data-keys //# The Encrypted Data Keys MUST consist of, in order, //# Encrypted Data Key Count, //# and Encrypted Data Key Entries. // Encrypted Data Key Count - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //# The length of the serialized encrypted data key count MUST be 2 bytes. // - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //# The encrypted data key count MUST be interpreted as a UInt16. let count = read_u16(r, raw)?; + //= spec/data-format/message-header.md#encrypted-data-key-count + //# This value MUST be greater than 0. + if count == 0 { + return ser_err("Encrypted data key count must be greater than 0"); + } + if let Some(max_edks) = max_edks && usize::from(count) > max_edks.get() { - //= specification/client-apis/decrypt.md#v2-header-deserialization + //= spec/client-apis/decrypt.md#v2-header-deserialization //# If the number of [encrypted data keys](../framework/structures.md#encrypted-data-keys) //# deserialized from the [message header](../data-format/message-header.md) //# is greater than the [maximum number of encrypted data keys](client.md#maximum-number-of-encrypted-data-keys) configured in the [client](client.md), @@ -161,7 +171,7 @@ pub(crate) fn read_edk( r: &mut dyn SafeRead, raw: &mut dyn SafeWrite, ) -> Result { - //= specification/data-format/message-header.md#encrypted-data-key-entries + //= spec/data-format/message-header.md#encrypted-data-key-entries //# Each Encrypted Data Key Entry MUST consist of, in order, //# Key Provider ID Length, //# Key Provider ID, @@ -172,49 +182,49 @@ pub(crate) fn read_edk( // Key Provider ID Length and Key Provider ID - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //# The key provider ID length MUST be interpreted as a UInt16. // - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //# The length of the serialized key provider ID length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //= reason=read_str_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. // - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. let provider_id = read_str_u16(r, raw)?; // Key Provider Information Length and Key Provider Information - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //# The key provider information length MUST be interpreted as a UInt16. // - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //# The length of the serialized key provider information length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //= reason=read_seq_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. // - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //# The key provider information MUST be interpreted as bytes. let provider_info = read_seq_u16(r, raw)?; // Encrypted Data Key Length and Encrypted Data Key - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //# The encrypted data key length MUST be interpreted as a UInt16. // - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //# The length of the serialized encrypted data key length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#encrypted-data-key + //= spec/data-format/message-header.md#encrypted-data-key //= reason=read_seq_u16 reads a u16 length then that many bytes, so the length field and data are equal by construction. //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. // - //= specification/data-format/message-header.md#encrypted-data-key + //= spec/data-format/message-header.md#encrypted-data-key //= type=implication //# The encrypted data key MUST be interpreted as bytes. let ciphertext = read_seq_u16(r, raw)?; diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index e921185f7..ab89fb122 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -19,14 +19,28 @@ pub(crate) fn read_canonical_ec( return Ok(Vec::new()); } - // Count, then `count` (key, value) pairs. + // Count, then `count` (key, value) pairs. Track bytes consumed so we can + // reject a message whose length field disagrees with the parsed contents. let count = usize::from(read_u16(r, raw)?); + let mut consumed: usize = 2; // the Key Value Pair Count field we just read let mut result: ESDKCanonicalEncryptionContext = Vec::with_capacity(count); for _ in 0..count { let key = read_str_u16(r, raw)?; + consumed += 2 + key.len(); let value = read_str_u16(r, raw)?; + consumed += 2 + value.len(); result.push((key, value)); } + + // PROPOSED + //= spec/data-format/message-header.md#key-value-pairs-length + //# The key value pairs length value MUST equal the number of bytes consumed + //# by deserializing the Key Value Pairs field. A decryptor MUST reject a + //# message whose key value pairs length does not match the deserialized size. + if consumed != bytes { + return ser_err("Encryption context length field does not match parsed contents"); + } + Ok(result) } @@ -36,7 +50,7 @@ pub(crate) fn write_empty_ec_or_write_aad( data: &ESDKCanonicalEncryptionContext, ) -> Result<(), Error> { if data.is_empty() { - //= specification/data-format/message-header.md#key-value-pairs + //= spec/data-format/message-header.md#key-value-pairs //# When the [encryption context](../framework/structures.md#encryption-context) is empty, //# this field MUST NOT be included in the [AAD](#aad). Ok(()) @@ -60,31 +74,37 @@ pub(crate) fn write_aad_section( w: &mut dyn SafeWrite, data: &ESDKCanonicalEncryptionContext, ) -> Result<(), Error> { - //= specification/data-format/message-header.md#aad - //# The AAD MUST consist of, in order, - //# Key Value Pairs Length, - //# and Key Value Pairs. if data.is_empty() { - //= specification/data-format/message-header.md#key-value-pairs-length + //= spec/data-format/message-header.md#key-value-pairs-length //# When the [encryption context](../framework/structures.md#encryption-context) is empty, the value of this field MUST be 0. write_u16(w, 0)?; return Ok(()); } - // Key Value Pairs Length. - let bytes = get_length(data); + //= spec/data-format/message-header.md#aad + //# The AAD MUST consist of, in order, + //# Key Value Pairs Length, + //# and Key Value Pairs. - //= specification/data-format/message-header.md#key-value-pairs-length - //# The length of the serialized key value pairs length field MUST be 2 bytes. + // Key Value Pairs Length: covers the Key Value Pair Count field plus all pairs. - //= specification/data-format/message-header.md#key-value-pairs-length + // PROPOSED + //= spec/data-format/message-header.md#key-value-pairs-length + //# The key value pairs length value MUST equal the byte length of the Key Value Pair Count field plus all Key Value Pair entries. + let bytes = 2 + get_length(data); // 2 for the Key Value Pair Count UInt16. + + //= spec/data-format/message-header.md#key-value-pairs-length + //# The length of the serialized key value pairs length field MUST be 2 bytes. + // + //= spec/data-format/message-header.md#key-value-pairs-length //# The key value pairs length MUST be interpreted as a UInt16. let Ok(bytes_u16) = u16::try_from(bytes) else { return ser_err("value too large for u16"); }; write_u16(w, bytes_u16)?; - // Key Value Pairs. + // Key Value Pairs + write_aad(w, data) } @@ -100,7 +120,7 @@ pub(crate) fn write_aad( write_u16(w, data_len)?; for pair in data { - //= specification/data-format/message-header.md#key-value-pairs + //= spec/data-format/message-header.md#key-value-pairs //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). // Key: length + UTF-8 bytes. diff --git a/esdk/src/message/serializable_types.rs b/esdk/src/message/serializable_types.rs index 34dc346a4..5f17cc070 100644 --- a/esdk/src/message/serializable_types.rs +++ b/esdk/src/message/serializable_types.rs @@ -80,17 +80,17 @@ pub(crate) fn from_canonical_pairs(pairs: ESDKCanonicalEncryptionContext) -> ESD /// True iff `ec` fits the on-wire encoding: pair count, each key/value length, /// and total serialized length all fit in a UInt16. pub(crate) fn is_esdk_encryption_context(ec: &EncryptionContext) -> bool { - if ec.len() >= usize::from(u16::MAX) { + if ec.len() > usize::from(u16::MAX) { return false; } - if length(ec) >= ESDK_CANONICAL_ENCRYPTION_CONTEXT_MAX_LENGTH { + if length(ec) > ESDK_CANONICAL_ENCRYPTION_CONTEXT_MAX_LENGTH { return false; } for (key, value) in ec { - if key.len() >= usize::from(u16::MAX) { + if key.len() > usize::from(u16::MAX) { return false; } - if value.len() >= usize::from(u16::MAX) { + if value.len() > usize::from(u16::MAX) { return false; } } @@ -106,7 +106,7 @@ pub(crate) fn is_esdk_encrypted_data_key(edk: &EncryptedDataKey) -> bool { /// True iff the EDK count and each entry fit in UInt16. pub(crate) fn is_esdk_encrypted_data_keys(edks: &[EncryptedDataKey]) -> bool { - if edks.len() >= usize::from(u16::MAX) { + if edks.len() > usize::from(u16::MAX) { return false; } for edk in edks { diff --git a/esdk/tests/test_encrypted_data_keys.rs b/esdk/tests/test_encrypted_data_keys.rs index cd2beab99..ef9287f40 100644 --- a/esdk/tests/test_encrypted_data_keys.rs +++ b/esdk/tests/test_encrypted_data_keys.rs @@ -1,7 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -//! Tests for the Encrypted Data Keys sections of specification/data-format/message-header.md +//! Tests for the Encrypted Data Keys sections of spec/data-format/message-header.md mod fixtures; mod test_helpers; @@ -27,7 +27,7 @@ async fn test_encrypted_data_keys_ordering() { ("triple", triple, 3u16), ] { for version in VERSIONS { - //= specification/data-format/message-header.md#encrypted-data-keys + //= spec/data-format/message-header.md#encrypted-data-keys //= type=test //# The Encrypted Data Keys MUST consist of, in order, //# Encrypted Data Key Count, @@ -82,11 +82,11 @@ async fn test_edk_section_length_fields_are_big_endian_uint16() { let edk_len_wire = u16::from_be_bytes([ct[edk_len_offset], ct[edk_len_offset + 1]]); // EDK count: 2 keyrings → UInt16 value 2 ([0x00, 0x02]). - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //= type=test //# The length of the serialized encrypted data key count MUST be 2 bytes. // - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //= type=test //# The encrypted data key count MUST be interpreted as a UInt16. assert_eq!(count_wire, 2, "{version:?}: EDK count UInt16 value"); @@ -94,33 +94,33 @@ async fn test_edk_section_length_fields_are_big_endian_uint16() { assert_eq!(ct[parsed.edk_count_offset + 1], 0x02, "{version:?}: EDK count low byte"); // Key provider ID length: the UInt16 at this offset equals the known keyring namespace byte length. - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //= type=test //# The length of the serialized key provider ID length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#key-provider-id-length + //= spec/data-format/message-header.md#key-provider-id-length //= type=test //# The key provider ID length MUST be interpreted as a UInt16. assert_eq!(pid_len_wire, expected_pid_len, "{version:?}: provider ID length UInt16 value"); // Key provider information length: the UInt16 at this offset must be positive for a raw AES keyring // (which packs key name + bit length + IV length + IV into provider info). - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //= type=test //# The length of the serialized key provider information length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#key-provider-information-length + //= spec/data-format/message-header.md#key-provider-information-length //= type=test //# The key provider information length MUST be interpreted as a UInt16. assert!(pinfo_len_wire > 0, "{version:?}: provider info length UInt16 must be positive"); // Encrypted data key length: raw AES keyring stores IV in provider_info; the ciphertext field is // wrapped data key (32 bytes) + GCM tag (16 bytes) = 48. - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //= type=test //# The length of the serialized encrypted data key length field MUST be 2 bytes. // - //= specification/data-format/message-header.md#encrypted-data-key-length + //= spec/data-format/message-header.md#encrypted-data-key-length //= type=test //# The encrypted data key length MUST be interpreted as a UInt16. assert_eq!(edk_len_wire, 48, "{version:?}: EDK ciphertext length UInt16 value (wrapped 32B key + 16B tag)"); @@ -143,7 +143,7 @@ async fn test_edk_count_zero_rejected_on_decrypt() { dec.commitment_policy = EsdkCommitmentPolicy::ForbidEncryptAllowDecrypt; } - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //= type=test //= reason=Tampering the count to 0 and verifying decrypt rejects it proves the >0 constraint is enforced on the deserialization path. //# This value MUST be greater than 0. @@ -172,7 +172,7 @@ async fn test_edk_count_max_enforcement() { ); }; - //= specification/data-format/message-header.md#encrypted-data-key-count + //= spec/data-format/message-header.md#encrypted-data-key-count //= type=test //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. @@ -209,7 +209,7 @@ async fn test_edk_entry_field_order() { let edk_start = skip_to_edk_section(&ct, version) + 2; // skip count let mut pos = edk_start; - //= specification/data-format/message-header.md#encrypted-data-key-entries + //= spec/data-format/message-header.md#encrypted-data-key-entries //= type=test //# Each Encrypted Data Key Entry MUST consist of, in order, //# Key Provider ID Length, @@ -274,7 +274,7 @@ async fn test_edk_entries_preserve_keyring_order() { let ct = encrypt_with_version(b"order check", version, mk.clone()).await; let parsed = parse_edk_section(&ct, version); - //= specification/data-format/message-header.md#encrypted-data-keys + //= spec/data-format/message-header.md#encrypted-data-keys //= type=test //= reason=Verifying that EDK provider IDs appear in generator-then-children order proves entries are serialized in the order they appear in the encryption materials, exercising the "Entries" component of the Count+Entries structure. //# The Encrypted Data Keys MUST consist of, in order, @@ -303,7 +303,7 @@ async fn test_edk_entry_lengths_match_fields() { let parsed = parse_edk_section(&ct, version); for (i, edk) in parsed.edks.iter().enumerate() { - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //= type=test //# The length of the serialized key provider ID MUST be equal to the value of the [Key Provider ID Length](#key-provider-id-length) field. assert_eq!( @@ -311,7 +311,7 @@ async fn test_edk_entry_lengths_match_fields() { "{version:?}: EDK {i}: provider ID byte length must equal the provider ID length field" ); - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //= type=test //# The length of the serialized key provider information MUST be equal to the value of the [Key Provider Information Length](#key-provider-information-length) field. assert_eq!( @@ -319,7 +319,7 @@ async fn test_edk_entry_lengths_match_fields() { "{version:?}: EDK {i}: provider info byte length must equal the provider info length field" ); - //= specification/data-format/message-header.md#encrypted-data-key + //= spec/data-format/message-header.md#encrypted-data-key //= type=test //# The length of the serialized encrypted data key MUST be equal to the value of the [Encrypted Data Key Length](#encrypted-data-key-length) field. assert_eq!( @@ -340,7 +340,7 @@ async fn test_key_provider_id_is_utf8() { let ct = encrypt_with_version(b"pid utf8", version, mk.clone()).await; let parsed = parse_edk_section(&ct, version); - //= specification/data-format/message-header.md#key-provider-id + //= spec/data-format/message-header.md#key-provider-id //= type=test //# The key provider ID MUST be interpreted as UTF-8 encoded bytes. for (i, edk) in parsed.edks.iter().enumerate() { @@ -366,7 +366,7 @@ async fn test_key_provider_info_interpreted_as_bytes() { // Provider info for raw AES keyring starts with the key name. let (_, expected_name) = namespace_and_name(0); - //= specification/data-format/message-header.md#key-provider-information + //= spec/data-format/message-header.md#key-provider-information //= type=test //# The key provider information MUST be interpreted as bytes. assert!( diff --git a/esdk/tests/test_encryption_context_aad.rs b/esdk/tests/test_encryption_context_aad.rs index 6c5373dde..bede4d50d 100644 --- a/esdk/tests/test_encryption_context_aad.rs +++ b/esdk/tests/test_encryption_context_aad.rs @@ -1,7 +1,7 @@ // Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 -//! Tests for specification/data-format/message-header.md#aad, +//! Tests for spec/data-format/message-header.md#aad, //! #key-value-pairs-length, and #key-value-pairs mod fixtures; @@ -25,8 +25,7 @@ fn aad_offset(version: Version) -> usize { /// Assert that every (key, value) pair in `expected` is present in `actual`. /// Used to verify the encryption context survives the round trip intact, -/// while ignoring any keys the SDK may add (e.g. `aws-crypto-public-key` -/// for signing suites — not used by these tests, but the check is defensive). +/// while ignoring any keys the SDK may add (e.g. `aws-crypto-public-key`) fn assert_ec_contains(actual: &EncryptionContext, expected: &EncryptionContext, version: Version) { for (k, v) in expected { assert_eq!( @@ -45,7 +44,7 @@ async fn test_aad_serialization_order() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#aad + //= spec/data-format/message-header.md#aad //= type=test //# The AAD MUST consist of, in order, //# Key Value Pairs Length, @@ -76,14 +75,17 @@ async fn test_aad_key_value_pairs_length_field_size() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#key-value-pairs-length + //= spec/data-format/message-header.md#key-value-pairs-length //= type=test //# The length of the serialized key value pairs length field MUST be 2 bytes. // The KVP length field occupies exactly 2 bytes at [off..off+2]. let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]) as usize; - // For "A" (keyA=valA): key_len(2) + key(4) + val_len(2) + val(4) = 12 bytes of pair data. - assert_eq!(kvp_len, 12, "{version:?}: KVP length for single pair keyA=valA must be 12"); + // For "A" (keyA=valA) the Key Value Pairs field is: + // count(2) + key_len(2) + key(4) + val_len(2) + val(4) = 14 bytes. + // The length field covers the entire Key Value Pairs structure, including + // the Key Value Pair Count. + assert_eq!(kvp_len, 14, "{version:?}: KVP length for single pair keyA=valA must be 14"); // Cross-check: the decrypted EC matches what we encrypted, confirming the 2-byte // length field was parsed correctly on the decrypt side too. @@ -100,14 +102,14 @@ async fn test_aad_key_value_pairs_length_uint16() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#key-value-pairs-length + //= spec/data-format/message-header.md#key-value-pairs-length //= type=test //# The key value pairs length MUST be interpreted as a UInt16. // Read the 2 bytes as big-endian u16 and verify the value. let kvp_len = u16::from_be_bytes([ct[off], ct[off + 1]]); - // keyA=valA: key_len(2) + key(4) + val_len(2) + val(4) = 12. - assert_eq!(kvp_len, 12, "{version:?}: UInt16 KVP length for keyA=valA must be 12"); + // keyA=valA: count(2) + key_len(2) + key(4) + val_len(2) + val(4) = 14. + assert_eq!(kvp_len, 14, "{version:?}: UInt16 KVP length for keyA=valA must be 14"); // Cross-check: the decrypted EC round-trips, confirming both sides agree that // the field is a big-endian UInt16. @@ -124,7 +126,7 @@ async fn test_aad_empty_encryption_context_length_zero() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#key-value-pairs-length + //= spec/data-format/message-header.md#key-value-pairs-length //= type=test //# When the [encryption context](../framework/structures.md#encryption-context) is empty, the value of this field MUST be 0. @@ -155,7 +157,7 @@ async fn test_aad_key_value_pairs_serialization() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#key-value-pairs + //= spec/data-format/message-header.md#key-value-pairs //= type=test //# The encryption context key-value pairs MUST be serialized according to its [specification for serialization](../framework/structures.md#serialization). @@ -204,7 +206,7 @@ async fn test_aad_empty_encryption_context_no_kvp_field() { let ct = encrypt_no_sign_with_ec(pt, ec.clone(), version).await; let off = aad_offset(version); - //= specification/data-format/message-header.md#key-value-pairs + //= spec/data-format/message-header.md#key-value-pairs //= type=test //# When the [encryption context](../framework/structures.md#encryption-context) is empty, //# this field MUST NOT be included in the [AAD](#aad). From ba880f04a5380dd985798d65f01fbf9dff7de596 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Mon, 4 May 2026 11:59:56 -0700 Subject: [PATCH 14/22] docs(native-rust): sync reworded key-value-pairs-length annotation from unreviewed --- esdk/src/message/encryption_context.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index ab89fb122..0164eae43 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -90,7 +90,7 @@ pub(crate) fn write_aad_section( // PROPOSED //= spec/data-format/message-header.md#key-value-pairs-length - //# The key value pairs length value MUST equal the byte length of the Key Value Pair Count field plus all Key Value Pair entries. + //# The Key Value Pairs Length value MUST be the byte length of the serialized Key Value Pairs field, where the Key Value Pairs field consists of the Key Value Pair Count followed by the Key Value Pair entries. let bytes = 2 + get_length(data); // 2 for the Key Value Pair Count UInt16. //= spec/data-format/message-header.md#key-value-pairs-length From 82e427a3cd3d1cc8c4c2f57a44d51b3805280ff0 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 5 May 2026 16:25:55 -0700 Subject: [PATCH 15/22] docs(native-rust): sync simplified key-value-pairs-length rejection annotation from unreviewed --- esdk/src/message/encryption_context.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index 0164eae43..ebdeffc0e 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -34,9 +34,7 @@ pub(crate) fn read_canonical_ec( // PROPOSED //= spec/data-format/message-header.md#key-value-pairs-length - //# The key value pairs length value MUST equal the number of bytes consumed - //# by deserializing the Key Value Pairs field. A decryptor MUST reject a - //# message whose key value pairs length does not match the deserialized size. + //# A decryptor MUST reject a message whose Key Value Pairs Length value does not equal the byte length of the deserialized Key Value Pairs field. if consumed != bytes { return ser_err("Encryption context length field does not match parsed contents"); } From e81af7d0c04a13cf8302384119aaff3f4198ed69 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 5 May 2026 16:29:52 -0700 Subject: [PATCH 16/22] perf(native-rust): sync drop-consumed-counter from unreviewed --- esdk/src/message/encryption_context.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index ebdeffc0e..bc63e9a1a 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -19,26 +19,15 @@ pub(crate) fn read_canonical_ec( return Ok(Vec::new()); } - // Count, then `count` (key, value) pairs. Track bytes consumed so we can - // reject a message whose length field disagrees with the parsed contents. + // Count, then `count` (key, value) pairs. let count = usize::from(read_u16(r, raw)?); - let mut consumed: usize = 2; // the Key Value Pair Count field we just read let mut result: ESDKCanonicalEncryptionContext = Vec::with_capacity(count); for _ in 0..count { let key = read_str_u16(r, raw)?; - consumed += 2 + key.len(); let value = read_str_u16(r, raw)?; - consumed += 2 + value.len(); result.push((key, value)); } - // PROPOSED - //= spec/data-format/message-header.md#key-value-pairs-length - //# A decryptor MUST reject a message whose Key Value Pairs Length value does not equal the byte length of the deserialized Key Value Pairs field. - if consumed != bytes { - return ser_err("Encryption context length field does not match parsed contents"); - } - Ok(result) } From ac2922252644f7cb3e70409f217565fd72ed7057 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 5 May 2026 16:32:01 -0700 Subject: [PATCH 17/22] docs(native-rust): sync per-field get_length layout from unreviewed --- esdk/src/message/encryption_context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index bc63e9a1a..cf2ef2ac6 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -50,8 +50,8 @@ pub(crate) fn write_empty_ec_or_write_aad( fn get_length(data: &ESDKCanonicalEncryptionContext) -> usize { let mut length = 0; for pair in data { - // 2 (key len) + 2 (val len) + key bytes + val bytes. - length += 4 + pair.0.len() + pair.1.len(); + // key_len(2) + key bytes + val_len(2) + val bytes. + length += 2 + pair.0.len() + 2 + pair.1.len(); } length } From 9f6f58df0e19d35672438afb2c6da8cfbbf656a1 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 5 May 2026 16:43:19 -0700 Subject: [PATCH 18/22] docs(native-rust): sync length cast-safety note from unreviewed --- esdk/src/message/serializable_types.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/esdk/src/message/serializable_types.rs b/esdk/src/message/serializable_types.rs index 5f17cc070..243eb0f81 100644 --- a/esdk/src/message/serializable_types.rs +++ b/esdk/src/message/serializable_types.rs @@ -52,6 +52,11 @@ pub(crate) const fn get_encrypt_key_length(a: &AlgorithmSuite) -> u8 { // Uint16-2-2-1 for the value data /// Serialized byte length of the key-value-pairs body (no outer length prefix). +/// +/// Accumulates in `usize` and casts to `u64` on return. Per the ESDK message +/// format, the AAD's maximum allowed length is `2^16 - 1` bytes, so a legal +/// encryption context never produces a sum that overflows even a 16-bit +/// accumulator — the `usize` sum and `as u64` cast are safe by construction. pub(crate) fn length(encryption_context: &ESDKEncryptionContext) -> u64 { let mut length: usize = 0; for (key, value) in encryption_context { From ea520408bbab4661bcf1b3e8f7b34cd66eb7beef Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Tue, 5 May 2026 16:54:52 -0700 Subject: [PATCH 19/22] docs(native-rust): sync PROPOSED marker removal from unreviewed --- esdk/src/message/encryption_context.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index cf2ef2ac6..d05ef92f5 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -75,7 +75,6 @@ pub(crate) fn write_aad_section( // Key Value Pairs Length: covers the Key Value Pair Count field plus all pairs. - // PROPOSED //= spec/data-format/message-header.md#key-value-pairs-length //# The Key Value Pairs Length value MUST be the byte length of the serialized Key Value Pairs field, where the Key Value Pairs field consists of the Key Value Pair Count followed by the Key Value Pair entries. let bytes = 2 + get_length(data); // 2 for the Key Value Pair Count UInt16. From 1df286f55cd526d6afa1ccf4c893d3748122bf02 Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 21 May 2026 11:00:32 -0700 Subject: [PATCH 20/22] feat(native-rust): sync header inline structures from unreviewed --- esdk/src/message/encrypted_data_keys.rs | 3 +++ esdk/src/message/encryption_context.rs | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index 508e8fb87..86ebda4c8 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -149,6 +149,9 @@ pub(crate) fn read_edks( if let Some(max_edks) = max_edks && usize::from(count) > max_edks.get() { + //= spec/data-format/message-header.md#encrypted-data-key-count + //# This value MUST be less than or equal to the [maximum number of encrypted data keys](../client-apis/client.md#maximum-number-of-encrypted-data-keys) if the maximum number is configured. + // //= spec/client-apis/decrypt.md#v2-header-deserialization //# If the number of [encrypted data keys](../framework/structures.md#encrypted-data-keys) //# deserialized from the [message header](../data-format/message-header.md) diff --git a/esdk/src/message/encryption_context.rs b/esdk/src/message/encryption_context.rs index d05ef92f5..e1b886820 100644 --- a/esdk/src/message/encryption_context.rs +++ b/esdk/src/message/encryption_context.rs @@ -51,6 +51,7 @@ fn get_length(data: &ESDKCanonicalEncryptionContext) -> usize { let mut length = 0; for pair in data { // key_len(2) + key bytes + val_len(2) + val bytes. + // `.len()` on a String returns the number of UTF-8 bytes, which is what we serialize. length += 2 + pair.0.len() + 2 + pair.1.len(); } length @@ -76,7 +77,7 @@ pub(crate) fn write_aad_section( // Key Value Pairs Length: covers the Key Value Pair Count field plus all pairs. //= spec/data-format/message-header.md#key-value-pairs-length - //# The Key Value Pairs Length value MUST be the byte length of the serialized Key Value Pairs field, where the Key Value Pairs field consists of the Key Value Pair Count followed by the Key Value Pair entries. + //# The length of the serialized key value pairs length field MUST be 2 bytes. let bytes = 2 + get_length(data); // 2 for the Key Value Pair Count UInt16. //= spec/data-format/message-header.md#key-value-pairs-length @@ -85,7 +86,7 @@ pub(crate) fn write_aad_section( //= spec/data-format/message-header.md#key-value-pairs-length //# The key value pairs length MUST be interpreted as a UInt16. let Ok(bytes_u16) = u16::try_from(bytes) else { - return ser_err("value too large for u16"); + return ser_err("Encryption context key value pair length value is too large for u16"); }; write_u16(w, bytes_u16)?; @@ -101,7 +102,7 @@ pub(crate) fn write_aad( ) -> Result<(), Error> { // Count. let Ok(data_len) = u16::try_from(data.len()) else { - return ser_err("value too large for u16"); + return ser_err("Encryption context key value pair count is too large for u16"); }; write_u16(w, data_len)?; @@ -111,14 +112,14 @@ pub(crate) fn write_aad( // Key: length + UTF-8 bytes. let Ok(key_len) = u16::try_from(pair.0.len()) else { - return ser_err("value too large for u16"); + return ser_err("Encryption context key length is too large for u16"); }; write_u16(w, key_len)?; write_bytes(w, pair.0.as_bytes())?; // Value: length + UTF-8 bytes. let Ok(val_len) = u16::try_from(pair.1.len()) else { - return ser_err("value too large for u16"); + return ser_err("Encryption context value length is too large for u16"); }; write_u16(w, val_len)?; write_bytes(w, pair.1.as_bytes())?; From 79cabc770c97f09fc1dbc08ca43fbe4445b5311d Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 21 May 2026 11:01:30 -0700 Subject: [PATCH 21/22] style(native-rust): sync write_u8 to_be_bytes consistency from unreviewed --- esdk/src/message/serialize_functions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esdk/src/message/serialize_functions.rs b/esdk/src/message/serialize_functions.rs index 3268f90e0..b2db5ecb4 100644 --- a/esdk/src/message/serialize_functions.rs +++ b/esdk/src/message/serialize_functions.rs @@ -88,7 +88,7 @@ pub(crate) fn write_bytes(w: &mut dyn SafeWrite, data: &[u8]) -> Result<(), Erro // Big-endian fixed-width writers. pub(crate) fn write_u8(w: &mut dyn SafeWrite, data: u8) -> Result<(), Error> { - write_bytes(w, &[data]) + write_bytes(w, &data.to_be_bytes()) } pub(crate) fn write_u16(w: &mut dyn SafeWrite, data: u16) -> Result<(), Error> { write_bytes(w, &data.to_be_bytes()) From d63bf9abede35803371c59621d707809e35f7a2e Mon Sep 17 00:00:00 2001 From: Lucas McDonald Date: Thu, 21 May 2026 11:05:01 -0700 Subject: [PATCH 22/22] style(native-rust): sync edk local rename from unreviewed --- esdk/src/message/encrypted_data_keys.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esdk/src/message/encrypted_data_keys.rs b/esdk/src/message/encrypted_data_keys.rs index 86ebda4c8..cd4b96d65 100644 --- a/esdk/src/message/encrypted_data_keys.rs +++ b/esdk/src/message/encrypted_data_keys.rs @@ -230,7 +230,7 @@ pub(crate) fn read_edk( //= spec/data-format/message-header.md#encrypted-data-key //= type=implication //# The encrypted data key MUST be interpreted as bytes. - let ciphertext = read_seq_u16(r, raw)?; + let edk = read_seq_u16(r, raw)?; - Ok(EncryptedDataKey::new(provider_id, provider_info, ciphertext)) + Ok(EncryptedDataKey::new(provider_id, provider_info, edk)) }