From bc93b8a039753a626406f15a346f84ecaed49583 Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Mon, 17 Aug 2020 12:44:22 +0200 Subject: 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. --- ipc/Cargo.toml | 3 + ipc/build.rs | 36 +++ ipc/src/gnupg.rs | 2 +- ipc/src/lib.rs | 26 ++ ipc/src/sexp.rs | 436 +++++++++++++++++++++++++++++++ ipc/src/sexp/parse/grammar.lalrpop | 64 +++++ ipc/src/sexp/parse/lexer.rs | 153 +++++++++++ ipc/src/sexp/parse/mod.rs | 144 ++++++++++ ipc/src/sexp/serialize.rs | 173 ++++++++++++ ipc/src/tests.rs | 41 +++ ipc/tests/data/sexp/dsa-signature.sexp | 1 + ipc/tests/data/sexp/ecdsa-signature.sexp | 2 + ipc/tests/data/sexp/eddsa-signature.sexp | 1 + ipc/tests/data/sexp/rsa-signature.sexp | 3 + 14 files changed, 1084 insertions(+), 1 deletion(-) create mode 100644 ipc/src/sexp.rs create mode 100644 ipc/src/sexp/parse/grammar.lalrpop create mode 100644 ipc/src/sexp/parse/lexer.rs create mode 100644 ipc/src/sexp/parse/mod.rs create mode 100644 ipc/src/sexp/serialize.rs create mode 100644 ipc/src/tests.rs create mode 100644 ipc/tests/data/sexp/dsa-signature.sexp create mode 100644 ipc/tests/data/sexp/ecdsa-signature.sexp create mode 100644 ipc/tests/data/sexp/eddsa-signature.sexp create mode 100644 ipc/tests/data/sexp/rsa-signature.sexp (limited to 'ipc') 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::>().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), +} + +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(&self, + recipient: &openpgp::packet::Key< + openpgp::packet::key::PublicParts, R>, + ciphertext: &mpi::Ciphertext, + padding: bool) + -> Result + 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 { + 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>> { + 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 { + 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: &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>); + +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) -> Self + where S: Into> + { + Self(s.into(), None) + } + + /// Constructs a new *String*. + pub fn with_display_hint(s: S, display_hint: T) -> Self + where S: Into>, T: Into> + { + 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: &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: +// +// :: | +pub Sexpr: Sexp = { + String, + List, +}; + +// :: "(" * ")" ; +List: Sexp = { + LPAREN RPAREN => Sexp::List(<>), +}; + +// :: ? ; +String: Sexp = { + => + Sexp::String(String_::with_display_hint(s, d)), + => + Sexp::String(String_::new(s)), +}; + +// :: ; +SimpleString: Vec = + RAW => if let lexer::Token::RAW(r) = <> { + r.iter().cloned().collect::>() + } else { + unreachable!("Production matched on Token::RAW") + }; + +// :: "[" "]" ; +Display = LBRACKET RBRACKET; + +// :: ":" ; +// :: "0" | ... | "9" ; +// :: + ; +// -- decimal numbers should have no unnecessary leading zeros +// -- 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 + = 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 + = Spanned; + +#[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> for String { + fn from(t: Token<'a>) -> String { + use self::Token::*; + match t { + LPAREN => '('.to_string(), + RPAREN => ')'.to_string(), + LBRACKET => '['.to_string(), + RBRACKET => ']'.to_string(), + RAW(b) => format!("{:?}", b), + } + } +} + +impl<'a> Token<'a> { + pub fn to_string(self) -> String { + self.into() + } +} + +#[derive(Debug)] +pub(crate) struct Lexer<'input> { + offset: usize, + input: &'input [u8], +} + +impl<'input> Lexer<'input> { + pub fn new(input: &'input [u8]) -> Self { + Lexer { offset: 0, input } + } +} + +impl<'input> Iterator for Lexer<'input> { + type Item = LexerItem, usize, LexicalError>; + + fn next(&mut self) -> Option { + use self::Token::*; + + tracer!(TRACE, "Lexer::next", 0); + t!("input is {:?}", String::from_utf8_lossy(self.input)); + + let len_token = (|input: &'input [u8]| { + let c = input.iter().next()?; + match *c as char { + '(' => return Some(Ok((1, LPAREN))), + ')' => return Some(Ok((1, RPAREN))), + '[' => return Some(Ok((1, LBRACKET))), + ']' => return Some(Ok((1, RBRACKET))), + '0'..='9' => { + for (i, c) in input.iter().enumerate() { + let offset = i + 1; // Offset in input. + + match *c as char { + '0'..='9' => + (), // Keep consuming all the digits. + ':' => { + let len = std::str::from_utf8(&input[..i]) + .expect("only contains digits"); + if let Ok(l) = len.parse() { + if input.len() - offset < l { + return Some(Err( + LexicalError::TruncatedInput( + format!("Expected {} octets, \ + got {}", l, + input.len() - offset)))); + } + + return Some(Ok((offset + l, RAW( + &input[offset..offset + l])))); + } else { + return Some(Err(LexicalError::LengthOverflow( + format!("{:?} overflows usize", len)))); + } + }, + _ => return + Some(Err(LexicalError::UnexpectedCharacter( + format!("Unexpected character {}, \ + got {:?} so far", + *c as char, &input[..offset])))), + } + } + }, + _ => return Some(Err(LexicalError::UnexpectedCharacter( + format!("Unexpected character {}", *c as char)))), + } + + unreachable!() + })(&self.input)?; + + let (l, token) = match len_token { + Ok(x) => x, + Err(e) => return Some(Err(e)), + }; + self.input = &self.input[l..]; + + let start = self.offset; + let end = start + l; + self.offset += l; + + t!("Returning token at offset {}: '{:?}'", + start, token); + + Some(Ok((start, token, end))) + } +} + +impl<'input> From<&'input [u8]> for Lexer<'input> { + fn from(i: &'input [u8]) -> Lexer<'input> { + Lexer::new(i) + } +} diff --git a/ipc/src/sexp/parse/mod.rs b/ipc/src/sexp/parse/mod.rs new file mode 100644 index 00000000..560f3764 --- /dev/null +++ b/ipc/src/sexp/parse/mod.rs @@ -0,0 +1,144 @@ +//! S-Expression support. +//! +//! 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 std::cmp; +use std::io::{Read, Write}; +use std::path::Path; + +use buffered_reader::{self, BufferedReader}; +use lalrpop_util::{lalrpop_mod, ParseError}; +use sequoia_openpgp as openpgp; +use openpgp::parse::Parse; + +use openpgp::Error; +use crate::Result; +use crate::sexp::Sexp; + +mod lexer; +use self::lexer::Lexer; + +// Load the generated code. +lalrpop_mod!(#[allow(missing_docs, unused_parens)] grammar, "/sexp/parse/grammar.rs"); + +impl<'a> Parse<'a, Sexp> for Sexp { + fn from_reader(reader: R) -> Result { + Self::from_bytes( + buffered_reader::Generic::new(reader, None).data_eof()?) + } + + fn from_file>(path: P) -> Result { + Self::from_bytes( + buffered_reader::File::open(path)?.data_eof()?) + } + + fn from_bytes + ?Sized>(data: &'a D) -> Result { + Self::from_bytes_private(data.as_ref()) + } +} + +impl Sexp { + fn from_bytes_private(data: &[u8]) -> Result { + match self::grammar::SexprParser::new().parse(Lexer::new(data)) { + Ok(r) => Ok(r), + Err(err) => { + let mut msg = Vec::new(); + writeln!(&mut msg, "Parsing: {:?}: {:?}", data, err)?; + if let ParseError::UnrecognizedToken { + token: (start, _, end), .. + } = err + { + writeln!(&mut msg, "Context:")?; + let chars = data.iter().enumerate() + .filter_map(|(i, c)| { + if cmp::max(8, start) - 8 <= i + && i <= end + 8 + { + Some((i, c)) + } else { + None + } + }); + for (i, c) in chars { + writeln!(&mut msg, "{} {} {}: {:?}", + if i == start { "*" } else { " " }, + i, + *c as char, + c)?; + } + } + Err(Error::InvalidArgument(String::from_utf8_lossy(&msg) + .to_string()).into()) + }, + } + } +} + + +#[cfg(test)] +mod tests { + use crate::sexp::{Sexp, String_}; + use sequoia_openpgp::parse::Parse; + + #[test] + fn basics() { + assert_eq!(Sexp::from_bytes(b"()").unwrap(), + Sexp::List(vec![])); + assert_eq!(Sexp::from_bytes(b"2:hi").unwrap(), + Sexp::String(b"hi"[..].into())); + assert_eq!(Sexp::from_bytes(b"[5:fancy]2:hi").unwrap(), + Sexp::String(String_::with_display_hint( + b"hi".to_vec(), b"fancy".to_vec()))); + assert_eq!(Sexp::from_bytes(b"(2:hi2:ho)").unwrap(), + Sexp::List(vec![ + Sexp::String(b"hi"[..].into()), + Sexp::String(b"ho"[..].into()), + ])); + assert_eq!(Sexp::from_bytes(b"(2:hi[5:fancy]2:ho)").unwrap(), + Sexp::List(vec![ + Sexp::String(b"hi"[..].into()), + Sexp::String(String_::with_display_hint( + b"ho".to_vec(), b"fancy".to_vec())), + ])); + assert_eq!(Sexp::from_bytes(b"(2:hi(2:ha2:ho))").unwrap(), + Sexp::List(vec![ + Sexp::String(b"hi"[..].into()), + Sexp::List(vec![ + Sexp::String(b"ha"[..].into()), + Sexp::String(b"ho"[..].into()), + ]), + ])); + assert_eq!(Sexp::from_bytes(b"(7:sig-val(3:rsa(1:s3:abc)))").unwrap(), + Sexp::List(vec![ + Sexp::String(b"sig-val"[..].into()), + Sexp::List(vec![ + Sexp::String(b"rsa"[..].into()), + Sexp::List(vec![ + Sexp::String(b"s"[..].into()), + Sexp::String(b"abc"[..].into()), + ]), + ]), + ])); + + assert!(Sexp::from_bytes(b"").is_err()); + assert!(Sexp::from_bytes(b"(").is_err()); + assert!(Sexp::from_bytes(b"(2:hi").is_err()); + assert!(Sexp::from_bytes(b"(2:hi)(2:hi)").is_err()); + assert!(Sexp::from_bytes(b"([2:hi])").is_err()); + } + + #[test] + fn signatures() { + assert!(Sexp::from_bytes( + crate::tests::file("sexp/dsa-signature.sexp")).is_ok()); + assert!(Sexp::from_bytes( + crate::tests::file("sexp/ecdsa-signature.sexp")).is_ok()); + assert!(Sexp::from_bytes( + crate::tests::file("sexp/eddsa-signature.sexp")).is_ok()); + assert!(Sexp::from_bytes( + crate::tests::file("sexp/rsa-signature.sexp")).is_ok()); + } +} diff --git a/ipc/src/sexp/serialize.rs b/ipc/src/sexp/serialize.rs new file mode 100644 index 00000000..a360c01e --- /dev/null +++ b/ipc/src/sexp/serialize.rs @@ -0,0 +1,173 @@ +use std::io; + +use sequoia_openpgp as openpgp; +use openpgp::{ + Result, + Error, + serialize::{ + Marshal, + MarshalInto, + }, +}; + +use crate::sexp::{Sexp, String_}; + +impl openpgp::serialize::Serialize for Sexp {} + +impl Marshal for Sexp { + fn serialize(&self, o: &mut dyn std::io::Write) -> Result<()> { + match self { + Sexp::String(ref s) => s.serialize(o), + Sexp::List(ref l) => { + write!(o, "(")?; + for sexp in l { + sexp.serialize(o)?; + } + write!(o, ")")?; + Ok(()) + }, + } + } +} + +impl openpgp::serialize::SerializeInto for Sexp {} + +impl MarshalInto for Sexp { + fn serialized_len(&self) -> usize { + match self { + Sexp::String(ref s) => s.serialized_len(), + Sexp::List(ref l) => + 2 + l.iter().map(|s| s.serialized_len()).sum::(), + } + } + + fn serialize_into(&self, buf: &mut [u8]) -> Result { + generic_serialize_into(self, self.serialized_len(), buf) + } +} + +impl Marshal for String_ { + fn serialize(&self, o: &mut dyn std::io::Write) -> Result<()> { + if let Some(display) = self.display_hint() { + write!(o, "[{}:", display.len())?; + o.write_all(display)?; + write!(o, "]")?; + } + write!(o, "{}:", self.len())?; + o.write_all(self)?; + Ok(()) + } +} + +/// Computes the length of the size tag for a given string length. +fn size_tag_len(len: usize) -> usize { + // Compute log10(self.len()). + let mut l = len; + let mut digits = 0; + while l > 0 { + l /= 10; + digits += 1; + } + + std::cmp::max(1, digits) // 0 takes up 1 char, too. +} + +impl MarshalInto for String_ { + fn serialized_len(&self) -> usize { + self.display_hint() + .map(|d| size_tag_len(d.len()) + 3 + d.len()).unwrap_or(0) + + size_tag_len(self.len()) + 1 + self.len() + } + + fn serialize_into(&self, buf: &mut [u8]) -> Result { + generic_serialize_into(self, self.serialized_len(), buf) + } +} + +/// Provides a generic implementation for SerializeInto::serialize_into. +/// +/// For now, we express SerializeInto using Serialize. In the future, +/// we may provide implementations not relying on Serialize for a +/// no_std configuration of this crate. +fn generic_serialize_into(o: &dyn Marshal, serialized_len: usize, + buf: &mut [u8]) + -> Result { + let buf_len = buf.len(); + let mut cursor = ::std::io::Cursor::new(buf); + match o.serialize(&mut cursor) { + Ok(_) => (), + Err(e) => { + let short_write = + if let Some(ioe) = e.downcast_ref::() { + ioe.kind() == io::ErrorKind::WriteZero + } else { + false + }; + return if short_write { + assert!(buf_len < serialized_len, + "o.serialized_len() underestimated the required space"); + Err(Error::InvalidArgument( + format!("Invalid buffer size, expected {}, got {}", + serialized_len, buf_len)).into()) + } else { + Err(e) + } + } + }; + Ok(cursor.position() as usize) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sexp() { + assert_eq!( + &Sexp::List(vec![]).to_vec().unwrap(), + b"()"); + assert_eq!( + &Sexp::List(vec![Sexp::String(b"hi"[..].into()), + Sexp::String(b"ho"[..].into()), + ]).to_vec().unwrap(), + b"(2:hi2:ho)"); + assert_eq!( + &Sexp::List(vec![ + Sexp::String(b"hi"[..].into()), + Sexp::String(String_::with_display_hint(b"ho".to_vec(), + b"fancy".to_vec())), + ]).to_vec().unwrap(), + b"(2:hi[5:fancy]2:ho)"); + assert_eq!( + &Sexp::List(vec![ + Sexp::String(b"hi"[..].into()), + Sexp::List(vec![ + Sexp::String(b"ha"[..].into()), + Sexp::String(b"ho"[..].into()), + ]), + ]).to_vec().unwrap(), + b"(2:hi(2:ha2:ho))"); + assert_eq!( + &Sexp::List(vec![ + Sexp::String(b"sig-val"[..].into()), + Sexp::List(vec![ + Sexp::String(b"rsa"[..].into()), + Sexp::List(vec![ + Sexp::String(b"s"[..].into()), + Sexp::String(b"abc"[..].into()), + ]), + ]), + ]).to_vec().unwrap(), + b"(7:sig-val(3:rsa(1:s3:abc)))"); + } + + #[test] + fn string() { + assert_eq!(&String_::new(b"hi".to_vec()).to_vec().unwrap(), + b"2:hi"); + assert_eq!(&String_::with_display_hint(b"hi".to_vec(), + b"fancy".to_vec()) + .to_vec().unwrap(), + b"[5:fancy]2:hi"); + } +} diff --git a/ipc/src/tests.rs b/ipc/src/tests.rs new file mode 100644 index 00000000..3a827f92 --- /dev/null +++ b/ipc/src/tests.rs @@ -0,0 +1,41 @@ +//! Test data for Sequoia. +//! +//! This module includes the test data from `ipc/tests/data` in a +//! structured way. + +use std::collections::BTreeMap; + +/// Returns the content of the given file below `ipc/tests/data`. +pub fn file(name: &str) -> &'static [u8] { + lazy_static::lazy_static! { + static ref FILES: BTreeMap<&'static str, &'static [u8]> = { + let mut m: BTreeMap<&'static str, &'static [u8]> = + Default::default(); + + macro_rules! add { + ( $key: expr, $path: expr ) => { + m.insert($key, include_bytes!($path)) + } + } + include!(concat!(env!("OUT_DIR"), "/tests.index.rs.inc")); + + // Sanity checks. + assert!(m.contains_key("sexp/rsa-signature.sexp")); + m + }; + } + + FILES.get(name).unwrap_or_else(|| panic!("No such file {:?}", name)) +} + +/// Returns the content of the given file below `ipc/tests/data/keys`. +#[allow(dead_code)] +pub fn key(name: &str) -> &'static [u8] { + file(&format!("keys/{}", name)) +} + +/// Returns the content of the given file below `ipc/tests/data/messages`. +#[allow(dead_code)] +pub fn message(name: &str) -> &'static [u8] { + file(&format!("messages/{}", name)) +} diff --git a/ipc/tests/data/sexp/dsa-signature.sexp b/ipc/tests/data/sexp/dsa-signature.sexp new file mode 100644 index 00000000..8585774f --- /dev/null +++ b/ipc/tests/data/sexp/dsa-signature.sexp @@ -0,0 +1 @@ +(7:sig-val(3:dsa(1:r20:OiWtfqA&)(1:s20:F)@e 5sz ))) \ No newline at end of file diff --git a/ipc/tests/data/sexp/ecdsa-signature.sexp b/ipc/tests/data/sexp/ecdsa-signature.sexp new file mode 100644 index 00000000..03f0743d --- /dev/null +++ b/ipc/tests/data/sexp/ecdsa-signature.sexp @@ -0,0 +1,2 @@ +(7:sig-val(5:ecdsa(1:r32:KyLЦ+3j! $s%8@)(1:s32:/pM4 FLŊ%ٌS(FUӮf"pĺ!G>Mmc8T/-Zj8Je +ﰣ`t,uFj|XЍF4A/+ qK G8Ys/tUp/N×&DY/)-W0STL/3H@–Lc?xj +Gl˿ߓ7r q/=f%?OPxBcGackZ=ɧ\|zm&O85d1?^tdr QOÃMɐ!))) \ No newline at end of file -- cgit v1.2.3