From 89ab24d5a46c78572c356f8d5cf7987092471a87 Mon Sep 17 00:00:00 2001 From: "Neal H. Walfield" Date: Wed, 10 Jan 2024 14:49:27 +0100 Subject: openpgp: Simplify validating third-party certifications. - Add `UserIDAmalgamation::valid_certifications_by_key` and `KeyAmalgamation::valid_certifications_by_key`, which return third-party certifications that were made by a particular key at a particular time, i.e., they also verify that the signatures are valid. - Add `UserIDAmalgamation::active_certifications_by_key` and `KeyAmalgamation::active_certifications_by_key`, which return the active certifications made by a particular key at a particular time. --- openpgp/NEWS | 4 + openpgp/src/cert/amalgamation.rs | 517 +++++++++++++++++++++++++++++++++- openpgp/src/cert/amalgamation/key.rs | 524 +++++++++++++++++++++++++++++++++++ 3 files changed, 1044 insertions(+), 1 deletion(-) diff --git a/openpgp/NEWS b/openpgp/NEWS index 96c98873..ff47ce37 100644 --- a/openpgp/NEWS +++ b/openpgp/NEWS @@ -6,6 +6,10 @@ * Changes in 1.18.0 ** New functionality - ComponentAmalgamation::certifications_by_key + - UserIDAmalgamation::valid_certifications_by_key + - KeyAmalgamation::valid_certifications_by_key + - UserIDAmalgamation::active_certifications_by_key + - KeyAmalgamation::active_certifications_by_key * Changes in 1.17.0 ** Notable fixes - Sequoia now ignores some formatting errors when reading secret diff --git a/openpgp/src/cert/amalgamation.rs b/openpgp/src/cert/amalgamation.rs index 6f77e378..2ef37eba 100644 --- a/openpgp/src/cert/amalgamation.rs +++ b/openpgp/src/cert/amalgamation.rs @@ -222,7 +222,10 @@ //! [`Deref` trait]: std::ops::Deref //! [`ComponentAmalgamation::component`]: ComponentAmalgamation::component() use std::time; -use std::time::SystemTime; +use std::time::{ + Duration, + SystemTime, +}; use std::clone::Clone; use std::borrow::Borrow; @@ -231,6 +234,7 @@ use crate::{ crypto::{Signer, hash::{Hash, Digest}}, Error, KeyHandle, + packet, packet::{ Signature, Unknown, @@ -893,6 +897,115 @@ impl<'a, C> ComponentAmalgamation<'a, C> { -> impl Iterator + Send + Sync { self.bundle().signatures() } + + // Used to implement + // [`UserIDAmalgamation::valid_certifications_by_key`], + // [`KeyAmalgamation::valid_certifications_by_key`], + // [`UserIDAmalgamation::active_certifications_by_key`], and + // [`KeyAmalgamation::active_certifications_by_key`]. + fn valid_certifications_by_key_( + &self, + policy: &'a dyn Policy, + reference_time: Option, + issuer: &'a packet::Key, + only_active: bool, + verify_certification: F) + -> impl Iterator + Send + Sync + where + F: Fn(&Signature) -> Result<()> + { + let reference_time = reference_time.unwrap_or_else(crate::now); + let issuer_handle = issuer.key_handle(); + + let mut certifications: Vec<(&Signature, _)> = self + .certifications_by_key(&[ issuer_handle ]) + .filter_map(|certification| { + // Extract the signature's creation time. Ignore + // certifications without a creation time: those are + // malformed. + certification + .signature_creation_time() + .map(|ct| (certification, ct)) + }) + .filter(|(_certification, ct)| { + // Skip certifications created after the reference + // time. + *ct <= reference_time + }) + .filter(|(certification, ct)| { + // Check that the certification is not expired as of + // the reference time. + if let Some(validity) + = certification.signature_validity_period() + { + if validity == Duration::new(0, 0) { + // "If this is not present or has a value of + // zero, it never expires." + // + // https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-13.html#name-key-expiration-time + true + } else { + // "the number of seconds after the signature + // creation time that the signature expires" + // + // Assume validity = 1 second, then: + // + // expiry time reference time status + // ----------- -------------- ------ + // > ct live + // ct + 1 = ct + 1 expired + // < ct + 2 expired + *ct + validity > reference_time + } + } else { + true + } + }) + .filter(|(_certification, ct)| { + // Make sure the certification was created after the + // certificate. + self.cert.primary_key().creation_time() <= *ct + }) + .filter(|(certification, _ct)| { + // Make sure the certification conforms to the policy. + policy + .signature(certification, + HashAlgoSecurity::CollisionResistance) + .is_ok() + }) + .collect(); + + // Sort the certifications by creation time so that the newest + // certifications come first. + certifications.sort_unstable_by(|(_, a), (_, b)| { + a.cmp(b).reverse() + }); + + // Check that the issuer actually made the signatures, and + // collect the most recent certifications. + let mut valid = Vec::new(); + for (certification, ct) in certifications.into_iter() { + if only_active { + if let Some((_active, active_ct)) = valid.get(0) { + if *active_ct != ct { + // This certification is further in the past. + // We're done. + break; + } + } + } + + if let Ok(()) = verify_certification(certification) { + valid.push((certification, ct)); + } + } + + valid.into_iter() + .map(|(certification, _creation_time)| certification) + .collect::>() + .into_iter() + } } macro_rules! impl_with_policy { @@ -954,6 +1067,189 @@ impl<'a> UserIDAmalgamation<'a> { self.component() } + /// Returns the third-party certifications issued by the specified + /// key, and valid at the specified time. + /// + /// This function returns the certifications issued by the + /// specified key. Specifically, it returns a certification if: + /// + /// - it is well formed, + /// - it is live with respect to the reference time, + /// - it conforms to the policy, and + /// - the signature is cryptographically valid. + /// + /// This method is implemented on a [`UserIDAmalgamation`] and not + /// a [`ValidUserIDAmalgamation`], because a third-party + /// certification does not require the user ID to be self signed. + /// + /// # Examples + /// + /// Alice has certified that a certificate belongs to Bob on two + /// occasions. Whereas + /// [`UserIDAmalgamation::valid_certifications_by_key`] returns + /// both certifications, + /// [`UserIDAmalgamation::active_certifications_by_key`] only + /// returns the most recent certification. + /// + /// ```rust + /// use sequoia_openpgp as openpgp; + /// use openpgp::cert::prelude::*; + /// # use openpgp::packet::signature::SignatureBuilder; + /// # use openpgp::packet::UserID; + /// use openpgp::policy::StandardPolicy; + /// # use openpgp::types::SignatureType; + /// + /// const P: &StandardPolicy = &StandardPolicy::new(); + /// + /// # fn main() -> openpgp::Result<()> { + /// # let epoch = std::time::SystemTime::now() + /// # - std::time::Duration::new(100, 0); + /// # let t0 = epoch; + /// # + /// # let (alice, _) = CertBuilder::new() + /// # .set_creation_time(t0) + /// # .add_userid("") + /// # .generate() + /// # .unwrap(); + /// let alice: Cert = // ... + /// # alice; + /// # + /// # let bob_userid = ""; + /// # let (bob, _) = CertBuilder::new() + /// # .set_creation_time(t0) + /// # .add_userid(bob_userid) + /// # .generate() + /// # .unwrap(); + /// let bob: Cert = // ... + /// # bob; + /// + /// # // Alice has not certified Bob's User ID. + /// # let ua = bob.userids().next().expect("have a user id"); + /// # assert_eq!( + /// # ua.active_certifications_by_key( + /// # P, t0, alice.primary_key().key()).count(), + /// # 0); + /// # + /// # // Have Alice certify Bob's certificate. + /// # let mut alice_signer = alice + /// # .keys() + /// # .with_policy(P, None) + /// # .for_certification() + /// # .next().expect("have a certification-capable key") + /// # .key() + /// # .clone() + /// # .parts_into_secret().expect("have unencrypted key material") + /// # .into_keypair().expect("have unencrypted key material"); + /// # + /// # let mut bob = bob; + /// # for i in 1..=2usize { + /// # let ti = t0 + std::time::Duration::new(i as u64, 0); + /// # + /// # let certification = SignatureBuilder::new(SignatureType::GenericCertification) + /// # .set_signature_creation_time(ti)? + /// # .sign_userid_binding( + /// # &mut alice_signer, + /// # bob.primary_key().key(), + /// # &UserID::from(bob_userid))?; + /// # bob = bob.insert_packets(certification)?; + /// # + /// # let ua = bob.userids().next().expect("have a user id"); + /// # assert_eq!( + /// # ua.valid_certifications_by_key( + /// # P, ti, alice.primary_key().key()).count(), + /// # i); + /// # + /// # assert_eq!( + /// # ua.active_certifications_by_key( + /// # P, ti, alice.primary_key().key()).count(), + /// # 1); + /// # } + /// let ua = bob.userids().next().expect("have user id"); + /// + /// let valid_certifications = ua.valid_certifications_by_key( + /// P, None, alice.primary_key().key()); + /// // Alice certified Bob's certificate twice. + /// assert_eq!(valid_certifications.count(), 2); + /// + /// let active_certifications = ua.active_certifications_by_key( + /// P, None, alice.primary_key().key()); + /// // But only the most recent one is active. + /// assert_eq!(active_certifications.count(), 1); + /// # Ok(()) } + /// ``` + pub fn valid_certifications_by_key(&self, + policy: &'a dyn Policy, + reference_time: T, + issuer: PK) + -> impl Iterator + Send + Sync + where + T: Into>, + PK: Into<&'a packet::Key>, + { + let reference_time = reference_time.into(); + let issuer = issuer.into(); + + self.valid_certifications_by_key_( + policy, reference_time, issuer, false, + |sig| { + sig.clone().verify_userid_binding( + issuer, + self.cert.primary_key().key(), + self.userid()) + }) + } + + /// Returns any active third-party certifications issued by the + /// specified key. + /// + /// This function is like + /// [`UserIDAmalgamation::valid_certifications_by_key`], but it + /// only returns active certifications. Active certifications are + /// the most recent valid certifications with respect to the + /// reference time. + /// + /// Although there is normally only a single active certification, + /// there can be multiple certifications with the same timestamp. + /// In this case, all of them are returned. + /// + /// Unlike self-signatures, multiple third-party certifications + /// issued by the same key at the same time can be sensible. For + /// instance, Alice may fully trust a CA for user IDs in a + /// particular domain, and partially trust it for everything else. + /// This can only be expressed using multiple certifications. + /// + /// This method is implemented on a [`UserIDAmalgamation`] and not + /// a [`ValidUserIDAmalgamation`], because a third-party + /// certification does not require the user ID to be self signed. + /// + /// # Examples + /// + /// See the examples for + /// [`UserIDAmalgamation::valid_certifications_by_key`]. + pub fn active_certifications_by_key(&self, + policy: &'a dyn Policy, + reference_time: T, + issuer: PK) + -> impl Iterator + Send + Sync + where + T: Into>, + PK: Into<&'a packet::Key>, + { + let reference_time = reference_time.into(); + let issuer = issuer.into(); + + self.valid_certifications_by_key_( + policy, reference_time, issuer, true, + |sig| { + sig.clone().verify_userid_binding( + issuer, + self.cert.primary_key().key(), + self.userid()) + }) + } + /// Attests to third-party certifications. /// /// This feature is [experimental](crate#experimental-features). @@ -1883,6 +2179,8 @@ impl<'a, C> crate::cert::Preferences<'a> mod test { use super::*; + use std::time::UNIX_EPOCH; + use crate::policy::StandardPolicy as P; use crate::packet::signature::SignatureBuilder; use crate::packet::UserID; @@ -2018,4 +2316,221 @@ mod test { Ok(()) } + + #[test] + fn user_id_amalgamation_certifications_by_key() -> Result<()> { + // Alice and Bob certify Carol's certificate. We then check + // that valid_certifications_by_key and + // active_certifications_by_key return them. + let p = &crate::policy::StandardPolicy::new(); + + // $ date -u -d '2024-01-02 13:00' +%s + let t0 = UNIX_EPOCH + Duration::new(1704200400, 0); + // $ date -u -d '2024-01-02 14:00' +%s + let t1 = UNIX_EPOCH + Duration::new(1704204000, 0); + // $ date -u -d '2024-01-02 15:00' +%s + let t2 = UNIX_EPOCH + Duration::new(1704207600, 0); + + let (alice, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("") + .generate() + .unwrap(); + let alice_primary = alice.primary_key().key(); + + let (bob, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("") + .generate() + .unwrap(); + let bob_primary = bob.primary_key().key(); + + let carol_userid = ""; + let (carol, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid(carol_userid) + .generate() + .unwrap(); + + let ua = alice.userids().next().expect("have a user id"); + assert_eq!(ua.valid_certifications_by_key(p, None, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, None, alice_primary).count(), 0); + + // Alice has not certified Bob's User ID. + let ua = bob.userids().next().expect("have a user id"); + assert_eq!(ua.valid_certifications_by_key(p, None, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, None, alice_primary).count(), 0); + + // Alice has not certified Carol's User ID. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.valid_certifications_by_key(p, None, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, None, alice_primary).count(), 0); + + + // Have Alice certify Carol's certificate at t1. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)? + .sign_userid_binding( + &mut alice_signer, + carol.primary_key().key(), + &UserID::from(carol_userid))?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.certifications().count(), 1); + + assert_eq!(ua.valid_certifications_by_key(p, t0, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, alice_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, alice_primary).count(), 1); + assert_eq!(ua.active_certifications_by_key(p, t1, alice_primary).count(), 1); + + assert_eq!(ua.valid_certifications_by_key(p, t1, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t1, bob_primary).count(), 0); + + + // Have Alice certify Carol's certificate at t1 (again). + // Since both certifications were created at t1, they should + // both be returned. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)? + .sign_userid_binding( + &mut alice_signer, + carol.primary_key().key(), + &UserID::from(carol_userid))?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.certifications().count(), 2); + assert_eq!(ua.valid_certifications_by_key(p, t0, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, alice_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, alice_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t1, alice_primary).count(), 2); + + assert_eq!(ua.valid_certifications_by_key(p, t2, alice_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t2, alice_primary).count(), 2); + + assert_eq!(ua.valid_certifications_by_key(p, t0, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, bob_primary).count(), 0); + + + // Have Alice certify Carol's certificate at t2. Now we only + // have one active certification. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t2)? + .sign_userid_binding( + &mut alice_signer, + carol.primary_key().key(), + &UserID::from(carol_userid))?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.certifications().count(), 3); + assert_eq!(ua.valid_certifications_by_key(p, t0, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, alice_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, alice_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t1, alice_primary).count(), 2); + + assert_eq!(ua.valid_certifications_by_key(p, t2, alice_primary).count(), 3); + assert_eq!(ua.active_certifications_by_key(p, t2, alice_primary).count(), 1); + + assert_eq!(ua.valid_certifications_by_key(p, t0, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, bob_primary).count(), 0); + + + // Have Bob certify Carol's certificate at t1 and have it expire at t2. + let mut bob_signer = bob.primary_key() + .key() + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)? + .set_signature_validity_period(t2.duration_since(t1)?)? + .sign_userid_binding( + &mut bob_signer, + carol.primary_key().key(), + &UserID::from(carol_userid))?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.certifications().count(), 4); + + assert_eq!(ua.valid_certifications_by_key(p, t0, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, alice_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, alice_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t1, alice_primary).count(), 2); + + assert_eq!(ua.valid_certifications_by_key(p, t2, alice_primary).count(), 3); + assert_eq!(ua.active_certifications_by_key(p, t2, alice_primary).count(), 1); + + assert_eq!(ua.valid_certifications_by_key(p, t0, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, bob_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, bob_primary).count(), 1); + assert_eq!(ua.active_certifications_by_key(p, t1, bob_primary).count(), 1); + + // It expired. + assert_eq!(ua.valid_certifications_by_key(p, t2, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t2, bob_primary).count(), 0); + + + // Have Bob certify Carol's certificate at t1 again. This + // time don't have it expire. + let mut bob_signer = bob.primary_key() + .key() + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)? + .sign_userid_binding( + &mut bob_signer, + carol.primary_key().key(), + &UserID::from(carol_userid))?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ua = carol.userids().next().expect("have a user id"); + assert_eq!(ua.certifications().count(), 5); + assert_eq!(ua.valid_certifications_by_key(p, t0, alice_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, alice_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, alice_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t1, alice_primary).count(), 2); + + assert_eq!(ua.valid_certifications_by_key(p, t2, alice_primary).count(), 3); + assert_eq!(ua.active_certifications_by_key(p, t2, alice_primary).count(), 1); + + assert_eq!(ua.valid_certifications_by_key(p, t0, bob_primary).count(), 0); + assert_eq!(ua.active_certifications_by_key(p, t0, bob_primary).count(), 0); + + assert_eq!(ua.valid_certifications_by_key(p, t1, bob_primary).count(), 2); + assert_eq!(ua.active_certifications_by_key(p, t1, bob_primary).count(), 2); + + // One of the certifications expired. + assert_eq!(ua.valid_certifications_by_key(p, t2, bob_primary).count(), 1); + assert_eq!(ua.active_certifications_by_key(p, t2, bob_primary).count(), 1); + + Ok(()) + } } diff --git a/openpgp/src/cert/amalgamation/key.rs b/openpgp/src/cert/amalgamation/key.rs index aed8dfdf..fe361cf4 100644 --- a/openpgp/src/cert/amalgamation/key.rs +++ b/openpgp/src/cert/amalgamation/key.rs @@ -829,6 +829,210 @@ impl<'a, P, R, R2> KeyAmalgamation<'a, P, R, R2> } } +impl<'a, P, R, R2> KeyAmalgamation<'a, P, R, R2> + where Self: PrimaryKey<'a, P, R>, + P: 'a + key::KeyParts, + R: 'a + key::KeyRole, +{ + /// Returns the third-party certifications issued by the specified + /// key, and valid at the specified time. + /// + /// This function returns the certifications issued by the + /// specified key. Specifically, it returns a certification if: + /// + /// - it is well formed, + /// - it is live with respect to the reference time, + /// - it conforms to the policy, and + /// - the signature is cryptographically valid. + /// + /// This method is implemented on a [`KeyAmalgamation`] and not + /// a [`ValidKeyAmalgamation`], because a third-party + /// certification does not require the key to be self signed. + /// + /// # Examples + /// + /// Alice has certified that a certificate belongs to Bob on two + /// occasions. Whereas + /// [`KeyAmalgamation::valid_certifications_by_key`] returns + /// both certifications, + /// [`KeyAmalgamation::active_certifications_by_key`] only + /// returns the most recent certification. + /// + /// ```rust + /// use sequoia_openpgp as openpgp; + /// use openpgp::cert::prelude::*; + /// # use openpgp::packet::signature::SignatureBuilder; + /// # use openpgp::packet::UserID; + /// use openpgp::policy::StandardPolicy; + /// # use openpgp::types::SignatureType; + /// + /// const P: &StandardPolicy = &StandardPolicy::new(); + /// + /// # fn main() -> openpgp::Result<()> { + /// # let epoch = std::time::SystemTime::now() + /// # - std::time::Duration::new(100, 0); + /// # let t0 = epoch; + /// # + /// # let (alice, _) = CertBuilder::new() + /// # .set_creation_time(t0) + /// # .add_userid("") + /// # .generate() + /// # .unwrap(); + /// let alice: Cert = // ... + /// # alice; + /// # + /// # let bob_userid = ""; + /// # let (bob, _) = CertBuilder::new() + /// # .set_creation_time(t0) + /// # .add_userid(bob_userid) + /// # .generate() + /// # .unwrap(); + /// let bob: Cert = // ... + /// # bob; + /// + /// # // Alice has not certified Bob's User ID. + /// # let ka = bob.primary_key(); + /// # assert_eq!( + /// # ka.active_certifications_by_key( + /// # P, t0, alice.primary_key().key()).count(), + /// # 0); + /// # + /// # // Have Alice certify Bob's certificate. + /// # let mut alice_signer = alice + /// # .keys() + /// # .with_policy(P, None) + /// # .for_certification() + /// # .next().expect("have a certification-capable key") + /// # .key() + /// # .clone() + /// # .parts_into_secret().expect("have unencrypted key material") + /// # .into_keypair().expect("have unencrypted key material"); + /// # + /// # let mut bob = bob; + /// # for i in 1..=2usize { + /// # let ti = t0 + std::time::Duration::new(i as u64, 0); + /// # + /// # let certification = SignatureBuilder::new(SignatureType::DirectKey) + /// # .set_signature_creation_time(ti)? + /// # .sign_direct_key( + /// # &mut alice_signer, + /// # bob.primary_key().key())?; + /// # bob = bob.insert_packets(certification)?; + /// # + /// # let ka = bob.primary_key(); + /// # assert_eq!( + /// # ka.valid_certifications_by_key( + /// # P, ti, alice.primary_key().key()).count(), + /// # i); + /// # + /// # assert_eq!( + /// # ka.active_certifications_by_key( + /// # P, ti, alice.primary_key().key()).count(), + /// # 1); + /// # } + /// let bob_pk = bob.primary_key(); + /// + /// let valid_certifications = bob_pk.valid_certifications_by_key( + /// P, None, alice.primary_key().key()); + /// // Alice certified Bob's certificate twice. + /// assert_eq!(valid_certifications.count(), 2); + /// + /// let active_certifications = bob_pk.active_certifications_by_key( + /// P, None, alice.primary_key().key()); + /// // But only the most recent one is active. + /// assert_eq!(active_certifications.count(), 1); + /// # Ok(()) } + /// ``` + pub fn valid_certifications_by_key(&self, + policy: &'a dyn Policy, + reference_time: T, + issuer: PK) + -> impl Iterator + Send + Sync + where + T: Into>, + PK: Into<&'a Key>, + { + let reference_time = reference_time.into(); + let issuer = issuer.into(); + + let primary = self.primary(); + + self.valid_certifications_by_key_( + policy, reference_time, issuer, false, + move |sig| { + if primary { + sig.clone().verify_direct_key( + issuer, + self.component().role_as_primary()) + } else { + sig.clone().verify_subkey_binding( + issuer, + self.cert.primary_key().key(), + self.component().role_as_subordinate()) + } + }) + } + + /// Returns any active third-party certifications issued by the + /// specified key. + /// + /// This function is like + /// [`KeyAmalgamation::valid_certifications_by_key`], but it + /// only returns active certifications. Active certifications are + /// the most recent valid certifications with respect to the + /// reference time. + /// + /// Although there is normally only a single active certification, + /// there can be multiple certifications with the same timestamp. + /// In this case, all of them are returned. + /// + /// Unlike self-signatures, multiple third-party certifications + /// issued by the same key at the same time can be sensible. For + /// instance, Alice may fully trust a CA for user IDs in a + /// particular domain, and partially trust it for everything else. + /// This can only be expressed using multiple certifications. + /// + /// This method is implemented on a [`KeyAmalgamation`] and not + /// a [`ValidKeyAmalgamation`], because a third-party + /// certification does not require the user ID to be self signed. + /// + /// # Examples + /// + /// See the examples for + /// [`KeyAmalgamation::valid_certifications_by_key`]. + pub fn active_certifications_by_key(&self, + policy: &'a dyn Policy, + reference_time: T, + issuer: PK) + -> impl Iterator + Send + Sync + where + T: Into>, + PK: Into<&'a Key>, + { + let reference_time = reference_time.into(); + let issuer = issuer.into(); + + let primary = self.primary(); + + self.valid_certifications_by_key_( + policy, reference_time, issuer, true, + move |sig| { + if primary { + sig.clone().verify_direct_key( + issuer, + self.component().role_as_primary()) + } else { + sig.clone().verify_subkey_binding( + issuer, + self.cert.primary_key().key(), + &self.component().role_as_subordinate()) + } + }) + } +} + /// A `KeyAmalgamation` plus a `Policy` and a reference time. /// /// In the same way that a [`ValidComponentAmalgamation`] extends a @@ -2325,9 +2529,13 @@ impl<'a, P, R, R2> ValidKeyAmalgamation<'a, P, R, R2> #[cfg(test)] mod test { + use std::time::Duration; + use std::time::UNIX_EPOCH; + use crate::policy::StandardPolicy as P; use crate::cert::prelude::*; use crate::packet::Packet; + use crate::packet::signature::SignatureBuilder; use super::*; @@ -2468,4 +2676,320 @@ mod test { Ok(()) } + + #[test] + fn key_amalgamation_certifications_by_key() -> Result<()> { + // Alice and Bob certify Carol's certificate. We then check + // that valid_certifications_by_key and + // active_certifications_by_key return them. + let p = &crate::policy::StandardPolicy::new(); + + // $ date -u -d '2024-01-02 13:00' +%s + let t0 = UNIX_EPOCH + Duration::new(1704200400, 0); + // $ date -u -d '2024-01-02 14:00' +%s + let t1 = UNIX_EPOCH + Duration::new(1704204000, 0); + // $ date -u -d '2024-01-02 15:00' +%s + let t2 = UNIX_EPOCH + Duration::new(1704207600, 0); + + let (alice, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("") + .generate() + .unwrap(); + let alice_primary = alice.primary_key().key(); + + let (bob, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("") + .generate() + .unwrap(); + let bob_primary = bob.primary_key().key(); + + let carol_userid = ""; + let (carol, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid(carol_userid) + .generate() + .unwrap(); + + let ka = alice.primary_key(); + assert_eq!( + ka.valid_certifications_by_key(p, None, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, None, alice_primary).count(), + 0); + + // Alice has not certified Bob's User ID. + let ka = bob.primary_key(); + assert_eq!( + ka.valid_certifications_by_key(p, None, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, None, alice_primary).count(), + 0); + + // Alice has not certified Carol's User ID. + let ka = carol.primary_key(); + assert_eq!( + ka.valid_certifications_by_key(p, None, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, None, alice_primary).count(), + 0); + + + // Have Alice certify Carol's certificate at t1. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::DirectKey) + .set_signature_creation_time(t1)? + .sign_direct_key( + &mut alice_signer, + carol.primary_key().key())?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ka = carol.primary_key(); + assert_eq!(ka.certifications().count(), 1); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, alice_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, alice_primary).count(), + 1); + assert_eq!( + ka.active_certifications_by_key(p, t1, alice_primary).count(), + 1); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t1, bob_primary).count(), + 0); + + + // Have Alice certify Carol's certificate at t1 (again). + // Since both certifications were created at t1, they should + // both be returned. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::DirectKey) + .set_signature_creation_time(t1)? + .sign_direct_key( + &mut alice_signer, + carol.primary_key().key())?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ka = carol.primary_key(); + assert_eq!(ka.certifications().count(), 2); + assert_eq!( + ka.valid_certifications_by_key(p, t0, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, alice_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, alice_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t1, alice_primary).count(), + 2); + + assert_eq!( + ka.valid_certifications_by_key(p, t2, alice_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t2, alice_primary).count(), + 2); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, bob_primary).count(), + 0); + + + // Have Alice certify Carol's certificate at t2. Now we only + // have one active certification. + let mut alice_signer = alice_primary + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::DirectKey) + .set_signature_creation_time(t2)? + .sign_direct_key( + &mut alice_signer, + carol.primary_key().key())?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ka = carol.primary_key(); + assert_eq!(ka.certifications().count(), 3); + assert_eq!( + ka.valid_certifications_by_key(p, t0, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, alice_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, alice_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t1, alice_primary).count(), + 2); + + assert_eq!( + ka.valid_certifications_by_key(p, t2, alice_primary).count(), + 3); + assert_eq!( + ka.active_certifications_by_key(p, t2, alice_primary).count(), + 1); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, bob_primary).count(), + 0); + + + // Have Bob certify Carol's certificate at t1 and have it expire at t2. + let mut bob_signer = bob.primary_key() + .key() + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::DirectKey) + .set_signature_creation_time(t1)? + .set_signature_validity_period(t2.duration_since(t1)?)? + .sign_direct_key( + &mut bob_signer, + carol.primary_key().key())?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ka = carol.primary_key(); + assert_eq!(ka.certifications().count(), 4); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, alice_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, alice_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t1, alice_primary).count(), + 2); + + assert_eq!( + ka.valid_certifications_by_key(p, t2, alice_primary).count(), + 3); + assert_eq!( + ka.active_certifications_by_key(p, t2, alice_primary).count(), + 1); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, bob_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, bob_primary).count(), + 1); + assert_eq!( + ka.active_certifications_by_key(p, t1, bob_primary).count(), + 1); + + // It expired. + assert_eq!( + ka.valid_certifications_by_key(p, t2, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t2, bob_primary).count(), + 0); + + + // Have Bob certify Carol's certificate at t1 again. This + // time don't have it expire. + let mut bob_signer = bob.primary_key() + .key() + .clone() + .parts_into_secret().expect("have unencrypted key material") + .into_keypair().expect("have unencrypted key material"); + let certification = SignatureBuilder::new(SignatureType::DirectKey) + .set_signature_creation_time(t1)? + .sign_direct_key( + &mut bob_signer, + carol.primary_key().key())?; + let carol = carol.insert_packets(certification.clone())?; + + // Check that it is returned. + let ka = carol.primary_key(); + assert_eq!(ka.certifications().count(), 5); + assert_eq!( + ka.valid_certifications_by_key(p, t0, alice_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, alice_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, alice_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t1, alice_primary).count(), + 2); + + assert_eq!( + ka.valid_certifications_by_key(p, t2, alice_primary).count(), + 3); + assert_eq!( + ka.active_certifications_by_key(p, t2, alice_primary).count(), + 1); + + assert_eq!( + ka.valid_certifications_by_key(p, t0, bob_primary).count(), + 0); + assert_eq!( + ka.active_certifications_by_key(p, t0, bob_primary).count(), + 0); + + assert_eq!( + ka.valid_certifications_by_key(p, t1, bob_primary).count(), + 2); + assert_eq!( + ka.active_certifications_by_key(p, t1, bob_primary).count(), + 2); + + // One of the certifications expired. + assert_eq!( + ka.valid_certifications_by_key(p, t2, bob_primary).count(), + 1); + assert_eq!( + ka.active_certifications_by_key(p, t2, bob_primary).count(), + 1); + + Ok(()) + } } -- cgit v1.2.3