diff --git a/Cargo.lock b/Cargo.lock index d9ff1d1ae..fa13599a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -71,7 +71,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -82,7 +82,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -182,7 +182,7 @@ dependencies = [ "polling", "rustix", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -240,7 +240,7 @@ dependencies = [ "rustix", "signal-hook-registry", "slab", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -272,6 +272,28 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.39.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -482,6 +504,15 @@ dependencies = [ "zbus", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "colorchoice" version = "1.0.5" @@ -668,9 +699,15 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.16.0" @@ -750,7 +787,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -840,6 +877,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.32" @@ -1784,7 +1827,7 @@ dependencies = [ "hermit-abi", "pin-project-lite", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2017,6 +2060,20 @@ dependencies = [ "syn", ] +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -2039,7 +2096,44 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -2249,6 +2343,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.117" @@ -2270,7 +2370,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2280,7 +2380,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2420,7 +2520,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -2441,6 +2541,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "utf8parse" version = "0.2.2" @@ -2677,6 +2783,8 @@ version = "0.1.0" dependencies = [ "anyhow", "itertools", + "log", + "rustls", "serde", "serde_json", "thiserror", @@ -2907,6 +3015,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -2916,6 +3033,70 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.3" @@ -3047,7 +3228,7 @@ dependencies = [ "tracing", "uds_windows", "uuid", - "windows-sys", + "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", @@ -3100,6 +3281,12 @@ dependencies = [ "syn", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zmij" version = "1.0.21" diff --git a/Cargo.toml b/Cargo.toml index 17ea98ccf..cc6ea9a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,6 +95,7 @@ itertools = "0.14.0" jiff = { version = "0.2", default-features = false, features = ["std"] } libc = "0.2.186" log = "0.4.30" +rustls = { version = "0.23.40", features = ["ring"] } sha2 = "0.11.0" signal-hook = "0.4.4" thiserror = "2.0.18" diff --git a/vm-migration/Cargo.toml b/vm-migration/Cargo.toml index 66b4e4f6a..09be84395 100644 --- a/vm-migration/Cargo.toml +++ b/vm-migration/Cargo.toml @@ -8,6 +8,8 @@ version = "0.1.0" [dependencies] anyhow = { workspace = true } itertools = { workspace = true } +log = { workspace = true } +rustls = { workspace = true } serde = { workspace = true, features = ["derive", "rc"] } serde_json = { workspace = true } thiserror = { workspace = true } diff --git a/vm-migration/src/lib.rs b/vm-migration/src/lib.rs index e91c87ba9..c163661de 100644 --- a/vm-migration/src/lib.rs +++ b/vm-migration/src/lib.rs @@ -19,6 +19,7 @@ use crate::protocol::MemoryRangeTable; mod bitpos_iterator; mod context; pub mod protocol; +pub mod tls; #[derive(Error, Debug)] pub enum UffdError { @@ -102,6 +103,9 @@ pub enum MigratableError { #[error("Lifecycle operation skipped for disconnected component {0}")] DeviceDisconnected(String), + + #[error("Error setting up a TLS-encrypted connection")] + Tls(#[source] tls::TlsError), } /// A Pausable component can be paused and resumed. diff --git a/vm-migration/src/tls.rs b/vm-migration/src/tls.rs new file mode 100644 index 000000000..149933de9 --- /dev/null +++ b/vm-migration/src/tls.rs @@ -0,0 +1,167 @@ +// Copyright © 2026 Cyberus Technology GmbH +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! TLS support for migration streams over TCP. +//! +//! This module wraps `rustls` to provide a blocking [`TlsStream`] for migration +//! traffic. [`TlsStream::new_client`] authenticates the server against +//! `ca-cert.pem` and the expected hostname, and presents `client-cert.pem` and +//! `client-key.pem` for mutual TLS (mTLS) authentication. [`TlsServerConfig`] loads +//! `server-cert.pem` and `server-key.pem`, trusts client certificates issued by +//! the CA in `ca-cert.pem`, and [`TlsStream::new_server`] uses that +//! configuration to establish the server side of the connection. +//! +//! [`TlsStream`] implements [`Read`], [`Write`], [`ReadVolatile`], +//! [`WriteVolatile`], and [`AsFd`] so it can be used by the transport layer like +//! other migration streams. All data must pass through rustls; direct I/O on the +//! underlying socket would bypass TLS processing and break the connection. + +use std::net::TcpStream; +use std::path::Path; +use std::result; +use std::sync::Arc; + +use log::warn; +use rustls::pki_types::pem::PemObject; +use rustls::pki_types::{CertificateDer, InvalidDnsNameError, PrivateKeyDer, ServerName}; +use rustls::server::VerifierBuilderError; +use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned}; +use thiserror::Error; + +use crate::MigratableError; + +const CA_CERT_FILE: &str = "ca-cert.pem"; +const CLIENT_CERT_FILE: &str = "client-cert.pem"; +const CLIENT_KEY_FILE: &str = "client-key.pem"; + +/// Errors that can occur when establishing a TLS-encrypted migration channel. +#[derive(Error, Debug)] +pub enum TlsError { + #[error("The provided hostname could not be parsed")] + InvalidDnsName(#[source] InvalidDnsNameError), + + #[error("Rustls protocol error")] + RustlsError(#[from] rustls::Error), + + #[error("Rustls verifier configuration error")] + RustlsVerifierBuilderError(#[source] VerifierBuilderError), + + #[error("Rustls protocol IO error")] + RustlsIoError(#[from] std::io::Error), + + #[error("TLS handshake stalled: no read/write progress while handshake is still in progress")] + HandshakeError, + + #[error("Error handling PEM file")] + RustlsPemError(#[from] rustls::pki_types::pem::Error), +} + +/// Wraps the concrete rustls stream for either side (server or client) of the +/// TLS connection. +/// +/// [`TlsStream`] uses this enum to store a [`StreamOwned`] with either a +/// [`ClientConnection`] or [`ServerConnection`] while exposing a single +/// transport-agnostic API. +#[derive(Debug)] +enum TlsStreamParticipant { + Client(StreamOwned), +} + +/// Server/Client-agnostic TLS stream. +pub struct TlsStream { + stream: TlsStreamParticipant, +} + +impl TlsStream { + /// Creates a client [`TlsStream`]. + /// + /// The client verifies the server certificate against `ca-cert.pem` and the + /// provided `hostname`, and presents the certificate chain in + /// `client-cert.pem` together with the private key in `client-key.pem` for + /// mutual TLS authentication. + pub fn new_client( + socket: TcpStream, + cert_dir: &Path, + hostname: &str, + ) -> result::Result { + let root_store = load_root_store(&cert_dir.join(CA_CERT_FILE))?; + let client_certs = load_cert_chain(&cert_dir.join(CLIENT_CERT_FILE))?; + let client_key = load_private_key(&cert_dir.join(CLIENT_KEY_FILE))?; + + let config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_client_auth_cert(client_certs, client_key) + .map_err(TlsError::RustlsError) + .map_err(MigratableError::Tls)?; + let config = Arc::new(config); + + let server_name = ServerName::try_from(hostname.to_string()) + .map_err(TlsError::InvalidDnsName) + .map_err(MigratableError::Tls)?; + let conn = ClientConnection::new(config, server_name) + .map_err(TlsError::RustlsError) + .map_err(MigratableError::Tls)?; + + let mut tls = StreamOwned::new(conn, socket); + while tls.conn.is_handshaking() { + let (rd, wr) = tls + .conn + .complete_io(&mut tls.sock) + .map_err(TlsError::RustlsIoError) + .map_err(MigratableError::Tls)?; + // No handshake progress on a connection that should be handshaking, we treat + // that as a failure. + if rd == 0 && wr == 0 { + return Err(MigratableError::Tls(TlsError::HandshakeError)); + } + } + + Ok(Self { + stream: TlsStreamParticipant::Client(tls), + }) + } +} + +/// Loads trusted CA certificates into a root store, i.e. the set of trust anchors +/// used to verify the peer's certificate chain. +fn load_root_store(cert_path: &Path) -> result::Result { + let mut root_store = RootCertStore::empty(); + let (_, ignored) = root_store.add_parsable_certificates( + CertificateDer::pem_file_iter(cert_path) + .map_err(TlsError::RustlsPemError) + .map_err(MigratableError::Tls)? + .map(|cert| cert.map_err(TlsError::RustlsPemError)) + .collect::>, TlsError>>() + .map_err(MigratableError::Tls)?, + ); + + if ignored > 0 { + warn!( + "Ignored {ignored} certificate(s) while loading TLS CA file {}", + cert_path.display() + ); + } + + Ok(root_store) +} + +/// Loads a certificate chain to present during the TLS handshake. +fn load_cert_chain( + cert_path: &Path, +) -> result::Result>, MigratableError> { + CertificateDer::pem_file_iter(cert_path) + .map_err(TlsError::RustlsPemError) + .map_err(MigratableError::Tls)? + .map(|cert| cert.map_err(TlsError::RustlsPemError)) + .collect::>, TlsError>>() + .map_err(MigratableError::Tls) +} + +/// Loads the private key that proves ownership of the presented certificate chain. +fn load_private_key(key_path: &Path) -> result::Result, MigratableError> { + PrivateKeyDer::from_pem_file(key_path) + .map_err(TlsError::RustlsPemError) + .map_err(MigratableError::Tls) +}