diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2023-04-07 13:40:50 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2023-06-26 13:24:45 +0200 |
commit | 0ccfa1491a17dd24b4dc82c1d9a40d2e48aac027 (patch) | |
tree | 6ae630e9f9d81944936b16c5499e232caecdeaa7 | |
parent | 5958fa43a9d0a190b293700cd31c018864d69ff5 (diff) |
openpgp: Implement the v6 cleartext signature framework.
-rw-r--r-- | openpgp/src/armor.rs | 59 | ||||
-rw-r--r-- | openpgp/src/parse/stream.rs | 16 | ||||
-rw-r--r-- | openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt | 16 | ||||
-rw-r--r-- | openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt.plain | 6 |
4 files changed, 83 insertions, 14 deletions
diff --git a/openpgp/src/armor.rs b/openpgp/src/armor.rs index d88ac3dc..1ff7d9f0 100644 --- a/openpgp/src/armor.rs +++ b/openpgp/src/armor.rs @@ -46,6 +46,7 @@ use base64::Engine; use base64::engine::general_purpose::STANDARD as base64std; use base64::engine::general_purpose::STANDARD_NO_PAD as base64nopad; +use crate::Fingerprint; use crate::packet::prelude::*; use crate::packet::header::{BodyLength, CTBNew, CTBOld}; use crate::parse::Cookie; @@ -1348,25 +1349,60 @@ impl<'a> Reader<'a> { match self.csft.as_ref().expect("CSFT has been initialized") { CSFTransformer::OPS => { // Determine the set of hash algorithms. - let mut algos: HashSet<HashAlgorithm> = self.headers.iter() - .filter(|(key, _value)| key == "Hash") - .flat_map(|(_key, value)| { - value.split(',') - .filter_map(|hash| hash.parse().ok()) + let mut algos: HashSet<(Vec<u8>, HashAlgorithm)> = + self.headers.iter() + .filter_map(|(key, value)| match key.as_str() { + "Hash" => Some(("", value.as_str())), + "SaltedHash" => { + let mut c = value.splitn(2, ':'); + let hash = c.next()?; + let salt = c.next()?; + Some((salt, hash)) + }, + _ => None, + }) + .flat_map(|(salt, value)| + -> Box<dyn Iterator<Item = (Vec<u8>, HashAlgorithm)>> { + if salt.is_empty() { + Box::new(value.split(',').filter_map( + |hash| Some((vec![], hash.parse().ok()?)))) + } else { + let mut r = Vec::with_capacity(1); + if let (Ok(hash), Ok(salt)) = + (value.parse(), + base64nopad.decode(salt)) + { + r.push((salt, hash)); + } + Box::new(r.into_iter()) + } }).collect(); if algos.is_empty() { // The default is MD5. + // XXX: the new default is to reject the message. #[allow(deprecated)] - algos.insert(HashAlgorithm::MD5); + algos.insert((vec![], HashAlgorithm::MD5)); } // Now create an OPS packet for every algorithm. let count = algos.len(); - for (i, &algo) in algos.iter().enumerate() { - let mut ops = OnePassSig3::new(SignatureType::Text); - ops.set_hash_algo(algo); - ops.set_last(i + 1 == count); + for (i, (salt, algo)) in algos.into_iter().enumerate() { + let ops: OnePassSig = if salt.is_empty() { + let mut ops = OnePassSig3::new(SignatureType::Text); + ops.set_hash_algo(algo); + ops.set_last(i + 1 == count); + ops.into() + } else { + let mut ops = OnePassSig6::new( + SignatureType::Text, + Fingerprint::V6(Default::default())); + ops.set_hash_algo(algo); + ops.set_salt(salt); + ops.set_last(i + 1 == count); + ops.into() + }; + Packet::from(ops).serialize(&mut self.decode_buffer) .expect("writing to vec does not fail"); } @@ -2351,6 +2387,9 @@ mod test { f(crate::tests::message("a-problematic-poem.txt.cleartext.sig"), crate::tests::message("a-problematic-poem.txt"), HashAlgorithm::SHA256)?; + f(crate::tests::file("crypto-refresh/cleartext-signed-message.txt"), + crate::tests::file("crypto-refresh/cleartext-signed-message.txt.plain"), + HashAlgorithm::SHA512)?; f(crate::tests::message("a-cypherpunks-manifesto.txt.cleartext.sig"), { // The transformation process trims trailing whitespace, diff --git a/openpgp/src/parse/stream.rs b/openpgp/src/parse/stream.rs index 8661143d..534d23ba 100644 --- a/openpgp/src/parse/stream.rs +++ b/openpgp/src/parse/stream.rs @@ -3159,11 +3159,12 @@ pub mod test { let p = P::new(); let certs = [ - "neal.pgp", - "testy-new.pgp", - "emmelie-dorothea-dina-samantha-awina-ed25519.pgp" + "keys/neal.pgp", + "keys/testy-new.pgp", + "keys/emmelie-dorothea-dina-samantha-awina-ed25519.pgp", + "crypto-refresh/v6-minimal-cert.key", ].iter() - .map(|f| Cert::from_bytes(crate::tests::key(f)).unwrap()) + .map(|f| Cert::from_bytes(crate::tests::file(f)).unwrap()) .collect::<Vec<_>>(); let tests = &[ // Signed messages. @@ -3218,6 +3219,13 @@ pub mod test { false, None, VHelper::new(1, 0, 0, 0, certs.clone())), + (crate::tests::file("crypto-refresh/cleartext-signed-message.txt") + .to_vec(), + crate::tests::file("crypto-refresh/cleartext-signed-message.txt.plain") + .to_vec(), + false, + None, + VHelper::new(1, 0, 0, 0, certs.clone())), // A key as example of an invalid message. (crate::tests::key("neal.pgp").to_vec(), crate::tests::manifesto().to_vec(), diff --git a/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt b/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt new file mode 100644 index 00000000..8a02b102 --- /dev/null +++ b/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt @@ -0,0 +1,16 @@ +-----BEGIN PGP SIGNED MESSAGE----- +SaltedHash: SHA512:dklfUCGIkPf14u48GCJRT3BQD1UdhuXJIeQE40pT+6w + +What we need from the grocery store: + +- - tofu +- - vegetables +- - noodles + +-----BEGIN PGP SIGNATURE----- + +wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6 +2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo +/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr +NK2ay45cX1IVAQ== +-----END PGP SIGNATURE----- diff --git a/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt.plain b/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt.plain new file mode 100644 index 00000000..f15c7c02 --- /dev/null +++ b/openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt.plain @@ -0,0 +1,6 @@ +What we need from the grocery store: + +- tofu +- vegetables +- noodles + |