diff options
author | Heiko <heiko@schaefer.name> | 2021-05-07 12:46:53 +0200 |
---|---|---|
committer | Heiko <heiko@schaefer.name> | 2021-05-07 12:47:44 +0200 |
commit | f581d1be5ae7c7d2d9abdfbfa2af4feef1d6e46f (patch) | |
tree | 5634268fd699abf622766a6b805bb4a7c0b35c91 | |
parent | e0d66ddf4a36d391546c70e73b592c530721260a (diff) | |
download | openpgp-ca-f581d1be5ae7c7d2d9abdfbfa2af4feef1d6e46f.tar.gz |
Move gnupg wrapper into its own crate "gnupg-test-wrapper".
-rw-r--r-- | Cargo.lock | 17 | ||||
-rw-r--r-- | Cargo.toml | 1 | ||||
-rw-r--r-- | gnupg-test-wrapper/Cargo.toml | 19 | ||||
-rw-r--r-- | gnupg-test-wrapper/src/lib.rs (renamed from openpgp-ca-lib/tests/gnupg/mod.rs) | 4 | ||||
-rw-r--r-- | openpgp-ca-lib/Cargo.toml | 3 | ||||
-rw-r--r-- | openpgp-ca-lib/tests/test_gpg.rs | 2 | ||||
-rw-r--r-- | openpgp-ca-lib/tests/test_oca.rs | 2 | ||||
-rw-r--r-- | openpgp-ca-restd/Cargo.toml | 4 | ||||
-rw-r--r-- | openpgp-ca-restd/tests/gnupg/mod.rs | 544 | ||||
-rw-r--r-- | openpgp-ca-restd/tests/test_restd.rs | 3 |
10 files changed, 39 insertions, 560 deletions
@@ -1025,6 +1025,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" [[package]] +name = "gnupg-test-wrapper" +version = "0.0.1" +dependencies = [ + "anyhow", + "csv", + "rexpect", + "tempfile", +] + +[[package]] name = "h2" version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1749,12 +1759,11 @@ dependencies = [ "anyhow", "chbs", "chrono", - "csv", "diesel", "diesel_migrations", + "gnupg-test-wrapper", "openpgp-keylist", "publicsuffix", - "rexpect", "sequoia-net", "sequoia-openpgp", "sha2", @@ -1769,18 +1778,16 @@ dependencies = [ "anyhow", "chrono", "clap", - "csv", + "gnupg-test-wrapper", "once_cell", "openpgp-ca-lib", "reqwest 0.10.10", - "rexpect", "rocket", "rocket_contrib", "sequoia-openpgp", "serde", "serde_json", "structopt", - "tempfile", "tokio 0.2.25", ] @@ -6,4 +6,5 @@ members = [ "openpgp-ca-lib", "openpgp-ca-bin", "openpgp-ca-restd", + "gnupg-test-wrapper", ] diff --git a/gnupg-test-wrapper/Cargo.toml b/gnupg-test-wrapper/Cargo.toml new file mode 100644 index 0000000..3f52c3b --- /dev/null +++ b/gnupg-test-wrapper/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2019-2021 Heiko Schaefer <heiko@schaefer.name> +# SPDX-License-Identifier: GPL-3.0-or-later + +[package] +name = "gnupg-test-wrapper" +version = "0.0.1" +description = "A wrapper for using GnuPG in integration tests. Don't use for security critical purposes!" +authors = ["Heiko Schaefer <heiko@schaefer.name>"] +license = "GPL-3.0-or-later" +categories = ["cryptography", "email"] +keywords = ["OpenPGP", "GnuPG", "PGP"] +repository = "https://gitlab.com/openpgp-ca/openpgp-ca" +edition = "2018" + +[dependencies] +anyhow = "1.0" +tempfile = "3.1" +rexpect = "0.4" +csv = "1.1" diff --git a/openpgp-ca-lib/tests/gnupg/mod.rs b/gnupg-test-wrapper/src/lib.rs index 6eb9c5b..9beb341 100644 --- a/openpgp-ca-lib/tests/gnupg/mod.rs +++ b/gnupg-test-wrapper/src/lib.rs @@ -1,9 +1,9 @@ -// Copyright 2019-2020 Heiko Schaefer <heiko@schaefer.name> +// Copyright 2019-2021 Heiko Schaefer <heiko@schaefer.name> // // This file is part of OpenPGP CA // https://gitlab.com/openpgp-ca/openpgp-ca // -// SPDX-FileCopyrightText: 2019-2020 Heiko Schaefer <heiko@schaefer.name> +// SPDX-FileCopyrightText: 2019-2021 Heiko Schaefer <heiko@schaefer.name> // SPDX-License-Identifier: GPL-3.0-or-later use std::collections::BTreeMap; diff --git a/openpgp-ca-lib/Cargo.toml b/openpgp-ca-lib/Cargo.toml index 8fabe52..5cab6da 100644 --- a/openpgp-ca-lib/Cargo.toml +++ b/openpgp-ca-lib/Cargo.toml @@ -39,6 +39,5 @@ sequoia-net = "0.23" # for tests [dev-dependencies] +gnupg-test-wrapper = { path = "../gnupg-test-wrapper" } tempfile = "3.1" -csv = "1.1" -rexpect = "0.4" diff --git a/openpgp-ca-lib/tests/test_gpg.rs b/openpgp-ca-lib/tests/test_gpg.rs index cb63488..fb67328 100644 --- a/openpgp-ca-lib/tests/test_gpg.rs +++ b/openpgp-ca-lib/tests/test_gpg.rs @@ -15,7 +15,7 @@ use openpgp_ca_lib::pgp::Pgp; use anyhow::{Context, Result}; use std::path::PathBuf; -pub mod gnupg; +use gnupg_test_wrapper as gnupg; #[test] /// Create a new CA. Create user certs for Alice and Bob in OpenPGP CA. diff --git a/openpgp-ca-lib/tests/test_oca.rs b/openpgp-ca-lib/tests/test_oca.rs index 8132e68..a269490 100644 --- a/openpgp-ca-lib/tests/test_oca.rs +++ b/openpgp-ca-lib/tests/test_oca.rs @@ -25,7 +25,7 @@ use openpgp_ca_lib::ca::OpenpgpCa; use openpgp_ca_lib::pgp::Pgp; use sequoia_net::Policy; -pub mod gnupg; +use gnupg_test_wrapper as gnupg; #[test] /// Creates a CA (with a custom name) and a user. diff --git a/openpgp-ca-restd/Cargo.toml b/openpgp-ca-restd/Cargo.toml index e0d3dcc..478a59a 100644 --- a/openpgp-ca-restd/Cargo.toml +++ b/openpgp-ca-restd/Cargo.toml @@ -46,6 +46,4 @@ features = ["fs", "io-std", "io-util", "rt-threaded", "sync", "signal", "macros" # for tests [dev-dependencies] -tempfile = "3.1" -csv = "1.1" -rexpect = "0.4" +gnupg-test-wrapper = { path = "../gnupg-test-wrapper" }
\ No newline at end of file diff --git a/openpgp-ca-restd/tests/gnupg/mod.rs b/openpgp-ca-restd/tests/gnupg/mod.rs deleted file mode 100644 index 6eb9c5b..0000000 --- a/openpgp-ca-restd/tests/gnupg/mod.rs +++ /dev/null @@ -1,544 +0,0 @@ -// Copyright 2019-2020 Heiko Schaefer <heiko@schaefer.name> -// -// This file is part of OpenPGP CA -// https://gitlab.com/openpgp-ca/openpgp-ca -// -// SPDX-FileCopyrightText: 2019-2020 Heiko Schaefer <heiko@schaefer.name> -// SPDX-License-Identifier: GPL-3.0-or-later - -use std::collections::BTreeMap; -use std::collections::HashMap; - -use std::fmt; -use std::io::Read; -use std::io::Write; -use std::path::{Path, PathBuf}; -use std::process::Command; -use std::process::Stdio; - -use anyhow::{Context, Result}; -use csv::StringRecord; - -// FIXME: `LC_ALL=C` to make calls locale-independent - -pub fn make_context() -> Result<Ctx> { - let ctx = Ctx::ephemeral().context( - "SKIP: Failed to create GnuPG context. Is GnuPG installed?", - )?; - - ctx.start("gpg-agent").context( - "SKIP: Failed to to start gpg-agent. Is the GnuPG agent installed?", - )?; - - Ok(ctx) -} - -/// A GnuPG context. -#[derive(Debug)] -pub struct Ctx { - homedir: Option<PathBuf>, - components: BTreeMap<String, PathBuf>, - directories: BTreeMap<String, PathBuf>, - sockets: BTreeMap<String, PathBuf>, - #[allow(dead_code)] // We keep it around for the cleanup. - ephemeral: Option<tempfile::TempDir>, -} - -impl Ctx { - /// Creates a new context for the default GnuPG home directory. - pub fn new() -> Result<Self> { - Self::make(None, None) - } - - /// get the homedir Path - pub fn get_homedir(&self) -> &Path { - self.homedir.as_ref().unwrap().as_path() - } - - /// Creates a new context for the given GnuPG home directory. - pub fn with_homedir<P>(homedir: P) -> Result<Self> - where - P: AsRef<Path>, - { - Self::make(Some(homedir.as_ref()), None) - } - - /// Creates a new ephemeral context. - /// - /// The created home directory will be deleted once this object is - /// dropped. - pub fn ephemeral() -> Result<Self> { - Self::make(None, Some(tempfile::tempdir()?)) - } - - /// don't delete home directory. - /// this is intended for manually debugging data that was created in a - /// test-run. - pub fn leak_tempdir(&mut self) -> Option<PathBuf> { - if self.ephemeral.is_some() { - let _ = self.stop_all(); - let _ = self.remove_socket_dir(); - } - self.ephemeral.take().map(tempfile::TempDir::into_path) - } - - fn make( - homedir: Option<&Path>, - ephemeral: Option<tempfile::TempDir>, - ) -> Result<Self> { - let mut components: BTreeMap<String, PathBuf> = Default::default(); - let mut directories: BTreeMap<String, PathBuf> = Default::default(); - let mut sockets: BTreeMap<String, PathBuf> = Default::default(); - - let homedir: Option<PathBuf> = ephemeral - .as_ref() - .map(|tmp| tmp.path()) - .or(homedir) - .map(|p| p.into()); - - for fields in - Self::gpgconf(&homedir, &["--list-components"], 3)?.into_iter() - { - components.insert( - String::from_utf8(fields[0].clone())?, - String::from_utf8(fields[2].clone())?.into(), - ); - } - - for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)?.into_iter() - { - let (mut key, value) = (fields[0].clone(), fields[1].clone()); - if key.ends_with(b"-socket") { - let l = key.len(); - key.truncate(l - b"-socket".len()); - sockets.insert( - String::from_utf8(key)?, - String::from_utf8(value)?.into(), - ); - } else { - directories.insert( - String::from_utf8(key)?, - String::from_utf8(value)?.into(), - ); - } - } - - Ok(Ctx { - homedir, - components, - directories, - sockets, - ephemeral, - }) - } - - fn gpgconf( - homedir: &Option<PathBuf>, - arguments: &[&str], - nfields: usize, - ) -> Result<Vec<Vec<Vec<u8>>>> { - let nl = |&c: &u8| c as char == '\n'; - let colon = |&c: &u8| c as char == ':'; - - let mut gpgconf = Command::new("gpgconf"); - if let Some(homedir) = homedir { - gpgconf.arg("--homedir").arg(homedir); - - // https://dev.gnupg.org/T4496 - gpgconf.env("GNUPGHOME", homedir); - } - - for argument in arguments { - gpgconf.arg(argument); - } - let output = gpgconf.output().map_err(|e| -> anyhow::Error { - GnupgError::GgpConf(e.to_string()).into() - })?; - - if output.status.success() { - let mut result = Vec::new(); - for line in output.stdout.split(nl) { - if line.is_empty() { - // EOF. - break; - } - - let fields = line - .splitn(nfields, colon) - .map(|f| f.to_vec()) - .collect::<Vec<_>>(); - - if fields.len() != nfields { - return Err(GnupgError::GgpConf(format!( - "Malformed response, expected {} fields, \ - on line: {:?}", - nfields, line - )) - .into()); - } - - result.push(fields); - } - Ok(result) - } else { - Err(GnupgError::GgpConf( - String::from_utf8_lossy(&output.stderr).into_owned(), - ) - .into()) - } - } - - /// Returns the path to a GnuPG component. - pub fn component<C>(&self, component: C) -> Result<&Path> - where - C: AsRef<str>, - { - self.components - .get(component.as_ref()) - .map(|p| p.as_path()) - .ok_or_else(|| { - GnupgError::GgpConf(format!( - "No such component {:?}", - component.as_ref() - )) - .into() - }) - } - - /// Returns the path to a GnuPG directory. - pub fn directory<C>(&self, directory: C) -> Result<&Path> - where - C: AsRef<str>, - { - self.directories - .get(directory.as_ref()) - .map(|p| p.as_path()) - .ok_or_else(|| { - GnupgError::GgpConf(format!( - "No such directory {:?}", - directory.as_ref() - )) - .into() - }) - } - - /// Returns the path to a GnuPG socket. - pub fn socket<C>(&self, socket: C) -> Result<&Path> - where - C: AsRef<str>, - { - self.sockets - .get(socket.as_ref()) - .map(|p| p.as_path()) - .ok_or_else(|| { - GnupgError::GgpConf(format!( - "No such socket {:?}", - socket.as_ref() - )) - .into() - }) - } - - /// Creates directories for RPC communication. - pub fn create_socket_dir(&self) -> Result<()> { - Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?; - Ok(()) - } - - /// Removes directories for RPC communication. - /// - /// Note: This will stop all servers once they note that their - /// socket is gone. - pub fn remove_socket_dir(&self) -> Result<()> { - Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?; - Ok(()) - } - - /// Starts a GnuPG component. - pub fn start(&self, component: &str) -> Result<()> { - self.create_socket_dir()?; - Self::gpgconf(&self.homedir, &["--launch", component], 1)?; - Ok(()) - } - - /// Stops a GnuPG component. - pub fn stop(&self, component: &str) -> Result<()> { - Self::gpgconf(&self.homedir, &["--kill", component], 1)?; - Ok(()) - } - - /// Stops all GnuPG components. - pub fn stop_all(&self) -> Result<()> { - self.stop("all") - } -} - -impl Drop for Ctx { - fn drop(&mut self) { - if self.ephemeral.is_some() { - let _ = self.stop_all(); - let _ = self.remove_socket_dir(); - } - } -} - -impl std::error::Error for GnupgError {} - -impl fmt::Display for GnupgError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - GnupgError::GgpConf(s) => write!(f, "gpgconf: {}", s), - GnupgError::OperationFailed(s) => { - write!(f, "Operation failed: {}", s) - } - GnupgError::ProtocolError(s) => { - write!(f, "Protocol violation: {}", s) - } - } - } -} - -#[derive(Debug)] -/// Errors used in this module. -pub enum GnupgError { - /// Errors related to `gpgconf`. - GgpConf(String), - - /// The remote operation failed. - OperationFailed(String), - - /// The remote party violated the protocol. - ProtocolError(String), -} - -pub fn import(ctx: &Ctx, what: &[u8]) { - let mut gpg = Command::new("gpg") - .stdin(Stdio::piped()) - .arg("--homedir") - .arg(ctx.directory("homedir").unwrap()) - .arg("--import") - .spawn() - .expect("failed to start gpg"); - gpg.stdin.as_mut().unwrap().write_all(what).unwrap(); - let status = gpg.wait().unwrap(); - assert!(status.success()); -} - -pub fn export(ctx: &Ctx, search: &str) -> String { - let mut out = String::new(); - - let mut gpg = Command::new("gpg") - .stdout(Stdio::piped()) - .arg("--homedir") - .arg(ctx.directory("homedir").unwrap()) - .arg("--armor") - .arg("--export") - .arg(search) - .spawn() - .expect("failed to start gpg"); - let status = gpg.wait().unwrap(); - gpg.stdout - .as_mut() - .unwrap() - .read_to_string(&mut out) - .unwrap(); - assert!(status.success()); - - out -} - -pub fn export_secret(ctx: &Ctx, search: &str) -> String { - let mut out = String::new(); - - let mut gpg = Command::new("gpg") - .stdout(Stdio::piped()) - .arg("--homedir") - .arg(ctx.directory("homedir").unwrap()) - .arg("--armor") - .arg("--export-secret-keys") - .arg(search) - .spawn() - .expect("failed to start gpg"); - let status = gpg.wait().unwrap(); - gpg.stdout - .as_mut() - .unwrap() - .read_to_string(&mut out) - .unwrap(); - assert!(status.success()); - - out -} - -pub fn list_keys(ctx: &Ctx) -> Result<HashMap<String, String>> { - let res = list_keys_raw(&ctx); - - // filter: keep only the "uid" lines - let uids = res - .iter() - .filter(|&line| line.get(0) == Some("uid")) - .cloned() - .collect::<Vec<_>>(); - - // map: uid -> trust - Ok(uids - .iter() - .map(|u| (u.get(9).unwrap().to_owned(), u.get(1).unwrap().to_owned())) - .collect()) -} - -fn list_keys_raw(ctx: &Ctx) -> Vec<StringRecord> { - let gpg = Command::new("gpg") - .stdin(Stdio::piped()) - .arg("--homedir") - .arg(ctx.directory("homedir").unwrap()) - .arg("--list-keys") - .arg("--with-colons") - .output() - .expect("failed to start gpg"); - - let mut rdr = csv::ReaderBuilder::new() - .has_headers(false) - .delimiter(b':') - .flexible(true) - .from_reader(gpg.stdout.as_slice()); - - let status = gpg.status; - assert!(status.success()); - - rdr.records().map(|rec| rec.unwrap()).collect() -} - -pub fn edit_trust(ctx: &Ctx, user_id: &str, trust: u8) -> Result<()> { - let homedir = - String::from(ctx.directory("homedir").unwrap().to_str().unwrap()); - - let cmd = format!("gpg --homedir {} --edit-key {}", homedir, user_id); - - let mut p = rexpect::spawn(&cmd, Some(10_000)).unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("trust").unwrap(); - p.exp_string("Your decision?").unwrap(); - p.send_line(&format!("{}", trust)).unwrap(); - p.exp_string( - "Do you really want to set this key to ultimate trust? (y/N)", - ) - .unwrap(); - p.send_line("y").unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("quit").unwrap(); - p.exp_eof().unwrap(); - - Ok(()) -} - -pub fn make_revocation( - ctx: &Ctx, - user_id: &str, - filename: &str, - reason: u8, -) -> Result<()> { - let homedir = - String::from(ctx.directory("homedir").unwrap().to_str().unwrap()); - - let cmd = format!( - "gpg --homedir {} --output {} --gen-revoke {}", - homedir, filename, user_id - ); - - let mut p = rexpect::spawn(&cmd, Some(10_000)).unwrap(); - p.exp_string("Create a revocation certificate for this key? (y/N)") - .unwrap(); - p.send_line("y").unwrap(); - p.exp_string("Your decision?").unwrap(); - p.send_line(&format!("{}", reason)).unwrap(); - p.exp_string(">").unwrap(); - p.send_line("").unwrap(); - p.exp_string("Is this okay? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_eof().unwrap(); - - Ok(()) -} - -pub fn edit_expire(ctx: &Ctx, user_id: &str, expires: &str) -> Result<()> { - let homedir = - String::from(ctx.directory("homedir").unwrap().to_str().unwrap()); - - let cmd = format!("gpg --homedir {} --edit-key {}", homedir, user_id); - - let mut p = rexpect::spawn(&cmd, Some(10_000)).unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("expire").unwrap(); - p.exp_string("Key is valid for? (0)").unwrap(); - p.send_line(expires).unwrap(); - p.exp_string("Is this correct? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("quit").unwrap(); - p.exp_string("Save changes? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_eof().unwrap(); - - Ok(()) -} - -pub fn create_user(ctx: &Ctx, user_id: &str) { - let mut gpg = Command::new("gpg") - .stdin(Stdio::piped()) - .arg("--homedir") - .arg(ctx.directory("homedir").unwrap()) - .arg("--quick-generate-key") - .arg("--batch") - .arg("--passphrase") - .arg("") - .arg(user_id.to_string()) - .spawn() - .expect("failed to start gpg"); - let status = gpg.wait().unwrap(); - assert!(status.success()); -} - -pub fn sign(ctx: &Ctx, user_id: &str) -> Result<()> { - let homedir = - String::from(ctx.directory("homedir").unwrap().to_str().unwrap()); - - let cmd = format!("gpg --homedir {} --edit-key {}", homedir, user_id); - - let mut p = rexpect::spawn(&cmd, Some(10_000)).unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("sign").unwrap(); - p.exp_string("Really sign? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("save").unwrap(); - p.exp_eof().unwrap(); - - Ok(()) -} - -pub fn tsign(ctx: &Ctx, user_id: &str, level: u8, trust: u8) -> Result<()> { - let homedir = - String::from(ctx.directory("homedir").unwrap().to_str().unwrap()); - - let cmd = format!("gpg --homedir {} --edit-key {}", homedir, user_id); - - let mut p = rexpect::spawn(&cmd, Some(10_000)).unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("tsign").unwrap(); - p.exp_string("Your selection?").unwrap(); - p.send_line(&format!("{}", trust)).unwrap(); - p.exp_string("Your selection?").unwrap(); - p.send_line(&format!("{}", level)).unwrap(); - p.exp_string("Your selection?").unwrap(); - p.send_line("").unwrap(); // domain - p.exp_string("Really sign? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_string("gpg>").unwrap(); - p.send_line("quit").unwrap(); - p.exp_string("Save changes? (y/N)").unwrap(); - p.send_line("y").unwrap(); - p.exp_eof().unwrap(); - - Ok(()) -} diff --git a/openpgp-ca-restd/tests/test_restd.rs b/openpgp-ca-restd/tests/test_restd.rs index b2d456b..0290ec5 100644 --- a/openpgp-ca-restd/tests/test_restd.rs +++ b/openpgp-ca-restd/tests/test_restd.rs @@ -12,12 +12,11 @@ use openpgp_ca_restd::json::{ }; use openpgp_ca_restd::restd; +use gnupg_test_wrapper as gnupg; use openpgp_ca_lib::ca::OpenpgpCa; use rocket::futures::prelude::future::{AbortHandle, Abortable}; -pub mod gnupg; - const ALICE_CERT: &str = r#"-----BEGIN PGP PUBLIC KEY BLOCK----- mDMEX419BRYJKwYBBAHaRw8BAQdAnfJuV3EHFAJ31D968YvLlAAu0YqUxySSJ1Lh |