diff options
author | Justus Winter <justus@sequoia-pgp.org> | 2021-04-20 15:52:25 +0200 |
---|---|---|
committer | Justus Winter <justus@sequoia-pgp.org> | 2021-04-26 13:13:22 +0200 |
commit | 0c349869786194214aca9ccb5f66640b28163f79 (patch) | |
tree | 5e2895d67d7fe9c58c99c7bf6ae9eb7ff6655809 | |
parent | 7961a663239567089508c7962a6c77d22b588c3a (diff) |
openpgp: Expose support for attested certifications.
- This is a low-level interface. We will provide nicer abstractions
in a followup.
- See #335.
-rw-r--r-- | openpgp/NEWS | 6 | ||||
-rw-r--r-- | openpgp/src/cert.rs | 30 | ||||
-rw-r--r-- | openpgp/src/packet/signature.rs | 47 | ||||
-rw-r--r-- | openpgp/src/packet/signature/subpacket.rs | 136 | ||||
-rw-r--r-- | openpgp/src/parse.rs | 45 | ||||
-rw-r--r-- | openpgp/src/policy.rs | 4 | ||||
-rw-r--r-- | openpgp/src/serialize.rs | 7 | ||||
-rw-r--r-- | sq/src/commands/dump.rs | 11 | ||||
-rw-r--r-- | sq/src/commands/key.rs | 30 |
9 files changed, 210 insertions, 106 deletions
diff --git a/openpgp/NEWS b/openpgp/NEWS index 782a5cb9..63b7b4f1 100644 --- a/openpgp/NEWS +++ b/openpgp/NEWS @@ -2,6 +2,12 @@ #+TITLE: sequoia-openpgp NEWS – history of user-visible changes #+STARTUP: content hidestars +* Changes in 1.2.0 +** New functionality + - SignatureBuilder::set_attested_certifications + - SubpacketAreas::attested_certifications + - SubpacketTag::AttestedCertifications + - SubpacketValue::AttestedCertifications * Changes in 1.1.0 ** New functionality - The new regex module provides regular expression support for diff --git a/openpgp/src/cert.rs b/openpgp/src/cert.rs index dad0b220..6339f311 100644 --- a/openpgp/src/cert.rs +++ b/openpgp/src/cert.rs @@ -6012,7 +6012,7 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= fn attested_key_signatures() -> Result<()> { use crate::{ crypto::hash::Hash, - packet::signature::{SignatureBuilder, subpacket::*}, + packet::signature::SignatureBuilder, types::*, }; @@ -6052,15 +6052,7 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= bob.userids().next().unwrap().userid().hash(&mut h); let attestation = SignatureBuilder::new(SignatureType__AttestedKey) - .modify_hashed_area(|mut a| { - a.add(Subpacket::new( - SubpacketValue::Unknown { - tag: SubpacketTag__AttestedCertifications, - body: digest, - }, - true)?)?; - Ok(a) - })? + .set_attested_certifications(vec![digest])? .sign_hash(&mut bob_signer, h)?; let bob = bob.insert_packets(vec![ @@ -6098,7 +6090,6 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= fn attested_key_signatures_dkgpg() -> Result<()> { const DUMP: bool = false; use crate::{ - packet::signature::subpacket::*, crypto::hash::Digest, }; @@ -6112,23 +6103,16 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= let attestation = &test.userids().next().unwrap().bundle().attestations[0]; - let digest_size = attestation.hash_algo().context()?.digest_size(); - let digests = if let Some(SubpacketValue::Unknown { body, .. }) = - attestation.subpacket(SubpacketTag__AttestedCertifications) - .map(|sp| sp.value()) - { - body.chunks(digest_size).map(|d| d.to_vec()).collect::<Vec<_>>() - } else { - unreachable!("Valid attestation signatures contain one"); - }; - if DUMP { - for (i, d) in digests.iter().enumerate() { + for (i, d) in attestation.attested_certifications()?.enumerate() { crate::fmt::hex::Dumper::new(std::io::stderr(), "") .write(d, format!("expected digest {}", i))?; } } + let digests: std::collections::HashSet<_> = + attestation.attested_certifications()?.collect(); + for (i, certification) in test.userids().next().unwrap().certifications().enumerate() { @@ -6142,7 +6126,7 @@ Pu1xwz57O4zo1VYf6TqHJzVC3OMvMUM2hhdecMUe5x6GorNaj6g= .write(&digest, format!("computed digest {}", i))?; } - assert!(digests.contains(&digest)); + assert!(digests.contains(&digest[..])); } Ok(()) diff --git a/openpgp/src/packet/signature.rs b/openpgp/src/packet/signature.rs index c824681a..36b308a2 100644 --- a/openpgp/src/packet/signature.rs +++ b/openpgp/src/packet/signature.rs @@ -2214,6 +2214,7 @@ impl crate::packet::Signature { | SignatureTarget | PreferredAEADAlgorithms | IntendedRecipient + | AttestedCertifications | Reserved(_) => false, Issuer @@ -2812,8 +2813,6 @@ impl Signature { R: key::KeyRole, { use crate::types::SignatureType__AttestedKey; - use crate::packet::signature::subpacket - ::SubpacketTag__AttestedCertifications; if self.typ() != SignatureType__AttestedKey { return Err(Error::UnsupportedSignatureType(self.typ()).into()); @@ -2821,29 +2820,14 @@ impl Signature { let mut hash = self.hash_algo().context()?; - if self.hashed_area() - .subpackets(SubpacketTag__AttestedCertifications).count() != 1 - || self.unhashed_area() - .subpackets(SubpacketTag__AttestedCertifications).count() != 0 + if self.attested_certifications()? + .any(|d| d.len() != hash.digest_size()) { return Err(Error::BadSignature( - "Wrong number of attested certification subpackets".into()) + "Wrong number of bytes in certification subpacket".into()) .into()); } - if let SubpacketValue::Unknown { body, .. } = - self.subpacket(SubpacketTag__AttestedCertifications).unwrap() - .value() - { - if body.len() % hash.digest_size() != 0 { - return Err(Error::BadSignature( - "Wrong number of bytes in certification subpacket".into()) - .into()); - } - } else { - unreachable!("Selected attested certifications, got wrong value"); - } - self.hash_userid_binding(&mut hash, pk, userid); self.verify_digest(signer, &hash.into_digest()?[..]) } @@ -2954,8 +2938,6 @@ impl Signature { R: key::KeyRole, { use crate::types::SignatureType__AttestedKey; - use crate::packet::signature::subpacket - ::SubpacketTag__AttestedCertifications; if self.typ() != SignatureType__AttestedKey { return Err(Error::UnsupportedSignatureType(self.typ()).into()); @@ -2963,29 +2945,14 @@ impl Signature { let mut hash = self.hash_algo().context()?; - if self.hashed_area() - .subpackets(SubpacketTag__AttestedCertifications).count() != 1 - || self.unhashed_area() - .subpackets(SubpacketTag__AttestedCertifications).count() != 0 + if self.attested_certifications()? + .any(|d| d.len() != hash.digest_size()) { return Err(Error::BadSignature( - "Wrong number of attested certification subpackets".into()) + "Wrong number of bytes in certification subpacket".into()) .into()); } - if let SubpacketValue::Unknown { body, .. } = - self.subpacket(SubpacketTag__AttestedCertifications).unwrap() - .value() - { - if body.len() % hash.digest_size() != 0 { - return Err(Error::BadSignature( - "Wrong number of bytes in certification subpacket".into()) - .into()); - } - } else { - unreachable!("Selected attested certifications, got wrong value"); - } - self.hash_user_attribute_binding(&mut hash, pk, ua); self.verify_digest(signer, &hash.into_digest()?[..]) } diff --git a/openpgp/src/packet/signature/subpacket.rs b/openpgp/src/packet/signature/subpacket.rs index bd522bbe..2f9570ef 100644 --- a/openpgp/src/packet/signature/subpacket.rs +++ b/openpgp/src/packet/signature/subpacket.rs @@ -317,6 +317,17 @@ pub enum SubpacketTag { /// /// [Section 5.2.3.29 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-09.html#section-5.2.3.29 IntendedRecipient, + /// The Attested Certifications subpacket (proposed). + /// + /// Allows the certificate holder to attest to third party + /// certifications, allowing them to be distributed with the + /// certificate. This can be used to address certificate flooding + /// concerns. + /// + /// See [Section 5.2.3.30 of RFC 4880bis] for details. + /// + /// [Section 5.2.3.30 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10.html#section-5.2.3.30 + AttestedCertifications, /// Reserved subpacket tag. Reserved(u8), /// Private subpacket tag. @@ -326,11 +337,6 @@ pub enum SubpacketTag { } assert_send_and_sync!(SubpacketTag); -/// The proposed Attested Certifications subpacket. -#[allow(non_upper_case_globals)] -pub(crate) const SubpacketTag__AttestedCertifications: SubpacketTag = - SubpacketTag::Unknown(37); - impl fmt::Display for SubpacketTag { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:?}", self) @@ -367,6 +373,7 @@ impl From<u8> for SubpacketTag { 33 => SubpacketTag::IssuerFingerprint, 34 => SubpacketTag::PreferredAEADAlgorithms, 35 => SubpacketTag::IntendedRecipient, + 37 => SubpacketTag::AttestedCertifications, 0| 1| 8| 13| 14| 15| 17| 18| 19 => SubpacketTag::Reserved(u), 100..=110 => SubpacketTag::Private(u), _ => SubpacketTag::Unknown(u), @@ -404,6 +411,7 @@ impl From<SubpacketTag> for u8 { SubpacketTag::IssuerFingerprint => 33, SubpacketTag::PreferredAEADAlgorithms => 34, SubpacketTag::IntendedRecipient => 35, + SubpacketTag::AttestedCertifications => 37, SubpacketTag::Reserved(u) => u, SubpacketTag::Private(u) => u, SubpacketTag::Unknown(u) => u, @@ -1575,6 +1583,17 @@ pub enum SubpacketValue { /// /// [Section 5.2.3.29 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-09.html#section-5.2.3.29 IntendedRecipient(Fingerprint), + /// The Attested Certifications subpacket (proposed). + /// + /// Allows the certificate holder to attest to third party + /// certifications, allowing them to be distributed with the + /// certificate. This can be used to address certificate flooding + /// concerns. + /// + /// See [Section 5.2.3.30 of RFC 4880bis] for details. + /// + /// [Section 5.2.3.30 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10.html#section-5.2.3.30 + AttestedCertifications(Vec<Box<[u8]>>), } assert_send_and_sync!(SubpacketValue); @@ -1669,6 +1688,7 @@ impl SubpacketValue { PreferredAEADAlgorithms(_) => SubpacketTag::PreferredAEADAlgorithms, IntendedRecipient(_) => SubpacketTag::IntendedRecipient, + AttestedCertifications(_) => SubpacketTag::AttestedCertifications, Unknown { tag, .. } => *tag, } } @@ -3652,6 +3672,59 @@ impl SubpacketAreas { } }) } + + /// Returns the digests of attested certifications. + /// + /// This feature is [experimental](crate#experimental-features). + /// + /// Allows the certificate holder to attest to third party + /// certifications, allowing them to be distributed with the + /// certificate. This can be used to address certificate flooding + /// concerns. + /// + /// Note: The maximum size of the hashed signature subpacket area + /// constrains the number of attestations that can be stored in a + /// signature. If the certificate holder attested to more + /// certifications, the digests are split across multiple attested + /// key signatures with the same creation time. + /// + /// The standard strongly suggests that the digests should be + /// sorted. However, this function returns the digests in the + /// order they are stored in the subpacket, which may not be + /// sorted. + /// + /// To address both issues, collect all digests from all attested + /// key signatures with the most recent creation time into a data + /// structure that allows efficient lookups, such as [`HashSet`] + /// or [`BTreeSet`]. + /// + /// See [Section 5.2.3.30 of RFC 4880bis] for details. + /// + /// [`HashSet`]: std::collections::HashSet + /// [`BTreeSet`]: std::collections::BTreeSet + /// [Section 5.2.3.30 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10.html#section-5.2.3.30 + pub fn attested_certifications(&self) + -> Result<impl Iterator<Item=&[u8]> + Send + Sync> + { + if self.hashed_area() + .subpackets(SubpacketTag::AttestedCertifications).count() > 1 + || self.unhashed_area() + .subpackets(SubpacketTag::AttestedCertifications).count() != 0 + { + return Err(Error::BadSignature( + "Wrong number of attested certification subpackets".into()) + .into()); + } + + Ok(self.subpackets(SubpacketTag::AttestedCertifications) + .flat_map(|sb| { + match sb.value() { + SubpacketValue::AttestedCertifications(digests) => + digests.iter().map(|d| d.as_ref()), + _ => unreachable!(), + } + })) + } } impl TryFrom<Signature> for Signature4 { @@ -6892,6 +6965,59 @@ impl signature::SignatureBuilder { Ok(self) } + + /// Adds an attested certifications subpacket. + /// + /// This feature is [experimental](crate#experimental-features). + /// + /// Allows the certificate holder to attest to third party + /// certifications, allowing them to be distributed with the + /// certificate. This can be used to address certificate flooding + /// concerns. + /// + /// Sorts the digests and adds an [Attested Certification + /// subpacket] to the hashed subpacket area. The digests must be + /// calculated using the same hash algorithm that is used in the + /// resulting signature. To attest a signature, hash it with + /// [`super::Signature::hash_for_confirmation`]. + /// + /// Note: The maximum size of the hashed signature subpacket area + /// constrains the number of attestations that can be stored in a + /// signature. If you need to attest to more certifications, + /// split the digests into chunks and create multiple attested key + /// signatures with the same creation time. + /// + /// See [Section 5.2.3.30 of RFC 4880bis] for details. + /// + /// [Section 5.2.3.30 of RFC 4880bis]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10.html#section-5.2.3.30 + /// [Attested Certification subpacket]: https://tools.ietf.org/html/draft-ietf-openpgp-rfc4880bis-10.html#section-5.2.3.30 + pub fn set_attested_certifications<A, C>(mut self, certifications: C) + -> Result<Self> + where C: IntoIterator<Item = A>, + A: AsRef<[u8]>, + { + let mut digests: Vec<_> = certifications.into_iter() + .map(|d| d.as_ref().to_vec().into_boxed_slice()) + .collect(); + + if let Some(first) = digests.get(0) { + if digests.iter().any(|d| d.len() != first.len()) { + return Err(Error::InvalidOperation( + "Inconsistent digest algorithm used".into()).into()); + } + } + + // Hashes SHOULD be sorted. This optimizes lookups for the + // consumer and provides a canonical form. + digests.sort_unstable(); + + self.hashed_area_mut().replace( + Subpacket::new( + SubpacketValue::AttestedCertifications(digests), + true)?)?; + + Ok(self) + } } #[test] diff --git a/openpgp/src/parse.rs b/openpgp/src/parse.rs index cdf9fb19..a410632b 100644 --- a/openpgp/src/parse.rs +++ b/openpgp/src/parse.rs @@ -1331,15 +1331,18 @@ impl Signature4 { let typ = php_try!(php.parse_u8("type")); let pk_algo: PublicKeyAlgorithm = php_try!(php.parse_u8("pk_algo")).into(); - let hash_algo = php_try!(php.parse_u8("hash_algo")); + let hash_algo: HashAlgorithm = + php_try!(php.parse_u8("hash_algo")).into(); let hashed_area_len = php_try!(php.parse_be_u16("hashed_area_len")); let hashed_area = php_try!(SubpacketArea::parse(&mut php, - hashed_area_len as usize)); + hashed_area_len as usize, + hash_algo)); let unhashed_area_len = php_try!(php.parse_be_u16("unhashed_area_len")); let unhashed_area = php_try!(SubpacketArea::parse(&mut php, - unhashed_area_len as usize)); + unhashed_area_len as usize, + hash_algo)); let digest_prefix1 = php_try!(php.parse_u8("digest_prefix1")); let digest_prefix2 = php_try!(php.parse_u8("digest_prefix2")); if ! pk_algo.for_signing() { @@ -1348,7 +1351,6 @@ impl Signature4 { let mpis = php_try!( crypto::mpi::Signature::_parse(pk_algo, &mut php)); - let hash_algo = hash_algo.into(); let typ = typ.into(); let need_hash = HashingMode::for_signature(hash_algo, typ); let mut pp = php.ok(Packet::Signature(Signature4::new( @@ -1511,12 +1513,15 @@ fn signature_parser_test () { impl SubpacketArea { // Parses a subpacket area. - fn parse<'a, T: 'a + BufferedReader<Cookie>>(php: &mut PacketHeaderParser<T>, mut limit: usize) - -> Result<Self> + fn parse<'a, T>(php: &mut PacketHeaderParser<T>, + mut limit: usize, + hash_algo: HashAlgorithm) + -> Result<Self> + where T: 'a + BufferedReader<Cookie>, { let mut packets = Vec::new(); while limit > 0 { - let p = Subpacket::parse(php, limit)?; + let p = Subpacket::parse(php, limit, hash_algo)?; assert!(limit >= p.length.len() + p.length.serialized_len()); limit -= p.length.len() + p.length.serialized_len(); packets.push(p); @@ -1528,8 +1533,12 @@ impl SubpacketArea { impl Subpacket { // Parses a raw subpacket. - fn parse<'a, T: 'a + BufferedReader<Cookie>>(php: &mut PacketHeaderParser<T>, limit: usize) - -> Result<Self> { + fn parse<'a, T>(php: &mut PacketHeaderParser<T>, + limit: usize, + hash_algo: HashAlgorithm) + -> Result<Self> + where T: 'a + BufferedReader<Cookie>, + { let length = SubpacketLength::parse(&mut php.reader)?; php.field("subpacket length", length.serialized_len()); let len = length.len() as usize; @@ -1745,6 +1754,24 @@ impl Subpacket { _ => Fingerprint::Invalid(bytes.into()), }) }, + SubpacketTag::AttestedCertifications => { + // If we don't know the hash algorithm, put all digest + // into one bucket. That way, at least it will + // roundtrip. It will never verify, because we don't + // know the hash. + let digest_size = + hash_algo.context().map(|c| c.digest_size()) + .unwrap_or(len); + + if len % digest_size != 0 { + return Err(Error::BadSignature( + "Wrong number of bytes in certification subpacket".into()) + .into()); + } + let bytes = php.parse_bytes("attested crts", len)?; + SubpacketValue::AttestedCertifications( + bytes.chunks(digest_size).map(Into::into).collect()) + }, SubpacketTag::Reserved(_) | SubpacketTag::PlaceholderForBackwardCompatibility | SubpacketTag::Private(_) diff --git a/openpgp/src/policy.rs b/openpgp/src/policy.rs index 75e5c5b8..70d097b2 100644 --- a/openpgp/src/policy.rs +++ b/openpgp/src/policy.rs @@ -635,7 +635,7 @@ a_cutoff_list!(SecondPreImageResistantHashCutoffList, HashAlgorithm, 12, ACCEPT, // 11. SHA224 ]); -a_cutoff_list!(SubpacketTagCutoffList, SubpacketTag, 36, +a_cutoff_list!(SubpacketTagCutoffList, SubpacketTag, 38, [ REJECT, // 0. Reserved. REJECT, // 1. Reserved. @@ -676,6 +676,8 @@ a_cutoff_list!(SubpacketTagCutoffList, SubpacketTag, 36, ACCEPT, // 33. IssuerFingerprint. ACCEPT, // 34. PreferredAEADAlgorithms. ACCEPT, // 35. IntendedRecipient. + REJECT, // 36. Reserved. + ACCEPT, // 37. AttestedCertifications. ]); a_cutoff_list!(AsymmetricAlgorithmCutoffList, AsymmetricAlgorithm, 18, diff --git a/openpgp/src/serialize.rs b/openpgp/src/serialize.rs index 6890533f..d13d461c 100644 --- a/openpgp/src/serialize.rs +++ b/openpgp/src/serialize.rs @@ -1483,6 +1483,11 @@ impl Marshal for SubpacketValue { _ => return Err(Error::InvalidArgument( "Unknown kind of fingerprint".into()).into()), } + AttestedCertifications(digests) => { + for digest in digests { + o.write_all(digest)?; + } + }, Unknown { body, .. } => o.write_all(body)?, } @@ -1530,6 +1535,8 @@ impl MarshalInto for SubpacketValue { // Educated guess for unknown versions. Fingerprint::Invalid(_) => 1 + fp.as_bytes().len(), }, + AttestedCertifications(digests) => + digests.iter().map(|d| d.len()).sum(), Unknown { body, .. } => body.len(), } } diff --git a/sq/src/commands/dump.rs b/sq/src/commands/dump.rs index 782344aa..081760a6 100644 --- a/sq/src/commands/dump.rs +++ b/sq/src/commands/dump.rs @@ -868,6 +868,17 @@ impl PacketDumper { .collect::<Vec<String>>().join(", "))?, IntendedRecipient(ref fp) => write!(output, "{} Intended Recipient: {}", i, fp)?, + AttestedCertifications(digests) => { + write!(output, "{} Attested Certifications:", i)?; + if digests.is_empty() { + writeln!(output, " None")?; + } else { + writeln!(output)?; + for d in digests { + writeln!(output, "{} {}", i, hex::encode(d))?; + } + } + }, // SubpacketValue is non-exhaustive. u => writeln!(output, "{} Unknown variant: {:?}", i, u)?, diff --git a/sq/src/commands/key.rs b/sq/src/commands/key.rs index 5dd45809..6073e886 100644 --- a/sq/src/commands/key.rs +++ b/sq/src/commands/key.rs @@ -429,15 +429,11 @@ fn attest_certifications(config: Config, m: &ArgMatches) // been standardized yet. use sequoia_openpgp::{ crypto::hash::{Hash, Digest}, - packet::signature::subpacket::*, types::HashAlgorithm, }; #[allow(non_upper_case_globals)] const SignatureType__AttestedKey: SignatureType = SignatureType::Unknown(0x16); - #[allow(non_upper_case_globals)] - const SubpacketTag__AttestedCertifications: SubpacketTag = - SubpacketTag::Unknown(37); // Attest to all certifications? let all = ! m.is_present("none"); // All is the default. @@ -495,21 +491,10 @@ fn attest_certifications(config: Config, m: &ArgMatches) uid.hash(&mut hash); for digests in attestations.chunks(digests_per_sig) { - let mut body = Vec::with_capacity(digest_size * digests.len()); - digests.iter().for_each(|d| body.extend(d)); - attestation_signatures.push( SignatureBuilder::new(SignatureType__AttestedKey) .set_signature_creation_time(t)? - .modify_hashed_area(|mut a| { - a.add(Subpacket::new( - SubpacketValue::Unknown { - tag: SubpacketTag__AttestedCertifications, - body, - }, - true)?)?; - Ok(a) - })? + .set_attested_certifications(digests)? .sign_hash(&mut pk_signer, hash.clone())?); } } @@ -538,21 +523,10 @@ fn attest_certifications(config: Config, m: &ArgMatches) ua.hash(&mut hash); for digests in attestations.chunks(digests_per_sig) { - let mut body = Vec::with_capacity(digest_size * digests.len()); - digests.iter().for_each(|d| body.extend(d)); - attestation_signatures.push( SignatureBuilder::new(SignatureType__AttestedKey) .set_signature_creation_time(t)? - .modify_hashed_area(|mut a| { - a.add(Subpacket::new( - SubpacketValue::Unknown { - tag: SubpacketTag__AttestedCertifications, - body, - }, - true)?)?; - Ok(a) - })? + .set_attested_certifications(digests)? .sign_hash(&mut pk_signer, hash.clone())?); } } |