//! GnuPG RPC support. #![warn(missing_docs)] use std::collections::BTreeMap; use std::convert::TryFrom; use std::ops::{Deref, DerefMut}; use std::path::{Path, PathBuf}; use std::process::Command; use futures::{Async, Future, Stream}; use sequoia_openpgp as openpgp; use openpgp::types::HashAlgorithm; use openpgp::fmt::hex; use openpgp::crypto; use openpgp::packet::prelude::*; use openpgp::parse::Parse; use openpgp::serialize::Serialize; use crate::Result; use crate::assuan; use crate::Keygrip; use crate::sexp::Sexp; /// A GnuPG context. #[derive(Debug)] pub struct Context { homedir: Option, components: BTreeMap, directories: BTreeMap, sockets: BTreeMap, #[allow(dead_code)] // We keep it around for the cleanup. ephemeral: Option, } 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 components: BTreeMap = Default::default(); let mut directories: BTreeMap = Default::default(); let mut sockets: BTreeMap = Default::default(); let homedir: Option = ephemeral.as_ref().map(|tmp| tmp.path()).or(homedir) .map(|p| p.into()); for fields in Self::gpgconf( &homedir, &["--list-components"], 3)?.into_iter() { components.insert(String::from_utf8(fields[0].clone())?, String::from_utf8(fields[2].clone())?.into()); } for fields in Self::gpgconf(&homedir, &["--list-dirs"], 2)?.into_iter() { let (mut key, value) = (fields[0].clone(), fields[1].clone()); if key.ends_with(b"-socket") { let l = key.len(); key.truncate(l - b"-socket".len()); sockets.insert(String::from_utf8(key)?, String::from_utf8(value)?.into()); } else { directories.insert(String::from_utf8(key)?, String::from_utf8(value)?.into()); } } Ok(Context { homedir, components, directories, sockets, ephemeral, }) } 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 = Command::new("gpgconf"); if let Some(homedir) = homedir { gpgconf.arg("--homedir").arg(homedir); // https://dev.gnupg.org/T4496 gpgconf.env("GNUPGHOME", homedir); } for argument in arguments { gpgconf.arg(argument); } let output = gpgconf.output().map_err(|e| -> anyhow::Error { Error::GPGConf(e.to_string()).into() })?; if output.status.success() { let mut result = Vec::new(); for line in output.stdout.split(nl) { if line.len() == 0 { // EOF. break; } 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 a GnuPG component. pub fn component(&self, component: C) -> Result<&Path> where C: AsRef { self.components.get(component.as_ref()) .map(|p| p.as_path()) .ok_or_else(|| { Error::GPGConf(format!("No such component {:?}", component.as_ref())).into() }) } /// Returns the path to a GnuPG directory. pub fn directory(&self, directory: C) -> Result<&Path> where C: AsRef { self.directories.get(directory.as_ref()) .map(|p| p.as_path()) .ok_or_else(|| { Error::GPGConf(format!("No such directory {:?}", directory.as_ref())).into() }) } /// 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<()> { Self::gpgconf(&self.homedir, &["--create-socketdir"], 1)?; Ok(()) } /// Removes directories for RPC communication. /// /// Note: This will stop all servers once they note that their /// socket is gone. pub fn remove_socket_dir(&self) -> Result<()> { Self::gpgconf(&self.homedir, &["--remove-socketdir"], 1)?; Ok(()) } /// Starts a GnuPG component. pub fn start(&self, component: &str) -> Result<()> { self.create_socket_dir()?; Self::gpgconf(&self.homedir, &["--launch", component], 1)?; Ok(()) } /// Stops a GnuPG component. pub fn stop(&self, component: &str) -> Result<()> { Self::gpgconf(&self.homedir, &["--kill", component], 1)?; Ok(()) } /// Stops all GnuPG components. pub fn stop_all(&self) -> Result<()> { self.stop("all") } } impl Drop for 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 = assuan::Response; type Error = anyhow::Error; /// 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(&mut self) -> std::result::Result>, Self::Error> { self.c.poll() } } 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 fn connect<'c>(ctx: &'c Context) -> impl Future + 'c { futures::lazy(move || ctx.socket("agent")) .and_then(Self::connect_to) } /// 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 fn connect_to

