#![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::TimeZone; 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) } pub fn artifact_s(filename: &str) -> String { artifact(filename).display().to_string() } // 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, } } } /// Designates data by path, or bytes (which are fed to stdin). #[derive(Clone, Debug)] pub enum FileOrBytes { FileOrStdin(PathBuf), Bytes(Vec), } impl From<&[u8]> for FileOrBytes { fn from(s: &[u8]) -> Self { FileOrBytes::Bytes(s.to_vec()) } } impl From> for FileOrBytes { fn from(s: Vec) -> Self { FileOrBytes::Bytes(s) } } impl From<&Vec> for FileOrBytes { fn from(s: &Vec) -> Self { FileOrBytes::Bytes(s.clone()) } } impl From<&Path> for FileOrBytes { fn from(path: &Path) -> Self { path.to_path_buf().into() } } impl From<&PathBuf> for FileOrBytes { fn from(path: &PathBuf) -> Self { path.clone().into() } } impl From for FileOrBytes { fn from(path: PathBuf) -> Self { FileOrBytes::FileOrStdin(path.into()) } } impl From<&FileOrBytes> for FileOrBytes { fn from(h: &FileOrBytes) -> Self { h.clone() } } impl FileOrBytes { /// Returns whether this contains a `FileOrStdin`. pub fn is_file(&self) -> bool { match self { FileOrBytes::FileOrStdin(_) => true, FileOrBytes::Bytes(_) => false, } } /// Returns whether this contains bytes. pub fn is_bytes(&self) -> bool { match self { FileOrBytes::FileOrStdin(_) => false, FileOrBytes::Bytes(_) => true, } } } /// An enum for user ID arguments. #[derive(Debug, Clone)] pub enum UserIDArg<'a> { UserID(&'a str), Email(&'a str), Name(&'a str), AddUserID(&'a str), AddEmail(&'a str), } impl<'a> From<&'a str> for UserIDArg<'a> { fn from(userid: &'a str) -> Self { UserIDArg::UserID(userid) } } impl<'a> From<&'a &'a str> for UserIDArg<'a> { fn from(userid: &'a &'a str) -> Self { UserIDArg::UserID(userid) } } impl<'a> From<&'a String> for UserIDArg<'a> { fn from(userid: &'a String) -> Self { UserIDArg::UserID(&userid) } } impl UserIDArg<'_> { /// Return the raw string. pub fn raw(&self) -> &str { match self { UserIDArg::UserID(s) | UserIDArg::Email(s) | UserIDArg::Name(s) | UserIDArg::AddUserID(s) | UserIDArg::AddEmail(s) => { s } } } /// Add the argument to a `Command`. pub fn as_arg(&self, cmd: &mut Command) { match self { UserIDArg::UserID(userid) => cmd.arg("--userid").arg(userid), UserIDArg::Email(email) => cmd.arg("--email").arg(email), UserIDArg::Name(name) => cmd.arg("--name").arg(name), UserIDArg::AddUserID(userid) => cmd.arg("--userid-or-add").arg(userid), UserIDArg::AddEmail(email) => cmd.arg("--email-or-add").arg(email), }; } } impl std::fmt::Display for UserIDArg<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { write!(f, "{}", self.raw()) } } // When calling a function like `Sq::key_generate` that has an `&[U] // where U: Into { 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_str(t: &str) -> Self { if let Ok(d) = chrono::NaiveDateTime::parse_from_str(t, "%Y-%m-%dT%H:%M:%S") { return Self::at(Utc.from_utc_datetime(&d).into()); } if let Ok(d) = chrono::NaiveDate::parse_from_str(t, "%Y-%m-%d") { let pad_date_with = chrono::NaiveTime::from_hms_opt(0, 0, 0).unwrap(); return Self::at(Utc.from_utc_datetime(&d.and_time(pad_date_with)).into()); } panic!("Invalid timestamp, must be YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS"); } /// 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 { self.command_args(&[]) } /// Returns a command that is set to run `sq`. The home directory /// and time are already set. The arguments in `pre` are added at /// the beginning of the command line. pub fn command_args(&self, pre: &[&str]) -> 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); for arg in pre { cmd.arg(arg); } 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: {}", std::iter::once(cmd.get_program()) .chain(cmd.get_args()) .map(|arg| { let arg = arg.to_string_lossy(); if arg.contains(" ") { format!("{:?}", arg) } else { arg.into_owned() } }) .collect::>() .join(" ")); 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: Into, { self.decrypt_maybe(args, msg).expect("can decrypt") } /// Decrypts a message. pub fn decrypt_maybe(&self, args: &[&str], msg: M) -> Result> where M: Into, { let mut cmd = self.command(); cmd.args([ "decrypt" ]); for arg in args { cmd.arg(arg); } match msg.into() { FileOrBytes::FileOrStdin(path) => { cmd.arg(path); } FileOrBytes::Bytes(bytes) => { cmd.write_stdin(bytes); } } 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: Into, { 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: Into, { let mut cmd = self.command(); cmd.args([ "encrypt" ]); for arg in args { cmd.arg(arg.as_ref()); } match msg.into() { FileOrBytes::FileOrStdin(path) => { cmd.arg(path); } FileOrBytes::Bytes(bytes) => { cmd.write_stdin(bytes); } } 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<'a, U>(&self, extra_args: &[&str], userids: &[U]) -> (Cert, PathBuf, PathBuf) where U: Into> + Clone { let userids: Vec = userids.iter() .cloned() .map(|u| u.into()) .collect(); let mut cmd = self.command(); cmd.args([ "key", "generate", "--own-key" ]); 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") || a.starts_with("--userid")); if ! any_userids { cmd.arg("--no-userids"); } else { for userid in userids.iter() { userid.as_arg(&mut cmd); } } let cert_filename = self.scratch_file( userids.get(0).map(|u| format!("{}-cert", u.raw())).as_deref()); cmd.arg("--output").arg(&cert_filename); let rev_filename = self.scratch_file( userids.get(0).map(|u| format!("{}-rev", u.raw())).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 try_key_delete<'a, H, Q>(&self, cert_handle: H, output_file: Q) -> 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("delete"); match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--cert-file").arg(path); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } else { cmd.arg("--output").arg("-"); } } 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, None); self.handle_cert_output(output, cert_handle, output_file, None) } /// Delete the specified key. pub fn key_delete<'a, H, Q>(&self, cert_handle: H, output_file: Q) -> Cert where H: Into, Q: Into>, { self.try_key_delete(cert_handle, output_file) .expect("success") } /// 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("--cert-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(["--signature-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 try_key_password<'a, H, Q>(&self, cert_handle: H, old_password_file: Option<&'a Path>, new_password_file: Option<&'a Path>, output_file: Q) -> 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("--cert-file").arg(&cert_handle); if output_file.is_none() { cmd.arg("--output").arg("-"); } } 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, None); self.handle_cert_output(output, cert_handle, output_file, true) } /// 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) -> Cert where H: Into, Q: Into>, { self.try_key_password( cert_handle, old_password_file, new_password_file, output_file) .expect("success") } /// 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("--cert-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("--cert-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, cert: H1, khs: Vec

) -> Cert where H1: Into, H2: Into { self.key_subkey_export_maybe(cert, 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, cert: H1, khs: Vec

) -> Result where H1: Into, H2: Into, { let cert = cert.into(); let mut cmd = self.command(); cmd.args([ "key", "subkey", "export" ]); cmd.arg("--cert").arg(cert.to_string()); for kh in khs.into_iter() { let kh: KeyHandle = kh.into(); cmd.arg("--key").arg(kh.to_string()); } let output = self.run(cmd, None); self.handle_cert_output( output, cert.into(), Some(PathBuf::from("-").as_path()), true) } /// 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("--cert-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(["--signature-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 keys. pub fn try_key_subkey_delete<'a, H, Q>(&self, cert_handle: H, key_handles: &[KeyHandle], output_file: Q) -> 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("delete"); match &cert_handle { FileOrKeyHandle::FileOrStdin(path) => { cmd.arg("--cert-file").arg(path); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } else { cmd.arg("--output").arg("-"); } } FileOrKeyHandle::KeyHandle((_kh, s)) => { cmd.arg("--cert").arg(&s); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } } }; for kh in key_handles { cmd.arg("--key").arg(kh.to_string()); } let output = self.run(cmd, None); self.handle_cert_output(output, cert_handle, output_file, None) } /// Delete the specified keys. pub fn key_subkey_delete<'a, H, Q>(&self, cert_handle: H, key_handles: &[KeyHandle], output_file: Q) -> Cert where H: Into, Q: Into>, { self.try_key_subkey_delete(cert_handle, key_handles, output_file) .expect("success") } /// Change the key's password. pub fn try_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) -> 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("--cert-file").arg(&cert_handle); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } else { cmd.arg("--output").arg("-"); } } else { cmd.arg("--cert").arg(&cert_handle); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } }; 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"); } let output = self.run(cmd, None); self.handle_cert_output(output, cert_handle, output_file, None) } /// 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) -> Cert where H: Into, Q: Into>, { self.try_key_subkey_password( cert_handle, keys, old_password_file, new_password_file, output_file) .expect("success") } /// 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("--cert-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<'a, U>(&self, args: &[&str], key: Cert, userids: &[U]) -> Result where U: Into> + Clone { let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); 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); for userid in userids.iter() { userid.as_arg(&mut cmd); } 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, 'b, C, O, U>(&self, args: &[&str], cert: C, userid: U, reason: &str, message: &str, output_file: O) -> Result where C: Into, U: 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); } } userid.into().as_arg(&mut cmd); 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, 'b, C, O, U>(&self, args: &[&str], cert: C, userid: U, reason: &str, message: &str, output_file: O) -> Cert where C: Into, U: Into>, O: Into>, { self.key_userid_revoke_maybe(args, cert, userid, reason, message, output_file) .expect("succeeds") } /// Runs `sq cert list` with the supplied arguments. pub fn cert_list_maybe(&self, args: &[&str]) -> Result> { let mut cmd = self.command(); cmd.arg("cert").arg("list"); for arg in args { cmd.arg(arg); } let output = self.run(cmd, None); if output.status.success() { Ok(output.stdout) } else { Err(anyhow::anyhow!("sq cert list returned an error")) } } /// Runs `sq cert list` with the supplied arguments. pub fn cert_list(&self, args: &[&str]) -> Vec { self.cert_list_maybe(args).expect("success") } /// 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 try_pki_vouch_add<'a, 'b, H, C, U, Q>( &self, extra_args: &[&str], certifier: H, cert: C, userids: &[U], output_file: Q, success: bool) -> Result where H: Into, C: Into, U: Into> + Clone, Q: Into>, { let certifier = certifier.into(); let cert = cert.into(); let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "pki", "vouch", "add" ]); 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.iter() { userid.as_arg(&mut cmd); } 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_add<'a, 'b, H, C, U, Q>( &self, extra_args: &[&str], certifier: H, cert: C, userids: &[U], output_file: Q) -> Cert where H: Into, C: Into, U: Into> + Clone, Q: Into>, { self.try_pki_vouch_add( 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, 'b, H, C, U, Q>( &self, extra_args: &[&str], certifier: H, cert: C, userids: &[U], output_file: Q, success: bool) -> Result where H: Into, C: Into, U: Into> + Clone, Q: Into>, { let certifier = certifier.into(); let cert = cert.into(); let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); 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.iter() { userid.as_arg(&mut cmd); } 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, 'b, H, C, U, Q>( &self, extra_args: &[&str], certifier: H, cert: C, userids: &[U], output_file: Q) -> Cert where H: Into, C: Into, U: Into> + Clone, 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<'a, U>(&self, extra_args: &[&str], cert: KeyHandle, userids: &[U]) -> Result<()> where U: Into> + Clone, { let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); 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.iter() { userid.as_arg(&mut cmd); } 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<'a, U>(&self, args: &[&str], cert: KeyHandle, userids: &[U]) where U: Into> + Clone, { self.pki_link_add_maybe(args, cert, userids).expect("success") } /// Add a link for the binding. pub fn pki_link_retract_maybe<'a, U>(&self, extra_args: &[&str], cert: KeyHandle, userids: &[U]) -> Result<()> where U: Into> + Clone, { let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); 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.iter() { userid.as_arg(&mut cmd); } 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<'a, U>(&self, args: &[&str], cert: KeyHandle, userids: &[U]) where U: Into> + Clone, { self.pki_link_retract_maybe(args, cert, userids) .expect("success") } /// Add a link for the binding. pub fn pki_link_authorize_maybe<'a, U>(&self, extra_args: &[&str], cert: KeyHandle, userids: &[U]) -> Result<()> where U: Into> + Clone, { let userids = userids.iter() .cloned() .map(|u| u.into()) .collect::>(); 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.iter() { userid.as_arg(&mut cmd); } 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<'a, U>(&self, args: &[&str], cert: KeyHandle, userids: &[U]) where U: Into> + Clone, { self.pki_link_authorize_maybe(args, cert, userids) .expect("success") } /// Authenticate a binding. pub fn pki_authenticate<'a, U>(&self, extra_args: &[&str], cert: &str, userid: U) -> Result<()> where U: Into>, { let mut cmd = self.command(); cmd.args([ "pki", "authenticate", "--show-paths" ]); for arg in extra_args { cmd.arg(arg); } cmd.arg("--cert").arg(cert); userid.into().as_arg(&mut cmd); 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>, { self.sign_args(&[], signer, password_file, input_file, output_file) } pub fn sign_args<'a, H, Q>(&self, args: &[&str], 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").arg("--message"); cmd.args(args); 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>, { self.try_sign_detached(args, signer, input_file, output_file) .unwrap() } pub fn try_sign_detached<'a, H, O>(&self, args: &[&str], signer: H, input_file: &Path, output_file: O) -> Result> where H: Into, O: Into>, { let signer = signer.into(); let output_file = output_file.into(); let mut cmd = self.command(); cmd.arg("sign").arg("--signature-file"); 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, None); if output.status.success() { if let Some(output_file) = output_file { Ok(std::fs::read(output_file).expect("can read file")) } else { Ok(output.stdout) } } else { Err(anyhow::anyhow!("{}", String::from_utf8_lossy(&output.stderr))) } } pub fn verify_maybe<'a, P, Q>(&self, args: &[&str], op: Verify, input: P, output_file: Q) -> Result> where P: AsRef, Q: Into>, { let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "verify" ]); for arg in args { cmd.arg(arg); } op.apply(&mut cmd); cmd.arg(input.as_ref()); if let Some(output_file) = output_file { cmd.arg("--output").arg(output_file); } let output = self.run(cmd, None); if output.status.success() { if let Some(output_file) = output_file { if output_file != &PathBuf::from("-") { return Ok(std::fs::read(output_file)?); } } Ok(output.stdout) } else { Err(anyhow::anyhow!(format!( "Command failed:\n{}", String::from_utf8_lossy(&output.stderr)))) } } pub fn verify<'a, P, Q>(&self, args: &[&str], op: Verify, input: P, output_file: Q) -> Vec where P: AsRef, Q: Into>, { self.verify_maybe(args, op, input, output_file) .expect("success") } // Merges the certificates. pub fn keyring_merge_maybe<'a, P, Q>(&self, input_files: &[P], input_bytes: Option<&[u8]>, output_file: Q) -> Result> where P: AsRef, Q: Into>, { let output_file = output_file.into(); let mut cmd = self.command(); cmd.args([ "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, None); if output.status.success() { 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::>>() } else { Err(anyhow::anyhow!("sq key export returned an error")) } } // Merges the certificates. pub fn keyring_merge<'a, P, Q>(&self, input_files: &[P], input_bytes: Option<&[u8]>, output_file: Q) -> Vec where P: AsRef, Q: Into>, { self.keyring_merge_maybe(input_files, input_bytes, output_file) .expect("success") } } pub enum Verify { Message, SignatureFile(PathBuf), Cleartext, } impl Verify { fn apply(self, cmd: &mut Command) { match self { Verify::Message => cmd.arg("--message"), Verify::SignatureFile(f) => cmd.arg("--signature-file").arg(f), Verify::Cleartext => cmd.arg("--cleartext"), }; } } /// 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(()) }