mirror of
https://github.com/cloud-hypervisor/cloud-hypervisor.git
synced 2026-08-05 02:19:16 +00:00
vmm: validate TLS related files
Validate that all files that are necessary for TLS encryption are in the given folder. The knowledge which files are necessary is part of the TLS module. On-behalf-of: SAP sebastian.eydam@sap.com Signed-off-by: Sebastian Eydam <sebastian.eydam@cyberus-technology.de>
This commit is contained in:
committed by
Rob Bradford
parent
58baee16ac
commit
320403a11e
@@ -18,10 +18,11 @@
|
||||
//! 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::fs::{self, File};
|
||||
use std::io::{self, BufRead, Read, Write};
|
||||
use std::net::TcpStream;
|
||||
use std::os::fd::{AsFd, BorrowedFd};
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::result;
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -44,6 +45,130 @@ const CLIENT_KEY_FILE: &str = "client-key.pem";
|
||||
const SERVER_CERT_FILE: &str = "server-cert.pem";
|
||||
const SERVER_KEY_FILE: &str = "server-key.pem";
|
||||
|
||||
/// Identifies which side of live migration uses a TLS certificate directory.
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TlsEndpoint {
|
||||
Client,
|
||||
Server,
|
||||
}
|
||||
|
||||
impl TlsEndpoint {
|
||||
fn required_files(self) -> [&'static str; 3] {
|
||||
match self {
|
||||
Self::Client => [CA_CERT_FILE, CLIENT_CERT_FILE, CLIENT_KEY_FILE],
|
||||
Self::Server => [CA_CERT_FILE, SERVER_CERT_FILE, SERVER_KEY_FILE],
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::Client => "migration client",
|
||||
Self::Server => "migration server",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validation errors for a migration TLS certificate directory.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TlsConfigError {
|
||||
#[error("TLS directory does not exist or is inaccessible: {path}: {source}")]
|
||||
DirectoryMetadata {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error("TLS directory must point to a directory: {path}")]
|
||||
NotADirectory { path: PathBuf },
|
||||
|
||||
#[error("Missing required TLS file for {endpoint}: {path}")]
|
||||
MissingFile {
|
||||
endpoint: &'static str,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("Required TLS path for {endpoint} must be a regular file: {path}")]
|
||||
NotAFile {
|
||||
endpoint: &'static str,
|
||||
path: PathBuf,
|
||||
},
|
||||
|
||||
#[error("Required TLS file for {endpoint} is not readable: {path}: {source}")]
|
||||
FileRead {
|
||||
endpoint: &'static str,
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error("Failed to inspect required TLS file for {endpoint}: {path}: {source}")]
|
||||
FileMetadata {
|
||||
endpoint: &'static str,
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: io::Error,
|
||||
},
|
||||
}
|
||||
|
||||
/// Validates that a TLS directory contains all files required by the endpoint.
|
||||
///
|
||||
/// Each required file must exist, be a regular file, and be readable by the
|
||||
/// current Cloud Hypervisor process.
|
||||
pub fn validate_tls_dir(
|
||||
cert_dir: &Path,
|
||||
endpoint: TlsEndpoint,
|
||||
) -> result::Result<(), TlsConfigError> {
|
||||
let metadata = fs::metadata(cert_dir).map_err(|source| TlsConfigError::DirectoryMetadata {
|
||||
path: cert_dir.to_path_buf(),
|
||||
source,
|
||||
})?;
|
||||
|
||||
if !metadata.is_dir() {
|
||||
return Err(TlsConfigError::NotADirectory {
|
||||
path: cert_dir.to_path_buf(),
|
||||
});
|
||||
}
|
||||
|
||||
for file_name in endpoint.required_files() {
|
||||
let path = cert_dir.join(file_name);
|
||||
let endpoint_name = endpoint.as_str();
|
||||
|
||||
let metadata = match fs::metadata(&path) {
|
||||
Ok(metadata) => metadata,
|
||||
Err(source) if source.kind() == io::ErrorKind::NotFound => {
|
||||
return Err(TlsConfigError::MissingFile {
|
||||
endpoint: endpoint_name,
|
||||
path,
|
||||
});
|
||||
}
|
||||
Err(source) => {
|
||||
return Err(TlsConfigError::FileMetadata {
|
||||
endpoint: endpoint_name,
|
||||
path,
|
||||
source,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if !metadata.is_file() {
|
||||
return Err(TlsConfigError::NotAFile {
|
||||
endpoint: endpoint_name,
|
||||
path,
|
||||
});
|
||||
}
|
||||
|
||||
if let Err(source) = File::open(&path) {
|
||||
return Err(TlsConfigError::FileRead {
|
||||
endpoint: endpoint_name,
|
||||
path,
|
||||
source,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Errors that can occur when establishing a TLS-encrypted migration channel.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TlsError {
|
||||
@@ -348,3 +473,92 @@ fn load_private_key(key_path: &Path) -> result::Result<PrivateKeyDer<'static>, M
|
||||
.map_err(TlsError::RustlsPemError)
|
||||
.map_err(MigratableError::Tls)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{fs, process};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TestDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestDir {
|
||||
fn new(name: &str) -> Self {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"cloud-hypervisor-{name}-{}-{unique}",
|
||||
process::id()
|
||||
));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn add_file(&self, file_name: &str) {
|
||||
fs::write(self.path.join(file_name), b"test").unwrap();
|
||||
}
|
||||
|
||||
fn add_client_files(&self) {
|
||||
self.add_file(CA_CERT_FILE);
|
||||
self.add_file(CLIENT_CERT_FILE);
|
||||
self.add_file(CLIENT_KEY_FILE);
|
||||
}
|
||||
|
||||
fn add_server_files(&self) {
|
||||
self.add_file(CA_CERT_FILE);
|
||||
self.add_file(SERVER_CERT_FILE);
|
||||
self.add_file(SERVER_KEY_FILE);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_tls_dir_accepts_complete_client_directory() {
|
||||
let dir = TestDir::new("tls-client");
|
||||
dir.add_client_files();
|
||||
|
||||
validate_tls_dir(&dir.path, TlsEndpoint::Client).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_tls_dir_accepts_complete_server_directory() {
|
||||
let dir = TestDir::new("tls-server");
|
||||
dir.add_server_files();
|
||||
|
||||
validate_tls_dir(&dir.path, TlsEndpoint::Server).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_tls_dir_rejects_missing_role_specific_file() {
|
||||
let dir = TestDir::new("tls-missing-client-key");
|
||||
dir.add_file(CA_CERT_FILE);
|
||||
dir.add_file(CLIENT_CERT_FILE);
|
||||
|
||||
let err = validate_tls_dir(&dir.path, TlsEndpoint::Client).unwrap_err();
|
||||
let err = err.to_string();
|
||||
assert!(err.contains(CLIENT_KEY_FILE), "unexpected error: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_tls_dir_rejects_non_file_entry() {
|
||||
let dir = TestDir::new("tls-non-file");
|
||||
dir.add_file(CA_CERT_FILE);
|
||||
dir.add_file(CLIENT_KEY_FILE);
|
||||
fs::create_dir(dir.path.join(CLIENT_CERT_FILE)).unwrap();
|
||||
|
||||
let err = validate_tls_dir(&dir.path, TlsEndpoint::Client).unwrap_err();
|
||||
let err = err.to_string();
|
||||
assert!(err.contains(CLIENT_CERT_FILE), "unexpected error: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ use option_parser::{OptionParser, OptionParserError, Toggle};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use vm_migration::MigratableError;
|
||||
use vm_migration::tls::{TlsEndpoint, validate_tls_dir};
|
||||
use vmm_sys_util::eventfd::EventFd;
|
||||
|
||||
#[cfg(feature = "dbus_api")]
|
||||
@@ -342,6 +343,14 @@ impl VmReceiveMigrationData {
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(tls_dir) = &self.tls_dir {
|
||||
validate_tls_dir(tls_dir, TlsEndpoint::Server).map_err(|e| {
|
||||
VmReceiveMigrationConfigError::ValidationError(format!(
|
||||
"invalid TLS configuration for receive-migration: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -576,6 +585,14 @@ impl VmSendMigrationData {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(tls_dir) = &self.tls_dir {
|
||||
validate_tls_dir(tls_dir, TlsEndpoint::Client).map_err(|e| {
|
||||
VmSendMigrationConfigError::ValidationError(format!(
|
||||
"invalid TLS configuration for send-migration: {e}"
|
||||
))
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1849,8 +1866,53 @@ impl ApiAction for VmNmi {
|
||||
|
||||
#[cfg(test)]
|
||||
mod unit_tests {
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{fs, process};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct TestDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TestDir {
|
||||
fn new(name: &str) -> Self {
|
||||
let unique = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"cloud-hypervisor-api-{name}-{}-{unique}",
|
||||
process::id()
|
||||
));
|
||||
fs::create_dir(&path).unwrap();
|
||||
Self { path }
|
||||
}
|
||||
|
||||
fn add_file(&self, file_name: &str) {
|
||||
fs::write(self.path.join(file_name), b"test").unwrap();
|
||||
}
|
||||
|
||||
fn add_receive_tls_files(&self) {
|
||||
self.add_file("ca-cert.pem");
|
||||
self.add_file("server-cert.pem");
|
||||
self.add_file("server-key.pem");
|
||||
}
|
||||
|
||||
fn add_send_tls_files(&self) {
|
||||
self.add_file("ca-cert.pem");
|
||||
self.add_file("client-cert.pem");
|
||||
self.add_file("client-key.pem");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vm_receive_migration_data_parse() {
|
||||
let data = VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1:8080").unwrap();
|
||||
@@ -1872,20 +1934,29 @@ mod unit_tests {
|
||||
let data = VmReceiveMigrationData::parse("receiver_url=unix:/tmp/ch=migrate.sock").unwrap();
|
||||
assert_eq!(data.receiver_url, "unix:/tmp/ch=migrate.sock");
|
||||
|
||||
let tls_dir = std::env::temp_dir();
|
||||
let tls_dir = TestDir::new("receive-tls");
|
||||
tls_dir.add_receive_tls_files();
|
||||
let tls_dir_path = tls_dir.path.clone();
|
||||
let data = VmReceiveMigrationData::parse(&format!(
|
||||
"receiver_url=tcp:192.168.1.1:8080,tls_dir={}",
|
||||
tls_dir.display()
|
||||
tls_dir_path.display()
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
data,
|
||||
VmReceiveMigrationData {
|
||||
receiver_url: "tcp:192.168.1.1:8080".to_string(),
|
||||
tls_dir: Some(tls_dir),
|
||||
tls_dir: Some(tls_dir_path),
|
||||
}
|
||||
);
|
||||
|
||||
let tls_dir = TestDir::new("receive-empty-tls");
|
||||
VmReceiveMigrationData::parse(&format!(
|
||||
"receiver_url=tcp:192.168.1.1:8080,tls_dir={}",
|
||||
tls_dir.path.display()
|
||||
))
|
||||
.unwrap_err();
|
||||
|
||||
VmReceiveMigrationData::parse("receiver_url=file:///tmp/migration").unwrap_err();
|
||||
VmReceiveMigrationData::parse("receiver_url=tcp:192.168.1.1").unwrap_err();
|
||||
VmReceiveMigrationData::parse("receiver_url=tcp:[2001:db8::1]").unwrap_err();
|
||||
@@ -1995,9 +2066,11 @@ mod unit_tests {
|
||||
);
|
||||
|
||||
// Happy path, fully specified
|
||||
let tls_dir = std::env::temp_dir();
|
||||
let tls_dir = TestDir::new("send-tls");
|
||||
tls_dir.add_send_tls_files();
|
||||
let tls_dir_path = tls_dir.path.clone();
|
||||
let data =
|
||||
VmSendMigrationData::parse(&format!("destination_url=tcp:192.168.1.1:8080,downtime_ms=150,timeout_s=900,timeout_strategy=ignore,connections=4,tls_dir={}", tls_dir.display()))
|
||||
VmSendMigrationData::parse(&format!("destination_url=tcp:192.168.1.1:8080,downtime_ms=150,timeout_s=900,timeout_strategy=ignore,connections=4,tls_dir={}", tls_dir_path.display()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
data,
|
||||
@@ -2008,7 +2081,7 @@ mod unit_tests {
|
||||
timeout_s: NonZeroU64::new(900).unwrap(),
|
||||
timeout_strategy: TimeoutStrategy::Ignore,
|
||||
connections: NonZeroU32::new(4).unwrap(),
|
||||
tls_dir: Some(tls_dir),
|
||||
tls_dir: Some(tls_dir_path),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user