Feature-gate validation and document crate features
authorMatt Corallo <git@bluematt.me>
Wed, 7 Feb 2024 04:25:32 +0000 (04:25 +0000)
committerMatt Corallo <git@bluematt.me>
Wed, 7 Feb 2024 05:23:28 +0000 (05:23 +0000)
Cargo.toml
src/http.rs
src/lib.rs
src/query.rs
src/ser.rs
src/validation.rs
test.sh

index 602205bc2ae7cb99ac50e6c6a87c50e0c31990ba..5a71c1fb8898f8cfc2fee8b02f9c9cb83affff41 100644 (file)
@@ -11,13 +11,15 @@ edition = "2021"
 rust-version = "1.60.0"
 
 [features]
+default = ["validation"]
 std = []
+validation = ["ring", "hex_lit"]
 tokio = ["tokio_crate/net", "tokio_crate/io-util", "std"]
 build_server = ["tokio", "tokio_crate/rt-multi-thread", "tokio_crate/macros"]
 
 [dependencies]
-ring = { version = "0.17", default-features = false, features = ["alloc"] }
-hex_lit = { version = "0.1", default-features = false, features = ["rust_v_1_46"] }
+ring = { version = "0.17", default-features = false, features = ["alloc"], optional = true }
+hex_lit = { version = "0.1", default-features = false, features = ["rust_v_1_46"], optional = true }
 tokio_crate = { package = "tokio", version = "1.0", default-features = false, optional = true }
 
 [dev-dependencies]
index 53a0b18a71e3ba38615c37fb84d36fd9320170be..944bea1d2c07acd637bd62b6f71d043119217a4c 100644 (file)
@@ -5,11 +5,13 @@
 extern crate alloc;
 
 pub mod rr;
-pub mod validation;
-mod ser;
+pub mod ser;
 pub mod query;
 
-#[cfg(feature = "tokio")]
+#[cfg(feature = "validation")]
+pub mod validation;
+
+#[cfg(any(feature = "build_server", all(feature = "tokio", feature = "validation")))]
 use tokio_crate as tokio;
 
 #[cfg(feature = "build_server")]
@@ -26,7 +28,7 @@ async fn main() {
        imp::run_server(listener, resolver_sockaddr).await;
 }
 
-#[cfg(feature = "tokio")]
+#[cfg(any(feature = "build_server", all(feature = "tokio", feature = "validation")))]
 mod imp {
        use super::*;
 
@@ -134,7 +136,7 @@ mod imp {
        }
 }
 
-#[cfg(all(feature = "tokio", test))]
+#[cfg(all(feature = "tokio", feature = "validation", test))]
 mod test {
        use super::*;
 
@@ -157,4 +159,36 @@ mod test {
                let verified_rrs = verify_rr_stream(&rrs).unwrap();
                assert_eq!(verified_rrs.verified_rrs.len(), 1);
        }
+
+       #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+       async fn test_lookup_a() {
+               let ns = "4.4.4.4:53".parse().unwrap();
+               let listener = tokio::net::TcpListener::bind("127.0.0.1:17493").await
+                       .expect("Failed to bind to socket");
+               tokio::spawn(imp::run_server(listener, ns));
+               let resp = minreq::get(
+                       "http://127.0.0.1:17493/dnssecproof?d=cloudflare.com.&t=a"
+               ).send().unwrap();
+
+               assert_eq!(resp.status_code, 200);
+               let rrs = parse_rr_stream(resp.as_bytes()).unwrap();
+               let verified_rrs = verify_rr_stream(&rrs).unwrap();
+               assert_eq!(verified_rrs.verified_rrs.len(), 1);
+       }
+
+       #[tokio::test(flavor = "multi_thread", worker_threads = 1)]
+       async fn test_lookup_tlsa() {
+               let ns = "1.1.1.1:53".parse().unwrap();
+               let listener = tokio::net::TcpListener::bind("127.0.0.1:17494").await
+                       .expect("Failed to bind to socket");
+               tokio::spawn(imp::run_server(listener, ns));
+               let resp = minreq::get(
+                       "http://127.0.0.1:17494/dnssecproof?d=_25._tcp.mail.as397444.net.&t=TLSA"
+               ).send().unwrap();
+
+               assert_eq!(resp.status_code, 200);
+               let rrs = parse_rr_stream(resp.as_bytes()).unwrap();
+               let verified_rrs = verify_rr_stream(&rrs).unwrap();
+               assert_eq!(verified_rrs.verified_rrs.len(), 1);
+       }
 }
