Skip to content

Commit 5adfc52

Browse files
committed
fix: extend check for header byte
Packet payloads should always include a header byte according to the spec, but check that it actually does to better handle buggy clients or bad actors.
1 parent 1d0f155 commit 5adfc52

File tree

2 files changed

+47
-2
lines changed

2 files changed

+47
-2
lines changed

protocol/src/io.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,24 @@ use crate::{
2020
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
2121

2222
/// A decrypted BIP-324 payload.
23+
///
24+
/// # Invariants
25+
///
26+
/// The internal data vector must always contain at least one byte (the header byte).
27+
/// This invariant is maintained by the decrypt functions which validate that
28+
/// ciphertext contains at least `NUM_TAG_BYTES + NUM_HEADER_BYTES` before
29+
/// attempting decryption.
2330
pub struct Payload {
2431
data: Vec<u8>,
2532
}
2633

2734
impl Payload {
2835
/// Create a new payload from complete decrypted data (including header byte).
36+
///
37+
/// The data must contain at least one byte (the header). This is guaranteed
38+
/// by the decrypt functions, but can be asserted in debug builds.
2939
pub fn new(data: Vec<u8>) -> Self {
40+
debug_assert!(!data.is_empty(), "Payload data must contain at least the header byte");
3041
Self { data }
3142
}
3243

protocol/src/lib.rs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ impl InboundCipher {
360360
) -> Result<(PacketType, &'a [u8]), Error> {
361361
let auth = aad.unwrap_or_default();
362362
// Check minimum size of ciphertext.
363-
if ciphertext.len() < NUM_TAG_BYTES {
363+
if ciphertext.len() < NUM_TAG_BYTES + NUM_HEADER_BYTES {
364364
return Err(Error::CiphertextTooSmall);
365365
}
366366
let (msg, tag) = ciphertext.split_at_mut(ciphertext.len() - NUM_TAG_BYTES);
@@ -400,7 +400,7 @@ impl InboundCipher {
400400
) -> Result<PacketType, Error> {
401401
let auth = aad.unwrap_or_default();
402402
// Check minimum size of ciphertext.
403-
if ciphertext.len() < NUM_TAG_BYTES {
403+
if ciphertext.len() < NUM_TAG_BYTES + NUM_HEADER_BYTES {
404404
return Err(Error::CiphertextTooSmall);
405405
}
406406
let (msg, tag) = ciphertext.split_at(ciphertext.len() - NUM_TAG_BYTES);
@@ -763,6 +763,40 @@ mod tests {
763763
assert_eq!(message2, plaintext2[1..].to_vec()); // Skip header byte
764764
}
765765

766+
#[test]
767+
fn test_decrypt_min_length() {
768+
// Test that decrypt properly validates minimum ciphertext length.
769+
let alice =
770+
SecretKey::from_str("61062ea5071d800bbfd59e2e8b53d47d194b095ae5a4df04936b49772ef0d4d7")
771+
.unwrap();
772+
let elliswift_alice = ElligatorSwift::from_str("ec0adff257bbfe500c188c80b4fdd640f6b45a482bbc15fc7cef5931deff0aa186f6eb9bba7b85dc4dcc28b28722de1e3d9108b985e2967045668f66098e475b").unwrap();
773+
let elliswift_bob = ElligatorSwift::from_str("a4a94dfce69b4a2a0a099313d10f9f7e7d649d60501c9e1d274c300e0d89aafaffffffffffffffffffffffffffffffffffffffffffffffffffffffff8faf88d5").unwrap();
774+
let session_keys = SessionKeyMaterial::from_ecdh(
775+
elliswift_alice,
776+
elliswift_bob,
777+
alice,
778+
ElligatorSwiftParty::A,
779+
Network::Bitcoin,
780+
)
781+
.unwrap();
782+
let mut alice_cipher = CipherSession::new(session_keys, Role::Initiator);
783+
784+
// Test with ciphertext that is exactly NUM_TAG_BYTES (should fail).
785+
let too_small = vec![0u8; NUM_TAG_BYTES];
786+
let mut plaintext_buffer = vec![0u8; 100];
787+
let result = alice_cipher
788+
.inbound()
789+
.decrypt(&too_small, &mut plaintext_buffer, None);
790+
assert_eq!(result, Err(Error::CiphertextTooSmall));
791+
792+
// Test decrypt_in_place with same minimum length checks.
793+
let mut too_small = vec![0u8; NUM_TAG_BYTES];
794+
let result = alice_cipher
795+
.inbound()
796+
.decrypt_in_place(&mut too_small, None);
797+
assert_eq!(result, Err(Error::CiphertextTooSmall));
798+
}
799+
766800
#[test]
767801
fn test_fuzz_packets() {
768802
let mut rng = rand::thread_rng();

0 commit comments

Comments
 (0)