#![allow(unused)] use std::borrow::Borrow; use std::ffi::OsStr; use std::ffi::OsString; use std::fs::File; use std::path::Path; use std::path::PathBuf; use std::process::Output; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use anyhow::anyhow; use anyhow::Context; use assert_cmd::Command; use chrono::DateTime; use chrono::Duration; use chrono::Utc; use openpgp::packet::Signature; use openpgp::parse::Parse; use openpgp::policy::NullPolicy; use openpgp::policy::StandardPolicy; use openpgp::Cert; use openpgp::cert::CertParser; use openpgp::Fingerprint; use openpgp::KeyHandle; use openpgp::Result; use openpgp::serialize::Serialize; use sequoia_openpgp as openpgp; use tempfile::TempDir; pub const STANDARD_POLICY: &StandardPolicy = &StandardPolicy::new(); pub const NULL_POLICY: &NullPolicy = &NullPolicy::new(); pub fn artifact(filename: &str) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests").join("data").join(filename) } // Returns the power set excluding the empty set. pub fn power_set(set: &[T]) -> Vec> { let mut power_set: Vec> = Vec::new(); for element in set.iter() { power_set.extend( power_set.clone().into_iter().map(|mut v: Vec| { v.push(element.clone()); v })); power_set.push(vec![ element.clone() ]); } power_set } /// Returns the time formatted as an ISO 8106 string. pub fn time_as_string(t: DateTime) -> String { t.format("%Y-%m-%dT%H:%M:%SZ").to_string() } /// Designates a certificate by path, or by key handle. #[derive(Clone, Debug)] pub enum FileOrKeyHandle { FileOrStdin(PathBuf), KeyHandle((KeyHandle, OsString)), } impl From<&str> for FileOrKeyHandle { fn from(path: &str) -> Self { PathBuf::from(path).into() } } impl From for FileOrKeyHandle { fn from(path: String) -> Self { PathBuf::from(path).into() } } impl From<&Path> for FileOrKeyHandle { fn from(path: &Path) -> Self { path.to_path_buf().into() } } impl From<&PathBuf> for FileOrKeyHandle { fn from(path: &PathBuf) -> Self { path.clone().into() } } impl From for FileOrKeyHandle { fn from(path: PathBuf) -> Self { FileOrKeyHandle::FileOrStdin(path.into()) } } impl From<&KeyHandle> for FileOrKeyHandle { fn from(kh: &KeyHandle) -> Self { FileOrKeyHandle::KeyHandle((kh.clone(), kh.to_string().into())) } } impl From for FileOrKeyHandle { fn from(kh: KeyHandle) -> Self { let s = kh.to_string().into(); FileOrKeyHandle::KeyHandle((kh, s)) } } impl From<&Fingerprint> for FileOrKeyHandle { fn from(fpr: &Fingerprint) -> Self { KeyHandle::from(fpr).into() } } impl From for FileOrKeyHandle { fn from(fpr: Fingerprint) -> Self { KeyHandle::from(fpr).into() } } impl From<&FileOrKeyHandle> for FileOrKeyHandle { fn from(h: &FileOrKeyHandle) -> Self { h.clone() } } impl AsRef for FileOrKeyHandle { fn as_ref(&self) -> &OsStr { match self { FileOrKeyHandle::FileOrStdin(file) => file.as_os_str(), FileOrKeyHandle::KeyHandle((kh, s)) => s.as_os_str(), } } } impl FileOrKeyHandle { /// Returns whether this contains a `FileOrStdin`. pub fn is_file(&self) -> bool { match self { FileOrKeyHandle::FileOrStdin(_) => true, FileOrKeyHandle::KeyHandle(_) => false, } } /// Returns whether this contains a `KeyHandle`. pub fn is_key_handle(&self) -> bool { match self { FileOrKeyHandle::FileOrStdin(_) => false, FileOrKeyHandle::KeyHandle(_) => true, } } } pub struct Sq { base: TempDir, // Whether to preserve the directory on exit. Normally we clean // it up, but preserving it can simplify debugging when a test // fails. preserve: bool, home: PathBuf, /// The working directory to invoke commands in. working_dir: PathBuf, policy: PathBuf, certd: PathBuf, now: std::time::SystemTime, scratch: AtomicUsize, } impl Drop for Sq { fn drop(&mut self) { if self.preserve { self.preserve = false; match TempDir::new() { Ok(tmp) => { let base = std::mem::replace(&mut self.base, tmp); let path = base.into_path(); eprintln!("Preserving state in {}", path.display()); } Err(err) => { eprintln!("Error preserving state in {}: {}", self.base.path().display(), err); } } } } } impl Sq { /// Creates a new Sq context in a new, emphemeral home directory. /// The clock is set to the specified time. pub fn at(now: std::time::SystemTime) -> Self { let base = TempDir::new() .expect("can create a temporary directory"); let home = base.path().join("home"); let working_dir = base.path().join("working-dir"); std::fs::create_dir_all(&working_dir).unwrap(); // Create an empty policy configuration file. We use this // instead of the system-wide policy configuration file, which // might be more strict than what our test vectors expect. let policy = base.path().join("empty-policy.toml"); std::fs::write(&policy, "").unwrap(); let certd = home.join("data").join("pgp.cert.d"); Sq { base, preserve: false, home, working_dir, policy, certd, now, scratch: 0.into(), } } /// Creates a new Sq context in a new, emphemeral home directory. /// The clock is set to the current time. pub fn new() -> Self { // The current time. let mut now = std::time::SystemTime::now(); let since_epoch = now.duration_since(std::time::UNIX_EPOCH).unwrap(); now = now - std::time::Duration::new(0, since_epoch.subsec_nanos()); Self::at(now) } /// Preserves the files on exit. /// /// Normally we clean delete all files and directories. This /// suppresses that behavior. Preserving the state can facilitate /// debugging. Normally, you'll only enable this to debug a test, /// and then disable it again. pub fn preserve(&mut self) { self.preserve = true; } /// Returns the base directory. /// /// The sequoia home directory is under the `home` subdirectory. /// The rest can be used as scratch space. pub fn base(&self) -> &Path { self.base.path() } /// Returns the home directory. pub fn home(&self) -> &Path { &self.home } /// Returns the working directory. pub fn working_dir(&self) -> &Path { &self.working_dir } /// Returns the path to the cert.d. pub fn certd(&self) -> &Path { &self.certd } /// Returns the path to the test data. pub fn test_data(&self) -> PathBuf { PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests").join("data") } /// Returns the scratch directory. pub fn scratch_dir(&self) -> PathBuf { let dir = self.home.join("scratch"); std::fs::create_dir_all(&dir) .expect("can create scratch directory"); dir } /// Returns a new scratch file. /// /// The file is guaranteed to not exist, but it isn't actually /// created. pub fn scratch_file<'a, S>(&self, name: S) -> PathBuf where S: Into> { let name = name.into(); let name_; let name = if let Some(name) = name { name_ = name.chars() .map(|c| { if c.is_ascii_alphanumeric() { c } else { '-' } }) .collect::(); &name_ } else { name.unwrap_or("scratch-file") }; let dir = self.scratch_dir(); loop { let i = self.scratch.fetch_add(1, Ordering::Relaxed); let file = dir.join(format!("{}-{}", i, name)); if ! file.exists() { return file; } } } /// Returns the current time. pub fn now(&self) -> std::time::SystemTime { self.now } /// Returns the current time formatted as an ISO 8106 string. pub fn now_as_string(&self) -> String { time_as_string(self.now.into()) } /// Advances the clock by `sec` number of seconds. pub fn tick(&mut self, secs: u64) { self.now += std::time::Duration::new(secs, 0); } /// Returns a command that is set to run `sq`. The home directory /// and time are already set. pub fn command(&self) -> Command { let mut cmd = Command::cargo_bin("sq") .expect("can run sq"); cmd.current_dir(&self.working_dir); cmd.env("SEQUOIA_CRYPTO_POLICY", &self.policy); cmd.arg("--batch"); cmd.arg("--home").arg(self.home()); cmd.arg("--time").arg(&self.now_as_string()); cmd } /// Runs the command. If `expect` is `Some`, asserts that the /// command succeeds or fails as per the boolean. pub fn run(&self, mut cmd: Command, expect: E) -> Output where E: Into> { eprintln!("Running: {:?}", cmd); let output = cmd.output().expect("can run command"); let expect = expect.into(); match (output.status.success(), expect) { (true, Some(true)) | (false, Some(false)) | (_, None) => { eprintln!("Exit status:"); let dump = |id, stream| { let limit = 4096; let data = String::from_utf8_lossy(stream) .chars() .collect::>(); if data.is_empty() { eprintln!("{}: empty", id); } else { eprintln!("{}: {}{}", id, data.iter().take(limit).collect::(), if data.len() > limit { format!("... {} more bytes", data.len() - limit) } else { "".to_string() }); } }; dump("stdout", &output.stdout); dump("stderr", &output.stderr); } (got, expected) => { let expected = expect.unwrap(); panic!( "Running {:?}: {}, but should have {}:\n\ stdout: {}\n\ stderr: {}", cmd, if got { "succeeded" } else { "failed" }, if expected { "succeeded" } else { "failed" }, &String::from_utf8_lossy(&output.stdout), &String::from_utf8_lossy(&output.stderr)); } } output } // A helper function that handles a command's output when the // output is a certificate. // // If the command fails, returns an error. // // If `output_file` is `Some`, then reads the key or certificate // from stdin or the specified file, and returns it. Otherwise, // exports the key or certificate, and returns it. // // `is_key` is whether the returned certificate is expected to // include secret key material. If `is_key` is `None`, then it // may or may not (e.g., `sq key subkey delete`, which may delete // the last bit of secret key material, or not). fn handle_cert_output<'a, O, K>(&self, output: Output, cert: FileOrKeyHandle, output_file: O, is_key: K) -> Result where O: Into>, K: Into> { let output_file = output_file.into(); let is_key = is_key.into(); if output.status.success() { let cert = match (output_file, cert) { (Some(output_file), _) => { // An output file was specified. if output_file == &PathBuf::from("-") { Cert::from_bytes(&output.stdout) .with_context(|| { format!("Importing result from stdout") }) .expect("can parse certificate") } else { Cert::from_file(output_file) .with_context(|| { format!("Importing result from the file {}", output_file.display()) }) .expect("can parse certificate") } } (None, FileOrKeyHandle::KeyHandle((kh, _s))) => { // No output file was specified, input was read // from the certificate store => the updated // certificate is written to the certificate // store. match is_key { None => { // Try the key store, and then fallback to // the cert store. self.key_export_maybe(kh.clone()) .unwrap_or_else(|_| self.cert_export(kh)) } Some(true) => self.key_export(kh), Some(false) => self.cert_export(kh), } } (None, FileOrKeyHandle::FileOrStdin(_path)) => { // No output file was specified, input was read // from a file => the certificate is written to // stdout. Cert::from_bytes(&output.stdout) .with_context(|| { format!("Importing result from stdout") }) .expect("can parse certificate") } }; if let Some(is_key) = is_key { assert_eq!(cert.is_tsk(), is_key); } Ok(cert) } else { Err(anyhow::anyhow!(format!( "Failed (expected):\n{}", String::from_utf8_lossy(&output.stderr)))) } } /// Decrypts a message. pub fn decrypt(&self, args: &[&str], msg: M) -> Vec where M: AsRef<[u8]>, { self.decrypt_maybe(args, msg).expect("can decrypt") } /// Decrypts a message. pub fn decrypt_maybe(&self, args: &[&str], msg: M) -> Result> where M: AsRef<[u8]>, { let mut cmd = self.command(); cmd.args([ "decrypt" ]); for arg in args { cmd.arg(arg); } cmd.write_stdin(msg.as_ref()); let output = self.run(cmd, None); if output.status.success() { Ok(output.stdout.to_vec()) } else { Err(anyhow::anyhow!("sq decrypt returned an error")) } } /// Encrypts a message. pub fn encrypt(&self, args: &[A], msg: M) -> Vec where A: AsRef, M: AsRef<[u8]>, { self.encrypt_maybe(args, msg).expect("can encrypt") } /// Encrypts a message. pub fn encrypt_maybe(&self, args: &[A], msg: M) -> Result> where A: AsRef, M: AsRef<[u8]>, { let mut cmd = self.command(); cmd.args([ "encrypt" ]); for arg in args { cmd.arg(arg.as_ref()); } cmd.write_stdin(msg.as_ref()); let output = self.run(cmd, None); if output.status.success() { Ok(output.stdout.to_vec()) } else { Err(anyhow::anyhow!("sq encrypt returned an error")) } } /// Generates a new key. /// /// The certificate is not imported into the cert store or key /// store, but saved in a file. /// /// Returns the certificate, the certificate's filename, and the /// revocation certificate's filename. pub fn key_generate(&self, extra_args: &[&str], userids: &[&str]) -> (Cert, PathBuf, PathBuf) { let mut cmd = self.command(); cmd.args([ "key", "generate" ]); if ! extra_args.contains(&"--new-password-file") { cmd.arg("--without-password"); } for arg in extra_args { cmd.arg(arg); } let any_userids = ! userids.is_empty() || extra_args.iter().any(|a| a.starts_with("--name") || a.starts_with("--email")); if ! any_userids { cmd.arg("--no-userids"); } else { for userid in userids { cmd.arg("--userid").arg(userid); } } let cert_filename = self.scratch_file( userids.get(0).map(|u| format!("{}-cert", u)).as_deref()); cmd.arg("--output").arg(&cert_filename); let rev_filename = self.scratch_file( userids.get(0).map(|u| format!("{}-rev", u)).as_deref()); cmd.arg("--rev-cert").arg(&rev_filename); let output = self.run(cmd, Some(true)); let cert = Cert::from_file(&cert_filename) .expect("can parse certificate"); assert!(cert.is_tsk()); (cert, cert_filename, rev_filename) } /// Run `sq inspect` and return stdout. pub fn inspect(&self, handle: H) -> String where H: Into { let mut cmd = self.command(); cmd.arg("inspect"); match handle.into() { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg(path); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); } }; let output = self.run(cmd, Some(true)); String::from_utf8_lossy(&output.stdout).to_string() } /// Delete the specified key. pub fn key_delete<'a, H, Q>(&self, cert_handle: H, output_file: Q) -> Cert where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("delete"); match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--file").arg(path); assert!(output_file.is_some()); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); } }; if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(true)); self.handle_cert_output(output, cert_handle, output_file, None) .expect("can parse certificate") } /// Run `sq key revoked` and return the revocation certificate. pub fn key_revoke<'a, H, I, Q>(&self, cert_handle: H, revoker_handle: I, reason: &str, message: &str, revocation_time: Option>, notations: &[(&str, &str)], output_file: Q) -> Cert where H: Into, I: Into>, Q: Into>, { let cert_handle = cert_handle.into(); let revoker_handle = revoker_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("revoke") .arg("--reason").arg(reason) .arg("--message").arg(message); match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--file").arg(path); assert!(output_file.is_some()); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); } }; match revoker_handle.as_ref() { Some(FileOrKeyHandle::FileOrStdin(path)) => { cmd.arg("--revoker-file").arg(path); } Some(FileOrKeyHandle::KeyHandle((_kh, s))) => { cmd.arg("--revoker").arg(&s); } None => (), }; if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } for (k, v) in notations { cmd.args(["--notation", k, v]); } if let Some(time) = revocation_time { cmd.args([ "--time", &time.format("%Y-%m-%dT%H:%M:%SZ").to_string(), ]); } let output = self.run(cmd, Some(true)); assert!(output.status.success()); self.handle_cert_output(output, cert_handle, output_file, false) .expect("can parse certificate") } /// Imports the specified key into the keystore. pub fn key_import

