diff options
author | Wiktor Kwapisiewicz <wiktor@metacode.biz> | 2022-07-11 10:42:51 +0200 |
---|---|---|
committer | Wiktor Kwapisiewicz <wiktor@metacode.biz> | 2022-09-15 09:45:39 +0200 |
commit | 9cee22c8dbcd55f1b7e41668e9feb35bd1a250c3 (patch) | |
tree | 6c1def18c2d7866a9042e1f307b7967543dc4e26 /net/src | |
parent | e0e5174262ac3203eb378677f063abdc8d98d187 (diff) |
net: Add support for DANE certificate retrieval.
- Add dane::get.
- Make EmailAddress functions pub(crate) to use them from the DANE
module.
- Add tests for generating correct FQDN.
- See #865.
Diffstat (limited to 'net/src')
-rw-r--r-- | net/src/dane.rs | 135 | ||||
-rw-r--r-- | net/src/email.rs | 46 | ||||
-rw-r--r-- | net/src/lib.rs | 2 | ||||
-rw-r--r-- | net/src/wkd.rs | 46 |
4 files changed, 184 insertions, 45 deletions
diff --git a/net/src/dane.rs b/net/src/dane.rs new file mode 100644 index 00000000..7725a581 --- /dev/null +++ b/net/src/dane.rs @@ -0,0 +1,135 @@ +//! DANE protocol client. +//! +//! [DANE] is a protocol for retrieving and storing OpenPGP +//! certificates in the DNS. +//! +//! [DANE]: https://datatracker.ietf.org/doc/html/rfc7929 + +use super::email::EmailAddress; + +use sequoia_openpgp::{ + fmt, + Cert, + parse::Parse, + types::HashAlgorithm, + cert::prelude::*, +}; + +use super::Result; + +use trust_dns_client::rr::{RData, RecordType}; +use trust_dns_resolver::config::ResolverOpts; +use trust_dns_resolver::TokioAsyncResolver; + +/// Generates a Fully Qualified Domain Name that holds the OPENPGPKEY +/// record for given `local` and `domain` parameters. +/// +/// See: <https://datatracker.ietf.org/doc/html/rfc7929> +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 + )) +} + +/// Retrieves raw values for `OPENPGPKEY` records for User IDs with a +/// given e-mail address using the [DANE] protocol. +/// +/// This function unconditionally validates DNSSEC records and returns +/// the found certificates only on validation success. +/// +/// [DANE]: https://datatracker.ietf.org/doc/html/rfc7929 +/// +/// # Examples +/// +/// ```no_run +/// # use sequoia_net::{Result, dane}; +/// # use sequoia_openpgp::Cert; +/// # async fn f() -> Result<()> { +/// let email_address = "john@example.com"; +/// let certs = dane::get_raw(email_address).await?; +/// # Ok(()) +/// # } +/// ``` +pub 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?; + + let mut bytes = vec![]; + + for record in answers.iter() { + if let RData::OPENPGPKEY(key) = record { + bytes.push(key.public_key().into()); + } + } + + Ok(bytes) +} + +/// Retrieves certificates that contain User IDs with a given e-mail +/// address using the [DANE] protocol. +/// +/// This function unconditionally validates DNSSEC records and returns +/// the found certificates only on validation success. +/// +/// [DANE]: https://datatracker.ietf.org/doc/html/rfc7929 +/// +/// # Examples +/// +/// ```no_run +/// # use sequoia_net::{Result, dane}; +/// # use sequoia_openpgp::Cert; +/// # async fn f() -> Result<()> { +/// let email_address = "john@example.com"; +/// let certs = dane::get(email_address).await?; +/// # Ok(()) +/// # } +/// ``` +pub async fn get(email_address: impl AsRef<str>) -> Result<Vec<Cert>> { + let mut certs = vec![]; + + for bytes in get_raw(email_address).await?.iter() { + certs.extend(CertParser::from_bytes(bytes)?.flatten()); + } + + Ok(certs) +} + +#[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() { + // Must NOT lowercase "DKG" into "dkg". + // See: https://datatracker.ietf.org/doc/html/rfc7929#section-4 + assert_eq!( + generate_fqdn("DKG", "DEBIAN.ORG").unwrap(), + "46DE800073B375157AD8F4371E2713E118E3128FB1B4321ACE452F95._openpgpkey.DEBIAN.ORG" + ); + } +} diff --git a/net/src/email.rs b/net/src/email.rs new file mode 100644 index 00000000..60e26246 --- /dev/null +++ b/net/src/email.rs @@ -0,0 +1,46 @@ +//! Provides e-mail parsing functions. + +use super::{Result, Error}; + +/// Stores the local_part and domain of an email address. +pub(crate) struct EmailAddress { + pub(crate) local_part: String, + pub(crate) domain: String, +} + + +impl EmailAddress { + /// Returns an EmailAddress from an email address string. + /// + /// From [draft-koch]: + /// + ///```text + /// To help with the common pattern of using capitalized names + /// (e.g. "Joe.Doe@example.org") for mail addresses, and under the + /// premise that almost all MTAs treat the local-part case-insensitive + /// and that the domain-part is required to be compared + /// case-insensitive anyway, all upper-case ASCII characters in a User + /// ID are mapped to lowercase. Non-ASCII characters are not changed. + ///``` + pub(crate) fn from<S: AsRef<str>>(email_address: S) -> Result<Self> { + // Ensure that is a valid email address by parsing it and return the + // errors that it returns. + // This is also done in hagrid. + let email_address = email_address.as_ref(); + let v: Vec<&str> = email_address.split('@').collect(); + if v.len() != 2 { + return Err(Error::MalformedEmail(email_address.into()).into()) + }; + + // Convert domain to lowercase without tailoring, i.e. without taking any + // locale into account. See: + // https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase + // + // Keep the local part as-is as we'll need that to generate WKD URLs. + let email = EmailAddress { + local_part: v[0].to_string(), + domain: v[1].to_lowercase() + }; + Ok(email) + } +} diff --git a/net/src/lib.rs b/net/src/lib.rs index 5d8ef078..14549413 100644 --- a/net/src/lib.rs +++ b/net/src/lib.rs @@ -61,6 +61,8 @@ use openpgp::{ }; #[macro_use] mod macros; +pub mod dane; +mod email; pub mod pks; pub mod updates; pub mod wkd; diff --git a/net/src/wkd.rs b/net/src/wkd.rs index 0289508d..7a81831e 100644 --- a/net/src/wkd.rs +++ b/net/src/wkd.rs @@ -38,6 +38,7 @@ use sequoia_openpgp::{ }; use super::{Result, Error}; +use super::email::EmailAddress; /// WKD variants. /// @@ -62,51 +63,6 @@ impl Default for Variant { } } - -/// Stores the local_part and domain of an email address. -pub struct EmailAddress { - local_part: String, - domain: String, -} - - -impl EmailAddress { - /// Returns an EmailAddress from an email address string. - /// - /// From [draft-koch]: - /// - ///```text - /// To help with the common pattern of using capitalized names - /// (e.g. "Joe.Doe@example.org") for mail addresses, and under the - /// premise that almost all MTAs treat the local-part case-insensitive - /// and that the domain-part is required to be compared - /// case-insensitive anyway, all upper-case ASCII characters in a User - /// ID are mapped to lowercase. Non-ASCII characters are not changed. - ///``` - fn from<S: AsRef<str>>(email_address: S) -> Result<Self> { - // Ensure that is a valid email address by parsing it and return the - // errors that it returns. - // This is also done in hagrid. - let email_address = email_address.as_ref(); - let v: Vec<&str> = email_address.split('@').collect(); - if v.len() != 2 { - return Err(Error::MalformedEmail(email_address.into()).into()) - }; - - // Convert domain to lowercase without tailoring, i.e. without taking any - // locale into account. See: - // https://doc.rust-lang.org/std/primitive.str.html#method.to_lowercase - // - // Keep the local part as-is as we'll need that to generate WKD URLs. - let email = EmailAddress { - local_part: v[0].to_string(), - domain: v[1].to_lowercase() - }; - Ok(email) - } -} - - /// Stores the parts needed to create a Web Key Directory URL. /// /// NOTE: This is a different `Url` than [`url::Url`] (`url` crate) that is |