From f142fe3e91ae5f8d880a87ec9f4469dcfd9bede3 Mon Sep 17 00:00:00 2001 From: Xowiek Date: Mon, 17 Aug 2026 03:11:44 +0300 Subject: [PATCH] fix(rpc): validate a stored transaction encryption key on decode TransactionEncryptionKey documents verify() as its only constructor, so a key reaching the seal path has been vouched for by a chain-recognized validator. Deserializable is a second constructor and skipped both checks verify() makes: the IES scheme and the key id bounds. The key is cached in the store after verification and read back through Deserializable, and the cached value is not re-verified, so a corrupt row decoded into a key the seal path treated as verified. The attestation cannot be rechecked on decode, since it is not serialized and the trust anchors are not available there, but the invariants that do not depend on it are now enforced. The key id length is bounded before the read so a corrupt length cannot ask for a large allocation. --- crates/rust-client/src/rpc/encryption.rs | 65 ++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/crates/rust-client/src/rpc/encryption.rs b/crates/rust-client/src/rpc/encryption.rs index f3f73e1559..e5cfc35a21 100644 --- a/crates/rust-client/src/rpc/encryption.rs +++ b/crates/rust-client/src/rpc/encryption.rs @@ -160,10 +160,31 @@ impl Serializable for TransactionEncryptionKey { } impl Deserializable for TransactionEncryptionKey { + /// Decoding is the type's second constructor, so it holds the same line as + /// [`AttestedTransactionEncryptionKey::verify`]: a stored key that verification would have + /// rejected does not decode. The attestation itself cannot be rechecked here, since it is not + /// serialized and the trust anchors are not available, but the invariants that do not depend on + /// it are enforced. fn read_from(source: &mut R) -> Result { let scheme = source.read_u32()?; + if scheme != SUPPORTED_SCHEME { + return Err(DeserializationError::InvalidValue(format!( + "unsupported IES scheme '{scheme}'" + ))); + } + let key_id_len = source.read_usize()?; + // Checked before reading so a corrupt length cannot ask for a large allocation. + if key_id_len > MAX_KEY_ID_LEN { + return Err(DeserializationError::InvalidValue(format!( + "encryption key id is {key_id_len} bytes, which exceeds the maximum of \ + {MAX_KEY_ID_LEN}" + ))); + } let key_id = source.read_vec(key_id_len)?; + validate_key_id(&key_id, "encryption key id") + .map_err(|err| DeserializationError::InvalidValue(err.to_string()))?; + let public_key = PublicKey::read_from(source)?; let genesis_commitment = Word::read_from(source)?; @@ -475,6 +496,50 @@ mod tests { const TEST_KEY_ID: [u8; 4] = [0xde, 0xad, 0xbe, 0xef]; + /// A key the store already holds must still decode unchanged. + #[test] + fn a_verified_key_round_trips() { + let (key, _) = key_pair(); + + let decoded = TransactionEncryptionKey::read_from_bytes(&key.to_bytes()) + .expect("a verified key must decode"); + + assert_eq!(decoded, key); + } + + /// `verify` rejects a scheme it does not support, so decoding must reject it too rather than + /// hand the seal path a key that never passed verification. + #[test] + fn deserialization_rejects_an_unsupported_scheme() { + let (key, _) = key_pair(); + let stored = TransactionEncryptionKey { scheme: SUPPORTED_SCHEME + 1, ..key }.to_bytes(); + + let err = TransactionEncryptionKey::read_from_bytes(&stored) + .expect_err("an unsupported scheme must be rejected"); + + assert!(matches!(err, DeserializationError::InvalidValue(_)), "unexpected error: {err}"); + } + + /// Same for the key id bounds `verify` enforces through `validate_key_id`. + #[test] + fn deserialization_rejects_a_malformed_key_id() { + let (key, _) = key_pair(); + + for key_id in [Vec::new(), vec![0u8; MAX_KEY_ID_LEN + 1]] { + let len = key_id.len(); + let stored = TransactionEncryptionKey { key_id, ..key.clone() }.to_bytes(); + + let err = TransactionEncryptionKey::read_from_bytes(&stored) + .err() + .unwrap_or_else(|| panic!("a {len}-byte key id must be rejected")); + + assert!( + matches!(err, DeserializationError::InvalidValue(_)), + "unexpected error: {err}" + ); + } + } + fn rng() -> ChaCha20Rng { ChaCha20Rng::seed_from_u64(0xface) }