diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2020-08-17 12:44:22 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2020-08-17 16:19:24 +0200 |
commit | bc93b8a039753a626406f15a346f84ecaed49583 (patch) | |
tree | de89a8d571aac56f402baf2d6327abb838c64ce6 /ipc | |
parent | 5c80cd51fa5453be3207bb2c9491fbe8d9c0afe3 (diff) |
openpgp: Move crypto::sexp to the ipc crate.
- This is only used to communicate with the GnuPG agent, so it
should not be in the openpgp crate.
Diffstat (limited to 'ipc')
-rw-r--r-- | ipc/Cargo.toml | 3 | ||||
-rw-r--r-- | ipc/build.rs | 36 | ||||
-rw-r--r-- | ipc/src/gnupg.rs | 2 | ||||
-rw-r--r-- | ipc/src/lib.rs | 26 | ||||
-rw-r--r-- | ipc/src/sexp.rs | 436 | ||||
-rw-r--r-- | ipc/src/sexp/parse/grammar.lalrpop | 64 | ||||
-rw-r--r-- | ipc/src/sexp/parse/lexer.rs | 153 | ||||
-rw-r--r-- | ipc/src/sexp/parse/mod.rs | 144 | ||||
-rw-r--r-- | ipc/src/sexp/serialize.rs | 173 | ||||
-rw-r--r-- | ipc/src/tests.rs | 41 | ||||
-rw-r--r-- | ipc/tests/data/sexp/dsa-signature.sexp | 1 | ||||
-rw-r--r-- | ipc/tests/data/sexp/ecdsa-signature.sexp | 2 | ||||
-rw-r--r-- | ipc/tests/data/sexp/eddsa-signature.sexp | 1 | ||||
-rw-r--r-- | ipc/tests/data/sexp/rsa-signature.sexp | 3 |
14 files changed, 1084 insertions, 1 deletions
diff --git a/ipc/Cargo.toml b/ipc/Cargo.toml index b21fcfd1..ef5ed0ea 100644 --- a/ipc/Cargo.toml +++ b/ipc/Cargo.toml @@ -24,10 +24,12 @@ sequoia-openpgp = { path = "../openpgp", version = "0.18" } sequoia-core = { path = "../core", version = "0.18" } anyhow = "1" +buffered-reader = { path = "../buffered-reader", version = "0.18", default-features = false } capnp-rpc = "0.10" fs2 = "0.4.2" futures = "0.1" lalrpop-util = "0.17" +lazy_static = "1.3" libc = "0.2.33" memsec = "0.5.6" rand = { version = "0.7", default-features = false } @@ -52,3 +54,4 @@ lalrpop = "0.17" [dev-dependencies] clap = "2.32.0" +quickcheck = { version = "0.9", default-features = false } diff --git a/ipc/build.rs b/ipc/build.rs index 23c7d3f8..5c8a9ffb 100644 --- a/ipc/build.rs +++ b/ipc/build.rs @@ -1,5 +1,41 @@ +use std::env; +use std::fs; +use std::io::{self, Write}; +use std::path::PathBuf; extern crate lalrpop; fn main() { lalrpop::process_root().unwrap(); + include_test_data().unwrap(); +} + +/// Builds the index of the test data for use with the `::tests` +/// module. +fn include_test_data() -> io::Result<()> { + let cwd = env::current_dir()?; + let mut sink = fs::File::create( + PathBuf::from(env::var_os("OUT_DIR").unwrap()) + .join("tests.index.rs.inc")).unwrap(); + + writeln!(&mut sink, "{{")?; + let mut dirs = vec![PathBuf::from("tests/data")]; + while let Some(dir) = dirs.pop() { + println!("rerun-if-changed={}", dir.to_str().unwrap()); + for entry in fs::read_dir(dir).unwrap() { + let entry = entry?; + let path = entry.path(); + if path.is_file() { + writeln!( + &mut sink, " add!({:?}, {:?});", + path.components().skip(2) + .map(|c| c.as_os_str().to_str().expect("valid UTF-8")) + .collect::<Vec<_>>().join("/"), + cwd.join(path))?; + } else if path.is_dir() { + dirs.push(path.clone()); + } + } + } + writeln!(&mut sink, "}}")?; + Ok(()) } diff --git a/ipc/src/gnupg.rs b/ipc/src/gnupg.rs index 4b565553..b81e0927 100644 --- a/ipc/src/gnupg.rs +++ b/ipc/src/gnupg.rs @@ -14,13 +14,13 @@ use sequoia_openpgp as openpgp; use openpgp::types::HashAlgorithm; use openpgp::fmt::hex; use openpgp::crypto; -use openpgp::crypto::sexp::Sexp; use openpgp::packet::prelude::*; use openpgp::parse::Parse; use openpgp::serialize::Serialize; use crate::Result; use crate::assuan; +use crate::sexp::Sexp; /// A GnuPG context. #[derive(Debug)] diff --git a/ipc/src/lib.rs b/ipc/src/lib.rs index 7e6a62ab..f0d2c2b6 100644 --- a/ipc/src/lib.rs +++ b/ipc/src/lib.rs @@ -65,9 +65,35 @@ use std::thread; use sequoia_openpgp as openpgp; use sequoia_core as core; +// Turns an `if let` into an expression so that it is possible to do +// things like: +// +// ```rust,nocompile +// if destructures_to(Foo::Bar(_) = value) +// || destructures_to(Foo::Bam(_) = value) { ... } +// ``` +// TODO: Replace with `std::matches!` once MSRV is bumped to 1.42. +#[cfg(test)] +macro_rules! destructures_to { + ( $error: pat = $expr:expr ) => { + { + let x = $expr; + if let $error = x { + true + } else { + false + } + } + }; +} + #[macro_use] mod trace; pub mod assuan; pub mod gnupg; +pub mod sexp; + +#[cfg(test)] +mod tests; macro_rules! platform { { unix => { $($unix:tt)* }, windows => { $($windows:tt)* } } => { diff --git a/ipc/src/sexp.rs b/ipc/src/sexp.rs new file mode 100644 index 00000000..38429557 --- /dev/null +++ b/ipc/src/sexp.rs @@ -0,0 +1,436 @@ +//! *S-Expressions* for communicating cryptographic primitives. +//! +//! *S-Expressions* as described in the internet draft [S-Expressions], +//! are a way to communicate cryptographic primitives like keys, +//! signatures, and ciphertexts between agents or implementations. +//! +//! [S-Expressions]: https://people.csail.mit.edu/rivest/Sexp.txt + +use std::convert::TryFrom; +use std::fmt; +use std::ops::Deref; + +#[cfg(test)] +use quickcheck::{Arbitrary, Gen}; + +use sequoia_openpgp as openpgp; +use openpgp::crypto::{mpi, SessionKey}; +use openpgp::crypto::mem::Protected; + +use openpgp::Error; +use openpgp::Result; + +mod parse; +mod serialize; + +/// An *S-Expression*. +/// +/// An *S-Expression* is either a string, or a list of *S-Expressions*. +#[derive(Clone, PartialEq, Eq)] +pub enum Sexp { + /// Just a string. + String(String_), + /// A list of *S-Expressions*. + List(Vec<Sexp>), +} + +impl fmt::Debug for Sexp { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Sexp::String(ref s) => s.fmt(f), + Sexp::List(ref l) => l.fmt(f), + } + } +} + +impl Sexp { + /// Completes the decryption of this S-Expression representing a + /// wrapped session key. + /// + /// Such an expression is returned from gpg-agent's `PKDECRYPT` + /// command. `padding` must be set according to the status + /// messages sent. + pub fn finish_decryption<R>(&self, + recipient: &openpgp::packet::Key< + openpgp::packet::key::PublicParts, R>, + ciphertext: &mpi::Ciphertext, + padding: bool) + -> Result<SessionKey> + where R: openpgp::packet::key::KeyRole + { + use openpgp::crypto::mpi::PublicKey; + let not_a_session_key = || -> anyhow::Error { + Error::MalformedMPI( + format!("Not a session key: {:?}", self)).into() + }; + + let value = self.get(b"value")?.ok_or_else(not_a_session_key)? + .into_iter().nth(0).ok_or_else(not_a_session_key)?; + + match value { + Sexp::String(ref s) => match recipient.mpis() { + PublicKey::RSA { .. } | PublicKey::ElGamal { .. } if padding => + { + // The session key is padded. The format is + // described in g10/pubkey-enc.c (note that we, + // like GnuPG 2.2, only support the new encoding): + // + // * Later versions encode the DEK like this: + // * + // * 0 2 RND(n bytes) [...] + // * + // * (mpi_get_buffer already removed the leading zero). + // * + // * RND are non-zero random bytes. + let mut s = &s[..]; + + // The leading 0 may or may not be swallowed along + // the way due to MPI encoding. + if s[0] == 0 { + s = &s[1..]; + } + + // Version. + if s[0] != 2 { + return Err(Error::MalformedMPI( + format!("DEK encoding version {} not understood", + s[0])).into()); + } + + // Skip non-zero bytes. + while s.len() > 0 && s[0] > 0 { + s = &s[1..]; + } + + if s.len() == 0 { + return Err(Error::MalformedMPI( + "Invalid DEK encoding, no zero found".into()) + .into()); + } + + // Skip zero. + s = &s[1..]; + + Ok(s.to_vec().into()) + }, + + PublicKey::RSA { .. } | PublicKey::ElGamal { .. } => { + // The session key is not padded. Currently, this + // happens if the session key is decrypted using + // scdaemon. + assert!(! padding); // XXX: Don't assert that. + Ok(s.to_vec().into()) + }, + + PublicKey::ECDH { curve, .. } => { + // The shared point has been computed by the + // remote agent. The shared point is not padded. + let s_: mpi::ProtectedMPI = s.to_vec().into(); + #[allow(non_snake_case)] + let S: Protected = s_.decode_point(curve)?.0.into(); + // XXX: Erase shared point from s. + + // Now finish the decryption. + openpgp::crypto::ecdh::decrypt_shared(recipient, &S, ciphertext) + }, + + _ => + Err(Error::InvalidArgument( + format!("Don't know how to handle key {:?}", recipient)) + .into()), + } + Sexp::List(..) => Err(not_a_session_key()), + } + } + + /// Parses this s-expression to a signature. + /// + /// Such an expression is returned from gpg-agent's `PKSIGN` + /// command. + pub fn to_signature(&self) -> Result<mpi::Signature> { + let not_a_signature = || -> anyhow::Error { + Error::MalformedMPI( + format!("Not a signature: {:?}", self)).into() + }; + + let sig = self.get(b"sig-val")?.ok_or_else(not_a_signature)? + .into_iter().nth(0).ok_or_else(not_a_signature)?; + + if let Some(param) = sig.get(b"eddsa")? { + let r = param.iter().find_map(|p| { + p.get(b"r").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + let s = param.iter().find_map(|p| { + p.get(b"s").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + Ok(mpi::Signature::EdDSA { + r: mpi::MPI::new(&r), + s: mpi::MPI::new(&s), + }) + } else if let Some(param) = sig.get(b"ecdsa")? { + let r = param.iter().find_map(|p| { + p.get(b"r").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + let s = param.iter().find_map(|p| { + p.get(b"s").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + Ok(mpi::Signature::ECDSA { + r: mpi::MPI::new(&r), + s: mpi::MPI::new(&s), + }) + } else if let Some(param) = sig.get(b"rsa")? { + let s = param.iter().find_map(|p| { + p.get(b"s").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + Ok(mpi::Signature::RSA { + s: mpi::MPI::new(&s), + }) + } else if let Some(param) = sig.get(b"dsa")? { + let r = param.iter().find_map(|p| { + p.get(b"r").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + let s = param.iter().find_map(|p| { + p.get(b"s").ok().unwrap_or_default() + .and_then(|l| l.get(0).and_then(Sexp::string).cloned()) + }).ok_or_else(not_a_signature)?; + Ok(mpi::Signature::DSA { + r: mpi::MPI::new(&r), + s: mpi::MPI::new(&s), + }) + } else { + Err(Error::MalformedMPI( + format!("Unknown signature sexp: {:?}", self)).into()) + } + } + + /// Casts this to a string. + pub fn string(&self) -> Option<&String_> { + match self { + Sexp::String(ref s) => Some(s), + _ => None, + } + } + + /// Casts this to a list. + pub fn list(&self) -> Option<&[Sexp]> { + match self { + Sexp::List(ref s) => Some(s.as_slice()), + _ => None, + } + } + + /// Given an alist, selects by key and returns the value. + fn get(&self, key: &[u8]) -> Result<Option<Vec<Sexp>>> { + match self { + Sexp::List(ref ll) => match ll.get(0) { + Some(Sexp::String(ref tag)) => + if tag.deref() == key { + Ok(Some(ll[1..].iter().cloned().collect())) + } else { + Ok(None) + } + _ => + Err(Error::InvalidArgument( + format!("Malformed alist: {:?}", ll)).into()), + }, + _ => + Err(Error::InvalidArgument( + format!("Malformed alist: {:?}", self)).into()), + } + } +} + +impl TryFrom<&mpi::Ciphertext> for Sexp { + type Error = anyhow::Error; + + /// Constructs an S-Expression representing `ciphertext`. + /// + /// The resulting expression is suitable for gpg-agent's `INQUIRE + /// CIPHERTEXT` inquiry. + fn try_from(ciphertext: &mpi::Ciphertext) -> Result<Self> { + use openpgp::crypto::mpi::Ciphertext::*; + match ciphertext { + RSA { ref c } => + Ok(Sexp::List(vec![ + Sexp::String("enc-val".into()), + Sexp::List(vec![ + Sexp::String("rsa".into()), + Sexp::List(vec![ + Sexp::String("a".into()), + Sexp::String(c.value().into())])])])), + + &ElGamal { ref e, ref c } => + Ok(Sexp::List(vec![ + Sexp::String("enc-val".into()), + Sexp::List(vec![ + Sexp::String("elg".into()), + Sexp::List(vec![ + Sexp::String("a".into()), + Sexp::String(e.value().into())]), + Sexp::List(vec![ + Sexp::String("b".into()), + Sexp::String(c.value().into())])])])), + + &ECDH { ref e, ref key } => + Ok(Sexp::List(vec![ + Sexp::String("enc-val".into()), + Sexp::List(vec![ + Sexp::String("ecdh".into()), + Sexp::List(vec![ + Sexp::String("s".into()), + Sexp::String(key.as_ref().into())]), + Sexp::List(vec![ + Sexp::String("e".into()), + Sexp::String(e.value().into())])])])), + + &Unknown { .. } => + Err(Error::InvalidArgument( + format!("Don't know how to convert {:?}", ciphertext)) + .into()), + + __Nonexhaustive => unreachable!(), + } + } +} + +#[cfg(any(test, feature = "quickcheck"))] +impl Arbitrary for Sexp { + fn arbitrary<G: Gen>(g: &mut G) -> Self { + if f32::arbitrary(g) < 0.7 { + Sexp::String(String_::arbitrary(g)) + } else { + let mut v = Vec::new(); + for _ in 0..usize::arbitrary(g) % 3 { + v.push(Sexp::arbitrary(g)); + } + Sexp::List(v) + } + } +} + +/// A string. +/// +/// A string can optionally have a display hint. +#[derive(Clone, PartialEq, Eq)] +pub struct String_(Box<[u8]>, Option<Box<[u8]>>); + +impl fmt::Debug for String_ { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + fn bstring(f: &mut fmt::Formatter, buf: &[u8]) -> fmt::Result { + write!(f, "b\"")?; + for &b in buf { + match b { + 0..=31 | 128..=255 => + write!(f, "\\x{:02x}", b)?, + 0x22 => // " + write!(f, "\\\"")?, + 0x5c => // \ + write!(f, "\\\\")?, + _ => + write!(f, "{}", b as char)?, + } + } + write!(f, "\"") + } + + if let Some(hint) = self.display_hint() { + write!(f, "[")?; + bstring(f, hint)?; + write!(f, "]")?; + } + bstring(f, &self.0) + } +} + +impl String_ { + /// Constructs a new *Simple String*. + pub fn new<S>(s: S) -> Self + where S: Into<Box<[u8]>> + { + Self(s.into(), None) + } + + /// Constructs a new *String*. + pub fn with_display_hint<S, T>(s: S, display_hint: T) -> Self + where S: Into<Box<[u8]>>, T: Into<Box<[u8]>> + { + Self(s.into(), Some(display_hint.into())) + } + + /// Gets a reference to this *String*'s display hint, if any. + pub fn display_hint(&self) -> Option<&[u8]> { + self.1.as_ref().map(|b| b.as_ref()) + } +} + +impl From<&str> for String_ { + fn from(b: &str) -> Self { + Self::new(b.as_bytes().to_vec()) + } +} + +impl From<&[u8]> for String_ { + fn from(b: &[u8]) -> Self { + Self::new(b.to_vec()) + } +} + +impl Deref for String_ { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[cfg(any(test, feature = "quickcheck"))] +impl Arbitrary for String_ { + fn arbitrary<G: Gen>(g: &mut G) -> Self { + if bool::arbitrary(g) { + Self::new(Vec::arbitrary(g).into_boxed_slice()) + } else { + Self::with_display_hint(Vec::arbitrary(g).into_boxed_slice(), + Vec::arbitrary(g).into_boxed_slice()) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use openpgp::parse::Parse; + use openpgp::serialize::Serialize; + + quickcheck::quickcheck! { + fn roundtrip(s: Sexp) -> bool { + let mut buf = Vec::new(); + s.serialize(&mut buf).unwrap(); + let t = Sexp::from_bytes(&buf).unwrap(); + assert_eq!(s, t); + true + } + } + + #[test] + fn to_signature() { + use openpgp::crypto::mpi::Signature::*; + assert!(destructures_to!(DSA { .. } = Sexp::from_bytes( + crate::tests::file("sexp/dsa-signature.sexp")).unwrap() + .to_signature().unwrap())); + assert!(destructures_to!(ECDSA { .. } = Sexp::from_bytes( + crate::tests::file("sexp/ecdsa-signature.sexp")).unwrap() + .to_signature().unwrap())); + assert!(destructures_to!(EdDSA { .. } = Sexp::from_bytes( + crate::tests::file("sexp/eddsa-signature.sexp")).unwrap() + .to_signature().unwrap())); + assert!(destructures_to!(RSA { .. } = Sexp::from_bytes( + crate::tests::file("sexp/rsa-signature.sexp")).unwrap() + .to_signature().unwrap())); + } +} diff --git a/ipc/src/sexp/parse/grammar.lalrpop b/ipc/src/sexp/parse/grammar.lalrpop new file mode 100644 index 00000000..9a880361 --- /dev/null +++ b/ipc/src/sexp/parse/grammar.lalrpop @@ -0,0 +1,64 @@ +// -*- mode: Rust; -*- +// +// This implements parsing of [S-Expressions] encoded using the +// canonical and basic transport encoding. +// +// [S-Expressions]: https://people.csail.mit.edu/rivest/Sexp.txt + +use crate::sexp::parse::lexer::{self, LexicalError}; +use crate::sexp::{Sexp, String_}; + +grammar<'input>; + +// For canonical and basic transport: +// +// <sexpr> :: <string> | <list> +pub Sexpr: Sexp = { + String, + List, +}; + +// <list> :: "(" <sexp>* ")" ; +List: Sexp = { + LPAREN <Sexpr*> RPAREN => Sexp::List(<>), +}; + +// <string> :: <display>? <simple-string> ; +String: Sexp = { + <d: Display> <s: SimpleString> => + Sexp::String(String_::with_display_hint(s, d)), + <s: SimpleString> => + Sexp::String(String_::new(s)), +}; + +// <simple-string> :: <raw> ; +SimpleString: Vec<u8> = + RAW => if let lexer::Token::RAW(r) = <> { + r.iter().cloned().collect::<Vec<_>>() + } else { + unreachable!("Production matched on Token::RAW") + }; + +// <display> :: "[" <simple-string> "]" ; +Display = LBRACKET <SimpleString> RBRACKET; + +// <raw> :: <decimal> ":" <bytes> ; +// <decimal-digit> :: "0" | ... | "9" ; +// <decimal> :: <decimal-digit>+ ; +// -- decimal numbers should have no unnecessary leading zeros +// <bytes> -- any string of bytes, of the indicated length +// +// RAW is handled in the lexer. + +extern { + type Location = usize; + type Error = LexicalError; + + enum lexer::Token<'input> { + LPAREN => lexer::Token::LPAREN, + RPAREN => lexer::Token::RPAREN, + LBRACKET => lexer::Token::LBRACKET, + RBRACKET => lexer::Token::RBRACKET, + RAW => lexer::Token::RAW(_), + } +} diff --git a/ipc/src/sexp/parse/lexer.rs b/ipc/src/sexp/parse/lexer.rs new file mode 100644 index 00000000..0a570475 --- /dev/null +++ b/ipc/src/sexp/parse/lexer.rs @@ -0,0 +1,153 @@ +use std::fmt; + +// Controls tracing in the lexer. +const TRACE: bool = false; + +#[derive(Clone, PartialEq, Eq, Debug)] +pub enum LexicalError { + LengthOverflow(String), + TruncatedInput(String), + UnexpectedCharacter(String), +} + +impl fmt::Display for LexicalError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +pub type Spanned<Token, Loc, LexicalError> + = Result<(Loc, Token, Loc), LexicalError>; + +// The type of the parser's input. +// +// The parser iterators over tuples consisting of the token's starting +// position, the token itself, and the token's ending position. +pub(crate) type LexerItem<Token, Loc, LexicalError> + = Spanned<Token, Loc, LexicalError>; + +#[derive(Debug, Clone, PartialEq)] +pub enum Token<'a> { + LPAREN, + RPAREN, + LBRACKET, + RBRACKET, + RAW(&'a [u8]), +} + +impl<'a> fmt::Display for Token<'a> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +impl<'a> From<Token<'a>> for String { + fn from(t: Token<'a>) -> String { + use self::Token::*; + match t { + LPAREN => '('.to_string(), + RPAREN => ')'.to_string(), + LBRACKET => |