(&self, path: P) where P: AsRef { let mut cmd = self.command(); cmd.arg("key").arg("import").arg(path.as_ref()); self.run(cmd, Some(true)); } /// Exports the specified key. pub fn key_export(&self, kh: KeyHandle) -> Cert { self.key_export_maybe(kh) .expect("can export key") } /// Exports the specified key from the key store. /// /// Returns an error if `sq key export` fails. This happens if /// the certificate is known, but the key store doesn't manage any /// of its secret key material. pub fn key_export_maybe(&self, kh: KeyHandle) -> Result { let mut cmd = self.command(); cmd.args([ "key", "export", "--cert", &kh.to_string() ]); let output = self.run(cmd, None); if output.status.success() { Ok(Cert::from_bytes(&output.stdout).expect("can parse certificate")) } else { Err(anyhow::anyhow!("sq key export returned an error")) } } /// Change the key's password. pub fn key_password<'a, H, Q>(&self, cert_handle: H, old_password_file: Option<&'a Path>, new_password_file: Option<&'a Path>, output_file: Q, success: bool) -> Result where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("password"); if cert_handle.is_file() { cmd.arg("--file").arg(&cert_handle); assert!(output_file.is_some()); } else { cmd.arg("--cert").arg(&cert_handle); }; if let Some(p) = old_password_file { cmd.arg("--password-file").arg(p); } if let Some(p) = new_password_file { cmd.arg("--new-password-file").arg(p); } else { cmd.arg("--clear-password"); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert_handle, output_file, true) } /// Calls `sq key bind`. /// /// `keyrings` are a list of files to pass to `--keyring`. They /// usually contain the key to bind. /// /// `target` is the certificate that will bind the key. /// /// `keys` is the set of keys to bind. /// /// The resulting certificate is NOT imported into the key store /// or the cert store. pub fn key_subkey_bind_maybe(&self, extra_args: &[&str], keyrings: Vec

