#![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)]
use hyper::client::{ResponseFuture, HttpConnector};
use hyper::header::{CONTENT_LENGTH, CONTENT_TYPE, HeaderValue};
use hyper::{Client, Body, StatusCode, Request};
use hyper_tls::HttpsConnector;
use native_tls::{Certificate, TlsConnector};
use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use std::convert::{From, TryFrom};
use std::fmt;
use std::io::Cursor;
use url::Url;
use sequoia_openpgp as openpgp;
use openpgp::{
armor,
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 =
&CONTROLS.add(b' ').add(b'"').add(b'#').add(b'<').add(b'>').add(b'`')
.add(b'?').add(b'{').add(b'}')
.add(b'-').add(b'+').add(b'/');
#[derive(PartialEq, PartialOrd, Debug, Copy, Clone)]
pub enum Policy {
Offline,
Anonymized,
Encrypted,
Insecure,
}
impl fmt::Display for Policy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", match self {
Policy::Offline => "Offline",
Policy::Anonymized => "Anonymized",
Policy::Encrypted => "Encrypted",
Policy::Insecure => "Insecure",
})
}
}
impl Policy {
pub fn assert(&self, action: Policy) -> Result<()> {
if action > *self {
Err(Error::PolicyViolation(action).into())
} else {
Ok(())
}
}
}
impl<'a> From<&'a Policy> for u8 {
fn from(policy: &Policy) -> Self {
match policy {
Policy::Offline => 0,
Policy::Anonymized => 1,
Policy::Encrypted => 2,
Policy::Insecure => 3,
}
}
}
impl TryFrom<u8> for Policy {
type Error = TryFromU8Error;
fn try_from(policy: u8) -> std::result::Result<Self, Self::Error> {
match policy {
0 => Ok(Policy::Offline),
1 => Ok(Policy::Anonymized),
2 => Ok(Policy::Encrypted),
3 => Ok(Policy::Insecure),
n => Err(TryFromU8Error(n)),
}
}
}
#[derive(Debug)]
pub struct TryFromU8Error(u8);
impl fmt::Display for TryFromU8Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Bad network policy: {}", self.0)
}
}
impl std::error::Error for TryFromU8Error {}
pub struct KeyServer {
client: Box<dyn AClient>,
uri: Url,
}
assert_send_and_sync!(KeyServer);
impl KeyServer {
pub fn new(p: Policy, uri: &str) -> Result<Self> {
let uri: Url = uri.parse()
.or_else(|_| format!("hkps://{}", uri).parse())?;
let client: Box<dyn AClient> = match uri.scheme() {
"hkp" => Box::new(Client::new()),
"hkps" => {
Box::new(Client::builder()
.build(HttpsConnector::new()))
},
_ => return Err(Error::MalformedUri.into()),
};
Self::make(p, client, uri)
}
pub fn with_cert(p: Policy, uri: &str, cert: Certificate)
-> Result<Self> {
let uri: Url = uri.parse()?;
let client: Box<dyn AClient> = {
let mut tls = TlsConnector::builder();
tls.add_root_certificate(cert);
let tls = tls.build()?;
let mut http = HttpConnector::new();
http.enforce_http(false);
Box::new(Client::builder()
.build(HttpsConnector::from((http, tls.into()))))
};
Self::make(p, client, uri)
}
pub fn keys_openpgp_org(p: Policy) -> Result<Self> {
Self::new(p, "hkps://keys.openpgp.org")
}
fn make(p: Policy, client: Box<dyn AClient>, uri: Url) -> Result<Self> {
let s = uri.scheme();
match s {
"hkp" => p.assert(Policy::Insecure),
"hkps" => p.assert(Policy::Encrypted),
_ => return Err(Error::MalformedUri.into())
}?;
let uri =
format!("{}://{}:{}",
match s {"hkp" => "http", "hkps" => "https",
_ => unreachable!()},
uri.host().ok_or(Error::MalformedUri)?,
match s {
"hkp" => uri.port().or(Some(11371)),
"hkps" => uri.port().or(Some(443)),
_ => unreachable!(),
}.unwrap()).parse()?;
Ok(KeyServer{client, uri})
}
pub async fn get<H: Into<KeyHandle>>(&mut self, handle: H)
-> Result<Cert>
{
let handle = handle.into();
let want_handle = handle.clone();
let uri = self.uri.join(
&format!("pks/lookup?op=get&options=mr&search=0x{:X}", handle))?;
let res = self.client.do_get(uri).await?;
match res.status() {
StatusCode::OK => {
let body = hyper::body::to_bytes(res.into_body()).await?;
let r = armor::Reader::from_reader(
Cursor::new(body),
armor::ReaderMode::Tolerant(Some(armor::Kind::PublicKey)),
);
let cert = Cert::from_reader(r)?;
if cert.keys().any(|ka| ka.key_handle().aliases(&want_handle)) {
Ok(cert)
} else {
Err(Error::MismatchedKeyHandle(want_handle, cert).into())
}
}
StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
#[allow(clippy::blocks_in_if_conditions)]
pub async fn search<U: Into<UserID>>(&mut self, userid: U)
-> Result<Vec<Cert>>
{
let userid = userid.into();
let email = userid.email().and_then(|addr| addr.ok_or_else(||
openpgp::Error::InvalidArgument(
"UserID does not contain an email address".into()).into()))?;
let uri = self.uri.join(
&format!("pks/lookup?op=get&options=mr&search={}", email))?;
let res = self.client.do_get(uri).await?;
match res.status() {
StatusCode::OK => {
let body = hyper::body::to_bytes(res.into_body()).await?;
let mut certs = Vec::new();
for certo in CertParser::from_bytes(&body)? {
let cert = certo?;
if cert.userids().any(|uid| {
uid.email().ok()
.and_then(|addro| addro)
.map(|addr| addr == email)
.unwrap_or(false)
}) {
certs.push(cert);
}
}
Ok(certs)
},
StatusCode::NOT_FOUND => Err(Error::NotFound.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
pub async fn send(&mut self, key: &Cert) -> Result<()> {
use sequoia_openpgp::armor::{Writer, Kind};
let uri = self.uri.join("pks/add")?;
let mut w = Writer::new(Vec::new(), Kind::PublicKey)?;
key.serialize(&mut w)?;
let armored_blob = w.finalize()?;
let mut post_data = b"keytext=".to_vec();
post_data.extend_from_slice(percent_encode(&armored_blob, KEYSERVER_ENCODE_SET)
.collect::<String>().as_bytes());
let length = post_data.len();
let mut request = Request::post(url2uri(uri)).body(Body::from(post_data))?;
request.headers_mut().insert(
CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"));
request.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(&format!("{}", length))
.expect("cannot fail: only ASCII characters"));
let res = self.client.do_request(request).await?;
match res.status() {
StatusCode::OK => Ok(()),
StatusCode::NOT_FOUND => Err(Error::ProtocolViolation.into()),
n => Err(Error::HttpStatus(n).into()),
}
}
}
trait AClient: Send + Sync {
fn do_get(&mut self, uri: Url) -> ResponseFuture;
fn do_request(&mut self, request: Request<Body>) -> ResponseFuture;
}
impl AClient for Client<HttpConnector> {
fn do_get(&mut self, uri: Url) -> ResponseFuture {
self.get(url2uri(uri))
}
fn do_request(&mut self, request: Request<Body>) -> ResponseFuture {
self.request(request)
}
}
impl AClient for Client<HttpsConnector<HttpConnector>> {
fn do_get(&mut self, uri: Url) -> ResponseFuture {
self.get(url2uri(uri))
}
fn do_request(&mut self, request: Request<Body>) -> ResponseFuture {
self.request(request)
}
}
pub(crate) fn url2uri(uri: Url) -> hyper::Uri {
format!("{}", uri).parse().unwrap()
}
pub type Result<T> = ::std::result::Result<T, anyhow::Error>;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Unmet network policy requirement: {0}")]
PolicyViolation(Policy),
#[error("Key not found")]
NotFound,
#[error("Mismatched key handle, expected {0}")]
MismatchedKeyHandle(KeyHandle, Cert),
#[error("Malformed URI; expected hkp: or hkps:")]
MalformedUri,
#[error("Malformed response from server")]
MalformedResponse,
#[error("Protocol violation")]
ProtocolViolation,
#[error("Error communicating with server")]
HttpStatus(hyper::StatusCode),
#[error("URI Error")]
UriError(#[from] url::ParseError),
#[error("http Error")]
HttpError(#[from] http::Error),
#[error("Hyper Error")]
HyperError(#[from] hyper::Error),
#[error("TLS Error")]
TlsError(native_tls::Error),
#[error("Malformed email address {0}")]
MalformedEmail(String),
#[error("Email address {0} not found in Cert's userids")]
EmailNotInUserids(String),
}
#[cfg(test)]
mod tests {
use super::*;
fn ok(policy: Policy, required: Policy) {
assert!(policy.assert(required).is_ok());
}
fn fail(policy: Policy, required: Policy) {
assert!(matches!(
policy.assert(required)
.err().unwrap().downcast::<Error>().unwrap(),
Error::PolicyViolation(_)));
}
#[test]
fn offline() {
let p = Policy::Offline;
ok(p, Policy::Offline);
fail(p, Policy::Anonymized);
fail(p, Policy::Encrypted);
fail(p, Policy::Insecure);
}
#[test]
fn anonymized() {
let p = Policy::Anonymized;
ok(p, Policy::Offline);
ok(p, Policy::Anonymized);
fail(p, Policy::Encrypted);
fail(p, Policy::Insecure);
}
#[test]
fn encrypted() {
let p = Policy::Encrypted;
ok(p, Policy::Offline);
ok(p, Policy::Anonymized);
ok(p, Policy::Encrypted);
fail(p, Policy::Insecure);
}
#[test]
fn insecure() {
let p = Policy::Insecure;
ok(p, Policy::Offline);
ok(p, Policy::Anonymized);
ok(p, Policy::Encrypted);
ok(p, Policy::Insecure);
}
#[test]
fn uris() {
let p = Policy::Insecure;
assert!(KeyServer::new(p, "keys.openpgp.org").is_ok());
assert!(KeyServer::new(p, "hkp://keys.openpgp.org").is_ok());
assert!(KeyServer::new(p, "hkps://keys.openpgp.org").is_ok());
let p = Policy::Encrypted;
assert!(KeyServer::new(p, "keys.openpgp.org").is_ok());
assert!(KeyServer::new(p, "hkp://keys.openpgp.org").is_err());
assert!(KeyServer::new(p, "hkps://keys.openpgp.org").is_ok());
}
}