use std::collections::HashMap;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};
use anyhow::Context;
use futures_util::{FutureExt, future::{BoxFuture, TryFutureExt}};
use http::Response;
use hyper::{Body, Client, Uri};
use hyper_tls::HttpsConnector;
use openpgp::policy::StandardPolicy;
use sequoia_openpgp::{
self as openpgp,
Fingerprint,
Cert,
parse::Parse,
serialize::Serialize,
types::HashAlgorithm,
cert::prelude::*,
};
use super::{Result, Error};
use super::email::EmailAddress;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Variant {
Advanced,
Direct,
}
impl Default for Variant {
fn default() -> Self {
Variant::Advanced
}
}
#[derive(Debug, Clone)]
pub struct Url {
domain: String,
local_encoded: String,
local_part: String,
}
impl fmt::Display for Url {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.build(None))
}
}
impl Url {
pub fn from<S: AsRef<str>>(email_address: S) -> Result<Self> {
let email = EmailAddress::from(email_address)?;
let local_encoded = encode_local_part(&email.local_part.to_lowercase());
let url = Url {
domain : email.domain,
local_encoded,
local_part : email.local_part,
};
Ok(url)
}
pub fn build<V>(&self, variant: V) -> String
where V: Into<Option<Variant>>
{
let variant = variant.into().unwrap_or_default();
if variant == Variant::Direct {
format!("https://{}/.well-known/openpgpkey/hu/{}?l={}",
self.domain, self.local_encoded, self.local_part)
} else {
format!("https://openpgpkey.{}/.well-known/openpgpkey/{}/hu/{}\
?l={}", self.domain, self.domain, self.local_encoded,
self.local_part)
}
}
pub fn to_url<V>(&self, variant: V) -> Result<url::Url>
where V: Into<Option<Variant>> {
let url_string = self.build(variant);
let url_url = url::Url::parse(url_string.as_str())?;
Ok(url_url)
}
pub fn to_uri<V>(&self, variant: V) -> Result<Uri>
where V: Into<Option<Variant>> {
let url_string = self.build(variant);
let uri = url_string.as_str().parse::<Uri>()?;
Ok(uri)
}
pub fn to_file_path<V>(&self, variant: V) -> Result<PathBuf>
where V: Into<Option<Variant>>
{
let variant = variant.into().unwrap_or_default();
let url = self.to_url(variant)?;
Ok(PathBuf::from(url.path()).strip_prefix("/")?.into())
}
}
fn encode_local_part<S: AsRef<str>>(local_part: S) -> String {
let local_part = local_part.as_ref();
let mut digest = vec![0; 20];
let mut ctx = HashAlgorithm::SHA1.context().expect("must be implemented");
ctx.update(local_part.as_bytes());
let _ = ctx.digest(&mut digest);
zbase32::encode_full_bytes(&digest[..])
}
fn parse_body<S: AsRef<str>>(body: &[u8], email_address: S)
-> Result<Vec<Cert>> {
let email_address = email_address.as_ref();
let packets = CertParser::from_bytes(&body)?;
let certs: Vec<Cert> = packets.flatten().collect();
if certs.is_empty() {
return Err(Error::NotFound.into());
}
let valid_certs: Vec<Cert> = certs.iter()
.filter(|cert| {cert.userids()
.any(|uidb|
if let Ok(Some(a)) = uidb.userid().email() {
a == email_address
} else { false })
}).cloned().collect();
if valid_certs.is_empty() {
Err(Error::EmailNotInUserids(email_address.into()).into())
} else {
Ok(valid_certs)
}
}
fn get_following_redirects<T>(
client: &hyper::client::Client<T>,
url: Uri,
depth: i32,
) -> BoxFuture<Result<Response<Body>>>
where
T: hyper::client::connect::Connect + Clone + Send + Sync + 'static,
{
async move {
let response = client.get(url).await;
if depth < 0 {
return Err(anyhow::anyhow!("Too many redirects"));
}
if let Ok(ref resp) = response {
if resp.status().is_redirection() {
let url = resp.headers().get("Location")
.and_then(|value| value.to_str().ok())
.map(|value| value.parse::<Uri>());
if let Some(Ok(url)) = url {
return get_following_redirects(client, url, depth - 1).await;
}
}
}
response.map_err(|err| anyhow::anyhow!(err))
}
.boxed()
}
pub async fn get<S: AsRef<str>>(email_address: S) -> Result<Vec<Cert>> {
let email = email_address.as_ref().to_string();
let wkd_url = Url::from(&email)?;
let https = HttpsConnector::new();
let client = Client::builder().build::<_, hyper::Body>(https);
let advanced_uri = wkd_url.to_uri(Variant::Advanced)?;
let direct_uri = wkd_url.to_uri(Variant::Direct)?;
const REDIRECT_LIMIT: i32 = 10;
let res = get_following_redirects(&client, advanced_uri, REDIRECT_LIMIT)
.or_else(|_| get_following_redirects(&client, direct_uri, REDIRECT_LIMIT))
.await?;
let body = hyper::body::to_bytes(res.into_body()).await?;
parse_body(&body, &email)
}
fn get_cert_domains<'a>(domain: &'a str, cert: &ValidCert<'a>) -> impl Iterator<Item = Url> + 'a
{
cert.userids().filter_map(move |uidb| {
uidb.userid().email().unwrap_or(None).and_then(|addr| {
if EmailAddress::from(&addr).ok().map(|e| e.domain == domain)
.unwrap_or(false)
{
Url::from(&addr).ok()
} else {
None
}
})
})
}
pub fn cert_contains_domain_userid<S>(domain: S, cert: &ValidCert) -> bool
where S: AsRef<str>
{
get_cert_domains(domain.as_ref(), cert).next().is_some()
}
pub fn insert<P, S, V>(base_path: P, domain: S, variant: V,
cert: &Cert)
-> Result<()>
where P: AsRef<Path>,
S: AsRef<str>,
V: Into<Option<Variant>>
{
let base_path = base_path.as_ref();
let domain = domain.as_ref();
let variant = variant.into().unwrap_or_default();
let policy = &StandardPolicy::new();
let cert = cert.with_policy(policy, None)?;
let addresses = get_cert_domains(domain, &cert).collect::<Vec<_>>();
if addresses.is_empty() {
return Err(openpgp::Error::InvalidArgument(
format!("Key {} does not have a User ID in {}", cert, domain)
).into());
}
let mut well_known = None;
for address in addresses.into_iter() {
let path = base_path.join(address.to_file_path(variant)?);
fs::create_dir_all(path.parent().expect("by construction"))?;
let mut keyring = KeyRing::default();
if path.is_file() {
for t in CertParser::from_file(&path).context(
format!("Error parsing existing file {:?}", path))?
{
keyring.insert(t.context(
format!("Malformed Cert in existing {:?}", path))?)?;
}
}
keyring.insert(cert.cert().clone())?;
let mut file = fs::File::create(&path)?;
keyring.export(&mut file)?;
well_known = Some(path
.parent().expect("by construction")
.parent().expect("by construction")
.to_path_buf());
}
match std::fs::OpenOptions::new().write(true).create_new(true)
.open(well_known.expect("at least one address").join("policy"))
{
Err(ref e) if e.kind() == std::io::ErrorKind::AlreadyExists => (),
r => drop(r?),
}
Ok(())
}
#[derive(Default)]
struct KeyRing(HashMap<Fingerprint, Cert>);
impl KeyRing {
fn insert(&mut self, cert: Cert) -> Result<()> {
let fp = cert.fingerprint();
if let Some(existing) = self.0.get_mut(&fp) {
*existing = existing.clone().merge_public(cert)?;
} else {
self.0.insert(fp, cert);
}
Ok(())
}
fn export(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> {
for cert in self.0.values() {
cert.export(o)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use self::Variant::*;
#[test]
fn encode_local_part_succed() {
let encoded_part = encode_local_part("test1");
assert_eq!("stnkabub89rpcphiz4ppbxixkwyt1pic", encoded_part);
assert_eq!(32, encoded_part.len());
}
#[test]
fn email_address_from() {
let email_address = EmailAddress::from("test1@example.com").unwrap();
assert_eq!(email_address.domain, "example.com");
assert_eq!(email_address.local_part, "test1");
assert!(EmailAddress::from("thisisnotanemailaddress").is_err());
}
#[test]
fn url_roundtrip() {
let expected_url =
"https://openpgpkey.example.com/\
.well-known/openpgpkey/example.com/hu/\
stnkabub89rpcphiz4ppbxixkwyt1pic?l=test1";
let wkd_url = Url::from("test1@example.com").unwrap();
assert_eq!(expected_url, wkd_url.to_string());
assert_eq!(url::Url::parse(expected_url).unwrap(),
wkd_url.to_url(None).unwrap());
assert_eq!(expected_url.parse::<Uri>().unwrap(),
wkd_url.to_uri(None).unwrap());
let expected_url =
"https://example.com/\
.well-known/openpgpkey/hu/\
stnkabub89rpcphiz4ppbxixkwyt1pic?l=test1";
assert_eq!(expected_url, wkd_url.build(Direct));
assert_eq!(url::Url::parse(expected_url).unwrap(),
wkd_url.to_url(Direct).unwrap());
assert_eq!(expected_url.parse::<Uri>().unwrap(),
wkd_url.to_uri(Direct).unwrap());
}
#[test]
fn url_to_file_path() {
let expected_path =
".well-known/openpgpkey/example.com/hu/\
stnkabub89rpcphiz4ppbxixkwyt1pic";
let wkd_url = Url::from("test1@example.com").unwrap();
assert_eq!(expected_path,
wkd_url.to_file_path(None).unwrap().to_str().unwrap());
let expected_path =
".well-known/openpgpkey/hu/\
stnkabub89rpcphiz4ppbxixkwyt1pic";
assert_eq!(expected_path,
wkd_url.to_file_path(Direct).unwrap().to_str().unwrap());
}
#[test]
fn test_parse_body() {
let (cert, _) = CertBuilder::new()
.add_userid("test@example.example")
.generate()
.unwrap();
let mut buffer: Vec<u8> = Vec::new();
cert.serialize(&mut buffer).unwrap();
let valid_certs = parse_body(&buffer, "juga@sequoia-pgp.org");
assert!(valid_certs.is_err());
let (cert, _) = CertBuilder::new()
.add_userid("test@example.example")
.add_userid("juga@sequoia-pgp.org")
.generate()
.unwrap();
cert.serialize(&mut buffer).unwrap();
let valid_certs = parse_body(&buffer, "juga@sequoia-pgp.org");
assert!(valid_certs.is_ok());
assert!(valid_certs.unwrap().len() == 1);
}
#[test]
fn wkd_generate() {
let (cert, _) = CertBuilder::new()
.add_userid("test1@example.example")
.add_userid("juga@sequoia-pgp.org")
.generate()
.unwrap();
let (cert2, _) = CertBuilder::new()
.add_userid("justus@sequoia-pgp.org")
.generate()
.unwrap();
let dir = tempfile::tempdir().unwrap();
let dir_path = dir.path();
insert(&dir_path, "sequoia-pgp.org", None, &cert).unwrap();
insert(&dir_path, "sequoia-pgp.org", None, &cert2).unwrap();
let path = dir_path.join(
".well-known/openpgpkey/sequoia-pgp.org/hu\
/jwp7xjqkdujgz5op6bpsoypg34pnrgmq");
assert!(path.is_file());
let path = dir_path.join(
".well-known/openpgpkey/sequoia-pgp.org/hu\
/7t1uqk9cwh1955776rc4z1gqf388566j");
assert!(path.is_file());
let path = dir_path.join(
".well-known/openpgpkey/example.com/hu/\
stnkabub89rpcphiz4ppbxixkwyt1pic");
assert!(!path.is_file());
}
#[test]
fn test_get_cert_domains() -> Result<()> {
let (cert, _) = CertBuilder::new()
.add_userid("test1@example.example")
.add_userid("juga@sequoia-pgp.org")
.generate()
.unwrap();
let policy = &StandardPolicy::new();
let user_ids: Vec<_> = get_cert_domains("sequoia-pgp.org", &cert.with_policy(policy, None)?)
.map(|addr| addr.to_string())
.collect();
assert_eq!(user_ids, vec!["https://openpgpkey.sequoia-pgp.org/.well-known/openpgpkey/sequoia-pgp.org/hu/7t1uqk9cwh1955776rc4z1gqf388566j?l=juga"]);
let user_ids: Vec<_> = get_cert_domains("example.example", &cert.with_policy(policy, None)?)
.map(|addr| addr.to_string())
.collect();
assert_eq!(user_ids, vec!["https://openpgpkey.example.example/.well-known/openpgpkey/example.example/hu/stnkabub89rpcphiz4ppbxixkwyt1pic?l=test1"]);
Ok(())
}
#[test]
fn test_cert_contains_domain_userid() -> Result<()> {
let (cert, _) = CertBuilder::new()
.add_userid("test1@example.example")
.add_userid("juga@sequoia-pgp.org")
.generate()
.unwrap();
let policy = &StandardPolicy::new();
assert!(cert_contains_domain_userid("sequoia-pgp.org", &cert.with_policy(policy, None)?));
assert!(cert_contains_domain_userid("example.example", &cert.with_policy(policy, None)?));
assert!(!cert_contains_domain_userid("example.org", &cert.with_policy(policy, None)?));
Ok(())
}
}