1#![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
41pub 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
66const KEYSERVER_ENCODE_SET: &AsciiSet =
68 &CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`')
70 .add(b'?').add(b'{').add(b'}')
71 .add(b'-').add(b'+').add(b'/');
74const KEYSERVER_GET_ENCODE_SET: &AsciiSet = &KEYSERVER_ENCODE_SET
78 .add(b'&').add(b'=')
80 .add(b'%');
82
83#[derive(Clone)]
85pub struct KeyServer {
86 client: reqwest::Client,
87 url: Url,
89 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 pub fn new(url: &str) -> Result<Self> {
104 Self::with_client(url, reqwest::Client::new())
105 }
106
107 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 pub fn url(&self) -> &reqwest::Url {
134 &self.url
135 }
136
137 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 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 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 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
225pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
227
228#[derive(thiserror::Error, Debug)]
229#[non_exhaustive]
231pub enum Error {
232 #[error("Cert not found")]
234 NotFound,
235 #[error("Malformed URL; expected hkp: or hkps:")]
237 MalformedUrl,
238 #[error("Malformed response from server")]
240 MalformedResponse,
241 #[error("Protocol violation")]
243 ProtocolViolation,
244 #[error("server returned status {0}")]
246 HttpStatus(hyper::StatusCode),
247 #[error(transparent)]
249 UrlError(#[from] url::ParseError),
250 #[error(transparent)]
252 HttpError(#[from] http::Error),
253 #[error(transparent)]
255 HyperError(#[from] hyper::Error),
256
257 #[error("Malformed email address {0}")]
260 MalformedEmail(String),
261
262 #[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}