diff --git a/crates/rust-client/src/rpc/encryption.rs b/crates/rust-client/src/rpc/encryption.rs index f3f73e1559..ef0db1d631 100644 --- a/crates/rust-client/src/rpc/encryption.rs +++ b/crates/rust-client/src/rpc/encryption.rs @@ -30,7 +30,7 @@ //! The canonical definitions live in the node's `miden_node_proto::domain::encryption`. This module //! is a hand-maintained mirror of them, because that is a node crate and this client is `no_std`. -use alloc::string::ToString; +use alloc::string::{String, ToString}; use alloc::vec::Vec; use miden_protocol::block::{BlockNumber, ValidatorKeys}; @@ -89,8 +89,8 @@ const MAX_KEY_ID_LEN: usize = 64; /// Holds public key material only, and is shared by every validator in the set; the matching /// secret never leaves the validators. /// -/// Only [`AttestedTransactionEncryptionKey::verify`] constructs one, so a key that reaches the seal -/// path has necessarily been vouched for by a chain-recognized validator. +/// RPC responses can only construct one through [`AttestedTransactionEncryptionKey::verify`]. +/// Deserialization revalidates the scheme and key ID of cached keys. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TransactionEncryptionKey { scheme: u32, @@ -163,6 +163,8 @@ impl Deserializable for TransactionEncryptionKey { fn read_from(source: &mut R) -> Result { let scheme = source.read_u32()?; let key_id_len = source.read_usize()?; + validate_key_metadata(scheme, key_id_len).map_err(DeserializationError::InvalidValue)?; + let key_id = source.read_vec(key_id_len)?; let public_key = PublicKey::read_from(source)?; let genesis_commitment = Word::read_from(source)?; @@ -246,16 +248,11 @@ impl AttestedTransactionEncryptionKey { genesis_commitment: Word, validator_keys: &ValidatorKeys, ) -> Result { - if self.scheme != SUPPORTED_SCHEME { - return Err(RpcError::TransactionEncryptionKeyRejected(format!( - "unsupported IES scheme '{}'", - self.scheme - ))); - } - - validate_key_id(&self.key_id, "encryption key id")?; + validate_key_metadata(self.scheme, self.key_id.len()) + .map_err(RpcError::TransactionEncryptionKeyRejected)?; if let Some(next) = &self.next_key { - validate_key_id(&next.key_id, "next encryption key id")?; + validate_key_id_len(next.key_id.len(), "next encryption key id") + .map_err(RpcError::TransactionEncryptionKeyRejected)?; } let commitment = attestation_commitment( @@ -291,19 +288,22 @@ impl AttestedTransactionEncryptionKey { } } -/// Rejects a served key identifier that is empty or longer than [`MAX_KEY_ID_LEN`]. -/// -/// Mirrors the validator's `validate_key_id` so the client refuses a key the node itself -/// would never serve. -fn validate_key_id(key_id: &[u8], field: &str) -> Result<(), RpcError> { - if key_id.is_empty() { - return Err(RpcError::TransactionEncryptionKeyRejected(format!("{field} is empty"))); - } - if key_id.len() > MAX_KEY_ID_LEN { - return Err(RpcError::TransactionEncryptionKeyRejected(format!( - "{field} is {} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}", - key_id.len() - ))); +fn validate_key_metadata(scheme: u32, key_id_len: usize) -> Result<(), String> { + if scheme != SUPPORTED_SCHEME { + return Err(format!("unsupported IES scheme '{scheme}'")); + } + + validate_key_id_len(key_id_len, "encryption key id") +} + +fn validate_key_id_len(key_id_len: usize, field: &str) -> Result<(), String> { + if key_id_len == 0 { + return Err(format!("{field} is empty")); + } + if key_id_len > MAX_KEY_ID_LEN { + return Err(format!( + "{field} is {key_id_len} bytes, which exceeds the maximum of {MAX_KEY_ID_LEN}" + )); } Ok(()) } @@ -475,6 +475,30 @@ mod tests { const TEST_KEY_ID: [u8; 4] = [0xde, 0xad, 0xbe, 0xef]; + #[test] + fn transaction_encryption_key_deserialization_validates_metadata() { + let (key, _) = key_pair(); + assert_eq!(TransactionEncryptionKey::read_from_bytes(&key.to_bytes()).unwrap(), key); + + let invalid_keys = [ + TransactionEncryptionKey { + scheme: SUPPORTED_SCHEME + 1, + ..key.clone() + }, + TransactionEncryptionKey { key_id: Vec::new(), ..key.clone() }, + TransactionEncryptionKey { + key_id: vec![0; MAX_KEY_ID_LEN + 1], + ..key + }, + ]; + for key in invalid_keys { + assert!(matches!( + TransactionEncryptionKey::read_from_bytes(&key.to_bytes()), + Err(DeserializationError::InvalidValue(_)) + )); + } + } + fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xface) }