diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2023-02-13 16:57:48 +0100 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2024-07-01 12:46:43 +0200 |
commit | 53ac52b6626dce9252cba68d36187073d7243f19 (patch) | |
tree | e2c933c04a4578078b50084001087deabcdac277 | |
parent | 75a1e25ccb7ccdc9ad52c34ef3ad0828407d1b5f (diff) |
openpgp: Add support for v6 signature packets.
todo:
- check salt lengths on verification
- salt length in arbitrary
- streaming signing
-rw-r--r-- | openpgp/src/cert.rs | 21 | ||||
-rw-r--r-- | openpgp/src/crypto/hash.rs | 202 | ||||
-rw-r--r-- | openpgp/src/packet/mod.rs | 18 | ||||
-rw-r--r-- | openpgp/src/packet/prelude.rs | 1 | ||||
-rw-r--r-- | openpgp/src/packet/signature.rs | 155 | ||||
-rw-r--r-- | openpgp/src/packet/signature/v6.rs | 282 | ||||
-rw-r--r-- | openpgp/src/parse.rs | 49 | ||||
-rw-r--r-- | openpgp/src/serialize.rs | 96 | ||||
-rw-r--r-- | openpgp/src/types/mod.rs | 14 | ||||
-rw-r--r-- | openpgp/tests/data/crypto-refresh/v6-minimal-cert.key | 12 | ||||
-rw-r--r-- | openpgp/tests/data/crypto-refresh/v6-minimal-secret.key | 14 |
11 files changed, 832 insertions, 32 deletions
diff --git a/openpgp/src/cert.rs b/openpgp/src/cert.rs index 9720d0d4..36cb984e 100644 --- a/openpgp/src/cert.rs +++ b/openpgp/src/cert.rs @@ -7541,7 +7541,28 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= let sp = crate::policy::StandardPolicy::new(); assert!(matches!(c.revocation_status(&sp, None), RevocationStatus::Revoked(_))); + Ok(()) + } + #[test] + fn v6_minimal_cert() -> Result<()> { + let p = &crate::policy::StandardPolicy::new(); + let t = None; // XXX + let cert = Cert::from_bytes( + crate::tests::file("crypto-refresh/v6-minimal-cert.key"))?; + assert_eq!(cert.userids().count(), 0); + let vcert = cert.with_policy(p, t)?; + assert_eq!(vcert.keys().count(), 2); + assert_eq!(vcert.keys().for_signing().count(), 1); + assert_eq!(vcert.keys().for_transport_encryption().count(), 1); + + let cert = Cert::from_bytes( + crate::tests::file("crypto-refresh/v6-minimal-secret.key")).unwrap(); + assert_eq!(cert.userids().count(), 0); + let vcert = cert.with_policy(p, t)?; + assert_eq!(vcert.keys().count(), 2); + assert_eq!(vcert.keys().for_signing().count(), 1); + assert_eq!(vcert.keys().for_transport_encryption().count(), 1); Ok(()) } } diff --git a/openpgp/src/crypto/hash.rs b/openpgp/src/crypto/hash.rs index f5cd05c1..40c0c551 100644 --- a/openpgp/src/crypto/hash.rs +++ b/openpgp/src/crypto/hash.rs @@ -40,7 +40,7 @@ use crate::packet::UserAttribute; use crate::packet::key; use crate::packet::key::{Key4, Key6}; use crate::packet::Signature; -use crate::packet::signature::{self, Signature3, Signature4}; +use crate::packet::signature::{self, Signature3, Signature4, Signature6}; use crate::Result; use crate::types::Timestamp; @@ -483,13 +483,14 @@ impl Hash for Signature { match self { Signature::V3(sig) => sig.hash(hash), Signature::V4(sig) => sig.hash(hash), + Signature::V6(sig) => sig.hash(hash), } } } impl Hash for Signature3 { fn hash(&self, hash: &mut dyn Digest) { - Self::hash_signature(self, hash); + Self::hash_fields(hash, self); } } @@ -498,7 +499,7 @@ impl Signature3 { /// /// Because we need to call this from SignatureFields::hash, we /// provide this as associated method. - fn hash_signature(f: &signature::SignatureFields, hash: &mut dyn Digest) { + fn hash_fields(hash: &mut dyn Digest, f: &signature::SignatureFields) { // XXX: Annoyingly, we have no proper way of handling errors // here. @@ -526,7 +527,7 @@ impl Signature3 { impl Hash for Signature4 { fn hash(&self, hash: &mut dyn Digest) { - Self::hash_signature(self, hash); + Self::hash_fields(hash, &self.fields); } } @@ -535,8 +536,7 @@ impl Signature4 { /// /// Because we need to call this from SignatureFields::hash, we /// provide this as associated method. - fn hash_signature(f: &signature::SignatureFields, mut hash: &mut dyn Digest) - { + fn hash_fields(mut hash: &mut dyn Digest, f: &signature::SignatureFields) { use crate::serialize::{Marshal, MarshalInto}; // A version 4 signature packet is laid out as follows: @@ -590,23 +590,100 @@ impl Signature4 { } } +impl Hash for Signature6 { + fn hash(&self, hash: &mut dyn Digest) { + Self::hash_fields(hash, &self.fields); + } +} + +impl Signature6 { + fn hash_fields(mut hash: &mut dyn Digest, sig: &signature::SignatureFields) { + use crate::serialize::{Marshal, MarshalInto}; + + // A version 6 signature packet is laid out as follows: + // + // version - 1 byte \ + // type - 1 byte \ + // pk_algo - 1 byte \ + // hash_algo - 1 byte Included in the hash + // hashed_area_len - 4 bytes (big endian)/ + // hashed_area _/ + // ... <- Not included in the hash + + let mut header = [0u8; 8]; + + // Version. + header[0] = 6; + header[1] = sig.typ().into(); + header[2] = sig.pk_algo().into(); + header[3] = sig.hash_algo().into(); + + // The length of the hashed area, as a 32-bit big endian number. + let hashed_area_len = sig.hashed_area().serialized_len(); + header[4..8].copy_from_slice(&(hashed_area_len as u32).to_be_bytes()); + + hash.update(&header[..]); + + // XXX: Annoyingly, we have no proper way of handling errors + // here. + let _ = sig.hashed_area().serialize(&mut hash as &mut dyn Write); + + // A version 6 signature trailer is: + // + // version - 1 byte + // 0xFF (constant) - 1 byte + // amount - 4 bytes (big endian) + // + // The amount field is the amount of hashed from this + // packet (this excludes the message content, and this + // trailer) modulo 2**32. + // + // See https://tools.ietf.org/html/rfc4880#section-5.2.4 + let mut trailer = [0u8; 6]; + + trailer[0] = 6; + trailer[1] = 0xff; + // The signature packet's length, not including the previous + // two bytes and the length modulo 2**32. + let len = (header.len() + hashed_area_len) as u32; + trailer[2..6].copy_from_slice(&len.to_be_bytes()); + + hash.update(&trailer[..]); + } +} + impl Hash for signature::SignatureFields { fn hash(&self, hash: &mut dyn Digest) { match self.version() { - 3 => Signature3::hash_signature(self, hash), - 4 => Signature4::hash_signature(self, hash), + 3 => Signature3::hash_fields(hash, self), + 4 => Signature4::hash_fields(hash, self), + 6 => Signature6::hash_fields(hash, self), _ => (), } } } +impl Hash for signature::SignatureBuilder { + fn hash(&self, hash: &mut dyn Digest) { + match self.version { + signature::SBVersion::V4 {} => + Signature4::hash_fields(hash, &self.fields), + signature::SBVersion::V6 { .. } => + Signature6::hash_fields(hash, &self.fields), + } + } +} + /// Hashing-related functionality. /// /// <a id="hashing-functions"></a> -impl signature::SignatureFields { +impl signature::SignatureBuilder { /// Hashes this standalone signature. pub fn hash_standalone(&self, hash: &mut dyn Digest) { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } self.hash(hash); } @@ -622,6 +699,9 @@ impl signature::SignatureFields { key: &Key<P, key::PrimaryRole>) where P: key::KeyParts, { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } key.hash(hash); self.hash(hash); } @@ -634,6 +714,9 @@ impl signature::SignatureFields { where P: key::KeyParts, Q: key::KeyParts, { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } key.hash(hash); subkey.hash(hash); self.hash(hash); @@ -647,6 +730,9 @@ impl signature::SignatureFields { where P: key::KeyParts, Q: key::KeyParts, { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } self.hash_subkey_binding(hash, key, subkey); } @@ -657,6 +743,9 @@ impl signature::SignatureFields { userid: &UserID) where P: key::KeyParts, { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } key.hash(hash); userid.hash(hash); self.hash(hash); @@ -672,6 +761,9 @@ impl signature::SignatureFields { ua: &UserAttribute) where P: key::KeyParts, { + if let Some(salt) = self.prefix_salt() { + hash.update(salt); + } key.hash(hash); ua.hash(hash); self.hash(hash); @@ -682,12 +774,104 @@ impl signature::SignatureFields { /// /// <a id="hashing-functions"></a> impl Signature { + /// Hashes this standalone signature. + pub fn hash_standalone(&self, hash: &mut dyn Digest) + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + self.hash(hash); + } + + /// Hashes this timestamp signature. + pub fn hash_timestamp(&self, hash: &mut dyn Digest) + { + self.hash_standalone(hash); + } + + /// Hashes this direct key signature over the specified primary + /// key, and the primary key. + pub fn hash_direct_key<P>(&self, hash: &mut dyn Digest, + key: &Key<P, key::PrimaryRole>) + where P: key::KeyParts, + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + key.hash(hash); + self.hash(hash); + } + + /// Hashes this subkey binding over the specified primary key and + /// subkey, the primary key, and the subkey. + pub fn hash_subkey_binding<P, Q>(&self, hash: &mut dyn Digest, + key: &Key<P, key::PrimaryRole>, + subkey: &Key<Q, key::SubordinateRole>) + where P: key::KeyParts, + Q: key::KeyParts, + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + key.hash(hash); + subkey.hash(hash); + self.hash(hash); + } + + /// Hashes this primary key binding over the specified primary key + /// and subkey, the primary key, and the subkey. + pub fn hash_primary_key_binding<P, Q>(&self, hash: &mut dyn Digest, + key: &Key<P, key::PrimaryRole>, + subkey: &Key<Q, key::SubordinateRole>) + where P: key::KeyParts, + Q: key::KeyParts, + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + self.hash_subkey_binding(hash, key, subkey); + } + + /// Hashes this user ID binding over the specified primary key and + /// user ID, the primary key, and the userid. + pub fn hash_userid_binding<P>(&self, hash: &mut dyn Digest, + key: &Key<P, key::PrimaryRole>, + userid: &UserID) + where P: key::KeyParts, + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + key.hash(hash); + userid.hash(hash); + self.hash(hash); + } + + /// Hashes this user attribute binding over the specified primary + /// key and user attribute, the primary key, and the user + /// attribute. + pub fn hash_user_attribute_binding<P>( + &self, + hash: &mut dyn Digest, + key: &Key<P, key::PrimaryRole>, + ua: &UserAttribute) + where P: key::KeyParts, + { + if let Some(salt) = self.salt() { + hash.update(salt); + } + key.hash(hash); + ua.hash(hash); + self.hash(hash); + } + /// Hashes this signature for use in a Third-Party Confirmation /// signature. pub fn hash_for_confirmation(&self, hash: &mut dyn Digest) { match self { Signature::V3(s) => s.hash_for_confirmation(hash), Signature::V4(s) => s.hash_for_confirmation(hash), + Signature::V6(s) => s.hash_for_confirmation(hash), } } } diff --git a/openpgp/src/packet/mod.rs b/openpgp/src/packet/mod.rs index 05b79869..81c738a4 100644 --- a/openpgp/src/packet/mod.rs +++ b/openpgp/src/packet/mod.rs @@ -1012,6 +1012,9 @@ pub enum Signature { /// Signature packet version 4. V4(self::signature::Signature4), + + /// Signature packet version 6. + V6(self::signature::Signature6), } assert_send_and_sync!(Signature); @@ -1021,6 +1024,7 @@ impl Signature { match self { Signature::V3(_) => 3, Signature::V4(_) => 4, + Signature::V6(_) => 6, } } } @@ -1031,6 +1035,18 @@ impl From<Signature> for Packet { } } +impl Signature { + /// Gets the salt, if any. + pub fn salt(&self) -> Option<&[u8]> { + match self { + Signature::V3(_) => None, + Signature::V4(_) => None, + Signature::V6(s) => Some(s.salt()), + } + } + +} + // Trivial forwarder for singleton enum. impl Deref for Signature { type Target = signature::Signature4; @@ -1039,6 +1055,7 @@ impl Deref for Signature { match self { Signature::V3(sig) => &sig.intern, Signature::V4(sig) => sig, + Signature::V6(sig) => &sig.common, } } } @@ -1049,6 +1066,7 @@ impl DerefMut for Signature { match self { Signature::V3(ref mut sig) => &mut sig.intern, Signature::V4(ref mut sig) => sig, + Signature::V6(ref mut sig) => &mut sig.common, } } } diff --git a/openpgp/src/packet/prelude.rs b/openpgp/src/packet/prelude.rs index cd69d3dd..5c3d7782 100644 --- a/openpgp/src/packet/prelude.rs +++ b/openpgp/src/packet/prelude.rs @@ -52,6 +52,7 @@ pub use crate::packet::{ seip::SEIP1, signature, signature::Signature4, + signature::Signature6, signature::SignatureBuilder, skesk::SKESK4, skesk::SKESK5, diff --git a/openpgp/src/packet/signature.rs b/openpgp/src/packet/signature.rs index 7df24cf2..d3c70a69 100644 --- a/openpgp/src/packet/signature.rs +++ b/openpgp/src/packet/signature.rs @@ -156,7 +156,7 @@ use crate::types::Timestamp; #[cfg(test)] /// Like quickcheck::Arbitrary, but bounded. -trait ArbitraryBounded { +pub(crate) trait ArbitraryBounded { /// Generates an arbitrary value, but only recurses if `depth > /// 0`. fn arbitrary_bounded(g: &mut Gen, depth: usize) -> Self; @@ -181,6 +181,8 @@ macro_rules! impl_arbitrary_with_bound { pub mod subpacket; pub mod cache; +mod v6; +pub use v6::Signature6; /// How many seconds to backdate signatures. /// @@ -288,6 +290,20 @@ impl SignatureFields { } } +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub(crate) enum SBVersion { + V4 {}, + V6 { + salt: Vec<u8>, + }, +} + +impl Default for SBVersion { + fn default() -> Self { + SBVersion::V4 {} + } +} + /// A Signature builder. /// /// The `SignatureBuilder` is used to create [`Signature`]s. Although @@ -454,7 +470,8 @@ pub struct SignatureBuilder { reference_time: Option<SystemTime>, overrode_creation_time: bool, original_creation_time: Option<SystemTime>, - fields: SignatureFields, + pub(crate) fields: SignatureFields, + pub(crate) version: SBVersion, } assert_send_and_sync!(SignatureBuilder); @@ -485,7 +502,8 @@ impl SignatureBuilder { pk_algo: PublicKeyAlgorithm::Unknown(0), hash_algo: HashAlgorithm::default(), subpackets: SubpacketAreas::default(), - } + }, + version: SBVersion::default(), } } @@ -1705,7 +1723,24 @@ impl SignatureBuilder { /// # Ok(()) } /// ``` pub fn pre_sign(mut self, signer: &dyn Signer) -> Result<Self> { - self.pk_algo = signer.public().pk_algo(); + let pk = signer.public(); + self.pk_algo = pk.pk_algo(); + + // Set the version. A Key6 will create a Signature6, a key4 + // will create a Signature4. If necessary, generate a salt. + // If the salt has been explicitly set, it is not changed. + self.version = match (self.version, pk.version()) { + (SBVersion::V4 {}, 4) => SBVersion::V4 {}, + (SBVersion::V6 { .. }, 4) => SBVersion::V4 {}, + (SBVersion::V4 {}, 6) => { + let mut salt = vec![0; self.fields.hash_algo().salt_size()?]; + crate::crypto::random(&mut salt); + SBVersion::V6 { salt } + }, + (SBVersion::V6 { salt }, 6) => SBVersion::V6 { salt }, + (_, n) => return Err(Error::InvalidOperation( + format!("Unsupported key version {}", n)).into()), + }; // Set the creation time. if ! self.overrode_creation_time { @@ -1714,31 +1749,83 @@ impl SignatureBuilder { } } - // Make sure we have an issuer packet. - if self.issuers().next().is_none() - && self.issuer_fingerprints().next().is_none() - { - self = self.set_issuer(signer.public().keyid())? - .set_issuer_fingerprint(signer.public().fingerprint())?; - } + match &self.version { + SBVersion::V4 {} => { + // Make sure we have an issuer packet. + if self.issuers().next().is_none() + && self.issuer_fingerprints().next().is_none() + { + self = self.set_issuer(signer.public().keyid())? + .set_issuer_fingerprint(signer.public().fingerprint())?; + } + + // Add a salt to v4 signatures to make the signature + // unpredictable. + let mut salt = [0; 32]; + crate::crypto::random(&mut salt); + self = self.set_notation("salt@notations.sequoia-pgp.org", + salt, None, false)?; + }, + SBVersion::V6 { .. } => { + // Make sure we have an issuer fingerprint packet. + if self.issuer_fingerprints().next().is_none() { + self = self + .set_issuer_fingerprint(signer.public().fingerprint())?; + } - // Add a salt to make the signature unpredictable. - let mut salt = [0; 32]; - crate::crypto::random(&mut salt); - self = self.set_notation("salt@notations.sequoia-pgp.org", - salt, None, false)?; + // In v6 signatures, we have a proper prefix salt. + }, + } self.sort(); Ok(self) } + /// Returns the prefix salt. + /// + /// In OpenPGP v6 signatures, a salt is prefixed to the data + /// stream hashed in the signature. If a v6 signature is + /// generated by using a v6 key, then a salt will be added + /// automatically. If a salt has been set explicitly (see + /// [`SignatureBuilder::set_prefix_salt`]), this function will + /// return the salt. + pub fn prefix_salt(&self) -> Option<&[u8]> { + match &self.version { + SBVersion::V4 {} => None, + SBVersion::V6 { salt } => Some(salt), + } + } + + /// Explicitly sets the prefix salt. + /// + /// In OpenPGP v6 signatures, a salt is prefixed to the data + /// stream hashed in the signature. If a v6 signature is + /// generated by using a v6 key, then a salt will be added + /// automatically. In general, you do not need to call this + /// function. + /// + /// When streaming a signed message, it is useful to explicitly + /// set the salt using this function. + pub fn set_prefix_salt(mut self, new_salt: Vec<u8>) -> (Self, Option<Vec<u8>>) { + let mut old = None; + + self.version = match std::mem::take(&mut self.version) { + SBVersion::V4 {} => SBVersion::V6 { salt: new_salt }, + SBVersion::V6 { salt } => { + old = Some(salt); + SBVersion::V6 { salt: new_salt } + }, + }; + + (self, old) + } + fn sign(self, signer: &mut dyn Signer, digest: Vec<u8>) -> Result<Signature> { let mpis = signer.sign(self.hash_algo, &digest)?; - - Ok(Signature4 { + let v4 = Signature4 { common: Default::default(), fields: self.fields, digest_prefix: [digest[0], digest[1]], @@ -1746,7 +1833,13 @@ impl SignatureBuilder { computed_digest: digest.into(), level: 0, additional_issuers: OnceLock::new(), - }.into()) + }; + + match self.version { + SBVersion::V4 {} => Ok(v4.into()), + SBVersion::V6 { salt } => + Ok(Signature6::from_common(v4, salt)?.into()) + } } } @@ -1755,6 +1848,7 @@ impl From<Signature> for SignatureBuilder { match sig { Signature::V3(sig) => sig.into(), Signature::V4(sig) => sig.into(), + Signature::V6(sig) => sig.into(), } } } @@ -1780,10 +1874,17 @@ impl From<Signature4> for SignatureBuilder { overrode_creation_time: false, original_creation_time: creation_time, fields, + version: Default::default(), } } } +impl From<Signature6> for SignatureBuilder { + fn from(sig: Signature6) -> Self { + SignatureBuilder::from(sig.common) + } +} + /// Holds a v4 Signature packet. /// /// This holds a [version 4] Signature packet. Normally, you won't @@ -2799,6 +2900,13 @@ impl Signature { key.verify(self.mpis(), self.hash_algo(), digest) }; + // XXX if hash_algo.salt_size().map(|expected| expected != salt_len) + // XXX .unwrap_or(false) + // XXX { + // XXX return php.fail(format!("bad salt length, expected {} got {}", + // XXX ); + // XXX } + if result.is_ok() { // Mark information in this signature as authenticated. @@ -3506,10 +3614,11 @@ impl From<Signature4> for super::Signature { #[cfg(test)] impl ArbitraryBounded for super::Signature { fn arbitrary_bounded(g: &mut Gen, depth: usize) -> Self { - if bool::arbitrary(g) { - Signature3::arbitrary_bounded(g, depth).into() - } else { - Signature4::arbitrary_bounded(g, depth).into() + match u8::arbitrary(g) % 3 { + 0 => Signature3::arbitrary_bounded(g, depth).into(), + 1 => Signature4::arbitrary_bounded(g, depth).into(), + 2 => Signature6::arbitrary_bounded(g, depth).into(), + _ => unreachable!(), } } } diff --git a/openpgp/src/packet/signature/v6.rs b/openpgp/src/packet/signature/v6.rs new file mode 100644 index 00000000..e045f8e3 --- /dev/null +++ b/openpgp/src/packet/signature/v6.rs @@ -0,0 +1,282 @@ +//! OpenPGP v6 signature implementation. + +use std::cmp::Ordering; +use std::convert::TryFrom; +use std::fmt; +use std::ops::{Deref, DerefMut}; + +use crate::{ + Error, + HashAlgorithm, + Packet, + PublicKeyAlgorithm, + Result, + SignatureType, + crypto::mpi, + packet::{ + Signature, + signature::{ + Signature4, + subpacket::{ + SubpacketArea, + }, + }, + }, +}; + +/// Holds a v6 Signature packet. +/// +/// This holds a [version 6] Signature packet. Normally, you won't +/// directly work with this data structure, but with the [`Signature`] +/// enum, which is version agnostic. An exception is when you need to +/// do version-specific operations. But currently, there aren't any +/// version-specific methods. +/// +/// [version 6]: https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-08.html#name-version-4-and-6-signature-p |