summaryrefslogtreecommitdiffstats
path: root/net
diff options
context:
space:
mode:
authorWiktor Kwapisiewicz <wiktor@metacode.biz>2022-07-11 10:42:51 +0200
committerWiktor Kwapisiewicz <wiktor@metacode.biz>2022-09-15 09:45:39 +0200
commit9cee22c8dbcd55f1b7e41668e9feb35bd1a250c3 (patch)
tree6c1def18c2d7866a9042e1f307b7967543dc4e26 /net
parente0e5174262ac3203eb378677f063abdc8d98d187 (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')
-rw-r--r--net/Cargo.toml3
-rw-r--r--net/src/dane.rs135
-rw-r--r--net/src/email.rs46
-rw-r--r--net/src/lib.rs2
-rw-r--r--net/src/wkd.rs46
5 files changed, 186 insertions, 46 deletions
diff --git a/net/Cargo.toml b/net/Cargo.toml
index 7dab5b3b..96ad9d77 100644
--- a/net/Cargo.toml
+++ b/net/Cargo.toml
@@ -37,6 +37,8 @@ url = "2.1"
zbase32 = "0.1.2"
tokio = { version = "1.13.1", features = [ "macros" ] }
base64 = ">=0.12"
+trust-dns-client = "0.22"
+trust-dns-resolver = { version = "0.22", features = ["dnssec-ring"] }
[dev-dependencies]
rand = { version = "0.8", default-features = false, features = [ "getrandom" ] }
@@ -52,4 +54,3 @@ crypto-cng = ["sequoia-openpgp/crypto-cng"]
compression = ["sequoia-openpgp/compression"]
compression-deflate = ["sequoia-openpgp/compression-deflate"]
compression-bzip2 = ["sequoia-openpgp/compression-bzip2"]
-
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