bootstrap: verify signatures
This commit is contained in:
@ -30,6 +30,9 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub crypt: Vec<CryptDev>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub signer_public_key: Option<String>,
|
||||
|
||||
pub bootstrap: Bootstrap,
|
||||
}
|
||||
|
||||
|
@ -257,10 +257,19 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
|
||||
error!("failed to create dir {dst}: {e}");
|
||||
}
|
||||
|
||||
let mut is_file = false;
|
||||
|
||||
if let Some(src) = src {
|
||||
retry_or_ignore(async || {
|
||||
is_file = (fs::metadata(src).await)
|
||||
.map_err(|e| format_err!("stat {src} failed: {e}"))?
|
||||
.is_file();
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
match fstype {
|
||||
"ext4" => {
|
||||
exec("fsck", &["-p", src]).await;
|
||||
exec("fsck.ext4", &["-p", src]).await;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@ -272,12 +281,10 @@ async fn mount(src: Option<&str>, dst: &str, fstype: &str, opts: Option<&str>) {
|
||||
}
|
||||
|
||||
retry_or_ignore(async || {
|
||||
// check source, use a loopdev if needed
|
||||
if let Some(src) = src {
|
||||
if fs::metadata(src).await?.is_file() {
|
||||
// loopdev crate has annoying dependencies, just use the normal mount program
|
||||
return try_exec("mount", &args).await;
|
||||
}
|
||||
// if it's a file, we need to use a loopdev
|
||||
if is_file {
|
||||
// loopdev crate has annoying dependencies, just use the normal mount program
|
||||
return try_exec("mount", &args).await;
|
||||
}
|
||||
|
||||
let (cmd_str, _) = cmd_str("mount", &args);
|
||||
|
@ -11,10 +11,10 @@ use crate::bootstrap::config::Config;
|
||||
use crate::{dkl, utils};
|
||||
|
||||
pub async fn bootstrap(cfg: Config) {
|
||||
let verifier = retry(async || Verifier::from_config(&cfg)).await;
|
||||
let bs = cfg.bootstrap;
|
||||
|
||||
retry_or_ignore(async || {
|
||||
fs::create_dir_all("/boostrap").await?;
|
||||
mount(Some(&bs.dev), "/bootstrap", "ext4", None).await;
|
||||
Ok(())
|
||||
})
|
||||
@ -33,12 +33,12 @@ pub async fn bootstrap(cfg: Config) {
|
||||
.await;
|
||||
|
||||
let sys_cfg: dkl::Config = retry(async || {
|
||||
let sys_cfg_bytes = seed_config(base_dir, &bs.seed).await?;
|
||||
let sys_cfg_bytes = seed_config(base_dir, &bs.seed, &verifier).await?;
|
||||
Ok(serde_yaml::from_slice(&sys_cfg_bytes)?)
|
||||
})
|
||||
.await;
|
||||
|
||||
mount_system(&sys_cfg, base_dir).await;
|
||||
mount_system(&sys_cfg, base_dir, &verifier).await;
|
||||
|
||||
retry_or_ignore(async || {
|
||||
let path = "/etc/resolv.conf";
|
||||
@ -68,7 +68,67 @@ pub async fn bootstrap(cfg: Config) {
|
||||
exec("chroot", &["/system", "update-ca-certificates"]).await
|
||||
}
|
||||
|
||||
async fn seed_config(base_dir: &str, seed_url: &Option<String>) -> Result<Vec<u8>> {
|
||||
struct Verifier {
|
||||
pubkey: Option<Vec<u8>>,
|
||||
}
|
||||
impl Verifier {
|
||||
fn from_config(cfg: &Config) -> Result<Self> {
|
||||
let Some(ref pubkey) = cfg.signer_public_key else {
|
||||
return Ok(Self { pubkey: None });
|
||||
};
|
||||
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
let pubkey = BASE64_STANDARD.decode(pubkey)?;
|
||||
let pubkey = Some(pubkey);
|
||||
|
||||
return Ok(Self { pubkey });
|
||||
}
|
||||
|
||||
async fn verify_path(&self, path: &str) -> Result<()> {
|
||||
let Some(ref pubkey) = self.pubkey else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("verifying {path}");
|
||||
|
||||
let mut pubkey = std::io::Cursor::new(pubkey);
|
||||
|
||||
let sig = format!("{path}.sig");
|
||||
|
||||
use std::process::Stdio;
|
||||
use tokio::process::Command;
|
||||
|
||||
let mut openssl = Command::new("openssl")
|
||||
.stdin(Stdio::piped())
|
||||
.args(&[
|
||||
"dgst",
|
||||
"-sha512",
|
||||
"-verify",
|
||||
"/dev/stdin",
|
||||
"-signature",
|
||||
&sig,
|
||||
path,
|
||||
])
|
||||
.spawn()?;
|
||||
|
||||
tokio::io::copy(&mut pubkey, openssl.stdin.as_mut().unwrap()).await?;
|
||||
|
||||
let status = openssl.wait().await?;
|
||||
if status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format_err!(
|
||||
"signature verification failed for {path}: {status}"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn seed_config(
|
||||
base_dir: &str,
|
||||
seed_url: &Option<String>,
|
||||
verifier: &Verifier,
|
||||
) -> Result<Vec<u8>> {
|
||||
let cfg_path = &format!("{base_dir}/config.yaml");
|
||||
|
||||
if fs::try_exists(cfg_path).await? {
|
||||
@ -92,6 +152,8 @@ async fn seed_config(base_dir: &str, seed_url: &Option<String>) -> Result<Vec<u8
|
||||
return Err(format_err!("{cfg_path} does not exist after seeding"));
|
||||
}
|
||||
|
||||
verifier.verify_path(&cfg_path).await?;
|
||||
|
||||
Ok(fs::read(cfg_path).await?)
|
||||
}
|
||||
|
||||
@ -107,7 +169,7 @@ async fn fetch_bootstrap(seed_url: &str, output_file: &str) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn mount_system(cfg: &dkl::Config, bs_dir: &str) {
|
||||
async fn mount_system(cfg: &dkl::Config, bs_dir: &str, verifier: &Verifier) {
|
||||
let mem_dir = "/mem";
|
||||
mount(None, mem_dir, "tmpfs", Some("size=512m")).await;
|
||||
|
||||
@ -116,14 +178,17 @@ async fn mount_system(cfg: &dkl::Config, bs_dir: &str) {
|
||||
|
||||
for layer in &cfg.layers {
|
||||
let src = if layer == "modules" {
|
||||
"/modules.sqfs"
|
||||
"/modules.sqfs".to_string()
|
||||
} else {
|
||||
&format!("{bs_dir}/{layer}.fs")
|
||||
let p = format!("{bs_dir}/{layer}.fs");
|
||||
retry(async || verifier.verify_path(&p).await).await;
|
||||
p
|
||||
};
|
||||
|
||||
let tgt = &format!("{mem_dir}/{layer}.fs");
|
||||
retry(async || {
|
||||
info!("copying layer {layer} from {src}");
|
||||
fs::copy(src, tgt).await?;
|
||||
fs::copy(&src, tgt).await?;
|
||||
Ok(())
|
||||
})
|
||||
.await;
|
||||
|
Reference in New Issue
Block a user