index b25eb999c7ed3db51c51bd3de8a2661b052aebdf..c7027b0f1a36ad0310348a0c3720a4b3f6c04bf1 100644 (file)
 //!
 //! It is no-std (but requires `alloc`) and seeks to have minimal dependencies and a reasonably
 //! conservative MSRV policy, allowing it to be used in as many places as possible.
+//!
+//! Most of the crate's logic is feature-gated:
+//!  * By default, the `validate` feature is set, using `ring` to validate DNSSEC signatures and
+//!    proofs using the [`validation`] module.
+//!  * The `std` feature enables the [`query`] module, allowing for the building of proofs by
+//!    querying a recursive resolver over TCP.
+//!  * The `tokio` feature further enables async versions of the [`query`] methods, doing the same
+//!    querying async using `tokio`'s TCP streams.
+//!  * Finally, the crate can be built as a binary using the `build_server` feature, responding to
+//!    queries over HTTP GET calls to `/dnssecproof?d=domain.name.&t=RecordType` with DNSSEC
+//!    proofs.
 
 #![deny(missing_docs)]
 
 extern crate alloc;
 
 pub mod rr;
+pub mod ser;
+
+#[cfg(feature = "validation")]
 pub mod validation;
-mod ser;
 
 #[cfg(feature = "std")]
 pub mod query;
index 548ff153d0ce2e551e112131d8a02777be554e64..27f21601661b4dcd420787ac3cc91af619bd2d7d 100644 (file)
@@ -9,8 +9,6 @@ use tokio_crate::net::TcpStream as TokioTcpStream;
 #[cfg(feature = "tokio")]
 use tokio_crate::io::{AsyncReadExt, AsyncWriteExt};
 
-
-use crate::validation::write_rr;
 use crate::rr::*;
 use crate::ser::*;
 
@@ -191,7 +189,7 @@ pub async fn build_tlsa_proof_async(resolver: SocketAddr, domain: Name) -> Resul
        build_proof_async(resolver, domain, TLSA::TYPE).await
 }
 
