summaryrefslogtreecommitdiffstats
path: root/openpgp/src/serialize/stream.rs
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2021-02-10 16:02:00 +0100
committerJustus Winter <justus@sequoia-pgp.org>2021-02-24 11:08:00 +0100
commit1a9a05b875abd94a1d5886592b86876d2ff39aec (patch)
tree7cd26a8bfcaf10205e6b58d6b7885c95d47ed9ce /openpgp/src/serialize/stream.rs
parent6529777daf9118b90928a77b051024e67fe2b4dd (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.rs199
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;
}