detee-cli/src/snp/injector.rs

154 lines
5.8 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
use crate::config::Config;
use crate::snp::grpc::proto;
use log::debug;
use std::net::IpAddr;
#[derive(Debug)]
pub struct Args {
pub vm_id: String,
pub vcpus: u32,
pub kernel: String,
pub initrd: String,
pub args: proto::MeasurementArgs,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to execute: {0}")]
FailedExecution(String),
#[error("Disk access error: {0}")]
DiskAccess(#[from] std::io::Error),
#[error("Could not parse kernel parameters sent by deamon")]
AlteredKernelParams,
#[error("Could not inject secrets to VM: {0}")]
InjectorError(String),
#[error(transparent)]
ConfigError(#[from] crate::config::Error),
#[error("The endpoint returned by the daemon is not valid: {0}")]
InvalidVmAddress(String),
}
impl From<std::net::AddrParseError> for Error {
fn from(_: std::net::AddrParseError) -> Self {
Self::AlteredKernelParams
}
}
impl From<std::num::ParseIntError> for Error {
fn from(_: std::num::ParseIntError) -> Self {
Self::AlteredKernelParams
}
}
impl Args {
pub fn get_measurement(&self) -> Result<String, Error> {
log::debug!("Measurement args: {self:#?}");
let params = self.kernel_params()?;
Config::verify_and_install_artefacts(&self.args.ovmf_hash)?;
Config::verify_and_install_artefacts(&self.initrd)?;
Config::verify_and_install_artefacts(&self.kernel)?;
let artefacts_dir = Config::artefacts_dir()? + "/";
let script_result = std::process::Command::new("sev-snp-measure.py")
.arg("--mode=snp")
.arg(format!("--vcpus={}", self.vcpus))
.arg("--vcpu-type=EPYC-v4")
.arg("--output-format=hex")
.arg(format!("--ovmf={}", artefacts_dir.clone() + &self.args.ovmf_hash))
.arg(format!("--kernel={}", artefacts_dir.clone() + &self.kernel))
.arg(format!("--initrd={}", artefacts_dir + &self.initrd))
.arg(format!("--append={}", params))
.output()
.map_err(|e| {
Error::FailedExecution(format!("Failed to execute sev-snp-measure.py: {e:?}"))
})?;
if !script_result.status.success() {
return Err(Error::FailedExecution(format!(
"sev-snp-measure.py failed: !!! stdout:\n{}\n!!! stderr:\n{}",
String::from_utf8(script_result.stdout.clone())
.unwrap_or("Could not grab stdout from installation script.".to_string()),
String::from_utf8(script_result.stderr.clone())
.unwrap_or("Could not grab stderr from installation script.".to_string())
)));
}
Ok(String::from_utf8(script_result.stdout)
.map_err(|e| {
Error::FailedExecution(format!("Could not parse sev-snp-measure.py output: {e:?}"))
})?
.trim_end()
.to_string())
}
// This must produce the exact same params as the same operation on the daemon.
// Passing kernel params as String from the daemon leaves the install open for attacks.
fn kernel_params(&self) -> Result<String, Error> {
let mut ip_string = String::new();
let mut public_ipv4 = false;
for ip in self.args.ips.iter() {
if ip.address.parse::<IpAddr>()?.is_ipv4() {
public_ipv4 = true;
}
ip.gateway.parse::<IpAddr>()?;
ip.mask.parse::<u128>()?;
ip_string += &format!(
"detee_net_eth{}={}_{}_{} ",
ip.nic_index, ip.address, ip.mask, ip.gateway
);
}
if !public_ipv4 {
ip_string = "detee_net_eth0=10.0.2.15_24_10.0.2.2 ".to_string() + &ip_string;
}
let admin_key = format!("detee_admin={} ", Config::get_detee_wallet()?);
let hostname = format!("detee_vm_id={}", self.vm_id);
let params = format!("{}{}{}", ip_string, admin_key, hostname);
debug!("Calculated kernel params for {} to: {}", self.vm_id, params);
Ok(params)
}
}
pub fn execute(
measurement: String,
server_addr: String,
os_template: Option<(&str, &str)>,
vm_hostname: &str,
) -> Result<super::VmSshArgs, Error> {
let parsed_addr = match server_addr.parse::<std::net::SocketAddrV4>() {
Ok(addr) => addr,
Err(_) => return Err(Error::InvalidVmAddress(server_addr)),
};
let ssh_pubkey = Config::init_config().get_ssh_pubkey()?;
let ssh_args = super::VmSshArgs {
ip: parsed_addr.ip().to_string(),
port: parsed_addr.port().to_string(),
user: "root".to_string(),
key_path: ssh_pubkey.trim_end_matches(".pub").to_string(),
..Default::default()
};
eprintln!("Injecting disk encryption key into VM. This will take a minute. Do not interrupt.");
let (os_template_url, os_template_sha) = os_template.unwrap_or(("", ""));
let logs_path = Config::logs_dir()? + "/" + vm_hostname;
log::info!("Logs will be saved to {}", logs_path);
let logs_file = std::fs::File::create(logs_path.clone())?;
let mut child_process = std::process::Command::new("detee-cli_injector.sh")
.env("SERVER_ADDR", server_addr)
.env("SSH_KEY_FILE", ssh_pubkey)
.env("DETEE_INSTALL_URL", os_template_url)
.env("DETEE_INSTALL_SHA", os_template_sha)
.env("MEASUREMENT", measurement)
.env("VM_HOSTNAME", vm_hostname)
.stdout(logs_file.try_clone()?)
.stderr(logs_file)
.spawn()?;
let status = child_process.wait()?;
if !status.success() {
log::error!(
"detee-cli_injector.sh exit code: {:?}! Check logs at: {}",
status.code(),
logs_path
);
return Err(Error::InjectorError("Script execution failed".to_string()));
}
Ok(ssh_args)
}