, target: T, keys: Vec, output_file: Q) -> Result where P: AsRef, T: Into, K: Into, Q: AsRef, { let target = target.into(); let output_file = output_file.as_ref(); let mut cmd = self.command(); cmd.arg("key").arg("subkey").arg("bind"); for arg in extra_args { cmd.arg(arg); } for k in keyrings.into_iter() { cmd.arg("--keyring").arg(k.as_ref()); } if target.is_file() { cmd.arg("--file").arg(&target); } else { cmd.arg("--cert").arg(&target); }; assert!(! keys.is_empty()); for k in keys.into_iter() { let k: KeyHandle = k.into(); cmd.arg("--key").arg(k.to_string()); } cmd.arg("--output").arg(&output_file); let output = self.run(cmd, None); self.handle_cert_output(output, target, output_file, None) } /// Calls `sq key bind`. /// /// `keyrings` are a list of files to pass to `--keyring`. They /// usually contain the key to bind. /// /// `target` is the certificate that will bind the key. /// /// `keys` is the set of keys to bind. /// /// The resulting certificate is NOT imported into the key store /// or the cert store. /// /// This version panics if `sq key bind` fails. pub fn key_subkey_bind(&self, extra_args: &[&str], keyrings: Vec

, target: T, keys: Vec, output_file: Q) -> Cert where P: AsRef, T: Into, K: Into, Q: AsRef, { self.key_subkey_bind_maybe( extra_args, keyrings, target, keys, output_file) .expect("sq key subkey bind succeeds") } pub fn key_approvals_update<'a, H, Q>(&self, cert: H, args: &[&str], output_file: Q) -> Cert where H: Into, Q: Into>, { let cert = cert.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("approvals").arg("update"); match &cert { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--cert-file").arg(file); assert!(output_file.is_some()); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(s); } } cmd.args(args); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(true)); self.handle_cert_output(output, cert, output_file, None) .expect("can parse certificate") } /// Change the certificate's expiration. pub fn key_expire<'a, H, Q>(&self, cert_handle: H, expire: &str, password_file: Option<&'a Path>, output_file: Q, success: bool) -> Result where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("expire") .arg("--expiration").arg(expire); if cert_handle.is_file() { cmd.arg("--file").arg(&cert_handle); assert!(output_file.is_some()); } else { cmd.arg("--cert").arg(&cert_handle); }; if let Some(p) = password_file { cmd.arg("--password-file").arg(p); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert_handle, output_file, None) } /// Exports the specified keys. pub fn key_subkey_export(&self, khs: Vec) -> Vec where H: Into { self.key_subkey_export_maybe(khs) .expect("can export key") } /// Exports the specified keys from the key store. /// /// Returns an error if `sq key subkey export` fails. This /// happens if the key is known, but the key store doesn't manage /// any of its secret key material. pub fn key_subkey_export_maybe(&self, khs: Vec) -> Result> where H: Into { let mut cmd = self.command(); cmd.args([ "key", "subkey", "export" ]); for kh in khs.into_iter() { let kh: KeyHandle = kh.into(); cmd.arg("--key").arg(kh.to_string()); } let output = self.run(cmd, None); if output.status.success() { let parser = CertParser::from_bytes(&output.stdout) .expect("can parse certificate"); Ok(parser.collect::>>()?) } else { Err(anyhow::anyhow!("sq key export returned an error")) } } /// Run `sq key subkey revoke` and return the revocation certificate. pub fn key_subkey_revoke<'a, H, I, Q>(&self, cert_handle: H, key_handles: &[KeyHandle], revoker_handle: I, reason: &str, message: &str, revocation_time: Option>, notations: &[(&str, &str)], output_file: Q) -> Cert where H: Into, I: Into>, Q: Into>, { let cert_handle = cert_handle.into(); let revoker_handle = revoker_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("subkey").arg("revoke") .arg("--reason").arg(reason) .arg("--message").arg(message); for key in key_handles { cmd.arg("--key").arg(key.to_string()); } match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--file").arg(path); assert!(output_file.is_some()); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); } }; match revoker_handle.as_ref() { Some(FileOrKeyHandle::FileOrStdin(path)) => { cmd.arg("--revoker-file").arg(path); } Some(FileOrKeyHandle::KeyHandle((_kh, s))) => { cmd.arg("--revoker").arg(&s); } None => (), }; if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } for (k, v) in notations { cmd.args(["--notation", k, v]); } if let Some(time) = revocation_time { cmd.args([ "--time", &time.format("%Y-%m-%dT%H:%M:%SZ").to_string(), ]); } let output = self.run(cmd, Some(true)); self.handle_cert_output(output, cert_handle, output_file, false) .expect("can parse certificate") } /// Delete the specified key. pub fn key_subkey_delete<'a, H, Q>(&self, cert_handle: H, key_handles: &[KeyHandle], output_file: Q) -> Cert where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("subkey").arg("delete"); match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--file").arg(path); assert!(output_file.is_some()); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); } }; for kh in key_handles { cmd.arg("--key").arg(kh.to_string()); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(true)); assert!(output.status.success()); self.handle_cert_output(output, cert_handle, output_file, None) .expect("can parse certificate") } /// Change the key's password. pub fn key_subkey_password<'a, H, Q>(&self, cert_handle: H, keys: &[KeyHandle], old_password_file: Option<&'a Path>, new_password_file: Option<&'a Path>, output_file: Q, success: bool) -> Result where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("subkey").arg("password"); if cert_handle.is_file() { cmd.arg("--file").arg(&cert_handle); assert!(output_file.is_some()); } else { cmd.arg("--cert").arg(&cert_handle); }; for key in keys.iter() { cmd.arg("--key").arg(key.to_string()); } if let Some(p) = old_password_file { cmd.arg("--password-file").arg(p); } if let Some(p) = new_password_file { cmd.arg("--new-password-file").arg(p); } else { cmd.arg("--clear-password"); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert_handle, output_file, None) } /// Change the key's expiration. pub fn key_subkey_expire<'a, H, Q>(&self, cert_handle: H, keys: &[KeyHandle], expire: &str, password_file: Option<&'a Path>, output_file: Q, success: bool) -> Result where H: Into, Q: Into>, { let cert_handle = cert_handle.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("key").arg("subkey").arg("expire") .arg("--expiration").arg(expire); if cert_handle.is_file() { cmd.arg("--file").arg(&cert_handle); assert!(output_file.is_some()); } else { cmd.arg("--cert").arg(&cert_handle); }; for key in keys.iter() { cmd.arg("--key").arg(key.to_string()); } if let Some(p) = password_file { cmd.arg("--password-file").arg(p); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert_handle, output_file, None) } /// Adds user IDs to the given key. pub fn key_userid_add(&self, key: Cert, args: &[&str]) -> Result { let mut cmd = self.command(); cmd.args(["key", "userid", "add"]); for arg in args { cmd.arg(arg); } let in_filename = self.scratch_file(None); key.as_tsk().serialize(&mut File::create(&in_filename)?)?; cmd.arg("--cert-file").arg(&in_filename); let out_filename = self.scratch_file(None); cmd.arg("--output").arg(&out_filename); let output = self.run(cmd, Some(true)); let out_key = Cert::from_file(&out_filename)?; assert!(out_key.is_tsk()); Ok(out_key) } /// Revokes a user ID. pub fn key_userid_revoke_maybe<'a, C, O>(&self, args: &[&str], cert: C, userid: &str, reason: &str, message: &str, output_file: O) -> Result where C: Into, O: Into>, { let cert = cert.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "key", "userid", "revoke", "--reason", reason, "--message", message, ]); for arg in args { cmd.arg(arg); } match &cert { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--cert-file").arg(file); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(s); } } cmd.arg("--userid").arg(userid); if let Some(output_file) = output_file { cmd.arg("--overwrite").arg("--output").arg(output_file); } let output = self.run(cmd, None); self.handle_cert_output(output, cert, output_file, false) } pub fn key_userid_revoke<'a, C, O>(&self, args: &[&str], cert: C, userid: &str, reason: &str, message: &str, output_file: O) -> Cert where C: Into, O: Into>, { self.key_userid_revoke_maybe(args, cert, userid, reason, message, output_file) .expect("succeeds") } /// Strips user IDs to the given key. pub fn toolbox_strip_userid(&self, key: Cert, args: &[&str]) -> Result { let mut cmd = self.command(); cmd.args(["toolbox", "strip-userid"]); for arg in args { cmd.arg(arg); } let in_filename = self.scratch_file(None); key.as_tsk().serialize(&mut File::create(&in_filename)?)?; cmd.arg("--cert-file").arg(&in_filename); let out_filename = self.scratch_file(None); cmd.arg("--output").arg(&out_filename); let output = self.run(cmd, Some(true)); let out_key = Cert::from_file(&out_filename)?; assert!(out_key.is_tsk()); Ok(out_key) } /// Imports the specified certificate into the keystore. pub fn cert_import_maybe

