summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2019-06-08 14:47:27 +0200
committerJustus Winter <justus@sequoia-pgp.org>2019-06-26 09:20:33 +0200
commit433437b20b8cb33263a293f34589e29eeacabfca (patch)
tree9b2d2ec214f8595f6e8c7e4a741ea359ba89f586
parentadcf04a7a2489ff4d5fe983fe7caa76c22fded2b (diff)
ipc: GnuPG RPC support.
- This allows us to communicate with gpg-agent, and use it for cryptographic operations.
-rw-r--r--Cargo.lock1
-rw-r--r--ipc/Cargo.toml1
-rw-r--r--ipc/examples/gpg-agent-client.rs38
-rw-r--r--ipc/examples/gpg-agent-decrypt.rs161
-rw-r--r--ipc/examples/gpg-agent-sign.rs76
-rw-r--r--ipc/src/gnupg.rs764
-rw-r--r--ipc/src/lib.rs1
-rw-r--r--ipc/tests/gpg-agent.rs282
8 files changed, 1324 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4f45913c..f8c1950a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1582,6 +1582,7 @@ dependencies = [
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"sequoia-core 0.8.0",
"sequoia-openpgp 0.8.0",
+ "tempfile 3.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio 0.1.21 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
diff --git a/ipc/Cargo.toml b/ipc/Cargo.toml
index c67378d1..9399fa23 100644
--- a/ipc/Cargo.toml
+++ b/ipc/Cargo.toml
@@ -31,6 +31,7 @@ lalrpop-util = "0.17"
libc = "0.2.33"
memsec = "0.5.6"
rand = "0.6"
+tempfile = "3.0"
tokio = "0.1"
tokio-core = "0.1"
tokio-io = "0.1.4"
diff --git a/ipc/examples/gpg-agent-client.rs b/ipc/examples/gpg-agent-client.rs
new file mode 100644
index 00000000..c586cfca
--- /dev/null
+++ b/ipc/examples/gpg-agent-client.rs
@@ -0,0 +1,38 @@
+/// Connects to and sends commands to gpg-agent.
+
+extern crate futures;
+use futures::future::Future;
+use futures::stream::Stream;
+extern crate clap;
+extern crate sequoia_ipc as ipc;
+use ipc::gnupg::{Context, Agent};
+
+fn main() {
+ let matches = clap::App::new("gpg-agent-client")
+ .version(env!("CARGO_PKG_VERSION"))
+ .about("Connects to and sends commands to gpg-agent.")
+ .arg(clap::Arg::with_name("homedir").value_name("PATH")
+ .long("homedir")
+ .help("Use this GnuPG home directory, default: $GNUPGHOME"))
+ .arg(clap::Arg::with_name("commands").value_name("COMMAND")
+ .required(true)
+ .multiple(true)
+ .help("Commands to send to the server"))
+ .get_matches();
+
+ let ctx = if let Some(homedir) = matches.value_of("homedir") {
+ Context::with_homedir(homedir).unwrap()
+ } else {
+ Context::new().unwrap()
+ };
+ let mut agent = Agent::connect(&ctx).wait().unwrap();
+
+ for command in matches.values_of("commands").unwrap() {
+ eprintln!("> {}", command);
+ agent.send(command).unwrap();
+ agent.by_ref().for_each(|response| {
+ eprintln!("< {:?}", response);
+ Ok(())
+ }).wait().unwrap();
+ }
+}
diff --git a/ipc/examples/gpg-agent-decrypt.rs b/ipc/examples/gpg-agent-decrypt.rs
new file mode 100644
index 00000000..2e423f12
--- /dev/null
+++ b/ipc/examples/gpg-agent-decrypt.rs
@@ -0,0 +1,161 @@
+/// Decrypts data using the openpgp crate and secrets in gpg-agent.
+
+use std::collections::HashMap;
+use std::io;
+
+extern crate clap;
+extern crate sequoia_openpgp as openpgp;
+extern crate sequoia_ipc as ipc;
+
+use openpgp::crypto::SessionKey;
+use openpgp::constants::SymmetricAlgorithm;
+use openpgp::parse::{
+ Parse,
+ stream::{
+ DecryptionHelper,
+ Decryptor,
+ VerificationHelper,
+ VerificationResult,
+ MessageStructure,
+ MessageLayer,
+ },
+};
+use ipc::gnupg::{Context, KeyPair};
+
+fn main() {
+ let matches = clap::App::new("gpg-agent-decrypt")
+ .version(env!("CARGO_PKG_VERSION"))
+ .about("Connects to gpg-agent and decrypts a message.")
+ .arg(clap::Arg::with_name("homedir").value_name("PATH")
+ .long("homedir")
+ .help("Use this GnuPG home directory, default: $GNUPGHOME"))
+ .arg(clap::Arg::with_name("tpk").value_name("TPK")
+ .required(true)
+ .multiple(true)
+ .help("Public part of the secret keys managed by gpg-agent"))
+ .get_matches();
+
+ let ctx = if let Some(homedir) = matches.value_of("homedir") {
+ Context::with_homedir(homedir).unwrap()
+ } else {
+ Context::new().unwrap()
+ };
+
+ // Read the TPKs from the given files.
+ let tpks =
+ matches.values_of("tpk").expect("required").map(|f| {
+ openpgp::TPK::from_file(f)
+ .expect("Failed to read key")
+ }).collect();
+
+ // Now, create a decryptor with a helper using the given TPKs.
+ let mut decryptor =
+ Decryptor::from_reader(io::stdin(), Helper::new(&ctx, tpks), None)
+ .unwrap();
+
+ // Finally, stream the decrypted data to stdout.
+ io::copy(&mut decryptor, &mut io::stdout())
+ .expect("Decryption failed");
+}
+
+/// This helper provides secrets for the decryption, fetches public
+/// keys for the signature verification and implements the
+/// verification policy.
+struct Helper<'a> {
+ ctx: &'a Context,
+ keys: HashMap<openpgp::KeyID, openpgp::packet::Key>,
+}
+
+impl<'a> Helper<'a> {
+ /// Creates a Helper for the given TPKs with appropriate secrets.
+ fn new(ctx: &'a Context, tpks: Vec<openpgp::TPK>) -> Self {
+ // Map (sub)KeyIDs to secrets.
+ let mut keys = HashMap::new();
+ for tpk in tpks {
+ for (sig, _, key) in tpk.keys_all() {
+ if sig.map(|s| (s.key_flags().can_encrypt_at_rest()
+ || s.key_flags().can_encrypt_for_transport()))
+ .unwrap_or(false)
+ {
+ keys.insert(key.keyid(), key.clone());
+ }
+ }
+ }
+
+ Helper { ctx, keys, }
+ }
+}
+
+impl<'a> DecryptionHelper for Helper<'a> {
+ fn decrypt<D>(&mut self,
+ pkesks: &[openpgp::packet::PKESK],
+ _skesks: &[openpgp::packet::SKESK],
+ mut decrypt: D)
+ -> openpgp::Result<Option<openpgp::Fingerprint>>
+ where D: FnMut(SymmetricAlgorithm, &SessionKey) -> openpgp::Result<()>
+ {
+ // Try each PKESK until we succeed.
+ for pkesk in pkesks {
+ if let Some(key) = self.keys.get(pkesk.recipient()) {
+ let mut pair = KeyPair::new(self.ctx, key)?;
+ if let Ok(_) = pkesk.decrypt(&mut pair)
+ .and_then(|(algo, session_key)| decrypt(algo, &session_key))
+ {
+ break;
+ }
+ }
+ }
+ // XXX: In production code, return the Fingerprint of the
+ // recipient's TPK here
+ Ok(None)
+ }
+}
+
+impl<'a> VerificationHelper for Helper<'a> {
+ fn get_public_keys(&mut self, _ids: &[openpgp::KeyID])
+ -> failure::Fallible<Vec<openpgp::TPK>> {
+ Ok(Vec::new()) // Feed the TPKs to the verifier here.
+ }
+ fn check(&mut self, structure: &MessageStructure)
+ -> failure::Fallible<()> {
+ use self::VerificationResult::*;
+ for layer in structure.iter() {
+ match layer {
+ MessageLayer::Compression { algo } =>
+ eprintln!("Compressed using {}", algo),
+ MessageLayer::Encryption { sym_algo, aead_algo } =>
+ if let Some(aead_algo) = aead_algo {
+ eprintln!("Encrypted and protected using {}/{}",
+ sym_algo, aead_algo);
+ } else {
+ eprintln!("Encrypted using {}", sym_algo);
+ },
+ MessageLayer::SignatureGroup { ref results } =>
+ for result in results {
+ match result {
+ GoodChecksum(ref sig, ..) => {
+ let issuer = sig.issuer()
+ .expect("good checksum has an issuer");
+ eprintln!("Good signature from {}", issuer);
+ },
+ MissingKey(ref sig) => {
+ let issuer = sig.issuer()
+ .expect("missing key checksum has an \
+ issuer");
+ eprintln!("No key to check signature from {}",
+ issuer);
+ },
+ BadChecksum(ref sig) =>
+ if let Some(issuer) = sig.issuer() {
+ eprintln!("Bad signature from {}", issuer);
+ } else {
+ eprintln!("Bad signature without issuer \
+ information");
+ },
+ }
+ }
+ }
+ }
+ Ok(()) // Implement your verification policy here.
+ }
+}
diff --git a/ipc/examples/gpg-agent-sign.rs b/ipc/examples/gpg-agent-sign.rs
new file mode 100644
index 00000000..8525abbb
--- /dev/null
+++ b/ipc/examples/gpg-agent-sign.rs
@@ -0,0 +1,76 @@
+/// Signs data using the openpgp crate and secrets in gpg-agent.
+
+use std::io;
+
+extern crate clap;
+extern crate sequoia_openpgp as openpgp;
+extern crate sequoia_ipc as ipc;
+
+use openpgp::armor;
+use openpgp::constants::DataFormat;
+use openpgp::parse::Parse;
+use openpgp::serialize::stream::{Message, LiteralWriter, Signer};
+use ipc::gnupg::{Context, KeyPair};
+
+fn main() {
+ let matches = clap::App::new("gpg-agent-sign")
+ .version(env!("CARGO_PKG_VERSION"))
+ .about("Connects to gpg-agent and creates a dummy signature.")
+ .arg(clap::Arg::with_name("homedir").value_name("PATH")
+ .long("homedir")
+ .help("Use this GnuPG home directory, default: $GNUPGHOME"))
+ .arg(clap::Arg::with_name("tpk").value_name("TPK")
+ .required(true)
+ .multiple(true)
+ .help("Public part of the secret keys managed by gpg-agent"))
+ .get_matches();
+
+ let ctx = if let Some(homedir) = matches.value_of("homedir") {
+ Context::with_homedir(homedir).unwrap()
+ } else {
+ Context::new().unwrap()
+ };
+
+ // Read the TPKs from the given files.
+ let tpks =
+ matches.values_of("tpk").expect("required").map(|f| {
+ openpgp::TPK::from_file(f)
+ .expect("Failed to read key")
+ }).collect::<Vec<_>>();
+
+ // Construct a KeyPair for every signing-capable (sub)key.
+ let mut keypairs = tpks.iter().flat_map(|tpk| tpk.keys_valid().signing_capable().filter_map(|(_, _, key)| {
+ KeyPair::new(&ctx, key).ok()
+ })).collect::<Vec<KeyPair>>();
+
+ // Well, this is awkward...
+ let signers = keypairs.iter_mut()
+ .map(|s| -> &mut dyn openpgp::crypto::Signer { s })
+ .collect();
+
+ // Compose a writer stack corresponding to the output format and
+ // packet structure we want. First, we want the output to be
+ // ASCII armored.
+ let sink = armor::Writer::new(io::stdout(), armor::Kind::Message, &[])
+ .expect("Failed to create an armored writer.");
+
+ // Stream an OpenPGP message.
+ let message = Message::new(sink);
+
+ // Now, create a signer that emits a signature.
+ let signer = Signer::new(message, signers, None)
+ .expect("Failed to create signer");
+
+ // Then, create a literal writer to wrap the data in a literal
+ // message packet.
+ let mut literal = LiteralWriter::new(signer, DataFormat::Binary, None, None)
+ .expect("Failed to create literal writer");
+
+ // Copy all the data.
+ io::copy(&mut io::stdin(), &mut literal)
+ .expect("Failed to sign data");
+
+ // Finally, teardown the stack to ensure all the data is written.
+ literal.finalize()
+ .expect("Failed to write data");
+}
diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs
new file mode 100644
index 00000000..4236c9c4
--- /dev/null
+++ b/ipc/src/gnupg.rs
@@ -0,0 +1,764 @@
+//! GnuPG RPC support.
+
+#![warn(missing_docs)]
+
+use std::collections::BTreeMap;
+use std::ops::{Deref, DerefMut};
+use std::path::{Path, PathBuf};
+use std::process::Command;
+
+use futures::{Async, Future, Stream};
+
+extern crate libc;
+extern crate tempfile;
+
+use openpgp::constants::HashAlgorithm;
+use openpgp::conversions::hex;
+use openpgp::crypto;
+use openpgp::crypto::sexp::Sexp;
+use openpgp::parse::Parse;
+use openpgp::serialize::Serialize;
+
+use Result;
+use assuan;
+
+/// A GnuPG context.
+#[derive(Debug)]
+pub struct Context {
+ homedir: Option<PathBuf>,
+ components: BTreeMap<String, PathBuf>,
+ directories: BTreeMap<String, PathBuf>,
+ sockets: BTreeMap<String, PathBuf>,
+ #[allow(dead_code)] // We keep it around for the cleanup.
+ ephemeral: Option<tempfile::TempDir>,
+}
+
+impl Context {
+ /// Creates a new context for the default GnuPG home directory.
+ pub fn new() -> Result<Self> {
+ Self::make(None, None)
+ }
+
+ /// Creates a new context for the given GnuPG home directory.
+ pub fn with_homedir<P>(homedir: P) -> Result<Self>
+ where P: AsRef<Path>
+ {
+ 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> {
+ Self::make(None, Some(tempfile::tempdir()?))
+ }
+
+ fn make(homedir: Option<&Path>, ephemeral: Option<tempfile::TempDir>)
+ -> Result<Self> {
+ let mut components: BTreeMap<String, PathBuf> = Default::default();
+ let mut directories: BTreeMap<String, PathBuf> = Default::default();
+ let mut sockets: BTreeMap<String, PathBuf> = Default::default();
+
+ let homedir: Option<PathBuf> =
+ 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<PathBuf>, arguments: &[&str], nfields: usize)
+ -> Result<Vec<Vec<Vec<u8>>>> {
+ 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| -> failure::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::<Vec<_>>();
+
+ 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<C>(&self, component: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ 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<C>(&self, directory: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ 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<C>(&self, socket: C) -> Result<&Path>
+ where C: AsRef<str>
+ {
+ 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 = failure::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<Async<Option<Self::Item>>, 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<Item = Self, Error = failure::Error> + '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<P>(path: P)
+ -> impl Future<Item = Self, Error = failure::Error>
+ where P: AsRef<Path>
+ {
+ 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>(&'a mut self, key: &'a openpgp::packet::Key,
+ algo: HashAlgorithm, digest: &'a [u8])
+ -> impl Future<Item = crypto::mpis::Signature,
+ Error = failure::Error> + 'a
+ {
+ SigningRequest::new(&mut self.c, key, algo, digest)
+ }
+
+ /// Decrypts `ciphertext` using `key` with the secret bits managed
+ /// by the agent.
+ pub fn decrypt<'a>(&'a mut self, key: &'a openpgp::packet::Key,
+ ciphertext: &'a crypto::mpis::Ciphertext)
+ -> impl Future<Item = crypto::SessionKey,
+ Error = failure::Error> + 'a
+ {
+ DecryptionRequest::new(&mut self.c, key, ciphertext)
+ }
+
+ /// Computes options that we want to communicate.
+ fn options() -> Vec<String> {
+ use std::env::var;
+ use std::ffi::CStr;
+
+ let mut r = Vec::new();
+
+ if let Ok(tty) = var("GPG_TTY") {
+ r.push(format!("OPTION ttyname={}", tty));
+ } else {
+ unsafe {
+ 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> {
+ c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::Key,
+ algo: HashAlgorithm,
+ digest: &'c [u8],
+ options: Vec<String>,
+ state: SigningRequestState,
+}
+
+impl<'a, 'b, 'c> SigningRequest<'a, 'b, 'c> {
+ fn new(c: &'a mut assuan::Client,
+ key: &'b openpgp::packet::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<u8>),
+}
+
+/// Returns a convenient Err value for use in the state machines
+/// below.
+fn operation_failed<T>(message: &Option<String>) -> Result<T> {
+ 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<T>(response: &assuan::Response) -> Result<T> {
+ Err(Error::ProtocolError(
+ format!("Got unexpected response {:?}", response))
+ .into())
+}
+
+impl<'a, 'b, 'c> Future for SigningRequest<'a, 'b, 'c> {
+ type Item = crypto::mpis::Signature;
+ type Error = failure::Error;
+
+ fn poll(&mut self) -> std::result::Result<Async<Self::Item>, Self::Error> {
+ use self::SigningRequestState::*;
+
+ loop {
+ match self.state {
+ Start => {
+ if self.options.is_empty() {
+ self.c.send(format!("SIGKEY {}",
+ self.key.mpis().keygrip()?))?;
+ 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 {}",
+ self.key.mpis().keygrip()?))?;
+ self.state = SigKey;
+ }
+ },
+ Async::NotReady =>
+ return Ok(Async::NotReady),
+ },
+
+ SigKey => match self.c.poll()? {
+ Async::Ready(Some(r)) => match r {
+