use std::time::Duration;
use base64::{Engine as _, engine::general_purpose};
use super::email::EmailAddress;
use sequoia_openpgp::{
self as openpgp,
fmt,
Cert,
Packet,
parse::Parse,
serialize::SerializeInto,
types::{HashAlgorithm, RevocationStatus},
cert::prelude::*,
};
use super::Result;
use hickory_client::rr::{RData, RecordType};
use hickory_resolver::config::ResolverOpts;
use hickory_resolver::TokioAsyncResolver;
fn generate_fqdn(local: &str, domain: &str) -> Result<String> {
let mut ctx = HashAlgorithm::SHA256.context()?;
ctx.update(local.as_bytes());
let mut digest = vec![0; ctx.digest_size()];
ctx.digest(&mut digest)?;
Ok(format!(
"{}._openpgpkey.{}",
fmt::hex::encode(&digest[..28]),
domain
))
}
async fn get_raw(email_address: impl AsRef<str>) -> Result<Vec<Vec<u8>>> {
let email_address = EmailAddress::from(email_address)?;
let fqdn = generate_fqdn(&email_address.local_part, &email_address.domain)?;
let mut opts = ResolverOpts::default();
opts.validate = true;
let resolver = TokioAsyncResolver::tokio(Default::default(), opts);
let answers = resolver
.lookup(fqdn, RecordType::OPENPGPKEY)
.await
.map_err(Error::NotFound)?;
let mut bytes = vec![];
for record in answers.iter() {
if let RData::OPENPGPKEY(key) = record {
bytes.push(key.public_key().into());
}
}
Ok(bytes)
}
pub async fn get(email_address: impl AsRef<str>) -> Result<Vec<Result<Cert>>> {
let mut certs = vec![];
for bytes in get_raw(email_address).await?.iter() {
certs.push(Cert::from_bytes(bytes));
}
Ok(certs)
}
pub fn generate<'a, F, T, L>(cert: &ValidCert<'a>, fqdn: F, ttl: T,
size_target: L)
-> Result<Vec<String>>
where
F: AsRef<str>,
T: Into<Option<Duration>>,
L: Into<Option<usize>>,
{
generate_(cert, fqdn.as_ref(), ttl.into(), size_target.into(), false)
}
pub fn generate_generic<'a, F, T, L>(cert: &ValidCert<'a>, fqdn: F, ttl: T,
size_target: L)
-> Result<Vec<String>>
where
F: AsRef<str>,
T: Into<Option<Duration>>,
L: Into<Option<usize>>,
{
generate_(cert, fqdn.as_ref(), ttl.into(), size_target.into(), true)
}
fn generate_<'a>(cert: &ValidCert<'a>, fqdn: &str, ttl: Option<Duration>,
size_target: Option<usize>, generic: bool)
-> Result<Vec<String>>
{
let ttl = ttl.unwrap_or(Duration::new(3 * 60 * 60, 0));
let size_target = size_target.unwrap_or(16384 / 4 * 3);
let policy = cert.policy();
let time = cert.time();
let mut addresses: Vec<_> =
cert.userids().filter_map(|uidb| {
uidb.userid().email2().unwrap_or(None)
.and_then(|e| EmailAddress::from(e).ok())
.filter(|e| e.domain == fqdn)
})
.collect();
addresses.sort();
addresses.dedup();
if addresses.is_empty() {
return Err(openpgp::Error::InvalidArgument(
format!("Cert {} does not have a User ID in {}", cert, fqdn)
).into());
}
let mut records = Vec::new();
for email in addresses.into_iter() {
let mut cert = cert.cert().clone()
.retain_userids(
|u| u.email2().unwrap_or(None)
.and_then(|e| EmailAddress::from(e).ok())
.as_ref() == Some(&email))
.retain_user_attributes(|_| false);
if cert.serialized_len() > size_target {
cert = cert.retain_subkeys(
|s| s.with_policy(policy, time)
.map(|s| s.alive().is_ok()).unwrap_or(false));
}
if cert.serialized_len() > size_target {
let mut acc: Vec<Packet> = Vec::new();
let vcert = cert.with_policy(policy, time)?;
acc.push(vcert.primary_key().key().clone().into());
vcert.primary_key().signatures()
.for_each(|s| acc.push(s.clone().into()));
for uidb in vcert.userids() {
acc.push(uidb.userid().clone().into());
match uidb.revocation_status() {
| RevocationStatus::Revoked(revs)
| RevocationStatus::CouldBe(revs) => {
revs.iter()
.for_each(|&s| acc.push(s.clone().into()));
},
RevocationStatus::NotAsFarAsWeKnow => {
uidb.signatures()
.for_each(|s| acc.push(s.clone().into()));
},
}
}
for skb in vcert.keys().subkeys() {
acc.push(skb.key().clone().into());
match skb.revocation_status() {
| RevocationStatus::Revoked(revs)
| RevocationStatus::CouldBe(revs) => {
revs.iter()
.for_each(|&s| acc.push(s.clone().into()));
},
RevocationStatus::NotAsFarAsWeKnow => {
skb.signatures()
.for_each(|s| acc.push(s.clone().into()));
},
}
}
cert = Cert::from_packets(acc.into_iter())?;
}
if cert.serialized_len() > size_target {
let mut acc: Vec<Packet> = Vec::new();
let vcert = cert.with_policy(policy, time)?;
acc.push(vcert.primary_key().key().clone().into());
acc.push(vcert.primary_key().binding_signature().clone().into());
vcert.primary_key().self_revocations()
.chain(vcert.primary_key().other_revocations())
.chain(vcert.primary_key().certifications())
.for_each(|s| acc.push(s.clone().into()));
for uidb in vcert.userids() {
acc.push(uidb.userid().clone().into());
acc.push(uidb.binding_signature().clone().into());
uidb.self_revocations()
.chain(uidb.other_revocations())
.chain(uidb.certifications())
.for_each(|s| acc.push(s.clone().into()));
}
for skb in vcert.keys().subkeys() {
acc.push(skb.key().clone().into());
acc.push(skb.binding_signature().clone().into());
skb.self_revocations()
.chain(skb.other_revocations())
.chain(skb.certifications())
.for_each(|s| acc.push(s.clone().into()));
}
cert = Cert::from_packets(acc.into_iter())?;
}
if cert.serialized_len() > size_target {
let mut acc: Vec<Packet> = Vec::new();
let vcert = cert.with_policy(policy, time)?;
acc.push(vcert.primary_key().key().clone().into());
acc.push(vcert.primary_key().binding_signature().clone().into());
vcert.primary_key().self_revocations()
.chain(vcert.primary_key().other_revocations())
.for_each(|s| acc.push(s.clone().into()));
for uidb in vcert.userids() {
acc.push(uidb.userid().clone().into());
acc.push(uidb.binding_signature().clone().into());
uidb.self_revocations()
.chain(uidb.other_revocations())
.for_each(|s| acc.push(s.clone().into()));
}
for skb in vcert.keys().subkeys() {
acc.push(skb.key().clone().into());
acc.push(skb.binding_signature().clone().into());
skb.self_revocations()
.chain(skb.other_revocations())
.for_each(|s| acc.push(s.clone().into()));
}
cert = Cert::from_packets(acc.into_iter())?;
}
let bin = cert.to_vec()?;
if generic {
records.push(format!(
"; {} => {}\n{}. {} IN TYPE61 \\# {} {}",
email,
cert.fingerprint(),
generate_fqdn(&email.local_part, fqdn)?,
ttl.as_secs(),
bin.len(),
openpgp::fmt::hex::encode(&bin)));
} else {
records.push(format!(
"; {} => {}\n{}. {} IN OPENPGPKEY {}",
email,
cert.fingerprint(),
generate_fqdn(&email.local_part, fqdn)?,
ttl.as_secs(),
general_purpose::STANDARD.encode(&bin)));
}
}
Ok(records)
}
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
#[error("Cert not found")]
NotFound(#[from] hickory_resolver::error::ResolveError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generating_fqdn() {
assert_eq!(
generate_fqdn("dkg", "debian.org").unwrap(),
"A47CB586A51ACB93ACB9EF806F35F29131548E59E2FACD58CF6232E3._openpgpkey.debian.org"
);
}
#[test]
fn test_generating_fqdn_lower_case() {
assert_eq!(
generate_fqdn("DKG", "DEBIAN.ORG").unwrap(),
"46DE800073B375157AD8F4371E2713E118E3128FB1B4321ACE452F95._openpgpkey.DEBIAN.ORG"
);
}
#[test]
fn test_generate() -> Result<()> {
let p = openpgp::policy::StandardPolicy::new();
let (cert, _) = openpgp::cert::CertBuilder::new()
.add_userid("dkg <dkg@debian.org>")
.add_userid("dkg <dkg@somethingsomethinghorsesomething.org>")
.add_transport_encryption_subkey()
.generate()?;
let vcert = cert.with_policy(&p, None)?;
let records = generate(&vcert, "debian.org", None, None)?;
assert_eq!(records.len(), 1);
let record = &records[0];
eprintln!("{}", record);
assert!(record.starts_with(&format!(
"; dkg@debian.org => {}\n\
A47CB586A51ACB93ACB9EF806F35F29131548E59E2FACD58CF6232E3\
._openpgpkey.debian.org. 10800 IN OPENPGPKEY ",
cert.fingerprint())));
let asc = record.split(' ').last().unwrap();
let bin = general_purpose::STANDARD.decode(&asc)?;
let c = Cert::from_bytes(&bin)?;
assert_eq!(c.userids().count(), 1);
Ok(())
}
#[test]
fn test_generate_aliasing() -> Result<()> {
let p = openpgp::policy::StandardPolicy::new();
let (cert, _) = openpgp::cert::CertBuilder::new()
.add_userid("dkg")
.add_userid("dkg <dkg@debian.org>")
.add_userid("<dkg@debian.org>")
.add_userid("dkg@debian.org")
.add_userid("dkg <dkg@somethingsomethinghorsesomething.org>")
.add_transport_encryption_subkey()
.generate()?;
let vcert = cert.with_policy(&p, None)?;
let records = generate(&vcert, "debian.org", None, None)?;
assert_eq!(records.len(), 1);
let record = &records[0];
eprintln!("{}", record);
assert!(record.starts_with(&format!(
"; dkg@debian.org => {}\n\
A47CB586A51ACB93ACB9EF806F35F29131548E59E2FACD58CF6232E3\
._openpgpkey.debian.org. 10800 IN OPENPGPKEY ",
cert.fingerprint())));
let asc = record.split(' ').last().unwrap();
let bin = general_purpose::STANDARD.decode(&asc)?;
let c = Cert::from_bytes(&bin)?;
assert_eq!(c.userids().count(), 3);
Ok(())
}
#[test]
fn test_generate_disjoint() -> Result<()> {
let p = openpgp::policy::StandardPolicy::new();
let (cert, _) = openpgp::cert::CertBuilder::new()
.add_userid("dkg")
.add_userid("dkg <dkg@debian.org>")
.add_userid("dkg <evildkg@debian.org>")
.add_userid("dkg <dkg@somethingsomethinghorsesomething.org>")
.add_transport_encryption_subkey()
.generate()?;
let vcert = cert.with_policy(&p, None)?;
let records = generate(&vcert, "debian.org", None, None)?;
assert_eq!(records.len(), 2);
for record in records {
eprintln!("{}", record);
let asc = record.split(' ').last().unwrap();
let bin = general_purpose::STANDARD.decode(&asc)?;
let c = Cert::from_bytes(&bin)?;
assert_eq!(c.userids().count(), 1);
}
Ok(())
}
#[test]
fn test_generate_generic() -> Result<()> {
let p = openpgp::policy::StandardPolicy::new();
let (cert, _) = openpgp::cert::CertBuilder::new()
.add_userid("dkg <dkg@debian.org>")
.add_transport_encryption_subkey()
.generate()?;
let vcert = cert.with_policy(&p, None)?;
let records =
generate_generic(&vcert, "debian.org", Duration::new(300, 0), None)?;
assert_eq!(records.len(), 1);
let record = &records[0];
eprintln!("{}", record);
assert!(record.starts_with(&format!(
"; dkg@debian.org => {}\n\
A47CB586A51ACB93ACB9EF806F35F29131548E59E2FACD58CF6232E3\
._openpgpkey.debian.org. 300 IN TYPE61 \\# {} ",
cert.fingerprint(),
cert.serialized_len())));
let asc = record.split(' ').last().unwrap();
let bin = openpgp::fmt::hex::decode(&asc)?;
let c = Cert::from_bytes(&bin)?;
assert_eq!(c.userids().count(), 1);
Ok(())
}
}