Skip to main content

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/// Encoding set for HTTP/GET requests
75///
76/// This expands KEYSERVER_ENCODE_SET by '&', '=' and '%'.
77const KEYSERVER_GET_ENCODE_SET: &AsciiSet = &KEYSERVER_ENCODE_SET
78    // add chars to protect the key/value structure in a HTTP/GET.
79    .add(b'&').add(b'=')
80    // as '%' has a special meaning, it needs to also be encoded.
81    .add(b'%');
82
83/// For accessing keyservers using HKP.
84#[derive(Clone)]
85pub struct KeyServer {
86    client: reqwest::Client,
87    /// The original URL given to the constructor.
88    url: Url,
89    /// The URL we use for the requests.
90    request_url: Url,
91}
92
93assert_send_and_sync!(KeyServer);
94
95impl Default for KeyServer {
96    fn default() -> Self {
97	Self::new("hkps://keys.openpgp.org/").unwrap()
98    }
99}
100
101impl KeyServer {
102    /// Returns a handle for the given URL.
103    pub fn new(url: &str) -> Result<Self> {
104	Self::with_client(url, reqwest::Client::new())
105    }
106
107    /// Returns a handle for the given URL with a custom `Client`.
108    pub fn with_client(url: &str, client: reqwest::Client) -> Result<Self> {
109        let url = reqwest::Url::parse(url)?;
110
111        let s = url.scheme();
112        match s {
113            "hkp" => (),
114            "hkps" => (),
115            _ => return Err(Error::MalformedUrl.into()),
116        }
117
118        let request_url =
119            format!("{}://{}:{}",
120                    match s {"hkp" => "http", "hkps" => "https",
121                             _ => unreachable!()},
122                    url.host().ok_or(Error::MalformedUrl)?,
123                    match s {
124                        "hkp" => url.port().or(Some(11371)),
125                        "hkps" => url.port().or(Some(443)),
126                        _ => unreachable!(),
127                    }.unwrap()).parse()?;
128
129        Ok(KeyServer { client, url, request_url })
130    }
131
132    /// Returns the keyserver's base URL.
133    pub fn url(&self) -> &reqwest::Url {
134        &self.url
135    }
136
137    /// Retrieves the certificate with the given handle.
138    ///
139    /// # Warning
140    ///
141    /// Returned certificates must be mistrusted, and be carefully
142    /// interpreted under a policy and trust model.
143    pub async fn get<H: Into<KeyHandle>>(&self, handle: H)
144                                         -> Result<Vec<Result<Cert>>>
145    {
146        let handle = handle.into();
147        let url = self.request_url.join(
148            &format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?;
149
150        let res = self.client.get(url).send().await?;
151        match res.status() {
152            StatusCode::OK => {
153                let body = res.bytes().await?;
154                let certs = CertParser::from_bytes(&body)?.collect();
155                Ok(certs)
156            }
157            StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
158            n => Err(Error::HttpStatus(n).into()),
159        }
160    }
161
162    /// Retrieves certificates containing the given `UserID`.
163    ///
164    /// If the given [`UserID`] does not follow the de facto
165    /// conventions for userids, or it does not contain a email
166    /// address, an error is returned.
167    ///
168    ///   [`UserID`]: sequoia_openpgp::packet::UserID
169    ///
170    /// # Warning
171    ///
172    /// Returned certificates must be mistrusted, and be carefully
173    /// interpreted under a policy and trust model.
174    pub async fn search<U: Into<UserID>>(&self, userid: U)
175                                         -> Result<Vec<Result<Cert>>>
176    {
177        let userid = userid.into();
178        let email = userid.email().and_then(|addr| addr.ok_or_else(||
179            openpgp::Error::InvalidArgument(
180                "UserID does not contain an email address".into()).into()))?;
181        let url = self.request_url.join(
182            &format!("pks/lookup?op=get&options=mr&search={}",
183                percent_encode(email.as_bytes(), KEYSERVER_GET_ENCODE_SET)
184            ))?;
185
186        let res = self.client.get(url).send().await?;
187        match res.status() {
188            StatusCode::OK => {
189                Ok(CertParser::from_bytes(&res.bytes().await?)?.collect())
190            },
191            StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
192            n => Err(Error::HttpStatus(n).into()),
193        }
194    }
195
196    /// Sends the given key to the server.
197    pub async fn send(&self, key: &Cert) -> Result<()> {
198        use sequoia_openpgp::armor::{Writer, Kind};
199
200        let url = self.request_url.join("pks/add")?;
201        let mut w =  Writer::new(Vec::new(), Kind::PublicKey)?;
202        key.serialize(&mut w)?;
203
204        let armored_blob = w.finalize()?;
205
206        // Prepare to send url-encoded data.
207        let mut post_data = b"keytext=".to_vec();
208        post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET)
209                                    .collect::<String>().as_bytes());
210        let length = post_data.len();
211
212        let res = self.client.post(url)
213            .header("content-type", "application/x-www-form-urlencoded")
214            .header("content-length", length.to_string())
215            .body(post_data).send().await?;
216
217        match res.status() {
218            StatusCode::OK => Ok(()),
219            StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()),
220            n => Err(Error::HttpStatus(n).into()),
221        }
222    }
223}
224
225/// Results for sequoia-net.
226pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
227
228#[derive(thiserror::Error, Debug)]
229/// Errors returned from the network routines.
230#[non_exhaustive]
231pub enum Error {
232    /// A requested cert was not found.
233    #[error("Cert not found")]
234    NotFound,
235    /// A given keyserver URL was malformed.
236    #[error("Malformed URL; expected hkp: or hkps:")]
237    MalformedUrl,
238    /// The server provided malformed data.
239    #[error("Malformed response from server")]
240    MalformedResponse,
241    /// A communication partner violated the protocol.
242    #[error("Protocol violation")]
243    ProtocolViolation,
244    /// Encountered an unexpected low-level http status.
245    #[error("server returned status {0}")]
246    HttpStatus(hyper::StatusCode),
247    /// A `hyper::error::UrlError` occurred.
248    #[error(transparent)]
249    UrlError(#[from] url::ParseError),
250    /// A `http::Error` occurred.
251    #[error(transparent)]
252    HttpError(#[from] http::Error),
253    /// A `hyper::Error` occurred.
254    #[error(transparent)]
255    HyperError(#[from] hyper::Error),
256
257    /// wkd errors:
258    /// An email address is malformed
259    #[error("Malformed email address {0}")]
260    MalformedEmail(String),
261
262    /// An email address was not found in Cert userids.
263    #[error("Email address {0} not found in Cert's userids")]
264    EmailNotInUserids(String),
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270
271    #[test]
272    fn urls() {
273        assert!(KeyServer::new("keys.openpgp.org").is_err());
274        assert!(KeyServer::new("hkp://keys.openpgp.org").is_ok());
275        assert!(KeyServer::new("hkps://keys.openpgp.org").is_ok());
276    }
277
278    #[test]
279    fn encoding() {
280        assert_eq!(
281            percent_encode(b"alice@example.com",
282                           KEYSERVER_GET_ENCODE_SET)
283                .collect::<String>(),
284            "alice@example.com"
285        );
286        assert_eq!(
287            percent_encode(b"alice+tag@example.com",
288                           KEYSERVER_GET_ENCODE_SET)
289                .collect::<String>(),
290            "alice%2Btag@example.com"
291        );
292        assert_eq!(
293            percent_encode(b"alice+tag&role=admin%@example.com",
294                           KEYSERVER_GET_ENCODE_SET)
295                .collect::<String>(),
296            "alice%2Btag%26role%3Dadmin%25@example.com"
297        );
298
299    }
300}