database: serve first-party attested third-party certifications

This implements support for third-party userid certifications.  To
prevent denial-of-service attacks, we only merge those certifications
that are attested by the key holder.

The key holder attests the certifications using an Attested Key
Signature containing the digests of the certifications in an Attested
Certifications subpacket as specified in RFC4880bis-10.

Fixes #124.
This commit is contained in:
Justus Winter 2021-01-12 10:53:21 +01:00 committed by Vincent Breitmoser
parent 3ecd264c59
commit 39c0e12ac6
4 changed files with 143 additions and 10 deletions

14
Cargo.lock generated
View File

@ -185,11 +185,6 @@ name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "base64"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]]
name = "bindgen"
version = "0.51.1"
@ -274,7 +269,7 @@ dependencies = [
[[package]]
name = "buffered-reader"
version = "1.0.0"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"libc 0.2.80 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2072,8 +2067,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"anyhow 1.0.34 (registry+https://github.com/rust-lang/crates.io-index)",
"backtrace 0.3.54 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
"buffered-reader 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)",
"buffered-reader 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
"dyn-clone 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"eax 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -2778,7 +2773,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b25d992356d2eb0ed82172f5248873db5560c4721f564b13cb5193bda5e668e"
"checksum base64 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7"
"checksum base64 0.12.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
"checksum base64 0.13.0 (registry+https://github.com/rust-lang/crates.io-index)" = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
"checksum base64 0.9.3 (registry+https://github.com/rust-lang/crates.io-index)" = "489d6c0ed21b11d038c31b6ceccca973e65d73ba3bd8ecb9a2babf5546164643"
"checksum bindgen 0.51.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ebd71393f1ec0509b553aa012b9b58e81dadbdff7130bd3b8cba576e69b32f75"
"checksum bit-set 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de"
@ -2789,7 +2783,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
"checksum block-cipher-trait 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774"
"checksum block-padding 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5"
"checksum buf_redux 0.8.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b953a6887648bb07a535631f2bc00fbdb2a2216f135552cb3f534ed136b9c07f"
"checksum buffered-reader 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f5711ccfa79a8167779ad2176d3334078f03b1579ddf8f42aa556196eba60a42"
"checksum buffered-reader 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5f76f15096822ca97dcc626a98ce3eb93c8afc795f33994a63e8d4ed767007e4"
"checksum bumpalo 3.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2e8c087f005730276d1096a652e92a8bacee2e2472bcc9715a74d2bec38b5820"
"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7"
"checksum byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"

View File

@ -928,4 +928,12 @@ mod tests {
Some(fp.clone()));
db.check_consistency().expect("inconsistent database");
}
#[test]
fn attested_key_signatures() -> Result<()> {
let (_tmp_dir, mut db, log_path) = open_db();
test::attested_key_signatures(&mut db, &log_path)?;
db.check_consistency()?;
Ok(())
}
}

View File

@ -4,6 +4,7 @@ use std::convert::TryFrom;
use openpgp::{
Cert,
types::RevocationStatus,
cert::prelude::*,
serialize::SerializeInto as _,
policy::StandardPolicy,
};
@ -50,6 +51,17 @@ pub fn tpk_clean(tpk: &Cert) -> Result<Cert> {
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()) }
// Reasoning about the currently attested certifications
// requires a policy.
if let Ok(vuid) = uidb.with_policy(&POLICY, None) {
for s in vuid.attestation_key_signatures() {
acc.push(s.clone().into());
}
for s in vuid.attested_certifications() {
acc.push(s.clone().into());
}
}
}
Cert::from_packets(acc.into_iter())

View File

@ -16,6 +16,7 @@
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use anyhow::Result;
use Database;
use Query;
@ -1117,6 +1118,124 @@ pub fn test_no_selfsig(db: &mut impl Database, log_path: &Path) {
}, tpk_status);
}
/// Makes sure that attested key signatures are correctly handled.
pub fn attested_key_signatures(db: &mut impl Database, log_path: &Path)
-> Result<()> {
use std::time::{SystemTime, Duration};
use openpgp::{
packet::signature::SignatureBuilder,
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 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 attest that certification.
let attestations =
bob.userids().next().unwrap().attest_certifications(
&POLICY,
&mut bob_signer,
vec![&alice_certifies_bob])?;
assert_eq!(attestations.len(), 1);
let attestation = attestations[0].clone();
// 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);
// Add the attestation, merge into the db, check that the
// certification is now included.
let bob_attested = bob.clone().insert_packets(vec![
attestation.clone(),
])?;
db.merge(bob_attested.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(), 1);
assert_eq!(bob_.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attestation_key_signatures().count(), 1);
assert_eq!(bob_.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attested_certifications().count(), 1);
// Make a random merge with Bob's unattested cert, demonstrating
// that the attestation still works.
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(), 1);
// Finally, withdraw consent by overriding the attestation, merge
// into the db, check that the certification is now gone.
let attestations =
bob_attested.userids().next().unwrap().attest_certifications(
&POLICY,
&mut bob_signer,
&[])?;
assert_eq!(attestations.len(), 1);
let clear_attestation = attestations[0].clone();
let bob = bob.insert_packets(vec![
clear_attestation.clone(),
])?;
assert_eq!(bob.userids().nth(0).unwrap().certifications().count(), 1);
assert_eq!(bob.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attestation_key_signatures().count(), 1);
assert_eq!(bob.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attested_certifications().count(), 0);
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);
assert_eq!(bob_.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attestation_key_signatures().count(), 1);
assert_eq!(bob_.with_policy(&POLICY, None)?
.userids().nth(0).unwrap().attested_certifications().count(), 0);
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