diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2021-02-10 16:02:00 +0100 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2021-02-24 11:08:00 +0100 |
commit | 1a9a05b875abd94a1d5886592b86876d2ff39aec (patch) | |
tree | 7cd26a8bfcaf10205e6b58d6b7885c95d47ed9ce /openpgp/src/serialize/stream.rs | |
parent | 6529777daf9118b90928a77b051024e67fe2b4dd (diff) |
openpgp: Sign messages using the Cleartext Signature Framework.
- See #151.
Diffstat (limited to 'openpgp/src/serialize/stream.rs')
-rw-r--r-- | openpgp/src/serialize/stream.rs | 199 |
1 files changed, 196 insertions, 3 deletions
diff --git a/openpgp/src/serialize/stream.rs b/openpgp/src/serialize/stream.rs index 75ad40e9..769221c2 100644 --- a/openpgp/src/serialize/stream.rs +++ b/openpgp/src/serialize/stream.rs @@ -163,6 +163,11 @@ pub(crate) mod writer; pub mod padding; mod partial_body; use partial_body::PartialBodyFilter; +mod dash_escape; +use dash_escape::DashEscapeFilter; +mod trim_whitespace; +use trim_whitespace::TrailingWSFilter; + /// Cookie must be public because the writers are. #[derive(Debug)] @@ -645,6 +650,12 @@ pub struct Signer<'a> { hash: Box<dyn crypto::hash::Digest>, cookie: Cookie, position: u64, + + /// When creating a message using the cleartext signature + /// framework, the final newline is not part of the signature, + /// hence, we delay hashing up to two bytes so that we can omit + /// them when the message is finalized. + hash_stash: Vec<u8>, } assert_send_and_sync!(Signer<'_>); @@ -652,6 +663,7 @@ assert_send_and_sync!(Signer<'_>); enum SignatureMode { Inline, Detached, + Cleartext, } impl<'a> Signer<'a> { @@ -818,6 +830,7 @@ impl<'a> Signer<'a> { private: Private::Signer, }, position: 0, + hash_stash: Vec::with_capacity(0), } } @@ -827,8 +840,11 @@ impl<'a> Signer<'a> { /// [Section 11.4 of RFC 4880]). Note that the literal data *must /// not* be wrapped using the [`LiteralWriter`]. /// + /// This overrides any prior call to [`Signer::cleartext`]. + /// /// [Section 11.4 of RFC 4880]: https://tools.ietf.org/html/rfc4880#section-11.4 /// [`LiteralWriter`]: ../struct.LiteralWriter.html + /// [`Signer::cleartext`]: #method.cleartext /// /// # Examples /// @@ -897,6 +913,97 @@ impl<'a> Signer<'a> { self } + /// Creates a signer for a cleartext signed message. + /// + /// Changes the `Signer` to create a cleartext signed message (see + /// [Section 7 of RFC 4880]). Note that the literal data *must + /// not* be wrapped using the [`LiteralWriter`]. This implies + /// ASCII armored output, *do not* add an [`Armorer`] to the + /// stack. + /// + /// Note: + /// + /// - If your message does not end in a newline, creating a signed + /// message using the Cleartext Signature Framework will add + /// one. + /// + /// - The cleartext signature framework does not hash trailing + /// whitespace (in this case, space and tab, see [Section 7.1 of + /// RFC 4880] for more information). We align what we emit and + /// what is being signed by trimming whitespace off of line + /// endings. + /// + /// - That means that you can not recover a byte-accurate copy of + /// the signed message if your message contains either a line + /// with trailing whitespace, or no final newline. This is a + /// limitation of the Cleartext Signature Framework, which is + /// not designed to be reversible (see [Section 7 of RFC 4880]). + /// + /// This overrides any prior call to [`Signer::detached`]. + /// + /// [Section 7 of RFC 4880]: https://tools.ietf.org/html/rfc4880#section-7 + /// [Section 7.1 of RFC 4880]: https://tools.ietf.org/html/rfc4880#section-7.1 + /// [`LiteralWriter`]: ../struct.LiteralWriter.html + /// [`Armorer`]: ../struct.Armorer.html + /// [`Signer::detached`]: #method.detached + /// + /// # Examples + /// + /// ``` + /// # fn main() -> sequoia_openpgp::Result<()> { + /// use std::io::Write; + /// use sequoia_openpgp as openpgp; + /// use openpgp::serialize::stream::{Message, Signer}; + /// use openpgp::policy::StandardPolicy; + /// # use openpgp::{Result, Cert}; + /// # use openpgp::packet::prelude::*; + /// # use openpgp::crypto::KeyPair; + /// # use openpgp::parse::Parse; + /// # use openpgp::parse::stream::*; + /// + /// let p = &StandardPolicy::new(); + /// # let cert = Cert::from_bytes(&include_bytes!( + /// # "../../tests/data/keys/testy-new-private.pgp")[..])?; + /// # let signing_keypair + /// # = cert.keys().secret() + /// # .with_policy(p, None).supported().alive().revoked(false).for_signing() + /// # .nth(0).unwrap() + /// # .key().clone().into_keypair()?; + /// + /// let mut sink = vec![]; + /// { + /// let message = Message::new(&mut sink); + /// let mut signer = Signer::new(message, signing_keypair) + /// .cleartext() + /// // Customize the `Signer` here. + /// .build()?; + /// + /// // Write the data directly to the `Signer`. + /// signer.write_all(b"Make it so, number one!")?; + /// // In reality, just io::copy() the file to be signed. + /// signer.finalize()?; + /// } + /// # Ok(()) } + /// ``` + // + // Some notes on the implementation: + // + // There are a few pitfalls when implementing the CSF. We + // separate concerns as much as possible. + // + // - Trailing whitespace must be stripped. We do this using the + // TrailingWSFilter before the data hits this streaming signer. + // This filter also adds a final newline, if missing. + // + // - We hash what we get from the TrailingWSFilter. + // + // - We write into the DashEscapeFilter, which takes care of the + // dash-escaping. + pub fn cleartext(mut self) -> Self { + self.mode = SignatureMode::Cleartext; + self + } + /// Adds an additional signer. /// /// Can be used multiple times. @@ -1145,12 +1252,50 @@ impl<'a> Signer<'a> { } }, SignatureMode::Detached => (), // Do nothing. + SignatureMode::Cleartext => { + // Cleartext signatures are always text signatures. + self.template = self.template.set_type(SignatureType::Text); + + // Write the header. + let mut sink = self.inner.take().unwrap(); + writeln!(sink, "-----BEGIN PGP SIGNED MESSAGE-----")?; + writeln!(sink, "Hash: {}", self.hash.algo().text_name()?)?; + writeln!(sink)?; + + // We now install two filters. See the comment on + // Signer::cleartext. + + // Install the filter dash-escaping the text below us. + self.inner = + Some(writer::BoxStack::from( + DashEscapeFilter::new(Message::from(sink), + Default::default()))); + + // Install the filter trimming the trailing whitespace + // above us. + return Ok(TrailingWSFilter::new(Message::from(Box::new(self)), + Default::default())); + }, } Ok(Message::from(Box::new(self))) } fn emit_signatures(&mut self) -> Result<()> { + if self.mode == SignatureMode::Cleartext { + // Pop off the DashEscapeFilter. + let inner = self.inner.take().expect("It's the DashEscapeFilter") + .into_inner()?.expect("It's the DashEscapeFilter"); + + // And install an armorer. + self.inner = + Some(writer::BoxStack::from( + writer::Armorer::new(Message::from(inner), + Default::default(), + armor::Kind::Signature, + Option::<(&str, &str)>::None)?)); + } + if let Some(ref mut sink) = self.inner { // Emit the signatures in reverse, so that the // one-pass-signature and signature packets "bracket" the @@ -1194,6 +1339,13 @@ impl<'a> fmt::Debug for Signer<'a> { impl<'a> Write for Signer<'a> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> { + // Shortcut empty writes. This is important for the code + // below that delays hashing newlines when creating cleartext + // signed messages. + if buf.len() == 0 { + return Ok(0); + } + use SignatureMode::*; let written = match (self.inner.as_mut(), self.mode) { // If we are creating a normal signature, pass data @@ -1202,16 +1354,57 @@ impl<'a> Write for Signer<'a> { // If we are creating a detached signature, just hash all // bytes. (Some(_), Detached) => Ok(buf.len()), + // If we are creating a cleartext signed message, just + // write through (the DashEscapeFilter takes care of the + // encoding), and hash all bytes as is. + (Some(ref mut w), Cleartext) => w.write(buf), // When we are popped off the stack, we have no inner // writer. Just hash all bytes. (None, _) => Ok(buf.len()), }; if let Ok(amount) = written { - if self.template.typ() == SignatureType::Text { - crate::parse::hash_update_text(&mut self.hash, &buf[..amount]); + let data = &buf[..amount]; + + if self.mode == Cleartext { + // Delay hashing the last two bytes, because we the + // final newline is not part of the signature (see + // Section 7.1 of RFC4880). + + // First, hash the stashed bytes. We know that it is + // a newline, but we know that more text follows (buf + // is not empty), so it cannot be the last. + assert!(! buf.is_empty()); + crate::parse::hash_update_text(&mut self.hash, + &self.hash_stash[..]); + crate::vec_truncate(&mut self.hash_stash, 0); + + // Compute the length of data that should be hashed. + // If it ends in a newline, we delay hashing it. + let l = data.len() - if data.ends_with(b"\r\n") { + 2 + } else if data.ends_with(b"\n") { + 1 + } else { + 0 + }; + + // XXX: This logic breaks if we get a b"\r\n" in two + // writes. However, TrailingWSFilter will only emit + // b"\r\n" in one write. + + // Hash everything but the last newline now. + crate::parse::hash_update_text(&mut self.hash, &data[..l]); + // The newline we stash away. If more text is written + // later, we will hash it then. Otherwise, it is + // implicitly omitted when the signer is finalized. + self.hash_stash.extend_from_slice(&data[l..]); } else { - self.hash.update(&buf[..amount]); + if self.template.typ() == SignatureType::Text { + crate::parse::hash_update_text(&mut self.hash, data); + } else { + self.hash.update(data); + } } self.position += amount as u64; } |