diff --git a/database/src/fs.rs b/database/src/fs.rs index d45bf2378b6e786b155423962389aa1424337af3..7a8cc89bc07e8ef7325fdbeea0bb55380d483f45 100644 --- a/database/src/fs.rs +++ b/database/src/fs.rs @@ -923,6 +923,22 @@ mod tests { db.check_consistency().expect("inconsistent database"); } + #[test] + fn spam_protection() -> Result<()> { + let (_tmp_dir, mut db, log_path) = open_db(); + test::spam_protection(&mut db, &log_path)?; + db.check_consistency()?; + Ok(()) + } + + #[test] + fn mutual_certifications() -> Result<()> { + let (_tmp_dir, mut db, log_path) = open_db(); + test::mutual_certifications(&mut db, &log_path)?; + db.check_consistency()?; + Ok(()) + } + #[test] fn attested_key_signatures() -> Result<()> { let (_tmp_dir, mut db, log_path) = open_db(); @@ -931,6 +947,14 @@ mod tests { Ok(()) } + #[test] + fn openpgp_ca_certifications() -> Result<()> { + let (_tmp_dir, mut db, log_path) = open_db(); + test::openpgp_ca_certifications(&mut db, &log_path)?; + db.check_consistency()?; + Ok(()) + } + #[test] fn nonexportable_sigs() -> Result<()> { let (_tmp_dir, mut db, log_path) = open_db(); diff --git a/database/src/lib.rs b/database/src/lib.rs index bca1514e3a3c954025aff6f14a57aca88703cecd..7694d22a421d5e5148150340b7936e288ae91818 100644 --- a/database/src/lib.rs +++ b/database/src/lib.rs @@ -1,5 +1,6 @@ #![recursion_limit = "1024"] +use std::collections::{HashMap, HashSet}; use std::convert::TryFrom; use std::str::FromStr; @@ -184,17 +185,75 @@ pub trait Database: Sync + Send { /// Complex operation that updates a Cert in the database. /// + /// Merging changes to a cert may cause other certs to be updated. + /// We maintain a queue of certs to update, releasing the lock + /// between updates as not to starve other merges. + /// + /// We return the result of the original merge. + fn merge(&self, new_tpk: Cert) -> Result { + let mut boundary = Default::default(); + let result = self.merge_recursive(new_tpk, false, + &mut boundary)?; + self.propagate_changes(boundary); + Ok(result) + } + + /// Propagates changes recursively up to the given depth. + /// + /// Note: DO NOT call this function. + /// + /// Currently, we do not visit a node twice to avoid loops. + fn propagate_changes(&self, + mut boundary: HashMap) + { + // Keep track of visited nodes to make sure we don't walk in + // circles. + let mut visited = HashSet::::default(); + + // How many steps from the source should the changes be + // propagated? Currently, certs can only affect their + // neighbors, hence 1 step is enough. + let mut depth = 1; + + while depth > 0 { + for (fp, cert) in std::mem::take(&mut boundary) { + if visited.contains(&fp) { + continue; + } + visited.insert(fp); + + let _ = self.merge_recursive(cert, true, &mut boundary); + } + + depth -= 1; + } + } + + /// Merges a single cert update into the database. + /// + /// Note: DO NOT call this function, use Database::merge instead. + /// + /// 0. First, the lock is acquired. /// 1. Merge new Cert with old, full Cert - /// - if old full Cert == new full Cert, stop + /// - If old full Cert == new full Cert, and `invalidate` is not + /// true, stop. /// 2. Prepare new published Cert /// - retrieve UserIDs from old published Cert /// - create new Cert from full Cert by keeping only published UserIDs + /// - During certification filtering, we may discover that + /// other certs that need to be updated as well. These certs + /// will be put into the `boundary` and may be reconsidered. /// 3. Write full and published Cert to temporary files /// 4. Check for fingerprint and long key id collisions for published Cert /// - abort if any problems come up! /// 5. Move full and published temporary Cert to their location /// 6. Update all symlinks - fn merge(&self, new_tpk: Cert) -> Result { + fn merge_recursive(&self, + new_tpk: Cert, + invalidate: bool, + boundary: &mut HashMap) + -> Result + { let fpr_primary = Fingerprint::try_from(new_tpk.primary_key().fingerprint())?; let _lock = self.lock()?; @@ -262,8 +321,13 @@ pub trait Database: Sync + Send { // the same address, we keep the first. email_status.dedup_by(|(e1, _), (e2, _)| e1 == e2); - // Abort if no changes were made - if full_tpk_unchanged { + // Abort if no changes were made with respect to the full copy + // of the certificate we saw before. This is the cheapest way + // to shortcut the merge operation. However, if invalidate is + // given, we want to keep going here and do the merge. Later, + // we will compare the merged certificate to the already + // published version. + if full_tpk_unchanged && ! invalidate { return Ok(ImportResult::Unchanged(TpkStatus { is_revoked, email_status, unparsed_uids })); } @@ -303,7 +367,8 @@ pub trait Database: Sync + Send { let fpr_not_linked = fpr_checks.into_iter().flatten(); let full_tpk_tmp = self.write_to_temp(&tpk_to_string(&full_tpk_new)?)?; - let published_tpk_clean = tpk_clean(&published_tpk_new)?; + let published_tpk_clean = + tpk_clean(self, published_tpk_new, Some(boundary))?; let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?; // these are very unlikely to fail. but if it happens, @@ -402,6 +467,27 @@ pub trait Database: Sync + Send { /// Complex operation that publishes some user id for a Cert already in the database. /// + /// Merging changes to a cert may cause other certs to be updated. + /// We maintain a queue of certs to update, releasing the lock + /// between updates as not to starve other merges. + /// + /// We return the result of the original merge. + fn set_email_published(&self, fpr_primary: &Fingerprint, email_new: &Email) + -> Result<()> + { + let mut boundary = Default::default(); + let result = self.set_email_published_recursive(fpr_primary, + email_new, + &mut boundary)?; + self.propagate_changes(boundary); + Ok(result) + } + + /// Merges a single cert update into the database. + /// + /// Note: DO NOT call this function, use + /// Database::set_email_published instead. + /// /// 1. Load published Cert /// - if UserID is already in, stop /// 2. Load full Cert @@ -413,7 +499,12 @@ pub trait Database: Sync + Send { /// - abort if any problems come up! /// 5. Move full and published temporary Cert to their location /// 6. Update all symlinks - fn set_email_published(&self, fpr_primary: &Fingerprint, email_new: &Email) -> Result<()> { + fn set_email_published_recursive(&self, + fpr_primary: &Fingerprint, + email_new: &Email, + boundary: &mut HashMap) + -> Result<()> + { let _lock = self.lock()?; self.nolock_unlink_email_if_other(fpr_primary, email_new)?; @@ -453,7 +544,8 @@ pub trait Database: Sync + Send { return Err(anyhow!("Requested UserID not found!")); } - let published_tpk_clean = tpk_clean(&published_tpk_new)?; + let published_tpk_clean = tpk_clean(self, published_tpk_new, + Some(boundary))?; let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?; self.move_tmp_to_published(published_tpk_tmp, fpr_primary)?; @@ -536,7 +628,7 @@ pub trait Database: Sync + Send { .iter() .filter(|email| !published_emails_new.contains(email)); - let published_tpk_clean = tpk_clean(&published_tpk_new)?; + let published_tpk_clean = tpk_clean(self, published_tpk_new, None)?; let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?; self.move_tmp_to_published(published_tpk_tmp, fpr_primary)?; diff --git a/database/src/openpgp_utils.rs b/database/src/openpgp_utils.rs index cbaba90ad30aebc53d6b538f159162e5868cdce1..a7950c0239f447419fe07ce70b1830e4d5360bac 100644 --- a/database/src/openpgp_utils.rs +++ b/database/src/openpgp_utils.rs @@ -1,18 +1,26 @@ use openpgp::Result; +use std::collections::{HashSet, HashMap}; use std::convert::TryFrom; use openpgp::{ Cert, + KeyHandle, types::RevocationStatus, cert::prelude::*, serialize::SerializeInto as _, + packet::Signature, + parse::Parse, policy::StandardPolicy, }; use Email; +use Database; pub const POLICY: StandardPolicy = StandardPolicy::new(); +/// How many of the most recent certifications to publish. +const PUBLISH_N_MOST_RECENT_CERTIFICATIONS: usize = 3; + pub fn is_status_revoked(status: RevocationStatus) -> bool { match status { RevocationStatus::Revoked(_) => true, @@ -25,7 +33,13 @@ pub fn tpk_to_string(tpk: &Cert) -> Result> { tpk.armored().export_to_vec() } -pub fn tpk_clean(tpk: &Cert) -> Result { +pub fn tpk_clean(db: &D, tpk: Cert, + mut boundary: Option<&mut HashMap>) + -> Result +where + D: Database, + D: ?Sized, +{ // Iterate over the Cert, pushing packets we want to merge // into the accumulator. let mut acc = Vec::new(); @@ -37,6 +51,20 @@ pub fn tpk_clean(tpk: &Cert) -> Result { for s in pk_bundle.self_revocations() { acc.push(s.clone().into()) } for s in pk_bundle.other_revocations() { acc.push(s.clone().into()) } + // Keep symmetric certifications. These are useful to delegate + // trust to a CA. + let (accepted, _rejected) = + filter_symmetric(db, + &mut boundary, + &tpk, + pk_bundle.certifications().iter().collect(), + |mut s, other| + s.verify_direct_key(&other.primary_key(), + &tpk.primary_key()).is_ok()); + for s in accepted { + acc.push(s.clone().into()); + } + // The subkeys and related signatures. for skb in tpk.keys().subkeys() { acc.push(skb.key().clone().into()); @@ -47,11 +75,18 @@ pub fn tpk_clean(tpk: &Cert) -> Result { // The UserIDs. for uidb in tpk.userids() { + let userid = uidb.userid(); + acc.push(uidb.userid().clone().into()); for s in uidb.self_signatures() { acc.push(s.clone().into()) } for s in uidb.self_revocations() { acc.push(s.clone().into()) } for s in uidb.other_revocations() { acc.push(s.clone().into()) } + // Put all certifications in a set, deal with them using the + // cheapest method first, only using more expensive ones if + // necessary. + let mut certifications: HashSet<_> = uidb.certifications().collect(); + // Reasoning about the currently attested certifications // requires a policy. if let Ok(vuid) = uidb.with_policy(&POLICY, None) { @@ -59,14 +94,175 @@ pub fn tpk_clean(tpk: &Cert) -> Result { acc.push(s.clone().into()); } for s in vuid.attested_certifications() { + certifications.remove(s); acc.push(s.clone().into()); } } + + // Keep the N most recent ones made by the domain's openpgp-ca. + if ! certifications.is_empty() { + if let Some(ca) = Email::try_from(userid).ok() + .map(|m| m.corresponding_openpgp_ca()) + .and_then(|ca| db.by_email(&ca)) + .and_then(|s| Cert::from_bytes(s.as_bytes()).ok()) + { + let handle = ca.key_handle(); + let mut ca_certifications = Vec::new(); + for c in std::mem::take(&mut certifications) { + // See if it is made by the CA. + if c.get_issuers().iter().any(|i| i.aliases(&handle)) { + // It is. + ca_certifications.push(c); + } else { + // Try the next expensive method. + certifications.insert(c); + } + } + + // Keep the most recent valid certification. + ca_certifications.sort_unstable_by_key(|s| { + s.signature_creation_time().unwrap_or(std::time::UNIX_EPOCH) + }); + + let mut n = PUBLISH_N_MOST_RECENT_CERTIFICATIONS; + while let Some(last) = ca_certifications.pop() { + // Check the signature. + if last.clone().verify_userid_binding(&ca.primary_key(), + &tpk.primary_key(), + userid).is_ok() + { + // Checked out, include it. + acc.push(last.clone().into()); + n -= 1; + if n == 0 { + break; // N are enough. + } + } + } + } + } + + // Keep symmetric certifications. + let (accepted, _rejected) = + filter_symmetric(db, + &mut boundary, + &tpk, + certifications.into_iter().collect(), + |mut s, other| + s.verify_userid_binding(&other.primary_key(), + &tpk.primary_key(), + userid).is_ok()); + for s in accepted { + acc.push(s.clone().into()); + } } Cert::from_packets(acc.into_iter()) } +fn filter_symmetric<'a, D, M, C>(db: &D, + boundary: &mut Option<&mut HashMap>, + us: &Cert, + mut sigs: Vec<&'a Signature>, + check: C) + -> (Vec<&'a Signature>, Vec<&'a Signature>) +where + D: Database, + D: ?Sized, + C: Fn(Signature, &Cert) -> bool, +{ + let our_handle = us.key_handle(); + let mut accepted = Vec::new(); + let mut rejected = Vec::new(); + + while let Some(c) = sigs.pop() { + // Get the issuer. Don't bother with signatures that + // don't include an issuer fingerprint. + let issuer = if let Some(i) = c.issuer_fingerprints().next() { + i + } else { + rejected.push(c); + continue; + }; + let handle = KeyHandle::from(issuer.clone()); + + // Collect all the signatures from the same issuer. This + // way, we can batch all of them together and only once + // lookup the issuer's cert. + let mut batch = vec![c]; + // XXX: Replace this with drain_filter once stabilized. + for c in std::mem::take(&mut sigs) { + if c.get_issuers().iter().any(|i| i.aliases(&handle)) { + batch.push(c); + } else { + // Not the same issuer, put it back. + sigs.push(c); + } + } + + // Locate the issuer. Note that we do a "full" lookup for + // the issuer. If we didn't, then there would be no way + // to add the first certification. + if let Some(other) = + crate::types::Fingerprint::try_from(issuer.clone()).ok() + .and_then(|fp| db.by_fpr_full(&fp)) + .and_then(|s| Cert::from_bytes(s.as_bytes()).ok()) + { + // XXX: If we want certifications to not be considered if + // they involve unpublished identities, we need to + // restrict other to the published identities here. If we + // do change this, also change + // Database::set_email_published to recurse like + // Database::merge does. + + // Did we certify any of other's User IDs? + if ! other.userids() + .any(|uidb| uidb.certifications() + .filter(|c| c.get_issuers().iter().any(|i| i.aliases(&our_handle))) + .any(|c| c.clone().verify_userid_binding( + &us.primary_key(), + &other.primary_key(), + uidb.userid()).is_ok())) + { + // No. This is not symmetric. Put all + // certifications back. + rejected.append(&mut batch); + continue; + } + + // We know the signature relation is symmetric. + + // Keep the N most recent valid certifications. + batch.sort_unstable_by_key(|s| { + s.signature_creation_time().unwrap_or(std::time::UNIX_EPOCH) + }); + + let mut n = PUBLISH_N_MOST_RECENT_CERTIFICATIONS; + while let Some(last) = batch.pop() { + // Check the signature. + if check(c.clone(), &other) { + // Checked out, include it. + accepted.push(last); + n -= 1; + if n == 0 { + break; // N are enough. + } + } + } + + // Recursively reconsider cert. + if let Some(boundary) = boundary { + boundary.insert(other.fingerprint(), other); + } + } else { + // Try the next expensive method. + rejected.append(&mut batch); + } + } + + (accepted, rejected) +} + /// Filters the Cert, keeping only UserIDs that aren't revoked, and whose emails match the given list pub fn tpk_filter_alive_emails(tpk: &Cert, emails: &[Email]) -> Cert { tpk.clone().retain_userids(|uid| { diff --git a/database/src/test.rs b/database/src/test.rs index e337860171ff10e3e5709cadfcddde92527659a3..f75601295e41991db7230632e0b993e1e1321fce 100644 --- a/database/src/test.rs +++ b/database/src/test.rs @@ -1122,6 +1122,149 @@ pub fn test_no_selfsig(db: &mut impl Database, log_path: &Path) { }, tpk_status); } +/// Makes sure that certificates cannot be spammed. +pub fn spam_protection(db: &mut impl Database, log_path: &Path) + -> Result<()> { + use std::time::{SystemTime, Duration}; + use openpgp::{ + types::*, + }; + let t0 = SystemTime::now() - Duration::new(5 * 60, 0); + let t1 = SystemTime::now() - Duration::new(4 * 60, 0); + + let (alice, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("alice@foo.org") + .generate()?; + let alices_fp = Fingerprint::try_from(alice.fingerprint())?; + + let (spam, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("spam@foo.com") + .generate()?; + let spams_fp = Fingerprint::try_from(spam.fingerprint())?; + let mut spam_signer = + spam.primary_key().key().clone().parts_into_secret()? + .into_keypair()?; + + // The spam certifies the binding between between alice and alice@foo.com. + let spam_certifies_alice = + alice.userids().nth(0).unwrap().userid().bind( + &mut spam_signer, &alice, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)?)?; + let alice = alice.insert_packets(vec![ + Packet::from(spam_certifies_alice)])?; + + // Import all certs. + db.merge(spam.clone())?; + check_log_entry(log_path, &spams_fp); + db.merge(alice.clone())?; + check_log_entry(log_path, &alices_fp); + + // Confirm the email so that we can inspect the userid component. + db.set_email_published(&alices_fp, &Email::from_str("alice@foo.org")?)?; + + // Check that the certification is stripped. + let alice_ = Cert::from_bytes(&db.by_fpr(&alices_fp).unwrap())?; + assert_eq!(alice_.bad_signatures().count(), 0); + assert_eq!(alice_.userids().nth(0).unwrap().certifications().count(), 0); + drop(alice_); + + Ok(()) +} + +/// Makes sure that mutual certifications are served. +pub fn mutual_certifications(db: &mut impl Database, log_path: &Path) + -> Result<()> { + use std::time::{SystemTime, Duration}; + use openpgp::{ + types::*, + }; + let t0 = SystemTime::now() - Duration::new(5 * 60, 0); + let t1 = SystemTime::now() - Duration::new(4 * 60, 0); + + let (alice, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("alice@foo.com") + .generate()?; + let alices_fp = Fingerprint::try_from(alice.fingerprint())?; + let mut alice_signer = + alice.primary_key().key().clone().parts_into_secret()? + .into_keypair()?; + + let (bob, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("bob@bar.com") + .generate()?; + let bobs_fp = Fingerprint::try_from(bob.fingerprint())?; + let mut bob_signer = + bob.primary_key().key().clone().parts_into_secret()? + .into_keypair()?; + + // Have Alice certify the binding between "bob@bar.com" and + // Bob's key. + let alice_certifies_bob = + bob.userids().nth(0).unwrap().userid().bind( + &mut alice_signer, &bob, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)?)?; + + // Have Bob certify the binding between "alice@foo.com" and + // Alice's key. + let bob_certifies_alice = + alice.userids().nth(0).unwrap().userid().bind( + &mut bob_signer, &alice, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)?)?; + + // Now for the test. First, import Bob's cert as is. + db.merge(bob.clone())?; + check_log_entry(log_path, &bobs_fp); + + // Confirm the email so that we can inspect the userid component. + db.set_email_published(&bobs_fp, &Email::from_str("bob@bar.com")?)?; + + // Then, add the certification, merge into the db, check that the + // certification is stripped. + let bob = bob.insert_packets(vec![ + alice_certifies_bob.clone(), + ])?; + db.merge(bob.clone())?; + check_log_entry(log_path, &bobs_fp); + let bob_ = Cert::from_bytes(&db.by_fpr(&bobs_fp).unwrap())?; + assert_eq!(bob_.bad_signatures().count(), 0); + assert_eq!(bob_.userids().nth(0).unwrap().certifications().count(), 0); + drop(bob_); + + // Now import Alice's key with Bob's certification. Because the + // certification is mutual, we expect it to be served. + let alice = alice.insert_packets(vec![ + bob_certifies_alice.clone(), + ])?; + db.merge(alice.clone())?; + check_log_entry(log_path, &alices_fp); + + // Confirm the email so that we can inspect the userid component. + db.set_email_published(&alices_fp, &Email::from_str("alice@foo.com")?)?; + + let alice_ = Cert::from_bytes(&db.by_fpr(&alices_fp).unwrap())?; + assert_eq!(alice_.bad_signatures().count(), 0); + assert_eq!(alice_.userids().nth(0).unwrap().certifications().count(), 1); + drop(alice_); + + // Because the certification is mutual, we also expect Bob's key + // to have Alice's certification. + let bob_ = Cert::from_bytes(&db.by_fpr(&bobs_fp).unwrap())?; + assert_eq!(bob_.bad_signatures().count(), 0); + assert_eq!(bob_.userids().nth(0).unwrap().certifications().count(), 1); + assert_eq!(bob_.userids().nth(0).unwrap().certifications() + .next().unwrap(), &alice_certifies_bob); + drop(bob_); + + Ok(()) +} + /// Makes sure that attested key signatures are correctly handled. pub fn attested_key_signatures(db: &mut impl Database, log_path: &Path) -> Result<()> { @@ -1239,6 +1382,126 @@ pub fn attested_key_signatures(db: &mut impl Database, log_path: &Path) Ok(()) } +/// Makes sure that certifications issued by openpgp-ca are served. +pub fn openpgp_ca_certifications(db: &mut impl Database, log_path: &Path) + -> Result<()> { + use std::time::{SystemTime, Duration}; + use openpgp::{ + types::*, + }; + let t0 = SystemTime::now() - Duration::new(5 * 60, 0); + let t1 = SystemTime::now() - Duration::new(4 * 60, 0); + + let (alice, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("alice@foo.com") + .add_userid("alice@foo.org") + .generate()?; + let alices_fp = Fingerprint::try_from(alice.fingerprint())?; + let mut alice_signer = + alice.primary_key().key().clone().parts_into_secret()? + .into_keypair()?; + + let (ca, _) = CertBuilder::new() + .set_creation_time(t0) + .add_userid("openpgp-ca@foo.com") + .generate()?; + let cas_fp = Fingerprint::try_from(ca.fingerprint())?; + let mut ca_signer = + ca.primary_key().key().clone().parts_into_secret()? + .into_keypair()?; + + // The ca certifies the binding between between alice and alice@foo.com. + let ca_certifies_alice_com = + alice.userids().nth(0).unwrap().userid().bind( + &mut ca_signer, &alice, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)?)?; + + // The ca also certifies the binding between between alice and + // alice@foo.org, but it does not have the authority over foo.org. + let ca_certifies_alice_org = + alice.userids().nth(1).unwrap().userid().bind( + &mut ca_signer, &alice, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_signature_creation_time(t1)?)?; + + // Alice tsigs the ca key. + let alice_tsigs_ca_uid = + ca.userids().nth(0).unwrap().userid().bind( + &mut alice_signer, &ca, + SignatureBuilder::new(SignatureType::GenericCertification) + .set_trust_signature(1, 120)? + .set_signature_creation_time(t1)?)?; + // Again, this time with a direct-key delegation. + let alice_tsigs_ca_direct = + SignatureBuilder::new(SignatureType::DirectKey) + .set_trust_signature(1, 120)? + .set_signature_creation_time(t1)? + .sign_direct_key(&mut alice_signer, ca.primary_key().key())?; + + // First, import the ca key and confirm it. + db.merge(ca.clone())?; + check_log_entry(log_path, &cas_fp); + db.set_email_published(&cas_fp, &Email::from_str("openpgp-ca@foo.com")?)?; + // From now on, the ca key should be recognized as a CA. + + // Alice gets certified. As a result, the CA uploads Alice's + // certificate with the certification. + let alice = alice.insert_packets(vec![ + Packet::from(ca_certifies_alice_com), + Packet::from(ca_certifies_alice_org), + ])?; + + db.merge(alice.clone())?; + check_log_entry(log_path, &alices_fp); + + // Confirm the email so that we can inspect the userid component. + db.set_email_published(&alices_fp, &Email::from_str("alice@foo.com")?)?; + db.set_email_published(&alices_fp, &Email::from_str("alice@foo.org")?)?; + + // Check that the foo.com certification is served but the foo.org + // one is stripped because the ca doesn't have authority for that + // domain. + let alice_ = Cert::from_bytes(&db.by_fpr(&alices_fp).unwrap())?; + assert_eq!(alice_.bad_signatures().count(), 0); + assert_eq!(alice_.userids().nth(0).unwrap().userid().value(), b"alice@foo.com"); + assert_eq!(alice_.userids().nth(0).unwrap().certifications().count(), 1); + assert_eq!(alice_.userids().nth(1).unwrap().userid().value(), b"alice@foo.org"); + assert_eq!(alice_.userids().nth(1).unwrap().certifications().count(), 0); + drop(alice_); + + // Now, Alice tsigns the CA. + let ca = ca.insert_packets(vec![ + Packet::from(alice_tsigs_ca_uid), + Packet::from(alice_tsigs_ca_direct), + ])?; + db.merge(ca.clone())?; + // think it should, actually. + // Now Alice's key also has her @foo.org userid certified, because + // Alice and CA both certified each other. + check_log_entry(log_path, &alices_fp); + + // Check that Alice's tsig are served. + let ca_ = Cert::from_bytes(&db.by_fpr(&cas_fp).unwrap())?; + assert_eq!(ca_.bad_signatures().count(), 0); + assert_eq!(ca_.primary_key().certifications().count(), 1); + assert_eq!(ca_.userids().nth(0).unwrap().certifications().count(), 1); + drop(ca_); + + // Now Alice's key also has her @foo.org userid certified, because + // Alice and CA both certified each other. + let alice_ = Cert::from_bytes(&db.by_fpr(&alices_fp).unwrap())?; + assert_eq!(alice_.bad_signatures().count(), 0); + assert_eq!(alice_.userids().nth(0).unwrap().userid().value(), b"alice@foo.com"); + assert_eq!(alice_.userids().nth(0).unwrap().certifications().count(), 1); + assert_eq!(alice_.userids().nth(1).unwrap().userid().value(), b"alice@foo.org"); + assert_eq!(alice_.userids().nth(1).unwrap().certifications().count(), 1); + drop(alice_); + + Ok(()) +} + fn check_log_entry(log_path: &Path, fpr: &Fingerprint) { let log_data = fs::read_to_string(log_path).unwrap(); let last_entry = log_data diff --git a/database/src/types.rs b/database/src/types.rs index 441a88744314e3e93509aa3f1eb43f1834e543a0..439d8d2a118fd07bf4549ec89855054bcfe0081b 100644 --- a/database/src/types.rs +++ b/database/src/types.rs @@ -24,6 +24,25 @@ impl Email { pub fn as_str(&self) -> &str { &self.0 } + + /// Returns the address of the domain's openpgp-ca. + fn split(&self) -> (&str, &str) { + let mut iter = self.0.split('@'); + let localpart = iter.next().expect("Invalid email address"); + let domain = iter.next().expect("Invalid email address"); + assert!(iter.next().is_none(), "Invalid email address"); + (&localpart, &domain) + } + + /// Tests whether this email is a openpgp-ca address. + pub fn is_openpgp_ca(&self) -> bool { + self.split().0 == "openpgp-ca" + } + + /// Returns the address of the domain's openpgp-ca address. + pub fn corresponding_openpgp_ca(&self) -> Self { + Self(format!("openpgp-ca@{}", self.split().1)) + } } impl TryFrom<&UserID> for Email { diff --git a/dist/templates/about/news.html.hbs b/dist/templates/about/news.html.hbs index 02839264db1d1a40c21275fabaa5574761dd7508..abad65abd952803d932ca443a6516fa7ba5f960b 100644 --- a/dist/templates/about/news.html.hbs +++ b/dist/templates/about/news.html.hbs @@ -2,6 +2,105 @@

