summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2023-04-07 13:40:50 +0200
committerJustus Winter <justus@sequoia-pgp.org>2023-06-26 13:24:45 +0200
commit0ccfa1491a17dd24b4dc82c1d9a40d2e48aac027 (patch)
tree6ae630e9f9d81944936b16c5499e232caecdeaa7
parent5958fa43a9d0a190b293700cd31c018864d69ff5 (diff)
openpgp: Implement the v6 cleartext signature framework.
-rw-r--r--openpgp/src/armor.rs59
-rw-r--r--openpgp/src/parse/stream.rs16
-rw-r--r--openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt16
-rw-r--r--openpgp/tests/data/crypto-refresh/cleartext-signed-message.txt.plain6
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
+