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/src | |
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/src')
-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 |
8 files changed, 1038 insertions, 1 deletions
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 => '['.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<Token<'input>, usize, LexicalError>; + + fn next(&mut self) -> Option<Self::Item> { + 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<R: 'a + Read>(reader: R) -> Result<Sexp> { + Self::from_bytes( + buffered_reader::Generic::new(reader, None).data_eof()?) + } + + fn from_file<P: AsRef<Pa |