About | News | Usage | FAQ | Stats | Privacy

+

+
2021-12-20 📅
+ Adding support for third-party certifications to Hagrid +

+

+ When we designed keys.openpgp.org, we wanted users to be in control + of their certificates. That's one of the reasons that keys.openpgp.org + strips third-party certifications. But certifications are useful, and + key servers are a convenient way to distribute them. So, we're adding + support to Hagrid to distribute them while still keeping users in control. + +

+ Historically, when Alice signed Bob's certificate, and uploaded + the certification to a key server, the key server attached her + certification to Bob's certificate, and there was nothing that Bob could do + to remove it. + +

+ In general, attaching third-party certifications to the signed certificate + isn't really a problem. If a certification is irrelevant, it is just ignored. + But this policy means that third parties can attach an arbitrary number of certifications + to Bob's key (see here and here). Bob could get a lot of certifications if he is very popular. Or, + an attacker could upload a lot of valid, but useless certifications. + +

+ We've identified three mechanisms that allow keys.openpgp.org to + distribute certifications while preventing these types of attacks. + +

+ Support for 1pa3pc certifications + +

+ First, a user can add a so-called 1pa3pc signature to their certificate. + This special signature allows the user to decide who is allowed to sign + their certificate. + +

+ Using 1pa3pc, if Bob is happy for keys.openpgp.org to distribute + signatures by Alice, then he adds Alice's key to his 1pa3pc signature. + When keys.openpgp.org sees that signature, it won't strip third-party certifications + from Alice, but it will strip other third-party certifications. If an + attacker tries to flood Bob's key, the certifications will simply be ignored, since + they are not made by keys on Bob's good list. + +

