summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2023-11-16 14:29:09 +0100
committerJustus Winter <justus@sequoia-pgp.org>2023-11-16 14:43:21 +0100
commitfebcf473d87faecc4d2d598fa86065856ab9a07d (patch)
treeeb72967c1a81c6ab1ca8dbddd68ddfad66260a84
parentc2cf12bbb75653640ecd9d665f8956077e6a8a7e (diff)
net: Implement DANE record generation.
- Fixes #840.
-rw-r--r--net/src/dane.rs348
-rw-r--r--net/src/email.rs7
2 files changed, 354 insertions, 1 deletions
diff --git a/net/src/dane.rs b/net/src/dane.rs
index b245f954..f98d00f8 100644
--- a/net/src/dane.rs
+++ b/net/src/dane.rs
@@ -5,13 +5,20 @@
//!
//! [DANE]: https://datatracker.ietf.org/doc/html/rfc7929
+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,
- types::HashAlgorithm,
+ serialize::SerializeInto,
+ types::{HashAlgorithm, RevocationStatus},
cert::prelude::*,
};
@@ -111,6 +118,239 @@ pub async fn get(email_address: impl AsRef<str>) -> Result<Vec<Cert>> {
Ok(certs)
}
+/// Generates a [DANE] record for the given cert in the zonefile
+/// format.
+///
+/// Only user IDs with email addresses in `fqdn` are considered.
+///
+/// If `ttl` is `None`, records are cached for three hours.
+///
+/// We make an effort to shrink the certificate so that it fits within
+/// `size_target`. However, this is a best-effort mechanism, and we
+/// may emit larger resource records. If `size_target` is `None`, we
+/// try to shrink the certificates to 12k.
+///
+/// [DANE]: https://datatracker.ietf.org/doc/html/rfc7929
+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)
+}
+
+/// Generates a [DANE] record for the given cert in the zonefile
+/// format.
+///
+/// This is like [`generate`], but uses the generic syntax for servers
+/// that do not support the OPENPGPKEY RRtype.
+///
+/// Only user IDs with email addresses in `fqdn` are considered.
+///
+/// If `ttl` is `None`, records are cached for three hours.
+///
+/// We make an effort to shrink the certificate so that it fits within
+/// `size_target`. However, this is a best-effort mechanism, and we
+/// may emit larger resource records. If `size_target` is `None`, we
+/// try to shrink the certificates to 12k.
+///
+/// [DANE]: https://datatracker.ietf.org/doc/html/rfc7929
+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));
+ // This is somewhat arbitrary, but Gandi doesn't like the
+ // records being larger than 16k under base64 encoding.
+ let size_target = size_target.unwrap_or(16384 / 4 * 3);
+
+ let policy = cert.policy();
+ let time = cert.time();
+
+ // First, check which UserIDs are in `domain`.
+ 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();
+
+ // We want to emit one record per email-address, even if multiple
+ // user IDs map to that address.
+ addresses.sort();
+ addresses.dedup();
+
+ // Any?
+ 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() {
+ // Create a trimmed down view of cert, following the advice in
+ // RFC7929, Section 2.1.2. Reducing the Transferable Public
+ // Key Size.
+
+ // 1. & 2. Retain all user IDs matching the current email
+ // address, no user attribute.
+ 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);
+
+ // 3a. Keep only alive subkeys.
+ 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));
+ }
+
+ // 3b. For expired components, only keep the component and
+ // revocation signatures.
+ 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())?;
+ }
+
+ // 4. Only keep the current binding signatures.
+ 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())?;
+ }
+
+ // 5. Strip third-party certifications.
+ 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)
+}
+
#[cfg(test)]
mod tests {
use super::*;
@@ -132,4 +372,110 @@ mod tests {
"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(())
+ }
}
diff --git a/net/src/email.rs b/net/src/email.rs
index 60e26246..ded30555 100644
--- a/net/src/email.rs
+++ b/net/src/email.rs
@@ -1,13 +1,20 @@
//! Provides e-mail parsing functions.
+use std::fmt;
use super::{Result, Error};
/// Stores the local_part and domain of an email address.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct EmailAddress {
pub(crate) local_part: String,
pub(crate) domain: String,
}
+impl fmt::Display for EmailAddress {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}@{}", self.local_part, self.domain)
+ }
+}
impl EmailAddress {
/// Returns an EmailAddress from an email address string.