diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cbad43917..02a0af673 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: with: rust-version: ${{ matrix.rust }} - if: matrix.os == 'ubuntu-latest' - run: sudo apt install libssl-dev + run: sudo apt-get install -y libssl-dev - if: matrix.os == 'windows-latest' id: vcpkg uses: johnwason/vcpkg-action@v6 @@ -53,7 +53,7 @@ jobs: with: rust-version: "1.68.2" - name: Install OpenSSL - run: sudo apt install libssl-dev + run: sudo apt-get install -y libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions diff --git a/Cargo.lock b/Cargo.lock index eaf9191fb..ca7fb4b69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,7 @@ dependencies = [ "rstest", "rustls-pemfile", "rustversion", + "secrecy", "serde", "serde_json", "serde_test", @@ -1027,6 +1028,15 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + [[package]] name = "semver" version = "1.0.23" diff --git a/Cargo.toml b/Cargo.toml index 78d0f2eda..4c60ad9d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build ring = { version = "0.17", optional = true } rustversion = { version = "1", optional = true } +secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } @@ -61,16 +62,16 @@ ring = ["dep:ring"] openssl = ["dep:openssl"] # Crate features +net = ["bytes", "futures-util", "rand", "std", "tokio"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] -net = ["bytes", "futures-util", "rand", "std", "tokio"] tsig = ["bytes", "ring", "smallvec"] zonefile = ["bytes", "serde", "std"] # Unstable features unstable-client-transport = ["moka", "net", "tracing"] unstable-server-transport = ["arc-swap", "chrono/clock", "libc", "net", "siphasher", "tracing"] -unstable-sign = ["std", "unstable-validate"] +unstable-sign = ["std", "dep:secrecy", "unstable-validate"] unstable-stelline = ["tokio/test-util", "tracing", "tracing-subscriber", "tsig", "unstable-client-transport", "unstable-server-transport", "zonefile"] unstable-validate = ["bytes", "std", "ring"] unstable-validator = ["unstable-validate", "zonefile", "unstable-client-transport"] diff --git a/src/lib.rs b/src/lib.rs index 119adc66f..0d0a4a2ba 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,61 +61,79 @@ //! //! # Reference of feature flags //! -//! The following is the complete list of the feature flags with the -//! exception of unstable features which are described below. +//! Several feature flags simply enable support for other crates, e.g. by +//! adding `impl`s for their types. They are optional and do not introduce +//! new functionality into this crate. //! //! * `bytes`: Enables using the types `Bytes` and `BytesMut` from the //! [bytes](https://github.com/tokio-rs/bytes) crate as octet sequences. -//! * `chrono`: Adds the [chrono](https://github.com/chronotope/chrono) -//! crate as a dependency. This adds support for generating serial numbers -//! from time stamps. +//! //! * `heapless`: enables the use of the `Vec` type from the //! [heapless](https://github.com/japaric/heapless) crate as octet //! sequences. -//! * `interop`: Activate interoperability tests that rely on other software -//! to be installed in the system (currently NSD and dig) and will fail if -//! it isn’t. This feature is not meaningful for users of the crate. +//! +//! * `smallvec`: enables the use of the `Smallvec` type from the +//! [smallvec](https://github.com/servo/rust-smallvec) crate as octet +//! sequences. +//! +//! Some flags enable support for specific kinds of operations that are not +//! otherwise possible. They are gated as they may not always be necessary +//! and they may introduce new dependencies. +//! +//! * `chrono`: Adds the [chrono](https://github.com/chronotope/chrono) +//! crate as a dependency. This adds support for generating serial numbers +//! from time stamps. +//! //! * `rand`: Enables a number of methods that rely on a random number //! generator being available in the system. -//! * `resolv`: Enables the asynchronous stub resolver via the -#![cfg_attr(feature = "resolv", doc = " [resolv]")] -#![cfg_attr(not(feature = "resolv"), doc = " resolv")] -//! module. -//! * `resolv-sync`: Enables the synchronous version of the stub resolver. -//! * `ring`: Enables crypto functionality via the -//! [ring](https://github.com/briansmith/ring) crate. +//! //! * `serde`: Enables serde serialization for a number of basic types. -//! * `sign`: basic DNSSEC signing support. This will enable the -#![cfg_attr(feature = "unstable-sign", doc = " [sign]")] -#![cfg_attr(not(feature = "unstable-sign"), doc = " sign")] -//! module and requires the `std` feature. Note that this will not directly -//! enable actual signing. For that you will also need to pick a crypto -//! module via an additional feature. Currently we only support the `ring` -//! module, but support for OpenSSL is coming soon. +//! //! * `siphasher`: enables the dependency on the //! [siphasher](https://github.com/jedisct1/rust-siphash) crate which allows //! generating and checking hashes in [standard server //! cookies][crate::base::opt::cookie::StandardServerCookie]. -//! * `smallvec`: enables the use of the `Smallvec` type from the -//! [smallvec](https://github.com/servo/rust-smallvec) crate as octet -//! sequences. +//! //! * `std`: support for the Rust std library. This feature is enabled by //! default. +//! +//! A special case here is cryptographic backends. Certain modules (e.g. for +//! DNSSEC signing and validation) require a backend to provide cryptography. +//! At least one such module should be enabled. +//! +//! * `openssl`: Enables crypto functionality via OpenSSL through the +//! [rust-openssl](https://github.com/sfackler/rust-openssl) crate. +//! +//! * `ring`: Enables crypto functionality via the +//! [ring](https://github.com/briansmith/ring) crate. +//! +//! Some flags represent entire categories of functionality within this crate. +//! Each flag is associated with a particular module. Note that some of these +//! modules are under heavy development, and so have unstable feature flags +//! which are categorized separately. +//! +//! * `net`: Enables sending and receiving DNS messages via the +#![cfg_attr(feature = "net", doc = " [net]")] +#![cfg_attr(not(feature = "net"), doc = " net")] +//! module. +//! +//! * `resolv`: Enables the asynchronous stub resolver via the +#![cfg_attr(feature = "resolv", doc = " [resolv]")] +#![cfg_attr(not(feature = "resolv"), doc = " resolv")] +//! module. +//! +//! * `resolv-sync`: Enables the synchronous version of the stub resolver. +//! //! * `tsig`: support for signing and validating message exchanges via TSIG //! signatures. This enables the #![cfg_attr(feature = "tsig", doc = " [tsig]")] #![cfg_attr(not(feature = "tsig"), doc = " tsig")] -//! module and currently pulls in the -//! `bytes`, `ring`, and `smallvec` features. -//! * `validate`: basic DNSSEC validation support. This feature enables the -#![cfg_attr(feature = "unstable-validate", doc = " [validate]")] -#![cfg_attr(not(feature = "unstable-validate"), doc = " validate")] -//! module and currently also enables the `std` and `ring` -//! features. +//! module and currently enables `bytes`, `ring`, and `smallvec`. +//! //! * `zonefile`: reading and writing of zonefiles. This feature enables the #![cfg_attr(feature = "zonefile", doc = " [zonefile]")] #![cfg_attr(not(feature = "zonefile"), doc = " zonefile")] -//! module and currently also enables the `bytes` and `std` features. +//! module and currently also enables `bytes`, `serde`, and `std`. //! //! # Unstable features //! @@ -137,6 +155,16 @@ //! a client perspective; primarily the `net::client` module. //! * `unstable-server-transport`: receiving and sending DNS messages from //! a server perspective; primarily the `net::server` module. +//! * `unstable-sign`: basic DNSSEC signing support. This will enable the +#![cfg_attr(feature = "unstable-sign", doc = " [sign]")] +#![cfg_attr(not(feature = "unstable-sign"), doc = " sign")] +//! module and requires the `std` feature. In order to actually perform any +//! signing, also enable one or more cryptographic backend modules (`ring` +//! and `openssl`). +//! * `unstable-validate`: basic DNSSEC validation support. This enables the +#![cfg_attr(feature = "unstable-validate", doc = " [validate]")] +#![cfg_attr(not(feature = "unstable-validate"), doc = " validate")] +//! module and currently also enables the `std` and `ring` features. //! * `unstable-validator`: a DNSSEC validator, primarily the `validator` //! and the `net::client::validator` modules. //! * `unstable-xfr`: zone transfer related functionality.. diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index fdb79dd52..eb0259411 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -2169,6 +2169,11 @@ impl> RtypeBitmap { ) -> Result<(), Target::AppendError> { target.append_slice(self.0.as_ref()) } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.iter().next().is_none() + } } //--- AsRef diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index e2a19468d..a09e4c309 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -358,7 +358,10 @@ impl> fmt::Display for Nsec3 { self.hash_algorithm, self.flags, self.iterations, self.salt )?; base32::display_hex(&self.next_owner, f)?; - write!(f, " {}", self.types) + if !self.types.is_empty() { + write!(f, " {}", self.types)?; + } + Ok(()) } } diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index 1187a6dbf..6393a0aca 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -2,6 +2,7 @@ use core::{fmt, str}; +use secrecy::{ExposeSecret, SecretBox}; use std::boxed::Box; use std::vec::Vec; @@ -89,22 +90,22 @@ pub enum SecretKeyBytes { /// An ECDSA P-256/SHA-256 keypair. /// /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256(Box<[u8; 32]>), + EcdsaP256Sha256(SecretBox<[u8; 32]>), /// An ECDSA P-384/SHA-384 keypair. /// /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384(Box<[u8; 48]>), + EcdsaP384Sha384(SecretBox<[u8; 48]>), /// An Ed25519 keypair. /// /// The private key is a single 32-byte string. - Ed25519(Box<[u8; 32]>), + Ed25519(SecretBox<[u8; 32]>), /// An Ed448 keypair. /// /// The private key is a single 57-byte string. - Ed448(Box<[u8; 57]>), + Ed448(SecretBox<[u8; 57]>), } //--- Inspection @@ -139,23 +140,27 @@ impl SecretKeyBytes { } Self::EcdsaP256Sha256(s) => { + let s = s.expose_secret(); writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { + let s = s.expose_secret(); writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { + let s = s.expose_secret(); writeln!(w, "Algorithm: 15 (ED25519)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { + let s = s.expose_secret(); writeln!(w, "Algorithm: 16 (ED448)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } } @@ -182,20 +187,24 @@ impl SecretKeyBytes { /// Parse private keys for most algorithms (except RSA). fn parse_pkey( mut data: &str, - ) -> Result, BindFormatError> { + ) -> Result, BindFormatError> { // Look for the 'PrivateKey' field. - while let Some((key, val, rest)) = parse_dns_pair(data)? { + while let Some((key, val, rest)) = parse_bind_entry(data)? { data = rest; if key != "PrivateKey" { continue; } - return base64::decode::>(val) - .map_err(|_| BindFormatError::Misformatted)? - .into_boxed_slice() + // TODO: Evaluate security of 'base64::decode()'. + let val: Vec = base64::decode(val) + .map_err(|_| BindFormatError::Misformatted)?; + let val: Box<[u8]> = val.into_boxed_slice(); + let val: Box<[u8; N]> = val .try_into() - .map_err(|_| BindFormatError::Misformatted); + .map_err(|_| BindFormatError::Misformatted)?; + + return Ok(val.into()); } // The 'PrivateKey' field was not found. @@ -203,7 +212,7 @@ impl SecretKeyBytes { } // The first line should specify the key format. - let (_, _, data) = parse_dns_pair(data)? + let (_, _, data) = parse_bind_entry(data)? .filter(|&(k, v, _)| { k == "Private-key-format" && v.strip_prefix("v1.") @@ -213,7 +222,7 @@ impl SecretKeyBytes { .ok_or(BindFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. - let (_, val, data) = parse_dns_pair(data)? + let (_, val, data) = parse_bind_entry(data)? .filter(|&(k, _, _)| k == "Algorithm") .ok_or(BindFormatError::Misformatted)?; @@ -245,21 +254,6 @@ impl SecretKeyBytes { } } -//--- Drop - -impl Drop for SecretKeyBytes { - fn drop(&mut self) { - // Zero the bytes for each field. - match self { - Self::RsaSha256(_) => {} - Self::EcdsaP256Sha256(s) => s.fill(0), - Self::EcdsaP384Sha384(s) => s.fill(0), - Self::Ed25519(s) => s.fill(0), - Self::Ed448(s) => s.fill(0), - } - } -} - //----------- RsaSecretKeyBytes --------------------------------------------------- /// An RSA secret key expressed as raw bytes. @@ -275,22 +269,22 @@ pub struct RsaSecretKeyBytes { pub e: Box<[u8]>, /// The private exponent. - pub d: Box<[u8]>, + pub d: SecretBox<[u8]>, /// The first prime factor of `d`. - pub p: Box<[u8]>, + pub p: SecretBox<[u8]>, /// The second prime factor of `d`. - pub q: Box<[u8]>, + pub q: SecretBox<[u8]>, /// The exponent corresponding to the first prime factor of `d`. - pub d_p: Box<[u8]>, + pub d_p: SecretBox<[u8]>, /// The exponent corresponding to the second prime factor of `d`. - pub d_q: Box<[u8]>, + pub d_q: SecretBox<[u8]>, /// The inverse of the second prime factor modulo the first. - pub q_i: Box<[u8]>, + pub q_i: SecretBox<[u8]>, } //--- Conversion to and from the BIND format @@ -308,17 +302,17 @@ impl RsaSecretKeyBytes { w.write_str("PublicExponent: ")?; writeln!(w, "{}", base64::encode_display(&self.e))?; w.write_str("PrivateExponent: ")?; - writeln!(w, "{}", base64::encode_display(&self.d))?; + writeln!(w, "{}", base64::encode_display(&self.d.expose_secret()))?; w.write_str("Prime1: ")?; - writeln!(w, "{}", base64::encode_display(&self.p))?; + writeln!(w, "{}", base64::encode_display(&self.p.expose_secret()))?; w.write_str("Prime2: ")?; - writeln!(w, "{}", base64::encode_display(&self.q))?; + writeln!(w, "{}", base64::encode_display(&self.q.expose_secret()))?; w.write_str("Exponent1: ")?; - writeln!(w, "{}", base64::encode_display(&self.d_p))?; + writeln!(w, "{}", base64::encode_display(&self.d_p.expose_secret()))?; w.write_str("Exponent2: ")?; - writeln!(w, "{}", base64::encode_display(&self.d_q))?; + writeln!(w, "{}", base64::encode_display(&self.d_q.expose_secret()))?; w.write_str("Coefficient: ")?; - writeln!(w, "{}", base64::encode_display(&self.q_i))?; + writeln!(w, "{}", base64::encode_display(&self.q_i.expose_secret()))?; Ok(()) } @@ -351,7 +345,7 @@ impl RsaSecretKeyBytes { let mut d_q = None; let mut q_i = None; - while let Some((key, val, rest)) = parse_dns_pair(data)? { + while let Some((key, val, rest)) = parse_bind_entry(data)? { let field = match key { "Modulus" => &mut n, "PublicExponent" => &mut e, @@ -389,12 +383,12 @@ impl RsaSecretKeyBytes { Ok(Self { n: n.unwrap(), e: e.unwrap(), - d: d.unwrap(), - p: p.unwrap(), - q: q.unwrap(), - d_p: d_p.unwrap(), - d_q: d_q.unwrap(), - q_i: q_i.unwrap(), + d: d.unwrap().into(), + p: p.unwrap().into(), + q: q.unwrap().into(), + d_p: d_p.unwrap().into(), + d_q: d_q.unwrap().into(), + q_i: q_i.unwrap().into(), }) } } @@ -410,26 +404,10 @@ impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { } } -//--- Drop - -impl Drop for RsaSecretKeyBytes { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.fill(0u8); - self.e.fill(0u8); - self.d.fill(0u8); - self.p.fill(0u8); - self.q.fill(0u8); - self.d_p.fill(0u8); - self.d_q.fill(0u8); - self.q_i.fill(0u8); - } -} - //----------- Helpers for parsing the BIND format ---------------------------- -/// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair( +/// Extract the next key-value pair in a BIND-format private key file. +fn parse_bind_entry( data: &str, ) -> Result, BindFormatError> { // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. diff --git a/src/sign/common.rs b/src/sign/common.rs index fc10803e3..fe0fd1113 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -26,10 +26,10 @@ use super::ring; /// A key pair based on a built-in backend. /// -/// This supports any built-in backend (currently, that is OpenSSL and Ring). -/// Wherever possible, the Ring backend is preferred over OpenSSL -- but for -/// more uncommon or insecure algorithms, that Ring does not support, OpenSSL -/// must be used. +/// This supports any built-in backend (currently, that is OpenSSL and Ring, +/// if their respective feature flags are enabled). Wherever possible, it +/// will prefer the Ring backend over OpenSSL -- but for more uncommon or +/// insecure algorithms, that Ring does not support, OpenSSL must be used. pub enum KeyPair { /// A key backed by Ring. #[cfg(feature = "ring")] diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 586bedada..008d6933f 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -8,11 +8,10 @@ //! "offline" (outside of a name server). Once generated, signatures can be //! serialized as DNS records and stored alongside the authenticated records. //! -//! A DNSSEC key actually has two components: a cryptographic key, which can -//! be used to make and verify signatures, and key metadata, which defines how -//! the key should be used. These components are brought together by the -//! [`SigningKey`] type. It must be instantiated with a cryptographic key -//! type, such as [`common::KeyPair`], in order to be used. +//! Signatures can be generated using a [`SigningKey`], which combines +//! cryptographic key material with additional information that defines how +//! the key should be used. [`SigningKey`] relies on a cryptographic backend +//! to provide the underlying signing operation (e.g. [`common::KeyPair`]). //! //! # Example Usage //! @@ -47,12 +46,13 @@ //! This crate supports OpenSSL and Ring for performing cryptography. These //! cryptographic backends are gated on the `openssl` and `ring` features, //! respectively. They offer mostly equivalent functionality, but OpenSSL -//! supports a larger set of signing algorithms. A [`common`] backend is -//! provided for users that wish to use either or both backends at runtime. +//! supports a larger set of signing algorithms (and, for RSA keys, supports +//! weaker key sizes). A [`common`] backend is provided for users that wish +//! to use either or both backends at runtime. //! -//! Each backend module exposes a `KeyPair` type, representing a cryptographic -//! key that can be used for signing, and a `generate()` function for creating -//! new keys. +//! Each backend module (`openssl`, `ring`, and `common`) exposes a `KeyPair` +//! type, representing a cryptographic key that can be used for signing, and a +//! `generate()` function for creating new keys. //! //! Users can choose to bring their own cryptography by providing their own //! `KeyPair` type that implements [`SignRaw`]. Note that `async` signing @@ -238,10 +238,11 @@ impl SigningKey { /// information (for zone signing keys, DNS records; for key signing keys, /// subsidiary public keys). /// -/// Before a key can be used for signing, it should be validated. If the -/// implementing type allows [`sign_raw()`] to be called on unvalidated keys, -/// it will have to check the validity of the key for every signature; this is -/// unnecessary overhead when many signatures have to be generated. +/// Implementing types should validate keys during construction, so that +/// signing does not fail due to invalid keys. If the implementing type +/// allows [`sign_raw()`] to be called on unvalidated keys, it will have to +/// check the validity of the key for every signature; this is unnecessary +/// overhead when many signatures have to be generated. /// /// [`sign_raw()`]: SignRaw::sign_raw() pub trait SignRaw { @@ -282,6 +283,11 @@ pub enum GenerateParams { /// A ~3000-bit key corresponds to a 128-bit security level. However, /// RSA is mostly used with 2048-bit keys. Some backends (like Ring) /// do not support smaller key sizes than that. + /// + /// For more information about security levels, see [NIST SP 800-57 + /// part 1 revision 5], page 54, table 2. + /// + /// [NIST SP 800-57 part 1 revision 5]: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-57pt1r5.pdf bits: u32, }, diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 85257137a..a7250081a 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -12,7 +12,7 @@ #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] use core::fmt; -use std::vec::Vec; +use std::{boxed::Box, vec::Vec}; use openssl::{ bn::BigNum, @@ -20,6 +20,7 @@ use openssl::{ error::ErrorStack, pkey::{self, PKey, Private}, }; +use secrecy::ExposeSecret; use crate::{ base::iana::SecAlg, @@ -70,12 +71,12 @@ impl KeyPair { let n = num(&s.n)?; let e = num(&s.e)?; - let d = secure_num(&s.d)?; - let p = secure_num(&s.p)?; - let q = secure_num(&s.q)?; - let d_p = secure_num(&s.d_p)?; - let d_q = secure_num(&s.d_q)?; - let q_i = secure_num(&s.q_i)?; + let d = secure_num(s.d.expose_secret())?; + let p = secure_num(s.p.expose_secret())?; + let q = secure_num(s.q.expose_secret())?; + let d_p = secure_num(s.d_p.expose_secret())?; + let d_q = secure_num(s.d_q.expose_secret())?; + let q_i = secure_num(s.q_i.expose_secret())?; // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the @@ -101,7 +102,7 @@ impl KeyPair { let mut ctx = bn::BigNumContext::new_secure()?; let group = nid::Nid::X9_62_PRIME256V1; let group = ec::EcGroup::from_curve_name(group)?; - let n = secure_num(s.as_slice())?; + let n = secure_num(s.expose_secret().as_slice())?; let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx)?; let k = ec::EcKey::from_private_components(&group, &n, &p)?; k.check_key().map_err(|_| FromBytesError::InvalidKey)?; @@ -117,7 +118,7 @@ impl KeyPair { let mut ctx = bn::BigNumContext::new_secure()?; let group = nid::Nid::SECP384R1; let group = ec::EcGroup::from_curve_name(group)?; - let n = secure_num(s.as_slice())?; + let n = secure_num(s.expose_secret().as_slice())?; let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx)?; let k = ec::EcKey::from_private_components(&group, &n, &p)?; k.check_key().map_err(|_| FromBytesError::InvalidKey)?; @@ -128,7 +129,8 @@ impl KeyPair { use openssl::memcmp; let id = pkey::Id::ED25519; - let k = PKey::private_key_from_raw_bytes(&**s, id)?; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { @@ -140,7 +142,8 @@ impl KeyPair { use openssl::memcmp; let id = pkey::Id::ED448; - let k = PKey::private_key_from_raw_bytes(&**s, id)?; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { @@ -182,20 +185,24 @@ impl KeyPair { SecAlg::ECDSAP256SHA256 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(32).unwrap(); - SecretKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP256Sha256(key.into()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(48).unwrap(); - SecretKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) + let key: Box<[u8; 48]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP384Sha384(key.into()) } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); - SecretKeyBytes::Ed25519(key.try_into().unwrap()) + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::Ed25519(key.into()) } SecAlg::ED448 => { let key = self.pkey.raw_private_key().unwrap(); - SecretKeyBytes::Ed448(key.try_into().unwrap()) + let key: Box<[u8; 57]> = key.try_into().unwrap(); + SecretKeyBytes::Ed448(key.into()) } _ => unreachable!(), } diff --git a/src/sign/records.rs b/src/sign/records.rs index 2df3aa1ab..e3ad7460c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -3,7 +3,9 @@ use core::convert::From; use core::fmt::Display; use std::boxed::Box; +use std::collections::HashMap; use std::fmt::Debug; +use std::hash::Hash; use std::string::String; use std::vec::Vec; use std::{fmt, io, slice}; @@ -329,7 +331,7 @@ impl SortedRecords { } let mut bitmap = RtypeBitmap::::builder(); - // Assume there’s gonna be an RRSIG. + // Assume there's gonna be an RRSIG. bitmap.add(Rtype::RRSIG).unwrap(); if family.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. @@ -370,9 +372,10 @@ impl SortedRecords { ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, + capture_hash_to_owner_mappings: bool, ) -> Result, Nsec3HashError> where - N: ToName + Clone + From> + Display, + N: ToName + Clone + From> + Display + Ord + Hash, N: From::Octets>>, D: RecordData, Octets: FromBuilder + OctetsFrom> + Clone + Default, @@ -383,6 +386,7 @@ impl SortedRecords { + AsMut<[u8]> + EmptyBuilder + FreezeBuilder, + ::Octets: AsRef<[u8]>, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -405,6 +409,8 @@ impl SortedRecords { // We store the NSEC3s as we create them in a self-sorting vec. let mut nsec3s = SortedRecords::new(); + let mut ents = Vec::::new(); + // The owner name of a zone cut if we currently are at or below one. let mut cut: Option> = None; @@ -418,6 +424,13 @@ impl SortedRecords { let apex_owner = families.first_owner().clone(); let apex_label_count = apex_owner.iter_labels().count(); + let mut last_nent_stack: Vec = vec![]; + let mut nsec3_hash_map = if capture_hash_to_owner_mappings { + Some(HashMap::::new()) + } else { + None + }; + for family in families { // If the owner is out of zone, we have moved out of our zone and // are done. @@ -457,9 +470,20 @@ impl SortedRecords { // the original owner name is greater than 1, additional NSEC3 // RRs need to be added for every empty non-terminal between // the apex and the original owner name." + let mut last_nent_distance_to_apex = 0; + let mut last_nent = None; + while let Some(this_last_nent) = last_nent_stack.pop() { + if name.owner().ends_with(&this_last_nent) { + last_nent_distance_to_apex = + this_last_nent.iter_labels().count() + - apex_label_count; + last_nent = Some(this_last_nent); + break; + } + } let distance_to_root = name.owner().iter_labels().count(); let distance_to_apex = distance_to_root - apex_label_count; - if distance_to_apex > 1 { + if distance_to_apex > last_nent_distance_to_apex { // Are there any empty nodes between this node and the apex? // The zone file records are already sorted so if all of the // parent labels had records at them, i.e. they were non-empty @@ -480,7 +504,8 @@ impl SortedRecords { // It will NOT construct the last name as that will be dealt // with in the next outer loop iteration. // - a.b.c.mail.example.com - for n in (1..distance_to_apex - 1).rev() { + let distance = distance_to_apex - last_nent_distance_to_apex; + for n in (1..=distance - 1).rev() { let rev_label_it = name.owner().iter_labels().skip(n); // Create next longest ENT name. @@ -491,22 +516,9 @@ impl SortedRecords { let name = builder.append_origin(&apex_owner).unwrap().into(); - // Create the type bitmap, empty for an ENT NSEC3. - let bitmap = RtypeBitmap::::builder(); - - let rec = Self::mk_nsec3( - &name, - params.hash_algorithm(), - nsec3_flags, - params.iterations(), - params.salt(), - &apex_owner, - bitmap, - ttl, - )?; - - // Store the record by order of its owner name. - let _ = nsec3s.insert(rec); + if let Err(pos) = ents.binary_search(&name) { + ents.insert(pos, name); + } } } @@ -542,7 +554,45 @@ impl SortedRecords { ttl, )?; - let _ = nsec3s.insert(rec); + if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + nsec3_hash_map + .insert(rec.owner().clone(), name.owner().clone()); + } + + // Store the record by order of its owner name. + if nsec3s.insert(rec).is_err() { + return Err(Nsec3HashError::CollisionDetected); + } + + if let Some(last_nent) = last_nent { + last_nent_stack.push(last_nent); + } + last_nent_stack.push(name.owner().clone()); + } + + for name in ents { + // Create the type bitmap, empty for an ENT NSEC3. + let bitmap = RtypeBitmap::::builder(); + + let rec = Self::mk_nsec3( + &name, + params.hash_algorithm(), + nsec3_flags, + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + if let Some(nsec3_hash_map) = &mut nsec3_hash_map { + nsec3_hash_map.insert(rec.owner().clone(), name); + } + + // Store the record by order of its owner name. + if nsec3s.insert(rec).is_err() { + return Err(Nsec3HashError::CollisionDetected); + } } // RFC 5155 7.1 step 7: @@ -582,9 +632,15 @@ impl SortedRecords { // "If a hash collision is detected, then a new salt has to be // chosen, and the signing process restarted." // - // TODO + // Handled above. - Ok(Nsec3Records::new(nsec3s.records, nsec3param)) + let res = Nsec3Records::new(nsec3s.records, nsec3param); + + if let Some(nsec3_hash_map) = nsec3_hash_map { + Ok(res.with_hashes(nsec3_hash_map)) + } else { + Ok(res) + } } pub fn write(&self, target: &mut W) -> Result<(), io::Error> @@ -605,6 +661,39 @@ impl SortedRecords { Ok(()) } + + pub fn write_with_comments( + &self, + target: &mut W, + comment_cb: F, + ) -> Result<(), io::Error> + where + N: fmt::Display, + D: RecordData + fmt::Display, + W: io::Write, + C: fmt::Display, + F: Fn(&Record) -> Option, + { + for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) + { + if let Some(comment) = comment_cb(record) { + writeln!(target, "{record} ;{}", comment)?; + } else { + writeln!(target, "{record}")?; + } + } + + for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) + { + if let Some(comment) = comment_cb(record) { + writeln!(target, "{record} ;{}", comment)?; + } else { + writeln!(target, "{record}")?; + } + } + + Ok(()) + } } /// Helper functions used to create NSEC3 records per RFC 5155. @@ -728,21 +817,34 @@ where //------------ Nsec3Records --------------------------------------------------- -/// The set of records created by [`SortedRecords::nsec3s()`]. pub struct Nsec3Records { /// The NSEC3 records. - pub nsec3s: Vec>>, + pub recs: Vec>>, /// The NSEC3PARAM record. - pub nsec3param: Record>, + pub param: Record>, + + /// A map of hashes to owner names. + /// + /// For diagnostic purposes. None if not generated. + pub hashes: Option>, } impl Nsec3Records { pub fn new( - nsec3s: Vec>>, - nsec3param: Record>, + recs: Vec>>, + param: Record>, ) -> Self { - Self { nsec3s, nsec3param } + Self { + recs, + param, + hashes: None, + } + } + + pub fn with_hashes(mut self, hashes: HashMap) -> Self { + self.hashes = Some(hashes); + self } } @@ -1058,7 +1160,7 @@ pub enum Nsec3OptOut { // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 // 7.1. Zone Signing // "Zones using NSEC3 must satisfy the following properties: -// +// // o Each owner name within the zone that owns authoritative RRSets // MUST have a corresponding NSEC3 RR. Owner names that correspond // to unsigned delegations MAY have a corresponding NSEC3 RR. @@ -1066,16 +1168,16 @@ pub enum Nsec3OptOut { // an Opt-Out NSEC3 RR that covers the "next closer" name to the // delegation. Other non-authoritative RRs are not represented by // NSEC3 RRs. -// +// // o Each empty non-terminal MUST have a corresponding NSEC3 RR, unless // the empty non-terminal is only derived from an insecure delegation // covered by an Opt-Out NSEC3 RR. -// +// // o The TTL value for any NSEC3 RR SHOULD be the same as the minimum // TTL value field in the zone SOA RR. -// +// // o The Type Bit Maps field of every NSEC3 RR in a signed zone MUST // indicate the presence of all types present at the original owner // name, except for the types solely contributed by an NSEC3 RR // itself. Note that this means that the NSEC3 type itself will -// never be present in the Type Bit Maps." \ No newline at end of file +// never be present in the Type Bit Maps." diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 09435188c..7ff8fb574 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -13,8 +13,10 @@ use core::fmt; use std::{boxed::Box, sync::Arc, vec::Vec}; -use ring::signature::KeyPair as _; -use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, RsaKeyPair}; +use ring::signature::{ + EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, +}; +use secrecy::ExposeSecret; use crate::base::iana::SecAlg; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -73,12 +75,12 @@ impl KeyPair { n: s.n.as_ref(), e: s.e.as_ref(), }, - d: s.d.as_ref(), - p: s.p.as_ref(), - q: s.q.as_ref(), - dP: s.d_p.as_ref(), - dQ: s.d_q.as_ref(), - qInv: s.q_i.as_ref(), + d: s.d.expose_secret(), + p: s.p.expose_secret(), + q: s.q.expose_secret(), + dP: s.d_p.expose_secret(), + dQ: s.d_q.expose_secret(), + qInv: s.q_i.expose_secret(), }; ring::signature::RsaKeyPair::from_components(&components) .map_err(|_| FromBytesError::InvalidKey) @@ -92,7 +94,7 @@ impl KeyPair { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; EcdsaKeyPair::from_private_key_and_public_key( alg, - s.as_slice(), + s.expose_secret(), p.as_slice(), &*rng, ) @@ -107,7 +109,7 @@ impl KeyPair { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; EcdsaKeyPair::from_private_key_and_public_key( alg, - s.as_slice(), + s.expose_secret(), p.as_slice(), &*rng, ) @@ -117,7 +119,7 @@ impl KeyPair { (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { Ed25519KeyPair::from_seed_and_public_key( - s.as_slice(), + s.expose_secret(), p.as_slice(), ) .map_err(|_| FromBytesError::InvalidKey) @@ -237,8 +239,8 @@ pub fn generate( // Manually parse the PKCS#8 document for the private key. let sk: Box<[u8]> = Box::from(&doc.as_ref()[36..68]); - let sk = sk.try_into().unwrap(); - let sk = SecretKeyBytes::EcdsaP256Sha256(sk); + let sk: Box<[u8; 32]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::EcdsaP256Sha256(sk.into()); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); @@ -255,8 +257,8 @@ pub fn generate( // Manually parse the PKCS#8 document for the private key. let sk: Box<[u8]> = Box::from(&doc.as_ref()[35..83]); - let sk = sk.try_into().unwrap(); - let sk = SecretKeyBytes::EcdsaP384Sha384(sk); + let sk: Box<[u8; 48]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::EcdsaP384Sha384(sk.into()); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); @@ -272,8 +274,8 @@ pub fn generate( // Manually parse the PKCS#8 document for the private key. let sk: Box<[u8]> = Box::from(&doc.as_ref()[16..48]); - let sk = sk.try_into().unwrap(); - let sk = SecretKeyBytes::Ed25519(sk); + let sk: Box<[u8; 32]> = sk.try_into().unwrap(); + let sk = SecretKeyBytes::Ed25519(sk.into()); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); diff --git a/src/validate.rs b/src/validate.rs index c076009de..3293df0f0 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1707,6 +1707,9 @@ pub enum Nsec3HashError { /// /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) OwnerHashError, + + /// The hashing process produced a hash that already exists. + CollisionDetected, } /// Compute an [RFC 5155] NSEC3 hash using default settings.