(&self, path: P) -> Result<()> where P: AsRef { let mut cmd = self.command(); cmd.arg("cert").arg("import").arg(path.as_ref()); let output = self.run(cmd, None); if output.status.success() { Ok(()) } else { Err(anyhow::anyhow!("sq cert import returned an error")) } } /// Imports the specified certificate into the keystore. pub fn cert_import

(&self, path: P) where P: AsRef { self.cert_import_maybe(path) .expect("succeeds") } /// Exports the specified certificate. pub fn cert_export(&self, kh: H) -> Cert where H: Borrow, { let mut cmd = self.command(); cmd.args([ "cert", "export", "--cert", &kh.borrow().to_string() ]); let output = self.run(cmd, Some(true)); Cert::from_bytes(&output.stdout) .expect("can parse certificate") } /// Try to certify the user ID binding. /// /// If `output_file` is `Some`, then the output is written to that /// file. Otherwise, the default behavior is followed. pub fn pki_vouch_certify_p<'a, H, C, Q>(&self, extra_args: &[&str], certifier: H, cert: C, userids: &[&str], output_file: Q, success: bool) -> Result where H: Into, C: Into, Q: Into>, { let certifier = certifier.into(); let cert = cert.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "pki", "vouch", "certify" ]); for arg in extra_args { cmd.arg(arg); } match &certifier { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--certifier-file").arg(file); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--certifier").arg(s); } } match &cert { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--cert-file").arg(file); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(s); } } for userid in userids { cmd.arg("--userid").arg(userid); } if let Some(output_file) = output_file { cmd.arg("--overwrite").arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert, output_file, false) } /// Certify the user ID binding. pub fn pki_vouch_certify<'a, H, C, Q>(&self, extra_args: &[&str], certifier: H, cert: C, userids: &[&str], output_file: Q) -> Cert where H: Into, C: Into, Q: Into>, { self.pki_vouch_certify_p( extra_args, certifier, cert, userids, output_file, true) .expect("success") } /// Try to make an authorization. /// /// If `output_file` is `Some`, then the output is written to that /// file. Otherwise, the default behavior is followed. pub fn pki_vouch_authorize_p<'a, H, C, Q>(&self, extra_args: &[&str], certifier: H, cert: C, userids: &[&str], output_file: Q, success: bool) -> Result where H: Into, C: Into, Q: Into>, { let certifier = certifier.into(); let cert = cert.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "pki", "vouch", "authorize" ]); for arg in extra_args { cmd.arg(arg); } match &certifier { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--certifier-file").arg(file); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--certifier").arg(s); } } match &cert { FileOrKeyHandle::FileOrStdin(file) => { cmd.arg("--cert-file").arg(file); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(s); } } for userid in userids { cmd.arg("--userid").arg(userid); } if let Some(output_file) = output_file { cmd.arg("--overwrite").arg("--output").arg(output_file); } let output = self.run(cmd, Some(success)); self.handle_cert_output(output, cert, output_file, false) } /// Authorize a certificate. pub fn pki_vouch_authorize<'a, H, C, Q>(&self, extra_args: &[&str], certifier: H, cert: C, userids: &[&str], output_file: Q) -> Cert where H: Into, C: Into, Q: Into>, { self.pki_vouch_authorize_p( extra_args, certifier, cert, userids, output_file, true) .expect("success") } /// Add a link for the binding. pub fn pki_link_add_maybe(&self, extra_args: &[&str], cert: KeyHandle, userids: &[&str]) -> Result<()> { let mut cmd = self.command(); cmd.args([ "pki", "link", "add" ]); for arg in extra_args { cmd.arg(arg); } cmd.arg("--cert").arg(cert.to_string()); for userid in userids { cmd.arg("--userid").arg(userid); } let output = self.run(cmd, None); if output.status.success() { Ok(()) } else { Err(anyhow::anyhow!(format!( "Command failed:\n{}", String::from_utf8_lossy(&output.stderr)))) } } /// Add a link for the binding. pub fn pki_link_add(&self, args: &[&str], cert: KeyHandle, userids: &[&str]) { self.pki_link_add_maybe(args, cert, userids).expect("success") } /// Add a link for the binding. pub fn pki_link_retract_maybe(&self, extra_args: &[&str], cert: KeyHandle, userids: &[&str]) -> Result<()> { let mut cmd = self.command(); cmd.args([ "pki", "link", "retract" ]); for arg in extra_args { cmd.arg(arg); } cmd.arg("--cert").arg(cert.to_string()); for userid in userids { cmd.arg("--userid").arg(userid); } let output = self.run(cmd, None); if output.status.success() { Ok(()) } else { Err(anyhow::anyhow!(format!( "Command failed:\n{}", String::from_utf8_lossy(&output.stderr)))) } } /// Add a link for the binding. pub fn pki_link_retract(&self, args: &[&str], cert: KeyHandle, userids: &[&str]) { self.pki_link_retract_maybe(args, cert, userids) .expect("success") } /// Add a link for the binding. pub fn pki_link_authorize_maybe(&self, extra_args: &[&str], cert: KeyHandle, userids: &[&str]) -> Result<()> { let mut cmd = self.command(); cmd.args([ "pki", "link", "authorize" ]); for arg in extra_args { cmd.arg(arg); } cmd.arg("--cert").arg(cert.to_string()); for userid in userids { cmd.arg("--userid").arg(userid); } let output = self.run(cmd, None); if output.status.success() { Ok(()) } else { Err(anyhow::anyhow!(format!( "Command failed:\n{}", String::from_utf8_lossy(&output.stderr)))) } } /// Add a link for the binding. pub fn pki_link_authorize(&self, args: &[&str], cert: KeyHandle, userids: &[&str]) { self.pki_link_authorize_maybe(args, cert, userids) .expect("success") } /// Authenticate a binding. pub fn pki_authenticate(&self, extra_args: &[&str], cert: &str, userid: &str) -> Result<()> { let mut cmd = self.command(); cmd.args([ "pki", "authenticate" ]); for arg in extra_args { cmd.arg(arg); } cmd.arg(cert); cmd.arg(userid); let output = self.run(cmd, None); if output.status.success() { Ok(()) } else { Err(anyhow::anyhow!(format!( "Command failed:\n{}", String::from_utf8_lossy(&output.stderr)))) } } pub fn sign<'a, H, Q>(&self, signer: H, password_file: Option<&Path>, input_file: &Path, output_file: Q) -> Vec where H: Into, Q: Into>, { let signer = signer.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("sign"); match &signer { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--signer-file").arg(path); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--signer").arg(&s); } }; if let Some(password_file) = password_file { cmd.arg("--password-file").arg(password_file); } cmd.arg(input_file); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); }; let output = self.run(cmd, Some(true)); assert!(output.status.success()); if let Some(output_file) = output_file { std::fs::read(output_file).expect("can read file") } else { output.stdout } } pub fn sign_detached<'a, H, O>(&self, args: &[&str], signer: H, input_file: &Path, output_file: O) -> Vec where H: Into, O: Into>, { let signer = signer.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("sign").arg("--detached"); for arg in args { cmd.arg(arg); } match &signer { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--signer-file").arg(path); } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--signer").arg(&s); } }; cmd.arg(input_file); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); }; let output = self.run(cmd, Some(true)); assert!(output.status.success()); if let Some(output_file) = output_file { std::fs::read(output_file).expect("can read file") } else { output.stdout } } // Strips the secret key material from input. Writes it to // `output_file`, if `Some`. pub fn toolbox_extract_cert<'a, P, Q>(&self, input: P, output_file: Q) -> Cert where P: AsRef, Q: Into>, { let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "toolbox", "extract-cert" ]); cmd.arg(input.as_ref()); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(true)); if let Some(output_file) = output_file { if output_file != &PathBuf::from("-") { return Cert::from_file(&output_file) .expect("can parse certificate"); } } // Read from stdout. Cert::from_bytes(&output.stdout) .expect("can parse certificate") } // Merges the certificates. pub fn toolbox_keyring_merge<'a, P, Q>(&self, input_files: Vec

