summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNeal H. Walfield <neal@sequoia-pgp.org>2024-03-09 23:10:06 +0100
committerNeal H. Walfield <neal@sequoia-pgp.org>2024-03-10 00:45:48 +0100
commit237b6ca36726a0487c7a46dffaebc90aa7a505a4 (patch)
tree9e9bb5bd701fd28b1562433ee32f7f62b021b13c
parentfd270aeedfffc7d03f8bd61bcf0842a831ec7ded (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.rs630
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())
- }
-}