From cd750c3d78c892351bffb7924482da992f2df98b Mon Sep 17 00:00:00 2001 From: Justus Winter Date: Mon, 11 Mar 2019 12:07:26 +0100 Subject: [PATCH] Handle uploads of multiple keys. - This is required for HKP. - I think allowing multiple keys in one upload is a good way to handle #75 too. For old keys that are uploaded by accident, we get and publish the revocations, without bothering the user with verification mails. - One concern that is not addressed is uploading multiple keys with the same user id. But, we do not handle this by design. - Fixes #75. --- README.md | 1 - dist/templates/apidoc.html.hbs | 1 - src/web/mod.rs | 101 +++++++++++++++++++++++++++++++++ src/web/upload.rs | 29 ++++++---- 4 files changed, 119 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index b283a08..d095d94 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,6 @@ the full HKP protocol. The main limitations are: - the `fingerprint` variable is ignored, - the `nm` option is ignored, - `op=index` returns either one or no keys, - - only one key may be submitted in one request, - uploads are restricted to 1 MiB, - all packets that aren't public keys, user IDs or signatures are filtered out. diff --git a/dist/templates/apidoc.html.hbs b/dist/templates/apidoc.html.hbs index 7f83a58..44aa638 100644 --- a/dist/templates/apidoc.html.hbs +++ b/dist/templates/apidoc.html.hbs @@ -101,7 +101,6 @@
  • the fingerprint variable is ignored,
  • the nm option is ignored,
  • op=index returns either one or no keys,
  • -
  • only one key may be submitted in one request,
  • uploads are restricted to 1 MiB,
  • all packets that aren't public keys, user IDs or signatures are filtered out.
  • diff --git a/src/web/mod.rs b/src/web/mod.rs index 6791150..c4f95bb 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -893,6 +893,61 @@ mod tests { &tpk); } + #[test] + fn upload_two() { + let (tmpdir, config) = configuration().unwrap(); + let filemail_into = tmpdir.path().join("filemail"); + + let db = Polymorphic::Filesystem( + Filesystem::new(config.root().unwrap().to_path_buf()).unwrap()); + let rocket = rocket_factory(rocket::custom(config), db); + let client = Client::new(rocket).expect("valid rocket instance"); + + // Generate two keys and upload them. + let tpk_0 = TPKBuilder::autocrypt( + None, Some("foo@invalid.example.com".into())) + .generate().unwrap().0; + let tpk_1 = TPKBuilder::autocrypt( + None, Some("bar@invalid.example.com".into())) + .generate().unwrap().0; + + let mut tpk_serialized = Vec::new(); + tpk_0.serialize(&mut tpk_serialized).unwrap(); + tpk_1.serialize(&mut tpk_serialized).unwrap(); + let response = vks_publish_submit(&client, &tpk_serialized); + assert_eq!(response.status(), Status::SeeOther); + assert_eq!(response.headers().get_one("Location"), + Some("/vks/v1/publish?ok")); + + // Prior to email confirmation, we should not be able to look + // them up by email address. + check_null_responses_by_email(&client, "foo@invalid.example.com"); + check_null_responses_by_email(&client, "bar@invalid.example.com"); + + // And check that we can get them back via the machine readable + // interface. + check_mr_responses_by_fingerprint(&client, &tpk_0, 0); + check_mr_responses_by_fingerprint(&client, &tpk_1, 0); + + // And check that we can see the human-readable result page. + check_hr_responses_by_fingerprint(&client, &tpk_0); + check_hr_responses_by_fingerprint(&client, &tpk_1); + + // Now check for the confirmation mails. + check_mails_and_confirm(&client, filemail_into.as_path()); + check_mails_and_confirm(&client, filemail_into.as_path()); + + // Now lookups using the mail address should work. + check_mr_response( + &client, + "/vks/v1/by-email/foo@invalid.example.com", + &tpk_0, 1); + check_mr_response( + &client, + "/vks/v1/by-email/bar@invalid.example.com", + &tpk_1, 1); + } + /// Asserts that the given URI 404s. fn check_null_response(client: &Client, uri: &str) { let response = client.get(uri).dispatch(); @@ -1063,6 +1118,52 @@ mod tests { check_hr_responses_by_fingerprint(&client, &tpk); } + #[test] + fn hkp_add_two() { + let (tmpdir, config) = configuration().unwrap(); + let filemail_into = tmpdir.path().join("filemail"); + + let db = Polymorphic::Filesystem( + Filesystem::new(config.root().unwrap().to_path_buf()).unwrap()); + let rocket = rocket_factory(rocket::custom(config), db); + let client = Client::new(rocket).expect("valid rocket instance"); + + // Generate two keys and upload them. + let tpk_0 = TPKBuilder::autocrypt( + None, Some("foo@invalid.example.com".into())) + .generate().unwrap().0; + let tpk_1 = TPKBuilder::autocrypt( + None, Some("bar@invalid.example.com".into())) + .generate().unwrap().0; + + // Prepare to /pks/add + let mut armored = Vec::new(); + { + use sequoia_openpgp::armor::{Writer, Kind}; + let mut w = Writer::new(&mut armored, Kind::PublicKey, &[]) + .unwrap(); + tpk_0.serialize(&mut w).unwrap(); + tpk_1.serialize(&mut w).unwrap(); + } + let mut post_data = String::from("keytext="); + for enc in url::form_urlencoded::byte_serialize(&armored) { + post_data.push_str(enc); + } + + // Add! + let response = client.post("/pks/add") + .body(post_data.as_bytes()) + .header(ContentType::Form) + .dispatch(); + assert_eq!(response.status(), Status::Ok); + let confirm_mail = pop_mail(filemail_into.as_path()).unwrap(); + assert!(confirm_mail.is_none()); + check_mr_responses_by_fingerprint(&client, &tpk_0, 0); + check_mr_responses_by_fingerprint(&client, &tpk_1, 0); + check_hr_responses_by_fingerprint(&client, &tpk_0); + check_hr_responses_by_fingerprint(&client, &tpk_1); + } + /// Returns and removes the first mail it finds from the given /// directory. fn pop_mail(dir: &Path) -> Result> { diff --git a/src/web/upload.rs b/src/web/upload.rs index 220fdbc..5b5c698 100644 --- a/src/web/upload.rs +++ b/src/web/upload.rs @@ -195,20 +195,27 @@ where R: Read, { use sequoia_openpgp::parse::Parse; - use sequoia_openpgp::TPK; + use sequoia_openpgp::tpk::TPKParser; + + // First, parse all TPKs and error out if one fails. + let mut tpks = Vec::new(); + for tpk in TPKParser::from_reader(reader)? { + tpks.push(tpk?); + } - let tpk = TPK::from_reader(reader)?; - let tokens = db.merge_or_publish(tpk)?; let mut results: Vec = vec!(); + for tpk in tpks { + let tokens = db.merge_or_publish(tpk)?; - if let Some(mail_service) = mail_service { - for (email, token) in tokens { - mail_service.send_verification( - &email, - &token, - domain, - )?; - results.push(email.to_string()); + if let Some(ref mail_service) = mail_service { + for (email, token) in tokens { + mail_service.send_verification( + &email, + &token, + domain, + )?; + results.push(email.to_string()); + } } }