//! Discovering and publishing OpenPGP certificates over the network. //! //! This crate provides access to keyservers using the [HKP] protocol, //! and searching and publishing [Web Key Directories]. //! //! Additionally the `pks` module exposes private key operations using //! the [PKS][PKS] protocol. //! //! [HKP]: https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00 //! [Web Key Directories]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service //! [PKS]: https://gitlab.com/wiktor/pks //! //! # Examples //! //! This example demonstrates how to fetch a certificate from the //! default key server: //! //! ```no_run //! # use sequoia_openpgp::KeyID; //! # use sequoia_net::{KeyServer, Result}; //! # async fn f() -> Result<()> { //! let mut ks = KeyServer::default(); //! let keyid: KeyID = "31855247603831FD".parse()?; //! println!("{:?}", ks.get(keyid).await?); //! # Ok(()) //! # } //! ``` //! //! This example demonstrates how to fetch a certificate using WKD: //! //! ```no_run //! # async fn f() -> sequoia_net::Result<()> { //! let certs = sequoia_net::wkd::get(&reqwest::Client::new(), "juliett@example.org").await?; //! # Ok(()) } //! ``` #![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")] #![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")] #![warn(missing_docs)] // Re-exports of crates that we use in our API. pub use reqwest; use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; use reqwest::{ StatusCode, Url, }; use sequoia_openpgp::{ self as openpgp, cert::{Cert, CertParser}, KeyHandle, packet::UserID, parse::Parse, serialize::Serialize, }; #[macro_use] mod macros; pub mod dane; mod email; pub mod pks; pub mod updates; pub mod wkd; /// const KEYSERVER_ENCODE_SET: &AsciiSet = // Formerly DEFAULT_ENCODE_SET &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`') .add(b'?').add(b'{').add(b'}') // The SKS keyserver as of version 1.1.6 is a bit picky with // respect to the encoding. .add(b'-').add(b'+').add(b'/'); /// For accessing keyservers using HKP. #[derive(Clone)] pub struct KeyServer { client: reqwest::Client, /// The original URL given to the constructor. url: Url, /// The URL we use for the requests. request_url: Url, } assert_send_and_sync!(KeyServer); impl Default for KeyServer { fn default() -> Self { Self::new("hkps://keys.openpgp.org/").unwrap() } } impl KeyServer { /// Returns a handle for the given URL. pub fn new(url: &str) -> Result { Self::with_client(url, reqwest::Client::new()) } /// Returns a handle for the given URL with a custom `Client`. pub fn with_client(url: &str, client: reqwest::Client) -> Result { let url = reqwest::Url::parse(url)?; let s = url.scheme(); match s { "hkp" => (), "hkps" => (), _ => return Err(Error::MalformedUrl.into()), } let request_url = format!("{}://{}:{}", match s {"hkp" => "http", "hkps" => "https", _ => unreachable!()}, url.host().ok_or(Error::MalformedUrl)?, match s { "hkp" => url.port().or(Some(11371)), "hkps" => url.port().or(Some(443)), _ => unreachable!(), }.unwrap()).parse()?; Ok(KeyServer { client, url, request_url }) } /// Returns the keyserver's base URL. pub fn url(&self) -> &reqwest::Url { &self.url } /// Retrieves the certificate with the given handle. /// /// # Warning /// /// Returned certificates must be mistrusted, and be carefully /// interpreted under a policy and trust model. pub async fn get>(&self, handle: H) -> Result>> { let handle = handle.into(); let url = self.request_url.join( &format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?; let res = self.client.get(url).send().await?; match res.status() { StatusCode::OK => { let body = res.bytes().await?; let certs = CertParser::from_bytes(&body)?.collect(); Ok(certs) } StatusCode::NOT_FOUND => Err(Error::NotFound.into()), n => Err(Error::HttpStatus(n).into()), } } /// Retrieves certificates containing the given `UserID`. /// /// If the given [`UserID`] does not follow the de facto /// conventions for userids, or it does not contain a email /// address, an error is returned. /// /// [`UserID`]: sequoia_openpgp::packet::UserID /// /// # Warning /// /// Returned certificates must be mistrusted, and be carefully /// interpreted under a policy and trust model. #[allow(clippy::blocks_in_if_conditions)] pub async fn search>(&self, userid: U) -> Result>> { let userid = userid.into(); let email = userid.email2().and_then(|addr| addr.ok_or_else(|| openpgp::Error::InvalidArgument( "UserID does not contain an email address".into()).into()))?; let url = self.request_url.join( &format!("pks/lookup?op=get&options=mr&search={}", email))?; let res = self.client.get(url).send().await?; match res.status() { StatusCode::OK => { Ok(CertParser::from_bytes(&res.bytes().await?)?.collect()) }, StatusCode::NOT_FOUND => Err(Error::NotFound.into()), n => Err(Error::HttpStatus(n).into()), } } /// Sends the given key to the server. pub async fn send(&self, key: &Cert) -> Result<()> { use sequoia_openpgp::armor::{Writer, Kind}; let url = self.request_url.join("pks/add")?; let mut w = Writer::new(Vec::new(), Kind::PublicKey)?; key.serialize(&mut w)?; let armored_blob = w.finalize()?; // Prepare to send url-encoded data. let mut post_data = b"keytext=".to_vec(); post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET) .collect::().as_bytes()); let length = post_data.len(); let res = self.client.post(url) .header("content-type", "application/x-www-form-urlencoded") .header("content-length", length.to_string()) .body(post_data).send().await?; match res.status() { StatusCode::OK => Ok(()), StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()), n => Err(Error::HttpStatus(n).into()), } } } /// Results for sequoia-net. pub type Result = ::std::result::Result; #[derive(thiserror::Error, Debug)] /// Errors returned from the network routines. #[non_exhaustive] pub enum Error { /// A requested cert was not found. #[error("Cert not found")] NotFound, /// A given keyserver URL was malformed. #[error("Malformed URL; expected hkp: or hkps:")] MalformedUrl, /// The server provided malformed data. #[error("Malformed response from server")] MalformedResponse, /// A communication partner violated the protocol. #[error("Protocol violation")] ProtocolViolation, /// Encountered an unexpected low-level http status. #[error("Error communicating with server")] HttpStatus(hyper::StatusCode), /// A `hyper::error::UrlError` occurred. #[error("URL Error")] UrlError(#[from] url::ParseError), /// A `http::Error` occurred. #[error("http Error")] HttpError(#[from] http::Error), /// A `hyper::Error` occurred. #[error("Hyper Error")] HyperError(#[from] hyper::Error), /// wkd errors: /// An email address is malformed #[error("Malformed email address {0}")] MalformedEmail(String), /// An email address was not found in Cert userids. #[error("Email address {0} not found in Cert's userids")] EmailNotInUserids(String), } #[cfg(test)] mod tests { use super::*; #[test] fn urls() { assert!(KeyServer::new("keys.openpgp.org").is_err()); assert!(KeyServer::new("hkp://keys.openpgp.org").is_ok()); assert!(KeyServer::new("hkps://keys.openpgp.org").is_ok()); } }