diff options
author | Neal H. Walfield <neal@sequoia-pgp.org> | 2024-03-09 23:10:06 +0100 |
---|---|---|
committer | Neal H. Walfield <neal@sequoia-pgp.org> | 2024-03-10 00:45:48 +0100 |
commit | 237b6ca36726a0487c7a46dffaebc90aa7a505a4 (patch) | |
tree | 9e9bb5bd701fd28b1562433ee32f7f62b021b13c | |
parent | fd270aeedfffc7d03f8bd61bcf0842a831ec7ded (diff) |
ipc: Remove gnupg::Agent, and gnupg::KeyPair.neal/remove-gnupg-agent
- `gnupg::Agent` and `gnupg::KeyPair` have been moved to the
`sequoia-gpg-agent` crate. Remove the code from this crate.
https://gitlab.com/sequoia-pgp/sequoia-gpg-agent
-rw-r--r-- | ipc/src/gnupg.rs | 630 |
1 files changed, 14 insertions, 616 deletions
diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs index 3a84c93b..68d480fd 100644 --- a/ipc/src/gnupg.rs +++ b/ipc/src/gnupg.rs @@ -3,28 +3,25 @@ #![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 crate::Result; -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; +#[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), -use crate::Result; -use crate::assuan; -use crate::Keygrip; -use crate::sexp::Sexp; +} /// A GnuPG context. #[derive(Debug)] @@ -285,602 +282,3 @@ impl Drop for Context { } } } - -/// 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<assuan::Response>; - - /// 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<Option<Self::Item>> { - 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<Self> { - 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<P>(path: P) -> Result<Self> - where P: AsRef<Path> - { - 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<crypto::mpi::Signature> - { - 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<usize>) - -> Result<crypto::SessionKey> - { - 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<String> { - 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<dyn Fn(&[u8]) + Send + Sync>) - { - 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<dyn Fn(&[u8]) + Send + Sync>) - { - 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<key::PublicParts, key::UnspecifiedRole>, - agent_socket: PathBuf, - password: Option<crypto::Password>, - 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<R>(ctx: &Context, key: &Key<key::PublicParts, R>) - -> Result<KeyPair> - 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<key::PublicParts, key::UnspecifiedRole> { - &self.public - } - - fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) - -> openpgp::Result<openpgp::crypto::mpi::Signature> - { - 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<key::PublicParts, key::UnspecifiedRole> { - &self.public - } - - fn decrypt(&mut self, ciphertext: &crypto::mpi::Ciphertext, - plaintext_len: Option<usize>) - -> openpgp::Result<crypto::SessionKey> - { - 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<dyn std::any::Any + std::marker::Send>) -> 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 <KeyPair as Signer> 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 <KeyPair as Decryptor> 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()) - } -} |