particle-os-cli/process/drivers/cosign_driver.rs

241 lines
6.9 KiB
Rust

use std::{fmt::Debug, fs, io::Write, path::Path, process::Stdio};
use blue_build_utils::{
cmd,
constants::{COSIGN_PASSWORD, COSIGN_PUB_PATH, COSIGN_YES},
credentials::Credentials,
};
use log::{debug, trace};
use miette::{bail, miette, Context, IntoDiagnostic, Result};
use crate::drivers::opts::VerifyType;
use super::{
functions::get_private_key,
opts::{CheckKeyPairOpts, GenerateKeyPairOpts, SignOpts, VerifyOpts},
SigningDriver,
};
#[derive(Debug)]
pub struct CosignDriver;
impl SigningDriver for CosignDriver {
fn generate_key_pair(opts: &GenerateKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let mut command = cmd!(
"cosign",
"generate-key-pair",
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
command.current_dir(path);
let status = command.status().into_diagnostic()?;
if !status.success() {
bail!("Failed to generate cosign key-pair!");
}
Ok(())
}
fn check_signing_files(opts: &CheckKeyPairOpts) -> Result<()> {
let path = opts.dir.as_ref().map_or_else(|| Path::new("."), |dir| dir);
let priv_key = get_private_key(path)?;
let mut command = cmd!(
"cosign",
"public-key",
format!("--key={priv_key}"),
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
trace!("{command:?}");
let output = command.output().into_diagnostic()?;
if !output.status.success() {
bail!(
"Failed to run cosign public-key: {}",
String::from_utf8_lossy(&output.stderr)
);
}
let calculated_pub_key = String::from_utf8(output.stdout).into_diagnostic()?;
let found_pub_key = fs::read_to_string(path.join(COSIGN_PUB_PATH))
.into_diagnostic()
.with_context(|| format!("Failed to read {COSIGN_PUB_PATH}"))?;
trace!("calculated_pub_key={calculated_pub_key},found_pub_key={found_pub_key}");
if calculated_pub_key.trim() == found_pub_key.trim() {
debug!("Cosign files match, continuing build");
Ok(())
} else {
bail!("Public key '{COSIGN_PUB_PATH}' does not match private key")
}
}
fn signing_login() -> Result<()> {
trace!("CosignDriver::signing_login()");
if let Some(Credentials {
registry,
username,
password,
}) = Credentials::get()
{
let mut command = cmd!(
"cosign",
"login",
"-u",
username,
"--password-stdin",
registry,
stdin = Stdio::piped(),
stdout = Stdio::piped(),
stderr = Stdio::piped(),
);
trace!("{command:?}");
let mut child = command.spawn().into_diagnostic()?;
write!(
child
.stdin
.as_mut()
.ok_or_else(|| miette!("Unable to open pipe to stdin"))?,
"{password}"
)
.into_diagnostic()?;
let output = child.wait_with_output().into_diagnostic()?;
if !output.status.success() {
let err_out = String::from_utf8_lossy(&output.stderr);
bail!("Failed to login for cosign:\n{}", err_out.trim());
}
debug!("Logged into {registry}");
}
Ok(())
}
fn sign(opts: &SignOpts) -> Result<()> {
let image_digest: &str = opts.image.as_ref();
let mut command = cmd!(
"cosign",
"sign",
if let Some(ref key) = opts.key => format!("--key={key}"),
"--recursive",
image_digest,
COSIGN_PASSWORD => "",
COSIGN_YES => "true",
);
trace!("{command:?}");
if !command.status().into_diagnostic()?.success() {
bail!("Failed to sign {image_digest}");
}
Ok(())
}
fn verify(opts: &VerifyOpts) -> Result<()> {
let image_name_tag: &str = opts.image.as_ref();
let mut command = cmd!(
"cosign",
"verify",
|c| {
match &opts.verify_type {
VerifyType::File(path) => cmd!(c, format!("--key={}", path.display())),
VerifyType::Keyless { issuer, identity } => cmd!(
c,
"--certificate-identity-regexp",
identity as &str,
"--certificate-oidc-issuer",
issuer as &str,
),
};
},
image_name_tag
);
trace!("{command:?}");
if !command.status().into_diagnostic()?.success() {
bail!("Failed to verify {image_name_tag}");
}
Ok(())
}
}
#[cfg(test)]
mod test {
use std::{fs, path::Path};
use blue_build_utils::constants::{COSIGN_PRIV_PATH, COSIGN_PUB_PATH};
use tempfile::TempDir;
use crate::drivers::{
opts::{CheckKeyPairOpts, GenerateKeyPairOpts},
SigningDriver,
};
use super::CosignDriver;
#[test]
fn generate_key_pair() {
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::check_signing_files(&check_opts).unwrap();
}
#[test]
fn check_key_pairs() {
let path = Path::new("../test-files/keys");
let opts = CheckKeyPairOpts::builder().dir(path).build();
CosignDriver::check_signing_files(&opts).unwrap();
}
#[test]
#[cfg(feature = "sigstore")]
fn compatibility() {
use crate::drivers::sigstore_driver::SigstoreDriver;
let tempdir = TempDir::new().unwrap();
let gen_opts = GenerateKeyPairOpts::builder().dir(tempdir.path()).build();
CosignDriver::generate_key_pair(&gen_opts).unwrap();
eprintln!(
"Private key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PRIV_PATH)).unwrap()
);
eprintln!(
"Public key:\n{}",
fs::read_to_string(tempdir.path().join(COSIGN_PUB_PATH)).unwrap()
);
let check_opts = CheckKeyPairOpts::builder().dir(tempdir.path()).build();
SigstoreDriver::check_signing_files(&check_opts).unwrap();
}
}