sequoia_net/
lib.rs

1//! Discovering and publishing OpenPGP certificates over the network.
2//!
3//! This crate provides access to keyservers using the [HKP] protocol,
4//! and searching and publishing [Web Key Directories].
5//!
6//! Additionally the `pks` module exposes private key operations using
7//! the [PKS][PKS] protocol.
8//!
9//! [HKP]: https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00
10//! [Web Key Directories]: https://datatracker.ietf.org/doc/html/draft-koch-openpgp-webkey-service
11//! [PKS]: https://gitlab.com/wiktor/pks
12//!
13//! # Examples
14//!
15//! This example demonstrates how to fetch a certificate from the
16//! default key server:
17//!
18//! ```no_run
19//! # use sequoia_openpgp::KeyID;
20//! # use sequoia_net::{KeyServer, Result};
21//! # async fn f() -> Result<()> {
22//! let mut ks = KeyServer::default();
23//! let keyid: KeyID = "31855247603831FD".parse()?;
24//! println!("{:?}", ks.get(keyid).await?);
25//! # Ok(())
26//! # }
27//! ```
28//!
29//! This example demonstrates how to fetch a certificate using WKD:
30//!
31//! ```no_run
32//! # async fn f() -> sequoia_net::Result<()> {
33//! let certs = sequoia_net::wkd::get(&reqwest::Client::new(), "juliett@example.org").await?;
34//! # Ok(()) }
35//! ```
36
37#![doc(html_favicon_url = "https://docs.sequoia-pgp.org/favicon.png")]
38#![doc(html_logo_url = "https://docs.sequoia-pgp.org/logo.svg")]
39#![warn(missing_docs)]
40
41// Re-exports of crates that we use in our API.
42pub use reqwest;
43
44use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
45
46use reqwest::{
47    StatusCode,
48    Url,
49};
50
51use sequoia_openpgp::{
52    self as openpgp,
53    cert::{Cert, CertParser},
54    KeyHandle,
55    packet::UserID,
56    parse::Parse,
57    serialize::Serialize,
58};
59
60#[macro_use] mod macros;
61pub mod dane;
62mod email;
63pub mod updates;
64pub mod wkd;
65
66/// <https://url.spec.whatwg.org/#fragment-percent-encode-set>
67const KEYSERVER_ENCODE_SET: &AsciiSet =
68    // Formerly DEFAULT_ENCODE_SET
69    &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`')
70    .add(b'?').add(b'{').add(b'}')
71    // The SKS keyserver as of version 1.1.6 is a bit picky with
72    // respect to the encoding.
73    .add(b'-').add(b'+').add(b'/');
74
75/// For accessing keyservers using HKP.
76#[derive(Clone)]
77pub struct KeyServer {
78    client: reqwest::Client,
79    /// The original URL given to the constructor.
80    url: Url,
81    /// The URL we use for the requests.
82    request_url: Url,
83}
84
85assert_send_and_sync!(KeyServer);
86
87impl Default for KeyServer {
88    fn default() -> Self {
89	Self::new("hkps://keys.openpgp.org/").unwrap()
90    }
91}
92
93impl KeyServer {
94    /// Returns a handle for the given URL.
95    pub fn new(url: &str) -> Result<Self> {
96	Self::with_client(url, reqwest::Client::new())
97    }
98
99    /// Returns a handle for the given URL with a custom `Client`.
100    pub fn with_client(url: &str, client: reqwest::Client) -> Result<Self> {
101        let url = reqwest::Url::parse(url)?;
102
103        let s = url.scheme();
104        match s {
105            "hkp" => (),
106            "hkps" => (),
107            _ => return Err(Error::MalformedUrl.into()),
108        }
109
110        let request_url =
111            format!("{}://{}:{}",
112                    match s {"hkp" => "http", "hkps" => "https",
113                             _ => unreachable!()},
114                    url.host().ok_or(Error::MalformedUrl)?,
115                    match s {
116                        "hkp" => url.port().or(Some(11371)),
117                        "hkps" => url.port().or(Some(443)),
118                        _ => unreachable!(),
119                    }.unwrap()).parse()?;
120
121        Ok(KeyServer { client, url, request_url })
122    }
123
124    /// Returns the keyserver's base URL.
125    pub fn url(&self) -> &reqwest::Url {
126        &self.url
127    }
128
129    /// Retrieves the certificate with the given handle.
130    ///
131    /// # Warning
132    ///
133    /// Returned certificates must be mistrusted, and be carefully
134    /// interpreted under a policy and trust model.
135    pub async fn get<H: Into<KeyHandle>>(&self, handle: H)
136                                         -> Result<Vec<Result<Cert>>>
137    {
138        let handle = handle.into();
139        let url = self.request_url.join(
140            &format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?;
141
142        let res = self.client.get(url).send().await?;
143        match res.status() {
144            StatusCode::OK => {
145                let body = res.bytes().await?;
146                let certs = CertParser::from_bytes(&body)?.collect();
147                Ok(certs)
148            }
149            StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
150            n => Err(Error::HttpStatus(n).into()),
151        }
152    }
153
154    /// Retrieves certificates containing the given `UserID`.
155    ///
156    /// If the given [`UserID`] does not follow the de facto
157    /// conventions for userids, or it does not contain a email
158    /// address, an error is returned.
159    ///
160    ///   [`UserID`]: sequoia_openpgp::packet::UserID
161    ///
162    /// # Warning
163    ///
164    /// Returned certificates must be mistrusted, and be carefully
165    /// interpreted under a policy and trust model.
166    pub async fn search<U: Into<UserID>>(&self, userid: U)
167                                         -> Result<Vec<Result<Cert>>>
168    {
169        let userid = userid.into();
170        let email = userid.email().and_then(|addr| addr.ok_or_else(||
171            openpgp::Error::InvalidArgument(
172                "UserID does not contain an email address".into()).into()))?;
173        let url = self.request_url.join(
174            &format!("pks/lookup?op=get&options=mr&search={}", email))?;
175
176        let res = self.client.get(url).send().await?;
177        match res.status() {
178            StatusCode::OK => {
179                Ok(CertParser::from_bytes(&res.bytes().await?)?.collect())
180            },
181            StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
182            n => Err(Error::HttpStatus(n).into()),
183        }
184    }
185
186    /// Sends the given key to the server.
187    pub async fn send(&self, key: &Cert) -> Result<()> {
188        use sequoia_openpgp::armor::{Writer, Kind};
189
190        let url = self.request_url.join("pks/add")?;
191        let mut w =  Writer::new(Vec::new(), Kind::PublicKey)?;
192        key.serialize(&mut w)?;
193
194        let armored_blob = w.finalize()?;
195
196        // Prepare to send url-encoded data.
197        let mut post_data = b"keytext=".to_vec();
198        post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET)
199                                    .collect::<String>().as_bytes());
200        let length = post_data.len();
201
202        let res = self.client.post(url)
203            .header("content-type", "application/x-www-form-urlencoded")
204            .header("content-length", length.to_string())
205            .body(post_data).send().await?;
206
207        match res.status() {
208            StatusCode::OK => Ok(()),
209            StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()),
210            n => Err(Error::HttpStatus(n).into()),
211        }
212    }
213}
214
215/// Results for sequoia-net.
216pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
217
218#[derive(thiserror::Error, Debug)]
219/// Errors returned from the network routines.
220#[non_exhaustive]
221pub enum Error {
222    /// A requested cert was not found.
223    #[error("Cert not found")]
224    NotFound,
225    /// A given keyserver URL was malformed.
226    #[error("Malformed URL; expected hkp: or hkps:")]
227    MalformedUrl,
228    /// The server provided malformed data.
229    #[error("Malformed response from server")]
230    MalformedResponse,
231    /// A communication partner violated the protocol.
232    #[error("Protocol violation")]
233    ProtocolViolation,
234    /// Encountered an unexpected low-level http status.
235    #[error("server returned status {0}")]
236    HttpStatus(hyper::StatusCode),
237    /// A `hyper::error::UrlError` occurred.
238    #[error(transparent)]
239    UrlError(#[from] url::ParseError),
240    /// A `http::Error` occurred.
241    #[error(transparent)]
242    HttpError(#[from] http::Error),
243    /// A `hyper::Error` occurred.
244    #[error(transparent)]
245    HyperError(#[from] hyper::Error),
246
247    /// wkd errors:
248    /// An email address is malformed
249    #[error("Malformed email address {0}")]
250    MalformedEmail(String),
251
252    /// An email address was not found in Cert userids.
253    #[error("Email address {0} not found in Cert's userids")]
254    EmailNotInUserids(String),
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn urls() {
263        assert!(KeyServer::new("keys.openpgp.org").is_err());
264        assert!(KeyServer::new("hkp://keys.openpgp.org").is_ok());
265        assert!(KeyServer::new("hkps://keys.openpgp.org").is_ok());
266    }
267}