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 | |
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.
-rw-r--r-- | Cargo.lock | 230 | ||||
-rw-r--r-- | net/Cargo.toml | 3 | ||||
-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 |
6 files changed, 410 insertions, 52 deletions
@@ -96,6 +96,17 @@ dependencies = [ ] [[package]] +name = "async-trait" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "atty" version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -682,6 +693,12 @@ dependencies = [ ] [[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] name = "dbl" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -879,6 +896,24 @@ dependencies = [ ] [[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] name = "env_logger" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1319,6 +1354,17 @@ dependencies = [ ] [[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] name = "http" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1469,6 +1515,24 @@ dependencies = [ ] [[package]] +name = "ipconfig" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723519edce41262b05d4143ceb95050e4c614f483e78e9fd9e39a8275a84ad98" +dependencies = [ + "socket2", + "widestring", + "winapi", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + +[[package]] name = "itertools" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1613,12 +1677,27 @@ dependencies = [ ] [[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] name = "matchers" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1748,6 +1827,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] name = "no-std-compat" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2203,11 +2291,11 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.37" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec757218438d5fda206afc041538b2f6d889286160d649a86a24d37e1235afd1" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] @@ -2253,6 +2341,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" [[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] name = "rand" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2416,6 +2514,31 @@ dependencies = [ ] [[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + +[[package]] name = "ripemd160" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2621,6 +2744,8 @@ dependencies = [ "tempfile", "thiserror", "tokio", + "trust-dns-client", + "trust-dns-resolver", "url", "zbase32", ] @@ -3041,13 +3166,13 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.91" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b683b2b825c8eef438b77c36a06dc262294da3d5a5813fac20da149241dcd44d" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", - "unicode-xid", + "unicode-ident", ] [[package]] @@ -3402,6 +3527,72 @@ dependencies = [ ] [[package]] +name = "trust-dns-client" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c408c32e6a9dbb38037cece35740f2cf23c875d8ca134d33631cec83f74d3fe" +dependencies = [ + "cfg-if", + "data-encoding", + "futures-channel", + "futures-util", + "lazy_static", + "radix_trie", + "rand 0.8.5", + "thiserror", + "time 0.3.9", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "lazy_static", + "rand 0.8.5", + "ring", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + +[[package]] name = "try-lock" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3502,6 +3693,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] name = "unicode-linebreak" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3532,6 +3729,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" [[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] name = "url" version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3680,6 +3883,12 @@ dependencies = [ ] [[package]] +name = "widestring" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983" + +[[package]] name = "win-crypto-ng" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -3767,6 +3976,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d19538ccc21819d01deaf88d6a17eae6596a12e9aafdbb97916fb49896d89de9" [[package]] +name = "winreg" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" +dependencies = [ + "winapi", +] + +[[package]] name = "wyz" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" 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 |