(path: P) -> impl Future where P: AsRef { assuan::Client::connect(path) .and_then(|c| Ok(Agent { c })) } /// Creates a signature over the `digest` produced by `algo` using /// `key` with the secret bits managed by the agent. pub fn sign<'a, R>(&'a mut self, key: &'a Key, algo: HashAlgorithm, digest: &'a [u8]) -> impl Future + 'a where R: key::KeyRole { SigningRequest::new(&mut self.c, key, algo, digest) } /// Decrypts `ciphertext` using `key` with the secret bits managed /// by the agent. pub fn decrypt<'a, R>(&'a mut self, key: &'a Key, ciphertext: &'a crypto::mpi::Ciphertext) -> impl Future + 'a where R: key::KeyRole { DecryptionRequest::new(&mut self.c, key, ciphertext) } /// 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 } } struct SigningRequest<'a, 'b, 'c, R> where R: key::KeyRole { c: &'a mut assuan::Client, key: &'b Key, algo: HashAlgorithm, digest: &'c [u8], options: Vec, state: SigningRequestState, } impl<'a, 'b, 'c, R> SigningRequest<'a, 'b, 'c, R> where R: key::KeyRole { fn new(c: &'a mut assuan::Client, key: &'b Key, algo: HashAlgorithm, digest: &'c [u8]) -> Self { Self { c, key, algo, digest, options: Agent::options(), state: SigningRequestState::Start, } } } #[derive(Debug)] enum SigningRequestState { Start, Options, SigKey, SetHash, PkSign(Vec), } /// Returns a convenient Err value for use in the state machines /// below. fn operation_failed(message: &Option) -> Result { Err(Error::OperationFailed( message.as_ref().map(|e| e.to_string()) .unwrap_or_else(|| "Unknown reason".into())) .into()) } /// Returns a convenient Err value for use in the state machines /// below. fn protocol_error(response: &assuan::Response) -> Result { Err(Error::ProtocolError( format!("Got unexpected response {:?}", response)) .into()) } impl<'a, 'b, 'c, R> Future for SigningRequest<'a, 'b, 'c, R> where R: key::KeyRole { type Item = crypto::mpi::Signature; type Error = anyhow::Error; fn poll(&mut self) -> std::result::Result, Self::Error> { use self::SigningRequestState::*; loop { match self.state { Start => { if self.options.is_empty() { self.c.send(format!("SIGKEY {}", Keygrip::of(self.key.mpis())?))?; self.state = SigKey; } else { self.c.send(self.options.pop().unwrap())?; self.state = Options; } }, Options => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { if let Some(option) = self.options.pop() { self.c.send(option)?; } else { self.c.send(format!("SIGKEY {}", Keygrip::of(self.key.mpis())?))?; self.state = SigKey; } }, Async::NotReady => return Ok(Async::NotReady), }, SigKey => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { self.c.send(format!("SETHASH {} {}", u8::from(self.algo), hex::encode(&self.digest)))?; self.state = SetHash; }, Async::NotReady => return Ok(Async::NotReady), }, SetHash => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { self.c.send("PKSIGN")?; self.state = PkSign(Vec::new()); }, Async::NotReady => return Ok(Async::NotReady), }, PkSign(ref mut data) => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), assuan::Response::Data { ref partial } => data.extend_from_slice(partial), _ => return protocol_error(&r), }, Async::Ready(None) => { return Ok(Async::Ready( Sexp::from_bytes(&data)?.to_signature()?)); }, Async::NotReady => return Ok(Async::NotReady), }, } } } } struct DecryptionRequest<'a, 'b, 'c, R> where R: key::KeyRole { c: &'a mut assuan::Client, key: &'b Key, ciphertext: &'c crypto::mpi::Ciphertext, options: Vec, state: DecryptionRequestState, } impl<'a, 'b, 'c, R> DecryptionRequest<'a, 'b, 'c, R> where R: key::KeyRole { fn new(c: &'a mut assuan::Client, key: &'b Key, ciphertext: &'c crypto::mpi::Ciphertext) -> Self { Self { c, key, ciphertext, options: Agent::options(), state: DecryptionRequestState::Start, } } } #[derive(Debug)] enum DecryptionRequestState { Start, Options, SetKey, PkDecrypt, Inquire(Vec, bool), // Buffer and padding. } impl<'a, 'b, 'c, R> Future for DecryptionRequest<'a, 'b, 'c, R> where R: key::KeyRole { type Item = crypto::SessionKey; type Error = anyhow::Error; fn poll(&mut self) -> std::result::Result, Self::Error> { use self::DecryptionRequestState::*; loop { match self.state { Start => { if self.options.is_empty() { self.c.send(format!("SETKEY {}", Keygrip::of(self.key.mpis())?))?; self.state = SetKey; } else { self.c.send(self.options.pop().unwrap())?; self.state = Options; } }, Options => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { if let Some(option) = self.options.pop() { self.c.send(option)?; } else { self.c.send(format!("SETKEY {}", Keygrip::of(self.key.mpis())?))?; self.state = SetKey; } }, Async::NotReady => return Ok(Async::NotReady), }, SetKey => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { self.c.send("PKDECRYPT")?; self.state = PkDecrypt; }, Async::NotReady => return Ok(Async::NotReady), }, PkDecrypt => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Inquire { ref keyword, .. } if keyword == "CIPHERTEXT" => (), // What we expect. assuan::Response::Comment { .. } | assuan::Response::Status { .. } => (), // Ignore. assuan::Response::Error { ref message, .. } => return operation_failed(message), _ => return protocol_error(&r), }, Async::Ready(None) => { let mut buf = Vec::new(); Sexp::try_from(self.ciphertext)? .serialize(&mut buf)?; self.c.data(&buf)?; self.state = Inquire(Vec::new(), true); }, Async::NotReady => return Ok(Async::NotReady), }, Inquire(ref mut data, ref mut padding) => match self.c.poll()? { Async::Ready(Some(r)) => match r { assuan::Response::Ok { .. } | assuan::Response::Comment { .. } => (), // Ignore. assuan::Response::Status { ref keyword, ref message } => if keyword == "PADDING" { *padding = &message != &"0"; }, assuan::Response::Error { ref message, .. } => return operation_failed(message), assuan::Response::Data { ref partial } => data.extend_from_slice(partial), _ => return protocol_error(&r), }, Async::Ready(None) => { // 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); } return Ok(Async::Ready( Sexp::from_bytes(&data)?.finish_decryption( self.key, self.ciphertext, *padding)? )); }, Async::NotReady => return Ok(Async::NotReady), }, } } } } /// 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<'a> { public: &'a Key, agent_socket: PathBuf, } impl<'a> KeyPair<'a> { /// 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: &'a Key) -> Result> where R: key::KeyRole { Ok(KeyPair { public: key.role_as_unspecified(), agent_socket: ctx.socket("agent")?.into(), }) } } impl<'a> crypto::Signer for KeyPair<'a> { 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 { .. }) => { let mut a = Agent::connect_to(&self.agent_socket).wait()?; let sig = a.sign(self.public, hash_algo, digest).wait()?; Ok(sig) }, (pk_algo, _) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of algorithm {:?} and key {:?}", pk_algo, self.public)).into()), } } } impl<'a> crypto::Decryptor for KeyPair<'a> { 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 { .. }) => { let mut a = Agent::connect_to(&self.agent_socket).wait()?; let sk = a.decrypt(self.public, ciphertext).wait()?; Ok(sk) }, (public, ciphertext) => Err(openpgp::Error::InvalidOperation(format!( "unsupported combination of key pair {:?} \ and ciphertext {:?}", public, ciphertext)).into()), } } } #[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), }