diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..10c82bf --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +/target +/.cargo +/.rustup diff --git a/.gitignore b/.gitignore index 96ef6c0..d9375fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ /target Cargo.lock +/.cargo +/.rustup diff --git a/Cargo.toml b/Cargo.toml index 260599a..815b663 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,12 @@ edition = "2021" [dependencies] anyhow = "^1" +which = "^4" +glob = "^0.3" +log = "^0.4" + +[dev-dependencies] +coyote = "*" +openssl = "*" +tempdir = "*" +env_logger = "*" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3581620 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +# vim: ft=dockerfile + +FROM debian:latest + +RUN apt-get update -qq && apt-get install curl libnss3-tools build-essential libssl-dev pkg-config ca-certificates -y + +COPY hack/rustup.sh /bin +RUN chmod 755 /bin/rustup.sh + +CMD ["bash", "-c", "source /bin/rustup.sh && cd /root/ca_injector && cargo test -- --nocapture"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1f5d18d --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +DOCKER_IMAGE=ca-injector-test +DOCKER_RUN=docker run -e RUST_BACKTRACE=full -it --rm -v ${PWD}:/root/ca_injector -v ${PWD}/.rustup:/root/.rustup -v ${PWD}/.cargo:/root/.cargo ${DOCKER_IMAGE} + +test: build + mkdir -p .cargo .rustup + ${DOCKER_RUN} + +build: + docker build -t ${DOCKER_IMAGE} . diff --git a/hack/rustup.sh b/hack/rustup.sh new file mode 100644 index 0000000..72628d4 --- /dev/null +++ b/hack/rustup.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +if [ ! -x $HOME/.cargo/bin/rustup ] +then + curl -sSL sh.rustup.rs >/tmp/rustup.sh && bash /tmp/rustup.sh -y +fi + +if [ -f $HOME/.cargo/env ] +then + . $HOME/.cargo/env +fi + +rustup default stable diff --git a/src/lib.rs b/src/lib.rs index 9302d5d..d9b4c95 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,3 @@ pub fn uninstall_ca(filename: &str) -> Result<(), anyhow::Error> { filename )) } - -#[cfg(test)] -mod tests {} diff --git a/src/linux.rs b/src/linux.rs index 857c3ad..ae1b6cf 100644 --- a/src/linux.rs +++ b/src/linux.rs @@ -1,8 +1,103 @@ -use std::{path::PathBuf, str::FromStr}; +use std::{ + path::{Path, PathBuf}, + str::FromStr, +}; use anyhow::anyhow; +use glob::glob; +use which::which; + +fn certutil() -> Result { + Ok(which("certutil")?) +} + +fn nssdbs() -> Result, anyhow::Error> { + let home_var = std::env::var("HOME").unwrap_or("/".to_string()); + let home = Path::new(home_var.as_str()); + + // append all firefox profiles on the machine for the given user + // FIXME might want to enumerate all firefox profiles on the whole machine later + let mut paths = glob(home.join(".mozilla/firefox/*").to_str().unwrap())? + .map(|p| p.unwrap()) + .collect::>(); + + let mut other_paths = vec![ + home.join(".pki/nssdb"), + home.join("snap/chromium/current/.pki/nssdb"), + PathBuf::from_str("/etc/pki/nssdb")?, + ]; + + paths.append(&mut other_paths); + + Ok(paths) +} + +fn install_nss(filename: &str) -> Result<(), anyhow::Error> { + let certutil = certutil()?; + + for db in nssdbs()? { + match db.metadata() { + Ok(meta) => { + if meta.is_dir() { + log::debug!( + "Running certutil for {} against {} to install the cert", + db.display(), + filename + ); + std::process::Command::new(certutil.clone()) + .args(vec![ + "-A", + "-d", + db.to_str().unwrap(), + "-t", + "C,,", + "-n", + filename, + "-i", + filename, + ]) + .env_clear() + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status()?; + } + } + _ => {} + } + } + + Ok(()) +} + +fn uninstall_nss(filename: &str) -> Result<(), anyhow::Error> { + let certutil = certutil()?; + + for db in nssdbs()? { + match db.metadata() { + Ok(meta) => { + if meta.is_dir() { + log::debug!( + "Running certutil for {} against {} to install the cert", + db.display(), + filename + ); + std::process::Command::new(certutil.clone()) + .args(vec!["-D", "-d", db.to_str().unwrap(), "-n", filename]) + .env_clear() + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status()?; + } + } + _ => {} + } + } + + Ok(()) +} -#[derive(Debug, Clone)] struct TrustStoreMetadata { dir: &'static str, bin: &'static str, @@ -24,7 +119,7 @@ fn get_trust_store_command() -> Result { if md.is_dir() { return Ok(TrustStoreMetadata { dir: "/usr/local/share/ca-certificates", - bin: "update-ca-certificates", + bin: "/usr/sbin/update-ca-certificates", args: vec![], }); } @@ -55,13 +150,21 @@ fn get_trust_store_command() -> Result { fn template_filename(filename: &str, tsc: &TrustStoreMetadata) -> Result { let pb = PathBuf::from_str(tsc.dir)?; - Ok(pb.join(filename.replace(" ", "_").replace(".crt", ".pem"))) + Ok(pb.join( + Path::new(filename) + .file_name() + .unwrap() + .to_string_lossy() + .replace(" ", "_") + .replace(".pem", ".crt"), + )) } -fn update(tsc: &TrustStoreMetadata) -> Result { +fn update_ca(tsc: &TrustStoreMetadata) -> Result { + log::debug!("Executing {} {:?}", tsc.bin, tsc.args); + Ok(std::process::Command::new(tsc.bin) .args(tsc.args.clone()) - .env_clear() .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) @@ -70,26 +173,64 @@ fn update(tsc: &TrustStoreMetadata) -> Result Result<(), anyhow::Error> { let tsc = get_trust_store_command()?; - std::fs::copy(filename, template_filename(filename, &tsc)?)?; + let new_filename = template_filename(filename, &tsc)?; - let res = update(&tsc)?; + log::debug!( + "copying cert from {} to {}", + filename.to_string(), + new_filename.display() + ); + + std::fs::copy(filename, new_filename)?; + + let res = update_ca(&tsc)?; if !res.success() { return Err(anyhow!("Unable to install CA certificate")); } - Ok(()) + Ok(install_nss(filename)?) } pub fn uninstall_ca(filename: &str) -> Result<(), anyhow::Error> { let tsc = get_trust_store_command()?; std::fs::remove_file(template_filename(filename, &tsc)?)?; - let res = update(&tsc)?; + let res = update_ca(&tsc)?; if !res.success() { return Err(anyhow!("Unable to uninstall CA certificate")); } - Ok(()) + Ok(uninstall_nss(filename)?) +} + +#[cfg(test)] +mod tests { + use tempdir::TempDir; + + #[test] + fn test_install() { + env_logger::builder() + .filter_level(log::LevelFilter::Debug) + .init(); + + use coyote::acme::ca::CA; + + for filename in vec![ + "test.pem", + "file with spaces.pem", + "certificate.crt", + "this_other_thing.crt", + ] { + let ca = CA::new_test_ca().unwrap(); + let dir = TempDir::new("").unwrap(); + + let test_pem = dir.path().join(filename); + + std::fs::write(test_pem.clone(), ca.certificate().to_pem().unwrap()).unwrap(); + super::install_ca(test_pem.to_str().unwrap()).unwrap(); + super::uninstall_ca(test_pem.to_str().unwrap()).unwrap(); + } + } }