summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjuga0 <juga@riseup.net>2019-05-03 12:42:39 +0000
committerjuga0 <juga@riseup.net>2019-05-17 13:56:32 +0000
commite249693ccc1298202ebd21797c5c18379356bb7b (patch)
tree66bdefa8685d1d8736469b4dca4d57128efbd5e6
parent2cec2b2d55e31c78ab5c98f23fa85f7ba400b71e (diff)
net, tool: Add wkd module and command
implementing WKD client Closes: #251 (Commit 👾)
-rw-r--r--Cargo.lock11
-rw-r--r--net/Cargo.toml3
-rw-r--r--net/src/async.rs122
-rw-r--r--net/src/lib.rs23
-rw-r--r--net/src/wkd.rs246
-rw-r--r--net/tests/wkd.rs2
-rw-r--r--tool/src/sq-usage.rs45
-rw-r--r--tool/src/sq.rs32
-rw-r--r--tool/src/sq_cli.rs21
9 files changed, 503 insertions, 2 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 78c80361..bf962256 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,3 +1,5 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
[[package]]
name = "adler32"
version = "1.0.3"
@@ -1588,13 +1590,16 @@ dependencies = [
"hyper-tls 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)",
"native-tls 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "nettle 5.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"sequoia-core 0.7.0",
"sequoia-openpgp 0.7.0",
+ "sequoia-rfc2822 0.7.0",
"tokio-core 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
"tokio-io 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)",
"url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zbase32 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -2270,6 +2275,11 @@ dependencies = [
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "zbase32"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[metadata]
"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
"checksum aho-corasick 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "e6f484ae0c99fec2e858eb6134949117399f222608d84cadb3f58c1f97c2364c"
@@ -2512,3 +2522,4 @@ dependencies = [
"checksum wincolor 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "561ed901ae465d6185fa7864d63fbd5720d0ef718366c9a4dc83cf6170d7e9ba"
"checksum winconsole 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3ef84b96d10db72dd980056666d7f1e7663ce93d82fa33b63e71c966f4cf5032"
"checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+"checksum zbase32 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0f9079049688da5871a7558ddacb7f04958862c703e68258594cb7a862b5e33f"
diff --git a/net/Cargo.toml b/net/Cargo.toml
index 1fcd6734..27c3a547 100644
--- a/net/Cargo.toml
+++ b/net/Cargo.toml
@@ -22,6 +22,7 @@ maintenance = { status = "actively-developed" }
[dependencies]
sequoia-openpgp = { path = "../openpgp", version = "0.7" }
sequoia-core = { path = "../core", version = "0.7" }
+sequoia-rfc2822 = { path = "../rfc2822", version = "0.7" }
failure = "0.1.2"
futures = "0.1"
@@ -30,10 +31,12 @@ hyper = "0.12"
hyper-tls = "0.3"
libc = "0.2.33"
native-tls = "0.2.0"
+nettle = "5.0"
percent-encoding = "1.0.1"
tokio-core = "0.1"
tokio-io = "0.1.4"
url = "1.6.0"
+zbase32 = "0.1.2"
[dev-dependencies]
rand = "0.6"
diff --git a/net/src/async.rs b/net/src/async.rs
index bc355eef..b538a7ef 100644
--- a/net/src/async.rs
+++ b/net/src/async.rs
@@ -12,15 +12,18 @@ use hyper_tls::HttpsConnector;
use native_tls::{Certificate, TlsConnector};
use percent_encoding::{percent_encode, DEFAULT_ENCODE_SET};
use std::convert::From;
+use std::net::ToSocketAddrs;
use std::io::Cursor;
use tokio_core::reactor::Handle;
use url::Url;
use openpgp::TPK;
+ use openpgp::parse::Parse;
use openpgp::{KeyID, armor, serialize::Serialize};
-use openpgp::parse::Parse;
use sequoia_core::{Context, NetworkPolicy};
+use wkd;
+
use super::{Error, Result};
define_encode_set! {
@@ -228,3 +231,120 @@ impl AClient for Client<HttpsConnector<HttpConnector>> {
pub(crate) fn url2uri(uri: Url) -> hyper::Uri {
format!("{}", uri).parse().unwrap()
}
+
+
+/// Retrieves the TPKs that contain userids with a given email address from
+/// a Web Key Directory URL.
+///
+/// [draft-koch]:
+/// There are two variants on how to form the request URI: The advanced and the
+/// direct method. Implementations MUST first try the advanced method. Only if
+/// the required sub-domain does not exist, they SHOULD fall back to the direct
+/// method.
+/// [...]
+/// The HTTP GET method MUST return the binary representation of the OpenPGP
+/// key for the given mail address.
+/// [...]
+/// Note that the key may be revoked or expired - it is up to the client to
+/// handle such conditions. To ease distribution of revoked keys, a server may
+/// return revoked keys in addition to a new key. The keys are returned by a
+/// single request as concatenated key blocks.
+
+// XXX: Maybe the direct method should be tried on other errors too.
+// https://mailarchive.ietf.org/arch/msg/openpgp/6TxZc2dQFLKXtS0Hzmrk963EteE
+pub fn async_wkd_get<S: AsRef<str>>(email_address: S)
+ -> Box<Future<Item=Vec<TPK>, Error=failure::Error> + 'static> {
+ let email_address = email_address.as_ref().to_string();
+ // WKD must use TLS
+ // XXX: Change this expect for an error
+ let https = HttpsConnector::new(4).expect("TLS initialization failed");
+ let client = Client::builder()
+ .build::<_, hyper::Body>(https);
+
+ // FIXME: solve the move!!!
+ let wkd_url = wkd::WkdUrl::from(&email_address);
+ let wkd_url2 = wkd::WkdUrl::from(&email_address);
+
+ // Advanced method
+ let url_string = wkd_url.unwrap().to_string(None);
+
+ // 1. option: just resolve the domain.
+ // This is not async, solving the domain in an async way will require
+ // other dependency.
+ let uri = match url_string.unwrap().to_socket_addrs() {
+ Err(e) =>
+ // getaddrinfo is POSIX and seems to always return this string
+ // in all systems.
+ if e.to_string().contains("No address associated with hostname") {
+ // Direct method
+ wkd_url2.unwrap().to_uri(true)
+ } else {
+ Err(failure::Error::from(e))
+ },
+ Ok(_) => wkd_url2.unwrap().to_uri(None),
+ };
+ // Then, there is no need to repeat the HTTP request
+ Box::new(
+ client.get(uri.unwrap())
+ .from_err()
+ .and_then(|res| res.into_body().concat2().from_err())
+ .and_then(move |body|
+ match wkd::parse_body(&body, &email_address) {
+ Ok(tpks) => future::done(Ok(tpks)),
+ Err(e) => future::err(e).into(),
+ })
+ )
+
+ // 2. option: consume the future to know if it was an error and which
+ // type. But wait is blocking.
+ //
+ // Advanced method
+ // let mut uri = wkd_url1.unwrap().to_uri(None);
+ //
+ // let response = match client.get(uri.unwrap()).map(|r| r).wait() {
+ // Ok(r) => future::ok(r),
+ // Err(e) =>
+ // if e.to_string().contains("No address associated with hostname") {
+ // // Direct method
+ // uri = wkd_url2.unwrap().to_uri(false);
+ // // Then also have to consume it here
+ // future::done(client.get(uri.unwrap()).map(|r| r).wait())
+ // } else {
+ // future::err(e).into()
+ // },
+ // };
+ // Box::new(
+ // response.from_err()
+ // .and_then(|res| res.into_body().concat2().from_err())
+ // .and_then(move |body|
+ // match wkd::parse_body(&body, &email_address) {
+ // Ok(tpks) => future::done(Ok(tpks)),
+ // Err(e) => future::err(e).into(),
+ // })
+ // )
+
+ // 3. option: If there's a way to convert a future::done into a
+ // a response future, then this would work, it courrently doesn't in the
+ // else block.
+ // This does not work because of the else
+ // Box::new(
+ // client.get(uri.unwrap())
+ // .or_else(|e| -> ResponseFuture {
+ // // If i put this if, then i need to put an else
+ // // And the else must be other ResponseFuture
+ // if e.to_string().contains("No address associated with hostname") {
+ // // Direct method
+ // uri = wkd_url2.unwrap().to_uri(false);
+ // client.get(uri.unwrap())
+ // } else {
+ // // how to convert an hyper::error to a ResponseFuture?,
+ // // new is private
+ // ResponseFuture::new(Box::new(future::err(e).into()))
+ // }}) .and_then(|res| res.into_body().concat2().from_err())
+ // .and_then(move |body|
+ // match wkd::parse_body(&body, &email_address) {
+ // Ok(tpks) => future::done(Ok(tpks)),
+ // Err(e) => future::err(e).into(),
+ // })
+ // )
+}
diff --git a/net/src/lib.rs b/net/src/lib.rs
index c9f7cd0a..35ed8173 100644
--- a/net/src/lib.rs
+++ b/net/src/lib.rs
@@ -34,6 +34,7 @@
extern crate sequoia_openpgp as openpgp;
extern crate sequoia_core;
+extern crate sequoia_rfc2822 as rfc2822;
#[macro_use]
extern crate failure;
@@ -42,11 +43,13 @@ extern crate http;
extern crate hyper;
extern crate hyper_tls;
extern crate native_tls;
+extern crate nettle;
extern crate tokio_core;
extern crate tokio_io;
#[macro_use]
extern crate percent_encoding;
extern crate url;
+extern crate zbase32;
use hyper::client::{ResponseFuture, HttpConnector};
use hyper::{Client, Request, Body};
@@ -62,6 +65,7 @@ use sequoia_core::Context;
pub mod async;
use async::url2uri;
+pub mod wkd;
/// For accessing keyservers using HKP.
pub struct KeyServer {
@@ -169,6 +173,15 @@ pub enum Error {
/// A `native_tls::Error` occurred.
#[fail(display = "TLS Error")]
TlsError(native_tls::Error),
+
+ /// wkd errors:
+ /// An email address is malformed
+ #[fail(display = "Malformed email address {}", _0)]
+ MalformedEmail(String),
+
+ /// An email address was not found in TPK userids.
+ #[fail(display = "Email address {} not found in TPK's userids", _0)]
+ EmailNotInUserids(String),
}
impl From<http::Error> for Error {
@@ -189,6 +202,16 @@ impl From<url::ParseError> for Error {
}
}
+
+/// Runs async_wkd_get.
+// This function must have the same signature as async_wkd_get.
+// XXX: Maybe implement WkdServer and AWkdClient.
+pub fn wkd_get<S: AsRef<str>>(email_address: S) -> Result<Vec<TPK>> {
+ let mut core = Core::new()?;
+ core.run(async::async_wkd_get(&email_address))
+}
+
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/net/src/wkd.rs b/net/src/wkd.rs
new file mode 100644
index 00000000..7d0e4b7e
--- /dev/null
+++ b/net/src/wkd.rs
@@ -0,0 +1,246 @@
+//! OpenPGP Web Key Directory client[draft-koch]
+//!
+//! [draft-koch]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service/#section-3.1
+
+// XXX: We might want to merge the 2 structs in the future and move the
+// functions to methods.
+use hyper::Uri;
+// Hash implements the traits for Sha1
+// Sha1 is used to obtain a 20 bytes digest that after zbase32 encoding can
+// be used as file name
+use nettle::{
+ Hash, hash::insecure_do_not_use::Sha1,
+};
+use url::Url;
+
+use openpgp::TPK;
+use openpgp::parse::Parse;
+use openpgp::tpk::TPKParser;
+
+use super::{Result, Error};
+
+
+/// Stores the local_part and domain of an email address.
+pub struct EmailAddress {
+ local_part: String,
+ domain: String,
+}
+
+
+impl EmailAddress {
+ /// Returns an EmailAddress from an email address string.
+ ///
+ /// [draft-koch]:
+ /// To help with the common pattern of using capitalized names
+ /// (e.g. "Joe.Doe@example.org") for mail addresses, and under the
+ /// premise that almost all MTAs treat the local-part case-insensitive
+ /// and that the domain-part is required to be compared case-insensitive
+ /// anyway, all upper-case ASCII characters in a User ID are mapped to
+ /// lowercase. Non-ASCII characters are not changed.
+ fn from<S: AsRef<str>>(email_address: S) -> Result<Self> {
+ // Ensure that is a valid email address by parsing it.
+ // and return the errors that it returns.
+ // This is also done in hagrid.
+ let email_address = email_address.as_ref();
+ let v: Vec<&str> = email_address.split('@').collect();
+ if v.len() != 2 {
+ return Err(Error::MalformedEmail(email_address.into()).into())
+ };
+
+ // Convert to lowercase without tailoring, i.e. without taking
+ // any locale into account. See:
+ // https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase
+ let email = EmailAddress {
+ local_part: v[0].to_lowercase(),
+ domain: v[1].to_lowercase()
+ };
+ Ok(email)
+ }
+}
+
+
+/// Stores the parts needed to create a Web Key Directory URL.
+#[derive(Clone)]
+pub struct WkdUrl {
+ domain: String,
+ local_encoded: String,
+ local_part: String,
+}
+
+
+impl WkdUrl {
+ /// Returns a WkdUrl from an email address string.or error.
+ pub fn from<S: AsRef<str>>(email_address: S) -> Result<Self> {
+ let email = EmailAddress::from(email_address)?;
+ let local_encoded = encode_local_part(&email.local_part);
+ let wkd_url = WkdUrl {
+ domain : email.domain,
+ local_encoded : local_encoded,
+ local_part : email.local_part,
+ };
+ Ok(wkd_url)
+ }
+
+ /// Returns an URL string from a WkdUrl.
+ pub fn to_string<T>(self, direct_method: T) -> Result<String>
+ where T: Into<Option<bool>> {
+ let direct_method = direct_method.into().unwrap_or(false);
+ let authority;
+ let path;
+ if direct_method {
+ authority = self.domain;
+ path = ".well-known/openpgpkey/hu/".to_string() +
+ &self.local_encoded;
+ } else {
+ authority = "openpgpkey.".to_string() + &self.domain;
+ path = ".well-known/openpgpkey/".to_string() + &self.domain +
+ "/hu/" + &self.local_encoded;
+ };
+ let path_and_query = path + "?l=" + &self.local_part;
+ let url_string = "https://".to_string() + &authority + "/"
+ + &path_and_query + ":443";
+ Ok(url_string)
+ }
+
+ /// Returns an url::Url.
+ pub fn to_url<T>(self, direct_method: T) -> Result<Url>
+ where T: Into<Option<bool>> {
+ let url_string = self.to_string(direct_method)?;
+ let url = Url::parse(url_string.as_str())?;
+ Ok(url)
+ }
+
+ /// Returns an hyper::Uri.
+ pub fn to_uri<T>(self, direct_method: T) -> Result<Uri>
+ where T: Into<Option<bool>> {
+ // let url = self.to_url(direct_method)?;
+ let url_string = self.to_string(direct_method)?;
+ let uri = url_string.as_str().parse::<Uri>()?;
+ Ok(uri)
+ }
+}
+
+
+/// Returns a 32 characters string from the local part of an email address
+///
+/// [draft-koch] section 3.1, Key Discovery:
+/// The so mapped local-part is hashed using the SHA-1 algorithm. The
+/// resulting 160 bit digest is encoded using the Z-Base-32 method as
+/// described in [RFC6189], section 5.1.6. The resulting string has a
+/// fixed length of 32 octets.
+fn encode_local_part<S: AsRef<str>>(local_part: S) -> String {
+ let mut hasher = Sha1::default();
+ hasher.update(local_part.as_ref().as_bytes());
+ // Declare and assign a 20 bytes length vector to use in hasher.result
+ let mut local_hash = vec![0; 20];
+ hasher.digest(&mut local_hash);
+ // After z-base-32 encoding 20 bytes, it will be 32 bytes long.
+ zbase32::encode_full_bytes(&local_hash[..])
+}
+
+
+/// Parse an HTTP response body that may contain TPKs and filter them based on
+/// whether they contain a userid with the given email address.
+///
+/// [draft-koch]:
+/// The key needs to carry a User ID packet ([RFC4880]) with that mail address.
+pub(crate) fn parse_body<S: AsRef<str>>(body: &[u8], email_address: S)
+ -> Result<Vec<TPK>> {
+ let email_address = email_address.as_ref();
+ // This will fail on the first packet that can not be parsed.
+ let packets = TPKParser::from_bytes(&body)?;
+ // Collect only the correct packets.
+ let tpks: Vec<TPK> = packets.flatten().collect();
+ // Collect only the TPKs that contain the email in any of their userids
+ let valid_tpks: Vec<TPK> = tpks.iter()
+ // XXX: This filter could become a TPK method, but it adds other API
+ // method to maintain
+ .filter(|tpk| {tpk.userids()
+ .any(|uidb|
+ if let Ok(Some(a)) = uidb.userid().address() {
+ a == email_address
+ } else { false })
+ }).cloned().collect();
+ if valid_tpks.is_empty() {
+ Err(Error::EmailNotInUserids(email_address.into()).into())
+ } else {
+ Ok(valid_tpks)
+ }
+}
+
+
+#[cfg(test)]
+mod tests {
+ use openpgp::serialize::Serialize;
+ use openpgp::tpk::TPKBuilder;
+
+ use super::*;
+
+ #[test]
+ fn encode_local_part_works() {
+ let encoded_part = encode_local_part("test1");
+ assert_eq!("stnkabub89rpcphiz4ppbxixkwyt1pic", encoded_part);
+ assert_eq!(32, encoded_part.len());
+ }
+
+
+ #[test]
+ fn email_address_from() {
+ let email_address = EmailAddress::from("test1@example.com").unwrap();
+ assert_eq!(email_address.domain, "example.com");
+ assert_eq!(email_address.local_part, "test1");
+ assert!(EmailAddress::from("thisisnotanemailaddress").is_err());
+ }
+
+ #[test]
+ fn wkd_url_roundtrip() {
+ // Advanced method
+ let expected_url =
+ "https://openpgpkey.example.com/\
+ .well-known/openpgpkey/example.com/hu/\
+ stnkabub89rpcphiz4ppbxixkwyt1pic?l=test1";
+ let wkd_url = WkdUrl::from("test1@example.com").unwrap();
+ assert_eq!(expected_url, wkd_url.clone().to_string(None).unwrap());
+ assert_eq!(Url::parse(expected_url).unwrap(),
+ wkd_url.clone().to_url(None).unwrap());
+ assert_eq!(expected_url.parse::<Uri>().unwrap(),
+ wkd_url.clone().to_uri(None).unwrap());
+
+ // Direct method
+ let expected_url =
+ "https://example.com/\
+ .well-known/openpgpkey/hu/\
+ stnkabub89rpcphiz4ppbxixkwyt1pic?l=test1";
+ assert_eq!(expected_url, wkd_url.clone().to_string(None).unwrap());
+ assert_eq!(Url::parse(expected_url).unwrap(),
+ wkd_url.clone().to_url(None).unwrap());
+ assert_eq!(expected_url.parse::<Uri>().unwrap(),
+ wkd_url.to_uri(None).unwrap());
+ }
+
+ #[test]
+ fn test_parse_body() {
+ let (tpk, _) = TPKBuilder::new()
+ .add_userid("test@example.example")
+ .generate()
+ .unwrap();
+ let mut buffer: Vec<u8> = Vec::new();
+ tpk.serialize(&mut buffer).unwrap();
+ // FIXME!!!!
+ let valid_tpks = parse_body(&buffer, "juga@sequoia.org");
+ // The userid is not in the TPK
+ assert!(valid_tpks.is_err());
+ // XXX: add userid to the tpk, instead of creating a new one
+ // tpk.add_userid("juga@sequoia.org");
+ let (tpk, _) = TPKBuilder::new()
+ .add_userid("test@example.example")
+ .add_userid("juga@sequoia.org")
+ .generate()
+ .unwrap();
+ tpk.serialize(&mut buffer).unwrap();
+ let valid_tpks = parse_body(&buffer, "juga@sequoia.org");
+ assert!(valid_tpks.is_ok());
+ assert!(valid_tpks.unwrap().len() == 1);
+ // XXX: Test with more TPKs
+ }
+}
diff --git a/net/tests/wkd.rs b/net/tests/wkd.rs
new file mode 100644
index 00000000..c8b108a4
--- /dev/null
+++ b/net/tests/wkd.rs
@@ -0,0 +1,2 @@
+//! Integration tests for the Web Key Directory client
+// XXX
diff --git a/tool/src/sq-usage.rs b/tool/src/sq-usage.rs
index f3159937..9902d0a0 100644
--- a/tool/src/sq-usage.rs
+++ b/tool/src/sq-usage.rs
@@ -34,6 +34,7 @@
//! key Manipulates keys
//! list Lists key stores and known keys
//! packet OpenPGP Packet manipulation
+//! wkd Interacts with Web Key Directories
//! ```
//!
//! ## Subcommand decrypt
@@ -625,5 +626,49 @@
//! ARGS:
//! <FILE> Sets the input file to use
//! ```
+//! ## Subcommand wkd
+//! ```text
+//! Interacts with Web Key Directories
+//!
+//! USAGE:
+//! sq wkd [SUBCOMMAND]
+//!
+//! FLAGS:
+//! -h, --help Prints help information
+//! -V, --version Prints version information
+//!
+//! SUBCOMMANDS:
+//! get Writes to the standard output the Transferable Public Key retrieved from a Web Key Directory, given an email address
+//! help Prints this message or the help of the given subcommand(s)
+//! url Prints the Web Key Directory URI of an email address.
+//! ```
+//! ### Subcommand wkd url
+//! ```text
+//! Prints the Web Key Directory URI of an email address
+//!
+//! USAGE:
+//! sq wkd url [EMAIL_ADDRESS]
+//!
+//! FLAGS:
+//! -h, --help Prints help information
+//! -V, --version Prints version information
+//!
+//! ARGS:
+//! <EMAIL_ADDRESS> The email address from which to obtain the WKD URI.
+//! ```
+//! ### Subcommand wkd get
+//! ```text
+//!
+//! Writes to the standard output the Transferable Public Key retrieved from a Web Key Directory, given an email address
+//! USAGE:
+//! sq wkd get [EMAIL_ADDRESS]
+//!
+//! FLAGS:
+//! -h, --help Prints help information
+//! -V, --version Prints version information
+//!
+//! ARGS:
+//! <EMAIL_ADDRESS> The email address from which to obtain the TPK from a WKD.
+//! ```
include!("sq.rs");
diff --git a/tool/src/sq.rs b/tool/src/sq.rs
index b18bada4..648402d2 100644
--- a/tool/src/sq.rs
+++ b/tool/src/sq.rs
@@ -26,7 +26,7 @@ use openpgp::conversions::hex;
use openpgp::parse::Parse;
use openpgp::serialize::Serialize;
use sequoia_core::{Context, NetworkPolicy};
-use sequoia_net::KeyServer;
+use sequoia_net::{KeyServer, wkd, wkd_get};
use sequoia_store::{Store, LogIter};
mod sq_cli;
@@ -458,6 +458,36 @@ fn real_main() -> Result<(), failure::Error> {
("generate", Some(m)) => commands::key::generate(m, force)?,
_ => unreachable!(),
},
+ ("wkd", Some(m)) => {
+ match m.subcommand() {
+ ("url", Some(m)) => {
+ let email_address = m.value_of("input").unwrap();
+ let wkd_url = wkd::WkdUrl::from(email_address)?;
+ // XXX: Add other subcomand to specify whether it should be
+ // created with the advanced or the direct method.
+ let url = wkd_url.to_url(None)?;
+ println!("{}", url);
+ },
+ ("get", Some(m)) => {
+ let email_address = m.value_of("input").unwrap();
+ // XXX: EmailAddress could be created here to
+ // check it's a valid email address, print the error to
+ // stderr and exit.
+ // Because it might be created a WkdServer struct, not
+ // doing it for now.
+ let tpks = wkd_get(&email_address)?;
+ // This is different to `store export` and `keyserver get`,
+ // Since the output is always bytes.
+ // XXX: Still give the possibility to write to a file.
+ let mut output = create_or_stdout(m.value_of("output"), force)?;
+
+ for tpk in tpks {
+ tpk.serialize(&mut output)?;
+ }
+ },
+ _ => unreachable!(),
+ }
+ },
_ => unreachable!(),
}
diff --git a/tool/src/sq_cli.rs b/tool/src/sq_cli.rs
index 9c5e50b9..44cc03dd 100644
--- a/tool/src/sq_cli.rs
+++ b/tool/src/sq_cli.rs
@@ -425,4 +425,25 @@ pub fn build() -> App<'static, 'static> {
.help("Sets the prefix to use for output files \
(defaults to the input filename with a dash, \
or 'output')"))))
+
+ .subcommand(SubCommand::with_name("wkd")
+ .about("Interacts with Web Key Directories")
+ .setting(AppSettings::ArgRequiredElseHelp)
+ .subcommand(SubCommand::with_name("url")
+ .about("Prints the Web Key Directory URl of \
+ an email address.")
+ .arg(Arg::with_name("input")
+ .value_name("EMAIL_ADDRESS")
+ .help("The email address from which to \
+ obtain the WKD URI.")))
+ .subcommand(SubCommand::with_name("get")
+ .about("Writes to the standard output the \
+ Transferable Public Key retrieved \
+ from a Web Key Directory, given an \
+ email address")
+ .arg(Arg::with_name("input")
+ .value_name("EMAIL_ADDRESS")
+ .help("The email address from which to \
+ obtain the TPK from a WKD.")))
+ )
}