diff options
-rw-r--r-- | openpgp/examples/notarize.rs | 98 | ||||
-rw-r--r-- | openpgp/src/parse/hashed_reader.rs | 40 | ||||
-rw-r--r-- | openpgp/src/parse/parse.rs | 151 | ||||
-rw-r--r-- | openpgp/src/signature.rs | 14 | ||||
-rw-r--r-- | openpgp/tests/data/messages/signed-1-notarized-by-ed25519.pgp | bin | 0 -> 5626 bytes |
5 files changed, 250 insertions, 53 deletions
diff --git a/openpgp/examples/notarize.rs b/openpgp/examples/notarize.rs new file mode 100644 index 00000000..d3426131 --- /dev/null +++ b/openpgp/examples/notarize.rs @@ -0,0 +1,98 @@ +/// This program demonstrates how to notarize an OpenPGP message. + +use std::env; +use std::io; + +extern crate openpgp; +use openpgp::{ + armor, + Packet, + constants::DataFormat, + parse::PacketParserResult, + serialize::Serialize, +}; +use openpgp::serialize::stream::{wrap, LiteralWriter, Signer}; + +fn main() { + let args: Vec<String> = env::args().collect(); + if args.len() < 2 { + panic!("A simple notarizing filter.\n\n\ + Usage: {} <secret-keyfile> [<secret-keyfile>...] \ + <input >output\n", args[0]); + } + + // Read the transferable secret keys from the given files. + let tsks: Vec<openpgp::TPK> = args[1..].iter().map(|f| { + openpgp::TPK::from_reader( + // Use an openpgp::Reader so that we accept both armored + // and plain PGP data. + openpgp::Reader::from_file(f) + .expect("Failed to open file")) + .expect("Failed to read key") + }).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."); + + // Now, create a signer that emits a detached signature. + let mut signer = Signer::new( + wrap(sink), &tsks.iter().collect::<Vec<&openpgp::TPK>>()) + .expect("Failed to create signer"); + + // Create a parser for the message to be notarized. + let mut input = io::stdin(); + let mut ppr + = openpgp::parse::PacketParser::from_reader( + openpgp::Reader::from_reader(&mut input) + .expect("Failed to build reader")) + .expect("Failed to build parser"); + + while let PacketParserResult::Some(mut pp) = ppr { + if ! pp.possible_message() { + panic!("Malformed OpenPGP message"); + } + + match pp.packet { + Packet::PKESK(_) | Packet::SKESK(_) => + panic!("Encrypted messages are not supported"), + Packet::OnePassSig(ref ops) => + ops.serialize(&mut signer).expect("Failed to serialize"), + Packet::Literal(_) => { + // 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"); + + // Finally, just copy all the data. + io::copy(&mut pp, &mut literal) + .expect("Failed to sign data"); + + signer = literal.finalize_one() + .expect("Failed to sign data") + .unwrap(); + }, + Packet::Signature(ref sig) => + sig.serialize(&mut signer).expect("Failed to serialize"), + _ => (), + } + + let (_, _, ppr_tmp, _) = pp.recurse() + .expect("Failed to recurse"); + ppr = ppr_tmp; + } + if let PacketParserResult::EOF(eof) = ppr { + if ! eof.is_message() { + panic!("Malformed OpenPGP message") + } + } else { + unreachable!() + } + + // Teardown the stack to ensure all the data is written. + signer.finalize() + .expect("Failed to write data"); +} diff --git a/openpgp/src/parse/hashed_reader.rs b/openpgp/src/parse/hashed_reader.rs index 31716695..761935cf 100644 --- a/openpgp/src/parse/hashed_reader.rs +++ b/openpgp/src/parse/hashed_reader.rs @@ -41,6 +41,15 @@ impl<R: BufferedReader<Cookie>> HashedReader<R> { impl Cookie { fn hash_update(&mut self, data: &[u8]) { + // Hash stashed data first. + if let Some(stashed_data) = self.hash_stash.take() { + self.hash_update(&stashed_data); + } + + if data.len() == 0 { + return; + } + if TRACE { eprintln!("{}hash_update({} bytes, {} hashes, enabled: {:?})", indent(cmp::max(0, self.level.unwrap_or(0)) as u8), @@ -59,16 +68,31 @@ impl Cookie { let level = self.level.unwrap_or(0); let hashes_for = self.hashes_for; - for (algo, ref mut h) in self.sig_group_mut().hashes.iter_mut() { - if TRACE { - eprintln!("{} hash_update({:?}): {:?} hashing {} bytes.", - indent(cmp::max(0, level) as u8), - hashes_for, algo, data.len()); - if false { - eprintln!("{}", ::conversions::to_hex(data, true)); + let ngroups = self.sig_groups.len(); + let topmost_group = |i| i == ngroups - 1; + for (i, sig_group) in self.sig_groups.iter_mut().enumerate() { + if topmost_group(i) && self.hashing != Hashing::Enabled { + if TRACE { + eprintln!( + "{} hash_update: topmost group {} NOT hashing {} bytes: {}.", + indent(cmp::max(0, self.level.unwrap_or(0)) as u8), + i, data.len(), ::conversions::to_hex(data, true)); + } + + return; + } + + for (algo, ref mut h) in sig_group.hashes.iter_mut() { + if TRACE { + eprintln!("{} hash_update({:?}): group {} {:?} hashing {} bytes.", + indent(cmp::max(0, level) as u8), + hashes_for, i, algo, data.len()); + if false { + eprintln!("{}", ::conversions::to_hex(data, true)); + } } + h.update(data); } - h.update(data); } } } diff --git a/openpgp/src/parse/parse.rs b/openpgp/src/parse/parse.rs index 93fe2e61..b017da4c 100644 --- a/openpgp/src/parse/parse.rs +++ b/openpgp/src/parse/parse.rs @@ -133,6 +133,7 @@ pub(crate) struct PacketHeaderParser<'a> { // The current packet's header. header: Header, + header_bytes: Vec<u8>, // This packet's recursion depth. // @@ -213,14 +214,22 @@ impl<'a> PacketHeaderParser<'a> { fn new(inner: Box<'a + BufferedReader<Cookie>>, state: PacketParserState, recursion_depth: u8, header: Header, - header_bytes: Option<Vec<u8>>) -> Self + header_bytes: Vec<u8>) -> Self { + let mut cookie = Cookie::default(); + cookie.level = inner.cookie_ref().level; + let map = if state.settings.map { + Some(Map::new(header_bytes.clone())) + } else { + None + }; PacketHeaderParser { - reader: BufferedReaderDup::with_cookie(inner, Default::default()), + reader: BufferedReaderDup::with_cookie(inner, cookie), header: header, + header_bytes: header_bytes.clone(), recursion_depth: recursion_depth, state: state, - map: header_bytes.map(|h| Map::new(h)), + map: map, } } @@ -237,7 +246,7 @@ impl<'a> PacketHeaderParser<'a> { ctb: CTB::new(Tag::Reserved), length: BodyLength::Full(0), }, - None) + Vec::new()) } // Consumes the bytes belonging to the packet's header (i.e., the @@ -350,6 +359,8 @@ pub(crate) enum HashesFor { enum Hashing { /// Hashing is enabled. Enabled, + /// Hashing is enabled for notarized signatures. + Notarized, /// Hashing is disabled. Disabled, } @@ -409,7 +420,22 @@ pub(crate) struct Cookie { hashes_for: HashesFor, hashing: Hashing, + + /// Keeps track of whether the last one pass signature packet had + /// the last flag set. + saw_last: bool, sig_groups: Vec<SignatureGroup>, + + /// Stashed bytes that need to be hashed. + /// + /// When checking nested signatures, we need to hash the framing. + /// However, at the time we know that we want to hash it, it has + /// already been consumed. Deferring the consumption of headers + /// failed due to complications with the partial body decoder + /// eagerly consuming data. I (Justus) decided that doing the + /// right thing is not worth the trouble, at least for now. Also, + /// hash stash sounds funny. + hash_stash: Option<Vec<u8>>, } /// Contains hashes for consecutive one pass signature packets ending @@ -445,13 +471,23 @@ impl Default for SignatureGroup { } } +impl SignatureGroup { + /// Clears the signature group. + fn clear(&mut self) { + self.ops_count = 0; + self.hashes.clear(); + } +} + impl Default for Cookie { fn default() -> Self { Cookie { level: None, hashing: Hashing::Enabled, hashes_for: HashesFor::Nothing, + saw_last: false, sig_groups: vec![Default::default()], + hash_stash: None, } } } @@ -462,7 +498,9 @@ impl Cookie { level: Some(recursion_depth as isize), hashing: Hashing::Enabled, hashes_for: HashesFor::Nothing, + saw_last: false, sig_groups: vec![Default::default()], + hash_stash: None, } } @@ -484,6 +522,22 @@ impl Cookie { assert!(self.sig_groups.len() > 0); self.sig_groups[self.sig_groups.len() - 1].ops_count == 0 } + + /// Pushes a new signature group to the stack. + fn sig_group_push(&mut self) { + self.sig_groups.push(Default::default()); + } + + /// Pops a signature group from the stack. + fn sig_group_pop(&mut self) { + if self.sig_groups.len() == 1 { + // Don't pop the last one, just clear it. + self.sig_groups[0].clear(); + self.hashes_for = HashesFor::Nothing; + } else { + self.sig_groups.pop(); + } + } } impl Cookie { @@ -752,7 +806,7 @@ pub(crate) fn to_unknown_packet<R: Read>(reader: R) -> Result<Unknown> }; let parser = PacketHeaderParser::new( - reader, PacketParserState::new(Default::default()), 0, header, None); + reader, PacketParserState::new(Default::default()), 0, header, Vec::new()); let mut pp = Unknown::parse(parser)?; pp.buffer_unread_content()?; pp.finish()?; @@ -816,28 +870,29 @@ impl Signature { let hash_prefix2 = php_try!(php.parse_u8("hash_prefix2")); let mpis = php_try!(MPIs::parse_signature(pk_algo, &mut php)); - let mut sig = Signature { + let hash_algo = hash_algo.into(); + let mut pp = php.ok(Packet::Signature(Signature { common: Default::default(), version: version, sigtype: sigtype.into(), pk_algo: pk_algo.into(), - hash_algo: hash_algo.into(), + hash_algo: hash_algo, hashed_area: SubpacketArea::new(hashed_area), unhashed_area: SubpacketArea::new(unhashed_area), hash_prefix: [hash_prefix1, hash_prefix2], mpis: mpis, computed_hash: None, - }; + }))?; // Locate the corresponding HashedReader and extract the // computed hash. let mut computed_hash = None; { - let recursion_depth = php.recursion_depth; + let recursion_depth = pp.recursion_depth; // We know that the top reader is not a HashedReader (it's // a BufferedReaderDup). So, start with it's child. - let mut r = (&mut php.reader).get_mut(); + let mut r = (&mut pp.reader).get_mut(); while let Some(tmp) = r { { let cookie = tmp.cookie_mut(); @@ -855,19 +910,19 @@ impl Signature { if cookie.hashes_for == HashesFor::Signature { cookie.sig_group_mut().ops_count -= 1; if let Some(hash) = - cookie.sig_group().hashes.get(&sig.hash_algo) + cookie.sig_group().hashes.get(&hash_algo) { if TRACE { eprintln!("{}PacketParser::parse(): \ popped a {:?} HashedReader", indent(recursion_depth as u8), - sig.hash_algo); + hash_algo); } - computed_hash = Some((sig.hash_algo, hash.clone())); + computed_hash = Some((hash_algo, hash.clone())); } if cookie.sig_group_unused() { - cookie.hashes_for = HashesFor::Nothing; + cookie.sig_group_pop(); } break; } @@ -878,15 +933,19 @@ impl Signature { } if let Some((algo, mut hash)) = computed_hash { - sig.hash(&mut hash); + if let Packet::Signature(ref mut sig) = pp.packet { + sig.hash(&mut hash); - let mut digest = vec![0u8; hash.digest_size()]; - hash.digest(&mut digest); + let mut digest = vec![0u8; hash.digest_size()]; + hash.digest(&mut digest); - sig.computed_hash = Some((algo, digest)); + sig.computed_hash = Some((algo, digest)); + } else { + unreachable!() + } } - php.ok(Packet::Signature(sig)) + Ok(pp) } /// Returns whether the data appears to be a signature (no promises). @@ -981,7 +1040,7 @@ impl OnePassSig { let last = php_try!(php.parse_u8("last")); let hash_algo = hash_algo.into(); - let mut pp = php.ok(Packet::OnePassSig(OnePassSig { + let sig = OnePassSig { common: Default::default(), version: version, sigtype: sigtype.into(), @@ -989,16 +1048,16 @@ impl OnePassSig { pk_algo: pk_algo.into(), issuer: KeyID::from_bytes(&issuer), last: last, - }))?; + }; - let recursion_depth = pp.recursion_depth as isize; + let recursion_depth = php.recursion_depth as isize; // Walk up the reader chain to see if there is already a // hashed reader on level recursion_depth - 1. let done = { let mut done = false; let mut reader : Option<&mut BufferedReader<Cookie>> - = Some(&mut pp.reader); + = Some(&mut php.reader); while let Some(r) = reader { { let cookie = r.cookie_mut(); @@ -1009,9 +1068,15 @@ impl OnePassSig { if br_level == recursion_depth - 1 && cookie.hashes_for == HashesFor::Signature { // We found a suitable hashed reader. + if cookie.saw_last { + cookie.sig_group_push(); + cookie.saw_last = false; + cookie.hash_stash = + Some(php.header_bytes.clone()); + } + // Make sure that it uses the required // hash algorithm. - if ! cookie.sig_group() .hashes.contains_key(&hash_algo) { @@ -1024,6 +1089,9 @@ impl OnePassSig { // Account for this OPS packet. cookie.sig_group_mut().ops_count += 1; + // Keep track of the last flag. + cookie.saw_last = last > 0; + // We're done. done = true; break; @@ -1036,6 +1104,8 @@ impl OnePassSig { } done }; + // Commit here after potentially pushing a signature group. + let mut pp = php.ok(Packet::OnePassSig(sig))?; if done { return Ok(pp); } @@ -1069,6 +1139,8 @@ impl OnePassSig { reader.cookie_mut().level = Some(recursion_depth as isize - 1); // Account for this OPS packet. reader.cookie_mut().sig_group_mut().ops_count += 1; + // Keep track of the last flag. + reader.cookie_mut().saw_last = last > 0; if TRACE { eprintln!("{}OnePassSig::parse: \ @@ -2287,33 +2359,26 @@ impl <'a> PacketParser<'a> { } let tag = header.ctb.tag; - // We've extracted the hash context (if required). Now, we - // rip off the BufferedReaderDup and actually consume the - // header. + // Prepare to actually consume the header. let consumed = bio.total_out(); + // A BufferedReaderDup always has an inner. let mut bio = Box::new(bio).into_inner().unwrap(); - // If we have multiple one pass signature packets in a row, - // then we (XXX: incorrectly!, but gpg doesn't support this - // case either) assume that only the last one pass signature - // packet has the `last` bit set and therefore the one pass - // signature packets should not be included in any preceeding - // one pass signature packet's hashes. - if tag == Tag::Literal || tag == Tag::OnePassSig - || tag == Tag::Signature { + // Disable hashing for literal packets, Literal::parse will + // enable it for the body. Signatures and OnePassSig packets + // are only hashed by notarizing signatures. + if tag == Tag::Literal { Cookie::hashing( &mut bio, Hashing::Disabled, recursion_depth as isize - 1); + } else if tag == Tag::OnePassSig || tag == Tag::Signature { + Cookie::hashing( + &mut bio, Hashing::Notarized, recursion_depth as isize - 1); } - // Save header for the map. - let header_bytes = if state.settings.map { - Some(Vec::from(&bio.data_consume_hard(consumed)?[..consumed])) - } else { - // Or not. - bio.consume(consumed); - None - }; + // Save header for the map or nested signatures. + let header_bytes = + Vec::from(&bio.data_consume_hard(consumed)?[..consumed]); let bio : Box<BufferedReader<Cookie>> = match header.length { diff --git a/openpgp/src/signature.rs b/openpgp/src/signature.rs index 36f0fd79..7ef8ffcc 100644 --- a/openpgp/src/signature.rs +++ b/openpgp/src/signature.rs @@ -611,6 +611,16 @@ mod test { data: &"signed-twice-by-ed25519.pgp"[..], good: 2, }, + Test { + key: "neal.pgp", + data: "signed-1-notarized-by-ed25519.pgp", + good: 1, + }, + Test { + key: "emmelie-dorothea-dina-samantha-awina-ed25519.pgp", + data: "signed-1-notarized-by-ed25519.pgp", + good: 1, + }, // Check with the wrong key. Test { key: &"neal.pgp"[..], @@ -636,7 +646,7 @@ mod test { path_to(&format!("messages/{}", test.data)[..])).unwrap(); while let PacketParserResult::Some(mut pp) = ppr { if let Packet::Signature(ref sig) = pp.packet { - let result = sig.verify(tpk.primary()).unwrap(); + let result = sig.verify(tpk.primary()).unwrap_or(false); eprintln!(" Primary {:?}: {:?}", tpk.primary().fingerprint(), result); if result { @@ -644,7 +654,7 @@ mod test { } for sk in &tpk.subkeys { - let result = sig.verify(sk.subkey()).unwrap(); + let result = sig.verify(sk.subkey()).unwrap_or(false); eprintln!(" Subkey {:?}: {:?}", sk.subkey().fingerprint(), result); if result { diff --git a/openpgp/tests/data/messages/signed-1-notarized-by-ed25519.pgp b/openpgp/tests/data/messages/signed-1-notarized-by-ed25519.pgp Binary files differnew file mode 100644 index 00000000..251bce08 --- /dev/null +++ b/openpgp/tests/data/messages/signed-1-notarized-by-ed25519.pgp |