, input_bytes: Option<&[u8]>, output_file: Q) -> Vec where P: AsRef, Q: Into>, { let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "toolbox", "keyring", "merge" ]); for input_file in input_files.into_iter() { cmd.arg(input_file.as_ref()); } if let Some(input_bytes) = input_bytes { cmd.arg("-"); cmd.write_stdin(input_bytes); } if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, Some(true)); let parser = None; if let Some(output_file) = output_file { if PathBuf::from("-").as_path() != output_file { CertParser::from_file(&output_file) .expect("can parse certificate"); } }; let parser = if let Some(parser) = parser { parser } else { // Read from stdout. CertParser::from_bytes(&output.stdout) .expect("can parse certificate") }; parser.collect::>>() .expect("valid certificates") } } /// Ensure notations can be found in a Signature /// /// ## Errors /// /// Returns an error if a notation can not be found in the Signature pub fn compare_notations( signature: &Signature, notations: &[(&str, &str)], ) -> Result<()> { if ! notations.is_empty() { let found_notations: Vec<(&str, String)> = signature .notation_data() .map(|n| (n.name(), String::from_utf8_lossy(n.value()).into())) .collect(); for (key, value) in notations { if !found_notations.contains(&(key, String::from(*value))) { return Err(anyhow!(format!( "Expected notation \"{}: {}\" in {:?}", key, value, found_notations ))); } } } Ok(()) }