Skip to content

Commit 43cc27c

Browse files
committed
Bound protocol message sizes before allocating
recv_msg allocated the full message buffer from the peer-controlled length prefix before reading any of the body, so a TLS-authenticated (but not yet attested) peer could demand a 4 GiB zeroed allocation with a 4-byte prefix, and hold it by leaving the connection open — repeatable across connections for memory exhaustion. Authenticated-but-unattested is exactly the adversary class the attestation layer exists to screen out, so the framing layer must not trust it with allocation sizes. Bound messages at a named MAX_MSG_SIZE (1 MiB), with compile-time asserts that the bound admits the largest legitimate messages (the hubpacked measurement log and attestation). Oversized lengths are rejected as a new Error::MessageTooLarge before any allocation. No interoperability change: every legitimate protocol message is far below the bound.
1 parent 026a3fa commit 43cc27c

1 file changed

Lines changed: 98 additions & 1 deletion

File tree

tls/src/lib.rs

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
77
use camino::Utf8PathBuf;
88
use dice_mfg_msgs::PlatformId;
9+
use hubpack::SerializedSize;
910
use rustls::crypto::aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256;
1011
use rustls::crypto::aws_lc_rs::kx_group::X25519;
1112
use rustls::crypto::CryptoProvider;
@@ -104,6 +105,9 @@ pub enum Error {
104105
#[error("Hubpack error:")]
105106
Hubpack(#[from] hubpack::Error),
106107

108+
#[error("protocol message length {len} exceeds maximum {max}")]
109+
MessageTooLarge { len: usize, max: usize },
110+
107111
#[error("Failed to verify attestation")]
108112
AttestationVerifier(#[from] dice_verifier::VerifyAttestationError),
109113

@@ -280,14 +284,37 @@ type ProtocolRequestAck = Result<u32, ()>;
280284
const CURRENT_PROTOCOL_VERSION: u32 = 2;
281285
const PREVIOUS_PROTOCOL_VERSION: u32 = CURRENT_PROTOCOL_VERSION - 1;
282286

287+
/// The largest message [`recv_msg`] will accept.
288+
///
289+
/// The length prefix is peer-controlled and [`recv_msg`] allocates the full
290+
/// message buffer before reading the body, so without a bound a
291+
/// TLS-authenticated (but not yet attested) peer can demand a 4 GiB
292+
/// allocation with a 4-byte prefix. 1 MiB comfortably exceeds every
293+
/// legitimate protocol message: the largest are the hubpacked measurement
294+
/// log and attestation, whose bounds are asserted below; cert chains and
295+
/// nonces are far smaller.
296+
const MAX_MSG_SIZE: usize = 1024 * 1024;
297+
298+
// The bound must admit every legitimate protocol message.
299+
const _: () = assert!(dice_verifier::Log::MAX_SIZE <= MAX_MSG_SIZE);
300+
const _: () = assert!(dice_verifier::Attestation::MAX_SIZE <= MAX_MSG_SIZE);
301+
283302
async fn recv_msg<T: AsyncReadExt + Unpin>(
284303
stream: &mut T,
285304
) -> Result<Vec<u8>, Error> {
286305
// to receive a message we first get its length that is a u32 serialized as
287306
// a little endian byte array
288307
let mut msg_len = [0u8; 4];
289308
stream.read_exact(&mut msg_len).await?;
290-
let msg_len = u32::from_le_bytes(msg_len).try_into()?;
309+
let msg_len: usize = u32::from_le_bytes(msg_len).try_into()?;
310+
311+
// The length is peer-controlled: bound it before allocating.
312+
if msg_len > MAX_MSG_SIZE {
313+
return Err(Error::MessageTooLarge {
314+
len: msg_len,
315+
max: MAX_MSG_SIZE,
316+
});
317+
}
291318

292319
// with the length we can then get the message body
293320
let mut buf = vec![0u8; msg_len];
@@ -705,4 +732,74 @@ mod tests {
705732
sleep(Duration::from_millis(1)).await;
706733
}
707734
}
735+
736+
// A message whose length prefix exceeds MAX_MSG_SIZE, sent by a
737+
// TLS-authenticated client, is rejected as Error::MessageTooLarge before
738+
// the message buffer is allocated — the peer-controlled prefix must not
739+
// be able to demand a 4 GiB allocation.
740+
#[tokio::test]
741+
async fn oversized_message_rejected() {
742+
let log = logger();
743+
let pki_keydir = pki_keydir();
744+
let mock_datadir = mock_datadir();
745+
let addr: SocketAddrV6 = SocketAddrV6::from_str("[::1]:46468").unwrap();
746+
747+
let server_config =
748+
local_config(1, MeasurementConnectionPolicy::Enforced);
749+
let corpus = vec![
750+
mock_datadir.join("corim-rot.cbor"),
751+
mock_datadir.join("corim-sp.cbor"),
752+
];
753+
754+
let log2 = log.clone();
755+
let handle = tokio::spawn(async move {
756+
let server = Server::new(server_config, addr, log2.clone())
757+
.await
758+
.unwrap();
759+
760+
let result = server
761+
.accept(corpus.clone())
762+
.await
763+
.unwrap()
764+
.handshake()
765+
.await;
766+
match result {
767+
Err(Error::MessageTooLarge { .. }) => {}
768+
Err(other) => {
769+
panic!("expected MessageTooLarge, got {other:?}")
770+
}
771+
Ok(_) => {
772+
panic!("an oversized message must not complete")
773+
}
774+
}
775+
});
776+
777+
let client_config = client::Client::new_tls_local_client_config(
778+
pki_keydir.join("test-sprockets-auth-2.key.pem"),
779+
pki_keydir.join("test-sprockets-auth-2.certlist.pem"),
780+
vec![pki_keydir.join("test-root-a.cert.pem")],
781+
log,
782+
)
783+
.unwrap();
784+
785+
let dnsname =
786+
rustls::pki_types::ServerName::try_from("unknown.com").unwrap();
787+
let connector = tokio_rustls::TlsConnector::from(std::sync::Arc::new(
788+
client_config,
789+
));
790+
let stream = loop {
791+
if let Ok(s) = tokio::net::TcpStream::connect(addr).await {
792+
break s;
793+
};
794+
sleep(Duration::from_millis(1)).await;
795+
};
796+
let mut stream = connector.connect(dnsname, stream).await.unwrap();
797+
798+
// A length prefix claiming a 4 GiB message; no body ever follows. The
799+
// server must reject on the prefix alone.
800+
stream.write_all(&u32::MAX.to_le_bytes()).await.unwrap();
801+
stream.shutdown().await.unwrap();
802+
803+
handle.await.unwrap();
804+
}
708805
}

0 commit comments

Comments
 (0)