+ Sequoia already makes it convenient to + add 1pa3pc packets, as this example shows: + +

+    $ sq key attest-certifications <mykey.pgp >mykey.attested.pgp
+    $ sq key extract-cert <mykey.attested.pgp >mycert.attested.pgp
+    
+ +

+ We hope other tooling will add support for them + in the future. + +

+ Support for OpenPGP CA certifications + +

+ The second scenario where keys.openpgp.org distributes certifications + doesn't require the user to opt-in, which makes it much more convenient + for normal users. This mode is designed for OpenPGP CA, a new + tool that makes it easy for organizations to set up their own CA. + +

+ The problem that OpenPGP CA helps solve is how to do strong authentication. That is, how does Alice know that she has the right key for Bob? Traditionally, when using PGP (or Signal, or any other tool that doesn't rely on central authorities), it is up to each individual user to make sure they use the right key for each of their correspondents. One way do this with PGP is for users to check each of their correspondent's fingerprints. In Signal, users need to check each other's safety numbers. This doesn’t scale. + +

+ OpenPGP CA's proposition is that often it is better for Alice to rely on a third party to check fingerprints on her behalf. This is especially reasonable in many organizations where users already rely on their IT department to perform security relevant tasks for them. So, if Alice relies on her IT department to install updates on her machine, then she can just as well rely on them to perform these checks on her behalf. This doesn't only save Alice time and trouble, but the IT department staff is typically much better trained to perform such tasks safely and efficiently. + +

+ An organisation that uses OpenPGP CA has CA key with the User ID + openpgp-ca@example.org. + This key certifies all of the other keys in the organization. Of course, + for these certifications to be useful, they need to be distributed. + +

+ To better support OpenPGP CA, keys.openpgp.org will + return certifications made by a User ID of the form openpgp-ca@example.org, + if this email address has been verified, and for User IDs in their own domain. That is, the CA openpgp-ca@example.org is allowed to publish certifications for alice@example.org, + but not for bob@other.org. Conversely, if alice@example.org certifies + openpgp-ca@example.org, keys.openpgp.org will also publish that certification. + +

+ It is still possible for someone to spam certificates with this mechanism. For instance, + if an attacker registers openpgp-ca@web.de, they can issue a certification for every + certificate that has a web.de email address; we don't check that + openpgp-ca@web.de is a valid CA, we only check that the person who controls + that email address also controls the key. However, our design only allows + the publication of one such gratuitous certification per User ID, which is far from a + denial-of-service attack. + +

+ OpenPGP CA also supports the concept of CA bridges where one + organization certifies another organization's CA. These + certifications are supported if the certification is mutual. +

2019-11-12 📅
Celebrating 100.000 verified addresses! 📈 diff --git a/dist/templates/atom.xml.hbs b/dist/templates/atom.xml.hbs index 722989800839ec5d639832c19f824d1395ae3a43..0b78752dfe8ecacb0224f54c85f68150fd7137f5 100644 --- a/dist/templates/atom.xml.hbs +++ b/dist/templates/atom.xml.hbs @@ -3,7 +3,13 @@ keys.openpgp.org urn:uuid:8e783366-73b1-460e-83d3-42f01046646d - 2019-11-12T12:00:00Z + 2021-12-20T12:00:00Z + + Support for OpenPGP-CA + + 2021-12-20T12:00:00Z + urn:uuid:aca50bf2-5310-4d6a-8ee1-d361be7ce201 + Celebrating 100.000 verified addresses! 📈 diff --git a/dist/templates/index.html.hbs b/dist/templates/index.html.hbs index 65af7c241e6de77bd058fd0bccea140670ecf7ec..ddba9363070f0abe15198eec15ee5ae379ebab24 100644 --- a/dist/templates/index.html.hbs +++ b/dist/templates/index.html.hbs @@ -25,7 +25,7 @@

- {{ text "News:" }} {{ text "Celebrating 100.000 verified addresses! 📈 (2019-11-12)" }} + {{ text "News:" }} {{ text "Support for OpenPGP-CA (2021-12-20)" }}

{{/with}} {{/layout}} diff --git a/po/hagrid/de.po b/po/hagrid/de.po index c87900c419050b84401a85cc5c08d32a08004eb0..e7f0bea1db191f0b97eb6c0a22ca464dc7af5eb8 100644 --- a/po/hagrid/de.po +++ b/po/hagrid/de.po @@ -87,11 +87,9 @@ msgid "News:" msgstr "News:" msgid "" -"Celebrating 100.000 " -"verified addresses! 📈 (2019-11-12)" +"Support for third-party " +"certification signatures (2021-09-21)" msgstr "" -"Wir feiern 100.000 " -"überprüfte Adressen! 📈 (2019-11-12)" msgid "v{{ version }} built from" msgstr "v{{ version }}, Revision" @@ -401,3 +399,10 @@ msgstr "Zeitlimit beim Hochladen abgelaufen. Bitte versuch es erneut." msgid "Invalid verification link." msgstr "Ungültiger Bestätigungs-Link." + +#~ msgid "" +#~ "Celebrating 100.000 " +#~ "verified addresses! 📈 (2019-11-12)" +#~ msgstr "" +#~ "Wir feiern 100.000 " +#~ "überprüfte Adressen! 📈 (2019-11-12)" diff --git a/po/hagrid/en.po b/po/hagrid/en.po index c08ea1d0e59f333e3467aa7637c3c539df380663..7198f14cce578ba5e297c86aecc5c74577e47e3c 100644 --- a/po/hagrid/en.po +++ b/po/hagrid/en.po @@ -81,8 +81,8 @@ msgid "News:" msgstr "News:" msgid "" -"Celebrating 100.000 " -"verified addresses! 📈 (2019-11-12)" +"Support for third-party " +"certification signatures (2021-09-21)" msgstr "" #, fuzzy diff --git a/po/hagrid/hagrid.pot b/po/hagrid/hagrid.pot index 465cf08dddf5fc3122af1a4f19c0b7ad646a0e17..090ca2b56a04f10f3e915bb0801842af1dd5f456 100644 --- a/po/hagrid/hagrid.pot +++ b/po/hagrid/hagrid.pot @@ -71,7 +71,7 @@ msgstr "" msgid "News:" msgstr "" -msgid "Celebrating 100.000 verified addresses! 📈 (2019-11-12)" +msgid "Support for third-party certification signatures (2021-09-21)" msgstr "" msgid "v{{ version }} built from" diff --git a/po/hagrid/ja.po b/po/hagrid/ja.po index ec1ea34f4ab8b4901a82cc81435c706d11468b84..7057956de090d2db15523221a81b82bb68d3949b 100644 --- a/po/hagrid/ja.po +++ b/po/hagrid/ja.po @@ -86,8 +86,8 @@ msgid "News:" msgstr "ニュース:" msgid "" -"Celebrating 100.000 " -"verified addresses! 📈 (2019-11-12)" +"Support for third-party " +"certification signatures (2021-09-21)" msgstr "" msgid "v{{ version }} built from" diff --git a/src/gettext_strings.rs b/src/gettext_strings.rs index 8e36a4ee5d014a7f9c8870f9c6ab24deeaa5c87e..c18e1ae13a786237388943990b16bcfa644216e2 100644 --- a/src/gettext_strings.rs +++ b/src/gettext_strings.rs @@ -13,7 +13,7 @@ fn _dummy() { t!("You can also upload or manage your key."); t!("Find out more about this service."); t!("News:"); - t!("Celebrating 100.000 verified addresses! 📈 (2019-11-12)"); + t!("Support for third-party certification signatures (2021-09-21)"); t!("v{{ version }} built from"); t!("Powered by Sequoia-PGP"); t!("Background image retrieved from Subtle Patterns under CC BY-SA 3.0"); diff --git a/templates-untranslated/about/news.html.hbs b/templates-untranslated/about/news.html.hbs index 161d22885d011dc09aeab36f9a696fb1495e8054..8b45903bf16a4fe9aa4fe2859fbfa671a7b0d923 100644 --- a/templates-untranslated/about/news.html.hbs +++ b/templates-untranslated/about/news.html.hbs @@ -1,6 +1,105 @@

About | News | Usage | FAQ | Stats | Privacy

+

+
2021-12-20 📅
+ Adding support for third-party certifications to Hagrid +

+

+ When we designed keys.openpgp.org, we wanted users to be in control + of their certificates. That's one of the reasons that keys.openpgp.org + strips third-party certifications. But certifications are useful, and + key servers are a convenient way to distribute them. So, we're adding + support to Hagrid to distribute them while still keeping users in control. + +

+ Historically, when Alice signed Bob's certificate, and uploaded + the certification to a key server, the key server attached her + certification to Bob's certificate, and there was nothing that Bob could do + to remove it. + +

+ In general, attaching third-party certifications to the signed certificate + isn't really a problem. If a certification is irrelevant, it is just ignored. + But this policy means that third parties can attach an arbitrary number of certifications + to Bob's key (see here and here). Bob could get a lot of certifications if he is very popular. Or, + an attacker could upload a lot of valid, but useless certifications. + +

+ We've identified three mechanisms that allow keys.openpgp.org to + distribute certifications while preventing these types of attacks. + +

+ Support for 1pa3pc certifications + +

+ First, a user can add a so-called 1pa3pc signature to their certificate. + This special signature allows the user to decide who is allowed to sign + their certificate. + +

+ Using 1pa3pc, if Bob is happy for keys.openpgp.org to distribute + signatures by Alice, then he adds Alice's key to his 1pa3pc signature. + When keys.openpgp.org sees that signature, it won't strip third-party certifications + from Alice, but it will strip other third-party certifications. If an + attacker tries to flood Bob's key, the certifications will simply be ignored, since + they are not made by keys on Bob's good list. + +

+ Sequoia already makes it convenient to + add 1pa3pc packets, as this example shows: + +

+    $ sq key attest-certifications <mykey.pgp >mykey.attested.pgp
+    $ sq key extract-cert <mykey.attested.pgp >mycert.attested.pgp
+    
+ +

+ We hope other tooling will add support for them + in the future. + +

+ Support for OpenPGP CA certifications + +

+ The second scenario where keys.openpgp.org distributes certifications + doesn't require the user to opt-in, which makes it much more convenient + for normal users. This mode is designed for OpenPGP CA, a new + tool that makes it easy for organizations to set up their own CA. + +

+ The problem that OpenPGP CA helps solve is how to do strong authentication. That is, how does Alice know that she has the right key for Bob? Traditionally, when using PGP (or Signal, or any other tool that doesn't rely on central authorities), it is up to each individual user to make sure they use the right key for each of their correspondents. One way do this with PGP is for users to check each of their correspondent's fingerprints. In Signal, users need to check each other's safety numbers. This doesn’t scale. + +

+ OpenPGP CA's proposition is that often it is better for Alice to rely on a third party to check fingerprints on her behalf. This is especially reasonable in many organizations where users already rely on their IT department to perform security relevant tasks for them. So, if Alice relies on her IT department to install updates on her machine, then she can just as well rely on them to perform these checks on her behalf. This doesn't only save Alice time and trouble, but the IT department staff is typically much better trained to perform such tasks safely and efficiently. + +

+ An organisation that uses OpenPGP CA has CA key with the User ID + openpgp-ca@example.org. + This key certifies all of the other keys in the organization. Of course, + for these certifications to be useful, they need to be distributed. + +

+ To better support OpenPGP CA, keys.openpgp.org will + return certifications made by a User ID of the form openpgp-ca@example.org, + if this email address has been verified, and for User IDs in their own domain. That is, the CA openpgp-ca@example.org is allowed to publish certifications for alice@example.org, + but not for bob@other.org. Conversely, if alice@example.org certifies + openpgp-ca@example.org, keys.openpgp.org will also publish that certification. + +

+ It is still possible for someone to spam certificates with this mechanism. For instance, + if an attacker registers openpgp-ca@web.de, they can issue a certification for every + certificate that has a web.de email address; we don't check that + openpgp-ca@web.de is a valid CA, we only check that the person who controls + that email address also controls the key. However, our design only allows + the publication of one such gratuitous certification per User ID, which is far from a + denial-of-service attack. + +

+ OpenPGP CA also supports the concept of CA bridges where one + organization certifies another organization's CA. These + certifications are supported if the certification is mutual. +

2019-11-12 📅
Celebrating 100.000 verified addresses! 📈