diff options
Diffstat (limited to 'openpgp/src/packet')
23 files changed, 3541 insertions, 1454 deletions
diff --git a/openpgp/src/packet/aed.rs b/openpgp/src/packet/aed.rs index 8af94f00..b7f6e58c 100644 --- a/openpgp/src/packet/aed.rs +++ b/openpgp/src/packet/aed.rs @@ -1,4 +1,77 @@ //! AEAD encrypted data packets. +//! +//! An encryption container using [Authenticated Encryption with +//! Additional Data]. +//! +//! The AED packet is a new packet specified in [Section 5.16 of RFC +//! 4880bis]. Its aim is to replace the [SEIP packet], whose security +//! has been partially compromised. SEIP's weaknesses includes its +//! use of CFB mode (e.g., EFAIL-style CFB gadgets, see Section 5.3 of +//! the [EFAIL paper]), its use of [SHA-1] for integrity protection, and +//! the ability to [downgrade SEIP packets] to much weaker SED +//! packets. +//! +//! Although the decision to use AEAD is uncontroversial, the design +//! specified in RFC 4880bis is. According to [RFC 5116], decrypted +//! AEAD data can only be released for processing after its +//! authenticity has been checked: +//! +//! > [The authenticated decryption operation] has only a single +//! > output, either a plaintext value P or a special symbol FAIL that +//! > indicates that the inputs are not authentic. +//! +//! The controversy has to do with streaming, which OpenPGP has +//! traditionally supported. Streaming a message means that the +//! amount of data that needs to be buffered when processing a message +//! is independent of the message's length. +//! +//! At first glance, the AEAD mechanism in RFC 4880bis appears to +//! support this mode of operation: instead of encrypting the whole +//! message using AEAD, which would require buffering all of the +//! plaintext when decrypting the message, the message is chunked, the +//! individual chunks are linked together, and AEAD is used to encrypt +//! and protect each individual chunk. Because the plaintext from an +//! individual chunk can be integrity checked, an implementation only +//! needs to buffer a chunk worth of data. +//! +//! Unfortunately, RFC 4880bis allows chunk sizes that are, in +//! practice, unbounded. Specifically, a chunk can be up to 4 +//! exbibytes in size. Thus when encountering messages that can't be +//! buffered, an OpenPGP implementation has a choice: it can either +//! release data that has not been integrity checked and violate RFC +//! 5116, or it can fail to process the message. As of 2020, [GnuPG] +//! and [RNP] process unauthenticated plaintext. From a user +//! perspective, it then appears that implementations that choose to +//! follow RFC 5116 are impaired: "GnuPG can decrypt it," they think, +//! "why can't Sequoia?" This creates pressure on other +//! implementations to also behave insecurely. +//! +//! [Werner argues] that AEAD is not about authenticating the data. +//! That is the purpose of the signature. The reason to introduce +//! AEAD is to get the benefits of more modern cryptography, and to be +//! able to more quickly detect rare transmission errors. Our +//! position is that an integrity check provides real protection: it +//! can detect modified ciphertext. And, if we are going to stream, +//! then this protection is essential as it protects the user from +//! real, demonstrated attacks like [EFAIL]. +//! +//! RFC 4880bis has not been finalized. So, it is still possible that +//! the AEAD mechanism will change (which is why the AED packet is +//! marked as experimental). Despite our concerns, because other +//! OpenPGP implementations already emit the AEAD packet, we provide +//! *experimental* support for it in Sequoia. +//! +//! [Authenticated Encryption with Additional Data]: https://en.wikipedia.org/wiki/Authenticated_encryption +//! [Section 5.16 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-09#section-5.16 +//! [EFAIL paper]: https://www.usenix.org/conference/usenixsecurity18/presentation/poddebniak +//! [SHA-1]: https://sha-mbles.github.io/ +//! [SEIP packet]: https://tools.ietf.org/html/rfc4880#section-5.13 +//! [RFC 5116]: https://tools.ietf.org/html/rfc5116#section-2.2 +//! [downgrade SEIP packets]: https://mailarchive.ietf.org/arch/msg/openpgp/JLn7sL6TqikUf-cD34lN7kof7_A/ +//! [GnuPG]: https://mailarchive.ietf.org/arch/msg/openpgp/fmQgRm94jhvPLEOi0J-o7A8LpkY/ +//! [RNP]: https://github.com/rnpgp/rnp/issues/807 +//! [Werner argues]: https://mailarchive.ietf.org/arch/msg/openpgp/J428Mqq3-pHTU4C76EgP5sPkvtA +//! [EFAIL]: https://efail.de/ use crate::types::{ AEADAlgorithm, @@ -11,13 +84,31 @@ use crate::Result; /// Holds an AEAD encrypted data packet. /// -/// An AEAD encrypted data packet is a container. See [Section 5.16 -/// of RFC 4880bis] for details. +/// An AEAD encrypted data packet holds encrypted data. The data +/// contains additional OpenPGP packets. See [Section 5.16 of RFC +/// 4880bis] for details. +/// +/// An AED packet is not normally instantiated directly. In most +/// cases, you'll create one as a side-effect of encrypting a message +/// using the [streaming serializer], or parsing an encrypted message +/// using the [`PacketParser`]. +/// +/// This feature is +/// [experimental](../../index.html#experimental-features). It has +/// not been standardized and we advise users to not emit AED packets. /// /// [Section 5.16 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-05#section-5.16 +/// [streaming serializer]: ../serialize/stream/index.html +/// [`PacketParser`]: ../parse/index.html +/// +/// # A note on equality /// -/// This feature is [experimental](../../index.html#experimental-features). -#[derive(Clone, Debug)] +/// An unprocessed (encrypted) `AED` packet is never considered equal +/// to a processed (decrypted) one. Likewise, a processed (decrypted) +/// packet is never considered equal to a structured (parsed) one. +// IMPORTANT: If you add fields to this struct, you need to explicitly +// IMPORTANT: implement PartialEq, Eq, and Hash. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct AED1 { /// CTB packet header fields. pub(crate) common: packet::Common, @@ -26,7 +117,7 @@ pub struct AED1 { /// AEAD algorithm. aead: AEADAlgorithm, /// Chunk size. - chunk_size: usize, + chunk_size: u64, /// Initialization vector for the AEAD algorithm. iv: Box<[u8]>, @@ -34,25 +125,16 @@ pub struct AED1 { container: packet::Container, } -impl PartialEq for AED1 { - fn eq(&self, other: &AED1) -> bool { - self.sym_algo == other.sym_algo - && self.aead == other.aead - && self.chunk_size == other.chunk_size - && self.iv == other.iv - && self.container == other.container +impl std::ops::Deref for AED1 { + type Target = packet::Container; + fn deref(&self) -> &Self::Target { + &self.container } } -impl Eq for AED1 {} - -impl std::hash::Hash for AED1 { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - std::hash::Hash::hash(&self.sym_algo, state); - std::hash::Hash::hash(&self.aead, state); - std::hash::Hash::hash(&self.chunk_size, state); - std::hash::Hash::hash(&self.iv, state); - std::hash::Hash::hash(&self.container, state); +impl std::ops::DerefMut for AED1 { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.container } } @@ -60,7 +142,7 @@ impl AED1 { /// Creates a new AED1 object. pub fn new(sym_algo: SymmetricAlgorithm, aead: AEADAlgorithm, - chunk_size: usize, + chunk_size: u64, iv: Box<[u8]>) -> Result<Self> { if chunk_size.count_ones() != 1 { return Err(Error::InvalidArgument( @@ -76,10 +158,10 @@ impl AED1 { Ok(AED1 { common: Default::default(), - sym_algo: sym_algo, - aead: aead, - chunk_size: chunk_size, - iv: iv, + sym_algo, + aead, + chunk_size, + iv, container: Default::default(), }) } @@ -89,7 +171,7 @@ impl AED1 { self.sym_algo } - /// Sets the sym_algo algorithm. + /// Sets the symmetric algorithm. pub fn set_symmetric_algo(&mut self, sym_algo: SymmetricAlgorithm) -> SymmetricAlgorithm { ::std::mem::replace(&mut self.sym_algo, sym_algo) @@ -106,12 +188,12 @@ impl AED1 { } /// Gets the chunk size. - pub fn chunk_size(&self) -> usize { + pub fn chunk_size(&self) -> u64 { self.chunk_size } - /// Gets the chunk size. - pub fn set_chunk_size(&mut self, chunk_size: usize) -> Result<()> { + /// Sets the chunk size. + pub fn set_chunk_size(&mut self, chunk_size: u64) -> Result<()> { if chunk_size.count_ones() != 1 { return Err(Error::InvalidArgument( format!("chunk size is not a power of two: {}", chunk_size)) @@ -128,9 +210,9 @@ impl AED1 { Ok(()) } - /// Gets the size of a chunk with digest. - pub fn chunk_digest_size(&self) -> Result<usize> { - Ok(self.chunk_size + self.aead.digest_size()?) + /// Gets the size of a chunk with a digest. + pub fn chunk_digest_size(&self) -> Result<u64> { + Ok(self.chunk_size + self.aead.digest_size()? as u64) } /// Gets the initialization vector for the AEAD algorithm. @@ -144,8 +226,6 @@ impl AED1 { } } -impl_container_forwards!(AED1); - impl From<AED1> for Packet { fn from(p: AED1) -> Self { super::AED::from(p).into() @@ -157,19 +237,3 @@ impl From<AED1> for super::AED { super::AED::V1(p) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn deref() { - let mut s = AED1::new(SymmetricAlgorithm::AES128, - AEADAlgorithm::EAX, - 64, - vec![].into_boxed_slice()).unwrap(); - assert_eq!(s.body(), &[]); - s.set_body(vec![0, 1, 2]); - assert_eq!(s.body(), &[0, 1, 2]); - } -} diff --git a/openpgp/src/packet/compressed_data.rs b/openpgp/src/packet/compressed_data.rs index bf89956f..4f379496 100644 --- a/openpgp/src/packet/compressed_data.rs +++ b/openpgp/src/packet/compressed_data.rs @@ -1,5 +1,8 @@ use std::fmt; +#[cfg(any(test, feature = "quickcheck"))] +use quickcheck::{Arbitrary, Gen}; + use crate::packet; use crate::Packet; use crate::types::CompressionAlgorithm; @@ -14,7 +17,9 @@ use crate::types::CompressionAlgorithm; /// of a `CompressedData` packet. /// /// [Section 5.6 of RFC 4880]: https://tools.ietf.org/html/rfc4880#section-5.6 -#[derive(Clone)] +// IMPORTANT: If you add fields to this struct, you need to explicitly +// IMPORTANT: implement PartialEq, Eq, and Hash. +#[derive(Clone, PartialEq, Eq, Hash)] pub struct CompressedData { /// CTB packet header fields. pub(crate) common: packet::Common, @@ -25,19 +30,16 @@ pub struct CompressedData { container: packet::Container, } -impl PartialEq for CompressedData { - fn eq(&self, other: &CompressedData) -> bool { - self.algo == other.algo - && self.container == other.container +impl std::ops::Deref for CompressedData { + type Target = packet::Container; + fn deref(&self) -> &Self::Target { + &self.container } } -impl Eq for CompressedData {} - -impl std::hash::Hash for CompressedData { - fn hash<H: std::hash::Hasher>(&self, state: &mut H) { - std::hash::Hash::hash(&self.algo, state); - std::hash::Hash::hash(&self.container, state); +impl std::ops::DerefMut for CompressedData { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.container } } @@ -45,10 +47,7 @@ impl fmt::Debug for CompressedData { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("CompressedData") .field("algo", &self.algo) - .field("children", &self.container.children_ref()) - .field("body (bytes)", - &self.container.body().len()) - .field("body_digest", &self.container.body_digest()) + .field("container", &self.container) .finish() } } @@ -58,7 +57,7 @@ impl CompressedData { pub fn new(algo: CompressionAlgorithm) -> Self { CompressedData { common: Default::default(), - algo: algo, + algo, container: Default::default(), } } @@ -76,7 +75,7 @@ impl CompressedData { /// Adds a new packet to the container. #[cfg(test)] pub fn push(mut self, packet: Packet) -> Self { - self.container.children_mut().push(packet); + self.container.children_mut().unwrap().push(packet); self } @@ -86,15 +85,34 @@ impl CompressedData { /// packet, etc. #[cfg(test)] pub fn insert(mut self, i: usize, packet: Packet) -> Self { - self.container.children_mut().insert(i, packet); + self.container.children_mut().unwrap().insert(i, packet); self } } -impl_container_forwards!(CompressedData); - impl From<CompressedData> for Packet { fn from(s: CompressedData) -> Self { Packet::CompressedData(s) } } + +#[cfg(any(test, feature = "quickcheck"))] +impl Arbitrary for CompressedData { + fn arbitrary<G: Gen>(g: &mut G) -> Self { + use rand::Rng; + use crate::serialize::SerializeInto; + loop { + let a = CompressionAlgorithm::from(g.gen_range(0, 4)); + if a.is_supported() { + let mut c = CompressedData::new(a); + // We arbitrarily chose to create packets with + // processed bodies, so that + // Packet::from_bytes(c.to_vec()) will roundtrip them. + c.set_body(packet::Body::Processed( + Packet::arbitrary(g).to_vec().unwrap() + )); + return c; + } + } + } +} diff --git a/openpgp/src/packet/container.rs b/openpgp/src/packet/container.rs index 185659bb..2cc6c655 100644 --- a/openpgp/src/packet/container.rs +++ b/openpgp/src/packet/container.rs @@ -15,17 +15,75 @@ use crate::{ types::HashAlgorithm, }; -/// Holds zero or more OpenPGP packets. +/// A packet's body holds either unprocessed bytes, processed bytes, +/// or packets. /// -/// This is used by OpenPGP container packets, like the compressed -/// data packet, to store the containing packets. +/// We conceptually divide packets into two parts: the header and the +/// body. Whereas the header is read eagerly when the packet is +/// deserialized, the body is only read on demand. +/// +/// A packet's body is stored here either when configured via +/// [`PacketParserBuilder::buffer_unread_content`], when one of the +/// [`PacketPile`] deserialization routines is used, or on demand for +/// a particular packet using the +/// [`PacketParser::buffer_unread_content`] method. +/// +/// [`PacketParserBuilder::buffer_unread_content`]: ../parse/struct.PacketParserBuilder.html#method.buffer_unread_content +/// [`PacketPile`]: ../struct.PacketPile.html +/// [`PacketParser::buffer_unread_content`]: ../parse/struct.PacketParser.html#method.buffer_unread_content +/// +/// There are three different types of packets: +/// +/// - Packets like the [`UserID`] and [`Signature`] packets, don't +/// actually have a body. +/// +/// [`UserID`]: ../packet/struct.UserID.html +/// [`Signature`]: ../packet/signature/struct.Signature.html +/// +/// - One packet, the literal data packet, includes unstructured +/// data. That data is stored in [`Literal`]. +/// +/// [`Literal`]: ../packet/struct.Literal.html +/// +/// - Some packets are containers. If the parser does not parse the +/// packet's child, either because the caller used +/// [`PacketParser::next`] to get the next packet, or the maximum +/// recursion depth was reached, then the packets can be stored here +/// as a byte stream. (If the caller so chooses, the content can be +/// parsed later using the regular deserialization routines, since +/// the content is just an OpenPGP message.) +/// +/// [`PacketParser::next`]: ../parse/struct.PacketParser.html#method.next #[derive(Clone)] -pub(crate) struct Container { +pub enum Body { + /// Unprocessed packet body. + /// + /// The body has not been processed, i.e. it is still encrypted. + /// + /// Note: if some of a packet's data is streamed, and the + /// `PacketParser` is configured to buffer unread content, then + /// this is not the packet's entire content; it is just the unread + /// content. + Unprocessed(Vec<u8>), + + /// Processed packed body. + /// + /// The body has been processed, i.e. decompressed or decrypted, + /// but not parsed into packets. + /// + /// Note: if some of a packet's data is streamed, and the + /// `PacketParser` is configured to buffer unread content, then + /// this is not the packet's entire content; it is just the unread + /// content. + Processed(Vec<u8>), + + /// Parsed packet body. + /// /// Used by container packets (such as the encryption and /// compression packets) to reference their immediate children. /// This results in a tree structure. /// - /// This is automatically populated when using the `PacketPile` + /// This is automatically populated when using the [`PacketPile`] /// deserialization routines, e.g., [`PacketPile::from_file`]. By /// default, it is *not* automatically filled in by the /// [`PacketParser`] deserialization routines; this needs to be @@ -34,59 +92,29 @@ pub(crate) struct Container { /// [`PacketPile`]: ../struct.PacketPile.html /// [`PacketPile::from_file`]: ../struct.PacketPile.html#method.from_file /// [`PacketParser`]: ../parse/struct.PacketParser.html - pub(crate) packets: Vec<Packet>, + Structured(Vec<Packet>), +} +/// Holds packet bodies. +/// +/// This is used by OpenPGP container packets, like the compressed +/// data packet, to store the containing packets. +#[derive(Clone)] +pub struct Container { /// Holds a packet's body. - /// - /// We conceptually divide packets into two parts: the header and - /// the body. Whereas the header is read eagerly when the packet - /// is deserialized, the body is only read on demand. - /// - /// A packet's body is stored here either when configured via - /// [`PacketParserBuilder::buffer_unread_content`], when one of - /// the [`PacketPile`] deserialization routines is used, or on demand - /// for a particular packet using the - /// [`PacketParser::buffer_unread_content`] method. - /// - /// [`PacketParserBuilder::buffer_unread_content`]: ../parse/struct.PacketParserBuilder.html#method.buffer_unread_content - /// [`PacketPile`]: ../struct.PacketPile.html - /// [`PacketParser::buffer_unread_content`]: ../parse/struct.PacketParser.html#method.buffer_unread_content - /// - /// There are three different types of packets: - /// - /// - Packets like the [`UserID`] and [`Signature`] packets, - /// don't actually have a body. These packets don't use this - /// field. - /// - /// [`UserID`]: ../packet/struct.UserID.html - /// [`Signature`]: ../packet/signature/struct.Signature.html - /// - /// - One packet, the literal data packet, includes unstructured - /// data. That data is stored in [`Literal`]. - /// - /// [`Literal`]: ../packet/struct.Literal.html - /// - /// - Some packets are containers. If the parser does not parse - /// the packet's child, either because the caller used - /// [`PacketParser::next`] to get the next packet, or the - /// maximum recursion depth was reached, then the packets can - /// be stored here as a byte stream. (If the caller so - /// chooses, the content can be parsed later using the regular - /// deserialization routines, since the content is just an - /// OpenPGP message.) - /// - /// [`PacketParser::next`]: ../parse/struct.PacketParser.html#method.next - /// - /// Note: if some of a packet's data is processed, and the - /// `PacketParser` is configured to buffer unread content, then - /// this is not the packet's entire content; it is just the unread - /// content. - body: Vec<u8>, + body: Body, /// We compute a digest over the body to implement comparison. body_digest: Vec<u8>, } +impl std::ops::Deref for Container { + type Target = Body; + fn deref(&self) -> &Self::Target { + &self.body + } +} + // Pick the fastest hash function from the SHA2 family for the // architectures word size. On 64-bit architectures, SHA512 is almost // twice as fast, but on 32-bit ones, SHA256 is faster. @@ -97,8 +125,16 @@ const CONTAINER_BODY_HASH: HashAlgorithm = HashAlgorithm::SHA256; impl PartialEq for Container { fn eq(&self, other: &Container) -> bool { - self.packets == other.packets - && self.body_digest == other.body_digest + use Body::*; + match (&self.body, &other.body) { + (Unprocessed(_), Unprocessed(_)) => + self.body_digest == other.body_digest, + (Processed(_), Processed(_)) => + self.body_digest == other.body_digest, + (Structured(a), Structured(b)) => + a == b, + _ => false, + } } } @@ -106,17 +142,19 @@ impl Eq for Container {} impl Hash for Container { fn hash<H: Hasher>(&self, state: &mut H) { - self.packets.hash(state); - self.body_digest.hash(state); + if let Body::Structured(packets) = &self.body { + packets.hash(state); + } else { + self.body_digest.hash(state); + } } } impl Default for Container { fn default() -> Self { Self { - packets: Vec::with_capacity(0), - body: Vec::with_capacity(0), - body_digest: Self::empty_body_digest(), + body: Body::Structured(Vec::with_capacity(0)), + body_digest: Vec::with_capacity(0), } } } @@ -124,83 +162,122 @@ impl Default for Container { impl From<Vec<Packet>> for Container { fn from(packets: Vec<Packet>) -> Self { Self { - packets, - body: Vec::with_capacity(0), - body_digest: Self::empty_body_digest(), + body: Body::Structured(packets), + body_digest: Vec::with_capacity(0), } } } impl fmt::Debug for Container { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let threshold = 16; - let prefix = &self.body[..std::cmp::min(threshold, self.body.len())]; - let mut prefix_fmt = crate::fmt::hex::encode(prefix); - if self.body.len() > threshold { - prefix_fmt.push_str("..."); + fn fmt_bytes(f: &mut fmt::Formatter, tag: &str, bytes: &[u8], + digest: String) + -> fmt::Result + { + let threshold = 16; + let prefix = &bytes[..std::cmp::min(threshold, bytes.len())]; + let mut prefix_fmt = crate::fmt::hex::encode(prefix); + if bytes.len() > threshold { + prefix_fmt.push_str("..."); + } + prefix_fmt.push_str(&format!(" ({} bytes)", bytes.len())[..]); + + f.debug_struct("Container") + .field(tag, &prefix_fmt) + .field("digest", &digest) + .finish() } - prefix_fmt.push_str(&format!(" ({} bytes)", self.body.len())[..]); - f.debug_struct("Container") - .field("packets", &self.packets) - .field("body", &prefix_fmt) - .field("body_digest", &self.body_digest()) - .finish() + use Body::*; + match &self.body { + Unprocessed(bytes) => + fmt_bytes(f, "unprocessed", bytes, self.body_digest()), + Processed(bytes) => + fmt_bytes(f, "processed", bytes, self.body_digest()), + Structured(packets) => + f.debug_struct("Container").field("packets", packets).finish(), + } } } impl Container { + pub(crate) fn default_unprocessed() -> Self { + Self { + body: Body::Unprocessed(Vec::with_capacity(0)), + body_digest: Self::empty_body_digest(), + } + } + |