summaryrefslogtreecommitdiffstats
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
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.
-rw-r--r--Cargo.lock230
-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
6 files changed, 410 insertions, 52 deletions
diff --git a/Cargo.lock b/Cargo.lock
index db3913ff..ab90c42a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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