//! GnuPG RPC support. #![warn(missing_docs)] use std::collections::BTreeMap; use std::convert::TryFrom; use std::ffi::OsStr; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use futures::{Stream, StreamExt}; use std::task::{Poll, self}; use std::pin::Pin; use sequoia_openpgp as openpgp; use openpgp::types::{HashAlgorithm, Timestamp}; use openpgp::fmt::hex; use openpgp::cert::ValidCert; use openpgp::crypto; use openpgp::packet::prelude::*; use openpgp::parse::Parse; use crate::Result; use crate::assuan; use crate::Keygrip; use crate::sexp::Sexp; /// A GnuPG context. #[derive(Debug)] pub struct Context { homedir: Option, sockets: BTreeMap, ephemeral: Option, // XXX: Remove me once hack for Cygwin won't be necessary. #[cfg(windows)] cygwin: bool, } impl Context { /// Creates a new context for the default GnuPG home directory. pub fn new() -> Result { Self::make(None, None) } /// Creates a new context for the given GnuPG home directory. pub fn with_homedir

(homedir: P) -> Result where P: AsRef { 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::make(None, Some(tempfile::tempdir()?)) } fn make(homedir: Option<&Path>, ephemeral: Option) -> Result { let mut sockets: BTreeMap = Default::default(); let ephemeral_dir = ephemeral.as_ref().map(|tmp| tmp.path()); let homedir = ephemeral_dir.or(homedir); // Guess if we're dealing with Unix/Cygwin or native Windows variant // We need to do that in order to pass paths in correct style to gpgconf let a_gpg_path = Self::gpgconf(&None, &["--list-dirs", "homedir"], 1)?; let first_byte = a_gpg_path.get(0).and_then(|c| c.get(0)).and_then(|c| c.get(0)); let gpg_style = match first_byte { Some(b'/') => Mode::Unix, _ => Mode::native(), }; let homedir = homedir.map(|dir| convert_path(dir, gpg_style) .unwrap_or_else(|_| PathBuf::from(dir)) ); for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)? { let key = std::str::from_utf8(&fields[0])?; // For now, we're only interested in sockets. let socket = match key.strip_suffix("-socket") { Some(socket) => socket, _ => continue, }; // NOTE: Directories and socket paths are percent-encoded if no // argument to "--list-dirs" is given let mut value = std::str::from_utf8(&fields[1])?.to_owned(); // FIXME: Percent-decode everything, but for now at least decode // colons to support Windows drive letters value = value.replace("%3a", ":"); // Store paths in native format, following the least surprise rule. let path = convert_path(&value, Mode::native())?; sockets.insert(socket.into(), path); } /// Whether we're dealing with gpg that expects Windows or Unix-style paths. #[derive(Copy, Clone)] #[allow(dead_code)] enum Mode { Windows, Unix } impl Mode { fn native() -> Self { platform! { unix => Mode::Unix, windows => Mode::Windows, } } } #[cfg(not(windows))] fn convert_path(path: impl AsRef, mode: Mode) -> Result { match mode { Mode::Unix => Ok(PathBuf::from(path.as_ref())), Mode::Windows => Err(anyhow::anyhow!( "Converting to Windows-style paths is only supported on Windows" )), } } #[cfg(windows)] fn convert_path(path: impl AsRef, mode: Mode) -> Result { let conversion_type = match mode { Mode::Windows => "--windows", Mode::Unix => "--unix", }; crate::new_background_command("cygpath") .arg(conversion_type) .arg(path.as_ref()) .output() .map_err(Into::into) .and_then(|out| if out.status.success() { let output = std::str::from_utf8(&out.stdout)?.trim(); Ok(PathBuf::from(output)) } else { Err(anyhow::anyhow!( "Executing cygpath encountered error for path {}", path.as_ref().to_string_lossy() )) } ) } Ok(Context { homedir, sockets, ephemeral, #[cfg(windows)] cygwin: cfg!(windows) && matches!(gpg_style, Mode::Unix), }) } fn gpgconf(homedir: &Option, arguments: &[&str], nfields: usize) -> Result>>> { let nl = |&c: &u8| c as char == '\n'; let colon = |&c: &u8| c as char == ':'; let mut gpgconf = crate::new_background_command("gpgconf"); if let Some(homedir) = homedir { gpgconf.arg("--homedir").arg(homedir); // https://dev.gnupg.org/T4496 gpgconf.env("GNUPGHOME", homedir); } gpgconf.args(arguments); let output = gpgconf.output().map_err(|e| { Error::GPGConf(e.to_string()) })?; if output.status.success() { let mut result = Vec::new(); for mut line in output.stdout.split(nl) { if line.is_empty() { // EOF. break; } // Make sure to also skip \r on Windows if line[line.len() - 1] == b'\r' { line = &line[..line.len() - 1]; } let fields = line.splitn(nfields, colon).map(|f| f.to_vec()) .collect::>(); if fields.len() != nfields { return Err(Error::GPGConf( format!("Malformed response, expected {} fields, \ on line: {:?}", nfields, line)).into()); } result.push(fields); } Ok(result) } else { Err(Error::GPGConf(String::from_utf8_lossy( &output.stderr).into_owned()).into()) } } /// Returns the path to `homedir` directory. /// /// The path returned will be in a local format, i. e. one accepted by /// available `gpgconf` or `gpg` tools. /// /// pub fn homedir(&self) -> Option<&Path> { self.homedir.as_deref() } /// Returns the path to a GnuPG socket. pub fn socket(&self, socket: C) -> Result<&Path> where C: AsRef { self.sockets.get(socket.as_ref()) .map(|p| p.as_path()) .ok_or_else(|| { Error::GPGConf(format!("No such socket {:?}", socket.as_ref())).into() }) } /// Creates directories for RPC communication. pub fn create_socket_dir(&self) -> Result<()> { // FIXME: GnuPG as packaged by MinGW fails to create socketdir because // it follows upstream Unix logic, which expects Unix-like `/var/run` // sockets to work. Additionally, GnuPG expects to work with and set // correct POSIX permissions that MinGW does not even support/emulate, // so this fails loudly. // Instead, don't do anything and rely on on homedir being treated // (correctly) as a fallback here. #[cfg(windows)] if self.cygwin { return Ok(()); } 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<()> { let _ = self.create_socket_dir(); // Best effort. 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 Context { fn drop(&mut self) { if self.ephemeral.is_some() { let _ = self.stop_all(); let _ = self.remove_socket_dir(); } } } /// A connection to a GnuPG agent. pub struct Agent { c: assuan::Client, } impl Deref for Agent { type Target = assuan::Client; fn deref(&self) -> &Self::Target { &self.c } } impl DerefMut for Agent { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.c } } impl Stream for Agent { type Item = Result; /// Attempt to pull out the next value of this stream, returning /// None if the stream is finished. /// /// Note: It _is_ safe to call this again after the stream /// finished, i.e. returned `Ready(None)`. fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { Pin::new(&mut self.c).poll_next(cx) } } impl Agent { /// Connects to the agent. /// /// Note: This function does not try to start the server. If no /// server is running for the given context, this operation will /// fail. pub async fn connect(ctx: &Context) -> Result { let path = ctx.socket("agent")?; Self::connect_to(path).await } /// Connects to the agent at the given path. /// /// Note: This function does not try to start the server. If no /// server is running for the given context, this operation will /// fail. pub async fn connect_to

(path: P) -> Result where P: AsRef { Ok(Agent { c: assuan::Client::connect(path).await? }) } /// Creates a signature over the `digest` produced by `algo` using /// `key` with the secret bits managed by the agent. pub async fn sign<'a>(&'a mut self, key: &'a KeyPair, algo: HashAlgorithm, digest: &'a [u8]) -> Result { for option in Self::options() { self.send_simple(option).await?; } if key.password.is_some() { self.send_simple("OPTION pinentry-mode=loopback").await?; } let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SIGKEY {}", grip)).await?; self.send_simple( format!("SETKEYDESC {}", assuan::escape(&key.password_prompt))).await?; let algo = u8::from(algo); let digest = hex::encode(&digest); self.send_simple(format!("SETHASH {} {}", algo, digest)).await?; self.send("PKSIGN")?; let mut data = Vec::new(); while let Some(r) = self.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Inquire { keyword, .. } => match (keyword.as_str(), &key.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| self.data(p))?; // Dummy read to send the data. self.next().await; }, _ => acknowledge_inquiry(self).await?, }, assuan::Response::Error { ref message, .. } => return assuan::operation_failed(self, message).await, assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } Sexp::from_bytes(&data)?.to_signature() } /// Decrypts `ciphertext` using `key` with the secret bits managed /// by the agent. pub async fn decrypt<'a>(&'a mut self, key: &'a KeyPair, ciphertext: &'a crypto::mpi::Ciphertext, plaintext_len: Option) -> Result { for option in Self::options() { self.send_simple(option).await?; } if key.password.is_some() { self.send_simple("OPTION pinentry-mode=loopback").await?; } let grip = Keygrip::of(key.public.mpis())?; self.send_simple(format!("SETKEY {}", grip)).await?; self.send_simple(format!("SETKEYDESC {}", assuan::escape(&key.password_prompt))).await?; self.send("PKDECRYPT")?; let mut padding = true; let mut data = Vec::new(); while let Some(r) = self.next().await { match r? { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } => (), // Ignore. assuan::Response::Inquire { ref keyword, .. } => match (keyword.as_str(), &key.password) { ("PASSPHRASE", Some(p)) => { p.map(|p| self.data(p))?; // Dummy read to send the data. self.next().await; }, ("CIPHERTEXT", _) => { let mut buf = Vec::new(); Sexp::try_from(ciphertext)?.serialize(&mut buf)?; self.data(&buf)?; // Dummy read to send the data. self.next().await; }, _ => acknowledge_inquiry(self).await?, }, assuan::Response::Status { ref keyword, ref message } => if keyword == "PADDING" { padding = message != "0"; }, assuan::Response::Error { ref message, .. } => return assuan::operation_failed(self, message).await, assuan::Response::Data { ref partial } => data.extend_from_slice(partial), } } // Get rid of the safety-0. // // gpg-agent seems to add a trailing 0, supposedly for good // measure. if data.iter().last() == Some(&0) { let l = data.len(); data.truncate(l - 1); } Sexp::from_bytes(&data)?.finish_decryption( &key.public, ciphertext, plaintext_len, padding) } /// Computes options that we want to communicate. fn options() -> Vec { use std::env::var; let mut r = Vec::new(); if let Ok(tty) = var("GPG_TTY") { r.push(format!("OPTION ttyname={}", tty)); } else { #[cfg(unix)] unsafe { use std::ffi::CStr; let tty = libc::ttyname(0); if ! tty.is_null() { if let Ok(tty) = CStr::from_ptr(tty).to_str() { r.push(format!("OPTION ttyname={}", tty)); } } } } if let Ok(term) = var("TERM") { r.push(format!("OPTION ttytype={}", term)); } if let Ok(display) = var("DISPLAY") { r.push(format!("OPTION display={}", display)); } if let Ok(xauthority) = var("XAUTHORITY") { r.push(format!("OPTION xauthority={}", xauthority)); } if let Ok(dbus) = var("DBUS_SESSION_BUS_ADDRESS") { r.push(format!("OPTION putenv=DBUS_SESSION_BUS_ADDRESS={}", dbus)); } // We're going to pop() options off the end, therefore reverse // the vec here to preserve the above ordering, which is the // one GnuPG uses. r.reverse(); r } /// Start tracing the data that is sent to the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_sent(&mut self, fun: Box) { self.c.trace_data_sent(fun); } /// Start tracing the data that is received from the server. /// /// Note: if a tracing function is already registered, this /// replaces it. pub fn trace_data_received(&mut self, fun: Box) { self.c.trace_data_received(fun); } } /// A cryptographic key pair. /// /// A `KeyPair` is a combination of public and secret key. This /// particular implementation does not have the secret key, but /// diverges the cryptographic operations to `gpg-agent`. pub struct KeyPair { public: Key, agent_socket: PathBuf, password: Option, password_prompt: String, } impl KeyPair { /// Returns a `KeyPair` for `key` with the secret bits managed by /// the agent. /// /// This provides a convenient, synchronous interface for use with /// the low-level Sequoia crate. pub fn new(ctx: &Context, key: &Key) -> Result where R: key::KeyRole { Ok(KeyPair { password: None, password_prompt: format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", key.keyid(), Timestamp::try_from(key.creation_time()).unwrap()), public: key.role_as_unspecified().clone(), agent_socket: ctx.socket("agent")?.into(), }) } /// Changes the password prompt to include information about the /// cert. /// /// Use this function to give more context to the user when she is /// prompted for a password. This function will generate a prompt /// that is very similar to the prompts that GnuPG generates. /// /// To set an arbitrary password prompt, use /// [`KeyPair::with_password_prompt`]. pub fn with_cert(self, cert: &ValidCert) -> Self { let primary_id = cert.keyid(); let keyid = self.public.keyid(); let prompt = match (primary_id == keyid, cert.primary_userid() .map(|uid| uid.clone()) .ok()) { (true, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {}.", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, Some(uid)) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ {}\n\ ID {:X}, created {} (main key ID {}).", uid.userid(), keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), (true, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {}.", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), ), (false, None) => format!( "Please enter the passphrase to \ unlock the OpenPGP secret key:\n\ ID {:X}, created {} (main key ID {}).", keyid, Timestamp::try_from(self.public.creation_time()) .expect("creation time is representable"), primary_id, ), }; self.with_password_prompt(prompt) } /// Supplies a password to unlock the secret key. /// /// This will be used when the secret key operation is performed, /// e.g. when signing or decrypting a message. /// /// Note: This is the equivalent of GnuPG's /// `--pinentry-mode=loopback` and requires explicit opt-in in the /// gpg-agent configuration using the `allow-loopback-pinentry` /// option. If this is not enabled in the agent, the secret key /// operation will fail. It is likely only useful during testing. pub fn with_password(mut self, password: crypto::Password) -> Self { self.password = Some(password); self } /// Changes the password prompt. /// /// Use this function to give more context to the user when she is /// prompted for a password. /// /// To set an password prompt that uses information from the /// OpenPGP certificate, use [`KeyPair::with_cert`]. pub fn with_password_prompt(mut self, prompt: String) -> Self { self.password_prompt = prompt; self } } impl crypto::Signer for KeyPair { fn public(&self) -> &Key { &self.public } fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result { use crate::openpgp::types::PublicKeyAlgorithm::*; use crate::openpgp::crypto::mpi::PublicKey; #[allow(deprecated)] match (self.public.pk_algo(), self.public.mpis()) { (RSASign, PublicKey::RSA { .. }) | (RSAEncryptSign, PublicKey::RSA { .. }) | (DSA, PublicKey::DSA { .. }) | (EdDSA, PublicKey::EdDSA { .. }) | (ECDSA, PublicKey::ECDSA { .. }) => { use tokio::runtime::{Handle, Runtime}; // Connect to the agent and do the signing // operation. let do_it = async move { let mut a = Agent::connect_to(&self.agent_socket).await?; let sig = a.sign(self, hash_algo, digest).await?; Ok(sig) }; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(do_it) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. use crossbeam_utils::thread; thread::scope(|s| { s.spawn(move |_| { let rt = Runtime::new()?; rt.block_on(do_it) }).join() }).map_err(map_panic)? .map_err(map_panic)? } }, (pk_algo, _) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of algorithm {:?} and key {:?}", pk_algo, self.public)).into()), } } } impl crypto::Decryptor for KeyPair { fn public(&self) -> &Key { &self.public } fn decrypt(&mut self, ciphertext: &crypto::mpi::Ciphertext, plaintext_len: Option) -> openpgp::Result { use crate::openpgp::crypto::mpi::{PublicKey, Ciphertext}; match (self.public.mpis(), ciphertext) { (PublicKey::RSA { .. }, Ciphertext::RSA { .. }) | (PublicKey::ElGamal { .. }, Ciphertext::ElGamal { .. }) | (PublicKey::ECDH { .. }, Ciphertext::ECDH { .. }) => { use tokio::runtime::{Handle, Runtime}; // Connect to the agent and do the decryption // operation. let do_it = async move { let mut a = Agent::connect_to(&self.agent_socket).await?; let sk = a.decrypt(self, ciphertext, plaintext_len).await?; Ok(sk) }; // See if the current thread is managed by a tokio // runtime. if Handle::try_current().is_err() { // Doesn't seem to be the case, so it is safe // to create a new runtime and block. let rt = Runtime::new()?; rt.block_on(do_it) } else { // It is! We must not create a second runtime // on this thread, but instead we will // delegate this to a new thread. use crossbeam_utils::thread; thread::scope(|s| { s.spawn(move |_| { let rt = Runtime::new()?; rt.block_on(do_it) }).join() }).map_err(map_panic)? .map_err(map_panic)? } }, (public, ciphertext) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of key pair {:?} \ and ciphertext {:?}", public, ciphertext)).into()), } } } /// Maps a panic of a worker thread to an error. /// /// Unfortunately, there is nothing useful to do with the error, but /// returning a generic error is better than panicking. fn map_panic(_: Box) -> anyhow::Error { anyhow::anyhow!("worker thread panicked") } /// Helper function to respond to inquiries. /// /// This function doesn't do something useful, but it ends the current /// inquiry. async fn acknowledge_inquiry(agent: &mut Agent) -> Result<()> { agent.send("END")?; agent.next().await; // Dummy read to send END. Ok(()) } #[derive(thiserror::Error, Debug)] /// Errors used in this module. pub enum Error { /// Errors related to `gpgconf`. #[error("gpgconf: {0}")] GPGConf(String), /// The remote operation failed. #[error("Operation failed: {0}")] OperationFailed(String), /// The remote party violated the protocol. #[error("Protocol violation: {0}")] ProtocolError(String), } #[cfg(test)] mod tests { use super::*; use openpgp::{ Cert, crypto::{ hash::Digest, mpi::Ciphertext, Decryptor, Signer, }, }; /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::signer_in_async_context' panicked at /// 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn signer_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .primary_key().key().clone(); let mut pair = KeyPair::new(&ctx, &key)?; let algo = HashAlgorithm::default(); let digest = algo.context()?.into_digest()?; let _ = pair.sign(algo, &digest); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } /// Asserts that a is usable from an async /// context. /// /// Previously, the test died with /// /// thread 'gnupg::tests::decryptor_in_async_context' panicked /// at 'Cannot start a runtime from within a runtime. This /// happens because a function (like `block_on`) attempted to /// block the current thread while the thread is being used to /// drive asynchronous tasks.' #[test] fn decryptor_in_async_context() -> Result<()> { async fn async_context() -> Result<()> { let ctx = match Context::ephemeral() { Ok(c) => c, Err(e) => { eprintln!("Failed to create ephemeral context: {}", e); eprintln!("Most likely, GnuPG isn't installed."); eprintln!("Skipping test."); return Ok(()); }, }; let key = Cert::from_bytes(crate::tests::key("testy-new.pgp"))? .keys().nth(1).unwrap().key().clone(); let mut pair = KeyPair::new(&ctx, &key)?; let ciphertext = Ciphertext::ECDH { e: vec![].into(), key: vec![].into_boxed_slice(), }; let _ = pair.decrypt(&ciphertext, None); Ok(()) } let rt = tokio::runtime::Runtime::new()?; rt.block_on(async_context()) } }