|
6 | 6 |
|
7 | 7 | use camino::Utf8PathBuf; |
8 | 8 | use dice_mfg_msgs::PlatformId; |
| 9 | +use hubpack::SerializedSize; |
9 | 10 | use rustls::crypto::aws_lc_rs::cipher_suite::TLS13_CHACHA20_POLY1305_SHA256; |
10 | 11 | use rustls::crypto::aws_lc_rs::kx_group::X25519; |
11 | 12 | use rustls::crypto::CryptoProvider; |
@@ -104,6 +105,9 @@ pub enum Error { |
104 | 105 | #[error("Hubpack error:")] |
105 | 106 | Hubpack(#[from] hubpack::Error), |
106 | 107 |
|
| 108 | + #[error("protocol message length {len} exceeds maximum {max}")] |
| 109 | + MessageTooLarge { len: usize, max: usize }, |
| 110 | + |
107 | 111 | #[error("Failed to verify attestation")] |
108 | 112 | AttestationVerifier(#[from] dice_verifier::VerifyAttestationError), |
109 | 113 |
|
@@ -280,14 +284,37 @@ type ProtocolRequestAck = Result<u32, ()>; |
280 | 284 | const CURRENT_PROTOCOL_VERSION: u32 = 2; |
281 | 285 | const PREVIOUS_PROTOCOL_VERSION: u32 = CURRENT_PROTOCOL_VERSION - 1; |
282 | 286 |
|
| 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 | + |
283 | 302 | async fn recv_msg<T: AsyncReadExt + Unpin>( |
284 | 303 | stream: &mut T, |
285 | 304 | ) -> Result<Vec<u8>, Error> { |
286 | 305 | // to receive a message we first get its length that is a u32 serialized as |
287 | 306 | // a little endian byte array |
288 | 307 | let mut msg_len = [0u8; 4]; |
289 | 308 | 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 | + } |
291 | 318 |
|
292 | 319 | // with the length we can then get the message body |
293 | 320 | let mut buf = vec![0u8; msg_len]; |
@@ -705,4 +732,74 @@ mod tests { |
705 | 732 | sleep(Duration::from_millis(1)).await; |
706 | 733 | } |
707 | 734 | } |
| 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 | + } |
708 | 805 | } |
0 commit comments