-#[cfg(test)]
+#[cfg(all(feature = "validation", test))]
 mod tests {
        use super::*;
        use crate::validation::*;
index 462ad9260deb8c19623b9233577f280a2d8a32d0..0f7da1608b08ae8431118f611703074226d9d299 100644 (file)
@@ -1,10 +1,8 @@
-//! Serialization/Deserialization logic lives here
+//! Logic to read and write resource record (streams)
 
 use alloc::vec::Vec;
 use alloc::string::String;
 
-use ring::signature;
-
 use crate::rr::*;
 
 pub(crate) fn read_u8(inp: &mut &[u8]) -> Result<u8, ()> {
@@ -56,6 +54,7 @@ pub(crate) fn read_wire_packet_name(inp: &mut &[u8], wire_packet: &[u8]) -> Resu
 
 pub(crate) trait Writer { fn write(&mut self, buf: &[u8]); }
 impl Writer for Vec<u8> { fn write(&mut self, buf: &[u8]) { self.extend_from_slice(buf); } }
+#[cfg(feature = "validation")]
 impl Writer for ring::digest::Context { fn write(&mut self, buf: &[u8]) { self.update(buf); } }
 pub(crate) fn write_name<W: Writer>(out: &mut W, name: &str) {
        let canonical_name = name.to_ascii_lowercase();
@@ -110,23 +109,26 @@ pub(crate) fn parse_rr(inp: &mut &[u8]) -> Result<RR, ()> {
        parse_wire_packet_rr(inp, &[]).map(|(rr, _)| rr)
 }
 
-pub(crate) fn bytes_to_rsa_pk<'a>(pubkey: &'a [u8])
--> Result<signature::RsaPublicKeyComponents<&'a [u8]>, ()> {
-       if pubkey.len() <= 3 { return Err(()); }
-
-       let mut pos = 0;
-       let exponent_length;
-       if pubkey[0] == 0 {
-               exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize);
-               pos += 3;
-       } else {
-               exponent_length = pubkey[0] as usize;
-               pos += 1;
+/// Parse a stream of [`RR`]s from the format described in [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html).
+///
+/// Note that this is only the series of `AuthenticationChain` records, and does not read the
+/// `ExtSupportLifetime` field at the start of a `DnssecChainExtension`.
+pub fn parse_rr_stream(mut inp: &[u8]) -> Result<Vec<RR>, ()> {
+       let mut res = Vec::with_capacity(32);
+       while !inp.is_empty() {
+               res.push(parse_rr(&mut inp)?);
        }
+       Ok(res)
+}
 
-       if pubkey.len() <= pos + exponent_length { return Err(()); }
-       Ok(signature::RsaPublicKeyComponents {
-               n: &pubkey[pos + exponent_length..],
-               e: &pubkey[pos..pos + exponent_length]
-       })
+/// Writes the given resource record in its wire encoding to the given `Vec`.
+///
+/// An [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html) `AuthenticationChain` is simply a
+/// series of such records with no additional bytes in between.
+pub fn write_rr<RR: Record>(rr: &RR, ttl: u32, out: &mut Vec<u8>) {
+       write_name(out, rr.name());
+       out.extend_from_slice(&rr.ty().to_be_bytes());
+       out.extend_from_slice(&1u16.to_be_bytes()); // The INternet class
+       out.extend_from_slice(&ttl.to_be_bytes());
+       rr.write_u16_len_prefixed_data(out);
 }
index 491cbaa172d706f392eb5dd4c4039e5825cbba2e..892cbf5d173b6f99869730958c97959665d389c7 100644 (file)
@@ -7,7 +7,7 @@ use core::cmp;
 use ring::signature;
 
 use crate::rr::*;
-use crate::ser::{bytes_to_rsa_pk, parse_rr, write_name};
+use crate::ser::write_name;
 
 /// Gets the trusted root anchors
 ///
@@ -30,30 +30,6 @@ pub fn root_hints() -> Vec<DS> {
        res
 }
 
-/// Parse a stream of [`RR`]s from the format described in [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html).
-///
-/// Note that this is only the series of `AuthenticationChain` records, and does not read the
-/// `ExtSupportLifetime` field at the start of a `DnssecChainExtension`.
-pub fn parse_rr_stream(mut inp: &[u8]) -> Result<Vec<RR>, ()> {
-       let mut res = Vec::with_capacity(32);
-       while !inp.is_empty() {
-               res.push(parse_rr(&mut inp)?);
-       }
-       Ok(res)
-}
-
-/// Writes the given resource record in its wire encoding to the given `Vec`.
-///
-/// An [RFC 9102](https://www.rfc-editor.org/rfc/rfc9102.html) `AuthenticationChain` is simply a
-/// series of such records with no additional bytes in between.
-pub fn write_rr<RR: Record>(rr: &RR, ttl: u32, out: &mut Vec<u8>) {
-       write_name(out, rr.name());
-       out.extend_from_slice(&rr.ty().to_be_bytes());
-       out.extend_from_slice(&1u16.to_be_bytes()); // The INternet class
-       out.extend_from_slice(&ttl.to_be_bytes());
-       rr.write_u16_len_prefixed_data(out);
-}
-
 #[derive(Debug, PartialEq)]
 /// An error when validating DNSSEC signatures or other data
 pub enum ValidationError {
@@ -68,6 +44,27 @@ pub enum ValidationError {
        Invalid,
 }
 
+pub(crate) fn bytes_to_rsa_pk<'a>(pubkey: &'a [u8])
+-> Result<signature::RsaPublicKeyComponents<&'a [u8]>, ()> {
+       if pubkey.len() <= 3 { return Err(()); }
+
+       let mut pos = 0;
+       let exponent_length;
+       if pubkey[0] == 0 {
+               exponent_length = ((pubkey[1] as usize) << 8) | (pubkey[2] as usize);
+               pos += 3;
+       } else {
+               exponent_length = pubkey[0] as usize;
+               pos += 1;
+       }
+
+       if pubkey.len() <= pos + exponent_length { return Err(()); }
+       Ok(signature::RsaPublicKeyComponents {
+               n: &pubkey[pos + exponent_length..],
+               e: &pubkey[pos..pos + exponent_length]
+       })
+}
+
 fn verify_rrsig<'a, RR: Record, Keys>(sig: &RRSig, dnskeys: Keys, mut records: Vec<&RR>)
 -> Result<(), ValidationError>
 where Keys: IntoIterator<Item = &'a DnsKey> {
@@ -358,6 +355,8 @@ mod tests {
 
        use alloc::borrow::ToOwned;
 
+       use crate::ser::{parse_rr_stream, write_rr};
+
        use hex_conservative::FromHex;
        use rand::seq::SliceRandom;
 
diff --git a/test.sh b/test.sh
index 36609fdb52cef2b7bc8fb254b5dc3c936848e8eb..3346b74f123ce084c7d61d64a404249c7d75a238 100755 (executable)
--- a/test.sh
+++ b/test.sh
@@ -1,9 +1,10 @@
 #!/bin/sh
 set -eox
+cargo test --no-default-features
 cargo test
-cargo test --features std
-cargo test --features tokio
-cargo test --features build_server
+cargo test --no-default-features --features std
+cargo test --no-default-features --features tokio
+cargo test --no-default-features --features build_server
 cargo build --lib
 cargo build --lib --features std
 cargo build --lib --features tokio