From 4022c81f8cc1300e17865aa653637127bfbec553 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 001/569] [sign] Define 'KeyPair' and impl key export A private key converted into a 'KeyPair' can be exported in the conventional DNS format. This is an important step in implementing 'ldns-keygen' using 'domain'. It is up to the implementation modules to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs) won't support it at all. --- src/sign/mod.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d87acca0c..ff36b16b7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,10 +1,253 @@ //! DNSSEC signing. //! //! **This module is experimental and likely to change significantly.** +//! +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a +//! DNS record served by a secure-aware name server. But name servers are not +//! usually creating those signatures themselves. Within a DNS zone, it is the +//! zone administrator's responsibility to sign zone records (when the record's +//! time-to-live expires and/or when it changes). Those signatures are stored +//! as regular DNS data and automatically served by name servers. + #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +use core::{fmt, str}; + +use crate::base::iana::SecAlg; + pub mod key; //pub mod openssl; pub mod records; pub mod ring; + +/// A generic keypair. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// cryptographic implementation supports it). +pub enum KeyPair + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> KeyPair { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64(&*s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64(&*s, &mut *w) + } + } + } +} + +impl + AsMut<[u8]>> Drop for KeyPair { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6.2 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (b'0' - 52) as u32 + + (pluses & bcast) * (b'+' - 62) as u32 + + (slashs & bcast) * (b'/' - 63) as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + 3 => {} + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} From 7b51569d29eab960eb35ace4b53b3a01d27f0be3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:54:14 +0200 Subject: [PATCH 002/569] [sign] Define trait 'Sign' 'Sign' is a more generic version of 'sign::key::SigningKey' that does not provide public key information. It does not try to abstract over all the functionality of a keypair, since that can depend on the underlying cryptographic implementation. --- src/sign/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ff36b16b7..f4bac3c51 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -21,6 +21,42 @@ pub mod key; pub mod records; pub mod ring; +/// Signing DNS records. +/// +/// Implementors of this trait own a private key and sign DNS records for a zone +/// with that key. Signing is a synchronous operation performed on the current +/// thread; this rules out implementations like HSMs, where I/O communication is +/// necessary. +pub trait Sign { + /// An error in constructing a signature. + type Error; + + /// The signature algorithm used. + /// + /// The following algorithms can be used: + /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) + /// - [`SecAlg::DSA`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1`] (insecure, not recommended) + /// - [`SecAlg::DSA_NSEC3_SHA1`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1_NSEC3_SHA1`] (insecure, not recommended) + /// - [`SecAlg::RSASHA256`] + /// - [`SecAlg::RSASHA512`] (not recommended) + /// - [`SecAlg::ECC_GOST`] (do not use) + /// - [`SecAlg::ECDSAP256SHA256`] + /// - [`SecAlg::ECDSAP384SHA384`] + /// - [`SecAlg::ED25519`] + /// - [`SecAlg::ED448`] + fn algorithm(&self) -> SecAlg; + + /// Compute a signature. + /// + /// A regular signature of the given byte sequence is computed and is turned + /// into the selected buffer type. This provides a lot of flexibility in + /// how buffers are constructed; they may be heap-allocated or have a static + /// size. + fn sign(&self, data: &[u8]) -> Result; +} + /// A generic keypair. /// /// This type cannot be used for computing signatures, as it does not implement From cb97321dadf6ede90c42d51056081685650f6e1d Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 15:42:48 +0200 Subject: [PATCH 003/569] [sign] Implement parsing from the DNS format There are probably lots of bugs in this implementation, I'll add some tests soon. --- src/sign/mod.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 18 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f4bac3c51..691edb5e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -14,6 +14,8 @@ use core::{fmt, str}; +use std::vec::Vec; + use crate::base::iana::SecAlg; pub mod key; @@ -114,25 +116,84 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } } } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(())?; + + if !data.trim_ascii().is_empty() { + // There were more fields following. + return Err(()); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf)? != N { + // The private key was of the wrong size. + return Err(()); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(())?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(())?; + + // Parse the algorithm. + let mut words = val.split_ascii_whitespace(); + let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; + let name = words.next().ok_or(())?; + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(()), + } + } } impl + AsMut<[u8]>> Drop for KeyPair { @@ -183,26 +244,87 @@ impl + AsMut<[u8]>> RsaKey { /// /// The output does not include an 'Algorithm' specifier. /// - /// See RFC 5702, section 6.2 for examples of this format. + /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus:\t")?; - base64(self.n.as_ref(), &mut *w)?; + base64_encode(self.n.as_ref(), &mut *w)?; w.write_str("\nPublicExponent:\t")?; - base64(self.e.as_ref(), &mut *w)?; + base64_encode(self.e.as_ref(), &mut *w)?; w.write_str("\nPrivateExponent:\t")?; - base64(self.d.as_ref(), &mut *w)?; + base64_encode(self.d.as_ref(), &mut *w)?; w.write_str("\nPrime1:\t")?; - base64(self.p.as_ref(), &mut *w)?; + base64_encode(self.p.as_ref(), &mut *w)?; w.write_str("\nPrime2:\t")?; - base64(self.q.as_ref(), &mut *w)?; + base64_encode(self.q.as_ref(), &mut *w)?; w.write_str("\nExponent1:\t")?; - base64(self.d_p.as_ref(), &mut *w)?; + base64_encode(self.d_p.as_ref(), &mut *w)?; w.write_str("\nExponent2:\t")?; - base64(self.d_q.as_ref(), &mut *w)?; + base64_encode(self.d_q.as_ref(), &mut *w)?; w.write_str("\nCoefficient:\t")?; - base64(self.q_i.as_ref(), &mut *w)?; + base64_encode(self.q_i.as_ref(), &mut *w)?; w.write_char('\n') } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(()), + }; + + if field.is_some() { + // This field has already been filled. + return Err(()); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(()); + } + } + + 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(), + }) + } } impl + AsMut<[u8]>> Drop for RsaKey { @@ -219,11 +341,26 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair(data: &str) -> Result, ()> { + // Trim any pending newlines. + let data = data.trim_ascii_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = line.split_once(':').ok_or(())?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) +} + /// A utility function to format data as Base64. /// /// This is a simple implementation with the only requirement of being /// constant-time and side-channel resistant. -fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { // Convert a single chunk of bytes into Base64. fn encode(data: [u8; 3]) -> [u8; 4] { let [a, b, c] = data; @@ -254,9 +391,9 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (b'0' - 52) as u32 - + (pluses & bcast) * (b'+' - 62) as u32 - + (slashs & bcast) * (b'/' - 63) as u32; + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; // Convert back into a byte array. chunk.to_be_bytes() @@ -281,9 +418,109 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { 0 => return Ok(()), 1 => chunk[2..].fill(b'='), 2 => chunk[3..].fill(b'='), - 3 => {} _ => unreachable!(), } let chunk = str::from_utf8(&chunk).unwrap(); w.write_str(chunk) } + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} From db51ae64be8bdb1c6df3798fc485c6331c9a89f2 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 16:01:04 +0200 Subject: [PATCH 004/569] [sign] Provide some error information Also fixes 'cargo clippy' issues, particularly with the MSRV. --- src/sign/mod.rs | 96 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 691edb5e3..d320f0249 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -63,7 +63,7 @@ pub trait Sign { /// /// This type cannot be used for computing signatures, as it does not implement /// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). pub enum KeyPair + AsMut<[u8]>> { /// An RSA/SHA256 keypair. @@ -116,22 +116,22 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } } } @@ -141,26 +141,28 @@ impl + AsMut<[u8]>> KeyPair { /// - For RSA, see RFC 5702, section 6. /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result + pub fn from_dns(data: &str) -> Result where B: From>, { /// Parse private keys for most algorithms (except RSA). - fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { // Extract the 'PrivateKey' field. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; - if !data.trim_ascii().is_empty() { + if !data.trim().is_empty() { // There were more fields following. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf)? != N { + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { // The private key was of the wrong size. - return Err(()); + return Err(DnsFormatError::Misformatted); } Ok(buf) @@ -169,17 +171,24 @@ impl + AsMut<[u8]>> KeyPair { // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(())?; + .ok_or(DnsFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; // Parse the algorithm. - let mut words = val.split_ascii_whitespace(); - let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; - let name = words.next().ok_or(())?; + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } match (code, name) { (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), @@ -191,7 +200,7 @@ impl + AsMut<[u8]>> KeyPair { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(()), + _ => Err(DnsFormatError::UnsupportedAlgorithm), } } } @@ -268,7 +277,7 @@ impl + AsMut<[u8]>> RsaKey { /// Parse a key from the conventional DNS format. /// /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result + pub fn from_dns(mut data: &str) -> Result where B: From>, { @@ -291,16 +300,17 @@ impl + AsMut<[u8]>> RsaKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(()), + _ => return Err(DnsFormatError::Misformatted), }; if field.is_some() { // This field has already been filled. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer)?; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; buffer.truncate(size); *field = Some(buffer.into()); @@ -310,7 +320,7 @@ impl + AsMut<[u8]>> RsaKey { for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(()); + return Err(DnsFormatError::Misformatted); } } @@ -342,18 +352,23 @@ impl + AsMut<[u8]>> Drop for RsaKey { } /// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair(data: &str) -> Result, ()> { +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + // Trim any pending newlines. - let data = data.trim_ascii_start(); + let data = data.trim_start(); // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); // Split the line by a colon. - let (key, val) = line.split_once(':').ok_or(())?; + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) + Ok(Some((key.trim(), val.trim(), rest))) } /// A utility function to format data as Base64. @@ -388,6 +403,7 @@ fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let slashs = pluses >> 7; // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 @@ -461,6 +477,7 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { } // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk - (uppers & bcast) * (b'A' - 0) as u32 - (lowers & bcast) * (b'a' - 26) as u32 @@ -524,3 +541,28 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { Ok(index) } + +/// An error in loading a [`KeyPair`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} From a5054155713731de3ba0dc844e6c60e70b81d209 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 4 Oct 2024 13:08:07 +0200 Subject: [PATCH 005/569] [sign] Move 'KeyPair' to 'generic::SecretKey' I'm going to add a corresponding 'PublicKey' type, at which point it becomes important to differentiate from the generic representations and actual cryptographic implementations. --- src/sign/generic.rs | 513 ++++++++++++++++++++++++++++++++++++++++++++ src/sign/mod.rs | 513 +------------------------------------------- 2 files changed, 514 insertions(+), 512 deletions(-) create mode 100644 src/sign/generic.rs diff --git a/src/sign/generic.rs b/src/sign/generic.rs new file mode 100644 index 000000000..420d84530 --- /dev/null +++ b/src/sign/generic.rs @@ -0,0 +1,513 @@ +use core::{fmt, str}; + +use std::vec::Vec; + +use crate::base::iana::SecAlg; + +/// A generic secret key. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Sign`] (if the underlying +/// cryptographic implementation supports it). +pub enum SecretKey + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> SecretKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64_encode(s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64_encode(s, &mut *w) + } + } + } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(DnsFormatError::Misformatted)?; + + if !data.trim().is_empty() { + // There were more fields following. + return Err(DnsFormatError::Misformatted); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { + // The private key was of the wrong size. + return Err(DnsFormatError::Misformatted); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(DnsFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(DnsFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(DnsFormatError::UnsupportedAlgorithm), + } + } +} + +impl + AsMut<[u8]>> Drop for SecretKey { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64_encode(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64_encode(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64_encode(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64_encode(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64_encode(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64_encode(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64_encode(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(DnsFormatError::Misformatted), + }; + + if field.is_some() { + // This field has already been filled. + return Err(DnsFormatError::Misformatted); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(DnsFormatError::Misformatted); + } + } + + 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(), + }) + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} + +/// An error in loading a [`SecretKey`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d320f0249..a649f7ab2 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -12,12 +12,9 @@ #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use core::{fmt, str}; - -use std::vec::Vec; - use crate::base::iana::SecAlg; +pub mod generic; pub mod key; //pub mod openssl; pub mod records; @@ -58,511 +55,3 @@ pub trait Sign { /// size. fn sign(&self, data: &[u8]) -> Result; } - -/// A generic keypair. -/// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -pub enum KeyPair + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), - - /// An ECDSA P-256/SHA-256 keypair. - /// - /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), - - /// An ECDSA P-384/SHA-384 keypair. - /// - /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), - - /// An Ed25519 keypair. - /// - /// The private key is a single 32-byte string. - Ed25519([u8; 32]), - - /// An Ed448 keypair. - /// - /// The private key is a single 57-byte string. - Ed448([u8; 57]), -} - -impl + AsMut<[u8]>> KeyPair { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } - - /// Serialize this key in the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - match self { - Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; - k.into_dns(w) - } - - Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) - } - - Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) - } - } - } - - /// Parse a key from the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { - /// Parse private keys for most algorithms (except RSA). - fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } - - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } - - Ok(buf) - } - - // The first line should specify the key format. - let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; - - // The second line should specify the algorithm. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; - - // Parse the algorithm. - let mut words = val.split_whitespace(); - let code = words - .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; - if words.next().is_some() { - return Err(DnsFormatError::Misformatted); - } - - match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), - (13, "(ECDSAP256SHA256)") => { - parse_pkey(data).map(Self::EcdsaP256Sha256) - } - (14, "(ECDSAP384SHA384)") => { - parse_pkey(data).map(Self::EcdsaP384Sha384) - } - (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), - (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), - } - } -} - -impl + AsMut<[u8]>> Drop for KeyPair { - 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), - } - } -} - -/// An RSA private key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, - - /// The private exponent. - pub d: B, - - /// The first prime factor of `d`. - pub p: B, - - /// The second prime factor of `d`. - pub q: B, - - /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, - - /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, - - /// The inverse of the second prime factor modulo the first. - pub q_i: B, -} - -impl + AsMut<[u8]>> RsaKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. - /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; - w.write_char('\n') - } - - /// Parse a key from the conventional DNS format. - /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { - let mut n = None; - let mut e = None; - let mut d = None; - let mut p = None; - let mut q = None; - let mut d_p = None; - let mut d_q = None; - let mut q_i = None; - - while let Some((key, val, rest)) = parse_dns_pair(data)? { - let field = match key { - "Modulus" => &mut n, - "PublicExponent" => &mut e, - "PrivateExponent" => &mut d, - "Prime1" => &mut p, - "Prime2" => &mut q, - "Exponent1" => &mut d_p, - "Exponent2" => &mut d_q, - "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), - }; - - if field.is_some() { - // This field has already been filled. - return Err(DnsFormatError::Misformatted); - } - - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) - .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); - - *field = Some(buffer.into()); - data = rest; - } - - for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { - if field.is_none() { - // A field was missing. - return Err(DnsFormatError::Misformatted); - } - } - - 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(), - }) - } -} - -impl + AsMut<[u8]>> Drop for RsaKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair( - data: &str, -) -> Result, DnsFormatError> { - // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. - - // Trim any pending newlines. - let data = data.trim_start(); - - // Get the first line (NOTE: CR LF is handled later). - let (line, rest) = data.split_once('\n').unwrap_or((data, "")); - - // Split the line by a colon. - let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; - - // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim(), val.trim(), rest))) -} - -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - -/// An error in loading a [`KeyPair`] from the conventional DNS format. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { - /// The key file uses an unsupported version of the format. - UnsupportedFormat, - - /// The key file did not follow the DNS format correctly. - Misformatted, - - /// The key file used an unsupported algorithm. - UnsupportedAlgorithm, -} - -impl fmt::Display for DnsFormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedFormat => "unsupported format", - Self::Misformatted => "misformatted key file", - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -impl std::error::Error for DnsFormatError {} From ea80694fc7bad838b7269668fb7fd7bfb65e6f45 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 15:29:45 +0200 Subject: [PATCH 006/569] [sign/generic] Add 'PublicKey' --- src/sign/generic.rs | 135 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 420d84530..7c9ffbea4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,8 +1,9 @@ -use core::{fmt, str}; +use core::{fmt, mem, str}; use std::vec::Vec; use crate::base::iana::SecAlg; +use crate::rdata::Dnskey; /// A generic secret key. /// @@ -12,7 +13,7 @@ use crate::base::iana::SecAlg; /// cryptographic implementation supports it). pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// @@ -136,7 +137,9 @@ impl + AsMut<[u8]>> SecretKey { } match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (8, "(RSASHA256)") => { + RsaSecretKey::from_dns(data).map(Self::RsaSha256) + } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) } @@ -163,11 +166,11 @@ impl + AsMut<[u8]>> Drop for SecretKey { } } -/// An RSA private key. +/// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { +pub struct RsaSecretKey + AsMut<[u8]>> { /// The public modulus. pub n: B, @@ -193,7 +196,7 @@ pub struct RsaKey + AsMut<[u8]>> { pub q_i: B, } -impl + AsMut<[u8]>> RsaKey { +impl + AsMut<[u8]>> RsaSecretKey { /// Serialize this key in the conventional DNS format. /// /// The output does not include an 'Algorithm' specifier. @@ -282,7 +285,7 @@ impl + AsMut<[u8]>> RsaKey { } } -impl + AsMut<[u8]>> Drop for RsaKey { +impl + AsMut<[u8]>> Drop for RsaSecretKey { fn drop(&mut self) { // Zero the bytes for each field. self.n.as_mut().fill(0u8); @@ -296,6 +299,124 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// A generic public key. +pub enum PublicKey> { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + // TODO: RSA/SHA-1 with NSEC3/SHA-1? + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256([u8; 65]), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384([u8; 97]), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519([u8; 32]), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448([u8; 57]), +} + +impl> PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Construct a DNSKEY record with the given flags. + pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + where + Octs: From> + AsRef<[u8]>, + { + let protocol = 3u8; + let algorithm = self.algorithm(); + let public_key = match self { + Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { + let (n, e) = (k.n.as_ref(), k.e.as_ref()); + let e_len_len = if e.len() < 256 { 1 } else { 3 }; + let len = e_len_len + e.len() + n.len(); + let mut buf = Vec::with_capacity(len); + if let Ok(e_len) = u8::try_from(e.len()) { + buf.push(e_len); + } else { + // RFC 3110 is not explicit about the endianness of this, + // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network + // byte order, which I suppose makes sense. + let e_len = u16::try_from(e.len()).unwrap(); + buf.extend_from_slice(&e_len.to_be_bytes()); + } + buf.extend_from_slice(e); + buf.extend_from_slice(n); + buf + } + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].to_vec(), + Self::EcdsaP384Sha384(k) => k[1..].to_vec(), + + Self::Ed25519(k) => k.to_vec(), + Self::Ed448(k) => k.to_vec(), + }; + + Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaPublicKey> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, +} + +impl From> for RsaPublicKey +where + B: AsRef<[u8]> + AsMut<[u8]> + Default, +{ + fn from(mut value: RsaSecretKey) -> Self { + Self { + n: mem::take(&mut value.n), + e: mem::take(&mut value.e), + } + } +} + /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, From 7c94006653d4f68413ec36111978b9b57e6d03d0 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 16:41:57 +0200 Subject: [PATCH 007/569] [sign] Rewrite the 'ring' module to use the 'Sign' trait Key generation, for now, will only be provided by the OpenSSL backend (coming soon). However, generic keys (for RSA/SHA-256 or Ed25519) can be imported into the Ring backend and used freely. --- src/sign/generic.rs | 4 +- src/sign/ring.rs | 180 ++++++++++++++++---------------------------- 2 files changed, 68 insertions(+), 116 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 7c9ffbea4..f963a8def 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -11,6 +11,8 @@ use crate::rdata::Dnskey; /// any cryptographic primitives. Instead, it is a generic representation that /// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). +/// +/// [`Sign`]: super::Sign pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. RsaSha256(RsaSecretKey), @@ -355,7 +357,7 @@ impl> PublicKey { } /// Construct a DNSKEY record with the given flags. - pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + pub fn into_dns(self, flags: u16) -> Dnskey where Octs: From> + AsRef<[u8]>, { diff --git a/src/sign/ring.rs b/src/sign/ring.rs index bf4614f2b..75660dfd6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,140 +1,90 @@ -//! Key and Signer using ring. +//! DNSSEC signing using `ring`. + #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] -use super::key::SigningKey; -use crate::base::iana::{DigestAlg, SecAlg}; -use crate::base::name::ToName; -use crate::base::rdata::ComposeRecordData; -use crate::rdata::{Dnskey, Ds}; -#[cfg(feature = "bytes")] -use bytes::Bytes; -use octseq::builder::infallible; -use ring::digest; -use ring::error::Unspecified; -use ring::rand::SecureRandom; -use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaEncoding, RsaKeyPair, - Signature as RingSignature, ECDSA_P256_SHA256_FIXED_SIGNING, -}; use std::vec::Vec; -pub struct Key<'a> { - dnskey: Dnskey>, - key: RingKey, - rng: &'a dyn SecureRandom, -} - -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), - Ed25519(Ed25519KeyPair), - Rsa(RsaKeyPair, &'static dyn RsaEncoding), -} - -impl<'a> Key<'a> { - pub fn throwaway_13( - flags: u16, - rng: &'a dyn SecureRandom, - ) -> Result { - let pkcs8 = EcdsaKeyPair::generate_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - rng, - )?; - let keypair = EcdsaKeyPair::from_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - pkcs8.as_ref(), - rng, - )?; - let public_key = keypair.public_key().as_ref()[1..].into(); - Ok(Key { - dnskey: Dnskey::new( - flags, - 3, - SecAlg::ECDSAP256SHA256, - public_key, - ) - .expect("long key"), - key: RingKey::Ecdsa(keypair), - rng, - }) - } -} +use crate::base::iana::SecAlg; -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +use super::generic; - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } +/// A key pair backed by `ring`. +pub enum KeyPair<'a> { + /// An RSA/SHA256 keypair. + RsaSha256 { + key: ring::signature::RsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - infallible(owner.compose_canonical(&mut buf)); - infallible(self.dnskey.compose_canonical_rdata(&mut buf)); - let digest = - Vec::from(digest::digest(&digest::SHA256, &buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::SHA256, - digest, - ) - .expect("long digest")) - } + /// An Ed25519 keypair. + Ed25519(ring::signature::Ed25519KeyPair), +} - fn sign(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) +impl<'a> KeyPair<'a> { + /// Use a generic keypair with `ring`. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + rng: &'a dyn ring::rand::SecureRandom, + ) -> Result { + match &key { + generic::SecretKey::RsaSha256(k) => { + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: k.n.as_ref(), + e: k.e.as_ref(), + }, + d: k.d.as_ref(), + p: k.p.as_ref(), + q: k.q.as_ref(), + dP: k.d_p.as_ref(), + dQ: k.d_q.as_ref(), + qInv: k.q_i.as_ref(), + }; + ring::signature::RsaKeyPair::from_components(&components) + .map_err(|_| ImportError::InvalidKey) + .map(|key| Self::RsaSha256 { key, rng }) } - RingKey::Ed25519(ref key) => Ok(Signature::sig(key.sign(msg))), - RingKey::Rsa(ref key, encoding) => { - let mut sig = vec![0; key.public().modulus_len()]; - key.sign(encoding, self.rng, msg, &mut sig)?; - Ok(Signature::vec(sig)) + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + let k = k.as_ref(); + ring::signature::Ed25519KeyPair::from_seed_unchecked(k) + .map_err(|_| ImportError::InvalidKey) + .map(Self::Ed25519) } + _ => Err(ImportError::UnsupportedAlgorithm), } } } -pub struct Signature(SignatureInner); +/// An error in importing a key into `ring`. +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), + /// The provided keypair was invalid. + InvalidKey, } -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) - } - - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) - } -} +impl<'a> super::Sign> for KeyPair<'a> { + type Error = ring::error::Unspecified; -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self.0 { - SignatureInner::Sig(ref sig) => sig.as_ref(), - SignatureInner::Vec(ref vec) => vec.as_slice(), + fn algorithm(&self) -> SecAlg { + match self { + KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, + KeyPair::Ed25519(_) => SecAlg::ED25519, } } -} -#[cfg(feature = "bytes")] -impl From for Bytes { - fn from(sig: Signature) -> Self { - match sig.0 { - SignatureInner::Sig(sig) => Bytes::copy_from_slice(sig.as_ref()), - SignatureInner::Vec(sig) => Bytes::from(sig), + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + match self { + KeyPair::RsaSha256 { key, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, *rng, data, &mut buf)?; + Ok(buf) + } + KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From f9564c1d9f05b1829ea1e3209cb1aa8ce5f958b8 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:36:23 +0200 Subject: [PATCH 008/569] Implement DNSSEC signing with OpenSSL The OpenSSL backend supports import from and export to generic secret keys, making the formatting and parsing machinery for them usable. The next step is to implement generation of keys. --- Cargo.lock | 66 +++++++++++++++++ Cargo.toml | 2 + src/sign/mod.rs | 2 +- src/sign/openssl.rs | 167 ++++++++++++++++++++++++++++++++------------ src/sign/ring.rs | 16 ++--- 5 files changed, 200 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1aa725be1..43d1949d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -228,6 +228,7 @@ dependencies = [ "mock_instant", "moka", "octseq", + "openssl", "parking_lot", "proc-macro2", "rand", @@ -280,6 +281,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.30" @@ -631,6 +647,44 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -698,6 +752,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1323,6 +1383,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 82108c642..b42bbf0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } +openssl = { version = "0.10", optional = true } 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 } @@ -48,6 +49,7 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] +openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] diff --git a/src/sign/mod.rs b/src/sign/mod.rs index a649f7ab2..b1db46c26 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -16,7 +16,7 @@ use crate::base::iana::SecAlg; pub mod generic; pub mod key; -//pub mod openssl; +pub mod openssl; pub mod records; pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c49512b73..e62c9dcbb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,58 +1,137 @@ //! Key and Signer using OpenSSL. + #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] +use core::fmt; use std::vec::Vec; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::sha::sha256; -use openssl::sign::Signer as OpenSslSigner; -use unwrap::unwrap; -use crate::base::iana::DigestAlg; -use crate::base::name::ToDname; -use crate::base::octets::Compose; -use crate::rdata::{Ds, Dnskey}; -use super::key::SigningKey; - - -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, + +use openssl::{ + bn::BigNum, + pkey::{self, PKey, Private}, +}; + +use crate::base::iana::SecAlg; + +use super::generic; + +/// A key pair backed by OpenSSL. +pub struct SecretKey { + /// The algorithm used by the key. + algorithm: SecAlg, + + /// The private key. + pkey: PKey, } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +impl SecretKey { + /// Use a generic secret key with OpenSSL. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + ) -> Result { + fn num(slice: &[u8]) -> BigNum { + let mut v = BigNum::new_secure().unwrap(); + v.copy_from_slice(slice).unwrap(); + v + } - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } + let pkey = match &key { + generic::SecretKey::RsaSha256(k) => { + let n = BigNum::from_slice(k.n.as_ref()).unwrap(); + let e = BigNum::from_slice(k.e.as_ref()).unwrap(); + let d = num(k.d.as_ref()); + let p = num(k.p.as_ref()); + let q = num(k.q.as_ref()); + let d_p = num(k.d_p.as_ref()); + let d_q = num(k.d_q.as_ref()); + let q_i = num(k.q_i.as_ref()); - fn ds( - &self, - owner: N - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - unwrap!(owner.compose_canonical(&mut buf)); - unwrap!(self.dnskey.compose_canonical(&mut buf)); - let digest = Vec::from(sha256(&buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::Sha256, - digest, - )) + // NOTE: The 'openssl' crate doesn't seem to expose + // 'EVP_PKEY_fromdata', which could be used to replace the + // deprecated methods called here. + + openssl::rsa::Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + ) + .and_then(PKey::from_rsa) + .unwrap() + } + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + PKey::private_key_from_raw_bytes( + k.as_ref(), + pkey::Id::ED25519, + ) + .unwrap() + } + generic::SecretKey::Ed448(k) => { + PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) + .unwrap() + } + _ => return Err(ImportError::UnsupportedAlgorithm), + }; + + Ok(Self { + algorithm: key.algorithm(), + pkey, + }) } - fn sign(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + /// Export this key into a generic secret key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export(self) -> generic::SecretKey + where + B: AsRef<[u8]> + AsMut<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::SecretKey::RsaSha256(generic::RsaSecretKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } } } +/// An error in importing a key into OpenSSL. +#[derive(Clone, Debug)] +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided secret key was invalid. + InvalidKey, +} + +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 75660dfd6..872f8dadb 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,8 +10,8 @@ use crate::base::iana::SecAlg; use super::generic; /// A key pair backed by `ring`. -pub enum KeyPair<'a> { - /// An RSA/SHA256 keypair. +pub enum SecretKey<'a> { + /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, rng: &'a dyn ring::rand::SecureRandom, @@ -21,7 +21,7 @@ pub enum KeyPair<'a> { Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> KeyPair<'a> { +impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn import + AsMut<[u8]>>( key: generic::SecretKey, @@ -66,25 +66,25 @@ pub enum ImportError { InvalidKey, } -impl<'a> super::Sign> for KeyPair<'a> { +impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; fn algorithm(&self) -> SecAlg { match self { - KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, - KeyPair::Ed25519(_) => SecAlg::ED25519, + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::Ed25519(_) => SecAlg::ED25519, } } fn sign(&self, data: &[u8]) -> Result, Self::Error> { match self { - KeyPair::RsaSha256 { key, rng } => { + Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; key.sign(pad, *rng, data, &mut buf)?; Ok(buf) } - KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), + Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From c705428209212628225d6db28770364e3b4778e9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:57:33 +0200 Subject: [PATCH 009/569] [sign/openssl] Implement key generation --- src/sign/openssl.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e62c9dcbb..9d208737c 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -117,6 +117,27 @@ impl SecretKey { } } +/// Generate a new secret key for the given algorithm. +/// +/// If the algorithm is not supported, [`None`] is returned. +/// +/// # Panics +/// +/// Panics if OpenSSL fails or if memory could not be allocated. +pub fn generate(algorithm: SecAlg) -> Option { + let pkey = match algorithm { + // We generate 3072-bit keys for an estimated 128 bits of security. + SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) + .and_then(PKey::from_rsa) + .unwrap(), + SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), + SecAlg::ED448 => PKey::generate_ed448().unwrap(), + _ => return None, + }; + + Some(SecretKey { algorithm, pkey }) +} + /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] pub enum ImportError { @@ -135,3 +156,5 @@ impl fmt::Display for ImportError { }) } } + +impl std::error::Error for ImportError {} From 68476e781d4e442252644a1e74bcb021c5e9c879 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:08:06 +0200 Subject: [PATCH 010/569] [sign/openssl] Test key generation and import/export --- src/sign/openssl.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9d208737c..13c1f7808 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -86,7 +86,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(self) -> generic::SecretKey + pub fn export(&self) -> generic::SecretKey where B: AsRef<[u8]> + AsMut<[u8]> + From>, { @@ -158,3 +158,30 @@ impl fmt::Display for ImportError { } impl std::error::Error for ImportError {} + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{base::iana::SecAlg, sign::generic}; + + const ALGORITHMS: &[SecAlg] = + &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + + #[test] + fn generate_all() { + for &algorithm in ALGORITHMS { + let _ = super::generate(algorithm).unwrap(); + } + } + + #[test] + fn export_and_import() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let exp: generic::SecretKey> = key.export(); + let imp = super::SecretKey::import(exp).unwrap(); + assert!(key.pkey.public_eq(&imp.pkey)); + } + } +} From b68b639482584a38c1f31b12418fa3f8bfbfe8b1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:39:45 +0200 Subject: [PATCH 011/569] [sign/openssl] Add support for ECDSA --- src/sign/openssl.rs | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 13c1f7808..d35f45850 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -60,7 +60,32 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - // TODO: Support ECDSA. + generic::SecretKey::EcdsaP256Sha256(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } + generic::SecretKey::EcdsaP384Sha384(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::SECP384R1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } generic::SecretKey::Ed25519(k) => { PKey::private_key_from_raw_bytes( k.as_ref(), @@ -72,7 +97,6 @@ impl SecretKey { PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) .unwrap() } - _ => return Err(ImportError::UnsupportedAlgorithm), }; Ok(Self { @@ -90,6 +114,7 @@ impl SecretKey { where B: AsRef<[u8]> + AsMut<[u8]> + From>, { + // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); @@ -104,6 +129,16 @@ impl SecretKey { q_i: key.iqmp().unwrap().to_vec().into(), }) } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) + } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); generic::SecretKey::Ed25519(key.try_into().unwrap()) @@ -130,6 +165,20 @@ pub fn generate(algorithm: SecAlg) -> Option { SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) .and_then(PKey::from_rsa) .unwrap(), + SecAlg::ECDSAP256SHA256 => { + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } + SecAlg::ECDSAP384SHA384 => { + let group = openssl::nid::Nid::SECP384R1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), SecAlg::ED448 => PKey::generate_ed448().unwrap(), _ => return None, @@ -165,8 +214,13 @@ mod tests { use crate::{base::iana::SecAlg, sign::generic}; - const ALGORITHMS: &[SecAlg] = - &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + const ALGORITHMS: &[SecAlg] = &[ + SecAlg::RSASHA256, + SecAlg::ECDSAP256SHA256, + SecAlg::ECDSAP384SHA384, + SecAlg::ED25519, + SecAlg::ED448, + ]; #[test] fn generate_all() { From 79b2a083877d570fa83310d1ea230f24317ac150 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:41:36 +0200 Subject: [PATCH 012/569] [sign/openssl] satisfy clippy --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index d35f45850..1211d6225 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -67,7 +67,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) @@ -80,7 +80,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) From 1aeeede51a7176ddc9ca72ffff27711907d5758a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:57:33 +0200 Subject: [PATCH 013/569] [sign/openssl] Implement the 'Sign' trait --- src/sign/openssl.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 1211d6225..663e8a904 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -13,7 +13,7 @@ use openssl::{ use crate::base::iana::SecAlg; -use super::generic; +use super::{generic, Sign}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -152,6 +152,36 @@ impl SecretKey { } } +impl Sign> for SecretKey { + type Error = openssl::error::ErrorStack; + + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + let mut signer = match self.algorithm { + SecAlg::RSASHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP256SHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP384SHA384 => { + Signer::new(MessageDigest::sha384(), &self.pkey)? + } + SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, + SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, + _ => unreachable!(), + }; + + signer.sign_oneshot_to_vec(data) + } +} + /// Generate a new secret key for the given algorithm. /// /// If the algorithm is not supported, [`None`] is returned. From 90af63dba2e2b8a46af8495cb162407d001ce243 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:24:02 +0200 Subject: [PATCH 014/569] Install OpenSSL in CI builds --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224b..99a36d6cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,20 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install libssl-dev + echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + - if: matrix.os == 'windows-latest' + run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features --all-targets -- -D warnings + run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features --all-targets - - run: cargo test --all-features + - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets + - run: cargo test $OPENSSL_FLAVOR --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest @@ -37,6 +43,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.68.2" + - name: Install OpenSSL + run: sudo apt install libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions From 6370035030b646f07a5080b4bd31c19503c0bb31 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:28 +0200 Subject: [PATCH 015/569] Ensure 'openssl' dep supports 3.x.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index b42bbf0c7..881230157 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10", optional = true } +openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x 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 } From d53f85acf4ed5dac40ce06760e84551c670ef242 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:52 +0200 Subject: [PATCH 016/569] [workflows/ci] Use 'vcpkg' instead of vendoring OpenSSL --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99a36d6cc..18a8bdb13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,22 @@ jobs: with: rust-version: ${{ matrix.rust }} - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt install libssl-dev - echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' - run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: x64-windows-release + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets - - run: cargo test $OPENSSL_FLAVOR --all-features + - run: cargo check --no-default-features --all-targets + - run: cargo test --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest From 5148bd31d76d4a78611cd0149cb7d19e00d3e624 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:55:18 +0200 Subject: [PATCH 017/569] Ensure 'openssl' dep exposes necessary interfaces --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 881230157..899be5378 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x +openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions 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 } From 13bebd74d3126fdd5d0ba2451bf5346b5f8e10ec Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:03:14 +0200 Subject: [PATCH 018/569] [workflows/ci] Record location of 'vcpkg' --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18a8bdb13..362b3e146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true + - if: matrix.os == 'windows-latest' + run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From c86f2341f17f17c2668e22a6c42da232bcdecea3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:13:22 +0200 Subject: [PATCH 019/569] [workflows/ci] Use a YAML def for 'VCPKG_ROOT' --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362b3e146..514844da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" steps: - name: Checkout repository uses: actions/checkout@v1 @@ -26,8 +27,6 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true - - if: matrix.os == 'windows-latest' - run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 8939603f71c5179a0a96e90a800bf7feb50be196 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:16 +0200 Subject: [PATCH 020/569] [workflows/ci] Fix a vcpkg triplet to use --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 514844da8..12334fa51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: env: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release steps: - name: Checkout repository uses: actions/checkout@v1 @@ -24,7 +25,7 @@ jobs: uses: johnwason/vcpkg-action@v6 with: pkgs: openssl - triplet: x64-windows-release + triplet: ${{ env.VCPKGRS_TRIPLET }} token: ${{ github.token }} github-binarycache: true - if: matrix.rust == 'stable' From 9ed1f44592846e64ebb9d6b3719719dd4e6cf701 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:43 +0200 Subject: [PATCH 021/569] Upgrade openssl to 0.10.57 for bitflags 2.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 899be5378..bfc47fce4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions +openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x 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 } From 24b443c3365d9c401b121ce0421b26649aa73b6f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:22:18 +0200 Subject: [PATCH 022/569] [workflows/ci] Use dynamic linking for vcpkg openssl --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12334fa51..23c73a5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 From d3a071df03158880f6279196bd8c0d405928ba03 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:24:05 +0200 Subject: [PATCH 023/569] [workflows/ci] Correctly annotate 'vcpkg' --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23c73a5ee..299da6658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - if: matrix.os == 'ubuntu-latest' run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' + id: vcpkg uses: johnwason/vcpkg-action@v6 with: pkgs: openssl From 669da9306f1d613163aa69f97005848286a4c0bf Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:51:14 +0200 Subject: [PATCH 024/569] [sign/openssl] Implement exporting public keys --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 663e8a904..0147222f6 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -150,6 +150,55 @@ impl SecretKey { _ => unreachable!(), } } + + /// Export this key into a generic public key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } + } } impl Sign> for SecretKey { @@ -268,4 +317,12 @@ mod tests { assert!(key.pkey.public_eq(&imp.pkey)); } } + + #[test] + fn export_public() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let _: generic::PublicKey> = key.export_public(); + } + } } From 8a0c59a55d1bbbcb0b9f3915690688ac7ffe76d8 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:56:16 +0200 Subject: [PATCH 025/569] [sign/ring] Implement exporting public keys --- src/sign/ring.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 872f8dadb..185b97295 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -55,6 +55,28 @@ impl<'a> SecretKey<'a> { _ => Err(ImportError::UnsupportedAlgorithm), } } + + /// Export this key into a generic public key. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + Self::Ed25519(key) => { + use ring::signature::KeyPair; + let key = key.public_key().as_ref(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } } /// An error in importing a key into `ring`. From 7c6cde1f35def92329d44243a271495652069382 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 19:39:34 +0200 Subject: [PATCH 026/569] [sign/generic] Test (de)serialization for generic secret keys There were bugs in the Base64 encoding/decoding that are not worth trying to debug; there's a perfectly usable Base64 implementation in the crate already. --- src/sign/generic.rs | 272 +++++------------- test-data/dnssec-keys/Ktest.+008+55993.key | 1 + .../dnssec-keys/Ktest.+008+55993.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 + .../dnssec-keys/Ktest.+013+40436.private | 3 + test-data/dnssec-keys/Ktest.+014+17013.key | 1 + .../dnssec-keys/Ktest.+014+17013.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 + .../dnssec-keys/Ktest.+015+43769.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 + .../dnssec-keys/Ktest.+016+34114.private | 3 + 11 files changed, 100 insertions(+), 199 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.private create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f963a8def..01505239d 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -4,6 +4,7 @@ use std::vec::Vec; use crate::base::iana::SecAlg; use crate::rdata::Dnskey; +use crate::utils::base64; /// A generic secret key. /// @@ -56,6 +57,7 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Private-key-format: v1.2\n")?; match self { Self::RsaSha256(k) => { w.write_str("Algorithm: 8 (RSASHA256)\n")?; @@ -64,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } } } @@ -107,11 +109,12 @@ impl + AsMut<[u8]>> SecretKey { return Err(DnsFormatError::Misformatted); } - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } + let buf: Vec = base64::decode(val) + .map_err(|_| DnsFormatError::Misformatted)?; + let buf = buf + .as_slice() + .try_into() + .map_err(|_| DnsFormatError::Misformatted)?; Ok(buf) } @@ -205,22 +208,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_str("Modulus: ")?; + write!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("\nPublicExponent: ")?; + write!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("\nPrivateExponent: ")?; + write!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("\nPrime1: ")?; + write!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("\nPrime2: ")?; + write!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("\nExponent1: ")?; + write!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("\nExponent2: ")?; + write!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("\nCoefficient: ")?; + write!(w, "{}", base64::encode_display(&self.q_i))?; w.write_char('\n') } @@ -258,10 +261,8 @@ impl + AsMut<[u8]>> RsaSecretKey { return Err(DnsFormatError::Misformatted); } - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) + let buffer: Vec = base64::decode(val) .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); *field = Some(buffer.into()); data = rest; @@ -428,6 +429,11 @@ fn parse_dns_pair( // Trim any pending newlines. let data = data.trim_start(); + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); @@ -439,177 +445,6 @@ fn parse_dns_pair( Ok(Some((key.trim(), val.trim(), rest))) } -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DnsFormatError { @@ -634,3 +469,42 @@ impl fmt::Display for DnsFormatError { } impl std::error::Error for DnsFormatError {} + +#[cfg(test)] +mod tests { + use std::{string::String, vec::Vec}; + + use crate::base::iana::SecAlg; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 55993), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + let mut same = String::new(); + key.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key new file mode 100644 index 000000000..8248fbfe8 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private new file mode 100644 index 000000000..7a260e7a0 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= +PublicExponent: AQAB +PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= +Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== +Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== +Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== +Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== +Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key new file mode 100644 index 000000000..7f7cd0fcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+40436.private new file mode 100644 index 000000000..39f5e8a8d --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key new file mode 100644 index 000000000..c7b6aa1d4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private new file mode 100644 index 000000000..9648a876a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key new file mode 100644 index 000000000..8a1f24f67 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private new file mode 100644 index 000000000..e178a3bd4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key new file mode 100644 index 000000000..fc77e0491 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private new file mode 100644 index 000000000..fca7303dc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From d6a5313ab6373ad2da7f3fe2a7549cec9d9eec7f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:03:03 +0200 Subject: [PATCH 027/569] [sign] Thoroughly test import/export in both backends I had to swap out the RSA key since 'ring' found it to be too small. --- src/sign/generic.rs | 2 +- src/sign/openssl.rs | 73 +++++++++++++++---- src/sign/ring.rs | 57 +++++++++++++++ test-data/dnssec-keys/Ktest.+008+27096.key | 1 + .../dnssec-keys/Ktest.+008+27096.private | 10 +++ test-data/dnssec-keys/Ktest.+008+55993.key | 1 - .../dnssec-keys/Ktest.+008+55993.private | 10 --- 7 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 01505239d..5626e6ce9 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -477,7 +477,7 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 55993), + (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), (SecAlg::ECDSAP384SHA384, 17013), (SecAlg::ED25519, 43769), diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 0147222f6..9154abd55 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -289,28 +289,32 @@ impl std::error::Error for ImportError {} #[cfg(test)] mod tests { - use std::vec::Vec; + use std::{string::String, vec::Vec}; - use crate::{base::iana::SecAlg, sign::generic}; + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; - const ALGORITHMS: &[SecAlg] = &[ - SecAlg::RSASHA256, - SecAlg::ECDSAP256SHA256, - SecAlg::ECDSAP384SHA384, - SecAlg::ED25519, - SecAlg::ED448, + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), ]; #[test] - fn generate_all() { - for &algorithm in ALGORITHMS { + fn generate() { + for &(algorithm, _) in KEYS { let _ = super::generate(algorithm).unwrap(); } } #[test] - fn export_and_import() { - for &algorithm in ALGORITHMS { + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let exp: generic::SecretKey> = key.export(); let imp = super::SecretKey::import(exp).unwrap(); @@ -318,11 +322,50 @@ mod tests { } } + #[test] + fn imported_roundtrip() { + type GenericKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let imp = GenericKey::from_dns(&data).unwrap(); + let key = super::SecretKey::import(imp).unwrap(); + let exp: GenericKey = key.export(); + let mut same = String::new(); + exp.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } + #[test] fn export_public() { - for &algorithm in ALGORITHMS { - let key = super::generate(algorithm).unwrap(); - let _: generic::PublicKey> = key.export_public(); + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 185b97295..edea8ae14 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -3,6 +3,7 @@ #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] +use core::fmt; use std::vec::Vec; use crate::base::iana::SecAlg; @@ -42,6 +43,7 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) + .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } @@ -80,6 +82,7 @@ impl<'a> SecretKey<'a> { } /// An error in importing a key into `ring`. +#[derive(Clone, Debug)] pub enum ImportError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -88,6 +91,15 @@ pub enum ImportError { InvalidKey, } +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} + impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; @@ -110,3 +122,48 @@ impl<'a> super::Sign> for SecretKey<'a> { } } } + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; + + const KEYS: &[(SecAlg, u16)] = + &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + + #[test] + fn export_public() { + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key new file mode 100644 index 000000000..5aa614f71 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private new file mode 100644 index 000000000..b5819714f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 +PublicExponent: AQAB +PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN +Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj +Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd +Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf +Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 +Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key deleted file mode 100644 index 8248fbfe8..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private deleted file mode 100644 index 7a260e7a0..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= -PublicExponent: AQAB -PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= -Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== -Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== -Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== -Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== -Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== From 8321bbfb43cc5476121b767705911ac2b5f6abca Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:06:58 +0200 Subject: [PATCH 028/569] [sign] Remove debugging code and satisfy clippy --- src/sign/generic.rs | 8 ++++---- src/sign/ring.rs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 5626e6ce9..8dd610637 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -66,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index edea8ae14..864480933 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -43,7 +43,6 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) - .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } From db6820ed93ebec1320cd1e7229dfd81286e53ef1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:20:15 +0200 Subject: [PATCH 029/569] [sign] Account for CR LF in tests --- src/sign/generic.rs | 46 +++++++++++++++++++++++---------------------- src/sign/openssl.rs | 2 ++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8dd610637..8ad44ea88 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -57,30 +57,30 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Private-key-format: v1.2\n")?; + writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; + writeln!(w, "Algorithm: 8 (RSASHA256)")?; k.into_dns(w) } Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; + writeln!(w, "Algorithm: 15 (ED25519)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; + writeln!(w, "Algorithm: 16 (ED448)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } @@ -209,22 +209,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; - write!(w, "{}", base64::encode_display(&self.n))?; - w.write_str("\nPublicExponent: ")?; - write!(w, "{}", base64::encode_display(&self.e))?; - w.write_str("\nPrivateExponent: ")?; - write!(w, "{}", base64::encode_display(&self.d))?; - w.write_str("\nPrime1: ")?; - write!(w, "{}", base64::encode_display(&self.p))?; - w.write_str("\nPrime2: ")?; - write!(w, "{}", base64::encode_display(&self.q))?; - w.write_str("\nExponent1: ")?; - write!(w, "{}", base64::encode_display(&self.d_p))?; - w.write_str("\nExponent2: ")?; - write!(w, "{}", base64::encode_display(&self.d_q))?; - w.write_str("\nCoefficient: ")?; - write!(w, "{}", base64::encode_display(&self.q_i))?; - w.write_char('\n') + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i))?; + Ok(()) } /// Parse a key from the conventional DNS format. @@ -504,6 +504,8 @@ mod tests { let key = super::SecretKey::>::from_dns(&data).unwrap(); let mut same = String::new(); key.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9154abd55..2377dc250 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -335,6 +335,8 @@ mod tests { let exp: GenericKey = key.export(); let mut same = String::new(); exp.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } From e7f9709f6095dd5939a2cd07044f977690ed2a35 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 11 Oct 2024 16:16:12 +0200 Subject: [PATCH 030/569] [sign/openssl] Fix bugs in the signing procedure - RSA signatures were being made with an unspecified padding scheme. - ECDSA signatures were being output in ASN.1 DER format, instead of the fixed-size format required by DNSSEC (and output by 'ring'). - Tests for signature failures are now added for both backends. --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++-------- src/sign/ring.rs | 19 ++++++++++++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 2377dc250..8faa48f9e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -8,6 +8,7 @@ use std::vec::Vec; use openssl::{ bn::BigNum, + ecdsa::EcdsaSig, pkey::{self, PKey, Private}, }; @@ -212,22 +213,42 @@ impl Sign> for SecretKey { use openssl::hash::MessageDigest; use openssl::sign::Signer; - let mut signer = match self.algorithm { + match self.algorithm { SecAlg::RSASHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) } SecAlg::ECDSAP256SHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(32).unwrap(); + let s = signature.s().to_vec_padded(32).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) } SecAlg::ECDSAP384SHA384 => { - Signer::new(MessageDigest::sha384(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(48).unwrap(); + let s = signature.s().to_vec_padded(48).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) + } + SecAlg::ED25519 | SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) } - SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, - SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, _ => unreachable!(), - }; - - signer.sign_oneshot_to_vec(data) + } } } @@ -294,7 +315,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = &[ @@ -370,4 +391,20 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 864480933..0996552f6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -129,7 +129,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = @@ -165,4 +165,21 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } From 2663093854216dca66517932382808d2035fbae4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:04:46 +0200 Subject: [PATCH 031/569] Initial NSEC3 generation support. Lacks collision detection and tests. --- src/rdata/nsec3.rs | 37 +++++ src/sign/records.rs | 362 ++++++++++++++++++++++++++++++++++++++++-- src/sign/ring.rs | 125 ++++++++++++++- src/validator/nsec.rs | 75 ++------- 4 files changed, 529 insertions(+), 70 deletions(-) diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index 858af6720..aaf11986f 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -100,6 +100,10 @@ impl Nsec3 { &self.next_owner } + pub fn set_next_owner(&mut self, next_owner: OwnerHash) { + self.next_owner = next_owner; + } + pub fn types(&self) -> &RtypeBitmap { &self.types } @@ -428,6 +432,10 @@ impl Nsec3param { &self.salt } + pub fn into_salt(self) -> Nsec3Salt { + self.salt + } + pub(super) fn convert_octets( self, ) -> Result, Target::Error> @@ -471,6 +479,35 @@ impl Nsec3param { } } +//--- Default + +impl Default for Nsec3param +where + Octs: From<&'static [u8]>, +{ + /// Best practice default values for NSEC3 hashing. + /// + /// Per [RFC 9276] section 3.1: + /// + /// - _SHA-1, no extra iterations, empty salt._ + /// + /// Per [RFC 5155] section 4.1.2: + /// + /// - _The Opt-Out flag is not used and is set to zero._ + /// - _All other flags are reserved for future use, and must be zero._ + /// + /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html + /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html + fn default() -> Self { + Self { + hash_algorithm: Nsec3HashAlg::SHA1, + flags: 0, + iterations: 0, + salt: Nsec3Salt::empty(), + } + } +} + //--- OctetsFrom impl OctetsFrom> for Nsec3param diff --git a/src/sign/records.rs b/src/sign/records.rs index e8507a55c..97693447b 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,17 +1,30 @@ //! Actual signing. +use core::convert::From; +use core::fmt::Display; + +use std::fmt::Debug; +use std::string::String; +use std::vec::Vec; +use std::{fmt, io, slice}; + +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; +use octseq::{FreezeBuilder, OctetsFrom}; -use super::key::SigningKey; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, Rtype}; -use crate::base::name::ToName; +use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; +use crate::base::name::{ToLabelIter, ToName}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; -use crate::base::Ttl; -use crate::rdata::dnssec::{ProtoRrsig, RtypeBitmap, Timestamp}; -use crate::rdata::{Dnskey, Ds, Nsec, Rrsig}; -use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use std::vec::Vec; -use std::{fmt, io, slice}; +use crate::base::{Name, NameBuilder, Ttl}; +use crate::rdata::dnssec::{ + ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, +}; +use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; +use crate::rdata::{Dnskey, Ds, Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::utils::base32; + +use super::key::SigningKey; +use super::ring::{nsec3_hash, Nsec3HashError}; //------------ SortedRecords ------------------------------------------------- @@ -243,6 +256,239 @@ impl SortedRecords { res } + /// Generate [RFC5155] NSEC3 and NSEC3PARAM records for this record set. + /// + /// This function does NOT enforce use of current best practice settings, + /// as defined by [RFC 5155], [RFC 9077] and [RFC 9276] which state that: + /// + /// - The `ttl` should be the _"lesser of the MINIMUM field of the zone + /// SOA RR and the TTL of the zone SOA RR itself"_. + /// + /// - The `params` should be set to _"SHA-1, no extra iterations, empty + /// salt"_ and zero flags. See `Nsec3param::default()`. + /// + /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html + /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html + /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html + pub fn nsec3s( + &self, + apex: &FamilyName, + ttl: Ttl, + params: Nsec3param, + opt_out: bool, + ) -> Result, Nsec3HashError> + where + N: ToName + Clone + From> + Display, + N: From::Octets>>, + D: RecordData, + Octets: FromBuilder + OctetsFrom> + Clone + Default, + Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, + ::AppendError: Debug, + OctetsMut: OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder, + { + // TODO: + // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) + // - RFC 5155 section 2 Backwards compatibility: + // Reject old algorithms? if not, map 3 to 6 and 5 to 7, or reject + // use of 3 and 5? + + // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." + // We store the NSEC3s as we create them in a self-sorting vec. + let mut nsec3s = SortedRecords::new(); + + // The owner name of a zone cut if we currently are at or below one. + let mut cut: Option> = None; + + let mut families = self.families(); + + // Since the records are ordered, the first family is the apex -- + // we can skip everything before that. + families.skip_before(apex); + + // We also need the apex for the last NSEC. + let apex_owner = families.first_owner().clone(); + let apex_label_count = apex_owner.iter_labels().count(); + + for family in families { + // If the owner is out of zone, we have moved out of our zone and + // are done. + if !family.is_in_zone(apex) { + break; + } + + // If the family is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the + // family name for later. This also means below that if + // `cut.is_some()` we are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + Some(name.clone()) + } else { + None + }; + + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, owner names of unsigned + // delegations MAY be excluded." + let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); + if cut.is_some() && !has_ds && opt_out { + continue; + } + + // RFC 5155 7.1 step 4: + // "If the difference in number of labels between the apex and + // 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 distance_to_root = name.owner().iter_labels().count(); + let distance_to_apex = distance_to_root - apex_label_count; + if distance_to_apex > 1 { + // 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 + // then non_empty_label_count would be equal to label_distance. + // If it is less that means there are ENTs between us and the + // last non-empty label in our ancestor path to the apex. + + // Walk from the owner name down the tree of labels from the + // last known non-empty non-terminal label, extending the name + // each time by one label until we get to the current name. + + // Given a.b.c.mail.example.com where: + // - example.com is the apex owner + // - mail.example.com was the last non-empty non-terminal + // This loop will construct the names: + // - c.mail.example.com + // - b.c.mail.example.com + // 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 rev_label_it = name.owner().iter_labels().skip(n); + + // Create next longest ENT name. + let mut builder = NameBuilder::::new(); + for label in rev_label_it.take(distance_to_apex - n) { + builder.append_label(label.as_slice()).unwrap(); + } + 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(), + params.flags(), + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + // Store the record by order of its owner name. + let _ = nsec3s.insert(rec); + } + } + + // Create the type bitmap, assume there will be an RRSIG and an + // NSEC3PARAM. + let mut bitmap = RtypeBitmap::::builder(); + + // Authoritative RRsets will be signed. + if cut.is_none() || has_ds { + bitmap.add(Rtype::RRSIG).unwrap(); + } + + // RFC 5155 7.1 step 3: + // "For each RRSet at the original owner name, set the + // corresponding bit in the Type Bit Maps field." + for rrset in family.rrsets() { + bitmap.add(rrset.rtype()).unwrap(); + } + + if distance_to_apex == 0 { + bitmap.add(Rtype::NSEC3PARAM).unwrap(); + } + + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, set the Opt-Out bit to one." + let mut nsec3_flags = params.flags(); + if opt_out { + // Set the Opt-Out flag. + nsec3_flags |= 0b0000_0001; + } + + let rec = Self::mk_nsec3( + name.owner(), + params.hash_algorithm(), + nsec3_flags, + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + let _ = nsec3s.insert(rec); + } + + // RFC 5155 7.1 step 7: + // "In each NSEC3 RR, insert the next hashed owner name by using the + // value of the next NSEC3 RR in hash order. The next hashed owner + // name of the last NSEC3 RR in the zone contains the value of the + // hashed owner name of the first NSEC3 RR in the hash order." + for i in 1..=nsec3s.records.len() { + let next_i = if i == nsec3s.records.len() { 0 } else { i }; + let cur_owner = nsec3s.records[next_i].owner(); + let name: Name = cur_owner.try_to_name().unwrap(); + let label = name.iter_labels().next().unwrap(); + let owner_hash = if let Ok(hash_octets) = + base32::decode_hex(&format!("{label}")) + { + OwnerHash::::from_octets(hash_octets).unwrap() + } else { + OwnerHash::::from_octets(name.as_octets().clone()) + .unwrap() + }; + let last_rec = &mut nsec3s.records[i - 1]; + let last_nsec3: &mut Nsec3 = last_rec.data_mut(); + last_nsec3.set_next_owner(owner_hash.clone()); + } + + // RFC 5155 7.1 step 8: + // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, + // Iterations, and Salt fields to the zone apex." + let nsec3param_rec = Record::new( + apex.owner().try_to_name::().unwrap().into(), + Class::IN, + ttl, + params, + ); + + // RFC 5155 7.1 after step 8: + // "If a hash collision is detected, then a new salt has to be + // chosen, and the signing process restarted." + // + // TOOD + + Ok(Nsec3Records::new(nsec3s.records, nsec3param_rec)) + } + pub fn write(&self, target: &mut W) -> Result<(), io::Error> where N: fmt::Display, @@ -256,6 +502,81 @@ impl SortedRecords { } } +/// Helper functions used to create NSEC3 records per RFC 5155. +impl SortedRecords { + fn mk_nsec3( + name: &N, + alg: Nsec3HashAlg, + flags: u8, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, + bitmap: RtypeBitmapBuilder<::Builder>, + ttl: Ttl, + ) -> Result>, Nsec3HashError> + where + N: ToName + From>, + Octets: FromBuilder + Clone + Default, + ::Builder: + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, + { + // Create the base32hex ENT NSEC owner name. + let base32hex_label = + Self::mk_base32hex_label_for_name(&name, alg, iterations, salt)?; + + // Prepend it to the zone name to create the NSEC3 owner + // name. + let owner_name = Self::append_origin(base32hex_label, apex_owner); + + // RFC 5155 7.1. step 2: + // "The Next Hashed Owner Name field is left blank for the moment." + // Create a placeholder next owner, we'll fix it later. + let placeholder_next_owner = + OwnerHash::::from_octets(Octets::default()).unwrap(); + + // Create an NSEC3 record. + let nsec3 = Nsec3::new( + alg, + flags, + iterations, + salt.clone(), + placeholder_next_owner, + bitmap.finalize(), + ); + + Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) + } + + fn append_origin(base32hex_label: String, apex_owner: &N) -> N + where + N: ToName + From>, + Octets: FromBuilder, + ::Builder: + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + { + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name + } + + fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + ) -> Result + where + N: ToName, + Octets: AsRef<[u8]>, + { + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) + } +} + impl Default for SortedRecords { fn default() -> Self { Self::new() @@ -299,6 +620,29 @@ where } } +//------------ Nsec3Records --------------------------------------------------- + +/// The set of records created by [`SortedRecords::nsec3s()`]. +pub struct Nsec3Records { + /// The NSEC3 records. + pub nsec3_recs: Vec>>, + + /// The NSEC3PARAM record. + pub nsec3param_rec: Record>, +} + +impl Nsec3Records { + pub fn new( + nsec3_recs: Vec>>, + nsec3param_rec: Record>, + ) -> Self { + Self { + nsec3_recs, + nsec3param_rec, + } + } +} + //------------ Family -------------------------------------------------------- /// A set of records with the same owner name and class. diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 864480933..d29d963d1 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,9 +4,17 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; + +use std::fmt::Debug; use std::vec::Vec; -use crate::base::iana::SecAlg; +use octseq::{EmptyBuilder, OctetsBuilder, Truncate}; +use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; + +use crate::base::iana::{Nsec3HashAlg, SecAlg}; +use crate::base::ToName; +use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; +use crate::rdata::Nsec3param; use super::generic; @@ -122,6 +130,121 @@ impl<'a> super::Sign> for SecretKey<'a> { } } +//------------ Nsec3HashError ------------------------------------------------- + +/// An error when creating an NSEC3 hash. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Nsec3HashError { + /// The requested algorithm for NSEC3 hashing is not supported. + UnsupportedAlgorithm, + + /// Data could not be appended to a buffer. + /// + /// This could indicate an out of memory condition. + AppendError, + + /// The hashing process produced an invalid owner hash. + /// + /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) + OwnerHashError, +} + +/// Compute an [RFC 5155] NSEC3 hash using default settings. +/// +/// See: [Nsec3param::default]. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_default_hash( + owner: N, +) -> Result, Nsec3HashError> +where + N: ToName, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + let params = Nsec3param::::default(); + nsec3_hash( + owner, + params.hash_algorithm(), + params.iterations(), + params.salt(), + ) +} + +/// Compute an [RFC 5155] NSEC3 hash. +/// +/// Computes an NSEC3 hash according to [RFC 5155] section 5: +/// +/// IH(salt, x, 0) = H(x || salt) +/// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 +/// +/// Then the calculated hash of an owner name is: +/// +/// IH(salt, owner name, iterations), +/// +/// Note that the `iterations` parameter is the number of _additional_ +/// iterations as defined in [RFC 5155] section 3.1.3. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_hash( + owner: N, + algorithm: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result, Nsec3HashError> +where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + if algorithm != Nsec3HashAlg::SHA1 { + return Err(Nsec3HashError::UnsupportedAlgorithm); + } + + fn mk_hash( + owner: N, + iterations: u16, + salt: &Nsec3Salt, + ) -> Result + where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, + { + let mut buf = HashOcts::empty(); + + owner.compose_canonical(&mut buf)?; + buf.append_slice(salt.as_slice())?; + + let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(buf.as_ref()); + let mut h = ctx.finish(); + + for _ in 0..iterations { + buf.truncate(0); + buf.append_slice(h.as_ref())?; + buf.append_slice(salt.as_slice())?; + + let mut ctx = + ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(buf.as_ref()); + h = ctx.finish(); + } + + Ok(h.as_ref().into()) + } + + let hash = mk_hash(owner, iterations, salt) + .map_err(|_| Nsec3HashError::AppendError)?; + + let owner_hash = OwnerHash::from_octets(hash) + .map_err(|_| Nsec3HashError::OwnerHashError)?; + + Ok(owner_hash) +} + #[cfg(test)] mod tests { use std::vec::Vec; diff --git a/src/validator/nsec.rs b/src/validator/nsec.rs index 8f5d5c64a..81027fc8b 100644 --- a/src/validator/nsec.rs +++ b/src/validator/nsec.rs @@ -1,22 +1,25 @@ //! Helper functions for NSEC and NSEC3 validation. -use super::context::{Config, ValidationState}; -use super::group::ValidatedGroup; -use super::utilities::{make_ede, star_closest_encloser}; +use std::collections::VecDeque; +use std::str::{FromStr, Utf8Error}; +use std::sync::Arc; +use std::vec::Vec; + +use bytes::Bytes; +use moka::future::Cache; + use crate::base::iana::{ExtendedErrorCode, Nsec3HashAlg}; use crate::base::name::{Label, ToName}; use crate::base::opt::ExtendedError; use crate::base::{Name, ParsedName, Rtype}; -use crate::dep::octseq::{Octets, OctetsBuilder}; +use crate::dep::octseq::Octets; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{AllRecordData, Nsec, Nsec3}; -use bytes::Bytes; -use moka::future::Cache; -use ring::digest; -use std::collections::VecDeque; -use std::str::{FromStr, Utf8Error}; -use std::sync::Arc; -use std::vec::Vec; +use crate::sign::ring::nsec3_hash; + +use super::context::{Config, ValidationState}; +use super::group::ValidatedGroup; +use super::utilities::{make_ede, star_closest_encloser}; //----------- Nsec functions ------------------------------------------------- @@ -957,54 +960,6 @@ pub fn supported_nsec3_hash(h: Nsec3HashAlg) -> bool { h == Nsec3HashAlg::SHA1 } -/// Compute the NSEC3 hash according to Section 5 of RFC 5155: -/// -/// IH(salt, x, 0) = H(x || salt) -/// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 -/// -/// Then the calculated hash of an owner name is -/// IH(salt, owner name, iterations), -fn nsec3_hash( - owner: N, - algorithm: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, -) -> OwnerHash> -where - N: ToName, - HashOcts: AsRef<[u8]>, -{ - let mut buf = Vec::new(); - - owner.compose_canonical(&mut buf).expect("infallible"); - buf.append_slice(salt.as_slice()).expect("infallible"); - - let digest_type = if algorithm == Nsec3HashAlg::SHA1 { - &digest::SHA1_FOR_LEGACY_USE_ONLY - } else { - // totest, unsupported NSEC3 hash algorithm - // Unsupported. - panic!("should not be called with an unsupported algorithm"); - }; - - let mut ctx = digest::Context::new(digest_type); - ctx.update(&buf); - let mut h = ctx.finish(); - - for _ in 0..iterations { - buf.truncate(0); - buf.append_slice(h.as_ref()).expect("infallible"); - buf.append_slice(salt.as_slice()).expect("infallible"); - - let mut ctx = digest::Context::new(digest_type); - ctx.update(&buf); - h = ctx.finish(); - } - - // For normal hash algorithms this should not fail. - OwnerHash::from_octets(h.as_ref().to_vec()).expect("should not fail") -} - /// Return an NSEC3 hash using a cache. pub async fn cached_nsec3_hash( owner: &Name, @@ -1018,7 +973,7 @@ pub async fn cached_nsec3_hash( if let Some(ce) = cache.cache.get(&key).await { return ce; } - let hash = nsec3_hash(owner, algorithm, iterations, salt); + let hash = nsec3_hash(owner, algorithm, iterations, salt).unwrap(); let hash = Arc::new(hash); cache.cache.insert(key, hash.clone()).await; hash From bd31ebb546bf0b75e74088c62c18eba1863541f3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:22:20 +0200 Subject: [PATCH 032/569] Clippy. --- src/sign/records.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 97693447b..006dfcac2 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -504,6 +504,7 @@ impl SortedRecords { /// Helper functions used to create NSEC3 records per RFC 5155. impl SortedRecords { + #[allow(clippy::too_many_arguments)] fn mk_nsec3( name: &N, alg: Nsec3HashAlg, @@ -522,7 +523,7 @@ impl SortedRecords { { // Create the base32hex ENT NSEC owner name. let base32hex_label = - Self::mk_base32hex_label_for_name(&name, alg, iterations, salt)?; + Self::mk_base32hex_label_for_name(name, alg, iterations, salt)?; // Prepend it to the zone name to create the NSEC3 owner // name. From bbf110f3583c6e55f579260f6375f253eaf4d449 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:26:18 +0200 Subject: [PATCH 033/569] TOOD -> TODO ;-) --- src/sign/records.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 006dfcac2..16cf5ea14 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -484,7 +484,7 @@ impl SortedRecords { // "If a hash collision is detected, then a new salt has to be // chosen, and the signing process restarted." // - // TOOD + // TODO Ok(Nsec3Records::new(nsec3s.records, nsec3param_rec)) } From fbfbdeaeb58a459f1fcb7cbd5132b395a5086617 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Oct 2024 16:34:48 +0200 Subject: [PATCH 034/569] Fix doctest failure. --- src/sign/ring.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index d29d963d1..6a6d5f310 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -175,12 +175,12 @@ where /// /// Computes an NSEC3 hash according to [RFC 5155] section 5: /// -/// IH(salt, x, 0) = H(x || salt) -/// IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 +/// > IH(salt, x, 0) = H(x || salt) +/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 /// /// Then the calculated hash of an owner name is: /// -/// IH(salt, owner name, iterations), +/// > IH(salt, owner name, iterations), /// /// Note that the `iterations` parameter is the number of _additional_ /// iterations as defined in [RFC 5155] section 3.1.3. From dba5a8a0d6ce736d5cc7b0d6683f84f726f864bd Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 15 Oct 2024 17:32:36 +0200 Subject: [PATCH 035/569] Refactor the 'sign' module Most functions have been renamed. The public key types have been moved to the 'validate' module (which 'sign' now depends on), and they have been outfitted with conversions (e.g. to and from DNSKEY records). Importing a generic key into an OpenSSL or Ring key now requires the public key to also be available. In both implementations, the pair are checked for consistency -- this ensures that both are uncorrupted and that keys have not been mixed up. This also allows the Ring backend to support ECDSA keys (although key generation is still difficult). The 'PublicKey' and 'PrivateKey' enums now store their array data in 'Box'. This has two benefits: it is easier to securely manage memory on the heap (since the compiler will not copy it around the stack); and the smaller sizes of the types is beneficial (although negligibly) to performance. --- Cargo.toml | 3 +- src/sign/generic.rs | 393 ++++++++++++++++++++------------------------ src/sign/mod.rs | 81 ++++++--- src/sign/openssl.rs | 304 +++++++++++++++++++--------------- src/sign/ring.rs | 241 ++++++++++++++++++--------- src/validate.rs | 347 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 910 insertions(+), 459 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bfc47fce4..c6d72ffa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,10 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std"] +sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8ad44ea88..2589a6ab4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,10 +1,11 @@ -use core::{fmt, mem, str}; +use core::{fmt, str}; +use std::boxed::Box; use std::vec::Vec; use crate::base::iana::SecAlg; -use crate::rdata::Dnskey; use crate::utils::base64; +use crate::validate::RsaPublicKey; /// A generic secret key. /// @@ -14,32 +15,97 @@ use crate::utils::base64; /// cryptographic implementation supports it). /// /// [`Sign`]: super::Sign -pub enum SecretKey + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaSecretKey), +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +pub enum SecretKey { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), + EcdsaP256Sha256(Box<[u8; 32]>), /// An ECDSA P-384/SHA-384 keypair. /// /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), + EcdsaP384Sha384(Box<[u8; 48]>), /// An Ed25519 keypair. /// /// The private key is a single 32-byte string. - Ed25519([u8; 32]), + Ed25519(Box<[u8; 32]>), /// An Ed448 keypair. /// /// The private key is a single 57-byte string. - Ed448([u8; 57]), + Ed448(Box<[u8; 57]>), } -impl + AsMut<[u8]>> SecretKey { +impl SecretKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -51,99 +117,99 @@ impl + AsMut<[u8]>> SecretKey { } } - /// Serialize this key in the conventional DNS format. + /// Serialize this key in the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { writeln!(w, "Algorithm: 8 (RSASHA256)")?; - k.into_dns(w) + k.format_as_bind(w) } Self::EcdsaP256Sha256(s) => { writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::EcdsaP384Sha384(s) => { writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed25519(s) => { writeln!(w, "Algorithm: 15 (ED25519)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed448(s) => { writeln!(w, "Algorithm: 16 (ED448)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } } } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { /// Parse private keys for most algorithms (except RSA). fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_dns_pair(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } - let buf: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; - let buf = buf - .as_slice() - .try_into() - .map_err(|_| DnsFormatError::Misformatted)?; + return base64::decode::>(val) + .map_err(|_| BindFormatError::Misformatted)? + .into_boxed_slice() + .try_into() + .map_err(|_| BindFormatError::Misformatted); + } - Ok(buf) + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) } // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .map_or(false, |minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; + .ok_or(BindFormatError::Misformatted)?; // Parse the algorithm. let mut words = val.split_whitespace(); let code = words .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; if words.next().is_some() { - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } match (code, name) { (8, "(RSASHA256)") => { - RsaSecretKey::from_dns(data).map(Self::RsaSha256) + RsaSecretKey::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -153,12 +219,12 @@ impl + AsMut<[u8]>> SecretKey { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), + _ => Err(BindFormatError::UnsupportedAlgorithm), } } } -impl + AsMut<[u8]>> Drop for SecretKey { +impl Drop for SecretKey { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -175,39 +241,40 @@ impl + AsMut<[u8]>> Drop for SecretKey { /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaSecretKey + AsMut<[u8]>> { +pub struct RsaSecretKey { /// The public modulus. - pub n: B, + pub n: Box<[u8]>, /// The public exponent. - pub e: B, + pub e: Box<[u8]>, /// The private exponent. - pub d: B, + pub d: Box<[u8]>, /// The first prime factor of `d`. - pub p: B, + pub p: Box<[u8]>, /// The second prime factor of `d`. - pub q: B, + pub q: Box<[u8]>, /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, + pub d_p: Box<[u8]>, /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, + pub d_q: Box<[u8]>, /// The inverse of the second prime factor modulo the first. - pub q_i: B, + pub q_i: Box<[u8]>, } -impl + AsMut<[u8]>> RsaSecretKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. +impl RsaSecretKey { + /// Serialize this key in the conventional format used by BIND. /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKey`] for a + /// description of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -227,13 +294,13 @@ impl + AsMut<[u8]>> RsaSecretKey { Ok(()) } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKey`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; let mut d = None; @@ -253,25 +320,28 @@ impl + AsMut<[u8]>> RsaSecretKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), + _ => { + data = rest; + continue; + } }; if field.is_some() { // This field has already been filled. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } let buffer: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; + .map_err(|_| BindFormatError::Misformatted)?; - *field = Some(buffer.into()); + *field = Some(buffer.into_boxed_slice()); data = rest; } for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } } @@ -288,142 +358,33 @@ impl + AsMut<[u8]>> RsaSecretKey { } } -impl + AsMut<[u8]>> Drop for RsaSecretKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// A generic public key. -pub enum PublicKey> { - /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKey), - - // TODO: RSA/SHA-1 with NSEC3/SHA-1? - /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKey), - - /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKey), - - /// An ECDSA P-256/SHA-256 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (32 bytes). - /// - The encoding of the `y` coordinate (32 bytes). - EcdsaP256Sha256([u8; 65]), - - /// An ECDSA P-384/SHA-384 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (48 bytes). - /// - The encoding of the `y` coordinate (48 bytes). - EcdsaP384Sha384([u8; 97]), - - /// An Ed25519 public key. - /// - /// The public key is a 32-byte encoding of the public point. - Ed25519([u8; 32]), - - /// An Ed448 public key. - /// - /// The public key is a 57-byte encoding of the public point. - Ed448([u8; 57]), -} - -impl> PublicKey { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, +impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { + fn from(value: &'a RsaSecretKey) -> Self { + RsaPublicKey { + n: value.n.clone(), + e: value.e.clone(), } } - - /// Construct a DNSKEY record with the given flags. - pub fn into_dns(self, flags: u16) -> Dnskey - where - Octs: From> + AsRef<[u8]>, - { - let protocol = 3u8; - let algorithm = self.algorithm(); - let public_key = match self { - Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { - let (n, e) = (k.n.as_ref(), k.e.as_ref()); - let e_len_len = if e.len() < 256 { 1 } else { 3 }; - let len = e_len_len + e.len() + n.len(); - let mut buf = Vec::with_capacity(len); - if let Ok(e_len) = u8::try_from(e.len()) { - buf.push(e_len); - } else { - // RFC 3110 is not explicit about the endianness of this, - // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network - // byte order, which I suppose makes sense. - let e_len = u16::try_from(e.len()).unwrap(); - buf.extend_from_slice(&e_len.to_be_bytes()); - } - buf.extend_from_slice(e); - buf.extend_from_slice(n); - buf - } - - // From my reading of RFC 6605, the marker byte is not included. - Self::EcdsaP256Sha256(k) => k[1..].to_vec(), - Self::EcdsaP384Sha384(k) => k[1..].to_vec(), - - Self::Ed25519(k) => k.to_vec(), - Self::Ed448(k) => k.to_vec(), - }; - - Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() - } -} - -/// A generic RSA public key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaPublicKey> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, } -impl From> for RsaPublicKey -where - B: AsRef<[u8]> + AsMut<[u8]> + Default, -{ - fn from(mut value: RsaSecretKey) -> Self { - Self { - n: mem::take(&mut value.n), - e: mem::take(&mut value.e), - } +impl Drop for RsaSecretKey { + 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); } } /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, -) -> Result, DnsFormatError> { +) -> Result, BindFormatError> { // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. // Trim any pending newlines. @@ -439,7 +400,7 @@ fn parse_dns_pair( // Split the line by a colon. let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + line.split_once(':').ok_or(BindFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). Ok(Some((key.trim(), val.trim(), rest))) @@ -447,7 +408,7 @@ fn parse_dns_pair( /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { +pub enum BindFormatError { /// The key file uses an unsupported version of the format. UnsupportedFormat, @@ -458,7 +419,7 @@ pub enum DnsFormatError { UnsupportedAlgorithm, } -impl fmt::Display for DnsFormatError { +impl fmt::Display for BindFormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedFormat => "unsupported format", @@ -468,7 +429,7 @@ impl fmt::Display for DnsFormatError { } } -impl std::error::Error for DnsFormatError {} +impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { @@ -490,7 +451,7 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -501,9 +462,9 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); let mut same = String::new(); - key.into_dns(&mut same).unwrap(); + key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b1db46c26..b9773d7f0 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,37 +2,44 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a -//! DNS record served by a secure-aware name server. But name servers are not -//! usually creating those signatures themselves. Within a DNS zone, it is the -//! zone administrator's responsibility to sign zone records (when the record's -//! time-to-live expires and/or when it changes). Those signatures are stored -//! as regular DNS data and automatically served by name servers. +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of +//! a DNS record served by a security-aware name server. Signatures can be +//! made "online" (in an authoritative name server while it is running) or +//! "offline" (outside of a name server). Once generated, signatures can be +//! serialized as DNS records and stored alongside the authenticated records. #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, Signature}, +}; pub mod generic; -pub mod key; pub mod openssl; -pub mod records; pub mod ring; -/// Signing DNS records. +/// Sign DNS records. /// -/// Implementors of this trait own a private key and sign DNS records for a zone -/// with that key. Signing is a synchronous operation performed on the current -/// thread; this rules out implementations like HSMs, where I/O communication is -/// necessary. -pub trait Sign { - /// An error in constructing a signature. - type Error; - +/// Types that implement this trait own a private key and can sign arbitrary +/// 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()`] 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()`]: Sign::sign() +pub trait Sign { /// The signature algorithm used. /// - /// The following algorithms can be used: + /// The following algorithms are known to this crate. Recommendations + /// toward or against usage are based on published RFCs, not the crate + /// authors' opinion. Implementing types may choose to support some of + /// the prohibited algorithms anyway. + /// /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) /// - [`SecAlg::DSA`] (highly insecure, do not use) /// - [`SecAlg::RSASHA1`] (insecure, not recommended) @@ -47,11 +54,35 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// Compute a signature. + /// The public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn public_key(&self) -> PublicKey; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// There are three expected failure cases for this function: + /// + /// - The secret key was invalid. The implementing type is responsible + /// for validating the secret key during initialization, so that this + /// kind of error does not occur. + /// + /// - Not enough randomness could be obtained. This applies to signature + /// algorithms which use randomization (primarily ECDSA). On common + /// platforms like Linux, Mac OS, and Windows, cryptographically secure + /// pseudo-random number generation is provided by the OS, so this is + /// highly unlikely. + /// + /// - Not enough memory could be obtained. Signature generation does not + /// require significant memory and an out-of-memory condition means that + /// the application will probably panic soon. /// - /// A regular signature of the given byte sequence is computed and is turned - /// into the selected buffer type. This provides a lot of flexibility in - /// how buffers are constructed; they may be heap-allocated or have a static - /// size. - fn sign(&self, data: &[u8]) -> Result; + /// None of these are considered likely or recoverable, so panicking is + /// the simplest and most ergonomic solution. + fn sign(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 8faa48f9e..5c708f485 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,10 +1,7 @@ //! Key and Signer using OpenSSL. -#![cfg(feature = "openssl")] -#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] - use core::fmt; -use std::vec::Vec; +use std::boxed::Box; use openssl::{ bn::BigNum, @@ -12,7 +9,10 @@ use openssl::{ pkey::{self, PKey, Private}, }; -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; use super::{generic, Sign}; @@ -31,25 +31,31 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, - ) -> Result { + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, + ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); v.copy_from_slice(slice).unwrap(); v } - let pkey = match &key { - generic::SecretKey::RsaSha256(k) => { - let n = BigNum::from_slice(k.n.as_ref()).unwrap(); - let e = BigNum::from_slice(k.e.as_ref()).unwrap(); - let d = num(k.d.as_ref()); - let p = num(k.p.as_ref()); - let q = num(k.q.as_ref()); - let d_p = num(k.d_p.as_ref()); - let d_q = num(k.d_q.as_ref()); - let q_i = num(k.q_i.as_ref()); + let pkey = match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + + let n = BigNum::from_slice(&s.n).unwrap(); + let e = BigNum::from_slice(&s.e).unwrap(); + let d = num(&s.d); + let p = num(&s.p); + let q = num(&s.q); + let d_p = num(&s.d_p); + let d_q = num(&s.d_q); + let q_i = num(&s.q_i); // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the @@ -61,47 +67,75 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - generic::SecretKey::EcdsaP256Sha256(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::EcdsaP384Sha384(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::SECP384R1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::SECP384R1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::Ed25519(k) => { - PKey::private_key_from_raw_bytes( - k.as_ref(), - pkey::Id::ED25519, - ) - .unwrap() + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } - generic::SecretKey::Ed448(k) => { - PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) - .unwrap() + + (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } + + // The public and private key types did not match. + _ => return Err(FromGenericError::InvalidKey), }; Ok(Self { - algorithm: key.algorithm(), + algorithm: secret.algorithm(), pkey, }) } @@ -111,10 +145,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(&self) -> generic::SecretKey - where - B: AsRef<[u8]> + AsMut<[u8]> + From>, - { + pub fn to_generic(&self) -> generic::SecretKey { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { @@ -151,20 +182,18 @@ impl SecretKey { _ => unreachable!(), } } +} - /// Export this key into a generic public key. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { +impl Sign for SecretKey { + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn public_key(&self) -> PublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { + PublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -177,7 +206,7 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -187,65 +216,69 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + PublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed448(key.try_into().unwrap()) + PublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } -} - -impl Sign> for SecretKey { - type Error = openssl::error::ErrorStack; - fn algorithm(&self) -> SecAlg { - self.algorithm - } - - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn sign(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; match self.algorithm { SecAlg::RSASHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; - s.sign_oneshot_to_vec(data) + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + s.set_rsa_padding(openssl::rsa::Padding::PKCS1).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); + Signature::RsaSha256(signature.into_boxed_slice()) } SecAlg::ECDSAP256SHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(32).unwrap(); let s = signature.s().to_vec_padded(32).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 64]); + signature[..32].copy_from_slice(&r); + signature[32..].copy_from_slice(&s); + Signature::EcdsaP256Sha256(signature) } SecAlg::ECDSAP384SHA384 => { - let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha384(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(48).unwrap(); let s = signature.s().to_vec_padded(48).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 96]); + signature[..48].copy_from_slice(&r); + signature[48..].copy_from_slice(&s); + Signature::EcdsaP384Sha384(signature) + } + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed25519(signature.try_into().unwrap()) } - SecAlg::ED25519 | SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed448(signature.try_into().unwrap()) } _ => unreachable!(), } @@ -289,15 +322,15 @@ pub fn generate(algorithm: SecAlg) -> Option { /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, - /// The provided secret key was invalid. + /// The key's parameters were invalid. InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -306,18 +339,20 @@ impl fmt::Display for ImportError { } } -impl std::error::Error for ImportError {} +impl std::error::Error for FromGenericError {} #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), @@ -337,25 +372,32 @@ mod tests { fn generated_roundtrip() { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); - let exp: generic::SecretKey> = key.export(); - let imp = super::SecretKey::import(exp).unwrap(); - assert!(key.pkey.public_eq(&imp.pkey)); + let gen_key = key.to_generic(); + let pub_key = key.public_key(); + let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); } } #[test] fn imported_roundtrip() { - type GenericKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let imp = GenericKey::from_dns(&data).unwrap(); - let key = super::SecretKey::import(imp).unwrap(); - let exp: GenericKey = key.export(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + + let equiv = key.to_generic(); let mut same = String::new(); - exp.into_dns(&mut same).unwrap(); + equiv.format_as_bind(&mut same).unwrap(); + let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); @@ -363,48 +405,40 @@ mod tests { } #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 0996552f6..2a4867094 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,11 +4,16 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::vec::Vec; +use std::{boxed::Box, vec::Vec}; -use crate::base::iana::SecAlg; +use ring::signature::KeyPair; -use super::generic; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; + +use super::{generic, Sign}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -18,71 +23,97 @@ pub enum SecretKey<'a> { rng: &'a dyn ring::rand::SecureRandom, }, + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, rng: &'a dyn ring::rand::SecureRandom, - ) -> Result { - match &key { - generic::SecretKey::RsaSha256(k) => { + ) -> Result { + match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + let components = ring::rsa::KeyPairComponents { public_key: ring::rsa::PublicKeyComponents { - n: k.n.as_ref(), - e: k.e.as_ref(), + n: s.n.as_ref(), + e: s.e.as_ref(), }, - d: k.d.as_ref(), - p: k.p.as_ref(), - q: k.q.as_ref(), - dP: k.d_p.as_ref(), - dQ: k.d_q.as_ref(), - qInv: k.q_i.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(), }; ring::signature::RsaKeyPair::from_components(&components) - .map_err(|_| ImportError::InvalidKey) + .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } - // TODO: Support ECDSA. - generic::SecretKey::Ed25519(k) => { - let k = k.as_ref(); - ring::signature::Ed25519KeyPair::from_seed_unchecked(k) - .map_err(|_| ImportError::InvalidKey) - .map(Self::Ed25519) + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) } - _ => Err(ImportError::UnsupportedAlgorithm), - } - } - /// Export this key into a generic public key. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { - match self { - Self::RsaSha256 { key, rng: _ } => { - let components: ring::rsa::PublicKeyComponents> = - key.public().into(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { - n: components.n.into(), - e: components.e.into(), - }) + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - Self::Ed25519(key) => { - use ring::signature::KeyPair; - let key = key.public_key().as_ref(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + ring::signature::Ed25519KeyPair::from_seed_and_public_key( + s.as_slice(), + p.as_slice(), + ) + .map_err(|_| FromGenericError::InvalidKey) + .map(Self::Ed25519) } + + (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + Err(FromGenericError::UnsupportedAlgorithm) + } + + // The public and private key types did not match. + _ => Err(FromGenericError::InvalidKey), } } } /// An error in importing a key into `ring`. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -90,7 +121,7 @@ pub enum ImportError { InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -99,87 +130,135 @@ impl fmt::Display for ImportError { } } -impl<'a> super::Sign> for SecretKey<'a> { - type Error = ring::error::Unspecified; - +impl<'a> Sign for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, Self::Ed25519(_) => SecAlg::ED25519, } } - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn public_key(&self) -> PublicKey { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + PublicKey::RsaSha256(RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + + Self::EcdsaP256Sha256 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + + Self::EcdsaP384Sha384 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + + Self::Ed25519(key) => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } + + fn sign(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf)?; - Ok(buf) + key.sign(pad, *rng, data, &mut buf) + .expect("random generators do not fail"); + Signature::RsaSha256(buf.into_boxed_slice()) + } + Self::EcdsaP256Sha256 { key, rng } => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP256Sha256(buf) + } + Self::EcdsaP384Sha384 { key, rng } => { + let mut buf = Box::new([0u8; 96]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP384Sha384(buf) + } + Self::Ed25519(key) => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice(key.sign(data).as_ref()); + Signature::Ed25519(buf) } - Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } #[cfg(test)] mod tests { - use std::vec::Vec; - use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index 41b7456e5..b122c83c9 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -10,14 +10,361 @@ use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; +use crate::base::scan::IterScanner; use crate::base::wire::{Compose, Composer}; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use ring::{digest, signature}; +use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +/// A generic public key. +#[derive(Clone, Debug)] +pub enum PublicKey { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + /// An RSA/SHA-1 with NSEC3 public key. + RsaSha1Nsec3Sha1(RsaPublicKey), + + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256(Box<[u8; 65]>), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384(Box<[u8; 97]>), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519(Box<[u8; 32]>), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448(Box<[u8; 57]>), +} + +impl PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl PublicKey { + /// Parse a public key as stored in a DNSKEY record. + pub fn from_dnskey( + algorithm: SecAlg, + data: &[u8], + ) -> Result { + match algorithm { + SecAlg::RSASHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + } + SecAlg::RSASHA1_NSEC3_SHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + } + SecAlg::RSASHA256 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + } + SecAlg::RSASHA512 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + } + + SecAlg::ECDSAP256SHA256 => { + let mut key = Box::new([0u8; 65]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP256Sha256(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + SecAlg::ECDSAP384SHA384 => { + let mut key = Box::new([0u8; 97]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP384Sha384(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + + SecAlg::ED25519 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed25519) + .map_err(|_| FromDnskeyError::InvalidKey), + SecAlg::ED448 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed448) + .map_err(|_| FromDnskeyError::InvalidKey), + + _ => Err(FromDnskeyError::UnsupportedAlgorithm), + } + } + + /// Parse a public key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn from_dnskey_text( + dnskey: &str, + ) -> Result { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(FromDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Ensure the record header looks reasonable. + let mut words = line.split_ascii_whitespace().skip(2); + if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { + return Err(FromDnskeyTextError::Misformatted); + } + + // Parse the DNSKEY record data. + let mut data = IterScanner::new(words); + let dnskey: Dnskey> = Dnskey::scan(&mut data) + .map_err(|_| FromDnskeyTextError::Misformatted)?; + println!("importing {:?}", dnskey); + Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) + .map_err(FromDnskeyTextError::FromDnskey) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.to_dnskey(), + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].into(), + Self::EcdsaP384Sha384(k) => k[1..].into(), + + Self::Ed25519(k) => k.as_slice().into(), + Self::Ed448(k) => k.as_slice().into(), + } + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + match (self, other) { + (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, + (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, + (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, + (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, + (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed25519(a), Self::Ed25519(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed448(a), Self::Ed448(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + _ => false, + } + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +#[derive(Clone, Debug)] +pub struct RsaPublicKey { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, +} + +impl RsaPublicKey { + /// Parse an RSA public key as stored in a DNSKEY record. + pub fn from_dnskey(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(FromDnskeyError::InvalidKey); + } + + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else if data[1..3] != [0, 0] { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + } else { + // The extended encoding of the length just held a zero value. + return Err(FromDnskeyError::InvalidKey); + }; + + // NOTE: off <= 3 so is safe to index up to. + let e = data[off..] + .get(..exp_len) + .ok_or(FromDnskeyError::InvalidKey)? + .into(); + + // NOTE: The previous statement indexed up to 'exp_len'. + let n = data[off + exp_len..].into(); + + Ok(Self { n, e }) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + key.reserve_exact(1 + self.e.len() + self.n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + key.reserve_exact(3 + self.e.len() + self.n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(&*self.e); + key.extend(&*self.n); + key.into_boxed_slice() + } +} + +impl PartialEq for RsaPublicKey { + fn eq(&self, other: &Self) -> bool { + /// Compare after stripping leading zeros. + fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { + let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; + let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; + if a.len() == b.len() { + ring::constant_time::verify_slices_are_equal(a, b).is_ok() + } else { + false + } + } + + cmp_without_leading(&self.n, &other.n) + && cmp_without_leading(&self.e, &other.e) + } +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +pub enum Signature { + RsaSha1(Box<[u8]>), + RsaSha1Nsec3Sha1(Box<[u8]>), + RsaSha256(Box<[u8]>), + RsaSha512(Box<[u8]>), + EcdsaP256Sha256(Box<[u8; 64]>), + EcdsaP384Sha384(Box<[u8; 96]>), + Ed25519(Box<[u8; 64]>), + Ed448(Box<[u8; 114]>), +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From 25402edf67d13e79f13d7b3ec7852423bdba4e93 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 036/569] [sign] Define 'KeyPair' and impl key export A private key converted into a 'KeyPair' can be exported in the conventional DNS format. This is an important step in implementing 'ldns-keygen' using 'domain'. It is up to the implementation modules to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs) won't support it at all. --- src/sign/mod.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d87acca0c..ff36b16b7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,10 +1,253 @@ //! DNSSEC signing. //! //! **This module is experimental and likely to change significantly.** +//! +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a +//! DNS record served by a secure-aware name server. But name servers are not +//! usually creating those signatures themselves. Within a DNS zone, it is the +//! zone administrator's responsibility to sign zone records (when the record's +//! time-to-live expires and/or when it changes). Those signatures are stored +//! as regular DNS data and automatically served by name servers. + #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +use core::{fmt, str}; + +use crate::base::iana::SecAlg; + pub mod key; //pub mod openssl; pub mod records; pub mod ring; + +/// A generic keypair. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// cryptographic implementation supports it). +pub enum KeyPair + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> KeyPair { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64(&*s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64(&*s, &mut *w) + } + } + } +} + +impl + AsMut<[u8]>> Drop for KeyPair { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6.2 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (b'0' - 52) as u32 + + (pluses & bcast) * (b'+' - 62) as u32 + + (slashs & bcast) * (b'/' - 63) as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + 3 => {} + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} From a62139a276ba526b2526b38a5a989cfc8c25675b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:54:14 +0200 Subject: [PATCH 037/569] [sign] Define trait 'Sign' 'Sign' is a more generic version of 'sign::key::SigningKey' that does not provide public key information. It does not try to abstract over all the functionality of a keypair, since that can depend on the underlying cryptographic implementation. --- src/sign/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ff36b16b7..f4bac3c51 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -21,6 +21,42 @@ pub mod key; pub mod records; pub mod ring; +/// Signing DNS records. +/// +/// Implementors of this trait own a private key and sign DNS records for a zone +/// with that key. Signing is a synchronous operation performed on the current +/// thread; this rules out implementations like HSMs, where I/O communication is +/// necessary. +pub trait Sign { + /// An error in constructing a signature. + type Error; + + /// The signature algorithm used. + /// + /// The following algorithms can be used: + /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) + /// - [`SecAlg::DSA`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1`] (insecure, not recommended) + /// - [`SecAlg::DSA_NSEC3_SHA1`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1_NSEC3_SHA1`] (insecure, not recommended) + /// - [`SecAlg::RSASHA256`] + /// - [`SecAlg::RSASHA512`] (not recommended) + /// - [`SecAlg::ECC_GOST`] (do not use) + /// - [`SecAlg::ECDSAP256SHA256`] + /// - [`SecAlg::ECDSAP384SHA384`] + /// - [`SecAlg::ED25519`] + /// - [`SecAlg::ED448`] + fn algorithm(&self) -> SecAlg; + + /// Compute a signature. + /// + /// A regular signature of the given byte sequence is computed and is turned + /// into the selected buffer type. This provides a lot of flexibility in + /// how buffers are constructed; they may be heap-allocated or have a static + /// size. + fn sign(&self, data: &[u8]) -> Result; +} + /// A generic keypair. /// /// This type cannot be used for computing signatures, as it does not implement From a4f205605d98426f1a2a65d5ba1eacb91df10f42 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 15:42:48 +0200 Subject: [PATCH 038/569] [sign] Implement parsing from the DNS format There are probably lots of bugs in this implementation, I'll add some tests soon. --- src/sign/mod.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 18 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f4bac3c51..691edb5e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -14,6 +14,8 @@ use core::{fmt, str}; +use std::vec::Vec; + use crate::base::iana::SecAlg; pub mod key; @@ -114,25 +116,84 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } } } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(())?; + + if !data.trim_ascii().is_empty() { + // There were more fields following. + return Err(()); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf)? != N { + // The private key was of the wrong size. + return Err(()); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(())?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(())?; + + // Parse the algorithm. + let mut words = val.split_ascii_whitespace(); + let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; + let name = words.next().ok_or(())?; + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(()), + } + } } impl + AsMut<[u8]>> Drop for KeyPair { @@ -183,26 +244,87 @@ impl + AsMut<[u8]>> RsaKey { /// /// The output does not include an 'Algorithm' specifier. /// - /// See RFC 5702, section 6.2 for examples of this format. + /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus:\t")?; - base64(self.n.as_ref(), &mut *w)?; + base64_encode(self.n.as_ref(), &mut *w)?; w.write_str("\nPublicExponent:\t")?; - base64(self.e.as_ref(), &mut *w)?; + base64_encode(self.e.as_ref(), &mut *w)?; w.write_str("\nPrivateExponent:\t")?; - base64(self.d.as_ref(), &mut *w)?; + base64_encode(self.d.as_ref(), &mut *w)?; w.write_str("\nPrime1:\t")?; - base64(self.p.as_ref(), &mut *w)?; + base64_encode(self.p.as_ref(), &mut *w)?; w.write_str("\nPrime2:\t")?; - base64(self.q.as_ref(), &mut *w)?; + base64_encode(self.q.as_ref(), &mut *w)?; w.write_str("\nExponent1:\t")?; - base64(self.d_p.as_ref(), &mut *w)?; + base64_encode(self.d_p.as_ref(), &mut *w)?; w.write_str("\nExponent2:\t")?; - base64(self.d_q.as_ref(), &mut *w)?; + base64_encode(self.d_q.as_ref(), &mut *w)?; w.write_str("\nCoefficient:\t")?; - base64(self.q_i.as_ref(), &mut *w)?; + base64_encode(self.q_i.as_ref(), &mut *w)?; w.write_char('\n') } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(()), + }; + + if field.is_some() { + // This field has already been filled. + return Err(()); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(()); + } + } + + 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(), + }) + } } impl + AsMut<[u8]>> Drop for RsaKey { @@ -219,11 +341,26 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair(data: &str) -> Result, ()> { + // Trim any pending newlines. + let data = data.trim_ascii_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = line.split_once(':').ok_or(())?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) +} + /// A utility function to format data as Base64. /// /// This is a simple implementation with the only requirement of being /// constant-time and side-channel resistant. -fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { // Convert a single chunk of bytes into Base64. fn encode(data: [u8; 3]) -> [u8; 4] { let [a, b, c] = data; @@ -254,9 +391,9 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (b'0' - 52) as u32 - + (pluses & bcast) * (b'+' - 62) as u32 - + (slashs & bcast) * (b'/' - 63) as u32; + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; // Convert back into a byte array. chunk.to_be_bytes() @@ -281,9 +418,109 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { 0 => return Ok(()), 1 => chunk[2..].fill(b'='), 2 => chunk[3..].fill(b'='), - 3 => {} _ => unreachable!(), } let chunk = str::from_utf8(&chunk).unwrap(); w.write_str(chunk) } + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} From f00a9acbcd00372ab80e4409fd70eb10db31f7bf Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 16:01:04 +0200 Subject: [PATCH 039/569] [sign] Provide some error information Also fixes 'cargo clippy' issues, particularly with the MSRV. --- src/sign/mod.rs | 96 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 691edb5e3..d320f0249 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -63,7 +63,7 @@ pub trait Sign { /// /// This type cannot be used for computing signatures, as it does not implement /// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). pub enum KeyPair + AsMut<[u8]>> { /// An RSA/SHA256 keypair. @@ -116,22 +116,22 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } } } @@ -141,26 +141,28 @@ impl + AsMut<[u8]>> KeyPair { /// - For RSA, see RFC 5702, section 6. /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result + pub fn from_dns(data: &str) -> Result where B: From>, { /// Parse private keys for most algorithms (except RSA). - fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { // Extract the 'PrivateKey' field. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; - if !data.trim_ascii().is_empty() { + if !data.trim().is_empty() { // There were more fields following. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf)? != N { + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { // The private key was of the wrong size. - return Err(()); + return Err(DnsFormatError::Misformatted); } Ok(buf) @@ -169,17 +171,24 @@ impl + AsMut<[u8]>> KeyPair { // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(())?; + .ok_or(DnsFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; // Parse the algorithm. - let mut words = val.split_ascii_whitespace(); - let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; - let name = words.next().ok_or(())?; + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } match (code, name) { (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), @@ -191,7 +200,7 @@ impl + AsMut<[u8]>> KeyPair { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(()), + _ => Err(DnsFormatError::UnsupportedAlgorithm), } } } @@ -268,7 +277,7 @@ impl + AsMut<[u8]>> RsaKey { /// Parse a key from the conventional DNS format. /// /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result + pub fn from_dns(mut data: &str) -> Result where B: From>, { @@ -291,16 +300,17 @@ impl + AsMut<[u8]>> RsaKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(()), + _ => return Err(DnsFormatError::Misformatted), }; if field.is_some() { // This field has already been filled. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer)?; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; buffer.truncate(size); *field = Some(buffer.into()); @@ -310,7 +320,7 @@ impl + AsMut<[u8]>> RsaKey { for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(()); + return Err(DnsFormatError::Misformatted); } } @@ -342,18 +352,23 @@ impl + AsMut<[u8]>> Drop for RsaKey { } /// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair(data: &str) -> Result, ()> { +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + // Trim any pending newlines. - let data = data.trim_ascii_start(); + let data = data.trim_start(); // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); // Split the line by a colon. - let (key, val) = line.split_once(':').ok_or(())?; + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) + Ok(Some((key.trim(), val.trim(), rest))) } /// A utility function to format data as Base64. @@ -388,6 +403,7 @@ fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let slashs = pluses >> 7; // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 @@ -461,6 +477,7 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { } // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk - (uppers & bcast) * (b'A' - 0) as u32 - (lowers & bcast) * (b'a' - 26) as u32 @@ -524,3 +541,28 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { Ok(index) } + +/// An error in loading a [`KeyPair`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} From 6535747c0ec54fd061f4b8ea943bb1a04d4ba4c6 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 4 Oct 2024 13:08:07 +0200 Subject: [PATCH 040/569] [sign] Move 'KeyPair' to 'generic::SecretKey' I'm going to add a corresponding 'PublicKey' type, at which point it becomes important to differentiate from the generic representations and actual cryptographic implementations. --- src/sign/generic.rs | 513 ++++++++++++++++++++++++++++++++++++++++++++ src/sign/mod.rs | 513 +------------------------------------------- 2 files changed, 514 insertions(+), 512 deletions(-) create mode 100644 src/sign/generic.rs diff --git a/src/sign/generic.rs b/src/sign/generic.rs new file mode 100644 index 000000000..420d84530 --- /dev/null +++ b/src/sign/generic.rs @@ -0,0 +1,513 @@ +use core::{fmt, str}; + +use std::vec::Vec; + +use crate::base::iana::SecAlg; + +/// A generic secret key. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Sign`] (if the underlying +/// cryptographic implementation supports it). +pub enum SecretKey + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> SecretKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64_encode(s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64_encode(s, &mut *w) + } + } + } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(DnsFormatError::Misformatted)?; + + if !data.trim().is_empty() { + // There were more fields following. + return Err(DnsFormatError::Misformatted); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { + // The private key was of the wrong size. + return Err(DnsFormatError::Misformatted); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(DnsFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(DnsFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(DnsFormatError::UnsupportedAlgorithm), + } + } +} + +impl + AsMut<[u8]>> Drop for SecretKey { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64_encode(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64_encode(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64_encode(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64_encode(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64_encode(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64_encode(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64_encode(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(DnsFormatError::Misformatted), + }; + + if field.is_some() { + // This field has already been filled. + return Err(DnsFormatError::Misformatted); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(DnsFormatError::Misformatted); + } + } + + 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(), + }) + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} + +/// An error in loading a [`SecretKey`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d320f0249..a649f7ab2 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -12,12 +12,9 @@ #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use core::{fmt, str}; - -use std::vec::Vec; - use crate::base::iana::SecAlg; +pub mod generic; pub mod key; //pub mod openssl; pub mod records; @@ -58,511 +55,3 @@ pub trait Sign { /// size. fn sign(&self, data: &[u8]) -> Result; } - -/// A generic keypair. -/// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -pub enum KeyPair + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), - - /// An ECDSA P-256/SHA-256 keypair. - /// - /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), - - /// An ECDSA P-384/SHA-384 keypair. - /// - /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), - - /// An Ed25519 keypair. - /// - /// The private key is a single 32-byte string. - Ed25519([u8; 32]), - - /// An Ed448 keypair. - /// - /// The private key is a single 57-byte string. - Ed448([u8; 57]), -} - -impl + AsMut<[u8]>> KeyPair { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } - - /// Serialize this key in the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - match self { - Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; - k.into_dns(w) - } - - Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) - } - - Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) - } - } - } - - /// Parse a key from the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { - /// Parse private keys for most algorithms (except RSA). - fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } - - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } - - Ok(buf) - } - - // The first line should specify the key format. - let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; - - // The second line should specify the algorithm. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; - - // Parse the algorithm. - let mut words = val.split_whitespace(); - let code = words - .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; - if words.next().is_some() { - return Err(DnsFormatError::Misformatted); - } - - match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), - (13, "(ECDSAP256SHA256)") => { - parse_pkey(data).map(Self::EcdsaP256Sha256) - } - (14, "(ECDSAP384SHA384)") => { - parse_pkey(data).map(Self::EcdsaP384Sha384) - } - (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), - (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), - } - } -} - -impl + AsMut<[u8]>> Drop for KeyPair { - 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), - } - } -} - -/// An RSA private key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, - - /// The private exponent. - pub d: B, - - /// The first prime factor of `d`. - pub p: B, - - /// The second prime factor of `d`. - pub q: B, - - /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, - - /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, - - /// The inverse of the second prime factor modulo the first. - pub q_i: B, -} - -impl + AsMut<[u8]>> RsaKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. - /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; - w.write_char('\n') - } - - /// Parse a key from the conventional DNS format. - /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { - let mut n = None; - let mut e = None; - let mut d = None; - let mut p = None; - let mut q = None; - let mut d_p = None; - let mut d_q = None; - let mut q_i = None; - - while let Some((key, val, rest)) = parse_dns_pair(data)? { - let field = match key { - "Modulus" => &mut n, - "PublicExponent" => &mut e, - "PrivateExponent" => &mut d, - "Prime1" => &mut p, - "Prime2" => &mut q, - "Exponent1" => &mut d_p, - "Exponent2" => &mut d_q, - "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), - }; - - if field.is_some() { - // This field has already been filled. - return Err(DnsFormatError::Misformatted); - } - - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) - .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); - - *field = Some(buffer.into()); - data = rest; - } - - for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { - if field.is_none() { - // A field was missing. - return Err(DnsFormatError::Misformatted); - } - } - - 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(), - }) - } -} - -impl + AsMut<[u8]>> Drop for RsaKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair( - data: &str, -) -> Result, DnsFormatError> { - // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. - - // Trim any pending newlines. - let data = data.trim_start(); - - // Get the first line (NOTE: CR LF is handled later). - let (line, rest) = data.split_once('\n').unwrap_or((data, "")); - - // Split the line by a colon. - let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; - - // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim(), val.trim(), rest))) -} - -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - -/// An error in loading a [`KeyPair`] from the conventional DNS format. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { - /// The key file uses an unsupported version of the format. - UnsupportedFormat, - - /// The key file did not follow the DNS format correctly. - Misformatted, - - /// The key file used an unsupported algorithm. - UnsupportedAlgorithm, -} - -impl fmt::Display for DnsFormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedFormat => "unsupported format", - Self::Misformatted => "misformatted key file", - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -impl std::error::Error for DnsFormatError {} From 69e5066ab356c9e2ca5cd1cbc04caacd72a6f4fb Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 15:29:45 +0200 Subject: [PATCH 041/569] [sign/generic] Add 'PublicKey' --- src/sign/generic.rs | 135 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 420d84530..7c9ffbea4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,8 +1,9 @@ -use core::{fmt, str}; +use core::{fmt, mem, str}; use std::vec::Vec; use crate::base::iana::SecAlg; +use crate::rdata::Dnskey; /// A generic secret key. /// @@ -12,7 +13,7 @@ use crate::base::iana::SecAlg; /// cryptographic implementation supports it). pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// @@ -136,7 +137,9 @@ impl + AsMut<[u8]>> SecretKey { } match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (8, "(RSASHA256)") => { + RsaSecretKey::from_dns(data).map(Self::RsaSha256) + } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) } @@ -163,11 +166,11 @@ impl + AsMut<[u8]>> Drop for SecretKey { } } -/// An RSA private key. +/// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { +pub struct RsaSecretKey + AsMut<[u8]>> { /// The public modulus. pub n: B, @@ -193,7 +196,7 @@ pub struct RsaKey + AsMut<[u8]>> { pub q_i: B, } -impl + AsMut<[u8]>> RsaKey { +impl + AsMut<[u8]>> RsaSecretKey { /// Serialize this key in the conventional DNS format. /// /// The output does not include an 'Algorithm' specifier. @@ -282,7 +285,7 @@ impl + AsMut<[u8]>> RsaKey { } } -impl + AsMut<[u8]>> Drop for RsaKey { +impl + AsMut<[u8]>> Drop for RsaSecretKey { fn drop(&mut self) { // Zero the bytes for each field. self.n.as_mut().fill(0u8); @@ -296,6 +299,124 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// A generic public key. +pub enum PublicKey> { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + // TODO: RSA/SHA-1 with NSEC3/SHA-1? + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256([u8; 65]), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384([u8; 97]), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519([u8; 32]), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448([u8; 57]), +} + +impl> PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Construct a DNSKEY record with the given flags. + pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + where + Octs: From> + AsRef<[u8]>, + { + let protocol = 3u8; + let algorithm = self.algorithm(); + let public_key = match self { + Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { + let (n, e) = (k.n.as_ref(), k.e.as_ref()); + let e_len_len = if e.len() < 256 { 1 } else { 3 }; + let len = e_len_len + e.len() + n.len(); + let mut buf = Vec::with_capacity(len); + if let Ok(e_len) = u8::try_from(e.len()) { + buf.push(e_len); + } else { + // RFC 3110 is not explicit about the endianness of this, + // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network + // byte order, which I suppose makes sense. + let e_len = u16::try_from(e.len()).unwrap(); + buf.extend_from_slice(&e_len.to_be_bytes()); + } + buf.extend_from_slice(e); + buf.extend_from_slice(n); + buf + } + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].to_vec(), + Self::EcdsaP384Sha384(k) => k[1..].to_vec(), + + Self::Ed25519(k) => k.to_vec(), + Self::Ed448(k) => k.to_vec(), + }; + + Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaPublicKey> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, +} + +impl From> for RsaPublicKey +where + B: AsRef<[u8]> + AsMut<[u8]> + Default, +{ + fn from(mut value: RsaSecretKey) -> Self { + Self { + n: mem::take(&mut value.n), + e: mem::take(&mut value.e), + } + } +} + /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, From 3c80b2f21cca2ed29bc295eb9732e652c885877a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 16:41:57 +0200 Subject: [PATCH 042/569] [sign] Rewrite the 'ring' module to use the 'Sign' trait Key generation, for now, will only be provided by the OpenSSL backend (coming soon). However, generic keys (for RSA/SHA-256 or Ed25519) can be imported into the Ring backend and used freely. --- src/sign/generic.rs | 4 +- src/sign/ring.rs | 180 ++++++++++++++++---------------------------- 2 files changed, 68 insertions(+), 116 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 7c9ffbea4..f963a8def 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -11,6 +11,8 @@ use crate::rdata::Dnskey; /// any cryptographic primitives. Instead, it is a generic representation that /// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). +/// +/// [`Sign`]: super::Sign pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. RsaSha256(RsaSecretKey), @@ -355,7 +357,7 @@ impl> PublicKey { } /// Construct a DNSKEY record with the given flags. - pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + pub fn into_dns(self, flags: u16) -> Dnskey where Octs: From> + AsRef<[u8]>, { diff --git a/src/sign/ring.rs b/src/sign/ring.rs index bf4614f2b..75660dfd6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,140 +1,90 @@ -//! Key and Signer using ring. +//! DNSSEC signing using `ring`. + #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] -use super::key::SigningKey; -use crate::base::iana::{DigestAlg, SecAlg}; -use crate::base::name::ToName; -use crate::base::rdata::ComposeRecordData; -use crate::rdata::{Dnskey, Ds}; -#[cfg(feature = "bytes")] -use bytes::Bytes; -use octseq::builder::infallible; -use ring::digest; -use ring::error::Unspecified; -use ring::rand::SecureRandom; -use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaEncoding, RsaKeyPair, - Signature as RingSignature, ECDSA_P256_SHA256_FIXED_SIGNING, -}; use std::vec::Vec; -pub struct Key<'a> { - dnskey: Dnskey>, - key: RingKey, - rng: &'a dyn SecureRandom, -} - -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), - Ed25519(Ed25519KeyPair), - Rsa(RsaKeyPair, &'static dyn RsaEncoding), -} - -impl<'a> Key<'a> { - pub fn throwaway_13( - flags: u16, - rng: &'a dyn SecureRandom, - ) -> Result { - let pkcs8 = EcdsaKeyPair::generate_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - rng, - )?; - let keypair = EcdsaKeyPair::from_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - pkcs8.as_ref(), - rng, - )?; - let public_key = keypair.public_key().as_ref()[1..].into(); - Ok(Key { - dnskey: Dnskey::new( - flags, - 3, - SecAlg::ECDSAP256SHA256, - public_key, - ) - .expect("long key"), - key: RingKey::Ecdsa(keypair), - rng, - }) - } -} +use crate::base::iana::SecAlg; -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +use super::generic; - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } +/// A key pair backed by `ring`. +pub enum KeyPair<'a> { + /// An RSA/SHA256 keypair. + RsaSha256 { + key: ring::signature::RsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - infallible(owner.compose_canonical(&mut buf)); - infallible(self.dnskey.compose_canonical_rdata(&mut buf)); - let digest = - Vec::from(digest::digest(&digest::SHA256, &buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::SHA256, - digest, - ) - .expect("long digest")) - } + /// An Ed25519 keypair. + Ed25519(ring::signature::Ed25519KeyPair), +} - fn sign(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) +impl<'a> KeyPair<'a> { + /// Use a generic keypair with `ring`. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + rng: &'a dyn ring::rand::SecureRandom, + ) -> Result { + match &key { + generic::SecretKey::RsaSha256(k) => { + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: k.n.as_ref(), + e: k.e.as_ref(), + }, + d: k.d.as_ref(), + p: k.p.as_ref(), + q: k.q.as_ref(), + dP: k.d_p.as_ref(), + dQ: k.d_q.as_ref(), + qInv: k.q_i.as_ref(), + }; + ring::signature::RsaKeyPair::from_components(&components) + .map_err(|_| ImportError::InvalidKey) + .map(|key| Self::RsaSha256 { key, rng }) } - RingKey::Ed25519(ref key) => Ok(Signature::sig(key.sign(msg))), - RingKey::Rsa(ref key, encoding) => { - let mut sig = vec![0; key.public().modulus_len()]; - key.sign(encoding, self.rng, msg, &mut sig)?; - Ok(Signature::vec(sig)) + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + let k = k.as_ref(); + ring::signature::Ed25519KeyPair::from_seed_unchecked(k) + .map_err(|_| ImportError::InvalidKey) + .map(Self::Ed25519) } + _ => Err(ImportError::UnsupportedAlgorithm), } } } -pub struct Signature(SignatureInner); +/// An error in importing a key into `ring`. +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), + /// The provided keypair was invalid. + InvalidKey, } -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) - } - - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) - } -} +impl<'a> super::Sign> for KeyPair<'a> { + type Error = ring::error::Unspecified; -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self.0 { - SignatureInner::Sig(ref sig) => sig.as_ref(), - SignatureInner::Vec(ref vec) => vec.as_slice(), + fn algorithm(&self) -> SecAlg { + match self { + KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, + KeyPair::Ed25519(_) => SecAlg::ED25519, } } -} -#[cfg(feature = "bytes")] -impl From for Bytes { - fn from(sig: Signature) -> Self { - match sig.0 { - SignatureInner::Sig(sig) => Bytes::copy_from_slice(sig.as_ref()), - SignatureInner::Vec(sig) => Bytes::from(sig), + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + match self { + KeyPair::RsaSha256 { key, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, *rng, data, &mut buf)?; + Ok(buf) + } + KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From eace7b6a872b40038e20972b2c1e9d3fdc58ad5e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:36:23 +0200 Subject: [PATCH 043/569] Implement DNSSEC signing with OpenSSL The OpenSSL backend supports import from and export to generic secret keys, making the formatting and parsing machinery for them usable. The next step is to implement generation of keys. --- Cargo.lock | 66 +++++++++++++++++ Cargo.toml | 2 + src/sign/mod.rs | 2 +- src/sign/openssl.rs | 167 ++++++++++++++++++++++++++++++++------------ src/sign/ring.rs | 16 ++--- 5 files changed, 200 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f9bb8ba4..61f66927a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ "mock_instant", "moka", "octseq", + "openssl", "parking_lot", "proc-macro2", "rand", @@ -277,6 +278,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.31" @@ -620,6 +636,44 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -687,6 +741,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1320,6 +1380,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 499ce94e6..036519e3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } +openssl = { version = "0.10", optional = true } 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 } @@ -48,6 +49,7 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] +openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] diff --git a/src/sign/mod.rs b/src/sign/mod.rs index a649f7ab2..b1db46c26 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -16,7 +16,7 @@ use crate::base::iana::SecAlg; pub mod generic; pub mod key; -//pub mod openssl; +pub mod openssl; pub mod records; pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c49512b73..e62c9dcbb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,58 +1,137 @@ //! Key and Signer using OpenSSL. + #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] +use core::fmt; use std::vec::Vec; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::sha::sha256; -use openssl::sign::Signer as OpenSslSigner; -use unwrap::unwrap; -use crate::base::iana::DigestAlg; -use crate::base::name::ToDname; -use crate::base::octets::Compose; -use crate::rdata::{Ds, Dnskey}; -use super::key::SigningKey; - - -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, + +use openssl::{ + bn::BigNum, + pkey::{self, PKey, Private}, +}; + +use crate::base::iana::SecAlg; + +use super::generic; + +/// A key pair backed by OpenSSL. +pub struct SecretKey { + /// The algorithm used by the key. + algorithm: SecAlg, + + /// The private key. + pkey: PKey, } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +impl SecretKey { + /// Use a generic secret key with OpenSSL. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + ) -> Result { + fn num(slice: &[u8]) -> BigNum { + let mut v = BigNum::new_secure().unwrap(); + v.copy_from_slice(slice).unwrap(); + v + } - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } + let pkey = match &key { + generic::SecretKey::RsaSha256(k) => { + let n = BigNum::from_slice(k.n.as_ref()).unwrap(); + let e = BigNum::from_slice(k.e.as_ref()).unwrap(); + let d = num(k.d.as_ref()); + let p = num(k.p.as_ref()); + let q = num(k.q.as_ref()); + let d_p = num(k.d_p.as_ref()); + let d_q = num(k.d_q.as_ref()); + let q_i = num(k.q_i.as_ref()); - fn ds( - &self, - owner: N - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - unwrap!(owner.compose_canonical(&mut buf)); - unwrap!(self.dnskey.compose_canonical(&mut buf)); - let digest = Vec::from(sha256(&buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::Sha256, - digest, - )) + // NOTE: The 'openssl' crate doesn't seem to expose + // 'EVP_PKEY_fromdata', which could be used to replace the + // deprecated methods called here. + + openssl::rsa::Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + ) + .and_then(PKey::from_rsa) + .unwrap() + } + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + PKey::private_key_from_raw_bytes( + k.as_ref(), + pkey::Id::ED25519, + ) + .unwrap() + } + generic::SecretKey::Ed448(k) => { + PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) + .unwrap() + } + _ => return Err(ImportError::UnsupportedAlgorithm), + }; + + Ok(Self { + algorithm: key.algorithm(), + pkey, + }) } - fn sign(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + /// Export this key into a generic secret key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export(self) -> generic::SecretKey + where + B: AsRef<[u8]> + AsMut<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::SecretKey::RsaSha256(generic::RsaSecretKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } } } +/// An error in importing a key into OpenSSL. +#[derive(Clone, Debug)] +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided secret key was invalid. + InvalidKey, +} + +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 75660dfd6..872f8dadb 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,8 +10,8 @@ use crate::base::iana::SecAlg; use super::generic; /// A key pair backed by `ring`. -pub enum KeyPair<'a> { - /// An RSA/SHA256 keypair. +pub enum SecretKey<'a> { + /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, rng: &'a dyn ring::rand::SecureRandom, @@ -21,7 +21,7 @@ pub enum KeyPair<'a> { Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> KeyPair<'a> { +impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn import + AsMut<[u8]>>( key: generic::SecretKey, @@ -66,25 +66,25 @@ pub enum ImportError { InvalidKey, } -impl<'a> super::Sign> for KeyPair<'a> { +impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; fn algorithm(&self) -> SecAlg { match self { - KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, - KeyPair::Ed25519(_) => SecAlg::ED25519, + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::Ed25519(_) => SecAlg::ED25519, } } fn sign(&self, data: &[u8]) -> Result, Self::Error> { match self { - KeyPair::RsaSha256 { key, rng } => { + Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; key.sign(pad, *rng, data, &mut buf)?; Ok(buf) } - KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), + Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From c698403983dfe39cd8dc128451e3da8854a96cb4 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:57:33 +0200 Subject: [PATCH 044/569] [sign/openssl] Implement key generation --- src/sign/openssl.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e62c9dcbb..9d208737c 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -117,6 +117,27 @@ impl SecretKey { } } +/// Generate a new secret key for the given algorithm. +/// +/// If the algorithm is not supported, [`None`] is returned. +/// +/// # Panics +/// +/// Panics if OpenSSL fails or if memory could not be allocated. +pub fn generate(algorithm: SecAlg) -> Option { + let pkey = match algorithm { + // We generate 3072-bit keys for an estimated 128 bits of security. + SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) + .and_then(PKey::from_rsa) + .unwrap(), + SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), + SecAlg::ED448 => PKey::generate_ed448().unwrap(), + _ => return None, + }; + + Some(SecretKey { algorithm, pkey }) +} + /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] pub enum ImportError { @@ -135,3 +156,5 @@ impl fmt::Display for ImportError { }) } } + +impl std::error::Error for ImportError {} From 89dfdfc03dc0d4c8616faed719d62f2d28906fc2 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:08:06 +0200 Subject: [PATCH 045/569] [sign/openssl] Test key generation and import/export --- src/sign/openssl.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9d208737c..13c1f7808 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -86,7 +86,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(self) -> generic::SecretKey + pub fn export(&self) -> generic::SecretKey where B: AsRef<[u8]> + AsMut<[u8]> + From>, { @@ -158,3 +158,30 @@ impl fmt::Display for ImportError { } impl std::error::Error for ImportError {} + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{base::iana::SecAlg, sign::generic}; + + const ALGORITHMS: &[SecAlg] = + &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + + #[test] + fn generate_all() { + for &algorithm in ALGORITHMS { + let _ = super::generate(algorithm).unwrap(); + } + } + + #[test] + fn export_and_import() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let exp: generic::SecretKey> = key.export(); + let imp = super::SecretKey::import(exp).unwrap(); + assert!(key.pkey.public_eq(&imp.pkey)); + } + } +} From 4d912fb32777c32236188329051bb0208937e221 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:39:45 +0200 Subject: [PATCH 046/569] [sign/openssl] Add support for ECDSA --- src/sign/openssl.rs | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 13c1f7808..d35f45850 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -60,7 +60,32 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - // TODO: Support ECDSA. + generic::SecretKey::EcdsaP256Sha256(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } + generic::SecretKey::EcdsaP384Sha384(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::SECP384R1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } generic::SecretKey::Ed25519(k) => { PKey::private_key_from_raw_bytes( k.as_ref(), @@ -72,7 +97,6 @@ impl SecretKey { PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) .unwrap() } - _ => return Err(ImportError::UnsupportedAlgorithm), }; Ok(Self { @@ -90,6 +114,7 @@ impl SecretKey { where B: AsRef<[u8]> + AsMut<[u8]> + From>, { + // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); @@ -104,6 +129,16 @@ impl SecretKey { q_i: key.iqmp().unwrap().to_vec().into(), }) } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) + } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); generic::SecretKey::Ed25519(key.try_into().unwrap()) @@ -130,6 +165,20 @@ pub fn generate(algorithm: SecAlg) -> Option { SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) .and_then(PKey::from_rsa) .unwrap(), + SecAlg::ECDSAP256SHA256 => { + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } + SecAlg::ECDSAP384SHA384 => { + let group = openssl::nid::Nid::SECP384R1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), SecAlg::ED448 => PKey::generate_ed448().unwrap(), _ => return None, @@ -165,8 +214,13 @@ mod tests { use crate::{base::iana::SecAlg, sign::generic}; - const ALGORITHMS: &[SecAlg] = - &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + const ALGORITHMS: &[SecAlg] = &[ + SecAlg::RSASHA256, + SecAlg::ECDSAP256SHA256, + SecAlg::ECDSAP384SHA384, + SecAlg::ED25519, + SecAlg::ED448, + ]; #[test] fn generate_all() { From 24f6043f0b54964f7890cb1b34b0dbe418e34e3f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:41:36 +0200 Subject: [PATCH 047/569] [sign/openssl] satisfy clippy --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index d35f45850..1211d6225 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -67,7 +67,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) @@ -80,7 +80,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) From 1b5d640b5b3967bc7a10cf18e3302c38584e1343 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:57:33 +0200 Subject: [PATCH 048/569] [sign/openssl] Implement the 'Sign' trait --- src/sign/openssl.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 1211d6225..663e8a904 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -13,7 +13,7 @@ use openssl::{ use crate::base::iana::SecAlg; -use super::generic; +use super::{generic, Sign}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -152,6 +152,36 @@ impl SecretKey { } } +impl Sign> for SecretKey { + type Error = openssl::error::ErrorStack; + + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + let mut signer = match self.algorithm { + SecAlg::RSASHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP256SHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP384SHA384 => { + Signer::new(MessageDigest::sha384(), &self.pkey)? + } + SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, + SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, + _ => unreachable!(), + }; + + signer.sign_oneshot_to_vec(data) + } +} + /// Generate a new secret key for the given algorithm. /// /// If the algorithm is not supported, [`None`] is returned. From fbafbf05f77aefe019f81790cd7b320b297aa6d9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:24:02 +0200 Subject: [PATCH 049/569] Install OpenSSL in CI builds --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224b..99a36d6cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,20 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install libssl-dev + echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + - if: matrix.os == 'windows-latest' + run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features --all-targets -- -D warnings + run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features --all-targets - - run: cargo test --all-features + - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets + - run: cargo test $OPENSSL_FLAVOR --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest @@ -37,6 +43,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.68.2" + - name: Install OpenSSL + run: sudo apt install libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions From 3358747866f26d0712b55f8b000acf07a3609f79 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:28 +0200 Subject: [PATCH 050/569] Ensure 'openssl' dep supports 3.x.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 036519e3e..90d756b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10", optional = true } +openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x 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 } From e26b68d840dee68b2aba69c07c21405ad0d160f3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:52 +0200 Subject: [PATCH 051/569] [workflows/ci] Use 'vcpkg' instead of vendoring OpenSSL --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99a36d6cc..18a8bdb13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,22 @@ jobs: with: rust-version: ${{ matrix.rust }} - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt install libssl-dev - echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' - run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: x64-windows-release + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets - - run: cargo test $OPENSSL_FLAVOR --all-features + - run: cargo check --no-default-features --all-targets + - run: cargo test --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest From c1f3178a4c76c2a981d1f69468559e4533c1f419 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:55:18 +0200 Subject: [PATCH 052/569] Ensure 'openssl' dep exposes necessary interfaces --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 90d756b1b..3e045d822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x +openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions 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 } From 9c4f7b49bd712e320c17d329da618d4d39e4ec81 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:03:14 +0200 Subject: [PATCH 053/569] [workflows/ci] Record location of 'vcpkg' --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18a8bdb13..362b3e146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true + - if: matrix.os == 'windows-latest' + run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 2cae3cc43c5d45310a52261cea4d499c73710708 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:13:22 +0200 Subject: [PATCH 054/569] [workflows/ci] Use a YAML def for 'VCPKG_ROOT' --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362b3e146..514844da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" steps: - name: Checkout repository uses: actions/checkout@v1 @@ -26,8 +27,6 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true - - if: matrix.os == 'windows-latest' - run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 9ed98eda61f519f5afea1beb0dbb1290f81a5d3e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:16 +0200 Subject: [PATCH 055/569] [workflows/ci] Fix a vcpkg triplet to use --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 514844da8..12334fa51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: env: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release steps: - name: Checkout repository uses: actions/checkout@v1 @@ -24,7 +25,7 @@ jobs: uses: johnwason/vcpkg-action@v6 with: pkgs: openssl - triplet: x64-windows-release + triplet: ${{ env.VCPKGRS_TRIPLET }} token: ${{ github.token }} github-binarycache: true - if: matrix.rust == 'stable' From a1a5a0b74d9f5f91736a9722d6c613a11de007e2 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:43 +0200 Subject: [PATCH 056/569] Upgrade openssl to 0.10.57 for bitflags 2.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3e045d822..ed7edc95b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions +openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x 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 } From 0b85a4fd12491a95c07c29d43f8698f5fa6e460c Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:22:18 +0200 Subject: [PATCH 057/569] [workflows/ci] Use dynamic linking for vcpkg openssl --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12334fa51..23c73a5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 From e6bf6d9fca7ed3ab1f7663a0a3cea68589a9f094 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:24:05 +0200 Subject: [PATCH 058/569] [workflows/ci] Correctly annotate 'vcpkg' --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23c73a5ee..299da6658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - if: matrix.os == 'ubuntu-latest' run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' + id: vcpkg uses: johnwason/vcpkg-action@v6 with: pkgs: openssl From 2ab7178e73c2cf854480b643ab5068154e6c7fab Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:51:14 +0200 Subject: [PATCH 059/569] [sign/openssl] Implement exporting public keys --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 663e8a904..0147222f6 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -150,6 +150,55 @@ impl SecretKey { _ => unreachable!(), } } + + /// Export this key into a generic public key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } + } } impl Sign> for SecretKey { @@ -268,4 +317,12 @@ mod tests { assert!(key.pkey.public_eq(&imp.pkey)); } } + + #[test] + fn export_public() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let _: generic::PublicKey> = key.export_public(); + } + } } From d8c9b5fe9a77e166dcb6b5ffbb36030a058167d5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:56:16 +0200 Subject: [PATCH 060/569] [sign/ring] Implement exporting public keys --- src/sign/ring.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 872f8dadb..185b97295 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -55,6 +55,28 @@ impl<'a> SecretKey<'a> { _ => Err(ImportError::UnsupportedAlgorithm), } } + + /// Export this key into a generic public key. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + Self::Ed25519(key) => { + use ring::signature::KeyPair; + let key = key.public_key().as_ref(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } } /// An error in importing a key into `ring`. From 90ed9363180db7d3f423acbee13513efad61344c Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 19:39:34 +0200 Subject: [PATCH 061/569] [sign/generic] Test (de)serialization for generic secret keys There were bugs in the Base64 encoding/decoding that are not worth trying to debug; there's a perfectly usable Base64 implementation in the crate already. --- src/sign/generic.rs | 272 +++++------------- test-data/dnssec-keys/Ktest.+008+55993.key | 1 + .../dnssec-keys/Ktest.+008+55993.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 + .../dnssec-keys/Ktest.+013+40436.private | 3 + test-data/dnssec-keys/Ktest.+014+17013.key | 1 + .../dnssec-keys/Ktest.+014+17013.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 + .../dnssec-keys/Ktest.+015+43769.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 + .../dnssec-keys/Ktest.+016+34114.private | 3 + 11 files changed, 100 insertions(+), 199 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.private create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f963a8def..01505239d 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -4,6 +4,7 @@ use std::vec::Vec; use crate::base::iana::SecAlg; use crate::rdata::Dnskey; +use crate::utils::base64; /// A generic secret key. /// @@ -56,6 +57,7 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Private-key-format: v1.2\n")?; match self { Self::RsaSha256(k) => { w.write_str("Algorithm: 8 (RSASHA256)\n")?; @@ -64,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } } } @@ -107,11 +109,12 @@ impl + AsMut<[u8]>> SecretKey { return Err(DnsFormatError::Misformatted); } - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } + let buf: Vec = base64::decode(val) + .map_err(|_| DnsFormatError::Misformatted)?; + let buf = buf + .as_slice() + .try_into() + .map_err(|_| DnsFormatError::Misformatted)?; Ok(buf) } @@ -205,22 +208,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_str("Modulus: ")?; + write!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("\nPublicExponent: ")?; + write!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("\nPrivateExponent: ")?; + write!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("\nPrime1: ")?; + write!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("\nPrime2: ")?; + write!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("\nExponent1: ")?; + write!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("\nExponent2: ")?; + write!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("\nCoefficient: ")?; + write!(w, "{}", base64::encode_display(&self.q_i))?; w.write_char('\n') } @@ -258,10 +261,8 @@ impl + AsMut<[u8]>> RsaSecretKey { return Err(DnsFormatError::Misformatted); } - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) + let buffer: Vec = base64::decode(val) .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); *field = Some(buffer.into()); data = rest; @@ -428,6 +429,11 @@ fn parse_dns_pair( // Trim any pending newlines. let data = data.trim_start(); + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); @@ -439,177 +445,6 @@ fn parse_dns_pair( Ok(Some((key.trim(), val.trim(), rest))) } -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DnsFormatError { @@ -634,3 +469,42 @@ impl fmt::Display for DnsFormatError { } impl std::error::Error for DnsFormatError {} + +#[cfg(test)] +mod tests { + use std::{string::String, vec::Vec}; + + use crate::base::iana::SecAlg; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 55993), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + let mut same = String::new(); + key.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key new file mode 100644 index 000000000..8248fbfe8 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private new file mode 100644 index 000000000..7a260e7a0 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= +PublicExponent: AQAB +PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= +Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== +Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== +Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== +Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== +Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key new file mode 100644 index 000000000..7f7cd0fcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+40436.private new file mode 100644 index 000000000..39f5e8a8d --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key new file mode 100644 index 000000000..c7b6aa1d4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private new file mode 100644 index 000000000..9648a876a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key new file mode 100644 index 000000000..8a1f24f67 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private new file mode 100644 index 000000000..e178a3bd4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key new file mode 100644 index 000000000..fc77e0491 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private new file mode 100644 index 000000000..fca7303dc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From fff95955d351ed2675b821eb3d717638225d9e59 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:03:03 +0200 Subject: [PATCH 062/569] [sign] Thoroughly test import/export in both backends I had to swap out the RSA key since 'ring' found it to be too small. --- src/sign/generic.rs | 2 +- src/sign/openssl.rs | 73 +++++++++++++++---- src/sign/ring.rs | 57 +++++++++++++++ test-data/dnssec-keys/Ktest.+008+27096.key | 1 + .../dnssec-keys/Ktest.+008+27096.private | 10 +++ test-data/dnssec-keys/Ktest.+008+55993.key | 1 - .../dnssec-keys/Ktest.+008+55993.private | 10 --- 7 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 01505239d..5626e6ce9 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -477,7 +477,7 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 55993), + (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), (SecAlg::ECDSAP384SHA384, 17013), (SecAlg::ED25519, 43769), diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 0147222f6..9154abd55 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -289,28 +289,32 @@ impl std::error::Error for ImportError {} #[cfg(test)] mod tests { - use std::vec::Vec; + use std::{string::String, vec::Vec}; - use crate::{base::iana::SecAlg, sign::generic}; + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; - const ALGORITHMS: &[SecAlg] = &[ - SecAlg::RSASHA256, - SecAlg::ECDSAP256SHA256, - SecAlg::ECDSAP384SHA384, - SecAlg::ED25519, - SecAlg::ED448, + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), ]; #[test] - fn generate_all() { - for &algorithm in ALGORITHMS { + fn generate() { + for &(algorithm, _) in KEYS { let _ = super::generate(algorithm).unwrap(); } } #[test] - fn export_and_import() { - for &algorithm in ALGORITHMS { + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let exp: generic::SecretKey> = key.export(); let imp = super::SecretKey::import(exp).unwrap(); @@ -318,11 +322,50 @@ mod tests { } } + #[test] + fn imported_roundtrip() { + type GenericKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let imp = GenericKey::from_dns(&data).unwrap(); + let key = super::SecretKey::import(imp).unwrap(); + let exp: GenericKey = key.export(); + let mut same = String::new(); + exp.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } + #[test] fn export_public() { - for &algorithm in ALGORITHMS { - let key = super::generate(algorithm).unwrap(); - let _: generic::PublicKey> = key.export_public(); + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 185b97295..edea8ae14 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -3,6 +3,7 @@ #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] +use core::fmt; use std::vec::Vec; use crate::base::iana::SecAlg; @@ -42,6 +43,7 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) + .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } @@ -80,6 +82,7 @@ impl<'a> SecretKey<'a> { } /// An error in importing a key into `ring`. +#[derive(Clone, Debug)] pub enum ImportError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -88,6 +91,15 @@ pub enum ImportError { InvalidKey, } +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} + impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; @@ -110,3 +122,48 @@ impl<'a> super::Sign> for SecretKey<'a> { } } } + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; + + const KEYS: &[(SecAlg, u16)] = + &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + + #[test] + fn export_public() { + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key new file mode 100644 index 000000000..5aa614f71 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private new file mode 100644 index 000000000..b5819714f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 +PublicExponent: AQAB +PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN +Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj +Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd +Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf +Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 +Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key deleted file mode 100644 index 8248fbfe8..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private deleted file mode 100644 index 7a260e7a0..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= -PublicExponent: AQAB -PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= -Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== -Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== -Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== -Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== -Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== From 4c6aa4d5a619f0a40c279de2df6be494418c3bee Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:06:58 +0200 Subject: [PATCH 063/569] [sign] Remove debugging code and satisfy clippy --- src/sign/generic.rs | 8 ++++---- src/sign/ring.rs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 5626e6ce9..8dd610637 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -66,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index edea8ae14..864480933 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -43,7 +43,6 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) - .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } From fe29593a5d812f165d48f22c8f0115aeb96f4a06 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:20:15 +0200 Subject: [PATCH 064/569] [sign] Account for CR LF in tests --- src/sign/generic.rs | 46 +++++++++++++++++++++++---------------------- src/sign/openssl.rs | 2 ++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8dd610637..8ad44ea88 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -57,30 +57,30 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Private-key-format: v1.2\n")?; + writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; + writeln!(w, "Algorithm: 8 (RSASHA256)")?; k.into_dns(w) } Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; + writeln!(w, "Algorithm: 15 (ED25519)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; + writeln!(w, "Algorithm: 16 (ED448)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } @@ -209,22 +209,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; - write!(w, "{}", base64::encode_display(&self.n))?; - w.write_str("\nPublicExponent: ")?; - write!(w, "{}", base64::encode_display(&self.e))?; - w.write_str("\nPrivateExponent: ")?; - write!(w, "{}", base64::encode_display(&self.d))?; - w.write_str("\nPrime1: ")?; - write!(w, "{}", base64::encode_display(&self.p))?; - w.write_str("\nPrime2: ")?; - write!(w, "{}", base64::encode_display(&self.q))?; - w.write_str("\nExponent1: ")?; - write!(w, "{}", base64::encode_display(&self.d_p))?; - w.write_str("\nExponent2: ")?; - write!(w, "{}", base64::encode_display(&self.d_q))?; - w.write_str("\nCoefficient: ")?; - write!(w, "{}", base64::encode_display(&self.q_i))?; - w.write_char('\n') + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i))?; + Ok(()) } /// Parse a key from the conventional DNS format. @@ -504,6 +504,8 @@ mod tests { let key = super::SecretKey::>::from_dns(&data).unwrap(); let mut same = String::new(); key.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9154abd55..2377dc250 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -335,6 +335,8 @@ mod tests { let exp: GenericKey = key.export(); let mut same = String::new(); exp.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } From 8536c4c6fbc191161ff9a3530de34ebd04c5cb9b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 11 Oct 2024 16:16:12 +0200 Subject: [PATCH 065/569] [sign/openssl] Fix bugs in the signing procedure - RSA signatures were being made with an unspecified padding scheme. - ECDSA signatures were being output in ASN.1 DER format, instead of the fixed-size format required by DNSSEC (and output by 'ring'). - Tests for signature failures are now added for both backends. --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++-------- src/sign/ring.rs | 19 ++++++++++++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 2377dc250..8faa48f9e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -8,6 +8,7 @@ use std::vec::Vec; use openssl::{ bn::BigNum, + ecdsa::EcdsaSig, pkey::{self, PKey, Private}, }; @@ -212,22 +213,42 @@ impl Sign> for SecretKey { use openssl::hash::MessageDigest; use openssl::sign::Signer; - let mut signer = match self.algorithm { + match self.algorithm { SecAlg::RSASHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) } SecAlg::ECDSAP256SHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(32).unwrap(); + let s = signature.s().to_vec_padded(32).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) } SecAlg::ECDSAP384SHA384 => { - Signer::new(MessageDigest::sha384(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(48).unwrap(); + let s = signature.s().to_vec_padded(48).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) + } + SecAlg::ED25519 | SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) } - SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, - SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, _ => unreachable!(), - }; - - signer.sign_oneshot_to_vec(data) + } } } @@ -294,7 +315,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = &[ @@ -370,4 +391,20 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 864480933..0996552f6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -129,7 +129,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = @@ -165,4 +165,21 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } From 07b52ce43772781f882523e884f8ca66da2e827b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 15 Oct 2024 17:32:36 +0200 Subject: [PATCH 066/569] Refactor the 'sign' module Most functions have been renamed. The public key types have been moved to the 'validate' module (which 'sign' now depends on), and they have been outfitted with conversions (e.g. to and from DNSKEY records). Importing a generic key into an OpenSSL or Ring key now requires the public key to also be available. In both implementations, the pair are checked for consistency -- this ensures that both are uncorrupted and that keys have not been mixed up. This also allows the Ring backend to support ECDSA keys (although key generation is still difficult). The 'PublicKey' and 'PrivateKey' enums now store their array data in 'Box'. This has two benefits: it is easier to securely manage memory on the heap (since the compiler will not copy it around the stack); and the smaller sizes of the types is beneficial (although negligibly) to performance. --- Cargo.toml | 3 +- src/sign/generic.rs | 393 ++++++++++++++++++++------------------------ src/sign/mod.rs | 81 ++++++--- src/sign/openssl.rs | 304 +++++++++++++++++++--------------- src/sign/ring.rs | 241 ++++++++++++++++++--------- src/validate.rs | 347 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 910 insertions(+), 459 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed7edc95b..2bc526f81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,10 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std"] +sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8ad44ea88..2589a6ab4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,10 +1,11 @@ -use core::{fmt, mem, str}; +use core::{fmt, str}; +use std::boxed::Box; use std::vec::Vec; use crate::base::iana::SecAlg; -use crate::rdata::Dnskey; use crate::utils::base64; +use crate::validate::RsaPublicKey; /// A generic secret key. /// @@ -14,32 +15,97 @@ use crate::utils::base64; /// cryptographic implementation supports it). /// /// [`Sign`]: super::Sign -pub enum SecretKey + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaSecretKey), +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +pub enum SecretKey { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), + EcdsaP256Sha256(Box<[u8; 32]>), /// An ECDSA P-384/SHA-384 keypair. /// /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), + EcdsaP384Sha384(Box<[u8; 48]>), /// An Ed25519 keypair. /// /// The private key is a single 32-byte string. - Ed25519([u8; 32]), + Ed25519(Box<[u8; 32]>), /// An Ed448 keypair. /// /// The private key is a single 57-byte string. - Ed448([u8; 57]), + Ed448(Box<[u8; 57]>), } -impl + AsMut<[u8]>> SecretKey { +impl SecretKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -51,99 +117,99 @@ impl + AsMut<[u8]>> SecretKey { } } - /// Serialize this key in the conventional DNS format. + /// Serialize this key in the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { writeln!(w, "Algorithm: 8 (RSASHA256)")?; - k.into_dns(w) + k.format_as_bind(w) } Self::EcdsaP256Sha256(s) => { writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::EcdsaP384Sha384(s) => { writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed25519(s) => { writeln!(w, "Algorithm: 15 (ED25519)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed448(s) => { writeln!(w, "Algorithm: 16 (ED448)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } } } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { /// Parse private keys for most algorithms (except RSA). fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_dns_pair(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } - let buf: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; - let buf = buf - .as_slice() - .try_into() - .map_err(|_| DnsFormatError::Misformatted)?; + return base64::decode::>(val) + .map_err(|_| BindFormatError::Misformatted)? + .into_boxed_slice() + .try_into() + .map_err(|_| BindFormatError::Misformatted); + } - Ok(buf) + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) } // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .map_or(false, |minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; + .ok_or(BindFormatError::Misformatted)?; // Parse the algorithm. let mut words = val.split_whitespace(); let code = words .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; if words.next().is_some() { - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } match (code, name) { (8, "(RSASHA256)") => { - RsaSecretKey::from_dns(data).map(Self::RsaSha256) + RsaSecretKey::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -153,12 +219,12 @@ impl + AsMut<[u8]>> SecretKey { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), + _ => Err(BindFormatError::UnsupportedAlgorithm), } } } -impl + AsMut<[u8]>> Drop for SecretKey { +impl Drop for SecretKey { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -175,39 +241,40 @@ impl + AsMut<[u8]>> Drop for SecretKey { /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaSecretKey + AsMut<[u8]>> { +pub struct RsaSecretKey { /// The public modulus. - pub n: B, + pub n: Box<[u8]>, /// The public exponent. - pub e: B, + pub e: Box<[u8]>, /// The private exponent. - pub d: B, + pub d: Box<[u8]>, /// The first prime factor of `d`. - pub p: B, + pub p: Box<[u8]>, /// The second prime factor of `d`. - pub q: B, + pub q: Box<[u8]>, /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, + pub d_p: Box<[u8]>, /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, + pub d_q: Box<[u8]>, /// The inverse of the second prime factor modulo the first. - pub q_i: B, + pub q_i: Box<[u8]>, } -impl + AsMut<[u8]>> RsaSecretKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. +impl RsaSecretKey { + /// Serialize this key in the conventional format used by BIND. /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKey`] for a + /// description of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -227,13 +294,13 @@ impl + AsMut<[u8]>> RsaSecretKey { Ok(()) } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKey`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; let mut d = None; @@ -253,25 +320,28 @@ impl + AsMut<[u8]>> RsaSecretKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), + _ => { + data = rest; + continue; + } }; if field.is_some() { // This field has already been filled. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } let buffer: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; + .map_err(|_| BindFormatError::Misformatted)?; - *field = Some(buffer.into()); + *field = Some(buffer.into_boxed_slice()); data = rest; } for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } } @@ -288,142 +358,33 @@ impl + AsMut<[u8]>> RsaSecretKey { } } -impl + AsMut<[u8]>> Drop for RsaSecretKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// A generic public key. -pub enum PublicKey> { - /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKey), - - // TODO: RSA/SHA-1 with NSEC3/SHA-1? - /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKey), - - /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKey), - - /// An ECDSA P-256/SHA-256 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (32 bytes). - /// - The encoding of the `y` coordinate (32 bytes). - EcdsaP256Sha256([u8; 65]), - - /// An ECDSA P-384/SHA-384 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (48 bytes). - /// - The encoding of the `y` coordinate (48 bytes). - EcdsaP384Sha384([u8; 97]), - - /// An Ed25519 public key. - /// - /// The public key is a 32-byte encoding of the public point. - Ed25519([u8; 32]), - - /// An Ed448 public key. - /// - /// The public key is a 57-byte encoding of the public point. - Ed448([u8; 57]), -} - -impl> PublicKey { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, +impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { + fn from(value: &'a RsaSecretKey) -> Self { + RsaPublicKey { + n: value.n.clone(), + e: value.e.clone(), } } - - /// Construct a DNSKEY record with the given flags. - pub fn into_dns(self, flags: u16) -> Dnskey - where - Octs: From> + AsRef<[u8]>, - { - let protocol = 3u8; - let algorithm = self.algorithm(); - let public_key = match self { - Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { - let (n, e) = (k.n.as_ref(), k.e.as_ref()); - let e_len_len = if e.len() < 256 { 1 } else { 3 }; - let len = e_len_len + e.len() + n.len(); - let mut buf = Vec::with_capacity(len); - if let Ok(e_len) = u8::try_from(e.len()) { - buf.push(e_len); - } else { - // RFC 3110 is not explicit about the endianness of this, - // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network - // byte order, which I suppose makes sense. - let e_len = u16::try_from(e.len()).unwrap(); - buf.extend_from_slice(&e_len.to_be_bytes()); - } - buf.extend_from_slice(e); - buf.extend_from_slice(n); - buf - } - - // From my reading of RFC 6605, the marker byte is not included. - Self::EcdsaP256Sha256(k) => k[1..].to_vec(), - Self::EcdsaP384Sha384(k) => k[1..].to_vec(), - - Self::Ed25519(k) => k.to_vec(), - Self::Ed448(k) => k.to_vec(), - }; - - Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() - } -} - -/// A generic RSA public key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaPublicKey> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, } -impl From> for RsaPublicKey -where - B: AsRef<[u8]> + AsMut<[u8]> + Default, -{ - fn from(mut value: RsaSecretKey) -> Self { - Self { - n: mem::take(&mut value.n), - e: mem::take(&mut value.e), - } +impl Drop for RsaSecretKey { + 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); } } /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, -) -> Result, DnsFormatError> { +) -> Result, BindFormatError> { // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. // Trim any pending newlines. @@ -439,7 +400,7 @@ fn parse_dns_pair( // Split the line by a colon. let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + line.split_once(':').ok_or(BindFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). Ok(Some((key.trim(), val.trim(), rest))) @@ -447,7 +408,7 @@ fn parse_dns_pair( /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { +pub enum BindFormatError { /// The key file uses an unsupported version of the format. UnsupportedFormat, @@ -458,7 +419,7 @@ pub enum DnsFormatError { UnsupportedAlgorithm, } -impl fmt::Display for DnsFormatError { +impl fmt::Display for BindFormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedFormat => "unsupported format", @@ -468,7 +429,7 @@ impl fmt::Display for DnsFormatError { } } -impl std::error::Error for DnsFormatError {} +impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { @@ -490,7 +451,7 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -501,9 +462,9 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); let mut same = String::new(); - key.into_dns(&mut same).unwrap(); + key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b1db46c26..b9773d7f0 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,37 +2,44 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a -//! DNS record served by a secure-aware name server. But name servers are not -//! usually creating those signatures themselves. Within a DNS zone, it is the -//! zone administrator's responsibility to sign zone records (when the record's -//! time-to-live expires and/or when it changes). Those signatures are stored -//! as regular DNS data and automatically served by name servers. +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of +//! a DNS record served by a security-aware name server. Signatures can be +//! made "online" (in an authoritative name server while it is running) or +//! "offline" (outside of a name server). Once generated, signatures can be +//! serialized as DNS records and stored alongside the authenticated records. #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, Signature}, +}; pub mod generic; -pub mod key; pub mod openssl; -pub mod records; pub mod ring; -/// Signing DNS records. +/// Sign DNS records. /// -/// Implementors of this trait own a private key and sign DNS records for a zone -/// with that key. Signing is a synchronous operation performed on the current -/// thread; this rules out implementations like HSMs, where I/O communication is -/// necessary. -pub trait Sign { - /// An error in constructing a signature. - type Error; - +/// Types that implement this trait own a private key and can sign arbitrary +/// 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()`] 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()`]: Sign::sign() +pub trait Sign { /// The signature algorithm used. /// - /// The following algorithms can be used: + /// The following algorithms are known to this crate. Recommendations + /// toward or against usage are based on published RFCs, not the crate + /// authors' opinion. Implementing types may choose to support some of + /// the prohibited algorithms anyway. + /// /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) /// - [`SecAlg::DSA`] (highly insecure, do not use) /// - [`SecAlg::RSASHA1`] (insecure, not recommended) @@ -47,11 +54,35 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// Compute a signature. + /// The public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn public_key(&self) -> PublicKey; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// There are three expected failure cases for this function: + /// + /// - The secret key was invalid. The implementing type is responsible + /// for validating the secret key during initialization, so that this + /// kind of error does not occur. + /// + /// - Not enough randomness could be obtained. This applies to signature + /// algorithms which use randomization (primarily ECDSA). On common + /// platforms like Linux, Mac OS, and Windows, cryptographically secure + /// pseudo-random number generation is provided by the OS, so this is + /// highly unlikely. + /// + /// - Not enough memory could be obtained. Signature generation does not + /// require significant memory and an out-of-memory condition means that + /// the application will probably panic soon. /// - /// A regular signature of the given byte sequence is computed and is turned - /// into the selected buffer type. This provides a lot of flexibility in - /// how buffers are constructed; they may be heap-allocated or have a static - /// size. - fn sign(&self, data: &[u8]) -> Result; + /// None of these are considered likely or recoverable, so panicking is + /// the simplest and most ergonomic solution. + fn sign(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 8faa48f9e..5c708f485 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,10 +1,7 @@ //! Key and Signer using OpenSSL. -#![cfg(feature = "openssl")] -#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] - use core::fmt; -use std::vec::Vec; +use std::boxed::Box; use openssl::{ bn::BigNum, @@ -12,7 +9,10 @@ use openssl::{ pkey::{self, PKey, Private}, }; -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; use super::{generic, Sign}; @@ -31,25 +31,31 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, - ) -> Result { + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, + ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); v.copy_from_slice(slice).unwrap(); v } - let pkey = match &key { - generic::SecretKey::RsaSha256(k) => { - let n = BigNum::from_slice(k.n.as_ref()).unwrap(); - let e = BigNum::from_slice(k.e.as_ref()).unwrap(); - let d = num(k.d.as_ref()); - let p = num(k.p.as_ref()); - let q = num(k.q.as_ref()); - let d_p = num(k.d_p.as_ref()); - let d_q = num(k.d_q.as_ref()); - let q_i = num(k.q_i.as_ref()); + let pkey = match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + + let n = BigNum::from_slice(&s.n).unwrap(); + let e = BigNum::from_slice(&s.e).unwrap(); + let d = num(&s.d); + let p = num(&s.p); + let q = num(&s.q); + let d_p = num(&s.d_p); + let d_q = num(&s.d_q); + let q_i = num(&s.q_i); // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the @@ -61,47 +67,75 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - generic::SecretKey::EcdsaP256Sha256(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::EcdsaP384Sha384(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::SECP384R1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::SECP384R1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::Ed25519(k) => { - PKey::private_key_from_raw_bytes( - k.as_ref(), - pkey::Id::ED25519, - ) - .unwrap() + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } - generic::SecretKey::Ed448(k) => { - PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) - .unwrap() + + (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } + + // The public and private key types did not match. + _ => return Err(FromGenericError::InvalidKey), }; Ok(Self { - algorithm: key.algorithm(), + algorithm: secret.algorithm(), pkey, }) } @@ -111,10 +145,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(&self) -> generic::SecretKey - where - B: AsRef<[u8]> + AsMut<[u8]> + From>, - { + pub fn to_generic(&self) -> generic::SecretKey { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { @@ -151,20 +182,18 @@ impl SecretKey { _ => unreachable!(), } } +} - /// Export this key into a generic public key. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { +impl Sign for SecretKey { + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn public_key(&self) -> PublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { + PublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -177,7 +206,7 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -187,65 +216,69 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + PublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed448(key.try_into().unwrap()) + PublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } -} - -impl Sign> for SecretKey { - type Error = openssl::error::ErrorStack; - fn algorithm(&self) -> SecAlg { - self.algorithm - } - - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn sign(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; match self.algorithm { SecAlg::RSASHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; - s.sign_oneshot_to_vec(data) + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + s.set_rsa_padding(openssl::rsa::Padding::PKCS1).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); + Signature::RsaSha256(signature.into_boxed_slice()) } SecAlg::ECDSAP256SHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(32).unwrap(); let s = signature.s().to_vec_padded(32).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 64]); + signature[..32].copy_from_slice(&r); + signature[32..].copy_from_slice(&s); + Signature::EcdsaP256Sha256(signature) } SecAlg::ECDSAP384SHA384 => { - let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha384(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(48).unwrap(); let s = signature.s().to_vec_padded(48).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 96]); + signature[..48].copy_from_slice(&r); + signature[48..].copy_from_slice(&s); + Signature::EcdsaP384Sha384(signature) + } + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed25519(signature.try_into().unwrap()) } - SecAlg::ED25519 | SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed448(signature.try_into().unwrap()) } _ => unreachable!(), } @@ -289,15 +322,15 @@ pub fn generate(algorithm: SecAlg) -> Option { /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, - /// The provided secret key was invalid. + /// The key's parameters were invalid. InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -306,18 +339,20 @@ impl fmt::Display for ImportError { } } -impl std::error::Error for ImportError {} +impl std::error::Error for FromGenericError {} #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), @@ -337,25 +372,32 @@ mod tests { fn generated_roundtrip() { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); - let exp: generic::SecretKey> = key.export(); - let imp = super::SecretKey::import(exp).unwrap(); - assert!(key.pkey.public_eq(&imp.pkey)); + let gen_key = key.to_generic(); + let pub_key = key.public_key(); + let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); } } #[test] fn imported_roundtrip() { - type GenericKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let imp = GenericKey::from_dns(&data).unwrap(); - let key = super::SecretKey::import(imp).unwrap(); - let exp: GenericKey = key.export(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + + let equiv = key.to_generic(); let mut same = String::new(); - exp.into_dns(&mut same).unwrap(); + equiv.format_as_bind(&mut same).unwrap(); + let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); @@ -363,48 +405,40 @@ mod tests { } #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 0996552f6..2a4867094 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,11 +4,16 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::vec::Vec; +use std::{boxed::Box, vec::Vec}; -use crate::base::iana::SecAlg; +use ring::signature::KeyPair; -use super::generic; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; + +use super::{generic, Sign}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -18,71 +23,97 @@ pub enum SecretKey<'a> { rng: &'a dyn ring::rand::SecureRandom, }, + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, rng: &'a dyn ring::rand::SecureRandom, - ) -> Result { - match &key { - generic::SecretKey::RsaSha256(k) => { + ) -> Result { + match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + let components = ring::rsa::KeyPairComponents { public_key: ring::rsa::PublicKeyComponents { - n: k.n.as_ref(), - e: k.e.as_ref(), + n: s.n.as_ref(), + e: s.e.as_ref(), }, - d: k.d.as_ref(), - p: k.p.as_ref(), - q: k.q.as_ref(), - dP: k.d_p.as_ref(), - dQ: k.d_q.as_ref(), - qInv: k.q_i.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(), }; ring::signature::RsaKeyPair::from_components(&components) - .map_err(|_| ImportError::InvalidKey) + .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } - // TODO: Support ECDSA. - generic::SecretKey::Ed25519(k) => { - let k = k.as_ref(); - ring::signature::Ed25519KeyPair::from_seed_unchecked(k) - .map_err(|_| ImportError::InvalidKey) - .map(Self::Ed25519) + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) } - _ => Err(ImportError::UnsupportedAlgorithm), - } - } - /// Export this key into a generic public key. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { - match self { - Self::RsaSha256 { key, rng: _ } => { - let components: ring::rsa::PublicKeyComponents> = - key.public().into(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { - n: components.n.into(), - e: components.e.into(), - }) + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - Self::Ed25519(key) => { - use ring::signature::KeyPair; - let key = key.public_key().as_ref(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + ring::signature::Ed25519KeyPair::from_seed_and_public_key( + s.as_slice(), + p.as_slice(), + ) + .map_err(|_| FromGenericError::InvalidKey) + .map(Self::Ed25519) } + + (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + Err(FromGenericError::UnsupportedAlgorithm) + } + + // The public and private key types did not match. + _ => Err(FromGenericError::InvalidKey), } } } /// An error in importing a key into `ring`. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -90,7 +121,7 @@ pub enum ImportError { InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -99,87 +130,135 @@ impl fmt::Display for ImportError { } } -impl<'a> super::Sign> for SecretKey<'a> { - type Error = ring::error::Unspecified; - +impl<'a> Sign for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, Self::Ed25519(_) => SecAlg::ED25519, } } - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn public_key(&self) -> PublicKey { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + PublicKey::RsaSha256(RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + + Self::EcdsaP256Sha256 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + + Self::EcdsaP384Sha384 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + + Self::Ed25519(key) => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } + + fn sign(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf)?; - Ok(buf) + key.sign(pad, *rng, data, &mut buf) + .expect("random generators do not fail"); + Signature::RsaSha256(buf.into_boxed_slice()) + } + Self::EcdsaP256Sha256 { key, rng } => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP256Sha256(buf) + } + Self::EcdsaP384Sha384 { key, rng } => { + let mut buf = Box::new([0u8; 96]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP384Sha384(buf) + } + Self::Ed25519(key) => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice(key.sign(data).as_ref()); + Signature::Ed25519(buf) } - Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } #[cfg(test)] mod tests { - use std::vec::Vec; - use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index 41b7456e5..b122c83c9 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -10,14 +10,361 @@ use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; +use crate::base::scan::IterScanner; use crate::base::wire::{Compose, Composer}; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use ring::{digest, signature}; +use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +/// A generic public key. +#[derive(Clone, Debug)] +pub enum PublicKey { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + /// An RSA/SHA-1 with NSEC3 public key. + RsaSha1Nsec3Sha1(RsaPublicKey), + + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256(Box<[u8; 65]>), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384(Box<[u8; 97]>), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519(Box<[u8; 32]>), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448(Box<[u8; 57]>), +} + +impl PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl PublicKey { + /// Parse a public key as stored in a DNSKEY record. + pub fn from_dnskey( + algorithm: SecAlg, + data: &[u8], + ) -> Result { + match algorithm { + SecAlg::RSASHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + } + SecAlg::RSASHA1_NSEC3_SHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + } + SecAlg::RSASHA256 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + } + SecAlg::RSASHA512 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + } + + SecAlg::ECDSAP256SHA256 => { + let mut key = Box::new([0u8; 65]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP256Sha256(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + SecAlg::ECDSAP384SHA384 => { + let mut key = Box::new([0u8; 97]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP384Sha384(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + + SecAlg::ED25519 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed25519) + .map_err(|_| FromDnskeyError::InvalidKey), + SecAlg::ED448 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed448) + .map_err(|_| FromDnskeyError::InvalidKey), + + _ => Err(FromDnskeyError::UnsupportedAlgorithm), + } + } + + /// Parse a public key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn from_dnskey_text( + dnskey: &str, + ) -> Result { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(FromDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Ensure the record header looks reasonable. + let mut words = line.split_ascii_whitespace().skip(2); + if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { + return Err(FromDnskeyTextError::Misformatted); + } + + // Parse the DNSKEY record data. + let mut data = IterScanner::new(words); + let dnskey: Dnskey> = Dnskey::scan(&mut data) + .map_err(|_| FromDnskeyTextError::Misformatted)?; + println!("importing {:?}", dnskey); + Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) + .map_err(FromDnskeyTextError::FromDnskey) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.to_dnskey(), + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].into(), + Self::EcdsaP384Sha384(k) => k[1..].into(), + + Self::Ed25519(k) => k.as_slice().into(), + Self::Ed448(k) => k.as_slice().into(), + } + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + match (self, other) { + (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, + (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, + (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, + (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, + (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed25519(a), Self::Ed25519(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed448(a), Self::Ed448(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + _ => false, + } + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +#[derive(Clone, Debug)] +pub struct RsaPublicKey { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, +} + +impl RsaPublicKey { + /// Parse an RSA public key as stored in a DNSKEY record. + pub fn from_dnskey(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(FromDnskeyError::InvalidKey); + } + + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else if data[1..3] != [0, 0] { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + } else { + // The extended encoding of the length just held a zero value. + return Err(FromDnskeyError::InvalidKey); + }; + + // NOTE: off <= 3 so is safe to index up to. + let e = data[off..] + .get(..exp_len) + .ok_or(FromDnskeyError::InvalidKey)? + .into(); + + // NOTE: The previous statement indexed up to 'exp_len'. + let n = data[off + exp_len..].into(); + + Ok(Self { n, e }) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + key.reserve_exact(1 + self.e.len() + self.n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + key.reserve_exact(3 + self.e.len() + self.n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(&*self.e); + key.extend(&*self.n); + key.into_boxed_slice() + } +} + +impl PartialEq for RsaPublicKey { + fn eq(&self, other: &Self) -> bool { + /// Compare after stripping leading zeros. + fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { + let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; + let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; + if a.len() == b.len() { + ring::constant_time::verify_slices_are_equal(a, b).is_ok() + } else { + false + } + } + + cmp_without_leading(&self.n, &other.n) + && cmp_without_leading(&self.e, &other.e) + } +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +pub enum Signature { + RsaSha1(Box<[u8]>), + RsaSha1Nsec3Sha1(Box<[u8]>), + RsaSha256(Box<[u8]>), + RsaSha512(Box<[u8]>), + EcdsaP256Sha256(Box<[u8; 64]>), + EcdsaP384Sha384(Box<[u8; 96]>), + Ed25519(Box<[u8; 64]>), + Ed448(Box<[u8; 114]>), +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From c1f58417d4acc4d7dc0593888e1059b3c5fae0b6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Oct 2024 18:23:41 +0200 Subject: [PATCH 067/569] WIP --- src/sign/records.rs | 107 +++++++++++++++++++++++++++++++------------- 1 file changed, 76 insertions(+), 31 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 16cf5ea14..1f9c729fd 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -80,7 +80,8 @@ impl SortedRecords { apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - key: Key, + ksk: Key, + zsk: Option, ) -> Result>>, Key::Error> where N: ToName + Clone, @@ -89,6 +90,15 @@ impl SortedRecords { Octets: From + AsRef<[u8]>, ApexName: ToName + Clone, { + let csk = zsk.is_none(); + let zsk = zsk.as_ref().unwrap_or(&ksk); + let Ok(ksk_dnskey) = ksk.dnskey() else { + unreachable!() + }; // # SigningKey doesn't implement Debug + let Ok(zsk_dnskey) = zsk.dnskey() else { + unreachable!() + }; // # SigningKey doesn't implement Debug + let mut res = Vec::new(); let mut buf = Vec::new(); @@ -146,38 +156,66 @@ impl SortedRecords { // Create the signature. buf.clear(); - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm()?, - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.key_tag()?, - apex.owner().clone(), - ); - rrsig.compose_canonical(&mut buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(&mut buf).unwrap(); + + if rrset.rtype() == Rtype::DNSKEY { + let rrsig = ProtoRrsig::new( + rrset.rtype(), + ksk_dnskey.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + ksk_dnskey.key_tag(), + apex.owner().clone(), + ); + rrsig.compose_canonical(&mut buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(&mut buf).unwrap(); + } + res.push(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + rrsig + .into_rrsig(ksk.sign(&buf)?.into()) + .expect("long signature"), + )); } - // Create and push the RRSIG record. - res.push(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - rrsig - .into_rrsig(key.sign(&buf)?.into()) - .expect("long signature"), - )); + if rrset.rtype() != Rtype::DNSKEY || csk { + let rrsig = ProtoRrsig::new( + rrset.rtype(), + zsk_dnskey.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + zsk_dnskey.key_tag(), + apex.owner().clone(), + ); + rrsig.compose_canonical(&mut buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(&mut buf).unwrap(); + } + + // Create and push the RRSIG record. + res.push(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + rrsig + .into_rrsig(zsk.sign(&buf)?.into()) + .expect("long signature"), + )); + } } } Ok(res) } - pub fn nsecs( + pub fn nsecs( &self, - apex: &FamilyName, + apex: &FamilyName, ttl: Ttl, ) -> Vec>> where @@ -185,8 +223,7 @@ impl SortedRecords { D: RecordData, Octets: FromBuilder, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, - ::AppendError: fmt::Debug, - ApexName: ToName, + ::AppendError: Debug, { let mut res = Vec::new(); @@ -242,6 +279,7 @@ impl SortedRecords { let mut bitmap = RtypeBitmap::::builder(); // Assume there’s gonna be an RRSIG. bitmap.add(Rtype::RRSIG).unwrap(); + bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { bitmap.add(rrset.rtype()).unwrap() } @@ -491,13 +529,20 @@ impl SortedRecords { pub fn write(&self, target: &mut W) -> Result<(), io::Error> where - N: fmt::Display, - D: RecordData + fmt::Display, + N: fmt::Display + Eq, + D: RecordData + fmt::Display + Clone, W: io::Write, { - for record in &self.records { - writeln!(target, "{}", record)?; + for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) + { + writeln!(target, "{record}")?; } + + for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) + { + writeln!(target, "{record}")?; + } + Ok(()) } } From 48c006c0f122ff18b3e2760205755e4d5f7cfe03 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 068/569] [sign] Define 'KeyPair' and impl key export A private key converted into a 'KeyPair' can be exported in the conventional DNS format. This is an important step in implementing 'ldns-keygen' using 'domain'. It is up to the implementation modules to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs) won't support it at all. --- src/sign/mod.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d87acca0c..ff36b16b7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,10 +1,253 @@ //! DNSSEC signing. //! //! **This module is experimental and likely to change significantly.** +//! +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a +//! DNS record served by a secure-aware name server. But name servers are not +//! usually creating those signatures themselves. Within a DNS zone, it is the +//! zone administrator's responsibility to sign zone records (when the record's +//! time-to-live expires and/or when it changes). Those signatures are stored +//! as regular DNS data and automatically served by name servers. + #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +use core::{fmt, str}; + +use crate::base::iana::SecAlg; + pub mod key; //pub mod openssl; pub mod records; pub mod ring; + +/// A generic keypair. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// cryptographic implementation supports it). +pub enum KeyPair + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> KeyPair { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64(&*s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64(&*s, &mut *w) + } + } + } +} + +impl + AsMut<[u8]>> Drop for KeyPair { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6.2 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (b'0' - 52) as u32 + + (pluses & bcast) * (b'+' - 62) as u32 + + (slashs & bcast) * (b'/' - 63) as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + 3 => {} + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} From 66c8f4acea619a15cda1537b3819aaadf4aa92f2 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:54:14 +0200 Subject: [PATCH 069/569] [sign] Define trait 'Sign' 'Sign' is a more generic version of 'sign::key::SigningKey' that does not provide public key information. It does not try to abstract over all the functionality of a keypair, since that can depend on the underlying cryptographic implementation. --- src/sign/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ff36b16b7..f4bac3c51 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -21,6 +21,42 @@ pub mod key; pub mod records; pub mod ring; +/// Signing DNS records. +/// +/// Implementors of this trait own a private key and sign DNS records for a zone +/// with that key. Signing is a synchronous operation performed on the current +/// thread; this rules out implementations like HSMs, where I/O communication is +/// necessary. +pub trait Sign { + /// An error in constructing a signature. + type Error; + + /// The signature algorithm used. + /// + /// The following algorithms can be used: + /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) + /// - [`SecAlg::DSA`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1`] (insecure, not recommended) + /// - [`SecAlg::DSA_NSEC3_SHA1`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1_NSEC3_SHA1`] (insecure, not recommended) + /// - [`SecAlg::RSASHA256`] + /// - [`SecAlg::RSASHA512`] (not recommended) + /// - [`SecAlg::ECC_GOST`] (do not use) + /// - [`SecAlg::ECDSAP256SHA256`] + /// - [`SecAlg::ECDSAP384SHA384`] + /// - [`SecAlg::ED25519`] + /// - [`SecAlg::ED448`] + fn algorithm(&self) -> SecAlg; + + /// Compute a signature. + /// + /// A regular signature of the given byte sequence is computed and is turned + /// into the selected buffer type. This provides a lot of flexibility in + /// how buffers are constructed; they may be heap-allocated or have a static + /// size. + fn sign(&self, data: &[u8]) -> Result; +} + /// A generic keypair. /// /// This type cannot be used for computing signatures, as it does not implement From b613705dc29cf0cab051e362c90135b7ad9aea37 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 15:42:48 +0200 Subject: [PATCH 070/569] [sign] Implement parsing from the DNS format There are probably lots of bugs in this implementation, I'll add some tests soon. --- src/sign/mod.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 18 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f4bac3c51..691edb5e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -14,6 +14,8 @@ use core::{fmt, str}; +use std::vec::Vec; + use crate::base::iana::SecAlg; pub mod key; @@ -114,25 +116,84 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } } } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(())?; + + if !data.trim_ascii().is_empty() { + // There were more fields following. + return Err(()); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf)? != N { + // The private key was of the wrong size. + return Err(()); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(())?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(())?; + + // Parse the algorithm. + let mut words = val.split_ascii_whitespace(); + let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; + let name = words.next().ok_or(())?; + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(()), + } + } } impl + AsMut<[u8]>> Drop for KeyPair { @@ -183,26 +244,87 @@ impl + AsMut<[u8]>> RsaKey { /// /// The output does not include an 'Algorithm' specifier. /// - /// See RFC 5702, section 6.2 for examples of this format. + /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus:\t")?; - base64(self.n.as_ref(), &mut *w)?; + base64_encode(self.n.as_ref(), &mut *w)?; w.write_str("\nPublicExponent:\t")?; - base64(self.e.as_ref(), &mut *w)?; + base64_encode(self.e.as_ref(), &mut *w)?; w.write_str("\nPrivateExponent:\t")?; - base64(self.d.as_ref(), &mut *w)?; + base64_encode(self.d.as_ref(), &mut *w)?; w.write_str("\nPrime1:\t")?; - base64(self.p.as_ref(), &mut *w)?; + base64_encode(self.p.as_ref(), &mut *w)?; w.write_str("\nPrime2:\t")?; - base64(self.q.as_ref(), &mut *w)?; + base64_encode(self.q.as_ref(), &mut *w)?; w.write_str("\nExponent1:\t")?; - base64(self.d_p.as_ref(), &mut *w)?; + base64_encode(self.d_p.as_ref(), &mut *w)?; w.write_str("\nExponent2:\t")?; - base64(self.d_q.as_ref(), &mut *w)?; + base64_encode(self.d_q.as_ref(), &mut *w)?; w.write_str("\nCoefficient:\t")?; - base64(self.q_i.as_ref(), &mut *w)?; + base64_encode(self.q_i.as_ref(), &mut *w)?; w.write_char('\n') } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(()), + }; + + if field.is_some() { + // This field has already been filled. + return Err(()); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(()); + } + } + + 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(), + }) + } } impl + AsMut<[u8]>> Drop for RsaKey { @@ -219,11 +341,26 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair(data: &str) -> Result, ()> { + // Trim any pending newlines. + let data = data.trim_ascii_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = line.split_once(':').ok_or(())?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) +} + /// A utility function to format data as Base64. /// /// This is a simple implementation with the only requirement of being /// constant-time and side-channel resistant. -fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { // Convert a single chunk of bytes into Base64. fn encode(data: [u8; 3]) -> [u8; 4] { let [a, b, c] = data; @@ -254,9 +391,9 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (b'0' - 52) as u32 - + (pluses & bcast) * (b'+' - 62) as u32 - + (slashs & bcast) * (b'/' - 63) as u32; + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; // Convert back into a byte array. chunk.to_be_bytes() @@ -281,9 +418,109 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { 0 => return Ok(()), 1 => chunk[2..].fill(b'='), 2 => chunk[3..].fill(b'='), - 3 => {} _ => unreachable!(), } let chunk = str::from_utf8(&chunk).unwrap(); w.write_str(chunk) } + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} From 5e864967445be98b327903086fc5afee33a08b09 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 16:01:04 +0200 Subject: [PATCH 071/569] [sign] Provide some error information Also fixes 'cargo clippy' issues, particularly with the MSRV. --- src/sign/mod.rs | 96 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 691edb5e3..d320f0249 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -63,7 +63,7 @@ pub trait Sign { /// /// This type cannot be used for computing signatures, as it does not implement /// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). pub enum KeyPair + AsMut<[u8]>> { /// An RSA/SHA256 keypair. @@ -116,22 +116,22 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } } } @@ -141,26 +141,28 @@ impl + AsMut<[u8]>> KeyPair { /// - For RSA, see RFC 5702, section 6. /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result + pub fn from_dns(data: &str) -> Result where B: From>, { /// Parse private keys for most algorithms (except RSA). - fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { // Extract the 'PrivateKey' field. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; - if !data.trim_ascii().is_empty() { + if !data.trim().is_empty() { // There were more fields following. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf)? != N { + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { // The private key was of the wrong size. - return Err(()); + return Err(DnsFormatError::Misformatted); } Ok(buf) @@ -169,17 +171,24 @@ impl + AsMut<[u8]>> KeyPair { // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(())?; + .ok_or(DnsFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; // Parse the algorithm. - let mut words = val.split_ascii_whitespace(); - let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; - let name = words.next().ok_or(())?; + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } match (code, name) { (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), @@ -191,7 +200,7 @@ impl + AsMut<[u8]>> KeyPair { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(()), + _ => Err(DnsFormatError::UnsupportedAlgorithm), } } } @@ -268,7 +277,7 @@ impl + AsMut<[u8]>> RsaKey { /// Parse a key from the conventional DNS format. /// /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result + pub fn from_dns(mut data: &str) -> Result where B: From>, { @@ -291,16 +300,17 @@ impl + AsMut<[u8]>> RsaKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(()), + _ => return Err(DnsFormatError::Misformatted), }; if field.is_some() { // This field has already been filled. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer)?; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; buffer.truncate(size); *field = Some(buffer.into()); @@ -310,7 +320,7 @@ impl + AsMut<[u8]>> RsaKey { for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(()); + return Err(DnsFormatError::Misformatted); } } @@ -342,18 +352,23 @@ impl + AsMut<[u8]>> Drop for RsaKey { } /// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair(data: &str) -> Result, ()> { +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + // Trim any pending newlines. - let data = data.trim_ascii_start(); + let data = data.trim_start(); // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); // Split the line by a colon. - let (key, val) = line.split_once(':').ok_or(())?; + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) + Ok(Some((key.trim(), val.trim(), rest))) } /// A utility function to format data as Base64. @@ -388,6 +403,7 @@ fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let slashs = pluses >> 7; // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 @@ -461,6 +477,7 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { } // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk - (uppers & bcast) * (b'A' - 0) as u32 - (lowers & bcast) * (b'a' - 26) as u32 @@ -524,3 +541,28 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { Ok(index) } + +/// An error in loading a [`KeyPair`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} From c33f6f6525fca81b0c01d31f704eb0a96b285d32 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 4 Oct 2024 13:08:07 +0200 Subject: [PATCH 072/569] [sign] Move 'KeyPair' to 'generic::SecretKey' I'm going to add a corresponding 'PublicKey' type, at which point it becomes important to differentiate from the generic representations and actual cryptographic implementations. --- src/sign/generic.rs | 513 ++++++++++++++++++++++++++++++++++++++++++++ src/sign/mod.rs | 513 +------------------------------------------- 2 files changed, 514 insertions(+), 512 deletions(-) create mode 100644 src/sign/generic.rs diff --git a/src/sign/generic.rs b/src/sign/generic.rs new file mode 100644 index 000000000..420d84530 --- /dev/null +++ b/src/sign/generic.rs @@ -0,0 +1,513 @@ +use core::{fmt, str}; + +use std::vec::Vec; + +use crate::base::iana::SecAlg; + +/// A generic secret key. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Sign`] (if the underlying +/// cryptographic implementation supports it). +pub enum SecretKey + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> SecretKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64_encode(s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64_encode(s, &mut *w) + } + } + } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(DnsFormatError::Misformatted)?; + + if !data.trim().is_empty() { + // There were more fields following. + return Err(DnsFormatError::Misformatted); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { + // The private key was of the wrong size. + return Err(DnsFormatError::Misformatted); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(DnsFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(DnsFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(DnsFormatError::UnsupportedAlgorithm), + } + } +} + +impl + AsMut<[u8]>> Drop for SecretKey { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64_encode(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64_encode(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64_encode(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64_encode(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64_encode(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64_encode(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64_encode(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(DnsFormatError::Misformatted), + }; + + if field.is_some() { + // This field has already been filled. + return Err(DnsFormatError::Misformatted); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(DnsFormatError::Misformatted); + } + } + + 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(), + }) + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} + +/// An error in loading a [`SecretKey`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d320f0249..a649f7ab2 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -12,12 +12,9 @@ #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use core::{fmt, str}; - -use std::vec::Vec; - use crate::base::iana::SecAlg; +pub mod generic; pub mod key; //pub mod openssl; pub mod records; @@ -58,511 +55,3 @@ pub trait Sign { /// size. fn sign(&self, data: &[u8]) -> Result; } - -/// A generic keypair. -/// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -pub enum KeyPair + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), - - /// An ECDSA P-256/SHA-256 keypair. - /// - /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), - - /// An ECDSA P-384/SHA-384 keypair. - /// - /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), - - /// An Ed25519 keypair. - /// - /// The private key is a single 32-byte string. - Ed25519([u8; 32]), - - /// An Ed448 keypair. - /// - /// The private key is a single 57-byte string. - Ed448([u8; 57]), -} - -impl + AsMut<[u8]>> KeyPair { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } - - /// Serialize this key in the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - match self { - Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; - k.into_dns(w) - } - - Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) - } - - Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) - } - } - } - - /// Parse a key from the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { - /// Parse private keys for most algorithms (except RSA). - fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } - - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } - - Ok(buf) - } - - // The first line should specify the key format. - let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; - - // The second line should specify the algorithm. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; - - // Parse the algorithm. - let mut words = val.split_whitespace(); - let code = words - .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; - if words.next().is_some() { - return Err(DnsFormatError::Misformatted); - } - - match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), - (13, "(ECDSAP256SHA256)") => { - parse_pkey(data).map(Self::EcdsaP256Sha256) - } - (14, "(ECDSAP384SHA384)") => { - parse_pkey(data).map(Self::EcdsaP384Sha384) - } - (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), - (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), - } - } -} - -impl + AsMut<[u8]>> Drop for KeyPair { - 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), - } - } -} - -/// An RSA private key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, - - /// The private exponent. - pub d: B, - - /// The first prime factor of `d`. - pub p: B, - - /// The second prime factor of `d`. - pub q: B, - - /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, - - /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, - - /// The inverse of the second prime factor modulo the first. - pub q_i: B, -} - -impl + AsMut<[u8]>> RsaKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. - /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; - w.write_char('\n') - } - - /// Parse a key from the conventional DNS format. - /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { - let mut n = None; - let mut e = None; - let mut d = None; - let mut p = None; - let mut q = None; - let mut d_p = None; - let mut d_q = None; - let mut q_i = None; - - while let Some((key, val, rest)) = parse_dns_pair(data)? { - let field = match key { - "Modulus" => &mut n, - "PublicExponent" => &mut e, - "PrivateExponent" => &mut d, - "Prime1" => &mut p, - "Prime2" => &mut q, - "Exponent1" => &mut d_p, - "Exponent2" => &mut d_q, - "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), - }; - - if field.is_some() { - // This field has already been filled. - return Err(DnsFormatError::Misformatted); - } - - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) - .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); - - *field = Some(buffer.into()); - data = rest; - } - - for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { - if field.is_none() { - // A field was missing. - return Err(DnsFormatError::Misformatted); - } - } - - 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(), - }) - } -} - -impl + AsMut<[u8]>> Drop for RsaKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair( - data: &str, -) -> Result, DnsFormatError> { - // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. - - // Trim any pending newlines. - let data = data.trim_start(); - - // Get the first line (NOTE: CR LF is handled later). - let (line, rest) = data.split_once('\n').unwrap_or((data, "")); - - // Split the line by a colon. - let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; - - // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim(), val.trim(), rest))) -} - -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - -/// An error in loading a [`KeyPair`] from the conventional DNS format. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { - /// The key file uses an unsupported version of the format. - UnsupportedFormat, - - /// The key file did not follow the DNS format correctly. - Misformatted, - - /// The key file used an unsupported algorithm. - UnsupportedAlgorithm, -} - -impl fmt::Display for DnsFormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedFormat => "unsupported format", - Self::Misformatted => "misformatted key file", - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -impl std::error::Error for DnsFormatError {} From d2d0646abcde6526aaab97db6dd8dffe95376941 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 15:29:45 +0200 Subject: [PATCH 073/569] [sign/generic] Add 'PublicKey' --- src/sign/generic.rs | 135 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 420d84530..7c9ffbea4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,8 +1,9 @@ -use core::{fmt, str}; +use core::{fmt, mem, str}; use std::vec::Vec; use crate::base::iana::SecAlg; +use crate::rdata::Dnskey; /// A generic secret key. /// @@ -12,7 +13,7 @@ use crate::base::iana::SecAlg; /// cryptographic implementation supports it). pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// @@ -136,7 +137,9 @@ impl + AsMut<[u8]>> SecretKey { } match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (8, "(RSASHA256)") => { + RsaSecretKey::from_dns(data).map(Self::RsaSha256) + } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) } @@ -163,11 +166,11 @@ impl + AsMut<[u8]>> Drop for SecretKey { } } -/// An RSA private key. +/// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { +pub struct RsaSecretKey + AsMut<[u8]>> { /// The public modulus. pub n: B, @@ -193,7 +196,7 @@ pub struct RsaKey + AsMut<[u8]>> { pub q_i: B, } -impl + AsMut<[u8]>> RsaKey { +impl + AsMut<[u8]>> RsaSecretKey { /// Serialize this key in the conventional DNS format. /// /// The output does not include an 'Algorithm' specifier. @@ -282,7 +285,7 @@ impl + AsMut<[u8]>> RsaKey { } } -impl + AsMut<[u8]>> Drop for RsaKey { +impl + AsMut<[u8]>> Drop for RsaSecretKey { fn drop(&mut self) { // Zero the bytes for each field. self.n.as_mut().fill(0u8); @@ -296,6 +299,124 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// A generic public key. +pub enum PublicKey> { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + // TODO: RSA/SHA-1 with NSEC3/SHA-1? + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256([u8; 65]), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384([u8; 97]), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519([u8; 32]), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448([u8; 57]), +} + +impl> PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Construct a DNSKEY record with the given flags. + pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + where + Octs: From> + AsRef<[u8]>, + { + let protocol = 3u8; + let algorithm = self.algorithm(); + let public_key = match self { + Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { + let (n, e) = (k.n.as_ref(), k.e.as_ref()); + let e_len_len = if e.len() < 256 { 1 } else { 3 }; + let len = e_len_len + e.len() + n.len(); + let mut buf = Vec::with_capacity(len); + if let Ok(e_len) = u8::try_from(e.len()) { + buf.push(e_len); + } else { + // RFC 3110 is not explicit about the endianness of this, + // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network + // byte order, which I suppose makes sense. + let e_len = u16::try_from(e.len()).unwrap(); + buf.extend_from_slice(&e_len.to_be_bytes()); + } + buf.extend_from_slice(e); + buf.extend_from_slice(n); + buf + } + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].to_vec(), + Self::EcdsaP384Sha384(k) => k[1..].to_vec(), + + Self::Ed25519(k) => k.to_vec(), + Self::Ed448(k) => k.to_vec(), + }; + + Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaPublicKey> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, +} + +impl From> for RsaPublicKey +where + B: AsRef<[u8]> + AsMut<[u8]> + Default, +{ + fn from(mut value: RsaSecretKey) -> Self { + Self { + n: mem::take(&mut value.n), + e: mem::take(&mut value.e), + } + } +} + /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, From 6dae3a1de1be6213ed93e7f3ce1cc56666820357 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 16:41:57 +0200 Subject: [PATCH 074/569] [sign] Rewrite the 'ring' module to use the 'Sign' trait Key generation, for now, will only be provided by the OpenSSL backend (coming soon). However, generic keys (for RSA/SHA-256 or Ed25519) can be imported into the Ring backend and used freely. --- src/sign/generic.rs | 4 +- src/sign/ring.rs | 180 ++++++++++++++++---------------------------- 2 files changed, 68 insertions(+), 116 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 7c9ffbea4..f963a8def 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -11,6 +11,8 @@ use crate::rdata::Dnskey; /// any cryptographic primitives. Instead, it is a generic representation that /// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). +/// +/// [`Sign`]: super::Sign pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. RsaSha256(RsaSecretKey), @@ -355,7 +357,7 @@ impl> PublicKey { } /// Construct a DNSKEY record with the given flags. - pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + pub fn into_dns(self, flags: u16) -> Dnskey where Octs: From> + AsRef<[u8]>, { diff --git a/src/sign/ring.rs b/src/sign/ring.rs index bf4614f2b..75660dfd6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,140 +1,90 @@ -//! Key and Signer using ring. +//! DNSSEC signing using `ring`. + #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] -use super::key::SigningKey; -use crate::base::iana::{DigestAlg, SecAlg}; -use crate::base::name::ToName; -use crate::base::rdata::ComposeRecordData; -use crate::rdata::{Dnskey, Ds}; -#[cfg(feature = "bytes")] -use bytes::Bytes; -use octseq::builder::infallible; -use ring::digest; -use ring::error::Unspecified; -use ring::rand::SecureRandom; -use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaEncoding, RsaKeyPair, - Signature as RingSignature, ECDSA_P256_SHA256_FIXED_SIGNING, -}; use std::vec::Vec; -pub struct Key<'a> { - dnskey: Dnskey>, - key: RingKey, - rng: &'a dyn SecureRandom, -} - -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), - Ed25519(Ed25519KeyPair), - Rsa(RsaKeyPair, &'static dyn RsaEncoding), -} - -impl<'a> Key<'a> { - pub fn throwaway_13( - flags: u16, - rng: &'a dyn SecureRandom, - ) -> Result { - let pkcs8 = EcdsaKeyPair::generate_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - rng, - )?; - let keypair = EcdsaKeyPair::from_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - pkcs8.as_ref(), - rng, - )?; - let public_key = keypair.public_key().as_ref()[1..].into(); - Ok(Key { - dnskey: Dnskey::new( - flags, - 3, - SecAlg::ECDSAP256SHA256, - public_key, - ) - .expect("long key"), - key: RingKey::Ecdsa(keypair), - rng, - }) - } -} +use crate::base::iana::SecAlg; -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +use super::generic; - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } +/// A key pair backed by `ring`. +pub enum KeyPair<'a> { + /// An RSA/SHA256 keypair. + RsaSha256 { + key: ring::signature::RsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - infallible(owner.compose_canonical(&mut buf)); - infallible(self.dnskey.compose_canonical_rdata(&mut buf)); - let digest = - Vec::from(digest::digest(&digest::SHA256, &buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::SHA256, - digest, - ) - .expect("long digest")) - } + /// An Ed25519 keypair. + Ed25519(ring::signature::Ed25519KeyPair), +} - fn sign(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) +impl<'a> KeyPair<'a> { + /// Use a generic keypair with `ring`. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + rng: &'a dyn ring::rand::SecureRandom, + ) -> Result { + match &key { + generic::SecretKey::RsaSha256(k) => { + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: k.n.as_ref(), + e: k.e.as_ref(), + }, + d: k.d.as_ref(), + p: k.p.as_ref(), + q: k.q.as_ref(), + dP: k.d_p.as_ref(), + dQ: k.d_q.as_ref(), + qInv: k.q_i.as_ref(), + }; + ring::signature::RsaKeyPair::from_components(&components) + .map_err(|_| ImportError::InvalidKey) + .map(|key| Self::RsaSha256 { key, rng }) } - RingKey::Ed25519(ref key) => Ok(Signature::sig(key.sign(msg))), - RingKey::Rsa(ref key, encoding) => { - let mut sig = vec![0; key.public().modulus_len()]; - key.sign(encoding, self.rng, msg, &mut sig)?; - Ok(Signature::vec(sig)) + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + let k = k.as_ref(); + ring::signature::Ed25519KeyPair::from_seed_unchecked(k) + .map_err(|_| ImportError::InvalidKey) + .map(Self::Ed25519) } + _ => Err(ImportError::UnsupportedAlgorithm), } } } -pub struct Signature(SignatureInner); +/// An error in importing a key into `ring`. +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), + /// The provided keypair was invalid. + InvalidKey, } -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) - } - - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) - } -} +impl<'a> super::Sign> for KeyPair<'a> { + type Error = ring::error::Unspecified; -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self.0 { - SignatureInner::Sig(ref sig) => sig.as_ref(), - SignatureInner::Vec(ref vec) => vec.as_slice(), + fn algorithm(&self) -> SecAlg { + match self { + KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, + KeyPair::Ed25519(_) => SecAlg::ED25519, } } -} -#[cfg(feature = "bytes")] -impl From for Bytes { - fn from(sig: Signature) -> Self { - match sig.0 { - SignatureInner::Sig(sig) => Bytes::copy_from_slice(sig.as_ref()), - SignatureInner::Vec(sig) => Bytes::from(sig), + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + match self { + KeyPair::RsaSha256 { key, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, *rng, data, &mut buf)?; + Ok(buf) + } + KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From 4fccf7ff541f4908b137a992495eaad29536d6a7 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:36:23 +0200 Subject: [PATCH 075/569] Implement DNSSEC signing with OpenSSL The OpenSSL backend supports import from and export to generic secret keys, making the formatting and parsing machinery for them usable. The next step is to implement generation of keys. --- Cargo.lock | 66 +++++++++++++++++ Cargo.toml | 2 + src/sign/mod.rs | 2 +- src/sign/openssl.rs | 167 ++++++++++++++++++++++++++++++++------------ src/sign/ring.rs | 16 ++--- 5 files changed, 200 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9f9bb8ba4..61f66927a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,7 @@ dependencies = [ "mock_instant", "moka", "octseq", + "openssl", "parking_lot", "proc-macro2", "rand", @@ -277,6 +278,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.31" @@ -620,6 +636,44 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -687,6 +741,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1320,6 +1380,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index 499ce94e6..036519e3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } +openssl = { version = "0.10", optional = true } 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 } @@ -48,6 +49,7 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] +openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] diff --git a/src/sign/mod.rs b/src/sign/mod.rs index a649f7ab2..b1db46c26 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -16,7 +16,7 @@ use crate::base::iana::SecAlg; pub mod generic; pub mod key; -//pub mod openssl; +pub mod openssl; pub mod records; pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c49512b73..e62c9dcbb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,58 +1,137 @@ //! Key and Signer using OpenSSL. + #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] +use core::fmt; use std::vec::Vec; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::sha::sha256; -use openssl::sign::Signer as OpenSslSigner; -use unwrap::unwrap; -use crate::base::iana::DigestAlg; -use crate::base::name::ToDname; -use crate::base::octets::Compose; -use crate::rdata::{Ds, Dnskey}; -use super::key::SigningKey; - - -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, + +use openssl::{ + bn::BigNum, + pkey::{self, PKey, Private}, +}; + +use crate::base::iana::SecAlg; + +use super::generic; + +/// A key pair backed by OpenSSL. +pub struct SecretKey { + /// The algorithm used by the key. + algorithm: SecAlg, + + /// The private key. + pkey: PKey, } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +impl SecretKey { + /// Use a generic secret key with OpenSSL. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + ) -> Result { + fn num(slice: &[u8]) -> BigNum { + let mut v = BigNum::new_secure().unwrap(); + v.copy_from_slice(slice).unwrap(); + v + } - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } + let pkey = match &key { + generic::SecretKey::RsaSha256(k) => { + let n = BigNum::from_slice(k.n.as_ref()).unwrap(); + let e = BigNum::from_slice(k.e.as_ref()).unwrap(); + let d = num(k.d.as_ref()); + let p = num(k.p.as_ref()); + let q = num(k.q.as_ref()); + let d_p = num(k.d_p.as_ref()); + let d_q = num(k.d_q.as_ref()); + let q_i = num(k.q_i.as_ref()); - fn ds( - &self, - owner: N - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - unwrap!(owner.compose_canonical(&mut buf)); - unwrap!(self.dnskey.compose_canonical(&mut buf)); - let digest = Vec::from(sha256(&buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::Sha256, - digest, - )) + // NOTE: The 'openssl' crate doesn't seem to expose + // 'EVP_PKEY_fromdata', which could be used to replace the + // deprecated methods called here. + + openssl::rsa::Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + ) + .and_then(PKey::from_rsa) + .unwrap() + } + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + PKey::private_key_from_raw_bytes( + k.as_ref(), + pkey::Id::ED25519, + ) + .unwrap() + } + generic::SecretKey::Ed448(k) => { + PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) + .unwrap() + } + _ => return Err(ImportError::UnsupportedAlgorithm), + }; + + Ok(Self { + algorithm: key.algorithm(), + pkey, + }) } - fn sign(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + /// Export this key into a generic secret key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export(self) -> generic::SecretKey + where + B: AsRef<[u8]> + AsMut<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::SecretKey::RsaSha256(generic::RsaSecretKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } } } +/// An error in importing a key into OpenSSL. +#[derive(Clone, Debug)] +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided secret key was invalid. + InvalidKey, +} + +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 75660dfd6..872f8dadb 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,8 +10,8 @@ use crate::base::iana::SecAlg; use super::generic; /// A key pair backed by `ring`. -pub enum KeyPair<'a> { - /// An RSA/SHA256 keypair. +pub enum SecretKey<'a> { + /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, rng: &'a dyn ring::rand::SecureRandom, @@ -21,7 +21,7 @@ pub enum KeyPair<'a> { Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> KeyPair<'a> { +impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn import + AsMut<[u8]>>( key: generic::SecretKey, @@ -66,25 +66,25 @@ pub enum ImportError { InvalidKey, } -impl<'a> super::Sign> for KeyPair<'a> { +impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; fn algorithm(&self) -> SecAlg { match self { - KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, - KeyPair::Ed25519(_) => SecAlg::ED25519, + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::Ed25519(_) => SecAlg::ED25519, } } fn sign(&self, data: &[u8]) -> Result, Self::Error> { match self { - KeyPair::RsaSha256 { key, rng } => { + Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; key.sign(pad, *rng, data, &mut buf)?; Ok(buf) } - KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), + Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From 0ae002f52d1e236d23bd2f5c04930cb30938c962 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:57:33 +0200 Subject: [PATCH 076/569] [sign/openssl] Implement key generation --- src/sign/openssl.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e62c9dcbb..9d208737c 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -117,6 +117,27 @@ impl SecretKey { } } +/// Generate a new secret key for the given algorithm. +/// +/// If the algorithm is not supported, [`None`] is returned. +/// +/// # Panics +/// +/// Panics if OpenSSL fails or if memory could not be allocated. +pub fn generate(algorithm: SecAlg) -> Option { + let pkey = match algorithm { + // We generate 3072-bit keys for an estimated 128 bits of security. + SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) + .and_then(PKey::from_rsa) + .unwrap(), + SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), + SecAlg::ED448 => PKey::generate_ed448().unwrap(), + _ => return None, + }; + + Some(SecretKey { algorithm, pkey }) +} + /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] pub enum ImportError { @@ -135,3 +156,5 @@ impl fmt::Display for ImportError { }) } } + +impl std::error::Error for ImportError {} From 157a3b92b7c50bfdf91d5d23dbbee2cf0ec36df7 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:08:06 +0200 Subject: [PATCH 077/569] [sign/openssl] Test key generation and import/export --- src/sign/openssl.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9d208737c..13c1f7808 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -86,7 +86,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(self) -> generic::SecretKey + pub fn export(&self) -> generic::SecretKey where B: AsRef<[u8]> + AsMut<[u8]> + From>, { @@ -158,3 +158,30 @@ impl fmt::Display for ImportError { } impl std::error::Error for ImportError {} + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{base::iana::SecAlg, sign::generic}; + + const ALGORITHMS: &[SecAlg] = + &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + + #[test] + fn generate_all() { + for &algorithm in ALGORITHMS { + let _ = super::generate(algorithm).unwrap(); + } + } + + #[test] + fn export_and_import() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let exp: generic::SecretKey> = key.export(); + let imp = super::SecretKey::import(exp).unwrap(); + assert!(key.pkey.public_eq(&imp.pkey)); + } + } +} From 0a6e992130a6c704e8590eb238f7fa00fc4fbc1e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:39:45 +0200 Subject: [PATCH 078/569] [sign/openssl] Add support for ECDSA --- src/sign/openssl.rs | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 13c1f7808..d35f45850 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -60,7 +60,32 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - // TODO: Support ECDSA. + generic::SecretKey::EcdsaP256Sha256(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } + generic::SecretKey::EcdsaP384Sha384(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::SECP384R1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } generic::SecretKey::Ed25519(k) => { PKey::private_key_from_raw_bytes( k.as_ref(), @@ -72,7 +97,6 @@ impl SecretKey { PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) .unwrap() } - _ => return Err(ImportError::UnsupportedAlgorithm), }; Ok(Self { @@ -90,6 +114,7 @@ impl SecretKey { where B: AsRef<[u8]> + AsMut<[u8]> + From>, { + // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); @@ -104,6 +129,16 @@ impl SecretKey { q_i: key.iqmp().unwrap().to_vec().into(), }) } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) + } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); generic::SecretKey::Ed25519(key.try_into().unwrap()) @@ -130,6 +165,20 @@ pub fn generate(algorithm: SecAlg) -> Option { SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) .and_then(PKey::from_rsa) .unwrap(), + SecAlg::ECDSAP256SHA256 => { + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } + SecAlg::ECDSAP384SHA384 => { + let group = openssl::nid::Nid::SECP384R1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), SecAlg::ED448 => PKey::generate_ed448().unwrap(), _ => return None, @@ -165,8 +214,13 @@ mod tests { use crate::{base::iana::SecAlg, sign::generic}; - const ALGORITHMS: &[SecAlg] = - &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + const ALGORITHMS: &[SecAlg] = &[ + SecAlg::RSASHA256, + SecAlg::ECDSAP256SHA256, + SecAlg::ECDSAP384SHA384, + SecAlg::ED25519, + SecAlg::ED448, + ]; #[test] fn generate_all() { From 3a5d55ba0c0718ee8f45a1c83aed4157134ee8b0 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:41:36 +0200 Subject: [PATCH 079/569] [sign/openssl] satisfy clippy --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index d35f45850..1211d6225 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -67,7 +67,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) @@ -80,7 +80,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) From a2d64b430404bf6659fc0752caa0c1e6fec6feaf Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:57:33 +0200 Subject: [PATCH 080/569] [sign/openssl] Implement the 'Sign' trait --- src/sign/openssl.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 1211d6225..663e8a904 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -13,7 +13,7 @@ use openssl::{ use crate::base::iana::SecAlg; -use super::generic; +use super::{generic, Sign}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -152,6 +152,36 @@ impl SecretKey { } } +impl Sign> for SecretKey { + type Error = openssl::error::ErrorStack; + + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + let mut signer = match self.algorithm { + SecAlg::RSASHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP256SHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP384SHA384 => { + Signer::new(MessageDigest::sha384(), &self.pkey)? + } + SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, + SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, + _ => unreachable!(), + }; + + signer.sign_oneshot_to_vec(data) + } +} + /// Generate a new secret key for the given algorithm. /// /// If the algorithm is not supported, [`None`] is returned. From ad69e1fbfd034d9ee50506601a820cba27d5f1da Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:24:02 +0200 Subject: [PATCH 081/569] Install OpenSSL in CI builds --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224b..99a36d6cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,20 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install libssl-dev + echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + - if: matrix.os == 'windows-latest' + run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features --all-targets -- -D warnings + run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features --all-targets - - run: cargo test --all-features + - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets + - run: cargo test $OPENSSL_FLAVOR --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest @@ -37,6 +43,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.68.2" + - name: Install OpenSSL + run: sudo apt install libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions From 46f3f7fdb0b8a913edcbad32305bb6a81227a799 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:28 +0200 Subject: [PATCH 082/569] Ensure 'openssl' dep supports 3.x.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 036519e3e..90d756b1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10", optional = true } +openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x 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 } From 23ea439c6c22f529963ebee9c3411bfc473182fa Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:52 +0200 Subject: [PATCH 083/569] [workflows/ci] Use 'vcpkg' instead of vendoring OpenSSL --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99a36d6cc..18a8bdb13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,22 @@ jobs: with: rust-version: ${{ matrix.rust }} - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt install libssl-dev - echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' - run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: x64-windows-release + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets - - run: cargo test $OPENSSL_FLAVOR --all-features + - run: cargo check --no-default-features --all-targets + - run: cargo test --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest From b9fe3cb3b7e73ec3c5f8333cc01adb9096804bb5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:55:18 +0200 Subject: [PATCH 084/569] Ensure 'openssl' dep exposes necessary interfaces --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 90d756b1b..3e045d822 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x +openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions 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 } From 2469a78dc1ea68dd2c52aae90d8f0204f8b70339 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:03:14 +0200 Subject: [PATCH 085/569] [workflows/ci] Record location of 'vcpkg' --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18a8bdb13..362b3e146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true + - if: matrix.os == 'windows-latest' + run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 30951e899cb7987699878e51e54fd6afa708e244 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:13:22 +0200 Subject: [PATCH 086/569] [workflows/ci] Use a YAML def for 'VCPKG_ROOT' --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362b3e146..514844da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" steps: - name: Checkout repository uses: actions/checkout@v1 @@ -26,8 +27,6 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true - - if: matrix.os == 'windows-latest' - run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 174f0f493877b1a6fa092521d0a868c401fc9097 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:16 +0200 Subject: [PATCH 087/569] [workflows/ci] Fix a vcpkg triplet to use --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 514844da8..12334fa51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: env: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release steps: - name: Checkout repository uses: actions/checkout@v1 @@ -24,7 +25,7 @@ jobs: uses: johnwason/vcpkg-action@v6 with: pkgs: openssl - triplet: x64-windows-release + triplet: ${{ env.VCPKGRS_TRIPLET }} token: ${{ github.token }} github-binarycache: true - if: matrix.rust == 'stable' From 6add5c75c2819898b251281f311dd3cc66ed6de8 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:43 +0200 Subject: [PATCH 088/569] Upgrade openssl to 0.10.57 for bitflags 2.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3e045d822..ed7edc95b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions +openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x 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 } From 9395e443bdd81fceac004ad67654822adefea35e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:22:18 +0200 Subject: [PATCH 089/569] [workflows/ci] Use dynamic linking for vcpkg openssl --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12334fa51..23c73a5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 From 67987c8d25439ca0f4369e81d1b3cffacb859818 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:24:05 +0200 Subject: [PATCH 090/569] [workflows/ci] Correctly annotate 'vcpkg' --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23c73a5ee..299da6658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - if: matrix.os == 'ubuntu-latest' run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' + id: vcpkg uses: johnwason/vcpkg-action@v6 with: pkgs: openssl From d4c6bdf92cfe385b611fc602d2f442ceae17dca2 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:51:14 +0200 Subject: [PATCH 091/569] [sign/openssl] Implement exporting public keys --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 663e8a904..0147222f6 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -150,6 +150,55 @@ impl SecretKey { _ => unreachable!(), } } + + /// Export this key into a generic public key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } + } } impl Sign> for SecretKey { @@ -268,4 +317,12 @@ mod tests { assert!(key.pkey.public_eq(&imp.pkey)); } } + + #[test] + fn export_public() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let _: generic::PublicKey> = key.export_public(); + } + } } From 18d9a7d724690f4306c6716bd9fd1455d9f91c07 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:56:16 +0200 Subject: [PATCH 092/569] [sign/ring] Implement exporting public keys --- src/sign/ring.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 872f8dadb..185b97295 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -55,6 +55,28 @@ impl<'a> SecretKey<'a> { _ => Err(ImportError::UnsupportedAlgorithm), } } + + /// Export this key into a generic public key. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + Self::Ed25519(key) => { + use ring::signature::KeyPair; + let key = key.public_key().as_ref(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } } /// An error in importing a key into `ring`. From 792cb9fb6a84cb01c64faba76a188225b3fb4238 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 19:39:34 +0200 Subject: [PATCH 093/569] [sign/generic] Test (de)serialization for generic secret keys There were bugs in the Base64 encoding/decoding that are not worth trying to debug; there's a perfectly usable Base64 implementation in the crate already. --- src/sign/generic.rs | 272 +++++------------- test-data/dnssec-keys/Ktest.+008+55993.key | 1 + .../dnssec-keys/Ktest.+008+55993.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 + .../dnssec-keys/Ktest.+013+40436.private | 3 + test-data/dnssec-keys/Ktest.+014+17013.key | 1 + .../dnssec-keys/Ktest.+014+17013.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 + .../dnssec-keys/Ktest.+015+43769.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 + .../dnssec-keys/Ktest.+016+34114.private | 3 + 11 files changed, 100 insertions(+), 199 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.private create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f963a8def..01505239d 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -4,6 +4,7 @@ use std::vec::Vec; use crate::base::iana::SecAlg; use crate::rdata::Dnskey; +use crate::utils::base64; /// A generic secret key. /// @@ -56,6 +57,7 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Private-key-format: v1.2\n")?; match self { Self::RsaSha256(k) => { w.write_str("Algorithm: 8 (RSASHA256)\n")?; @@ -64,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } } } @@ -107,11 +109,12 @@ impl + AsMut<[u8]>> SecretKey { return Err(DnsFormatError::Misformatted); } - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } + let buf: Vec = base64::decode(val) + .map_err(|_| DnsFormatError::Misformatted)?; + let buf = buf + .as_slice() + .try_into() + .map_err(|_| DnsFormatError::Misformatted)?; Ok(buf) } @@ -205,22 +208,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_str("Modulus: ")?; + write!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("\nPublicExponent: ")?; + write!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("\nPrivateExponent: ")?; + write!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("\nPrime1: ")?; + write!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("\nPrime2: ")?; + write!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("\nExponent1: ")?; + write!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("\nExponent2: ")?; + write!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("\nCoefficient: ")?; + write!(w, "{}", base64::encode_display(&self.q_i))?; w.write_char('\n') } @@ -258,10 +261,8 @@ impl + AsMut<[u8]>> RsaSecretKey { return Err(DnsFormatError::Misformatted); } - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) + let buffer: Vec = base64::decode(val) .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); *field = Some(buffer.into()); data = rest; @@ -428,6 +429,11 @@ fn parse_dns_pair( // Trim any pending newlines. let data = data.trim_start(); + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); @@ -439,177 +445,6 @@ fn parse_dns_pair( Ok(Some((key.trim(), val.trim(), rest))) } -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DnsFormatError { @@ -634,3 +469,42 @@ impl fmt::Display for DnsFormatError { } impl std::error::Error for DnsFormatError {} + +#[cfg(test)] +mod tests { + use std::{string::String, vec::Vec}; + + use crate::base::iana::SecAlg; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 55993), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + let mut same = String::new(); + key.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key new file mode 100644 index 000000000..8248fbfe8 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private new file mode 100644 index 000000000..7a260e7a0 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= +PublicExponent: AQAB +PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= +Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== +Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== +Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== +Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== +Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key new file mode 100644 index 000000000..7f7cd0fcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+40436.private new file mode 100644 index 000000000..39f5e8a8d --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key new file mode 100644 index 000000000..c7b6aa1d4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private new file mode 100644 index 000000000..9648a876a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key new file mode 100644 index 000000000..8a1f24f67 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private new file mode 100644 index 000000000..e178a3bd4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key new file mode 100644 index 000000000..fc77e0491 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private new file mode 100644 index 000000000..fca7303dc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From 306429b69187e994844d21830ce37eab4cd94c26 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:03:03 +0200 Subject: [PATCH 094/569] [sign] Thoroughly test import/export in both backends I had to swap out the RSA key since 'ring' found it to be too small. --- src/sign/generic.rs | 2 +- src/sign/openssl.rs | 73 +++++++++++++++---- src/sign/ring.rs | 57 +++++++++++++++ test-data/dnssec-keys/Ktest.+008+27096.key | 1 + .../dnssec-keys/Ktest.+008+27096.private | 10 +++ test-data/dnssec-keys/Ktest.+008+55993.key | 1 - .../dnssec-keys/Ktest.+008+55993.private | 10 --- 7 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 01505239d..5626e6ce9 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -477,7 +477,7 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 55993), + (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), (SecAlg::ECDSAP384SHA384, 17013), (SecAlg::ED25519, 43769), diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 0147222f6..9154abd55 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -289,28 +289,32 @@ impl std::error::Error for ImportError {} #[cfg(test)] mod tests { - use std::vec::Vec; + use std::{string::String, vec::Vec}; - use crate::{base::iana::SecAlg, sign::generic}; + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; - const ALGORITHMS: &[SecAlg] = &[ - SecAlg::RSASHA256, - SecAlg::ECDSAP256SHA256, - SecAlg::ECDSAP384SHA384, - SecAlg::ED25519, - SecAlg::ED448, + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), ]; #[test] - fn generate_all() { - for &algorithm in ALGORITHMS { + fn generate() { + for &(algorithm, _) in KEYS { let _ = super::generate(algorithm).unwrap(); } } #[test] - fn export_and_import() { - for &algorithm in ALGORITHMS { + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let exp: generic::SecretKey> = key.export(); let imp = super::SecretKey::import(exp).unwrap(); @@ -318,11 +322,50 @@ mod tests { } } + #[test] + fn imported_roundtrip() { + type GenericKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let imp = GenericKey::from_dns(&data).unwrap(); + let key = super::SecretKey::import(imp).unwrap(); + let exp: GenericKey = key.export(); + let mut same = String::new(); + exp.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } + #[test] fn export_public() { - for &algorithm in ALGORITHMS { - let key = super::generate(algorithm).unwrap(); - let _: generic::PublicKey> = key.export_public(); + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 185b97295..edea8ae14 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -3,6 +3,7 @@ #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] +use core::fmt; use std::vec::Vec; use crate::base::iana::SecAlg; @@ -42,6 +43,7 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) + .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } @@ -80,6 +82,7 @@ impl<'a> SecretKey<'a> { } /// An error in importing a key into `ring`. +#[derive(Clone, Debug)] pub enum ImportError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -88,6 +91,15 @@ pub enum ImportError { InvalidKey, } +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} + impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; @@ -110,3 +122,48 @@ impl<'a> super::Sign> for SecretKey<'a> { } } } + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; + + const KEYS: &[(SecAlg, u16)] = + &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + + #[test] + fn export_public() { + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key new file mode 100644 index 000000000..5aa614f71 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private new file mode 100644 index 000000000..b5819714f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 +PublicExponent: AQAB +PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN +Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj +Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd +Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf +Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 +Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key deleted file mode 100644 index 8248fbfe8..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private deleted file mode 100644 index 7a260e7a0..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= -PublicExponent: AQAB -PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= -Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== -Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== -Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== -Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== -Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== From 0c3fb8b6c15553c8e4417ae7708541a5e6b73f4b Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:06:58 +0200 Subject: [PATCH 095/569] [sign] Remove debugging code and satisfy clippy --- src/sign/generic.rs | 8 ++++---- src/sign/ring.rs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 5626e6ce9..8dd610637 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -66,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index edea8ae14..864480933 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -43,7 +43,6 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) - .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } From e2bb31deba957cdf168057e397db62b34086abfe Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:20:15 +0200 Subject: [PATCH 096/569] [sign] Account for CR LF in tests --- src/sign/generic.rs | 46 +++++++++++++++++++++++---------------------- src/sign/openssl.rs | 2 ++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8dd610637..8ad44ea88 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -57,30 +57,30 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Private-key-format: v1.2\n")?; + writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; + writeln!(w, "Algorithm: 8 (RSASHA256)")?; k.into_dns(w) } Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; + writeln!(w, "Algorithm: 15 (ED25519)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; + writeln!(w, "Algorithm: 16 (ED448)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } @@ -209,22 +209,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; - write!(w, "{}", base64::encode_display(&self.n))?; - w.write_str("\nPublicExponent: ")?; - write!(w, "{}", base64::encode_display(&self.e))?; - w.write_str("\nPrivateExponent: ")?; - write!(w, "{}", base64::encode_display(&self.d))?; - w.write_str("\nPrime1: ")?; - write!(w, "{}", base64::encode_display(&self.p))?; - w.write_str("\nPrime2: ")?; - write!(w, "{}", base64::encode_display(&self.q))?; - w.write_str("\nExponent1: ")?; - write!(w, "{}", base64::encode_display(&self.d_p))?; - w.write_str("\nExponent2: ")?; - write!(w, "{}", base64::encode_display(&self.d_q))?; - w.write_str("\nCoefficient: ")?; - write!(w, "{}", base64::encode_display(&self.q_i))?; - w.write_char('\n') + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i))?; + Ok(()) } /// Parse a key from the conventional DNS format. @@ -504,6 +504,8 @@ mod tests { let key = super::SecretKey::>::from_dns(&data).unwrap(); let mut same = String::new(); key.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9154abd55..2377dc250 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -335,6 +335,8 @@ mod tests { let exp: GenericKey = key.export(); let mut same = String::new(); exp.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } From 9820be2b66036b0ca48c39a9e91fdcec6a1dabcb Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 11 Oct 2024 16:16:12 +0200 Subject: [PATCH 097/569] [sign/openssl] Fix bugs in the signing procedure - RSA signatures were being made with an unspecified padding scheme. - ECDSA signatures were being output in ASN.1 DER format, instead of the fixed-size format required by DNSSEC (and output by 'ring'). - Tests for signature failures are now added for both backends. --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++-------- src/sign/ring.rs | 19 ++++++++++++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 2377dc250..8faa48f9e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -8,6 +8,7 @@ use std::vec::Vec; use openssl::{ bn::BigNum, + ecdsa::EcdsaSig, pkey::{self, PKey, Private}, }; @@ -212,22 +213,42 @@ impl Sign> for SecretKey { use openssl::hash::MessageDigest; use openssl::sign::Signer; - let mut signer = match self.algorithm { + match self.algorithm { SecAlg::RSASHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) } SecAlg::ECDSAP256SHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(32).unwrap(); + let s = signature.s().to_vec_padded(32).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) } SecAlg::ECDSAP384SHA384 => { - Signer::new(MessageDigest::sha384(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(48).unwrap(); + let s = signature.s().to_vec_padded(48).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) + } + SecAlg::ED25519 | SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) } - SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, - SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, _ => unreachable!(), - }; - - signer.sign_oneshot_to_vec(data) + } } } @@ -294,7 +315,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = &[ @@ -370,4 +391,20 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 864480933..0996552f6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -129,7 +129,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = @@ -165,4 +165,21 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } From 94541da08ba59e0949e7ab1411961ce50e8a798d Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 15 Oct 2024 17:32:36 +0200 Subject: [PATCH 098/569] Refactor the 'sign' module Most functions have been renamed. The public key types have been moved to the 'validate' module (which 'sign' now depends on), and they have been outfitted with conversions (e.g. to and from DNSKEY records). Importing a generic key into an OpenSSL or Ring key now requires the public key to also be available. In both implementations, the pair are checked for consistency -- this ensures that both are uncorrupted and that keys have not been mixed up. This also allows the Ring backend to support ECDSA keys (although key generation is still difficult). The 'PublicKey' and 'PrivateKey' enums now store their array data in 'Box'. This has two benefits: it is easier to securely manage memory on the heap (since the compiler will not copy it around the stack); and the smaller sizes of the types is beneficial (although negligibly) to performance. --- Cargo.toml | 3 +- src/sign/generic.rs | 393 ++++++++++++++++++++------------------------ src/sign/mod.rs | 81 ++++++--- src/sign/openssl.rs | 304 +++++++++++++++++++--------------- src/sign/ring.rs | 241 ++++++++++++++++++--------- src/validate.rs | 347 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 910 insertions(+), 459 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ed7edc95b..2bc526f81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,11 +49,10 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std"] +sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8ad44ea88..2589a6ab4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,10 +1,11 @@ -use core::{fmt, mem, str}; +use core::{fmt, str}; +use std::boxed::Box; use std::vec::Vec; use crate::base::iana::SecAlg; -use crate::rdata::Dnskey; use crate::utils::base64; +use crate::validate::RsaPublicKey; /// A generic secret key. /// @@ -14,32 +15,97 @@ use crate::utils::base64; /// cryptographic implementation supports it). /// /// [`Sign`]: super::Sign -pub enum SecretKey + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaSecretKey), +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +pub enum SecretKey { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), + EcdsaP256Sha256(Box<[u8; 32]>), /// An ECDSA P-384/SHA-384 keypair. /// /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), + EcdsaP384Sha384(Box<[u8; 48]>), /// An Ed25519 keypair. /// /// The private key is a single 32-byte string. - Ed25519([u8; 32]), + Ed25519(Box<[u8; 32]>), /// An Ed448 keypair. /// /// The private key is a single 57-byte string. - Ed448([u8; 57]), + Ed448(Box<[u8; 57]>), } -impl + AsMut<[u8]>> SecretKey { +impl SecretKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -51,99 +117,99 @@ impl + AsMut<[u8]>> SecretKey { } } - /// Serialize this key in the conventional DNS format. + /// Serialize this key in the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { writeln!(w, "Algorithm: 8 (RSASHA256)")?; - k.into_dns(w) + k.format_as_bind(w) } Self::EcdsaP256Sha256(s) => { writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::EcdsaP384Sha384(s) => { writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed25519(s) => { writeln!(w, "Algorithm: 15 (ED25519)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed448(s) => { writeln!(w, "Algorithm: 16 (ED448)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } } } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { /// Parse private keys for most algorithms (except RSA). fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_dns_pair(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } - let buf: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; - let buf = buf - .as_slice() - .try_into() - .map_err(|_| DnsFormatError::Misformatted)?; + return base64::decode::>(val) + .map_err(|_| BindFormatError::Misformatted)? + .into_boxed_slice() + .try_into() + .map_err(|_| BindFormatError::Misformatted); + } - Ok(buf) + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) } // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .map_or(false, |minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; + .ok_or(BindFormatError::Misformatted)?; // Parse the algorithm. let mut words = val.split_whitespace(); let code = words .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; if words.next().is_some() { - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } match (code, name) { (8, "(RSASHA256)") => { - RsaSecretKey::from_dns(data).map(Self::RsaSha256) + RsaSecretKey::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -153,12 +219,12 @@ impl + AsMut<[u8]>> SecretKey { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), + _ => Err(BindFormatError::UnsupportedAlgorithm), } } } -impl + AsMut<[u8]>> Drop for SecretKey { +impl Drop for SecretKey { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -175,39 +241,40 @@ impl + AsMut<[u8]>> Drop for SecretKey { /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaSecretKey + AsMut<[u8]>> { +pub struct RsaSecretKey { /// The public modulus. - pub n: B, + pub n: Box<[u8]>, /// The public exponent. - pub e: B, + pub e: Box<[u8]>, /// The private exponent. - pub d: B, + pub d: Box<[u8]>, /// The first prime factor of `d`. - pub p: B, + pub p: Box<[u8]>, /// The second prime factor of `d`. - pub q: B, + pub q: Box<[u8]>, /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, + pub d_p: Box<[u8]>, /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, + pub d_q: Box<[u8]>, /// The inverse of the second prime factor modulo the first. - pub q_i: B, + pub q_i: Box<[u8]>, } -impl + AsMut<[u8]>> RsaSecretKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. +impl RsaSecretKey { + /// Serialize this key in the conventional format used by BIND. /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKey`] for a + /// description of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -227,13 +294,13 @@ impl + AsMut<[u8]>> RsaSecretKey { Ok(()) } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKey`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; let mut d = None; @@ -253,25 +320,28 @@ impl + AsMut<[u8]>> RsaSecretKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), + _ => { + data = rest; + continue; + } }; if field.is_some() { // This field has already been filled. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } let buffer: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; + .map_err(|_| BindFormatError::Misformatted)?; - *field = Some(buffer.into()); + *field = Some(buffer.into_boxed_slice()); data = rest; } for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } } @@ -288,142 +358,33 @@ impl + AsMut<[u8]>> RsaSecretKey { } } -impl + AsMut<[u8]>> Drop for RsaSecretKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// A generic public key. -pub enum PublicKey> { - /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKey), - - // TODO: RSA/SHA-1 with NSEC3/SHA-1? - /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKey), - - /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKey), - - /// An ECDSA P-256/SHA-256 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (32 bytes). - /// - The encoding of the `y` coordinate (32 bytes). - EcdsaP256Sha256([u8; 65]), - - /// An ECDSA P-384/SHA-384 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (48 bytes). - /// - The encoding of the `y` coordinate (48 bytes). - EcdsaP384Sha384([u8; 97]), - - /// An Ed25519 public key. - /// - /// The public key is a 32-byte encoding of the public point. - Ed25519([u8; 32]), - - /// An Ed448 public key. - /// - /// The public key is a 57-byte encoding of the public point. - Ed448([u8; 57]), -} - -impl> PublicKey { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, +impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { + fn from(value: &'a RsaSecretKey) -> Self { + RsaPublicKey { + n: value.n.clone(), + e: value.e.clone(), } } - - /// Construct a DNSKEY record with the given flags. - pub fn into_dns(self, flags: u16) -> Dnskey - where - Octs: From> + AsRef<[u8]>, - { - let protocol = 3u8; - let algorithm = self.algorithm(); - let public_key = match self { - Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { - let (n, e) = (k.n.as_ref(), k.e.as_ref()); - let e_len_len = if e.len() < 256 { 1 } else { 3 }; - let len = e_len_len + e.len() + n.len(); - let mut buf = Vec::with_capacity(len); - if let Ok(e_len) = u8::try_from(e.len()) { - buf.push(e_len); - } else { - // RFC 3110 is not explicit about the endianness of this, - // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network - // byte order, which I suppose makes sense. - let e_len = u16::try_from(e.len()).unwrap(); - buf.extend_from_slice(&e_len.to_be_bytes()); - } - buf.extend_from_slice(e); - buf.extend_from_slice(n); - buf - } - - // From my reading of RFC 6605, the marker byte is not included. - Self::EcdsaP256Sha256(k) => k[1..].to_vec(), - Self::EcdsaP384Sha384(k) => k[1..].to_vec(), - - Self::Ed25519(k) => k.to_vec(), - Self::Ed448(k) => k.to_vec(), - }; - - Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() - } -} - -/// A generic RSA public key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaPublicKey> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, } -impl From> for RsaPublicKey -where - B: AsRef<[u8]> + AsMut<[u8]> + Default, -{ - fn from(mut value: RsaSecretKey) -> Self { - Self { - n: mem::take(&mut value.n), - e: mem::take(&mut value.e), - } +impl Drop for RsaSecretKey { + 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); } } /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, -) -> Result, DnsFormatError> { +) -> Result, BindFormatError> { // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. // Trim any pending newlines. @@ -439,7 +400,7 @@ fn parse_dns_pair( // Split the line by a colon. let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + line.split_once(':').ok_or(BindFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). Ok(Some((key.trim(), val.trim(), rest))) @@ -447,7 +408,7 @@ fn parse_dns_pair( /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { +pub enum BindFormatError { /// The key file uses an unsupported version of the format. UnsupportedFormat, @@ -458,7 +419,7 @@ pub enum DnsFormatError { UnsupportedAlgorithm, } -impl fmt::Display for DnsFormatError { +impl fmt::Display for BindFormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedFormat => "unsupported format", @@ -468,7 +429,7 @@ impl fmt::Display for DnsFormatError { } } -impl std::error::Error for DnsFormatError {} +impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { @@ -490,7 +451,7 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -501,9 +462,9 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); let mut same = String::new(); - key.into_dns(&mut same).unwrap(); + key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b1db46c26..b9773d7f0 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,37 +2,44 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a -//! DNS record served by a secure-aware name server. But name servers are not -//! usually creating those signatures themselves. Within a DNS zone, it is the -//! zone administrator's responsibility to sign zone records (when the record's -//! time-to-live expires and/or when it changes). Those signatures are stored -//! as regular DNS data and automatically served by name servers. +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of +//! a DNS record served by a security-aware name server. Signatures can be +//! made "online" (in an authoritative name server while it is running) or +//! "offline" (outside of a name server). Once generated, signatures can be +//! serialized as DNS records and stored alongside the authenticated records. #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, Signature}, +}; pub mod generic; -pub mod key; pub mod openssl; -pub mod records; pub mod ring; -/// Signing DNS records. +/// Sign DNS records. /// -/// Implementors of this trait own a private key and sign DNS records for a zone -/// with that key. Signing is a synchronous operation performed on the current -/// thread; this rules out implementations like HSMs, where I/O communication is -/// necessary. -pub trait Sign { - /// An error in constructing a signature. - type Error; - +/// Types that implement this trait own a private key and can sign arbitrary +/// 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()`] 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()`]: Sign::sign() +pub trait Sign { /// The signature algorithm used. /// - /// The following algorithms can be used: + /// The following algorithms are known to this crate. Recommendations + /// toward or against usage are based on published RFCs, not the crate + /// authors' opinion. Implementing types may choose to support some of + /// the prohibited algorithms anyway. + /// /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) /// - [`SecAlg::DSA`] (highly insecure, do not use) /// - [`SecAlg::RSASHA1`] (insecure, not recommended) @@ -47,11 +54,35 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// Compute a signature. + /// The public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn public_key(&self) -> PublicKey; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// There are three expected failure cases for this function: + /// + /// - The secret key was invalid. The implementing type is responsible + /// for validating the secret key during initialization, so that this + /// kind of error does not occur. + /// + /// - Not enough randomness could be obtained. This applies to signature + /// algorithms which use randomization (primarily ECDSA). On common + /// platforms like Linux, Mac OS, and Windows, cryptographically secure + /// pseudo-random number generation is provided by the OS, so this is + /// highly unlikely. + /// + /// - Not enough memory could be obtained. Signature generation does not + /// require significant memory and an out-of-memory condition means that + /// the application will probably panic soon. /// - /// A regular signature of the given byte sequence is computed and is turned - /// into the selected buffer type. This provides a lot of flexibility in - /// how buffers are constructed; they may be heap-allocated or have a static - /// size. - fn sign(&self, data: &[u8]) -> Result; + /// None of these are considered likely or recoverable, so panicking is + /// the simplest and most ergonomic solution. + fn sign(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 8faa48f9e..5c708f485 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,10 +1,7 @@ //! Key and Signer using OpenSSL. -#![cfg(feature = "openssl")] -#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] - use core::fmt; -use std::vec::Vec; +use std::boxed::Box; use openssl::{ bn::BigNum, @@ -12,7 +9,10 @@ use openssl::{ pkey::{self, PKey, Private}, }; -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; use super::{generic, Sign}; @@ -31,25 +31,31 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, - ) -> Result { + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, + ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); v.copy_from_slice(slice).unwrap(); v } - let pkey = match &key { - generic::SecretKey::RsaSha256(k) => { - let n = BigNum::from_slice(k.n.as_ref()).unwrap(); - let e = BigNum::from_slice(k.e.as_ref()).unwrap(); - let d = num(k.d.as_ref()); - let p = num(k.p.as_ref()); - let q = num(k.q.as_ref()); - let d_p = num(k.d_p.as_ref()); - let d_q = num(k.d_q.as_ref()); - let q_i = num(k.q_i.as_ref()); + let pkey = match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + + let n = BigNum::from_slice(&s.n).unwrap(); + let e = BigNum::from_slice(&s.e).unwrap(); + let d = num(&s.d); + let p = num(&s.p); + let q = num(&s.q); + let d_p = num(&s.d_p); + let d_q = num(&s.d_q); + let q_i = num(&s.q_i); // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the @@ -61,47 +67,75 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - generic::SecretKey::EcdsaP256Sha256(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::EcdsaP384Sha384(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::SECP384R1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::SECP384R1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::Ed25519(k) => { - PKey::private_key_from_raw_bytes( - k.as_ref(), - pkey::Id::ED25519, - ) - .unwrap() + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } - generic::SecretKey::Ed448(k) => { - PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) - .unwrap() + + (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } + + // The public and private key types did not match. + _ => return Err(FromGenericError::InvalidKey), }; Ok(Self { - algorithm: key.algorithm(), + algorithm: secret.algorithm(), pkey, }) } @@ -111,10 +145,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(&self) -> generic::SecretKey - where - B: AsRef<[u8]> + AsMut<[u8]> + From>, - { + pub fn to_generic(&self) -> generic::SecretKey { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { @@ -151,20 +182,18 @@ impl SecretKey { _ => unreachable!(), } } +} - /// Export this key into a generic public key. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { +impl Sign for SecretKey { + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn public_key(&self) -> PublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { + PublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -177,7 +206,7 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -187,65 +216,69 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + PublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed448(key.try_into().unwrap()) + PublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } -} - -impl Sign> for SecretKey { - type Error = openssl::error::ErrorStack; - fn algorithm(&self) -> SecAlg { - self.algorithm - } - - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn sign(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; match self.algorithm { SecAlg::RSASHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; - s.sign_oneshot_to_vec(data) + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + s.set_rsa_padding(openssl::rsa::Padding::PKCS1).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); + Signature::RsaSha256(signature.into_boxed_slice()) } SecAlg::ECDSAP256SHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(32).unwrap(); let s = signature.s().to_vec_padded(32).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 64]); + signature[..32].copy_from_slice(&r); + signature[32..].copy_from_slice(&s); + Signature::EcdsaP256Sha256(signature) } SecAlg::ECDSAP384SHA384 => { - let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha384(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(48).unwrap(); let s = signature.s().to_vec_padded(48).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 96]); + signature[..48].copy_from_slice(&r); + signature[48..].copy_from_slice(&s); + Signature::EcdsaP384Sha384(signature) + } + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed25519(signature.try_into().unwrap()) } - SecAlg::ED25519 | SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed448(signature.try_into().unwrap()) } _ => unreachable!(), } @@ -289,15 +322,15 @@ pub fn generate(algorithm: SecAlg) -> Option { /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, - /// The provided secret key was invalid. + /// The key's parameters were invalid. InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -306,18 +339,20 @@ impl fmt::Display for ImportError { } } -impl std::error::Error for ImportError {} +impl std::error::Error for FromGenericError {} #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), @@ -337,25 +372,32 @@ mod tests { fn generated_roundtrip() { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); - let exp: generic::SecretKey> = key.export(); - let imp = super::SecretKey::import(exp).unwrap(); - assert!(key.pkey.public_eq(&imp.pkey)); + let gen_key = key.to_generic(); + let pub_key = key.public_key(); + let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); } } #[test] fn imported_roundtrip() { - type GenericKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let imp = GenericKey::from_dns(&data).unwrap(); - let key = super::SecretKey::import(imp).unwrap(); - let exp: GenericKey = key.export(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + + let equiv = key.to_generic(); let mut same = String::new(); - exp.into_dns(&mut same).unwrap(); + equiv.format_as_bind(&mut same).unwrap(); + let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); @@ -363,48 +405,40 @@ mod tests { } #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 0996552f6..2a4867094 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,11 +4,16 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::vec::Vec; +use std::{boxed::Box, vec::Vec}; -use crate::base::iana::SecAlg; +use ring::signature::KeyPair; -use super::generic; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; + +use super::{generic, Sign}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -18,71 +23,97 @@ pub enum SecretKey<'a> { rng: &'a dyn ring::rand::SecureRandom, }, + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, rng: &'a dyn ring::rand::SecureRandom, - ) -> Result { - match &key { - generic::SecretKey::RsaSha256(k) => { + ) -> Result { + match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + let components = ring::rsa::KeyPairComponents { public_key: ring::rsa::PublicKeyComponents { - n: k.n.as_ref(), - e: k.e.as_ref(), + n: s.n.as_ref(), + e: s.e.as_ref(), }, - d: k.d.as_ref(), - p: k.p.as_ref(), - q: k.q.as_ref(), - dP: k.d_p.as_ref(), - dQ: k.d_q.as_ref(), - qInv: k.q_i.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(), }; ring::signature::RsaKeyPair::from_components(&components) - .map_err(|_| ImportError::InvalidKey) + .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } - // TODO: Support ECDSA. - generic::SecretKey::Ed25519(k) => { - let k = k.as_ref(); - ring::signature::Ed25519KeyPair::from_seed_unchecked(k) - .map_err(|_| ImportError::InvalidKey) - .map(Self::Ed25519) + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) } - _ => Err(ImportError::UnsupportedAlgorithm), - } - } - /// Export this key into a generic public key. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { - match self { - Self::RsaSha256 { key, rng: _ } => { - let components: ring::rsa::PublicKeyComponents> = - key.public().into(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { - n: components.n.into(), - e: components.e.into(), - }) + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - Self::Ed25519(key) => { - use ring::signature::KeyPair; - let key = key.public_key().as_ref(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + ring::signature::Ed25519KeyPair::from_seed_and_public_key( + s.as_slice(), + p.as_slice(), + ) + .map_err(|_| FromGenericError::InvalidKey) + .map(Self::Ed25519) } + + (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + Err(FromGenericError::UnsupportedAlgorithm) + } + + // The public and private key types did not match. + _ => Err(FromGenericError::InvalidKey), } } } /// An error in importing a key into `ring`. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -90,7 +121,7 @@ pub enum ImportError { InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -99,87 +130,135 @@ impl fmt::Display for ImportError { } } -impl<'a> super::Sign> for SecretKey<'a> { - type Error = ring::error::Unspecified; - +impl<'a> Sign for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, Self::Ed25519(_) => SecAlg::ED25519, } } - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn public_key(&self) -> PublicKey { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + PublicKey::RsaSha256(RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + + Self::EcdsaP256Sha256 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + + Self::EcdsaP384Sha384 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + + Self::Ed25519(key) => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } + + fn sign(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf)?; - Ok(buf) + key.sign(pad, *rng, data, &mut buf) + .expect("random generators do not fail"); + Signature::RsaSha256(buf.into_boxed_slice()) + } + Self::EcdsaP256Sha256 { key, rng } => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP256Sha256(buf) + } + Self::EcdsaP384Sha384 { key, rng } => { + let mut buf = Box::new([0u8; 96]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP384Sha384(buf) + } + Self::Ed25519(key) => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice(key.sign(data).as_ref()); + Signature::Ed25519(buf) } - Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } #[cfg(test)] mod tests { - use std::vec::Vec; - use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index 41b7456e5..b122c83c9 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -10,14 +10,361 @@ use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; +use crate::base::scan::IterScanner; use crate::base::wire::{Compose, Composer}; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use ring::{digest, signature}; +use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +/// A generic public key. +#[derive(Clone, Debug)] +pub enum PublicKey { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + /// An RSA/SHA-1 with NSEC3 public key. + RsaSha1Nsec3Sha1(RsaPublicKey), + + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256(Box<[u8; 65]>), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384(Box<[u8; 97]>), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519(Box<[u8; 32]>), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448(Box<[u8; 57]>), +} + +impl PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl PublicKey { + /// Parse a public key as stored in a DNSKEY record. + pub fn from_dnskey( + algorithm: SecAlg, + data: &[u8], + ) -> Result { + match algorithm { + SecAlg::RSASHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + } + SecAlg::RSASHA1_NSEC3_SHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + } + SecAlg::RSASHA256 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + } + SecAlg::RSASHA512 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + } + + SecAlg::ECDSAP256SHA256 => { + let mut key = Box::new([0u8; 65]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP256Sha256(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + SecAlg::ECDSAP384SHA384 => { + let mut key = Box::new([0u8; 97]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP384Sha384(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + + SecAlg::ED25519 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed25519) + .map_err(|_| FromDnskeyError::InvalidKey), + SecAlg::ED448 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed448) + .map_err(|_| FromDnskeyError::InvalidKey), + + _ => Err(FromDnskeyError::UnsupportedAlgorithm), + } + } + + /// Parse a public key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn from_dnskey_text( + dnskey: &str, + ) -> Result { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(FromDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Ensure the record header looks reasonable. + let mut words = line.split_ascii_whitespace().skip(2); + if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { + return Err(FromDnskeyTextError::Misformatted); + } + + // Parse the DNSKEY record data. + let mut data = IterScanner::new(words); + let dnskey: Dnskey> = Dnskey::scan(&mut data) + .map_err(|_| FromDnskeyTextError::Misformatted)?; + println!("importing {:?}", dnskey); + Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) + .map_err(FromDnskeyTextError::FromDnskey) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.to_dnskey(), + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].into(), + Self::EcdsaP384Sha384(k) => k[1..].into(), + + Self::Ed25519(k) => k.as_slice().into(), + Self::Ed448(k) => k.as_slice().into(), + } + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + match (self, other) { + (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, + (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, + (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, + (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, + (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed25519(a), Self::Ed25519(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed448(a), Self::Ed448(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + _ => false, + } + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +#[derive(Clone, Debug)] +pub struct RsaPublicKey { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, +} + +impl RsaPublicKey { + /// Parse an RSA public key as stored in a DNSKEY record. + pub fn from_dnskey(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(FromDnskeyError::InvalidKey); + } + + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else if data[1..3] != [0, 0] { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + } else { + // The extended encoding of the length just held a zero value. + return Err(FromDnskeyError::InvalidKey); + }; + + // NOTE: off <= 3 so is safe to index up to. + let e = data[off..] + .get(..exp_len) + .ok_or(FromDnskeyError::InvalidKey)? + .into(); + + // NOTE: The previous statement indexed up to 'exp_len'. + let n = data[off + exp_len..].into(); + + Ok(Self { n, e }) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + key.reserve_exact(1 + self.e.len() + self.n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + key.reserve_exact(3 + self.e.len() + self.n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(&*self.e); + key.extend(&*self.n); + key.into_boxed_slice() + } +} + +impl PartialEq for RsaPublicKey { + fn eq(&self, other: &Self) -> bool { + /// Compare after stripping leading zeros. + fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { + let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; + let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; + if a.len() == b.len() { + ring::constant_time::verify_slices_are_equal(a, b).is_ok() + } else { + false + } + } + + cmp_without_leading(&self.n, &other.n) + && cmp_without_leading(&self.e, &other.e) + } +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +pub enum Signature { + RsaSha1(Box<[u8]>), + RsaSha1Nsec3Sha1(Box<[u8]>), + RsaSha256(Box<[u8]>), + RsaSha512(Box<[u8]>), + EcdsaP256Sha256(Box<[u8; 64]>), + EcdsaP384Sha384(Box<[u8; 96]>), + Ed25519(Box<[u8; 64]>), + Ed448(Box<[u8; 114]>), +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From c56b3fe90da4b60afa867af513b2ebb9a176bffd Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 09:49:40 +0200 Subject: [PATCH 099/569] Move 'sign' and 'validate' to unstable feature gates --- Cargo.toml | 6 +++--- src/lib.rs | 16 ++++++++-------- src/sign/mod.rs | 4 ++-- src/validate.rs | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2bc526f81..c652c24e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,19 +52,19 @@ heapless = ["dep:heapless", "octseq/heapless"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] tsig = ["bytes", "ring", "smallvec"] -validate = ["bytes", "std", "ring"] 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", "dep:openssl"] unstable-stelline = ["tokio/test-util", "tracing", "tracing-subscriber", "tsig", "unstable-client-transport", "unstable-server-transport", "zonefile"] -unstable-validator = ["validate", "zonefile", "unstable-client-transport"] +unstable-validate = ["bytes", "std", "ring"] +unstable-validator = ["unstable-validate", "zonefile", "unstable-client-transport"] unstable-xfr = ["net"] unstable-zonetree = ["futures-util", "parking_lot", "rustversion", "serde", "std", "tokio", "tracing", "unstable-xfr", "zonefile"] diff --git a/src/lib.rs b/src/lib.rs index 6d6cfd344..119adc66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,14 +36,14 @@ #![cfg_attr(not(feature = "resolv"), doc = "* resolv:")] //! An asynchronous DNS resolver based on the //! [Tokio](https://tokio.rs/) async runtime. -#![cfg_attr(feature = "sign", doc = "* [sign]:")] -#![cfg_attr(not(feature = "sign"), doc = "* sign:")] +#![cfg_attr(feature = "unstable-sign", doc = "* [sign]:")] +#![cfg_attr(not(feature = "unstable-sign"), doc = "* sign:")] //! Experimental support for DNSSEC signing. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig"), doc = "* tsig:")] //! Support for securing DNS transactions with TSIG records. -#![cfg_attr(feature = "validate", doc = "* [validate]:")] -#![cfg_attr(not(feature = "validate"), doc = "* validate:")] +#![cfg_attr(feature = "unstable-validate", doc = "* [validate]:")] +#![cfg_attr(not(feature = "unstable-validate"), doc = "* validate:")] //! Experimental support for DNSSEC validation. #![cfg_attr(feature = "unstable-validator", doc = "* [validator]:")] #![cfg_attr(not(feature = "unstable-validator"), doc = "* validator:")] @@ -86,8 +86,8 @@ //! [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 = "sign", doc = " [sign]")] -#![cfg_attr(not(feature = "sign"), doc = " sign")] +#![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` @@ -108,8 +108,8 @@ //! module and currently pulls in the //! `bytes`, `ring`, and `smallvec` features. //! * `validate`: basic DNSSEC validation support. This feature enables the -#![cfg_attr(feature = "validate", doc = " [validate]")] -#![cfg_attr(not(feature = "validate"), doc = " validate")] +#![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. //! * `zonefile`: reading and writing of zonefiles. This feature enables the diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b9773d7f0..7a96230e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -8,8 +8,8 @@ //! "offline" (outside of a name server). Once generated, signatures can be //! serialized as DNS records and stored alongside the authenticated records. -#![cfg(feature = "sign")] -#![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +#![cfg(feature = "unstable-sign")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] use crate::{ base::iana::SecAlg, diff --git a/src/validate.rs b/src/validate.rs index b122c83c9..eb162df8d 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,8 +1,8 @@ //! DNSSEC validation. //! //! **This module is experimental and likely to change significantly.** -#![cfg(feature = "validate")] -#![cfg_attr(docsrs, doc(cfg(feature = "validate")))] +#![cfg(feature = "unstable-validate")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] use crate::base::cmp::CanonicalOrd; use crate::base::iana::{DigestAlg, SecAlg}; From b2f0bbbebb79f7ba5ea3b2344e74a537715fa9bb Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 09:54:57 +0200 Subject: [PATCH 100/569] [workflows/ci] Document the vcpkg env vars --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 299da6658..cbad43917 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,10 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + # We use 'vcpkg' to install OpenSSL on Windows. VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + # Ensure that OpenSSL is dynamically linked. VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository From bbc3fb14af71c9a06d6189d663bcfb28ac9a6b33 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:05:27 +0200 Subject: [PATCH 101/569] Rename public/secret key interfaces to '*Raw*' This makes space for higher-level interfaces which track DNSKEY flags information (and possibly key rollover information). --- src/sign/generic.rs | 10 ++++----- src/sign/mod.rs | 18 ++++++++-------- src/sign/openssl.rs | 51 ++++++++++++++++++++++++--------------------- src/sign/ring.rs | 45 ++++++++++++++++++++------------------- src/validate.rs | 10 ++++----- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 2589a6ab4..f7caaa5a0 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -9,12 +9,10 @@ use crate::validate::RsaPublicKey; /// A generic secret key. /// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -/// -/// [`Sign`]: super::Sign +/// This is a low-level generic representation of a secret key from any one of +/// the commonly supported signature algorithms. It is useful for abstracting +/// over most cryptographic implementations, and it provides functionality for +/// importing and exporting keys from and to the disk. /// /// # Serialization /// diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 7a96230e3..6f31e7887 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -13,26 +13,26 @@ use crate::{ base::iana::SecAlg, - validate::{PublicKey, Signature}, + validate::{RawPublicKey, Signature}, }; pub mod generic; pub mod openssl; pub mod ring; -/// Sign DNS records. +/// Low-level signing functionality. /// /// Types that implement this trait own a private key and can sign arbitrary /// 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()`] to be called on unvalidated keys, it -/// will have to check the validity of the key for every signature; this is +/// 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()`]: Sign::sign() -pub trait Sign { +/// [`sign_raw()`]: SignRaw::sign_raw() +pub trait SignRaw { /// The signature algorithm used. /// /// The following algorithms are known to this crate. Recommendations @@ -54,13 +54,13 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// The public key. + /// The raw public key. /// /// This can be used to verify produced signatures. It must use the same /// algorithm as returned by [`algorithm()`]. /// /// [`algorithm()`]: Self::algorithm() - fn public_key(&self) -> PublicKey; + fn raw_public_key(&self) -> RawPublicKey; /// Sign the given bytes. /// @@ -84,5 +84,5 @@ pub trait Sign { /// /// None of these are considered likely or recoverable, so panicking is /// the simplest and most ergonomic solution. - fn sign(&self, data: &[u8]) -> Signature; + fn sign_raw(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 5c708f485..990e1c37e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -11,10 +11,10 @@ use openssl::{ use crate::{ base::iana::SecAlg, - validate::{PublicKey, RsaPublicKey, Signature}, + validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, Sign}; +use super::{generic, SignRaw}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -33,7 +33,7 @@ impl SecretKey { /// Panics if OpenSSL fails or if memory could not be allocated. pub fn from_generic( secret: &generic::SecretKey, - public: &PublicKey, + public: &RawPublicKey, ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); @@ -42,7 +42,10 @@ impl SecretKey { } let pkey = match (secret, public) { - (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + ( + generic::SecretKey::RsaSha256(s), + RawPublicKey::RsaSha256(p), + ) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromGenericError::InvalidKey); @@ -70,7 +73,7 @@ impl SecretKey { ( generic::SecretKey::EcdsaP256Sha256(s), - PublicKey::EcdsaP256Sha256(p), + RawPublicKey::EcdsaP256Sha256(p), ) => { use openssl::{bn, ec, nid}; @@ -88,7 +91,7 @@ impl SecretKey { ( generic::SecretKey::EcdsaP384Sha384(s), - PublicKey::EcdsaP384Sha384(p), + RawPublicKey::EcdsaP384Sha384(p), ) => { use openssl::{bn, ec, nid}; @@ -104,7 +107,7 @@ impl SecretKey { PKey::from_ec_key(k).unwrap() } - (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; @@ -117,7 +120,7 @@ impl SecretKey { } } - (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + (generic::SecretKey::Ed448(s), RawPublicKey::Ed448(p)) => { use openssl::memcmp; let id = pkey::Id::ED448; @@ -184,16 +187,16 @@ impl SecretKey { } } -impl Sign for SecretKey { +impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { self.algorithm } - fn public_key(&self) -> PublicKey { + fn raw_public_key(&self) -> RawPublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - PublicKey::RsaSha256(RsaPublicKey { + RawPublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -206,7 +209,7 @@ impl Sign for SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -216,21 +219,21 @@ impl Sign for SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - PublicKey::Ed25519(key.try_into().unwrap()) + RawPublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - PublicKey::Ed448(key.try_into().unwrap()) + RawPublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } - fn sign(&self, data: &[u8]) -> Signature { + fn sign_raw(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; @@ -347,8 +350,8 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, Sign}, - validate::PublicKey, + sign::{generic, SignRaw}, + validate::RawPublicKey, }; use super::SecretKey; @@ -373,7 +376,7 @@ mod tests { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let gen_key = key.to_generic(); - let pub_key = key.public_key(); + let pub_key = key.raw_public_key(); let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } @@ -386,7 +389,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -415,11 +418,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(key.public_key(), pub_key); + assert_eq!(key.raw_public_key(), pub_key); } } @@ -434,11 +437,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = key.sign(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 2a4867094..051861539 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,10 +10,10 @@ use ring::signature::KeyPair; use crate::{ base::iana::SecAlg, - validate::{PublicKey, RsaPublicKey, Signature}, + validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, Sign}; +use super::{generic, SignRaw}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -43,11 +43,14 @@ impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn from_generic( secret: &generic::SecretKey, - public: &PublicKey, + public: &RawPublicKey, rng: &'a dyn ring::rand::SecureRandom, ) -> Result { match (secret, public) { - (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + ( + generic::SecretKey::RsaSha256(s), + RawPublicKey::RsaSha256(p), + ) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromGenericError::InvalidKey); @@ -72,7 +75,7 @@ impl<'a> SecretKey<'a> { ( generic::SecretKey::EcdsaP256Sha256(s), - PublicKey::EcdsaP256Sha256(p), + RawPublicKey::EcdsaP256Sha256(p), ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( @@ -83,7 +86,7 @@ impl<'a> SecretKey<'a> { ( generic::SecretKey::EcdsaP384Sha384(s), - PublicKey::EcdsaP384Sha384(p), + RawPublicKey::EcdsaP384Sha384(p), ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( @@ -92,7 +95,7 @@ impl<'a> SecretKey<'a> { .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { ring::signature::Ed25519KeyPair::from_seed_and_public_key( s.as_slice(), p.as_slice(), @@ -101,7 +104,7 @@ impl<'a> SecretKey<'a> { .map(Self::Ed25519) } - (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + (generic::SecretKey::Ed448(_), RawPublicKey::Ed448(_)) => { Err(FromGenericError::UnsupportedAlgorithm) } @@ -130,7 +133,7 @@ impl fmt::Display for FromGenericError { } } -impl<'a> Sign for SecretKey<'a> { +impl<'a> SignRaw for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, @@ -140,12 +143,12 @@ impl<'a> Sign for SecretKey<'a> { } } - fn public_key(&self) -> PublicKey { + fn raw_public_key(&self) -> RawPublicKey { match self { Self::RsaSha256 { key, rng: _ } => { let components: ring::rsa::PublicKeyComponents> = key.public().into(); - PublicKey::RsaSha256(RsaPublicKey { + RawPublicKey::RsaSha256(RsaPublicKey { n: components.n.into(), e: components.e.into(), }) @@ -154,24 +157,24 @@ impl<'a> Sign for SecretKey<'a> { Self::EcdsaP256Sha256 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } Self::EcdsaP384Sha384 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } Self::Ed25519(key) => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::Ed25519(key.try_into().unwrap()) + RawPublicKey::Ed25519(key.try_into().unwrap()) } } } - fn sign(&self, data: &[u8]) -> Signature { + fn sign_raw(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; @@ -211,8 +214,8 @@ impl<'a> Sign for SecretKey<'a> { mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, Sign}, - validate::PublicKey, + sign::{generic, SignRaw}, + validate::RawPublicKey, }; use super::SecretKey; @@ -232,12 +235,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - assert_eq!(key.public_key(), pub_key); + assert_eq!(key.raw_public_key(), pub_key); } } @@ -253,12 +256,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = key.sign(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index eb162df8d..2360ee3c8 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -22,7 +22,7 @@ use std::{error, fmt}; /// A generic public key. #[derive(Clone, Debug)] -pub enum PublicKey { +pub enum RawPublicKey { /// An RSA/SHA-1 public key. RsaSha1(RsaPublicKey), @@ -64,7 +64,7 @@ pub enum PublicKey { Ed448(Box<[u8; 57]>), } -impl PublicKey { +impl RawPublicKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -80,7 +80,7 @@ impl PublicKey { } } -impl PublicKey { +impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. pub fn from_dnskey( algorithm: SecAlg, @@ -161,7 +161,7 @@ impl PublicKey { /// [`to_dnskey()`]: Self::to_dnskey() /// /// The `` is any text starting with an ASCII semicolon. - pub fn from_dnskey_text( + pub fn parse_dnskey_text( dnskey: &str, ) -> Result { // Ensure there is a single line in the input. @@ -206,7 +206,7 @@ impl PublicKey { } } -impl PartialEq for PublicKey { +impl PartialEq for RawPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; From 1fc5309984c4f42af02d4ea9c1aecda33a7409e9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:21:33 +0200 Subject: [PATCH 102/569] [sign/ring] Store the RNG in an 'Arc' --- src/sign/ring.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 051861539..977db8588 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,7 +4,7 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::{boxed::Box, vec::Vec}; +use std::{boxed::Box, sync::Arc, vec::Vec}; use ring::signature::KeyPair; @@ -16,35 +16,35 @@ use crate::{ use super::{generic, SignRaw}; /// A key pair backed by `ring`. -pub enum SecretKey<'a> { +pub enum SecretKey { /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256 { key: ring::signature::EcdsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An ECDSA P-384/SHA-384 keypair. EcdsaP384Sha384 { key: ring::signature::EcdsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> SecretKey<'a> { +impl SecretKey { /// Use a generic keypair with `ring`. pub fn from_generic( secret: &generic::SecretKey, public: &RawPublicKey, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, ) -> Result { match (secret, public) { ( @@ -79,7 +79,7 @@ impl<'a> SecretKey<'a> { ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), rng) + alg, s.as_slice(), p.as_slice(), &*rng) .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::EcdsaP256Sha256 { key, rng }) } @@ -90,7 +90,7 @@ impl<'a> SecretKey<'a> { ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), rng) + alg, s.as_slice(), p.as_slice(), &*rng) .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::EcdsaP384Sha384 { key, rng }) } @@ -133,7 +133,7 @@ impl fmt::Display for FromGenericError { } } -impl<'a> SignRaw for SecretKey<'a> { +impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, @@ -179,14 +179,14 @@ impl<'a> SignRaw for SecretKey<'a> { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf) + key.sign(pad, &**rng, data, &mut buf) .expect("random generators do not fail"); Signature::RsaSha256(buf.into_boxed_slice()) } Self::EcdsaP256Sha256 { key, rng } => { let mut buf = Box::new([0u8; 64]); buf.copy_from_slice( - key.sign(*rng, data) + key.sign(&**rng, data) .expect("random generators do not fail") .as_ref(), ); @@ -195,7 +195,7 @@ impl<'a> SignRaw for SecretKey<'a> { Self::EcdsaP384Sha384 { key, rng } => { let mut buf = Box::new([0u8; 96]); buf.copy_from_slice( - key.sign(*rng, data) + key.sign(&**rng, data) .expect("random generators do not fail") .as_ref(), ); @@ -212,6 +212,8 @@ impl<'a> SignRaw for SecretKey<'a> { #[cfg(test)] mod tests { + use std::sync::Arc; + use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, @@ -227,7 +229,7 @@ mod tests { fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); - let rng = ring::rand::SystemRandom::new(); + let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -238,7 +240,7 @@ mod tests { let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = - SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); assert_eq!(key.raw_public_key(), pub_key); } @@ -248,7 +250,7 @@ mod tests { fn sign() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); - let rng = ring::rand::SystemRandom::new(); + let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -259,7 +261,7 @@ mod tests { let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = - SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } From 2556e2aa156769f23ed8b5d00e056e3b1f0c14b5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:27:13 +0200 Subject: [PATCH 103/569] [validate] Enhance 'Signature' API --- src/validate.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/validate.rs b/src/validate.rs index 2360ee3c8..b584a982a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -354,6 +354,7 @@ pub enum FromDnskeyTextError { /// that are encoded into bytes. /// /// Signatures are too big to pass by value, so they are placed on the heap. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Signature { RsaSha1(Box<[u8]>), RsaSha1Nsec3Sha1(Box<[u8]>), @@ -365,6 +366,52 @@ pub enum Signature { Ed448(Box<[u8; 114]>), } +impl Signature { + /// The algorithm used to make the signature. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match self { + Self::RsaSha1(s) + | Self::RsaSha1Nsec3Sha1(s) + | Self::RsaSha256(s) + | Self::RsaSha512(s) => s, + Self::EcdsaP256Sha256(s) => &**s, + Self::EcdsaP384Sha384(s) => &**s, + Self::Ed25519(s) => &**s, + Self::Ed448(s) => &**s, + } + } +} + +impl From for Box<[u8]> { + fn from(value: Signature) -> Self { + match value { + Signature::RsaSha1(s) + | Signature::RsaSha1Nsec3Sha1(s) + | Signature::RsaSha256(s) + | Signature::RsaSha512(s) => s, + Signature::EcdsaP256Sha256(s) => s as _, + Signature::EcdsaP384Sha384(s) => s as _, + Signature::Ed25519(s) => s as _, + Signature::Ed448(s) => s as _, + } + } +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From 8086b450f5edde9cce99c20089d37466a77fdb7e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 11:40:21 +0200 Subject: [PATCH 104/569] [validate] Add high-level 'Key' type --- src/sign/openssl.rs | 19 ++-- src/sign/ring.rs | 16 +-- src/validate.rs | 271 +++++++++++++++++++++++++++++++------------- 3 files changed, 212 insertions(+), 94 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 990e1c37e..46553dbad 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -351,7 +351,7 @@ mod tests { use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, - validate::RawPublicKey, + validate::Key, }; use super::SecretKey; @@ -389,13 +389,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); let equiv = key.to_generic(); let mut same = String::new(); @@ -418,11 +419,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); - assert_eq!(key.raw_public_key(), pub_key); + assert_eq!(key.raw_public_key(), *pub_key); } } @@ -437,9 +439,10 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 977db8588..e0be1943a 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -212,12 +212,12 @@ impl SignRaw for SecretKey { #[cfg(test)] mod tests { - use std::sync::Arc; + use std::{sync::Arc, vec::Vec}; use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, - validate::RawPublicKey, + validate::Key, }; use super::SecretKey; @@ -237,12 +237,13 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let key = - SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); + SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); - assert_eq!(key.raw_public_key(), pub_key); + assert_eq!(key.raw_public_key(), *pub_key); } } @@ -258,10 +259,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let key = - SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); + SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } diff --git a/src/validate.rs b/src/validate.rs index b584a982a..b040acf9b 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -5,22 +5,197 @@ #![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlg, SecAlg}; +use crate::base::iana::{Class, DigestAlg, SecAlg}; use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; -use crate::base::scan::IterScanner; +use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; +use crate::base::Rtype; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; +use octseq::{EmptyBuilder, FromBuilder}; use ring::{digest, signature}; use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; -/// A generic public key. +/// A DNSSEC key for a particular zone. +#[derive(Clone)] +pub struct Key { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The raw public key. + /// + /// This identifies the key and can be used for signatures. + key: RawPublicKey, +} + +impl Key { + /// Construct a new DNSSEC key manually. + pub fn new(owner: Name, flags: u16, key: RawPublicKey) -> Self { + Self { owner, flags, key } + } + + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw public key. + pub fn raw_public_key(&self) -> &RawPublicKey { + &self.key + } + + /// Whether this is a zone signing key. + /// + /// From RFC 4034, section 2.1.1: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From RFC 4034, section 2.1.1: + /// + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + pub fn is_secure_entry_point(&self) -> bool { + self.flags & (1 << 15) != 0 + } +} + +impl> Key { + /// Deserialize a key from DNSKEY record data. + /// + /// # Errors + /// + /// Fails if the DNSKEY uses an unknown protocol or contains an invalid + /// public key (e.g. one of the wrong size for the signature algorithm). + pub fn from_dnskey( + owner: Name, + dnskey: Dnskey, + ) -> Result { + if dnskey.protocol() != 3 { + return Err(FromDnskeyError::UnsupportedProtocol); + } + + let flags = dnskey.flags(); + let algorithm = dnskey.algorithm(); + let key = dnskey.public_key().as_ref(); + let key = RawPublicKey::from_dnskey_format(algorithm, key)?; + Ok(Self { owner, flags, key }) + } + + /// Parse a DNSSEC key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn parse_dnskey_text( + dnskey: &str, + ) -> Result + where + Octs: FromBuilder, + Octs::Builder: EmptyBuilder + Composer, + { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(ParseDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Parse the entire record. + let mut scanner = IterScanner::new(line.split_ascii_whitespace()); + + let name = scanner + .scan_name() + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + let _ = Class::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { + return Err(ParseDnskeyTextError::Misformatted); + } + + let data = Dnskey::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + Self::from_dnskey(name, data) + .map_err(ParseDnskeyTextError::FromDnskey) + } + + /// Serialize the key into DNSKEY record data. + /// + /// The owner name can be combined with the returned record to serialize a + /// complete DNS record if necessary. + pub fn to_dnskey(&self) -> Dnskey> { + Dnskey::new( + self.flags, + 3, + self.key.algorithm(), + self.key.to_dnskey_format(), + ) + .expect("long public key") + } +} + +/// A low-level public key. #[derive(Clone, Debug)] pub enum RawPublicKey { /// An RSA/SHA-1 public key. @@ -82,22 +257,23 @@ impl RawPublicKey { impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. - pub fn from_dnskey( + pub fn from_dnskey_format( algorithm: SecAlg, data: &[u8], ) -> Result { match algorithm { SecAlg::RSASHA1 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha1) } SecAlg::RSASHA1_NSEC3_SHA1 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + RsaPublicKey::from_dnskey_format(data) + .map(Self::RsaSha1Nsec3Sha1) } SecAlg::RSASHA256 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha256) } SecAlg::RSASHA512 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha512) } SecAlg::ECDSAP256SHA256 => { @@ -134,67 +310,13 @@ impl RawPublicKey { } } - /// Parse a public key from a DNSKEY record in presentation format. - /// - /// This format is popularized for storing alongside private keys by the - /// BIND name server. This function is convenient for loading such keys. - /// - /// The text should consist of a single line of the following format (each - /// field is separated by a non-zero number of ASCII spaces): - /// - /// ```text - /// DNSKEY [] - /// ``` - /// - /// Where `` consists of the following fields: - /// - /// ```text - /// - /// ``` - /// - /// The first three fields are simple integers, while the last field is - /// Base64 encoded data (with or without padding). The [`from_dnskey()`] - /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data - /// format. - /// - /// [`from_dnskey()`]: Self::from_dnskey() - /// [`to_dnskey()`]: Self::to_dnskey() - /// - /// The `` is any text starting with an ASCII semicolon. - pub fn parse_dnskey_text( - dnskey: &str, - ) -> Result { - // Ensure there is a single line in the input. - let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); - if !rest.trim().is_empty() { - return Err(FromDnskeyTextError::Misformatted); - } - - // Strip away any semicolon from the line. - let (line, _) = line.split_once(';').unwrap_or((line, "")); - - // Ensure the record header looks reasonable. - let mut words = line.split_ascii_whitespace().skip(2); - if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { - return Err(FromDnskeyTextError::Misformatted); - } - - // Parse the DNSKEY record data. - let mut data = IterScanner::new(words); - let dnskey: Dnskey> = Dnskey::scan(&mut data) - .map_err(|_| FromDnskeyTextError::Misformatted)?; - println!("importing {:?}", dnskey); - Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) - .map_err(FromDnskeyTextError::FromDnskey) - } - /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey(&self) -> Box<[u8]> { + pub fn to_dnskey_format(&self) -> Box<[u8]> { match self { Self::RsaSha1(k) | Self::RsaSha1Nsec3Sha1(k) | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.to_dnskey(), + | Self::RsaSha512(k) => k.to_dnskey_format(), // From my reading of RFC 6605, the marker byte is not included. Self::EcdsaP256Sha256(k) => k[1..].into(), @@ -247,7 +369,7 @@ pub struct RsaPublicKey { impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. - pub fn from_dnskey(data: &[u8]) -> Result { + pub fn from_dnskey_format(data: &[u8]) -> Result { if data.len() < 3 { return Err(FromDnskeyError::InvalidKey); } @@ -278,7 +400,7 @@ impl RsaPublicKey { } /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey(&self) -> Box<[u8]> { + pub fn to_dnskey_format(&self) -> Box<[u8]> { let mut key = Vec::new(); // Encode the exponent length. @@ -301,19 +423,10 @@ impl RsaPublicKey { impl PartialEq for RsaPublicKey { fn eq(&self, other: &Self) -> bool { - /// Compare after stripping leading zeros. - fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { - let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; - let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; - if a.len() == b.len() { - ring::constant_time::verify_slices_are_equal(a, b).is_ok() - } else { - false - } - } + use ring::constant_time::verify_slices_are_equal; - cmp_without_leading(&self.n, &other.n) - && cmp_without_leading(&self.e, &other.e) + verify_slices_are_equal(&self.n, &other.n).is_ok() + && verify_slices_are_equal(&self.e, &other.e).is_ok() } } @@ -325,7 +438,7 @@ pub enum FromDnskeyError { } #[derive(Clone, Debug)] -pub enum FromDnskeyTextError { +pub enum ParseDnskeyTextError { Misformatted, FromDnskey(FromDnskeyError), } From 00b86de28b0304d6d6170561e37f5f428241560d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 16 Oct 2024 11:44:08 +0200 Subject: [PATCH 105/569] Update to match upstream changes. --- src/sign/mod.rs | 1 + src/sign/records.rs | 179 +++++++++++++++++++++++++------------------- 2 files changed, 101 insertions(+), 79 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 6f31e7887..12fcce8cc 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -18,6 +18,7 @@ use crate::{ pub mod generic; pub mod openssl; +pub mod records; pub mod ring; /// Low-level signing functionality. diff --git a/src/sign/records.rs b/src/sign/records.rs index 1f9c729fd..81f1f2eed 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -11,7 +11,7 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::{FreezeBuilder, OctetsFrom}; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; +use crate::base::iana::{Class, Nsec3HashAlg, Rtype, SecAlg}; use crate::base::name::{ToLabelIter, ToName}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; @@ -20,11 +20,12 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Ds, Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Rrsig}; use crate::utils::base32; -use super::key::SigningKey; use super::ring::{nsec3_hash, Nsec3HashError}; +use crate::validate::Signature; +use super::SignRaw; //------------ SortedRecords ------------------------------------------------- @@ -74,32 +75,61 @@ impl SortedRecords { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } + /// Sign a zone using the given keys. + /// + /// A DNSKEY RR will be output for each key. + /// + /// Keys with a supported algorithm with the ZONE flag set will be used as + /// ZSKs. + /// + /// Keys with a supported algorithm with the ZONE flag AND the SEP flag + /// set will be used as KSKs. + /// + /// If only one key has a supported algorithm and has the ZONE flag set + /// AND has the SEP flag set, it will be used as a CSK (i.e. both KSK and + /// ZSK). #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, - apex: &FamilyName, + apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - ksk: Key, - zsk: Option, - ) -> Result>>, Key::Error> + keys: &[(Key, Dnskey)], + ) -> Result< + ( + Vec>>, + Vec>>, + ), + (), + > where N: ToName + Clone, D: RecordData + ComposeRecordData, - Key: SigningKey, - Octets: From + AsRef<[u8]>, - ApexName: ToName + Clone, + Key: SignRaw, + Octets: AsRef<[u8]> + Clone, { - let csk = zsk.is_none(); - let zsk = zsk.as_ref().unwrap_or(&ksk); - let Ok(ksk_dnskey) = ksk.dnskey() else { - unreachable!() - }; // # SigningKey doesn't implement Debug - let Ok(zsk_dnskey) = zsk.dnskey() else { - unreachable!() - }; // # SigningKey doesn't implement Debug - - let mut res = Vec::new(); + // Per RFC 8624 section 3.1 "DNSSEC Signing" column guidance. + let unsupported_algorithms = [ + SecAlg::RSAMD5, + SecAlg::DSA, + SecAlg::DSA_NSEC3_SHA1, + SecAlg::ECC_GOST, + ]; + + let ksks: Vec<&(Key, Dnskey)> = keys + .iter() + .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) + .filter(|(_, dk)| dk.is_zone_key() && dk.is_secure_entry_point()) + .collect(); + + let zsks: Vec<&(Key, Dnskey)> = keys + .iter() + .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) + .filter(|(_, dk)| dk.is_zone_key() && !dk.is_secure_entry_point()) + .collect(); + + let mut out_dnskeys: Vec>> = Vec::new(); + let mut out_rrsigs = Vec::new(); let mut buf = Vec::new(); // The owner name of a zone cut if we currently are at or below one. @@ -112,6 +142,20 @@ impl SortedRecords { families.skip_before(apex); for family in families { + if out_dnskeys.is_empty() { + let apex_ttl = family.records().next().unwrap().ttl(); + + // Add DNSKEYs to the result. + for dnskey in keys.iter().map(|(_, dnskey)| dnskey) { + out_dnskeys.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + dnskey.clone(), + )); + } + } + // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { @@ -154,63 +198,40 @@ impl SortedRecords { } } - // Create the signature. - buf.clear(); - - if rrset.rtype() == Rtype::DNSKEY { - let rrsig = ProtoRrsig::new( - rrset.rtype(), - ksk_dnskey.algorithm(), - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - ksk_dnskey.key_tag(), - apex.owner().clone(), - ); - rrsig.compose_canonical(&mut buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(&mut buf).unwrap(); - } - res.push(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - rrsig - .into_rrsig(ksk.sign(&buf)?.into()) - .expect("long signature"), - )); - } + let keys = if rrset.rtype() == Rtype::DNSKEY { + &ksks + } else { + &zsks + }; - if rrset.rtype() != Rtype::DNSKEY || csk { + for (key, dnskey) in keys { let rrsig = ProtoRrsig::new( rrset.rtype(), - zsk_dnskey.algorithm(), + key.algorithm(), name.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, - zsk_dnskey.key_tag(), + dnskey.key_tag(), apex.owner().clone(), ); rrsig.compose_canonical(&mut buf).unwrap(); for record in rrset.iter() { record.compose_canonical(&mut buf).unwrap(); } - - // Create and push the RRSIG record. - res.push(Record::new( + out_rrsigs.push(Record::new( name.owner().clone(), name.class(), rrset.ttl(), rrsig - .into_rrsig(zsk.sign(&buf)?.into()) + .into_rrsig(key.sign_raw(&buf)) .expect("long signature"), )); } } } - Ok(res) + + Ok((out_dnskeys, out_rrsigs)) } pub fn nsecs( @@ -768,29 +789,29 @@ impl FamilyName { Record::new(self.owner.clone(), self.class, ttl, data) } - pub fn dnskey>( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: Clone, - { - key.dnskey() - .map(|dnskey| self.clone().into_record(ttl, dnskey.convert())) - } - - pub fn ds( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: ToName + Clone, - { - key.ds(&self.owner) - .map(|ds| self.clone().into_record(ttl, ds)) - } + // pub fn dnskey>( + // &self, + // ttl: Ttl, + // key: K, + // ) -> Result>, K::Error> + // where + // N: Clone, + // { + // key.dnskey() + // .map(|dnskey| self.clone().into_record(ttl, dnskey.convert())) + // } + + // pub fn ds( + // &self, + // ttl: Ttl, + // key: K, + // ) -> Result>, K::Error> + // where + // N: ToName + Clone, + // { + // key.ds(&self.owner) + // .map(|ds| self.clone().into_record(ttl, ds)) + // } } impl<'a, N: Clone> FamilyName<&'a N> { From 6388387c679b69fd6dcbbc2476b50909aa5b9e18 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 11:59:41 +0200 Subject: [PATCH 106/569] [sign/openssl] Pad ECDSA keys when exporting Tests would spuriously fail when generated keys were only 31 bytes in size. --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 46553dbad..4086f8947 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -166,12 +166,12 @@ impl SecretKey { } SecAlg::ECDSAP256SHA256 => { let key = self.pkey.ec_key().unwrap(); - let key = key.private_key().to_vec(); + let key = key.private_key().to_vec_padded(32).unwrap(); generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); - let key = key.private_key().to_vec(); + let key = key.private_key().to_vec_padded(48).unwrap(); generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { From b2cfa7bbf9fd691731bf2983e6188f1e6cae4928 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 13:49:41 +0200 Subject: [PATCH 107/569] [validate] Implement 'Key::key_tag()' This is more efficient than allocating a DNSKEY record and computing the key tag there. --- src/validate.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/validate.rs b/src/validate.rs index b040acf9b..303edb4ce 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -60,6 +60,11 @@ impl Key { &self.key } + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg { + self.key.algorithm() + } + /// Whether this is a zone signing key. /// /// From RFC 4034, section 2.1.1: @@ -92,6 +97,26 @@ impl Key { pub fn is_secure_entry_point(&self) -> bool { self.flags & (1 << 15) != 0 } + + /// The key tag. + pub fn key_tag(&self) -> u16 { + // NOTE: RSA/MD5 uses a different algorithm. + + // NOTE: A u32 can fit the sum of 65537 u16s without overflowing. A + // key can never exceed 64KiB anyway, so we won't even get close to + // the limit. Let's just add into a u32 and normalize it after. + let mut res = 0u32; + + // Add basic DNSKEY fields. + res += self.flags as u32; + res += u16::from_be_bytes([3, self.algorithm().to_int()]) as u32; + + // Add the raw key tag from the public key. + res += self.key.raw_key_tag(); + + // Normalize and return the result. + (res as u16).wrapping_add((res >> 16) as u16) + } } impl> Key { @@ -253,6 +278,32 @@ impl RawPublicKey { Self::Ed448(_) => SecAlg::ED448, } } + + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + fn compute(data: &[u8]) -> u32 { + data.chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + // A 0 byte is appended for an incomplete chunk. + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum() + } + + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.raw_key_tag(), + + Self::EcdsaP256Sha256(k) => compute(&k[1..]), + Self::EcdsaP384Sha384(k) => compute(&k[1..]), + Self::Ed25519(k) => compute(&**k), + Self::Ed448(k) => compute(&**k), + } + } } impl RawPublicKey { @@ -367,6 +418,44 @@ pub struct RsaPublicKey { pub e: Box<[u8]>, } +impl RsaPublicKey { + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + let mut res = 0u32; + + // Extended exponent lengths start with '00 (exp_len >> 8)', which is + // just zero for shorter exponents. That doesn't affect the result, + // so let's just do it unconditionally. + res += (self.e.len() >> 8) as u32; + res += u16::from_be_bytes([self.e.len() as u8, self.e[0]]) as u32; + + let mut chunks = self.e[1..].chunks_exact(2); + res += chunks + .by_ref() + .map(|chunk| u16::from_be_bytes(chunk.try_into().unwrap()) as u32) + .sum::(); + + let n = if !chunks.remainder().is_empty() { + res += + u16::from_be_bytes([chunks.remainder()[0], self.n[0]]) as u32; + &self.n[1..] + } else { + &self.n + }; + + res += n + .chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum::(); + + res + } +} + impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. pub fn from_dnskey_format(data: &[u8]) -> Result { @@ -929,6 +1018,14 @@ mod test { type Dnskey = crate::rdata::Dnskey>; type Rrsig = crate::rdata::Rrsig, Name>; + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + // Returns current root KSK/ZSK for testing (2048b) fn root_pubkey() -> (Dnskey, Dnskey) { let ksk = base64::decode::>( @@ -973,6 +1070,44 @@ mod test { ) } + #[test] + fn parse_dnskey_text() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let _ = Key::>::parse_dnskey_text(&data).unwrap(); + } + } + + #[test] + fn key_tag() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + assert_eq!(key.to_dnskey().key_tag(), key_tag); + assert_eq!(key.key_tag(), key_tag); + } + } + + #[test] + fn dnskey_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + let dnskey = key.to_dnskey().convert(); + let same = Key::from_dnskey(key.owner().clone(), dnskey).unwrap(); + assert_eq!(key.to_dnskey(), same.to_dnskey()); + } + } + #[test] fn dnskey_digest() { let (dnskey, _) = root_pubkey(); From e0344a6504e3abf2e5b8857b6646109f512644d1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 14:14:03 +0200 Subject: [PATCH 108/569] [validate] Correct bit offsets for flags --- src/validate.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 303edb4ce..6b48e8f10 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -75,6 +75,19 @@ impl Key { /// > the DNSKEY record holds some other type of DNS public key and MUST /// > NOT be used to verify RRSIGs that cover RRsets. pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From RFC 5011, section 3: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + pub fn is_revoked(&self) -> bool { self.flags & (1 << 7) != 0 } @@ -82,7 +95,6 @@ impl Key { /// /// From RFC 4034, section 2.1.1: /// - /// /// > Bit 15 of the Flags field is the Secure Entry Point flag, described /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a /// > key intended for use as a secure entry point. This flag is only @@ -95,7 +107,7 @@ impl Key { /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs /// > that cover RRsets. pub fn is_secure_entry_point(&self) -> bool { - self.flags & (1 << 15) != 0 + self.flags & 1 != 0 } /// The key tag. From f65c5ccde6d1853b88a8c685c0a872135506f155 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 15:10:31 +0200 Subject: [PATCH 109/569] [validate] Implement support for digests The test keys have been rotated and replaced with KSKs since they have associated DS records I can verify digests against. I also expanded Ring's testing to include ECDSA keys. The validate module tests SHA-1 keys as well, which aren't supported by 'sign'. --- src/sign/generic.rs | 16 +- src/sign/openssl.rs | 19 +- src/sign/ring.rs | 14 +- src/validate.rs | 239 ++++++++++++++++-- test-data/dnssec-keys/Ktest.+005+00439.ds | 1 + test-data/dnssec-keys/Ktest.+005+00439.key | 1 + .../dnssec-keys/Ktest.+005+00439.private | 10 + test-data/dnssec-keys/Ktest.+007+22204.ds | 1 + test-data/dnssec-keys/Ktest.+007+22204.key | 1 + .../dnssec-keys/Ktest.+007+22204.private | 10 + test-data/dnssec-keys/Ktest.+008+27096.key | 1 - .../dnssec-keys/Ktest.+008+27096.private | 10 - test-data/dnssec-keys/Ktest.+008+60616.ds | 1 + test-data/dnssec-keys/Ktest.+008+60616.key | 1 + .../dnssec-keys/Ktest.+008+60616.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 - test-data/dnssec-keys/Ktest.+013+42253.ds | 1 + test-data/dnssec-keys/Ktest.+013+42253.key | 1 + ...40436.private => Ktest.+013+42253.private} | 2 +- test-data/dnssec-keys/Ktest.+014+17013.key | 1 - .../dnssec-keys/Ktest.+014+17013.private | 3 - test-data/dnssec-keys/Ktest.+014+33566.ds | 1 + test-data/dnssec-keys/Ktest.+014+33566.key | 1 + .../dnssec-keys/Ktest.+014+33566.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 - .../dnssec-keys/Ktest.+015+43769.private | 3 - test-data/dnssec-keys/Ktest.+015+56037.ds | 1 + test-data/dnssec-keys/Ktest.+015+56037.key | 1 + .../dnssec-keys/Ktest.+015+56037.private | 3 + test-data/dnssec-keys/Ktest.+016+07379.ds | 1 + test-data/dnssec-keys/Ktest.+016+07379.key | 1 + .../dnssec-keys/Ktest.+016+07379.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 - .../dnssec-keys/Ktest.+016+34114.private | 3 - 34 files changed, 295 insertions(+), 72 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.ds create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.key create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.private create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.ds create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.key create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.ds create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.key create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.private delete mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.ds create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.key rename test-data/dnssec-keys/{Ktest.+013+40436.private => Ktest.+013+42253.private} (50%) delete mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key delete mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.ds create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.key create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.private delete mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key delete mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.ds create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.key create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.private create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.ds create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.key create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.private delete mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key delete mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f7caaa5a0..96a343b1e 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -436,17 +436,18 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; #[test] fn secret_from_dns() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKey::parse_from_bind(&data).unwrap(); @@ -457,7 +458,8 @@ mod tests { #[test] fn secret_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKey::parse_from_bind(&data).unwrap(); diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 4086f8947..def9ac40b 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -357,11 +357,11 @@ mod tests { use super::SecretKey; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; #[test] @@ -385,7 +385,8 @@ mod tests { #[test] fn imported_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -411,7 +412,8 @@ mod tests { #[test] fn public_key() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -431,7 +433,8 @@ mod tests { #[test] fn sign() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); diff --git a/src/sign/ring.rs b/src/sign/ring.rs index e0be1943a..67aab7829 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -222,13 +222,18 @@ mod tests { use super::SecretKey; - const KEYS: &[(SecAlg, u16)] = - &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + ]; #[test] fn public_key() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); @@ -250,7 +255,8 @@ mod tests { #[test] fn sign() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); diff --git a/src/validate.rs b/src/validate.rs index 6b48e8f10..0670d0030 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -13,7 +13,7 @@ use crate::base::record::Record; use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; use crate::base::Rtype; -use crate::rdata::{Dnskey, Rrsig}; +use crate::rdata::{Dnskey, Ds, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use octseq::{EmptyBuilder, FromBuilder}; @@ -22,6 +22,8 @@ use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +//----------- Key ------------------------------------------------------------ + /// A DNSSEC key for a particular zone. #[derive(Clone)] pub struct Key { @@ -39,12 +41,18 @@ pub struct Key { key: RawPublicKey, } +//--- Construction + impl Key { /// Construct a new DNSSEC key manually. pub fn new(owner: Name, flags: u16, key: RawPublicKey) -> Self { Self { owner, flags, key } } +} + +//--- Inspection +impl Key { /// The owner name attached to the key. pub fn owner(&self) -> &Name { &self.owner @@ -129,8 +137,53 @@ impl Key { // Normalize and return the result. (res as u16).wrapping_add((res >> 16) as u16) } + + /// The digest of this key. + pub fn digest( + &self, + algorithm: DigestAlg, + ) -> Result>, DigestError> + where + Octs: AsRef<[u8]>, + { + let mut context = ring::digest::Context::new(match algorithm { + DigestAlg::SHA1 => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, + DigestAlg::SHA256 => &ring::digest::SHA256, + DigestAlg::SHA384 => &ring::digest::SHA384, + _ => return Err(DigestError::UnsupportedAlgorithm), + }); + + // Add the owner name. + if self + .owner + .as_slice() + .iter() + .any(|&b| b.is_ascii_uppercase()) + { + let mut owner = [0u8; 256]; + owner[..self.owner.len()].copy_from_slice(self.owner.as_slice()); + owner.make_ascii_lowercase(); + context.update(&owner[..self.owner.len()]); + } else { + context.update(self.owner.as_slice()); + } + + // Add basic DNSKEY fields. + context.update(&self.flags.to_be_bytes()); + context.update(&[3, self.algorithm().to_int()]); + + // Add the public key. + self.key.digest(&mut context); + + // Finalize the digest. + let digest = context.finish().as_ref().into(); + Ok(Ds::new(self.key_tag(), self.algorithm(), algorithm, digest) + .unwrap()) + } } +//--- Conversion to and from DNSKEYs + impl> Key { /// Deserialize a key from DNSKEY record data. /// @@ -232,6 +285,8 @@ impl> Key { } } +//----------- RsaPublicKey --------------------------------------------------- + /// A low-level public key. #[derive(Clone, Debug)] pub enum RawPublicKey { @@ -276,6 +331,8 @@ pub enum RawPublicKey { Ed448(Box<[u8; 57]>), } +//--- Inspection + impl RawPublicKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { @@ -316,8 +373,25 @@ impl RawPublicKey { Self::Ed448(k) => compute(&**k), } } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.digest(context), + + Self::EcdsaP256Sha256(k) => context.update(&k[1..]), + Self::EcdsaP384Sha384(k) => context.update(&k[1..]), + Self::Ed25519(k) => context.update(&**k), + Self::Ed448(k) => context.update(&**k), + } + } } +//--- Conversion to and from DNSKEYs + impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. pub fn from_dnskey_format( @@ -391,6 +465,8 @@ impl RawPublicKey { } } +//--- Comparison + impl PartialEq for RawPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -417,6 +493,10 @@ impl PartialEq for RawPublicKey { } } +impl Eq for RawPublicKey {} + +//----------- RsaPublicKey --------------------------------------------------- + /// A generic RSA public key. /// /// All fields here are arbitrary-precision integers in big-endian format, @@ -430,6 +510,8 @@ pub struct RsaPublicKey { pub e: Box<[u8]>, } +//--- Inspection + impl RsaPublicKey { /// The raw key tag computation for this value. fn raw_key_tag(&self) -> u32 { @@ -466,8 +548,25 @@ impl RsaPublicKey { res } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + context.update(&[exp_len]); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + context.update(&[0u8, (exp_len >> 8) as u8, exp_len as u8]); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + context.update(&self.e); + context.update(&self.n); + } } +//--- Conversion to and from DNSKEYs + impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. pub fn from_dnskey_format(data: &[u8]) -> Result { @@ -522,6 +621,8 @@ impl RsaPublicKey { } } +//--- Comparison + impl PartialEq for RsaPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -531,18 +632,9 @@ impl PartialEq for RsaPublicKey { } } -#[derive(Clone, Debug)] -pub enum FromDnskeyError { - UnsupportedAlgorithm, - UnsupportedProtocol, - InvalidKey, -} +impl Eq for RsaPublicKey {} -#[derive(Clone, Debug)] -pub enum ParseDnskeyTextError { - Misformatted, - FromDnskey(FromDnskeyError), -} +//----------- Signature ------------------------------------------------------ /// A cryptographic signature. /// @@ -985,6 +1077,71 @@ fn rsa_exponent_modulus( //============ Error Types =================================================== +//----------- DigestError ---------------------------------------------------- + +/// An error when computing a digest. +#[derive(Clone, Debug)] +pub enum DigestError { + UnsupportedAlgorithm, +} + +//--- Display, Error + +impl fmt::Display for DigestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl error::Error for DigestError {} + +//----------- FromDnskeyError ------------------------------------------------ + +/// An error in reading a DNSKEY record. +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +//--- Display, Error + +impl fmt::Display for FromDnskeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + Self::UnsupportedProtocol => "unsupported protocol", + Self::InvalidKey => "malformed key", + }) + } +} + +impl error::Error for FromDnskeyError {} + +//----------- ParseDnskeyTextError ------------------------------------------- + +#[derive(Clone, Debug)] +pub enum ParseDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +//--- Display, Error + +impl fmt::Display for ParseDnskeyTextError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Misformatted => "misformatted DNSKEY record", + Self::FromDnskey(e) => return e.fmt(f), + }) + } +} + +impl error::Error for ParseDnskeyTextError {} + //------------ AlgorithmError ------------------------------------------------ /// An algorithm error during verification. @@ -995,17 +1152,15 @@ pub enum AlgorithmError { InvalidData, } -//--- Display and Error +//--- Display, Error impl fmt::Display for AlgorithmError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - AlgorithmError::Unsupported => { - f.write_str("unsupported algorithm") - } - AlgorithmError::BadSig => f.write_str("bad signature"), - AlgorithmError::InvalidData => f.write_str("invalid data"), - } + f.write_str(match self { + AlgorithmError::Unsupported => "unsupported algorithm", + AlgorithmError::BadSig => "bad signature", + AlgorithmError::InvalidData => "invalid data", + }) } } @@ -1031,11 +1186,13 @@ mod test { type Rrsig = crate::rdata::Rrsig, Name>; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA1, 439), + (SecAlg::RSASHA1_NSEC3_SHA1, 22204), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; // Returns current root KSK/ZSK for testing (2048b) @@ -1085,7 +1242,8 @@ mod test { #[test] fn parse_dnskey_text() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -1096,7 +1254,8 @@ mod test { #[test] fn key_tag() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -1106,10 +1265,34 @@ mod test { } } + #[test] + fn digest() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + + // Scan the DS record from the file. + let path = format!("test-data/dnssec-keys/K{}.ds", name); + let data = std::fs::read_to_string(path).unwrap(); + let mut scanner = IterScanner::new(data.split_ascii_whitespace()); + let _ = scanner.scan_name().unwrap(); + let _ = Class::scan(&mut scanner).unwrap(); + assert_eq!(Rtype::scan(&mut scanner).unwrap(), Rtype::DS); + let ds = Ds::scan(&mut scanner).unwrap(); + + assert_eq!(key.digest(ds.digest_type()).unwrap(), ds); + } + } + #[test] fn dnskey_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); diff --git a/test-data/dnssec-keys/Ktest.+005+00439.ds b/test-data/dnssec-keys/Ktest.+005+00439.ds new file mode 100644 index 000000000..543137100 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.ds @@ -0,0 +1 @@ +test. IN DS 439 5 1 3d54b51d59c71418104ec48bacb3d1a01b8eaa30 diff --git a/test-data/dnssec-keys/Ktest.+005+00439.key b/test-data/dnssec-keys/Ktest.+005+00439.key new file mode 100644 index 000000000..35999a0ae --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 5 AwEAAb5nA65uEYX1bRYwT53jRQqAk/mLbi3SlN3xxkdtn7rTkKgEdiBPIF8+0OVyS3x/OCLPTrto6ojUI5etA1VDZPiTLvuq6rIhn3oNyc5o9Kzl4RX4XptLTrt7ldRcpIjgcgqMJoERUWLQqxoXCfRqClxO2Erk0UZhe3GteCMSEfoGBU5MdPzrrEE6GMxEAKFHabjupQ4GazxfWO7+D38lsmUNJwgCg/B14CIcvTS6cHKFmKJKYEEmAj/kx+LnZd9bmeyagFz8CcgcI/NUiSDgdgx/OeCdSc39OHCp9a0NSJuywbbIxpLPw8cIvgZ8OnHuGjrNTROuyYXVxQM1xe914DM= ;{id = 439 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+005+00439.private b/test-data/dnssec-keys/Ktest.+005+00439.private new file mode 100644 index 000000000..1d8d11ce6 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 5 (RSASHA1) +Modulus: vmcDrm4RhfVtFjBPneNFCoCT+YtuLdKU3fHGR22futOQqAR2IE8gXz7Q5XJLfH84Is9Ou2jqiNQjl60DVUNk+JMu+6rqsiGfeg3Jzmj0rOXhFfhem0tOu3uV1FykiOByCowmgRFRYtCrGhcJ9GoKXE7YSuTRRmF7ca14IxIR+gYFTkx0/OusQToYzEQAoUdpuO6lDgZrPF9Y7v4PfyWyZQ0nCAKD8HXgIhy9NLpwcoWYokpgQSYCP+TH4udl31uZ7JqAXPwJyBwj81SJIOB2DH854J1Jzf04cKn1rQ1Im7LBtsjGks/Dxwi+Bnw6ce4aOs1NE67JhdXFAzXF73XgMw== +PublicExponent: AQAB +PrivateExponent: CSEarcAR+ltUhK4s/cQKPmurLK7rydSsAKGkFoQCFvd9RcvDojRJDWgPT2vAhNKmGBKFPY/VQa7yRJvYv2YrhDkCarISQ2zrSZ3kTDpUvlQzYQCiAKOGveSPauRE8K8vqKPPANHva2PX9bifEzy2YctXVu1Lv3/TEcCgibCcc2FwrKqzwHZ/AvMeMQD7UjetkpFELqYRHkdFQt+8vFDTmXNhhtm2O5xgYymsaaLW7mOLyR7oo25Uk93ouZx3Ibo9yNHdeJG6S6wFeWQaLGKA78tJK10gaUwiHIdEYh4qQ+pSsjztk6A2ObaWmlbt5Ve9qN1WW+KVizATJIQUQvhocQ== +Prime1: 42WKyzrGcBkhZz8xTvNWzlkhvb6aHgryXlgMP2E1GxRgZDApj6XqFzDHRbC/QaRvZi9skuoEz148xH6Hs2oJQ3I/2+dX/7YmnwPZyxHCx2LUlQ+AqEXXWNGCXQ5I6EvDDFeLSqb7m4sZhnnMaTOpyrmYqFzkxZkWrNiSHJjq5us= +Prime2: 1lo1/h5mxzarMFwfrOI+ErR8bvYrAp8hr33MV58MUwWy2IyUIlJRPJVg6DAaT87jwQuJEVarqq2IB48TI1SKglR5CJNcRuTviHWVViXDY7AVnUvHWiiKncTKDQG7vI4Ffft46qVEdaKLjkPBsapuibt0ocpKszVdmr0usP31qdk= +Exponent1: VIQbD+nqcyOD/MHJ69QZgVwzZDiBQ4VCC7qh4rSYblYmdVZJPDCoTrI8fjRxAU7CcLJTok8ENqaJ42Y7vX09sCm4flz/ofTradKekhEp2b1r0XMPmHtMzKAh2cBDbMMr3Vx0Uuy5O1h5xjdit/8Rrl1I1dqg1KhPezKLK8HSHL0= +Exponent2: QqGALyIcKMjhpgK9Bey+Bup707JJ5GK7AeZE4ufZ2OTol0/7rD+SaRa2LPbm9vAE9Dk1vmIGsuOGaXMcK9tXwvOnO/cytAbuPqjuZv0OI6rUzTSFH42CqVBGzow/Y3lyU5scFzSQd1CzuOFvEF8+RSo0MybC2bo5AqTUIsiO2OE= +Coefficient: wOxhD2sDrZhzWq99qjyaYSZxQrPhJWkLR8LhnZEmPlQwfExz939Qw1TkmBpYcr67sN8UTqY93N7mES2LOJrkE/RzstzaKQS2We8mypovFOwcZu3GfJSsRYJRhsW5dEIiLAVw8a/bnC+K0m2Ahiy8v3GwQVo0u1KZ6oSHmG8IWng= diff --git a/test-data/dnssec-keys/Ktest.+007+22204.ds b/test-data/dnssec-keys/Ktest.+007+22204.ds new file mode 100644 index 000000000..913575095 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.ds @@ -0,0 +1 @@ +test. IN DS 22204 7 1 0783210826bc4a4ab0d4b329458f216bf787a00c diff --git a/test-data/dnssec-keys/Ktest.+007+22204.key b/test-data/dnssec-keys/Ktest.+007+22204.key new file mode 100644 index 000000000..26bf24bfc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 7 AwEAAcOFirT7uFYwPyEhyio+mb/9yMQH6ENYEOboEX2c0WIPBFr1s34rZ3SWEWsTvxLOMKr3drzSZtcpCQ6vEyPpQpGo1cpWlVSZ7QB73iWw21rZkz/r4MykyloPoJ8ghr4SRSfJx6CjAb+Fhz3bUF4YWofJEshuZMbxLnOEi2hR9T2zTPRjYltA1sfhU478ixh6ddNym+kCIBEhoFIFyKYb5VznOoWcR/mOexQMfUdNqKoIwnhCX8Sg2dKYdgeDDPsZH3AaWp8BY3aqiqOEacSO2XI+7Pdr0rVfszJfcCsf4g+R/7oBt6dtO9WS+0YqVN0J8WQ/9HmWFeCJgY2Rs4c9eDk= ;{id = 22204 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+007+22204.private b/test-data/dnssec-keys/Ktest.+007+22204.private new file mode 100644 index 000000000..ecb576d4c --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 7 (RSASHA1_NSEC3) +Modulus: w4WKtPu4VjA/ISHKKj6Zv/3IxAfoQ1gQ5ugRfZzRYg8EWvWzfitndJYRaxO/Es4wqvd2vNJm1ykJDq8TI+lCkajVylaVVJntAHveJbDbWtmTP+vgzKTKWg+gnyCGvhJFJ8nHoKMBv4WHPdtQXhhah8kSyG5kxvEuc4SLaFH1PbNM9GNiW0DWx+FTjvyLGHp103Kb6QIgESGgUgXIphvlXOc6hZxH+Y57FAx9R02oqgjCeEJfxKDZ0ph2B4MM+xkfcBpanwFjdqqKo4RpxI7Zcj7s92vStV+zMl9wKx/iD5H/ugG3p2071ZL7RipU3QnxZD/0eZYV4ImBjZGzhz14OQ== +PublicExponent: AQAB +PrivateExponent: VaLpgGCaOgHSvK/AjOUzUVCWPSobdFefu4sckhB78v+R0Ec6cUIQg5NxGJ2i/FkcHt3Zf1WGXqnmAizzbLCvi/3PedqXeGEc2a/nOknuoamXYZFuOiPZTz32A4xrB9gXuxgZXAXZb6nL9O9YkYYILN4IYIpdkHc1ebotlykCiZ14YjS7sFiKNwxk4Pk5HC9qwQlRujO2LZN6Gp5Pqj3i8h/d9/xgCV+IJGwUiy8y0czEJH3f+k76IaM4ZQZiieS/3vXmytHieAVGIZBH5yztgy+p+GJgVXPEb/7WESC38WSn6GwqthcBZXrSOjhqP2PfFuDDfEhglTNSBqhONzE28w== +Prime1: 9trbMq0VgNtsJuyM5CMQa/feEidp51a1POok8pPAZ6SUpno+oNzITCrSga7i08HzBoW22k9jNmIJmpwXDeDoX2TICgDEyzIqzBH+V1zCE1dI8fv9w/hF9mt/qoZ0PN/Jh4Zcu/AHtmRaHAO6lBFblS6EZxdX4lTeVj8toGxR0ms= +Prime2: ysPYyIh9vwN5rKNPKnrjPtMshjFv6CEnXeFDhVvxcutudgayyu0+Gu8g54WjJ/tpEsDENjhi1Da21pn5RxpgCbe/qE+2Z7CGsw+FI+UcOgx8EEm1aGSenC+7AVACarPtU6zr5/kcPiqCm6zPatLJvXRfbQAa/hHdl5Xg28HX8Os= +Exponent1: Da4zV6uf9XQzmjSh2kLXNiSWegsVI2z6vlV7lrX5g8TrOA6uSdvyfcYhxG4cw/+LqGDgsViU9v6X6amc3XgJaL/9FhDU1y4AkS6uGclaOBguQrrkZWfs+KsceCbbakQ8tvYLTZ8PzlvhYowSWwJbQPlC/TOd+z0Y1U7LCIj4P+E= +Exponent2: LnOrqFVMqYP8TgajzlGU2gG7A4sz3fQqdqFyvIyRxggVqEhkkYTEY5tA6Il/FVvNeJRc3ycPzRozzPo9V4K9WbyU1dRdL2gLk94MXGrSiqHtkjWwr5fNlm6A4w4XX6aUykSlTuGNDNjkTxHJ+ukLerG8YtZRWL9zCpU1jGLeO70= +Coefficient: quDhRGQcA/iLpbDJym2ErykV+wsflci0KZIf7/rtCnsDJZSVYQlB/UPY2S5ne+zwuY8/fNYGIVMYN1sV8OPF3AIpTOtte5pc+1V+4rbuQEQhQw9uIvX4205GEc2sjJ637CT46FDP/lnPL7TdvV6NdOuLyDDImbaMqyLtMSJ5IEs= diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key deleted file mode 100644 index 5aa614f71..000000000 --- a/test-data/dnssec-keys/Ktest.+008+27096.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private deleted file mode 100644 index b5819714f..000000000 --- a/test-data/dnssec-keys/Ktest.+008+27096.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 -PublicExponent: AQAB -PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN -Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj -Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd -Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf -Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 -Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+60616.ds b/test-data/dnssec-keys/Ktest.+008+60616.ds new file mode 100644 index 000000000..65444f942 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.ds @@ -0,0 +1 @@ +test. IN DS 60616 8 2 6b91f7b7134cf916d909e2905b5707e3ea6c86842339f09d87c858d7ccd620b3 diff --git a/test-data/dnssec-keys/Ktest.+008+60616.key b/test-data/dnssec-keys/Ktest.+008+60616.key new file mode 100644 index 000000000..fa6c03d8a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 8 AwEAAdaxEmT1eAAnXMGDjYfivh6ax6BOESlNZY85BlVWkCOYV6jf5GcSgweqcCowFW2HtHKiE/FACwG5Wfq/xCDhLHYg4PQIvd5UcrDzj+WBEFe7pVhUjZrMsMRAVy2W4jliat6IrJv+CdycErp4cLxmqfNECIP7i9vI8onruvBe1YWebJN38TxdGCteg5waI27DNaQsXldxZoCfSY7Fkhj7BJ4XxHDeWzE876LmSMkkYFWqEQwesD280piL+4tmySMPxhVC1EUguQyn/Lc9FbEd3h1RyaO8hg8ub/70espLVElE9ImOibaY+gj9jK7HFD/mqdxYdFfr3yiQsGOt2ui4jGM= ;{id = 60616 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+008+60616.private b/test-data/dnssec-keys/Ktest.+008+60616.private new file mode 100644 index 000000000..8df7cdc20 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 1rESZPV4ACdcwYONh+K+HprHoE4RKU1ljzkGVVaQI5hXqN/kZxKDB6pwKjAVbYe0cqIT8UALAblZ+r/EIOEsdiDg9Ai93lRysPOP5YEQV7ulWFSNmsywxEBXLZbiOWJq3oism/4J3JwSunhwvGap80QIg/uL28jyieu68F7VhZ5sk3fxPF0YK16DnBojbsM1pCxeV3FmgJ9JjsWSGPsEnhfEcN5bMTzvouZIySRgVaoRDB6wPbzSmIv7i2bJIw/GFULURSC5DKf8tz0VsR3eHVHJo7yGDy5v/vR6yktUSUT0iY6Jtpj6CP2MrscUP+ap3Fh0V+vfKJCwY63a6LiMYw== +PublicExponent: AQAB +PrivateExponent: EBBYZ6ofnCYAgGY/J8S6easWdr3V9jjZTtnIdIgxPsiTqTTKGpWTAkwpb66rW8evTnMmz4KoOtfLOMIygvdLjrHabcgIVONitYTJO+CSqs3aiv0V9K2OKGZcCjLjoxbkbNmIeMo4TgPLjvJGFS1lV/4Q2Qya+WCpbSfF6V20gkvQ46dtdRaswFOeav0WIm8LdudWDlYei89EIL243JlDErRmcrh6ZrxIg2TMT+mYJCoM6zfhFkbZuQyagn0Fguymp3Kc31SFqdReF9Q/IIQKwNiW14gdxCEHxq+y7xajCF0bhRZAY/hVyRr4qpx2ZRNMdg5qR2a8IilhH2+YXkHBUQ== +Prime1: 7fuvTpTPTHAQV3nQEW6WLf9xrf0G6ka5E2Lvn+jaawk60VZHoVybpURd0Dq586ZinQpJ2ovEfCd9Os8vn31BNrtulz8mfmKz1rObbdKvo0XRSExcLFx2ZG35Bdo/6H8Ri5e/9gx0m0yJeKspNW20uJX9ndk8Lsm5J9d+8SvcZis= +Prime2: 5vH6ly1VSF1DafdVGMKiHY4icP4OAAPJ/Sl+ihcYzbguhZ82fJ3mZeYLDZWSozwnvhK9PTqGwVRhLJH875AUrU/YA+nEBb5dVHMgGb4Afx2PzOlhgDIhEiRD0QW/9bwq45nITfnFMbYzkE2e08KZ/tjiusQIRZAQCkEBEbNITqk= +Exponent1: pKvW7iUCG/4fEKh1VNqUiFeNLbs7obg2MDfxX1EccZv9WwS8o+cUvBLGZ2N7cCDdc5S+7b5wwwgAG0Vpyo49JcYkC/vigumBTzsQfbmfVvbkjYZo8Tk5otyFx4rxVcs3NMRYS8Tqmtsm9Jxa82Fp/5+p0iOTBT0IJY1zhSW4Z+k= +Exponent2: kvemyxIUVarUPdkiFFG4LSrIjDOA4U2H+02us14jcLcnE+3QFNm/R1Vv70MiQDMF75WpTA+0tc9mz6BP4HxGTEylYUggcK9GYXmqEfeyBTLg0jwqyhQcq5jcd2Y7VLxcZt70c3rhnNMgWVKsIoKS0XVgRA6AXRRiwMPBVGxNNZE= +Coefficient: HsJ5e503CSA3lF3sPrKuL4EuT1Qv0IMHRSd5cZyJj6fCvLYzXi+NtlUX+GMHKuzSm64t6Jrw+FN2I1XTn0QvnpMQqwgou/G79I3dy3a82B+I2qBXgPFqpb/Zj6Eno+aQ+jxD4i6C2b7GhpAxpENwBLIPoIhyJSmWl1o2DDo2irs= diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key deleted file mode 100644 index 7f7cd0fcc..000000000 --- a/test-data/dnssec-keys/Ktest.+013+40436.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+42253.ds b/test-data/dnssec-keys/Ktest.+013+42253.ds new file mode 100644 index 000000000..8d52a1301 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.ds @@ -0,0 +1 @@ +test. IN DS 42253 13 2 b55c30248246756635ee8eb9ff03a9492df46257f4f6537ea85e579b501765e6 diff --git a/test-data/dnssec-keys/Ktest.+013+42253.key b/test-data/dnssec-keys/Ktest.+013+42253.key new file mode 100644 index 000000000..c9d6127ea --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 13 /5DQ8gQAUp0yITNeE6p0rKQPblVGKOPAdPKxWLQ/FOrkcax3S7qJZh6Z9ayn+EewnpQcmdexlOvxsMf5q8ppCw== ;{id = 42253 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+42253.private similarity index 50% rename from test-data/dnssec-keys/Ktest.+013+40436.private rename to test-data/dnssec-keys/Ktest.+013+42253.private index 39f5e8a8d..7b26e96a1 100644 --- a/test-data/dnssec-keys/Ktest.+013+40436.private +++ b/test-data/dnssec-keys/Ktest.+013+42253.private @@ -1,3 +1,3 @@ Private-key-format: v1.2 Algorithm: 13 (ECDSAP256SHA256) -PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= +PrivateKey: uKp4Xz2aB3/LfLGADBjNYFvAZbDHBCO+uJdL+GFCVOY= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key deleted file mode 100644 index c7b6aa1d4..000000000 --- a/test-data/dnssec-keys/Ktest.+014+17013.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private deleted file mode 100644 index 9648a876a..000000000 --- a/test-data/dnssec-keys/Ktest.+014+17013.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 14 (ECDSAP384SHA384) -PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+014+33566.ds b/test-data/dnssec-keys/Ktest.+014+33566.ds new file mode 100644 index 000000000..7e3165c6c --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.ds @@ -0,0 +1 @@ +test. IN DS 33566 14 4 d27e8964b63e8f3db4001834d03f1034669e5d39500b06863cc9f38cd649131421bb78b0b08f0ec61a8c8caf0cf09a19 diff --git a/test-data/dnssec-keys/Ktest.+014+33566.key b/test-data/dnssec-keys/Ktest.+014+33566.key new file mode 100644 index 000000000..dd967bccb --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 14 mce1CBcESReUP0iQYCnnhoWrVYe86PnFHIkKkr7qmO5q7AwAENchMaBPzaPOOuwx8Z8AcqIjXLOGL13RDT1lvLLkH7IJMIPHRwiXiFoj0KXBugvKLmMT3a0Nc8s8Uau9 ;{id = 33566 (ksk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+33566.private b/test-data/dnssec-keys/Ktest.+014+33566.private new file mode 100644 index 000000000..276b9d315 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: 3e1YdfRwn8YOX3Ai84BWVLl3/SphcQIeCkvQnzszKqR3U2xmq/G5HtiGTnBZ1WSW diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key deleted file mode 100644 index 8a1f24f67..000000000 --- a/test-data/dnssec-keys/Ktest.+015+43769.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private deleted file mode 100644 index e178a3bd4..000000000 --- a/test-data/dnssec-keys/Ktest.+015+43769.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 15 (ED25519) -PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+015+56037.ds b/test-data/dnssec-keys/Ktest.+015+56037.ds new file mode 100644 index 000000000..fb802353f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.ds @@ -0,0 +1 @@ +test. IN DS 56037 15 2 665c358b671a9ed5310667b2bacfb526ace344f59d085c8331c532e6a7024f75 diff --git a/test-data/dnssec-keys/Ktest.+015+56037.key b/test-data/dnssec-keys/Ktest.+015+56037.key new file mode 100644 index 000000000..38dc516a9 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 15 ml9GKFR/doUuYnnQSPi6uiqvHV4VUGOjD4gmpc5dudc= ;{id = 56037 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+56037.private b/test-data/dnssec-keys/Ktest.+015+56037.private new file mode 100644 index 000000000..52c5034aa --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: Xg9BfVadQ07eubbyryukpn6lYr9BwDHBLSUOpaGLdrc= diff --git a/test-data/dnssec-keys/Ktest.+016+07379.ds b/test-data/dnssec-keys/Ktest.+016+07379.ds new file mode 100644 index 000000000..a1ca41c42 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.ds @@ -0,0 +1 @@ +test. IN DS 7379 16 2 0ec6db96a33efb0c80c9a90e34e80d32506883d0ed245eefd7bfa4d6e13927c9 diff --git a/test-data/dnssec-keys/Ktest.+016+07379.key b/test-data/dnssec-keys/Ktest.+016+07379.key new file mode 100644 index 000000000..a7eade4f9 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 16 9tIYxOhfSE0dS7m9mVxjgMeWJ5arrusV9VSvxYrbJVhucOm6I35HpHi4Eau5P06vpHaMdbp3aFOA ;{id = 7379 (ksk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+07379.private b/test-data/dnssec-keys/Ktest.+016+07379.private new file mode 100644 index 000000000..9d837bcc4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: /hmHKRERsvW761FDTmGlCBJNmy1H8pbsU2LeV1NP2wb0xM286RFIyUMAwRmkFqPVZwwfQluIBXqe diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key deleted file mode 100644 index fc77e0491..000000000 --- a/test-data/dnssec-keys/Ktest.+016+34114.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private deleted file mode 100644 index fca7303dc..000000000 --- a/test-data/dnssec-keys/Ktest.+016+34114.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 16 (ED448) -PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From 31f2bc4b43beda4012c32cd1fc572fa8735f2efb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:03:53 +0200 Subject: [PATCH 110/569] FIX: Parsing of BIND .key files fails if the file has leading comments. --- src/validate.rs | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 0670d0030..c3bb66d16 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -3,6 +3,14 @@ //! **This module is experimental and likely to change significantly.** #![cfg(feature = "unstable-validate")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] +use std::boxed::Box; +use std::vec::Vec; +use std::{error, fmt}; + +use bytes::Bytes; +use octseq::builder::with_infallible; +use octseq::{EmptyBuilder, FromBuilder}; +use ring::{digest, signature}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, DigestAlg, SecAlg}; @@ -14,13 +22,6 @@ use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; use crate::base::Rtype; use crate::rdata::{Dnskey, Ds, Rrsig}; -use bytes::Bytes; -use octseq::builder::with_infallible; -use octseq::{EmptyBuilder, FromBuilder}; -use ring::{digest, signature}; -use std::boxed::Box; -use std::vec::Vec; -use std::{error, fmt}; //----------- Key ------------------------------------------------------------ @@ -240,10 +241,21 @@ impl> Key { Octs: FromBuilder, Octs::Builder: EmptyBuilder + Composer, { - // Ensure there is a single line in the input. - let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); - if !rest.trim().is_empty() { - return Err(ParseDnskeyTextError::Misformatted); + // Skip leading comment lines (BIND uses these to record key timing + // data) + let mut line = dnskey; + while let Some((this_line, rest)) = line.split_once('\n') { + if !this_line.starts_with(';') { + // Ensure there is a single data line in the input. + if !rest.trim().is_empty() { + return Err(ParseDnskeyTextError::Misformatted); + } else { + line = this_line; + break; + } + } else { + line = rest; + } } // Strip away any semicolon from the line. From 6b1c60c03d73fa1d318b54fa62bae327936e8b6b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:10:01 +0200 Subject: [PATCH 111/569] - Follow upstream changes. - FIX: Clear the signing buffer between uses. - Output signed DNSKEY RRs from sign(). --- src/sign/records.rs | 142 ++++++++++++++++++++++++++++++-------------- 1 file changed, 96 insertions(+), 46 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 81f1f2eed..d2d83c9d0 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -2,13 +2,15 @@ use core::convert::From; use core::fmt::Display; +use std::boxed::Box; use std::fmt::Debug; use std::string::String; use std::vec::Vec; use std::{fmt, io, slice}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom}; +use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; +use tracing::{debug, enabled, Level}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Nsec3HashAlg, Rtype, SecAlg}; @@ -20,11 +22,11 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, ZoneRecordData}; use crate::utils::base32; +use crate::validate; use super::ring::{nsec3_hash, Nsec3HashError}; -use crate::validate::Signature; use super::SignRaw; //------------ SortedRecords ------------------------------------------------- @@ -89,24 +91,21 @@ impl SortedRecords { /// AND has the SEP flag set, it will be used as a CSK (i.e. both KSK and /// ZSK). #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - keys: &[(Key, Dnskey)], - ) -> Result< - ( - Vec>>, - Vec>>, - ), - (), - > + keys: &[(SigningKey, validate::Key)], // private, public key pair + ) -> Result>>, ()> where N: ToName + Clone, - D: RecordData + ComposeRecordData, - Key: SignRaw, - Octets: AsRef<[u8]> + Clone, + D: RecordData + ComposeRecordData + From>, + SigningKey: SignRaw, + Octets: AsRef<[u8]> + + Clone + + From> + + octseq::OctetsFrom>, { // Per RFC 8624 section 3.1 "DNSSEC Signing" column guidance. let unsupported_algorithms = [ @@ -116,46 +115,84 @@ impl SortedRecords { SecAlg::ECC_GOST, ]; - let ksks: Vec<&(Key, Dnskey)> = keys + let mut ksks: Vec<&(SigningKey, validate::Key)> = keys .iter() .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) - .filter(|(_, dk)| dk.is_zone_key() && dk.is_secure_entry_point()) + .filter(|(_, dk)| { + dk.is_zone_signing_key() && dk.is_secure_entry_point() + }) .collect(); - let zsks: Vec<&(Key, Dnskey)> = keys + let mut zsks: Vec<&(SigningKey, validate::Key)> = keys .iter() .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) - .filter(|(_, dk)| dk.is_zone_key() && !dk.is_secure_entry_point()) + .filter(|(_, dk)| { + dk.is_zone_signing_key() && !dk.is_secure_entry_point() + }) .collect(); - let mut out_dnskeys: Vec>> = Vec::new(); - let mut out_rrsigs = Vec::new(); - let mut buf = Vec::new(); + // CSK? + if !ksks.is_empty() && zsks.is_empty() { + zsks = ksks.clone(); + } else if ksks.is_empty() && !zsks.is_empty() { + ksks = zsks.clone(); + } - // The owner name of a zone cut if we currently are at or below one. - let mut cut: Option> = None; + if enabled!(Level::DEBUG) { + for key in keys { + debug!( + "Key : {} [supported={}], owner={}, flags={} (SEP={}, ZSK={}))", + key.0.algorithm(), + !unsupported_algorithms.contains(&key.0.algorithm()), + key.1.owner(), + key.1.flags(), + key.1.is_secure_entry_point(), + key.1.is_zone_signing_key(), + ) + } + debug!("# KSKs: {}", ksks.len()); + debug!("# ZSKs: {}", zsks.len()); + } + let mut res: Vec>> = Vec::new(); + let mut buf = Vec::new(); + let mut cut: Option> = None; let mut families = self.families(); // Since the records are ordered, the first family is the apex -- // we can skip everything before that. families.skip_before(apex); - for family in families { - if out_dnskeys.is_empty() { - let apex_ttl = family.records().next().unwrap().ttl(); + let mut families = families.peekable(); + + let apex_ttl = + families.peek().unwrap().records().next().unwrap().ttl(); + + let mut dnskey_rrs: Vec> = + Vec::with_capacity(keys.len()); + + for public_key in keys.iter().map(|(_, public_key)| public_key) { + let dnskey: Dnskey = + Dnskey::convert(public_key.to_dnskey()); + dnskey_rrs.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + dnskey.clone().into(), + )); + + res.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + ZoneRecordData::Dnskey(dnskey), + )); + } - // Add DNSKEYs to the result. - for dnskey in keys.iter().map(|(_, dnskey)| dnskey) { - out_dnskeys.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - dnskey.clone(), - )); - } - } + let dnskeys_iter = RecordsIter::new(dnskey_rrs.as_slice()); + let families_iter = dnskeys_iter.chain(families); + for family in families_iter { // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { @@ -204,34 +241,42 @@ impl SortedRecords { &zsks }; - for (key, dnskey) in keys { + for (private_key, public_key) in keys { let rrsig = ProtoRrsig::new( rrset.rtype(), - key.algorithm(), + private_key.algorithm(), name.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, - dnskey.key_tag(), + public_key.key_tag(), apex.owner().clone(), ); + + buf.clear(); rrsig.compose_canonical(&mut buf).unwrap(); for record in rrset.iter() { record.compose_canonical(&mut buf).unwrap(); } - out_rrsigs.push(Record::new( + let signature = private_key.sign_raw(&buf); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(()); + }; + + let rrsig = + rrsig.into_rrsig(signature).expect("long signature"); + res.push(Record::new( name.owner().clone(), name.class(), rrset.ttl(), - rrsig - .into_rrsig(key.sign_raw(&buf)) - .expect("long signature"), + ZoneRecordData::Rrsig(rrsig), )); } } } - Ok((out_dnskeys, out_rrsigs)) + Ok(res) } pub fn nsecs( @@ -240,7 +285,7 @@ impl SortedRecords { ttl: Ttl, ) -> Vec>> where - N: ToName + Clone, + N: ToName + Clone + PartialEq, D: RecordData, Octets: FromBuilder, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, @@ -300,6 +345,10 @@ impl SortedRecords { let mut bitmap = RtypeBitmap::::builder(); // Assume there’s gonna be an RRSIG. bitmap.add(Rtype::RRSIG).unwrap(); + if family.owner() == &apex_owner { + // Assume there's gonna be a DNSKEY. + bitmap.add(Rtype::DNSKEY).unwrap(); + } bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { bitmap.add(rrset.rtype()).unwrap() @@ -482,6 +531,7 @@ impl SortedRecords { if distance_to_apex == 0 { bitmap.add(Rtype::NSEC3PARAM).unwrap(); + bitmap.add(Rtype::DNSKEY).unwrap(); } // RFC 5155 7.1 step 2: From 7bfc0c31e0d40f554883c517a70936561aa26d28 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:24:43 +0200 Subject: [PATCH 112/569] Remove unnecessary bounds. --- src/sign/records.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index d2d83c9d0..d0e36618a 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -600,8 +600,8 @@ impl SortedRecords { pub fn write(&self, target: &mut W) -> Result<(), io::Error> where - N: fmt::Display + Eq, - D: RecordData + fmt::Display + Clone, + N: fmt::Display, + D: RecordData + fmt::Display, W: io::Write, { for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) From c133f13aefe4b5d558c079e1d7aa638efc657b6d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Oct 2024 15:25:13 +0200 Subject: [PATCH 113/569] Remove commented out code. --- src/sign/records.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index d0e36618a..0d65f15a5 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -838,30 +838,6 @@ impl FamilyName { { Record::new(self.owner.clone(), self.class, ttl, data) } - - // pub fn dnskey>( - // &self, - // ttl: Ttl, - // key: K, - // ) -> Result>, K::Error> - // where - // N: Clone, - // { - // key.dnskey() - // .map(|dnskey| self.clone().into_record(ttl, dnskey.convert())) - // } - - // pub fn ds( - // &self, - // ttl: Ttl, - // key: K, - // ) -> Result>, K::Error> - // where - // N: ToName + Clone, - // { - // key.ds(&self.owner) - // .map(|ds| self.clone().into_record(ttl, ds)) - // } } impl<'a, N: Clone> FamilyName<&'a N> { From 5ba894083d86b93b73fe5286a67d986fa851812a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 114/569] [sign] Define 'KeyPair' and impl key export A private key converted into a 'KeyPair' can be exported in the conventional DNS format. This is an important step in implementing 'ldns-keygen' using 'domain'. It is up to the implementation modules to provide conversion to and from 'KeyPair'; some impls (e.g. for HSMs) won't support it at all. --- src/sign/mod.rs | 243 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d87acca0c..ff36b16b7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -1,10 +1,253 @@ //! DNSSEC signing. //! //! **This module is experimental and likely to change significantly.** +//! +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a +//! DNS record served by a secure-aware name server. But name servers are not +//! usually creating those signatures themselves. Within a DNS zone, it is the +//! zone administrator's responsibility to sign zone records (when the record's +//! time-to-live expires and/or when it changes). Those signatures are stored +//! as regular DNS data and automatically served by name servers. + #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +use core::{fmt, str}; + +use crate::base::iana::SecAlg; + pub mod key; //pub mod openssl; pub mod records; pub mod ring; + +/// A generic keypair. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// cryptographic implementation supports it). +pub enum KeyPair + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> KeyPair { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64(&*s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64(&*s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64(&*s, &mut *w) + } + } + } +} + +impl + AsMut<[u8]>> Drop for KeyPair { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6.2 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (b'0' - 52) as u32 + + (pluses & bcast) * (b'+' - 62) as u32 + + (slashs & bcast) * (b'/' - 63) as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + 3 => {} + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} From 4c103819236e7c452aae85c806eec9e4b0c0152f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:54:14 +0200 Subject: [PATCH 115/569] [sign] Define trait 'Sign' 'Sign' is a more generic version of 'sign::key::SigningKey' that does not provide public key information. It does not try to abstract over all the functionality of a keypair, since that can depend on the underlying cryptographic implementation. --- src/sign/mod.rs | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ff36b16b7..f4bac3c51 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -21,6 +21,42 @@ pub mod key; pub mod records; pub mod ring; +/// Signing DNS records. +/// +/// Implementors of this trait own a private key and sign DNS records for a zone +/// with that key. Signing is a synchronous operation performed on the current +/// thread; this rules out implementations like HSMs, where I/O communication is +/// necessary. +pub trait Sign { + /// An error in constructing a signature. + type Error; + + /// The signature algorithm used. + /// + /// The following algorithms can be used: + /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) + /// - [`SecAlg::DSA`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1`] (insecure, not recommended) + /// - [`SecAlg::DSA_NSEC3_SHA1`] (highly insecure, do not use) + /// - [`SecAlg::RSASHA1_NSEC3_SHA1`] (insecure, not recommended) + /// - [`SecAlg::RSASHA256`] + /// - [`SecAlg::RSASHA512`] (not recommended) + /// - [`SecAlg::ECC_GOST`] (do not use) + /// - [`SecAlg::ECDSAP256SHA256`] + /// - [`SecAlg::ECDSAP384SHA384`] + /// - [`SecAlg::ED25519`] + /// - [`SecAlg::ED448`] + fn algorithm(&self) -> SecAlg; + + /// Compute a signature. + /// + /// A regular signature of the given byte sequence is computed and is turned + /// into the selected buffer type. This provides a lot of flexibility in + /// how buffers are constructed; they may be heap-allocated or have a static + /// size. + fn sign(&self, data: &[u8]) -> Result; +} + /// A generic keypair. /// /// This type cannot be used for computing signatures, as it does not implement From f33f775b30c26ea367ae087d1e28ccd4d72970f4 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 15:42:48 +0200 Subject: [PATCH 116/569] [sign] Implement parsing from the DNS format There are probably lots of bugs in this implementation, I'll add some tests soon. --- src/sign/mod.rs | 273 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 255 insertions(+), 18 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f4bac3c51..691edb5e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -14,6 +14,8 @@ use core::{fmt, str}; +use std::vec::Vec; + use crate::base::iana::SecAlg; pub mod key; @@ -114,25 +116,84 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64(&*s, &mut *w) + base64_encode(&*s, &mut *w) } } } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(())?; + + if !data.trim_ascii().is_empty() { + // There were more fields following. + return Err(()); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf)? != N { + // The private key was of the wrong size. + return Err(()); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(())?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(())?; + + // Parse the algorithm. + let mut words = val.split_ascii_whitespace(); + let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; + let name = words.next().ok_or(())?; + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(()), + } + } } impl + AsMut<[u8]>> Drop for KeyPair { @@ -183,26 +244,87 @@ impl + AsMut<[u8]>> RsaKey { /// /// The output does not include an 'Algorithm' specifier. /// - /// See RFC 5702, section 6.2 for examples of this format. + /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus:\t")?; - base64(self.n.as_ref(), &mut *w)?; + base64_encode(self.n.as_ref(), &mut *w)?; w.write_str("\nPublicExponent:\t")?; - base64(self.e.as_ref(), &mut *w)?; + base64_encode(self.e.as_ref(), &mut *w)?; w.write_str("\nPrivateExponent:\t")?; - base64(self.d.as_ref(), &mut *w)?; + base64_encode(self.d.as_ref(), &mut *w)?; w.write_str("\nPrime1:\t")?; - base64(self.p.as_ref(), &mut *w)?; + base64_encode(self.p.as_ref(), &mut *w)?; w.write_str("\nPrime2:\t")?; - base64(self.q.as_ref(), &mut *w)?; + base64_encode(self.q.as_ref(), &mut *w)?; w.write_str("\nExponent1:\t")?; - base64(self.d_p.as_ref(), &mut *w)?; + base64_encode(self.d_p.as_ref(), &mut *w)?; w.write_str("\nExponent2:\t")?; - base64(self.d_q.as_ref(), &mut *w)?; + base64_encode(self.d_q.as_ref(), &mut *w)?; w.write_str("\nCoefficient:\t")?; - base64(self.q_i.as_ref(), &mut *w)?; + base64_encode(self.q_i.as_ref(), &mut *w)?; w.write_char('\n') } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(()), + }; + + if field.is_some() { + // This field has already been filled. + return Err(()); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(()); + } + } + + 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(), + }) + } } impl + AsMut<[u8]>> Drop for RsaKey { @@ -219,11 +341,26 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair(data: &str) -> Result, ()> { + // Trim any pending newlines. + let data = data.trim_ascii_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = line.split_once(':').ok_or(())?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) +} + /// A utility function to format data as Base64. /// /// This is a simple implementation with the only requirement of being /// constant-time and side-channel resistant. -fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { // Convert a single chunk of bytes into Base64. fn encode(data: [u8; 3]) -> [u8; 4] { let [a, b, c] = data; @@ -254,9 +391,9 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (b'0' - 52) as u32 - + (pluses & bcast) * (b'+' - 62) as u32 - + (slashs & bcast) * (b'/' - 63) as u32; + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; // Convert back into a byte array. chunk.to_be_bytes() @@ -281,9 +418,109 @@ fn base64(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { 0 => return Ok(()), 1 => chunk[2..].fill(b'='), 2 => chunk[3..].fill(b'='), - 3 => {} _ => unreachable!(), } let chunk = str::from_utf8(&chunk).unwrap(); w.write_str(chunk) } + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} From 1d97597871fbc2bea99e5499acc473f0542d3fae Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 16:01:04 +0200 Subject: [PATCH 117/569] [sign] Provide some error information Also fixes 'cargo clippy' issues, particularly with the MSRV. --- src/sign/mod.rs | 96 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 69 insertions(+), 27 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 691edb5e3..d320f0249 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -63,7 +63,7 @@ pub trait Sign { /// /// This type cannot be used for computing signatures, as it does not implement /// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Signer`] (if the underlying +/// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). pub enum KeyPair + AsMut<[u8]>> { /// An RSA/SHA256 keypair. @@ -116,22 +116,22 @@ impl + AsMut<[u8]>> KeyPair { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(&*s, &mut *w) + base64_encode(s, &mut *w) } } } @@ -141,26 +141,28 @@ impl + AsMut<[u8]>> KeyPair { /// - For RSA, see RFC 5702, section 6. /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result + pub fn from_dns(data: &str) -> Result where B: From>, { /// Parse private keys for most algorithms (except RSA). - fn parse_pkey(data: &str) -> Result<[u8; N], ()> { + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { // Extract the 'PrivateKey' field. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; - if !data.trim_ascii().is_empty() { + if !data.trim().is_empty() { // There were more fields following. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf)? != N { + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { // The private key was of the wrong size. - return Err(()); + return Err(DnsFormatError::Misformatted); } Ok(buf) @@ -169,17 +171,24 @@ impl + AsMut<[u8]>> KeyPair { // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(())?; + .ok_or(DnsFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(())?; + .ok_or(DnsFormatError::Misformatted)?; // Parse the algorithm. - let mut words = val.split_ascii_whitespace(); - let code = words.next().ok_or(())?.parse::().map_err(|_| ())?; - let name = words.next().ok_or(())?; + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } match (code, name) { (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), @@ -191,7 +200,7 @@ impl + AsMut<[u8]>> KeyPair { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(()), + _ => Err(DnsFormatError::UnsupportedAlgorithm), } } } @@ -268,7 +277,7 @@ impl + AsMut<[u8]>> RsaKey { /// Parse a key from the conventional DNS format. /// /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result + pub fn from_dns(mut data: &str) -> Result where B: From>, { @@ -291,16 +300,17 @@ impl + AsMut<[u8]>> RsaKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(()), + _ => return Err(DnsFormatError::Misformatted), }; if field.is_some() { // This field has already been filled. - return Err(()); + return Err(DnsFormatError::Misformatted); } let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer)?; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; buffer.truncate(size); *field = Some(buffer.into()); @@ -310,7 +320,7 @@ impl + AsMut<[u8]>> RsaKey { for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(()); + return Err(DnsFormatError::Misformatted); } } @@ -342,18 +352,23 @@ impl + AsMut<[u8]>> Drop for RsaKey { } /// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair(data: &str) -> Result, ()> { +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + // Trim any pending newlines. - let data = data.trim_ascii_start(); + let data = data.trim_start(); // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); // Split the line by a colon. - let (key, val) = line.split_once(':').ok_or(())?; + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim_ascii(), val.trim_ascii(), rest))) + Ok(Some((key.trim(), val.trim(), rest))) } /// A utility function to format data as Base64. @@ -388,6 +403,7 @@ fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { let slashs = pluses >> 7; // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk + (uppers & bcast) * (b'A' - 0) as u32 + (lowers & bcast) * (b'a' - 26) as u32 @@ -461,6 +477,7 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { } // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] let chunk = chunk - (uppers & bcast) * (b'A' - 0) as u32 - (lowers & bcast) * (b'a' - 26) as u32 @@ -524,3 +541,28 @@ fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { Ok(index) } + +/// An error in loading a [`KeyPair`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} From fa306e97a9727b8a49ddb37e04987c11e70870a9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 4 Oct 2024 13:08:07 +0200 Subject: [PATCH 118/569] [sign] Move 'KeyPair' to 'generic::SecretKey' I'm going to add a corresponding 'PublicKey' type, at which point it becomes important to differentiate from the generic representations and actual cryptographic implementations. --- src/sign/generic.rs | 513 ++++++++++++++++++++++++++++++++++++++++++++ src/sign/mod.rs | 513 +------------------------------------------- 2 files changed, 514 insertions(+), 512 deletions(-) create mode 100644 src/sign/generic.rs diff --git a/src/sign/generic.rs b/src/sign/generic.rs new file mode 100644 index 000000000..420d84530 --- /dev/null +++ b/src/sign/generic.rs @@ -0,0 +1,513 @@ +use core::{fmt, str}; + +use std::vec::Vec; + +use crate::base::iana::SecAlg; + +/// A generic secret key. +/// +/// This type cannot be used for computing signatures, as it does not implement +/// any cryptographic primitives. Instead, it is a generic representation that +/// can be imported/exported or converted into a [`Sign`] (if the underlying +/// cryptographic implementation supports it). +pub enum SecretKey + AsMut<[u8]>> { + /// An RSA/SHA256 keypair. + RsaSha256(RsaKey), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256([u8; 32]), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384([u8; 48]), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519([u8; 32]), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448([u8; 57]), +} + +impl + AsMut<[u8]>> SecretKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Serialize this key in the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + match self { + Self::RsaSha256(k) => { + w.write_str("Algorithm: 8 (RSASHA256)\n")?; + k.into_dns(w) + } + + Self::EcdsaP256Sha256(s) => { + w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + base64_encode(s, &mut *w) + } + + Self::EcdsaP384Sha384(s) => { + w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed25519(s) => { + w.write_str("Algorithm: 15 (ED25519)\n")?; + base64_encode(s, &mut *w) + } + + Self::Ed448(s) => { + w.write_str("Algorithm: 16 (ED448)\n")?; + base64_encode(s, &mut *w) + } + } + } + + /// Parse a key from the conventional DNS format. + /// + /// - For RSA, see RFC 5702, section 6. + /// - For ECDSA, see RFC 6605, section 6. + /// - For EdDSA, see RFC 8080, section 6. + pub fn from_dns(data: &str) -> Result + where + B: From>, + { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + data: &str, + ) -> Result<[u8; N], DnsFormatError> { + // Extract the 'PrivateKey' field. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "PrivateKey") + .ok_or(DnsFormatError::Misformatted)?; + + if !data.trim().is_empty() { + // There were more fields following. + return Err(DnsFormatError::Misformatted); + } + + let mut buf = [0u8; N]; + if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { + // The private key was of the wrong size. + return Err(DnsFormatError::Misformatted); + } + + Ok(buf) + } + + // The first line should specify the key format. + let (_, _, data) = parse_dns_pair(data)? + .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) + .ok_or(DnsFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_dns_pair(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(DnsFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .ok_or(DnsFormatError::Misformatted)? + .parse::() + .map_err(|_| DnsFormatError::Misformatted)?; + let name = words.next().ok_or(DnsFormatError::Misformatted)?; + if words.next().is_some() { + return Err(DnsFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(DnsFormatError::UnsupportedAlgorithm), + } + } +} + +impl + AsMut<[u8]>> Drop for SecretKey { + 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), + } + } +} + +/// An RSA private key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaKey + AsMut<[u8]>> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, + + /// The private exponent. + pub d: B, + + /// The first prime factor of `d`. + pub p: B, + + /// The second prime factor of `d`. + pub q: B, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: B, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: B, + + /// The inverse of the second prime factor modulo the first. + pub q_i: B, +} + +impl + AsMut<[u8]>> RsaKey { + /// Serialize this key in the conventional DNS format. + /// + /// The output does not include an 'Algorithm' specifier. + /// + /// See RFC 5702, section 6 for examples of this format. + pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Modulus:\t")?; + base64_encode(self.n.as_ref(), &mut *w)?; + w.write_str("\nPublicExponent:\t")?; + base64_encode(self.e.as_ref(), &mut *w)?; + w.write_str("\nPrivateExponent:\t")?; + base64_encode(self.d.as_ref(), &mut *w)?; + w.write_str("\nPrime1:\t")?; + base64_encode(self.p.as_ref(), &mut *w)?; + w.write_str("\nPrime2:\t")?; + base64_encode(self.q.as_ref(), &mut *w)?; + w.write_str("\nExponent1:\t")?; + base64_encode(self.d_p.as_ref(), &mut *w)?; + w.write_str("\nExponent2:\t")?; + base64_encode(self.d_q.as_ref(), &mut *w)?; + w.write_str("\nCoefficient:\t")?; + base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_char('\n') + } + + /// Parse a key from the conventional DNS format. + /// + /// See RFC 5702, section 6. + pub fn from_dns(mut data: &str) -> Result + where + B: From>, + { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_dns_pair(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => return Err(DnsFormatError::Misformatted), + }; + + if field.is_some() { + // This field has already been filled. + return Err(DnsFormatError::Misformatted); + } + + let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; + let size = base64_decode(val.as_bytes(), &mut buffer) + .map_err(|_| DnsFormatError::Misformatted)?; + buffer.truncate(size); + + *field = Some(buffer.into()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(DnsFormatError::Misformatted); + } + } + + 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(), + }) + } +} + +impl + AsMut<[u8]>> Drop for RsaKey { + fn drop(&mut self) { + // Zero the bytes for each field. + self.n.as_mut().fill(0u8); + self.e.as_mut().fill(0u8); + self.d.as_mut().fill(0u8); + self.p.as_mut().fill(0u8); + self.q.as_mut().fill(0u8); + self.d_p.as_mut().fill(0u8); + self.d_q.as_mut().fill(0u8); + self.q_i.as_mut().fill(0u8); + } +} + +/// Extract the next key-value pair in a DNS private key file. +fn parse_dns_pair( + data: &str, +) -> Result, DnsFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +/// A utility function to format data as Base64. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { + // Convert a single chunk of bytes into Base64. + fn encode(data: [u8; 3]) -> [u8; 4] { + let [a, b, c] = data; + + // Expand the chunk using integer operations; it's pretty fast. + let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + + // Classify each output byte as A-Z, a-z, 0-9, + or /. + let bcast = 0x01010101u32; + let uppers = chunk + (128 - 26) * bcast; + let lowers = chunk + (128 - 52) * bcast; + let digits = chunk + (128 - 62) * bcast; + let pluses = chunk + (128 - 63) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = !uppers >> 7; + let lowers = (uppers & !lowers) >> 7; + let digits = (lowers & !digits) >> 7; + let pluses = (digits & !pluses) >> 7; + let slashs = pluses >> 7; + + // Add the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + + (uppers & bcast) * (b'A' - 0) as u32 + + (lowers & bcast) * (b'a' - 26) as u32 + - (digits & bcast) * (52 - b'0') as u32 + - (pluses & bcast) * (62 - b'+') as u32 + - (slashs & bcast) * (63 - b'/') as u32; + + // Convert back into a byte array. + chunk.to_be_bytes() + } + + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + let mut chunks = data.chunks_exact(3); + + // Iterate over the whole chunks in the input. + for chunk in &mut chunks { + let chunk = <[u8; 3]>::try_from(chunk).unwrap(); + let chunk = encode(chunk); + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk)?; + } + + // Encode the final chunk and handle padding. + let mut chunk = [0u8; 3]; + chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); + let mut chunk = encode(chunk); + match chunks.remainder().len() { + 0 => return Ok(()), + 1 => chunk[2..].fill(b'='), + 2 => chunk[3..].fill(b'='), + _ => unreachable!(), + } + let chunk = str::from_utf8(&chunk).unwrap(); + w.write_str(chunk) +} + +/// A utility function to decode Base64 data. +/// +/// This is a simple implementation with the only requirement of being +/// constant-time and side-channel resistant. +/// +/// Incorrect padding or garbage bytes will result in an error. +fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { + /// Decode a single chunk of bytes from Base64. + fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { + let chunk = u32::from_be_bytes(data); + let bcast = 0x01010101u32; + + // Mask out non-ASCII bytes early. + if chunk & 0x80808080 != 0 { + return Err(()); + } + + // Classify each byte as A-Z, a-z, 0-9, + or /. + let uppers = chunk + (128 - b'A' as u32) * bcast; + let lowers = chunk + (128 - b'a' as u32) * bcast; + let digits = chunk + (128 - b'0' as u32) * bcast; + let pluses = chunk + (128 - b'+' as u32) * bcast; + let slashs = chunk + (128 - b'/' as u32) * bcast; + + // For each byte, the LSB is set if it is in the class. + let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; + let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; + let digits = (digits ^ (digits - bcast * 10)) >> 7; + let pluses = (pluses ^ (pluses - bcast)) >> 7; + let slashs = (slashs ^ (slashs - bcast)) >> 7; + + // Check if an input was in none of the classes. + if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { + return Err(()); + } + + // Subtract the corresponding offset for each class. + #[allow(clippy::identity_op)] + let chunk = chunk + - (uppers & bcast) * (b'A' - 0) as u32 + - (lowers & bcast) * (b'a' - 26) as u32 + + (digits & bcast) * (52 - b'0') as u32 + + (pluses & bcast) * (62 - b'+') as u32 + + (slashs & bcast) * (63 - b'/') as u32; + + // Compress the chunk using integer operations. + // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) + let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); + // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) + let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); + // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 + let [_, a, b, c] = chunk.to_be_bytes(); + + Ok([a, b, c]) + } + + // Uneven inputs are not allowed; use padding. + if encoded.len() % 4 != 0 { + return Err(()); + } + + // The index into the decoded buffer. + let mut index = 0usize; + + // Iterate over the whole chunks in the input. + // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. + for chunk in encoded.chunks_exact(4) { + let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); + + // Check for padding. + let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); + if chunk[ppos..].iter().any(|&b| b != b'=') { + // A padding byte was followed by a non-padding byte. + return Err(()); + } + + // Mask out the padding for the main decoder. + chunk[ppos..].fill(b'A'); + + // Determine how many output bytes there are. + let amount = match ppos { + 0 | 1 => return Err(()), + 2 => 1, + 3 => 2, + 4 => 3, + _ => unreachable!(), + }; + + if index + amount >= decoded.len() { + // The input was too long, or the output was too short. + return Err(()); + } + + // Decode the chunk and write the unpadded amount. + let chunk = decode(chunk)?; + decoded[index..][..amount].copy_from_slice(&chunk[..amount]); + index += amount; + } + + Ok(index) +} + +/// An error in loading a [`SecretKey`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum DnsFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +impl fmt::Display for DnsFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl std::error::Error for DnsFormatError {} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index d320f0249..a649f7ab2 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -12,12 +12,9 @@ #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use core::{fmt, str}; - -use std::vec::Vec; - use crate::base::iana::SecAlg; +pub mod generic; pub mod key; //pub mod openssl; pub mod records; @@ -58,511 +55,3 @@ pub trait Sign { /// size. fn sign(&self, data: &[u8]) -> Result; } - -/// A generic keypair. -/// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -pub enum KeyPair + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), - - /// An ECDSA P-256/SHA-256 keypair. - /// - /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), - - /// An ECDSA P-384/SHA-384 keypair. - /// - /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), - - /// An Ed25519 keypair. - /// - /// The private key is a single 32-byte string. - Ed25519([u8; 32]), - - /// An Ed448 keypair. - /// - /// The private key is a single 57-byte string. - Ed448([u8; 57]), -} - -impl + AsMut<[u8]>> KeyPair { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } - - /// Serialize this key in the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - match self { - Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; - k.into_dns(w) - } - - Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) - } - - Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) - } - - Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) - } - } - } - - /// Parse a key from the conventional DNS format. - /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { - /// Parse private keys for most algorithms (except RSA). - fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } - - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } - - Ok(buf) - } - - // The first line should specify the key format. - let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; - - // The second line should specify the algorithm. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; - - // Parse the algorithm. - let mut words = val.split_whitespace(); - let code = words - .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; - if words.next().is_some() { - return Err(DnsFormatError::Misformatted); - } - - match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), - (13, "(ECDSAP256SHA256)") => { - parse_pkey(data).map(Self::EcdsaP256Sha256) - } - (14, "(ECDSAP384SHA384)") => { - parse_pkey(data).map(Self::EcdsaP384Sha384) - } - (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), - (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), - } - } -} - -impl + AsMut<[u8]>> Drop for KeyPair { - 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), - } - } -} - -/// An RSA private key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, - - /// The private exponent. - pub d: B, - - /// The first prime factor of `d`. - pub p: B, - - /// The second prime factor of `d`. - pub q: B, - - /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, - - /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, - - /// The inverse of the second prime factor modulo the first. - pub q_i: B, -} - -impl + AsMut<[u8]>> RsaKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. - /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; - w.write_char('\n') - } - - /// Parse a key from the conventional DNS format. - /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { - let mut n = None; - let mut e = None; - let mut d = None; - let mut p = None; - let mut q = None; - let mut d_p = None; - let mut d_q = None; - let mut q_i = None; - - while let Some((key, val, rest)) = parse_dns_pair(data)? { - let field = match key { - "Modulus" => &mut n, - "PublicExponent" => &mut e, - "PrivateExponent" => &mut d, - "Prime1" => &mut p, - "Prime2" => &mut q, - "Exponent1" => &mut d_p, - "Exponent2" => &mut d_q, - "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), - }; - - if field.is_some() { - // This field has already been filled. - return Err(DnsFormatError::Misformatted); - } - - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) - .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); - - *field = Some(buffer.into()); - data = rest; - } - - for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { - if field.is_none() { - // A field was missing. - return Err(DnsFormatError::Misformatted); - } - } - - 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(), - }) - } -} - -impl + AsMut<[u8]>> Drop for RsaKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// Extract the next key-value pair in a DNS private key file. -fn parse_dns_pair( - data: &str, -) -> Result, DnsFormatError> { - // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. - - // Trim any pending newlines. - let data = data.trim_start(); - - // Get the first line (NOTE: CR LF is handled later). - let (line, rest) = data.split_once('\n').unwrap_or((data, "")); - - // Split the line by a colon. - let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; - - // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim(), val.trim(), rest))) -} - -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - -/// An error in loading a [`KeyPair`] from the conventional DNS format. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { - /// The key file uses an unsupported version of the format. - UnsupportedFormat, - - /// The key file did not follow the DNS format correctly. - Misformatted, - - /// The key file used an unsupported algorithm. - UnsupportedAlgorithm, -} - -impl fmt::Display for DnsFormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedFormat => "unsupported format", - Self::Misformatted => "misformatted key file", - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -impl std::error::Error for DnsFormatError {} From 56dec850bde7783a1f9155f49c19feb66b57e589 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 15:29:45 +0200 Subject: [PATCH 119/569] [sign/generic] Add 'PublicKey' --- src/sign/generic.rs | 135 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 420d84530..7c9ffbea4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,8 +1,9 @@ -use core::{fmt, str}; +use core::{fmt, mem, str}; use std::vec::Vec; use crate::base::iana::SecAlg; +use crate::rdata::Dnskey; /// A generic secret key. /// @@ -12,7 +13,7 @@ use crate::base::iana::SecAlg; /// cryptographic implementation supports it). pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. - RsaSha256(RsaKey), + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// @@ -136,7 +137,9 @@ impl + AsMut<[u8]>> SecretKey { } match (code, name) { - (8, "(RSASHA256)") => RsaKey::from_dns(data).map(Self::RsaSha256), + (8, "(RSASHA256)") => { + RsaSecretKey::from_dns(data).map(Self::RsaSha256) + } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) } @@ -163,11 +166,11 @@ impl + AsMut<[u8]>> Drop for SecretKey { } } -/// An RSA private key. +/// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaKey + AsMut<[u8]>> { +pub struct RsaSecretKey + AsMut<[u8]>> { /// The public modulus. pub n: B, @@ -193,7 +196,7 @@ pub struct RsaKey + AsMut<[u8]>> { pub q_i: B, } -impl + AsMut<[u8]>> RsaKey { +impl + AsMut<[u8]>> RsaSecretKey { /// Serialize this key in the conventional DNS format. /// /// The output does not include an 'Algorithm' specifier. @@ -282,7 +285,7 @@ impl + AsMut<[u8]>> RsaKey { } } -impl + AsMut<[u8]>> Drop for RsaKey { +impl + AsMut<[u8]>> Drop for RsaSecretKey { fn drop(&mut self) { // Zero the bytes for each field. self.n.as_mut().fill(0u8); @@ -296,6 +299,124 @@ impl + AsMut<[u8]>> Drop for RsaKey { } } +/// A generic public key. +pub enum PublicKey> { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + // TODO: RSA/SHA-1 with NSEC3/SHA-1? + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256([u8; 65]), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384([u8; 97]), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519([u8; 32]), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448([u8; 57]), +} + +impl> PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } + + /// Construct a DNSKEY record with the given flags. + pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + where + Octs: From> + AsRef<[u8]>, + { + let protocol = 3u8; + let algorithm = self.algorithm(); + let public_key = match self { + Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { + let (n, e) = (k.n.as_ref(), k.e.as_ref()); + let e_len_len = if e.len() < 256 { 1 } else { 3 }; + let len = e_len_len + e.len() + n.len(); + let mut buf = Vec::with_capacity(len); + if let Ok(e_len) = u8::try_from(e.len()) { + buf.push(e_len); + } else { + // RFC 3110 is not explicit about the endianness of this, + // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network + // byte order, which I suppose makes sense. + let e_len = u16::try_from(e.len()).unwrap(); + buf.extend_from_slice(&e_len.to_be_bytes()); + } + buf.extend_from_slice(e); + buf.extend_from_slice(n); + buf + } + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].to_vec(), + Self::EcdsaP384Sha384(k) => k[1..].to_vec(), + + Self::Ed25519(k) => k.to_vec(), + Self::Ed448(k) => k.to_vec(), + }; + + Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +pub struct RsaPublicKey> { + /// The public modulus. + pub n: B, + + /// The public exponent. + pub e: B, +} + +impl From> for RsaPublicKey +where + B: AsRef<[u8]> + AsMut<[u8]> + Default, +{ + fn from(mut value: RsaSecretKey) -> Self { + Self { + n: mem::take(&mut value.n), + e: mem::take(&mut value.e), + } + } +} + /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, From 5f8e28f5a3db84983e613a704526160fecadc7f5 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 7 Oct 2024 16:41:57 +0200 Subject: [PATCH 120/569] [sign] Rewrite the 'ring' module to use the 'Sign' trait Key generation, for now, will only be provided by the OpenSSL backend (coming soon). However, generic keys (for RSA/SHA-256 or Ed25519) can be imported into the Ring backend and used freely. --- src/sign/generic.rs | 4 +- src/sign/ring.rs | 180 ++++++++++++++++---------------------------- 2 files changed, 68 insertions(+), 116 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 7c9ffbea4..f963a8def 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -11,6 +11,8 @@ use crate::rdata::Dnskey; /// any cryptographic primitives. Instead, it is a generic representation that /// can be imported/exported or converted into a [`Sign`] (if the underlying /// cryptographic implementation supports it). +/// +/// [`Sign`]: super::Sign pub enum SecretKey + AsMut<[u8]>> { /// An RSA/SHA256 keypair. RsaSha256(RsaSecretKey), @@ -355,7 +357,7 @@ impl> PublicKey { } /// Construct a DNSKEY record with the given flags. - pub fn into_dns<'a, Octs>(self, flags: u16) -> Dnskey + pub fn into_dns(self, flags: u16) -> Dnskey where Octs: From> + AsRef<[u8]>, { diff --git a/src/sign/ring.rs b/src/sign/ring.rs index bf4614f2b..75660dfd6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,140 +1,90 @@ -//! Key and Signer using ring. +//! DNSSEC signing using `ring`. + #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] -use super::key::SigningKey; -use crate::base::iana::{DigestAlg, SecAlg}; -use crate::base::name::ToName; -use crate::base::rdata::ComposeRecordData; -use crate::rdata::{Dnskey, Ds}; -#[cfg(feature = "bytes")] -use bytes::Bytes; -use octseq::builder::infallible; -use ring::digest; -use ring::error::Unspecified; -use ring::rand::SecureRandom; -use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair, RsaEncoding, RsaKeyPair, - Signature as RingSignature, ECDSA_P256_SHA256_FIXED_SIGNING, -}; use std::vec::Vec; -pub struct Key<'a> { - dnskey: Dnskey>, - key: RingKey, - rng: &'a dyn SecureRandom, -} - -#[allow(dead_code, clippy::large_enum_variant)] -enum RingKey { - Ecdsa(EcdsaKeyPair), - Ed25519(Ed25519KeyPair), - Rsa(RsaKeyPair, &'static dyn RsaEncoding), -} - -impl<'a> Key<'a> { - pub fn throwaway_13( - flags: u16, - rng: &'a dyn SecureRandom, - ) -> Result { - let pkcs8 = EcdsaKeyPair::generate_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - rng, - )?; - let keypair = EcdsaKeyPair::from_pkcs8( - &ECDSA_P256_SHA256_FIXED_SIGNING, - pkcs8.as_ref(), - rng, - )?; - let public_key = keypair.public_key().as_ref()[1..].into(); - Ok(Key { - dnskey: Dnskey::new( - flags, - 3, - SecAlg::ECDSAP256SHA256, - public_key, - ) - .expect("long key"), - key: RingKey::Ecdsa(keypair), - rng, - }) - } -} +use crate::base::iana::SecAlg; -impl<'a> SigningKey for Key<'a> { - type Octets = Vec; - type Signature = Signature; - type Error = Unspecified; +use super::generic; - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } +/// A key pair backed by `ring`. +pub enum KeyPair<'a> { + /// An RSA/SHA256 keypair. + RsaSha256 { + key: ring::signature::RsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - infallible(owner.compose_canonical(&mut buf)); - infallible(self.dnskey.compose_canonical_rdata(&mut buf)); - let digest = - Vec::from(digest::digest(&digest::SHA256, &buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::SHA256, - digest, - ) - .expect("long digest")) - } + /// An Ed25519 keypair. + Ed25519(ring::signature::Ed25519KeyPair), +} - fn sign(&self, msg: &[u8]) -> Result { - match self.key { - RingKey::Ecdsa(ref key) => { - key.sign(self.rng, msg).map(Signature::sig) +impl<'a> KeyPair<'a> { + /// Use a generic keypair with `ring`. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + rng: &'a dyn ring::rand::SecureRandom, + ) -> Result { + match &key { + generic::SecretKey::RsaSha256(k) => { + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: k.n.as_ref(), + e: k.e.as_ref(), + }, + d: k.d.as_ref(), + p: k.p.as_ref(), + q: k.q.as_ref(), + dP: k.d_p.as_ref(), + dQ: k.d_q.as_ref(), + qInv: k.q_i.as_ref(), + }; + ring::signature::RsaKeyPair::from_components(&components) + .map_err(|_| ImportError::InvalidKey) + .map(|key| Self::RsaSha256 { key, rng }) } - RingKey::Ed25519(ref key) => Ok(Signature::sig(key.sign(msg))), - RingKey::Rsa(ref key, encoding) => { - let mut sig = vec![0; key.public().modulus_len()]; - key.sign(encoding, self.rng, msg, &mut sig)?; - Ok(Signature::vec(sig)) + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + let k = k.as_ref(); + ring::signature::Ed25519KeyPair::from_seed_unchecked(k) + .map_err(|_| ImportError::InvalidKey) + .map(Self::Ed25519) } + _ => Err(ImportError::UnsupportedAlgorithm), } } } -pub struct Signature(SignatureInner); +/// An error in importing a key into `ring`. +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, -enum SignatureInner { - Sig(RingSignature), - Vec(Vec), + /// The provided keypair was invalid. + InvalidKey, } -impl Signature { - fn sig(sig: RingSignature) -> Signature { - Signature(SignatureInner::Sig(sig)) - } - - fn vec(vec: Vec) -> Signature { - Signature(SignatureInner::Vec(vec)) - } -} +impl<'a> super::Sign> for KeyPair<'a> { + type Error = ring::error::Unspecified; -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self.0 { - SignatureInner::Sig(ref sig) => sig.as_ref(), - SignatureInner::Vec(ref vec) => vec.as_slice(), + fn algorithm(&self) -> SecAlg { + match self { + KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, + KeyPair::Ed25519(_) => SecAlg::ED25519, } } -} -#[cfg(feature = "bytes")] -impl From for Bytes { - fn from(sig: Signature) -> Self { - match sig.0 { - SignatureInner::Sig(sig) => Bytes::copy_from_slice(sig.as_ref()), - SignatureInner::Vec(sig) => Bytes::from(sig), + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + match self { + KeyPair::RsaSha256 { key, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, *rng, data, &mut buf)?; + Ok(buf) + } + KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From 46b67e9650ea574a411437e71eb0123256915f9d Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:36:23 +0200 Subject: [PATCH 121/569] Implement DNSSEC signing with OpenSSL The OpenSSL backend supports import from and export to generic secret keys, making the formatting and parsing machinery for them usable. The next step is to implement generation of keys. --- Cargo.lock | 66 +++++++++++++++++ Cargo.toml | 2 + src/sign/mod.rs | 2 +- src/sign/openssl.rs | 167 ++++++++++++++++++++++++++++++++------------ src/sign/ring.rs | 16 ++--- 5 files changed, 200 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 58dfde030..eaf9191fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -232,6 +232,7 @@ dependencies = [ "mock_instant", "moka", "octseq", + "openssl", "parking_lot", "proc-macro2", "rand", @@ -284,6 +285,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "futures" version = "0.3.31" @@ -636,6 +652,44 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -703,6 +757,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1336,6 +1396,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/Cargo.toml b/Cargo.toml index a9b938811..d09e5f532 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,6 +33,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } +openssl = { version = "0.10", optional = true } 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 } @@ -49,6 +50,7 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] +openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] diff --git a/src/sign/mod.rs b/src/sign/mod.rs index a649f7ab2..b1db46c26 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -16,7 +16,7 @@ use crate::base::iana::SecAlg; pub mod generic; pub mod key; -//pub mod openssl; +pub mod openssl; pub mod records; pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c49512b73..e62c9dcbb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,58 +1,137 @@ //! Key and Signer using OpenSSL. + #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] +use core::fmt; use std::vec::Vec; -use openssl::error::ErrorStack; -use openssl::hash::MessageDigest; -use openssl::pkey::{PKey, Private}; -use openssl::sha::sha256; -use openssl::sign::Signer as OpenSslSigner; -use unwrap::unwrap; -use crate::base::iana::DigestAlg; -use crate::base::name::ToDname; -use crate::base::octets::Compose; -use crate::rdata::{Ds, Dnskey}; -use super::key::SigningKey; - - -pub struct Key { - dnskey: Dnskey>, - key: PKey, - digest: MessageDigest, + +use openssl::{ + bn::BigNum, + pkey::{self, PKey, Private}, +}; + +use crate::base::iana::SecAlg; + +use super::generic; + +/// A key pair backed by OpenSSL. +pub struct SecretKey { + /// The algorithm used by the key. + algorithm: SecAlg, + + /// The private key. + pkey: PKey, } -impl SigningKey for Key { - type Octets = Vec; - type Signature = Vec; - type Error = ErrorStack; +impl SecretKey { + /// Use a generic secret key with OpenSSL. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn import + AsMut<[u8]>>( + key: generic::SecretKey, + ) -> Result { + fn num(slice: &[u8]) -> BigNum { + let mut v = BigNum::new_secure().unwrap(); + v.copy_from_slice(slice).unwrap(); + v + } - fn dnskey(&self) -> Result, Self::Error> { - Ok(self.dnskey.clone()) - } + let pkey = match &key { + generic::SecretKey::RsaSha256(k) => { + let n = BigNum::from_slice(k.n.as_ref()).unwrap(); + let e = BigNum::from_slice(k.e.as_ref()).unwrap(); + let d = num(k.d.as_ref()); + let p = num(k.p.as_ref()); + let q = num(k.q.as_ref()); + let d_p = num(k.d_p.as_ref()); + let d_q = num(k.d_q.as_ref()); + let q_i = num(k.q_i.as_ref()); - fn ds( - &self, - owner: N - ) -> Result, Self::Error> { - let mut buf = Vec::new(); - unwrap!(owner.compose_canonical(&mut buf)); - unwrap!(self.dnskey.compose_canonical(&mut buf)); - let digest = Vec::from(sha256(&buf).as_ref()); - Ok(Ds::new( - self.key_tag()?, - self.dnskey.algorithm(), - DigestAlg::Sha256, - digest, - )) + // NOTE: The 'openssl' crate doesn't seem to expose + // 'EVP_PKEY_fromdata', which could be used to replace the + // deprecated methods called here. + + openssl::rsa::Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + ) + .and_then(PKey::from_rsa) + .unwrap() + } + // TODO: Support ECDSA. + generic::SecretKey::Ed25519(k) => { + PKey::private_key_from_raw_bytes( + k.as_ref(), + pkey::Id::ED25519, + ) + .unwrap() + } + generic::SecretKey::Ed448(k) => { + PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) + .unwrap() + } + _ => return Err(ImportError::UnsupportedAlgorithm), + }; + + Ok(Self { + algorithm: key.algorithm(), + pkey, + }) } - fn sign(&self, data: &[u8]) -> Result { - let mut signer = OpenSslSigner::new( - self.digest, &self.key - )?; - signer.update(data)?; - signer.sign_to_vec() + /// Export this key into a generic secret key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export(self) -> generic::SecretKey + where + B: AsRef<[u8]> + AsMut<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::SecretKey::RsaSha256(generic::RsaSecretKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + generic::SecretKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } } } +/// An error in importing a key into OpenSSL. +#[derive(Clone, Debug)] +pub enum ImportError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided secret key was invalid. + InvalidKey, +} + +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 75660dfd6..872f8dadb 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,8 +10,8 @@ use crate::base::iana::SecAlg; use super::generic; /// A key pair backed by `ring`. -pub enum KeyPair<'a> { - /// An RSA/SHA256 keypair. +pub enum SecretKey<'a> { + /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, rng: &'a dyn ring::rand::SecureRandom, @@ -21,7 +21,7 @@ pub enum KeyPair<'a> { Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> KeyPair<'a> { +impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn import + AsMut<[u8]>>( key: generic::SecretKey, @@ -66,25 +66,25 @@ pub enum ImportError { InvalidKey, } -impl<'a> super::Sign> for KeyPair<'a> { +impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; fn algorithm(&self) -> SecAlg { match self { - KeyPair::RsaSha256 { .. } => SecAlg::RSASHA256, - KeyPair::Ed25519(_) => SecAlg::ED25519, + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::Ed25519(_) => SecAlg::ED25519, } } fn sign(&self, data: &[u8]) -> Result, Self::Error> { match self { - KeyPair::RsaSha256 { key, rng } => { + Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; key.sign(pad, *rng, data, &mut buf)?; Ok(buf) } - KeyPair::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), + Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } From 2451e1beee12f6ebccb65280e743f7a7eda40088 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 10:57:33 +0200 Subject: [PATCH 122/569] [sign/openssl] Implement key generation --- src/sign/openssl.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e62c9dcbb..9d208737c 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -117,6 +117,27 @@ impl SecretKey { } } +/// Generate a new secret key for the given algorithm. +/// +/// If the algorithm is not supported, [`None`] is returned. +/// +/// # Panics +/// +/// Panics if OpenSSL fails or if memory could not be allocated. +pub fn generate(algorithm: SecAlg) -> Option { + let pkey = match algorithm { + // We generate 3072-bit keys for an estimated 128 bits of security. + SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) + .and_then(PKey::from_rsa) + .unwrap(), + SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), + SecAlg::ED448 => PKey::generate_ed448().unwrap(), + _ => return None, + }; + + Some(SecretKey { algorithm, pkey }) +} + /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] pub enum ImportError { @@ -135,3 +156,5 @@ impl fmt::Display for ImportError { }) } } + +impl std::error::Error for ImportError {} From 159a94a60725452c448460d4bf5a039203a9a1ee Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:08:06 +0200 Subject: [PATCH 123/569] [sign/openssl] Test key generation and import/export --- src/sign/openssl.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9d208737c..13c1f7808 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -86,7 +86,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(self) -> generic::SecretKey + pub fn export(&self) -> generic::SecretKey where B: AsRef<[u8]> + AsMut<[u8]> + From>, { @@ -158,3 +158,30 @@ impl fmt::Display for ImportError { } impl std::error::Error for ImportError {} + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{base::iana::SecAlg, sign::generic}; + + const ALGORITHMS: &[SecAlg] = + &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + + #[test] + fn generate_all() { + for &algorithm in ALGORITHMS { + let _ = super::generate(algorithm).unwrap(); + } + } + + #[test] + fn export_and_import() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let exp: generic::SecretKey> = key.export(); + let imp = super::SecretKey::import(exp).unwrap(); + assert!(key.pkey.public_eq(&imp.pkey)); + } + } +} From 4fb608499c9dfaeba382d1dc48e46bd2a6b9b793 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:39:45 +0200 Subject: [PATCH 124/569] [sign/openssl] Add support for ECDSA --- src/sign/openssl.rs | 62 ++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 13c1f7808..d35f45850 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -60,7 +60,32 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - // TODO: Support ECDSA. + generic::SecretKey::EcdsaP256Sha256(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } + generic::SecretKey::EcdsaP384Sha384(k) => { + // Calculate the public key manually. + let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); + let group = openssl::nid::Nid::SECP384R1; + let group = + openssl::ec::EcGroup::from_curve_name(group).unwrap(); + let mut p = openssl::ec::EcPoint::new(&group).unwrap(); + let n = num(&*k); + p.mul_generator(&group, &n, &ctx).unwrap(); + openssl::ec::EcKey::from_private_components(&group, &n, &p) + .and_then(PKey::from_ec_key) + .unwrap() + } generic::SecretKey::Ed25519(k) => { PKey::private_key_from_raw_bytes( k.as_ref(), @@ -72,7 +97,6 @@ impl SecretKey { PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) .unwrap() } - _ => return Err(ImportError::UnsupportedAlgorithm), }; Ok(Self { @@ -90,6 +114,7 @@ impl SecretKey { where B: AsRef<[u8]> + AsMut<[u8]> + From>, { + // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); @@ -104,6 +129,16 @@ impl SecretKey { q_i: key.iqmp().unwrap().to_vec().into(), }) } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec(); + generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) + } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); generic::SecretKey::Ed25519(key.try_into().unwrap()) @@ -130,6 +165,20 @@ pub fn generate(algorithm: SecAlg) -> Option { SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) .and_then(PKey::from_rsa) .unwrap(), + SecAlg::ECDSAP256SHA256 => { + let group = openssl::nid::Nid::X9_62_PRIME256V1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } + SecAlg::ECDSAP384SHA384 => { + let group = openssl::nid::Nid::SECP384R1; + let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); + openssl::ec::EcKey::generate(&group) + .and_then(PKey::from_ec_key) + .unwrap() + } SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), SecAlg::ED448 => PKey::generate_ed448().unwrap(), _ => return None, @@ -165,8 +214,13 @@ mod tests { use crate::{base::iana::SecAlg, sign::generic}; - const ALGORITHMS: &[SecAlg] = - &[SecAlg::RSASHA256, SecAlg::ED25519, SecAlg::ED448]; + const ALGORITHMS: &[SecAlg] = &[ + SecAlg::RSASHA256, + SecAlg::ECDSAP256SHA256, + SecAlg::ECDSAP384SHA384, + SecAlg::ED25519, + SecAlg::ED448, + ]; #[test] fn generate_all() { From 6bc9bce1cb3552f7c041af0f473381df43f730a1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:41:36 +0200 Subject: [PATCH 125/569] [sign/openssl] satisfy clippy --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index d35f45850..1211d6225 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -67,7 +67,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) @@ -80,7 +80,7 @@ impl SecretKey { let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(&*k); + let n = num(k.as_slice()); p.mul_generator(&group, &n, &ctx).unwrap(); openssl::ec::EcKey::from_private_components(&group, &n, &p) .and_then(PKey::from_ec_key) From be3e16908702d9681291450d3da19588013c3628 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 11:57:33 +0200 Subject: [PATCH 126/569] [sign/openssl] Implement the 'Sign' trait --- src/sign/openssl.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 1211d6225..663e8a904 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -13,7 +13,7 @@ use openssl::{ use crate::base::iana::SecAlg; -use super::generic; +use super::{generic, Sign}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -152,6 +152,36 @@ impl SecretKey { } } +impl Sign> for SecretKey { + type Error = openssl::error::ErrorStack; + + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn sign(&self, data: &[u8]) -> Result, Self::Error> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + let mut signer = match self.algorithm { + SecAlg::RSASHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP256SHA256 => { + Signer::new(MessageDigest::sha256(), &self.pkey)? + } + SecAlg::ECDSAP384SHA384 => { + Signer::new(MessageDigest::sha384(), &self.pkey)? + } + SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, + SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, + _ => unreachable!(), + }; + + signer.sign_oneshot_to_vec(data) + } +} + /// Generate a new secret key for the given algorithm. /// /// If the algorithm is not supported, [`None`] is returned. From 836812a94b53d4af486789a810ba36d661330e15 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:24:02 +0200 Subject: [PATCH 127/569] Install OpenSSL in CI builds --- .github/workflows/ci.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de6bf224b..99a36d6cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,14 +17,20 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: ${{ matrix.rust }} + - if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install libssl-dev + echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + - if: matrix.os == 'windows-latest' + run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features --all-targets -- -D warnings + run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features --all-targets - - run: cargo test --all-features + - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets + - run: cargo test $OPENSSL_FLAVOR --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest @@ -37,6 +43,8 @@ jobs: uses: hecrj/setup-rust-action@v2 with: rust-version: "1.68.2" + - name: Install OpenSSL + run: sudo apt install libssl-dev - name: Install nightly Rust run: rustup install nightly - name: Check with minimal-versions From 66290a576b9bfea9572607c912f1a414000c93b8 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:28 +0200 Subject: [PATCH 128/569] Ensure 'openssl' dep supports 3.x.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d09e5f532..dd00b9a12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10", optional = true } +openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x 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 } From 2a1489faeedc5585a9bc35e7fad91e3ad33a7988 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:39:52 +0200 Subject: [PATCH 129/569] [workflows/ci] Use 'vcpkg' instead of vendoring OpenSSL --- .github/workflows/ci.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 99a36d6cc..18a8bdb13 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,19 +18,22 @@ jobs: with: rust-version: ${{ matrix.rust }} - if: matrix.os == 'ubuntu-latest' - run: | - sudo apt install libssl-dev - echo "OPENSSL_FLAVOR=" >> "$GITHUB_ENV" + run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' - run: echo "OPENSSL_FLAVOR=--features openssl/vendored" >> "$GITHUB_ENV" + uses: johnwason/vcpkg-action@v6 + with: + pkgs: openssl + triplet: x64-windows-release + token: ${{ github.token }} + github-binarycache: true - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' - run: cargo clippy --all-features $OPENSSL_FLAVOR --all-targets -- -D warnings + run: cargo clippy --all-features --all-targets -- -D warnings - if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest' run: cargo fmt --all -- --check - - run: cargo check --no-default-features $OPENSSL_FLAVOR --all-targets - - run: cargo test $OPENSSL_FLAVOR --all-features + - run: cargo check --no-default-features --all-targets + - run: cargo test --all-features minimal-versions: name: Check minimal versions runs-on: ubuntu-latest From e8d208fb2f1437f4dc293592cde43b60c1e0bdc8 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 12:55:18 +0200 Subject: [PATCH 130/569] Ensure 'openssl' dep exposes necessary interfaces --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index dd00b9a12..abbd178ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.42", optional = true } # 0.10.42 adds support for OpenSSL 3.x.x +openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions 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 } From 045d52b85a05017cc25cb9d8e2af7a54323da4e4 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:03:14 +0200 Subject: [PATCH 131/569] [workflows/ci] Record location of 'vcpkg' --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 18a8bdb13..362b3e146 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true + - if: matrix.os == 'windows-latest' + run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 460679bc54e0f5cc66e25483642efcb73ddb0ce1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:13:22 +0200 Subject: [PATCH 132/569] [workflows/ci] Use a YAML def for 'VCPKG_ROOT' --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362b3e146..514844da8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,7 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" steps: - name: Checkout repository uses: actions/checkout@v1 @@ -26,8 +27,6 @@ jobs: triplet: x64-windows-release token: ${{ github.token }} github-binarycache: true - - if: matrix.os == 'windows-latest' - run: echo "VCPKG_ROOT=${{ github.workspace }}\\vcpkg" >> "$GITHUB_ENV" - if: matrix.rust == 'stable' run: rustup component add clippy - if: matrix.rust == 'stable' From 21ba8d349901657b5122c0e2a528ac4f1a86391e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:16 +0200 Subject: [PATCH 133/569] [workflows/ci] Fix a vcpkg triplet to use --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 514844da8..12334fa51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ jobs: env: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" + VCPKGRS_TRIPLET: x64-windows-release steps: - name: Checkout repository uses: actions/checkout@v1 @@ -24,7 +25,7 @@ jobs: uses: johnwason/vcpkg-action@v6 with: pkgs: openssl - triplet: x64-windows-release + triplet: ${{ env.VCPKGRS_TRIPLET }} token: ${{ github.token }} github-binarycache: true - if: matrix.rust == 'stable' From 4195dd49e76b747caa4dec170371c214b34c750f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:18:43 +0200 Subject: [PATCH 134/569] Upgrade openssl to 0.10.57 for bitflags 2.x --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index abbd178ea..a21bd0fbc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ heapless = { version = "0.8", optional = true } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.55", optional = true } # 0.10.55 adds support for PKey conversions +openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x 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 } From 4f4f6ff224933dff91c2985be616038500eeca8f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:22:18 +0200 Subject: [PATCH 135/569] [workflows/ci] Use dynamic linking for vcpkg openssl --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 12334fa51..23c73a5ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,7 @@ jobs: RUSTFLAGS: "-D warnings" VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository uses: actions/checkout@v1 From 608cbea8a6028f9b014e505731329f736adef6b4 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:24:05 +0200 Subject: [PATCH 136/569] [workflows/ci] Correctly annotate 'vcpkg' --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23c73a5ee..299da6658 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,6 +23,7 @@ jobs: - if: matrix.os == 'ubuntu-latest' run: sudo apt install libssl-dev - if: matrix.os == 'windows-latest' + id: vcpkg uses: johnwason/vcpkg-action@v6 with: pkgs: openssl From 632c1b06c5662bf41e5d1428c429711780ff219f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:51:14 +0200 Subject: [PATCH 137/569] [sign/openssl] Implement exporting public keys --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 663e8a904..0147222f6 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -150,6 +150,55 @@ impl SecretKey { _ => unreachable!(), } } + + /// Export this key into a generic public key. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self.algorithm { + SecAlg::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + }) + } + SecAlg::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + SecAlg::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let form = openssl::ec::PointConversionForm::UNCOMPRESSED; + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let key = key + .public_key() + .to_bytes(key.group(), form, &mut ctx) + .unwrap(); + generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + SecAlg::ED25519 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + SecAlg::ED448 => { + let key = self.pkey.raw_public_key().unwrap(); + generic::PublicKey::Ed448(key.try_into().unwrap()) + } + _ => unreachable!(), + } + } } impl Sign> for SecretKey { @@ -268,4 +317,12 @@ mod tests { assert!(key.pkey.public_eq(&imp.pkey)); } } + + #[test] + fn export_public() { + for &algorithm in ALGORITHMS { + let key = super::generate(algorithm).unwrap(); + let _: generic::PublicKey> = key.export_public(); + } + } } From 4350d8b610129dde5513f47aca060e867c6c1d26 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 13:56:16 +0200 Subject: [PATCH 138/569] [sign/ring] Implement exporting public keys --- src/sign/ring.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 872f8dadb..185b97295 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -55,6 +55,28 @@ impl<'a> SecretKey<'a> { _ => Err(ImportError::UnsupportedAlgorithm), } } + + /// Export this key into a generic public key. + pub fn export_public(&self) -> generic::PublicKey + where + B: AsRef<[u8]> + From>, + { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + generic::PublicKey::RsaSha256(generic::RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + Self::Ed25519(key) => { + use ring::signature::KeyPair; + let key = key.public_key().as_ref(); + generic::PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } } /// An error in importing a key into `ring`. From 4c465528e0f586942fa032ce69d0753795e4e89e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 19:39:34 +0200 Subject: [PATCH 139/569] [sign/generic] Test (de)serialization for generic secret keys There were bugs in the Base64 encoding/decoding that are not worth trying to debug; there's a perfectly usable Base64 implementation in the crate already. --- src/sign/generic.rs | 272 +++++------------- test-data/dnssec-keys/Ktest.+008+55993.key | 1 + .../dnssec-keys/Ktest.+008+55993.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 + .../dnssec-keys/Ktest.+013+40436.private | 3 + test-data/dnssec-keys/Ktest.+014+17013.key | 1 + .../dnssec-keys/Ktest.+014+17013.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 + .../dnssec-keys/Ktest.+015+43769.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 + .../dnssec-keys/Ktest.+016+34114.private | 3 + 11 files changed, 100 insertions(+), 199 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key create mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+40436.private create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key create mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key create mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key create mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f963a8def..01505239d 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -4,6 +4,7 @@ use std::vec::Vec; use crate::base::iana::SecAlg; use crate::rdata::Dnskey; +use crate::utils::base64; /// A generic secret key. /// @@ -56,6 +57,7 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + w.write_str("Private-key-format: v1.2\n")?; match self { Self::RsaSha256(k) => { w.write_str("Algorithm: 8 (RSASHA256)\n")?; @@ -64,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - base64_encode(s, &mut *w) + write!(w, "PrivateKey: {}\n", base64::encode_display(s)) } } } @@ -107,11 +109,12 @@ impl + AsMut<[u8]>> SecretKey { return Err(DnsFormatError::Misformatted); } - let mut buf = [0u8; N]; - if base64_decode(val.as_bytes(), &mut buf) != Ok(N) { - // The private key was of the wrong size. - return Err(DnsFormatError::Misformatted); - } + let buf: Vec = base64::decode(val) + .map_err(|_| DnsFormatError::Misformatted)?; + let buf = buf + .as_slice() + .try_into() + .map_err(|_| DnsFormatError::Misformatted)?; Ok(buf) } @@ -205,22 +208,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Modulus:\t")?; - base64_encode(self.n.as_ref(), &mut *w)?; - w.write_str("\nPublicExponent:\t")?; - base64_encode(self.e.as_ref(), &mut *w)?; - w.write_str("\nPrivateExponent:\t")?; - base64_encode(self.d.as_ref(), &mut *w)?; - w.write_str("\nPrime1:\t")?; - base64_encode(self.p.as_ref(), &mut *w)?; - w.write_str("\nPrime2:\t")?; - base64_encode(self.q.as_ref(), &mut *w)?; - w.write_str("\nExponent1:\t")?; - base64_encode(self.d_p.as_ref(), &mut *w)?; - w.write_str("\nExponent2:\t")?; - base64_encode(self.d_q.as_ref(), &mut *w)?; - w.write_str("\nCoefficient:\t")?; - base64_encode(self.q_i.as_ref(), &mut *w)?; + w.write_str("Modulus: ")?; + write!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("\nPublicExponent: ")?; + write!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("\nPrivateExponent: ")?; + write!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("\nPrime1: ")?; + write!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("\nPrime2: ")?; + write!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("\nExponent1: ")?; + write!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("\nExponent2: ")?; + write!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("\nCoefficient: ")?; + write!(w, "{}", base64::encode_display(&self.q_i))?; w.write_char('\n') } @@ -258,10 +261,8 @@ impl + AsMut<[u8]>> RsaSecretKey { return Err(DnsFormatError::Misformatted); } - let mut buffer = vec![0u8; (val.len() + 3) / 4 * 3]; - let size = base64_decode(val.as_bytes(), &mut buffer) + let buffer: Vec = base64::decode(val) .map_err(|_| DnsFormatError::Misformatted)?; - buffer.truncate(size); *field = Some(buffer.into()); data = rest; @@ -428,6 +429,11 @@ fn parse_dns_pair( // Trim any pending newlines. let data = data.trim_start(); + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + // Get the first line (NOTE: CR LF is handled later). let (line, rest) = data.split_once('\n').unwrap_or((data, "")); @@ -439,177 +445,6 @@ fn parse_dns_pair( Ok(Some((key.trim(), val.trim(), rest))) } -/// A utility function to format data as Base64. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -fn base64_encode(data: &[u8], w: &mut impl fmt::Write) -> fmt::Result { - // Convert a single chunk of bytes into Base64. - fn encode(data: [u8; 3]) -> [u8; 4] { - let [a, b, c] = data; - - // Expand the chunk using integer operations; it's pretty fast. - let chunk = (a as u32) << 16 | (b as u32) << 8 | (c as u32); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let chunk = (chunk & 0x00FFF000) << 4 | (chunk & 0x00000FFF); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FC00FC0) << 2 | (chunk & 0x003F003F); - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - - // Classify each output byte as A-Z, a-z, 0-9, + or /. - let bcast = 0x01010101u32; - let uppers = chunk + (128 - 26) * bcast; - let lowers = chunk + (128 - 52) * bcast; - let digits = chunk + (128 - 62) * bcast; - let pluses = chunk + (128 - 63) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = !uppers >> 7; - let lowers = (uppers & !lowers) >> 7; - let digits = (lowers & !digits) >> 7; - let pluses = (digits & !pluses) >> 7; - let slashs = pluses >> 7; - - // Add the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - + (uppers & bcast) * (b'A' - 0) as u32 - + (lowers & bcast) * (b'a' - 26) as u32 - - (digits & bcast) * (52 - b'0') as u32 - - (pluses & bcast) * (62 - b'+') as u32 - - (slashs & bcast) * (63 - b'/') as u32; - - // Convert back into a byte array. - chunk.to_be_bytes() - } - - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - let mut chunks = data.chunks_exact(3); - - // Iterate over the whole chunks in the input. - for chunk in &mut chunks { - let chunk = <[u8; 3]>::try_from(chunk).unwrap(); - let chunk = encode(chunk); - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk)?; - } - - // Encode the final chunk and handle padding. - let mut chunk = [0u8; 3]; - chunk[..chunks.remainder().len()].copy_from_slice(chunks.remainder()); - let mut chunk = encode(chunk); - match chunks.remainder().len() { - 0 => return Ok(()), - 1 => chunk[2..].fill(b'='), - 2 => chunk[3..].fill(b'='), - _ => unreachable!(), - } - let chunk = str::from_utf8(&chunk).unwrap(); - w.write_str(chunk) -} - -/// A utility function to decode Base64 data. -/// -/// This is a simple implementation with the only requirement of being -/// constant-time and side-channel resistant. -/// -/// Incorrect padding or garbage bytes will result in an error. -fn base64_decode(encoded: &[u8], decoded: &mut [u8]) -> Result { - /// Decode a single chunk of bytes from Base64. - fn decode(data: [u8; 4]) -> Result<[u8; 3], ()> { - let chunk = u32::from_be_bytes(data); - let bcast = 0x01010101u32; - - // Mask out non-ASCII bytes early. - if chunk & 0x80808080 != 0 { - return Err(()); - } - - // Classify each byte as A-Z, a-z, 0-9, + or /. - let uppers = chunk + (128 - b'A' as u32) * bcast; - let lowers = chunk + (128 - b'a' as u32) * bcast; - let digits = chunk + (128 - b'0' as u32) * bcast; - let pluses = chunk + (128 - b'+' as u32) * bcast; - let slashs = chunk + (128 - b'/' as u32) * bcast; - - // For each byte, the LSB is set if it is in the class. - let uppers = (uppers ^ (uppers - bcast * 26)) >> 7; - let lowers = (lowers ^ (lowers - bcast * 26)) >> 7; - let digits = (digits ^ (digits - bcast * 10)) >> 7; - let pluses = (pluses ^ (pluses - bcast)) >> 7; - let slashs = (slashs ^ (slashs - bcast)) >> 7; - - // Check if an input was in none of the classes. - if bcast & !(uppers | lowers | digits | pluses | slashs) != 0 { - return Err(()); - } - - // Subtract the corresponding offset for each class. - #[allow(clippy::identity_op)] - let chunk = chunk - - (uppers & bcast) * (b'A' - 0) as u32 - - (lowers & bcast) * (b'a' - 26) as u32 - + (digits & bcast) * (52 - b'0') as u32 - + (pluses & bcast) * (62 - b'+') as u32 - + (slashs & bcast) * (63 - b'/') as u32; - - // Compress the chunk using integer operations. - // (0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8, 0b00XXXXXXu8) - let chunk = (chunk & 0x3F003F00) >> 2 | (chunk & 0x003F003F); - // (0b0000XXXX_XXXXXXXXu16, 0b0000XXXX_XXXXXXXXu16) - let chunk = (chunk & 0x0FFF0000) >> 4 | (chunk & 0x00000FFF); - // 0b00000000_XXXXXXXX_XXXXXXXX_XXXXXXXXu32 - let [_, a, b, c] = chunk.to_be_bytes(); - - Ok([a, b, c]) - } - - // Uneven inputs are not allowed; use padding. - if encoded.len() % 4 != 0 { - return Err(()); - } - - // The index into the decoded buffer. - let mut index = 0usize; - - // Iterate over the whole chunks in the input. - // TODO: Use 'slice::array_chunks()' or 'slice::as_chunks()'. - for chunk in encoded.chunks_exact(4) { - let mut chunk = <[u8; 4]>::try_from(chunk).unwrap(); - - // Check for padding. - let ppos = chunk.iter().position(|&b| b == b'=').unwrap_or(4); - if chunk[ppos..].iter().any(|&b| b != b'=') { - // A padding byte was followed by a non-padding byte. - return Err(()); - } - - // Mask out the padding for the main decoder. - chunk[ppos..].fill(b'A'); - - // Determine how many output bytes there are. - let amount = match ppos { - 0 | 1 => return Err(()), - 2 => 1, - 3 => 2, - 4 => 3, - _ => unreachable!(), - }; - - if index + amount >= decoded.len() { - // The input was too long, or the output was too short. - return Err(()); - } - - // Decode the chunk and write the unpadded amount. - let chunk = decode(chunk)?; - decoded[index..][..amount].copy_from_slice(&chunk[..amount]); - index += amount; - } - - Ok(index) -} - /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum DnsFormatError { @@ -634,3 +469,42 @@ impl fmt::Display for DnsFormatError { } impl std::error::Error for DnsFormatError {} + +#[cfg(test)] +mod tests { + use std::{string::String, vec::Vec}; + + use crate::base::iana::SecAlg; + + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 55993), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = super::SecretKey::>::from_dns(&data).unwrap(); + let mut same = String::new(); + key.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key new file mode 100644 index 000000000..8248fbfe8 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private new file mode 100644 index 000000000..7a260e7a0 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+55993.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= +PublicExponent: AQAB +PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= +Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== +Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== +Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== +Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== +Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key new file mode 100644 index 000000000..7f7cd0fcc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+40436.private new file mode 100644 index 000000000..39f5e8a8d --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+40436.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 13 (ECDSAP256SHA256) +PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key new file mode 100644 index 000000000..c7b6aa1d4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private new file mode 100644 index 000000000..9648a876a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+17013.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key new file mode 100644 index 000000000..8a1f24f67 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private new file mode 100644 index 000000000..e178a3bd4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+43769.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key new file mode 100644 index 000000000..fc77e0491 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private new file mode 100644 index 000000000..fca7303dc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+34114.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From fc955233d71b2e1a6cc99cb1832b8a1779318fdf Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:03:03 +0200 Subject: [PATCH 140/569] [sign] Thoroughly test import/export in both backends I had to swap out the RSA key since 'ring' found it to be too small. --- src/sign/generic.rs | 2 +- src/sign/openssl.rs | 73 +++++++++++++++---- src/sign/ring.rs | 57 +++++++++++++++ test-data/dnssec-keys/Ktest.+008+27096.key | 1 + .../dnssec-keys/Ktest.+008+27096.private | 10 +++ test-data/dnssec-keys/Ktest.+008+55993.key | 1 - .../dnssec-keys/Ktest.+008+55993.private | 10 --- 7 files changed, 127 insertions(+), 27 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key create mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+55993.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 01505239d..5626e6ce9 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -477,7 +477,7 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 55993), + (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), (SecAlg::ECDSAP384SHA384, 17013), (SecAlg::ED25519, 43769), diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 0147222f6..9154abd55 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -289,28 +289,32 @@ impl std::error::Error for ImportError {} #[cfg(test)] mod tests { - use std::vec::Vec; + use std::{string::String, vec::Vec}; - use crate::{base::iana::SecAlg, sign::generic}; + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; - const ALGORITHMS: &[SecAlg] = &[ - SecAlg::RSASHA256, - SecAlg::ECDSAP256SHA256, - SecAlg::ECDSAP384SHA384, - SecAlg::ED25519, - SecAlg::ED448, + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), ]; #[test] - fn generate_all() { - for &algorithm in ALGORITHMS { + fn generate() { + for &(algorithm, _) in KEYS { let _ = super::generate(algorithm).unwrap(); } } #[test] - fn export_and_import() { - for &algorithm in ALGORITHMS { + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let exp: generic::SecretKey> = key.export(); let imp = super::SecretKey::import(exp).unwrap(); @@ -318,11 +322,50 @@ mod tests { } } + #[test] + fn imported_roundtrip() { + type GenericKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let imp = GenericKey::from_dns(&data).unwrap(); + let key = super::SecretKey::import(imp).unwrap(); + let exp: GenericKey = key.export(); + let mut same = String::new(); + exp.into_dns(&mut same).unwrap(); + assert_eq!(data, same); + } + } + #[test] fn export_public() { - for &algorithm in ALGORITHMS { - let key = super::generate(algorithm).unwrap(); - let _: generic::PublicKey> = key.export_public(); + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 185b97295..edea8ae14 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -3,6 +3,7 @@ #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] +use core::fmt; use std::vec::Vec; use crate::base::iana::SecAlg; @@ -42,6 +43,7 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) + .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } @@ -80,6 +82,7 @@ impl<'a> SecretKey<'a> { } /// An error in importing a key into `ring`. +#[derive(Clone, Debug)] pub enum ImportError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -88,6 +91,15 @@ pub enum ImportError { InvalidKey, } +impl fmt::Display for ImportError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + }) + } +} + impl<'a> super::Sign> for SecretKey<'a> { type Error = ring::error::Unspecified; @@ -110,3 +122,48 @@ impl<'a> super::Sign> for SecretKey<'a> { } } } + +#[cfg(test)] +mod tests { + use std::vec::Vec; + + use crate::{ + base::{iana::SecAlg, scan::IterScanner}, + rdata::Dnskey, + sign::generic, + }; + + const KEYS: &[(SecAlg, u16)] = + &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + + #[test] + fn export_public() { + type GenericSecretKey = generic::SecretKey>; + type GenericPublicKey = generic::PublicKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let pub_key: GenericPublicKey = sec_key.export_public(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let mut data = std::fs::read_to_string(path).unwrap(); + // Remove a trailing comment, if any. + if let Some(pos) = data.bytes().position(|b| b == b';') { + data.truncate(pos); + } + // Skip ' ' + let data = data.split_ascii_whitespace().skip(3); + let mut data = IterScanner::new(data); + let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + + assert_eq!(dns_key.key_tag(), key_tag); + assert_eq!(pub_key.into_dns::>(256), dns_key) + } + } +} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key new file mode 100644 index 000000000..5aa614f71 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.key @@ -0,0 +1 @@ +test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private new file mode 100644 index 000000000..b5819714f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+27096.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 +PublicExponent: AQAB +PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN +Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj +Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd +Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf +Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 +Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+55993.key b/test-data/dnssec-keys/Ktest.+008+55993.key deleted file mode 100644 index 8248fbfe8..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAdhof9Qcde/ND4SQxY+amGsRVm5q9uijkDJY14TBBOkC1BfS1s4Wo+zy15dsggHrbP5j6AFNZ7AUN7G9ZlcYSRH2POhojghf8VLD7oYzsi3oNAzvpnQF/q4xQxvfRKIo3XcBZykZUvDQLyUTTKjq+LN3ZHRjlc5v0cR03doI0iWD ;{id = 55993 (zsk), size = 1024b} diff --git a/test-data/dnssec-keys/Ktest.+008+55993.private b/test-data/dnssec-keys/Ktest.+008+55993.private deleted file mode 100644 index 7a260e7a0..000000000 --- a/test-data/dnssec-keys/Ktest.+008+55993.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: 2Gh/1Bx1780PhJDFj5qYaxFWbmr26KOQMljXhMEE6QLUF9LWzhaj7PLXl2yCAets/mPoAU1nsBQ3sb1mVxhJEfY86GiOCF/xUsPuhjOyLeg0DO+mdAX+rjFDG99EoijddwFnKRlS8NAvJRNMqOr4s3dkdGOVzm/RxHTd2gjSJYM= -PublicExponent: AQAB -PrivateExponent: HeFn7Qi0/BRrVRmMPcTR0M7HCV35k6up6Fm+AFWKcQXz9QomoLQdlET/oafY150DIqj2yt8+NuDDw+Xr8JCo3fIGUZ9rzrEuOOksWNy1yPxuBhlVUE9fK0tXqGRs1WZtHKq6vRQgBCL3PRfJLDJckLUGFXXE3IW+Nbb7QWuV1qk= -Prime1: 8Sa4eHpAZ3dSbckv7+KN3N9i/xnleIkkGC6POX0krCWKxcd5JuTi+IAo/mzBwkpcbFS09uSYn1MR2/07vCgyLQ== -Prime2: 5bvAtQ0hMu1Pe15l0rAIiwFOJ8nfTWVlIt6/n+NyMSPnmQb7JZOIDsEeAEWNCe+h4gvbuBr61xDcfWiDoEh0bw== -Exponent1: moO83zU13xXNcxrd5E69pzBbNilZpwn4XqY2jxdoUAUeDevp7MnrxF4Z5iu5Wsxau+7qpOeEA1Iut05i4ATBYQ== -Exponent2: AQ4cs3gs99vpKorjctVGJMVLw5kEwok9rqxROv3Db4BXtvc2PhTwYgj3B09Kd4o3Nx+Q0cal8kjsilLpj9nlVw== -Coefficient: QRJs+o7vXqzEonMJCuO9jUCwHkxDXBQ8aCkE2EL0W7Ls+Qd7ICCWMbuCtPjkrad1R2wtf3ZyXjDVz2PUkadeuQ== From 22e00a6ed9776dfab43462d719573170d3f551ac Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:06:58 +0200 Subject: [PATCH 141/569] [sign] Remove debugging code and satisfy clippy --- src/sign/generic.rs | 8 ++++---- src/sign/ring.rs | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 5626e6ce9..8dd610637 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -66,22 +66,22 @@ impl + AsMut<[u8]>> SecretKey { Self::EcdsaP256Sha256(s) => { w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { w.write_str("Algorithm: 15 (ED25519)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { w.write_str("Algorithm: 16 (ED448)\n")?; - write!(w, "PrivateKey: {}\n", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index edea8ae14..864480933 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -43,7 +43,6 @@ impl<'a> SecretKey<'a> { qInv: k.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) - .inspect_err(|e| println!("Got err {e:?}")) .map_err(|_| ImportError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } From 94b3e477a27a18cd43615ea7419f1b58ce2e36c1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 9 Oct 2024 20:20:15 +0200 Subject: [PATCH 142/569] [sign] Account for CR LF in tests --- src/sign/generic.rs | 46 +++++++++++++++++++++++---------------------- src/sign/openssl.rs | 2 ++ 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8dd610637..8ad44ea88 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -57,30 +57,30 @@ impl + AsMut<[u8]>> SecretKey { /// - For ECDSA, see RFC 6605, section 6. /// - For EdDSA, see RFC 8080, section 6. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { - w.write_str("Private-key-format: v1.2\n")?; + writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { - w.write_str("Algorithm: 8 (RSASHA256)\n")?; + writeln!(w, "Algorithm: 8 (RSASHA256)")?; k.into_dns(w) } Self::EcdsaP256Sha256(s) => { - w.write_str("Algorithm: 13 (ECDSAP256SHA256)\n")?; + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::EcdsaP384Sha384(s) => { - w.write_str("Algorithm: 14 (ECDSAP384SHA384)\n")?; + writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed25519(s) => { - w.write_str("Algorithm: 15 (ED25519)\n")?; + writeln!(w, "Algorithm: 15 (ED25519)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } Self::Ed448(s) => { - w.write_str("Algorithm: 16 (ED448)\n")?; + writeln!(w, "Algorithm: 16 (ED448)")?; writeln!(w, "PrivateKey: {}", base64::encode_display(s)) } } @@ -209,22 +209,22 @@ impl + AsMut<[u8]>> RsaSecretKey { /// See RFC 5702, section 6 for examples of this format. pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; - write!(w, "{}", base64::encode_display(&self.n))?; - w.write_str("\nPublicExponent: ")?; - write!(w, "{}", base64::encode_display(&self.e))?; - w.write_str("\nPrivateExponent: ")?; - write!(w, "{}", base64::encode_display(&self.d))?; - w.write_str("\nPrime1: ")?; - write!(w, "{}", base64::encode_display(&self.p))?; - w.write_str("\nPrime2: ")?; - write!(w, "{}", base64::encode_display(&self.q))?; - w.write_str("\nExponent1: ")?; - write!(w, "{}", base64::encode_display(&self.d_p))?; - w.write_str("\nExponent2: ")?; - write!(w, "{}", base64::encode_display(&self.d_q))?; - w.write_str("\nCoefficient: ")?; - write!(w, "{}", base64::encode_display(&self.q_i))?; - w.write_char('\n') + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i))?; + Ok(()) } /// Parse a key from the conventional DNS format. @@ -504,6 +504,8 @@ mod tests { let key = super::SecretKey::>::from_dns(&data).unwrap(); let mut same = String::new(); key.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9154abd55..2377dc250 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -335,6 +335,8 @@ mod tests { let exp: GenericKey = key.export(); let mut same = String::new(); exp.into_dns(&mut same).unwrap(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); assert_eq!(data, same); } } From 68a56569fbefaf3500c9dbbcccb222bcc2f9de10 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 11 Oct 2024 16:16:12 +0200 Subject: [PATCH 143/569] [sign/openssl] Fix bugs in the signing procedure - RSA signatures were being made with an unspecified padding scheme. - ECDSA signatures were being output in ASN.1 DER format, instead of the fixed-size format required by DNSSEC (and output by 'ring'). - Tests for signature failures are now added for both backends. --- src/sign/openssl.rs | 57 +++++++++++++++++++++++++++++++++++++-------- src/sign/ring.rs | 19 ++++++++++++++- 2 files changed, 65 insertions(+), 11 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 2377dc250..8faa48f9e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -8,6 +8,7 @@ use std::vec::Vec; use openssl::{ bn::BigNum, + ecdsa::EcdsaSig, pkey::{self, PKey, Private}, }; @@ -212,22 +213,42 @@ impl Sign> for SecretKey { use openssl::hash::MessageDigest; use openssl::sign::Signer; - let mut signer = match self.algorithm { + match self.algorithm { SecAlg::RSASHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) } SecAlg::ECDSAP256SHA256 => { - Signer::new(MessageDigest::sha256(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(32).unwrap(); + let s = signature.s().to_vec_padded(32).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) } SecAlg::ECDSAP384SHA384 => { - Signer::new(MessageDigest::sha384(), &self.pkey)? + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature).unwrap(); + let r = signature.r().to_vec_padded(48).unwrap(); + let s = signature.s().to_vec_padded(48).unwrap(); + let mut signature = Vec::new(); + signature.extend_from_slice(&r); + signature.extend_from_slice(&s); + Ok(signature) + } + SecAlg::ED25519 | SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) } - SecAlg::ED25519 => Signer::new_without_digest(&self.pkey)?, - SecAlg::ED448 => Signer::new_without_digest(&self.pkey)?, _ => unreachable!(), - }; - - signer.sign_oneshot_to_vec(data) + } } } @@ -294,7 +315,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = &[ @@ -370,4 +391,20 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let sec_key = super::SecretKey::import(sec_key).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 864480933..0996552f6 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -129,7 +129,7 @@ mod tests { use crate::{ base::{iana::SecAlg, scan::IterScanner}, rdata::Dnskey, - sign::generic, + sign::{generic, Sign}, }; const KEYS: &[(SecAlg, u16)] = @@ -165,4 +165,21 @@ mod tests { assert_eq!(pub_key.into_dns::>(256), dns_key) } } + + #[test] + fn sign() { + type GenericSecretKey = generic::SecretKey>; + + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let sec_key = GenericSecretKey::from_dns(&data).unwrap(); + let rng = ring::rand::SystemRandom::new(); + let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + + let _ = sec_key.sign(b"Hello, World!").unwrap(); + } + } } From a71c339fddd8ed485f82f9c026afb997c83093b6 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 15 Oct 2024 17:32:36 +0200 Subject: [PATCH 144/569] Refactor the 'sign' module Most functions have been renamed. The public key types have been moved to the 'validate' module (which 'sign' now depends on), and they have been outfitted with conversions (e.g. to and from DNSKEY records). Importing a generic key into an OpenSSL or Ring key now requires the public key to also be available. In both implementations, the pair are checked for consistency -- this ensures that both are uncorrupted and that keys have not been mixed up. This also allows the Ring backend to support ECDSA keys (although key generation is still difficult). The 'PublicKey' and 'PrivateKey' enums now store their array data in 'Box'. This has two benefits: it is easier to securely manage memory on the heap (since the compiler will not copy it around the stack); and the smaller sizes of the types is beneficial (although negligibly) to performance. --- Cargo.toml | 3 +- src/sign/generic.rs | 393 ++++++++++++++++++++------------------------ src/sign/mod.rs | 81 ++++++--- src/sign/openssl.rs | 304 +++++++++++++++++++--------------- src/sign/ring.rs | 241 ++++++++++++++++++--------- src/validate.rs | 347 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 910 insertions(+), 459 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a21bd0fbc..7efdc389d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,11 +50,10 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -openssl = ["dep:openssl"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std"] +sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8ad44ea88..2589a6ab4 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,10 +1,11 @@ -use core::{fmt, mem, str}; +use core::{fmt, str}; +use std::boxed::Box; use std::vec::Vec; use crate::base::iana::SecAlg; -use crate::rdata::Dnskey; use crate::utils::base64; +use crate::validate::RsaPublicKey; /// A generic secret key. /// @@ -14,32 +15,97 @@ use crate::utils::base64; /// cryptographic implementation supports it). /// /// [`Sign`]: super::Sign -pub enum SecretKey + AsMut<[u8]>> { - /// An RSA/SHA256 keypair. - RsaSha256(RsaSecretKey), +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +pub enum SecretKey { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKey), /// An ECDSA P-256/SHA-256 keypair. /// /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256([u8; 32]), + EcdsaP256Sha256(Box<[u8; 32]>), /// An ECDSA P-384/SHA-384 keypair. /// /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384([u8; 48]), + EcdsaP384Sha384(Box<[u8; 48]>), /// An Ed25519 keypair. /// /// The private key is a single 32-byte string. - Ed25519([u8; 32]), + Ed25519(Box<[u8; 32]>), /// An Ed448 keypair. /// /// The private key is a single 57-byte string. - Ed448([u8; 57]), + Ed448(Box<[u8; 57]>), } -impl + AsMut<[u8]>> SecretKey { +impl SecretKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -51,99 +117,99 @@ impl + AsMut<[u8]>> SecretKey { } } - /// Serialize this key in the conventional DNS format. + /// Serialize this key in the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { writeln!(w, "Algorithm: 8 (RSASHA256)")?; - k.into_dns(w) + k.format_as_bind(w) } Self::EcdsaP256Sha256(s) => { writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::EcdsaP384Sha384(s) => { writeln!(w, "Algorithm: 14 (ECDSAP384SHA384)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed25519(s) => { writeln!(w, "Algorithm: 15 (ED25519)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } Self::Ed448(s) => { writeln!(w, "Algorithm: 16 (ED448)")?; - writeln!(w, "PrivateKey: {}", base64::encode_display(s)) + writeln!(w, "PrivateKey: {}", base64::encode_display(&**s)) } } } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// - For RSA, see RFC 5702, section 6. - /// - For ECDSA, see RFC 6605, section 6. - /// - For EdDSA, see RFC 8080, section 6. - pub fn from_dns(data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { /// Parse private keys for most algorithms (except RSA). fn parse_pkey( - data: &str, - ) -> Result<[u8; N], DnsFormatError> { - // Extract the 'PrivateKey' field. - let (_, val, data) = parse_dns_pair(data)? - .filter(|&(k, _, _)| k == "PrivateKey") - .ok_or(DnsFormatError::Misformatted)?; - - if !data.trim().is_empty() { - // There were more fields following. - return Err(DnsFormatError::Misformatted); - } + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_dns_pair(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } - let buf: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; - let buf = buf - .as_slice() - .try_into() - .map_err(|_| DnsFormatError::Misformatted)?; + return base64::decode::>(val) + .map_err(|_| BindFormatError::Misformatted)? + .into_boxed_slice() + .try_into() + .map_err(|_| BindFormatError::Misformatted); + } - Ok(buf) + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) } // The first line should specify the key format. let (_, _, data) = parse_dns_pair(data)? - .filter(|&(k, v, _)| (k, v) == ("Private-key-format", "v1.2")) - .ok_or(DnsFormatError::UnsupportedFormat)?; + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .map_or(false, |minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; // The second line should specify the algorithm. let (_, val, data) = parse_dns_pair(data)? .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(DnsFormatError::Misformatted)?; + .ok_or(BindFormatError::Misformatted)?; // Parse the algorithm. let mut words = val.split_whitespace(); let code = words .next() - .ok_or(DnsFormatError::Misformatted)? - .parse::() - .map_err(|_| DnsFormatError::Misformatted)?; - let name = words.next().ok_or(DnsFormatError::Misformatted)?; + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; if words.next().is_some() { - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } match (code, name) { (8, "(RSASHA256)") => { - RsaSecretKey::from_dns(data).map(Self::RsaSha256) + RsaSecretKey::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -153,12 +219,12 @@ impl + AsMut<[u8]>> SecretKey { } (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(DnsFormatError::UnsupportedAlgorithm), + _ => Err(BindFormatError::UnsupportedAlgorithm), } } } -impl + AsMut<[u8]>> Drop for SecretKey { +impl Drop for SecretKey { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -175,39 +241,40 @@ impl + AsMut<[u8]>> Drop for SecretKey { /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaSecretKey + AsMut<[u8]>> { +pub struct RsaSecretKey { /// The public modulus. - pub n: B, + pub n: Box<[u8]>, /// The public exponent. - pub e: B, + pub e: Box<[u8]>, /// The private exponent. - pub d: B, + pub d: Box<[u8]>, /// The first prime factor of `d`. - pub p: B, + pub p: Box<[u8]>, /// The second prime factor of `d`. - pub q: B, + pub q: Box<[u8]>, /// The exponent corresponding to the first prime factor of `d`. - pub d_p: B, + pub d_p: Box<[u8]>, /// The exponent corresponding to the second prime factor of `d`. - pub d_q: B, + pub d_q: Box<[u8]>, /// The inverse of the second prime factor modulo the first. - pub q_i: B, + pub q_i: Box<[u8]>, } -impl + AsMut<[u8]>> RsaSecretKey { - /// Serialize this key in the conventional DNS format. - /// - /// The output does not include an 'Algorithm' specifier. +impl RsaSecretKey { + /// Serialize this key in the conventional format used by BIND. /// - /// See RFC 5702, section 6 for examples of this format. - pub fn into_dns(&self, w: &mut impl fmt::Write) -> fmt::Result { + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKey`] for a + /// description of this format. + pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -227,13 +294,13 @@ impl + AsMut<[u8]>> RsaSecretKey { Ok(()) } - /// Parse a key from the conventional DNS format. + /// Parse a key from the conventional format used by BIND. /// - /// See RFC 5702, section 6. - pub fn from_dns(mut data: &str) -> Result - where - B: From>, - { + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKey`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; let mut d = None; @@ -253,25 +320,28 @@ impl + AsMut<[u8]>> RsaSecretKey { "Exponent1" => &mut d_p, "Exponent2" => &mut d_q, "Coefficient" => &mut q_i, - _ => return Err(DnsFormatError::Misformatted), + _ => { + data = rest; + continue; + } }; if field.is_some() { // This field has already been filled. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } let buffer: Vec = base64::decode(val) - .map_err(|_| DnsFormatError::Misformatted)?; + .map_err(|_| BindFormatError::Misformatted)?; - *field = Some(buffer.into()); + *field = Some(buffer.into_boxed_slice()); data = rest; } for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { if field.is_none() { // A field was missing. - return Err(DnsFormatError::Misformatted); + return Err(BindFormatError::Misformatted); } } @@ -288,142 +358,33 @@ impl + AsMut<[u8]>> RsaSecretKey { } } -impl + AsMut<[u8]>> Drop for RsaSecretKey { - fn drop(&mut self) { - // Zero the bytes for each field. - self.n.as_mut().fill(0u8); - self.e.as_mut().fill(0u8); - self.d.as_mut().fill(0u8); - self.p.as_mut().fill(0u8); - self.q.as_mut().fill(0u8); - self.d_p.as_mut().fill(0u8); - self.d_q.as_mut().fill(0u8); - self.q_i.as_mut().fill(0u8); - } -} - -/// A generic public key. -pub enum PublicKey> { - /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKey), - - // TODO: RSA/SHA-1 with NSEC3/SHA-1? - /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKey), - - /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKey), - - /// An ECDSA P-256/SHA-256 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (32 bytes). - /// - The encoding of the `y` coordinate (32 bytes). - EcdsaP256Sha256([u8; 65]), - - /// An ECDSA P-384/SHA-384 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (48 bytes). - /// - The encoding of the `y` coordinate (48 bytes). - EcdsaP384Sha384([u8; 97]), - - /// An Ed25519 public key. - /// - /// The public key is a 32-byte encoding of the public point. - Ed25519([u8; 32]), - - /// An Ed448 public key. - /// - /// The public key is a 57-byte encoding of the public point. - Ed448([u8; 57]), -} - -impl> PublicKey { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, +impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { + fn from(value: &'a RsaSecretKey) -> Self { + RsaPublicKey { + n: value.n.clone(), + e: value.e.clone(), } } - - /// Construct a DNSKEY record with the given flags. - pub fn into_dns(self, flags: u16) -> Dnskey - where - Octs: From> + AsRef<[u8]>, - { - let protocol = 3u8; - let algorithm = self.algorithm(); - let public_key = match self { - Self::RsaSha1(k) | Self::RsaSha256(k) | Self::RsaSha512(k) => { - let (n, e) = (k.n.as_ref(), k.e.as_ref()); - let e_len_len = if e.len() < 256 { 1 } else { 3 }; - let len = e_len_len + e.len() + n.len(); - let mut buf = Vec::with_capacity(len); - if let Ok(e_len) = u8::try_from(e.len()) { - buf.push(e_len); - } else { - // RFC 3110 is not explicit about the endianness of this, - // but 'ldns' (in 'ldns_key_buf2rsa_raw()') uses network - // byte order, which I suppose makes sense. - let e_len = u16::try_from(e.len()).unwrap(); - buf.extend_from_slice(&e_len.to_be_bytes()); - } - buf.extend_from_slice(e); - buf.extend_from_slice(n); - buf - } - - // From my reading of RFC 6605, the marker byte is not included. - Self::EcdsaP256Sha256(k) => k[1..].to_vec(), - Self::EcdsaP384Sha384(k) => k[1..].to_vec(), - - Self::Ed25519(k) => k.to_vec(), - Self::Ed448(k) => k.to_vec(), - }; - - Dnskey::new(flags, protocol, algorithm, public_key.into()).unwrap() - } -} - -/// A generic RSA public key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaPublicKey> { - /// The public modulus. - pub n: B, - - /// The public exponent. - pub e: B, } -impl From> for RsaPublicKey -where - B: AsRef<[u8]> + AsMut<[u8]> + Default, -{ - fn from(mut value: RsaSecretKey) -> Self { - Self { - n: mem::take(&mut value.n), - e: mem::take(&mut value.e), - } +impl Drop for RsaSecretKey { + 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); } } /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, -) -> Result, DnsFormatError> { +) -> Result, BindFormatError> { // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. // Trim any pending newlines. @@ -439,7 +400,7 @@ fn parse_dns_pair( // Split the line by a colon. let (key, val) = - line.split_once(':').ok_or(DnsFormatError::Misformatted)?; + line.split_once(':').ok_or(BindFormatError::Misformatted)?; // Trim the key and value (incl. for CR LFs). Ok(Some((key.trim(), val.trim(), rest))) @@ -447,7 +408,7 @@ fn parse_dns_pair( /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum DnsFormatError { +pub enum BindFormatError { /// The key file uses an unsupported version of the format. UnsupportedFormat, @@ -458,7 +419,7 @@ pub enum DnsFormatError { UnsupportedAlgorithm, } -impl fmt::Display for DnsFormatError { +impl fmt::Display for BindFormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedFormat => "unsupported format", @@ -468,7 +429,7 @@ impl fmt::Display for DnsFormatError { } } -impl std::error::Error for DnsFormatError {} +impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { @@ -490,7 +451,7 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -501,9 +462,9 @@ mod tests { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::>::from_dns(&data).unwrap(); + let key = super::SecretKey::parse_from_bind(&data).unwrap(); let mut same = String::new(); - key.into_dns(&mut same).unwrap(); + key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b1db46c26..b9773d7f0 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,37 +2,44 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of a -//! DNS record served by a secure-aware name server. But name servers are not -//! usually creating those signatures themselves. Within a DNS zone, it is the -//! zone administrator's responsibility to sign zone records (when the record's -//! time-to-live expires and/or when it changes). Those signatures are stored -//! as regular DNS data and automatically served by name servers. +//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of +//! a DNS record served by a security-aware name server. Signatures can be +//! made "online" (in an authoritative name server while it is running) or +//! "offline" (outside of a name server). Once generated, signatures can be +//! serialized as DNS records and stored alongside the authenticated records. #![cfg(feature = "sign")] #![cfg_attr(docsrs, doc(cfg(feature = "sign")))] -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, Signature}, +}; pub mod generic; -pub mod key; pub mod openssl; -pub mod records; pub mod ring; -/// Signing DNS records. +/// Sign DNS records. /// -/// Implementors of this trait own a private key and sign DNS records for a zone -/// with that key. Signing is a synchronous operation performed on the current -/// thread; this rules out implementations like HSMs, where I/O communication is -/// necessary. -pub trait Sign { - /// An error in constructing a signature. - type Error; - +/// Types that implement this trait own a private key and can sign arbitrary +/// 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()`] 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()`]: Sign::sign() +pub trait Sign { /// The signature algorithm used. /// - /// The following algorithms can be used: + /// The following algorithms are known to this crate. Recommendations + /// toward or against usage are based on published RFCs, not the crate + /// authors' opinion. Implementing types may choose to support some of + /// the prohibited algorithms anyway. + /// /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) /// - [`SecAlg::DSA`] (highly insecure, do not use) /// - [`SecAlg::RSASHA1`] (insecure, not recommended) @@ -47,11 +54,35 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// Compute a signature. + /// The public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn public_key(&self) -> PublicKey; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// There are three expected failure cases for this function: + /// + /// - The secret key was invalid. The implementing type is responsible + /// for validating the secret key during initialization, so that this + /// kind of error does not occur. + /// + /// - Not enough randomness could be obtained. This applies to signature + /// algorithms which use randomization (primarily ECDSA). On common + /// platforms like Linux, Mac OS, and Windows, cryptographically secure + /// pseudo-random number generation is provided by the OS, so this is + /// highly unlikely. + /// + /// - Not enough memory could be obtained. Signature generation does not + /// require significant memory and an out-of-memory condition means that + /// the application will probably panic soon. /// - /// A regular signature of the given byte sequence is computed and is turned - /// into the selected buffer type. This provides a lot of flexibility in - /// how buffers are constructed; they may be heap-allocated or have a static - /// size. - fn sign(&self, data: &[u8]) -> Result; + /// None of these are considered likely or recoverable, so panicking is + /// the simplest and most ergonomic solution. + fn sign(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 8faa48f9e..5c708f485 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,10 +1,7 @@ //! Key and Signer using OpenSSL. -#![cfg(feature = "openssl")] -#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] - use core::fmt; -use std::vec::Vec; +use std::boxed::Box; use openssl::{ bn::BigNum, @@ -12,7 +9,10 @@ use openssl::{ pkey::{self, PKey, Private}, }; -use crate::base::iana::SecAlg; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; use super::{generic, Sign}; @@ -31,25 +31,31 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, - ) -> Result { + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, + ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); v.copy_from_slice(slice).unwrap(); v } - let pkey = match &key { - generic::SecretKey::RsaSha256(k) => { - let n = BigNum::from_slice(k.n.as_ref()).unwrap(); - let e = BigNum::from_slice(k.e.as_ref()).unwrap(); - let d = num(k.d.as_ref()); - let p = num(k.p.as_ref()); - let q = num(k.q.as_ref()); - let d_p = num(k.d_p.as_ref()); - let d_q = num(k.d_q.as_ref()); - let q_i = num(k.q_i.as_ref()); + let pkey = match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + + let n = BigNum::from_slice(&s.n).unwrap(); + let e = BigNum::from_slice(&s.e).unwrap(); + let d = num(&s.d); + let p = num(&s.p); + let q = num(&s.q); + let d_p = num(&s.d_p); + let d_q = num(&s.d_q); + let q_i = num(&s.q_i); // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the @@ -61,47 +67,75 @@ impl SecretKey { .and_then(PKey::from_rsa) .unwrap() } - generic::SecretKey::EcdsaP256Sha256(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::EcdsaP384Sha384(k) => { - // Calculate the public key manually. - let ctx = openssl::bn::BigNumContext::new_secure().unwrap(); - let group = openssl::nid::Nid::SECP384R1; - let group = - openssl::ec::EcGroup::from_curve_name(group).unwrap(); - let mut p = openssl::ec::EcPoint::new(&group).unwrap(); - let n = num(k.as_slice()); - p.mul_generator(&group, &n, &ctx).unwrap(); - openssl::ec::EcKey::from_private_components(&group, &n, &p) - .and_then(PKey::from_ec_key) - .unwrap() + + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + use openssl::{bn, ec, nid}; + + let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let group = nid::Nid::SECP384R1; + let group = ec::EcGroup::from_curve_name(group).unwrap(); + let n = num(s.as_slice()); + let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) + .map_err(|_| FromGenericError::InvalidKey)?; + let k = ec::EcKey::from_private_components(&group, &n, &p) + .map_err(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromGenericError::InvalidKey)?; + PKey::from_ec_key(k).unwrap() } - generic::SecretKey::Ed25519(k) => { - PKey::private_key_from_raw_bytes( - k.as_ref(), - pkey::Id::ED25519, - ) - .unwrap() + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } - generic::SecretKey::Ed448(k) => { - PKey::private_key_from_raw_bytes(k.as_ref(), pkey::Id::ED448) - .unwrap() + + (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let k = PKey::private_key_from_raw_bytes(&**s, id) + .map_err(|_| FromGenericError::InvalidKey)?; + if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { + k + } else { + return Err(FromGenericError::InvalidKey); + } } + + // The public and private key types did not match. + _ => return Err(FromGenericError::InvalidKey), }; Ok(Self { - algorithm: key.algorithm(), + algorithm: secret.algorithm(), pkey, }) } @@ -111,10 +145,7 @@ impl SecretKey { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export(&self) -> generic::SecretKey - where - B: AsRef<[u8]> + AsMut<[u8]> + From>, - { + pub fn to_generic(&self) -> generic::SecretKey { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { @@ -151,20 +182,18 @@ impl SecretKey { _ => unreachable!(), } } +} - /// Export this key into a generic public key. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { +impl Sign for SecretKey { + fn algorithm(&self) -> SecAlg { + self.algorithm + } + + fn public_key(&self) -> PublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { + PublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -177,7 +206,7 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -187,65 +216,69 @@ impl SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - generic::PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + PublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - generic::PublicKey::Ed448(key.try_into().unwrap()) + PublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } -} - -impl Sign> for SecretKey { - type Error = openssl::error::ErrorStack; - fn algorithm(&self) -> SecAlg { - self.algorithm - } - - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn sign(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; match self.algorithm { SecAlg::RSASHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; - s.sign_oneshot_to_vec(data) + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + s.set_rsa_padding(openssl::rsa::Padding::PKCS1).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); + Signature::RsaSha256(signature.into_boxed_slice()) } SecAlg::ECDSAP256SHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(32).unwrap(); let s = signature.s().to_vec_padded(32).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 64]); + signature[..32].copy_from_slice(&r); + signature[32..].copy_from_slice(&s); + Signature::EcdsaP256Sha256(signature) } SecAlg::ECDSAP384SHA384 => { - let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; + let mut s = + Signer::new(MessageDigest::sha384(), &self.pkey).unwrap(); + let signature = s.sign_oneshot_to_vec(data).unwrap(); // Convert from DER to the fixed representation. let signature = EcdsaSig::from_der(&signature).unwrap(); let r = signature.r().to_vec_padded(48).unwrap(); let s = signature.s().to_vec_padded(48).unwrap(); - let mut signature = Vec::new(); - signature.extend_from_slice(&r); - signature.extend_from_slice(&s); - Ok(signature) + let mut signature = Box::new([0u8; 96]); + signature[..48].copy_from_slice(&r); + signature[48..].copy_from_slice(&s); + Signature::EcdsaP384Sha384(signature) + } + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed25519(signature.try_into().unwrap()) } - SecAlg::ED25519 | SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey).unwrap(); + let signature = + s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); + Signature::Ed448(signature.try_into().unwrap()) } _ => unreachable!(), } @@ -289,15 +322,15 @@ pub fn generate(algorithm: SecAlg) -> Option { /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, - /// The provided secret key was invalid. + /// The key's parameters were invalid. InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -306,18 +339,20 @@ impl fmt::Display for ImportError { } } -impl std::error::Error for ImportError {} +impl std::error::Error for FromGenericError {} #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 27096), (SecAlg::ECDSAP256SHA256, 40436), @@ -337,25 +372,32 @@ mod tests { fn generated_roundtrip() { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); - let exp: generic::SecretKey> = key.export(); - let imp = super::SecretKey::import(exp).unwrap(); - assert!(key.pkey.public_eq(&imp.pkey)); + let gen_key = key.to_generic(); + let pub_key = key.public_key(); + let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); } } #[test] fn imported_roundtrip() { - type GenericKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let imp = GenericKey::from_dns(&data).unwrap(); - let key = super::SecretKey::import(imp).unwrap(); - let exp: GenericKey = key.export(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + + let equiv = key.to_generic(); let mut same = String::new(); - exp.into_dns(&mut same).unwrap(); + equiv.format_as_bind(&mut same).unwrap(); + let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); @@ -363,48 +405,40 @@ mod tests { } #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let sec_key = super::SecretKey::import(sec_key).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 0996552f6..2a4867094 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,11 +4,16 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::vec::Vec; +use std::{boxed::Box, vec::Vec}; -use crate::base::iana::SecAlg; +use ring::signature::KeyPair; -use super::generic; +use crate::{ + base::iana::SecAlg, + validate::{PublicKey, RsaPublicKey, Signature}, +}; + +use super::{generic, Sign}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -18,71 +23,97 @@ pub enum SecretKey<'a> { rng: &'a dyn ring::rand::SecureRandom, }, + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + key: ring::signature::EcdsaKeyPair, + rng: &'a dyn ring::rand::SecureRandom, + }, + /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. - pub fn import + AsMut<[u8]>>( - key: generic::SecretKey, + pub fn from_generic( + secret: &generic::SecretKey, + public: &PublicKey, rng: &'a dyn ring::rand::SecureRandom, - ) -> Result { - match &key { - generic::SecretKey::RsaSha256(k) => { + ) -> Result { + match (secret, public) { + (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + // Ensure that the public and private key match. + if p != &RsaPublicKey::from(s) { + return Err(FromGenericError::InvalidKey); + } + let components = ring::rsa::KeyPairComponents { public_key: ring::rsa::PublicKeyComponents { - n: k.n.as_ref(), - e: k.e.as_ref(), + n: s.n.as_ref(), + e: s.e.as_ref(), }, - d: k.d.as_ref(), - p: k.p.as_ref(), - q: k.q.as_ref(), - dP: k.d_p.as_ref(), - dQ: k.d_q.as_ref(), - qInv: k.q_i.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(), }; ring::signature::RsaKeyPair::from_components(&components) - .map_err(|_| ImportError::InvalidKey) + .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } - // TODO: Support ECDSA. - generic::SecretKey::Ed25519(k) => { - let k = k.as_ref(); - ring::signature::Ed25519KeyPair::from_seed_unchecked(k) - .map_err(|_| ImportError::InvalidKey) - .map(Self::Ed25519) + + ( + generic::SecretKey::EcdsaP256Sha256(s), + PublicKey::EcdsaP256Sha256(p), + ) => { + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) } - _ => Err(ImportError::UnsupportedAlgorithm), - } - } - /// Export this key into a generic public key. - pub fn export_public(&self) -> generic::PublicKey - where - B: AsRef<[u8]> + From>, - { - match self { - Self::RsaSha256 { key, rng: _ } => { - let components: ring::rsa::PublicKeyComponents> = - key.public().into(); - generic::PublicKey::RsaSha256(generic::RsaPublicKey { - n: components.n.into(), - e: components.e.into(), - }) + ( + generic::SecretKey::EcdsaP384Sha384(s), + PublicKey::EcdsaP384Sha384(p), + ) => { + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + ring::signature::EcdsaKeyPair::from_private_key_and_public_key( + alg, s.as_slice(), p.as_slice(), rng) + .map_err(|_| FromGenericError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - Self::Ed25519(key) => { - use ring::signature::KeyPair; - let key = key.public_key().as_ref(); - generic::PublicKey::Ed25519(key.try_into().unwrap()) + + (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + ring::signature::Ed25519KeyPair::from_seed_and_public_key( + s.as_slice(), + p.as_slice(), + ) + .map_err(|_| FromGenericError::InvalidKey) + .map(Self::Ed25519) } + + (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + Err(FromGenericError::UnsupportedAlgorithm) + } + + // The public and private key types did not match. + _ => Err(FromGenericError::InvalidKey), } } } /// An error in importing a key into `ring`. #[derive(Clone, Debug)] -pub enum ImportError { +pub enum FromGenericError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -90,7 +121,7 @@ pub enum ImportError { InvalidKey, } -impl fmt::Display for ImportError { +impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -99,87 +130,135 @@ impl fmt::Display for ImportError { } } -impl<'a> super::Sign> for SecretKey<'a> { - type Error = ring::error::Unspecified; - +impl<'a> Sign for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, Self::Ed25519(_) => SecAlg::ED25519, } } - fn sign(&self, data: &[u8]) -> Result, Self::Error> { + fn public_key(&self) -> PublicKey { + match self { + Self::RsaSha256 { key, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + PublicKey::RsaSha256(RsaPublicKey { + n: components.n.into(), + e: components.e.into(), + }) + } + + Self::EcdsaP256Sha256 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + } + + Self::EcdsaP384Sha384 { key, rng: _ } => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + } + + Self::Ed25519(key) => { + let key = key.public_key().as_ref(); + let key = Box::<[u8]>::from(key); + PublicKey::Ed25519(key.try_into().unwrap()) + } + } + } + + fn sign(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf)?; - Ok(buf) + key.sign(pad, *rng, data, &mut buf) + .expect("random generators do not fail"); + Signature::RsaSha256(buf.into_boxed_slice()) + } + Self::EcdsaP256Sha256 { key, rng } => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP256Sha256(buf) + } + Self::EcdsaP384Sha384 { key, rng } => { + let mut buf = Box::new([0u8; 96]); + buf.copy_from_slice( + key.sign(*rng, data) + .expect("random generators do not fail") + .as_ref(), + ); + Signature::EcdsaP384Sha384(buf) + } + Self::Ed25519(key) => { + let mut buf = Box::new([0u8; 64]); + buf.copy_from_slice(key.sign(data).as_ref()); + Signature::Ed25519(buf) } - Self::Ed25519(key) => Ok(key.sign(data).as_ref().to_vec()), } } } #[cfg(test)] mod tests { - use std::vec::Vec; - use crate::{ - base::{iana::SecAlg, scan::IterScanner}, - rdata::Dnskey, + base::iana::SecAlg, sign::{generic, Sign}, + validate::PublicKey, }; + use super::SecretKey; + const KEYS: &[(SecAlg, u16)] = &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; #[test] - fn export_public() { - type GenericSecretKey = generic::SecretKey>; - type GenericPublicKey = generic::PublicKey>; - + fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); - let pub_key: GenericPublicKey = sec_key.export_public(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); - let mut data = std::fs::read_to_string(path).unwrap(); - // Remove a trailing comment, if any. - if let Some(pos) = data.bytes().position(|b| b == b';') { - data.truncate(pos); - } - // Skip ' ' - let data = data.split_ascii_whitespace().skip(3); - let mut data = IterScanner::new(data); - let dns_key: Dnskey> = Dnskey::scan(&mut data).unwrap(); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); - assert_eq!(dns_key.key_tag(), key_tag); - assert_eq!(pub_key.into_dns::>(256), dns_key) + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + + assert_eq!(key.public_key(), pub_key); } } #[test] fn sign() { - type GenericSecretKey = generic::SecretKey>; - for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let rng = ring::rand::SystemRandom::new(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let sec_key = GenericSecretKey::from_dns(&data).unwrap(); - let rng = ring::rand::SystemRandom::new(); - let sec_key = super::SecretKey::import(sec_key, &rng).unwrap(); + let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + + let key = + SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = sec_key.sign(b"Hello, World!").unwrap(); + let _ = key.sign(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index 41b7456e5..b122c83c9 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -10,14 +10,361 @@ use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; +use crate::base::scan::IterScanner; use crate::base::wire::{Compose, Composer}; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use ring::{digest, signature}; +use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +/// A generic public key. +#[derive(Clone, Debug)] +pub enum PublicKey { + /// An RSA/SHA-1 public key. + RsaSha1(RsaPublicKey), + + /// An RSA/SHA-1 with NSEC3 public key. + RsaSha1Nsec3Sha1(RsaPublicKey), + + /// An RSA/SHA-256 public key. + RsaSha256(RsaPublicKey), + + /// An RSA/SHA-512 public key. + RsaSha512(RsaPublicKey), + + /// An ECDSA P-256/SHA-256 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (32 bytes). + /// - The encoding of the `y` coordinate (32 bytes). + EcdsaP256Sha256(Box<[u8; 65]>), + + /// An ECDSA P-384/SHA-384 public key. + /// + /// The public key is stored in uncompressed format: + /// + /// - A single byte containing the value 0x04. + /// - The encoding of the `x` coordinate (48 bytes). + /// - The encoding of the `y` coordinate (48 bytes). + EcdsaP384Sha384(Box<[u8; 97]>), + + /// An Ed25519 public key. + /// + /// The public key is a 32-byte encoding of the public point. + Ed25519(Box<[u8; 32]>), + + /// An Ed448 public key. + /// + /// The public key is a 57-byte encoding of the public point. + Ed448(Box<[u8; 57]>), +} + +impl PublicKey { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl PublicKey { + /// Parse a public key as stored in a DNSKEY record. + pub fn from_dnskey( + algorithm: SecAlg, + data: &[u8], + ) -> Result { + match algorithm { + SecAlg::RSASHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + } + SecAlg::RSASHA1_NSEC3_SHA1 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + } + SecAlg::RSASHA256 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + } + SecAlg::RSASHA512 => { + RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + } + + SecAlg::ECDSAP256SHA256 => { + let mut key = Box::new([0u8; 65]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP256Sha256(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + SecAlg::ECDSAP384SHA384 => { + let mut key = Box::new([0u8; 97]); + if key.len() == 1 + data.len() { + key[0] = 0x04; + key[1..].copy_from_slice(data); + Ok(Self::EcdsaP384Sha384(key)) + } else { + Err(FromDnskeyError::InvalidKey) + } + } + + SecAlg::ED25519 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed25519) + .map_err(|_| FromDnskeyError::InvalidKey), + SecAlg::ED448 => Box::<[u8]>::from(data) + .try_into() + .map(Self::Ed448) + .map_err(|_| FromDnskeyError::InvalidKey), + + _ => Err(FromDnskeyError::UnsupportedAlgorithm), + } + } + + /// Parse a public key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn from_dnskey_text( + dnskey: &str, + ) -> Result { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(FromDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Ensure the record header looks reasonable. + let mut words = line.split_ascii_whitespace().skip(2); + if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { + return Err(FromDnskeyTextError::Misformatted); + } + + // Parse the DNSKEY record data. + let mut data = IterScanner::new(words); + let dnskey: Dnskey> = Dnskey::scan(&mut data) + .map_err(|_| FromDnskeyTextError::Misformatted)?; + println!("importing {:?}", dnskey); + Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) + .map_err(FromDnskeyTextError::FromDnskey) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.to_dnskey(), + + // From my reading of RFC 6605, the marker byte is not included. + Self::EcdsaP256Sha256(k) => k[1..].into(), + Self::EcdsaP384Sha384(k) => k[1..].into(), + + Self::Ed25519(k) => k.as_slice().into(), + Self::Ed448(k) => k.as_slice().into(), + } + } +} + +impl PartialEq for PublicKey { + fn eq(&self, other: &Self) -> bool { + use ring::constant_time::verify_slices_are_equal; + + match (self, other) { + (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, + (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, + (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, + (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, + (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed25519(a), Self::Ed25519(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + (Self::Ed448(a), Self::Ed448(b)) => { + verify_slices_are_equal(&**a, &**b).is_ok() + } + _ => false, + } + } +} + +/// A generic RSA public key. +/// +/// All fields here are arbitrary-precision integers in big-endian format, +/// without any leading zero bytes. +#[derive(Clone, Debug)] +pub struct RsaPublicKey { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, +} + +impl RsaPublicKey { + /// Parse an RSA public key as stored in a DNSKEY record. + pub fn from_dnskey(data: &[u8]) -> Result { + if data.len() < 3 { + return Err(FromDnskeyError::InvalidKey); + } + + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else if data[1..3] != [0, 0] { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + } else { + // The extended encoding of the length just held a zero value. + return Err(FromDnskeyError::InvalidKey); + }; + + // NOTE: off <= 3 so is safe to index up to. + let e = data[off..] + .get(..exp_len) + .ok_or(FromDnskeyError::InvalidKey)? + .into(); + + // NOTE: The previous statement indexed up to 'exp_len'. + let n = data[off + exp_len..].into(); + + Ok(Self { n, e }) + } + + /// Serialize this public key as stored in a DNSKEY record. + pub fn to_dnskey(&self) -> Box<[u8]> { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + key.reserve_exact(1 + self.e.len() + self.n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + key.reserve_exact(3 + self.e.len() + self.n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(&*self.e); + key.extend(&*self.n); + key.into_boxed_slice() + } +} + +impl PartialEq for RsaPublicKey { + fn eq(&self, other: &Self) -> bool { + /// Compare after stripping leading zeros. + fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { + let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; + let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; + if a.len() == b.len() { + ring::constant_time::verify_slices_are_equal(a, b).is_ok() + } else { + false + } + } + + cmp_without_leading(&self.n, &other.n) + && cmp_without_leading(&self.e, &other.e) + } +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +#[derive(Clone, Debug)] +pub enum FromDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +pub enum Signature { + RsaSha1(Box<[u8]>), + RsaSha1Nsec3Sha1(Box<[u8]>), + RsaSha256(Box<[u8]>), + RsaSha512(Box<[u8]>), + EcdsaP256Sha256(Box<[u8; 64]>), + EcdsaP384Sha384(Box<[u8; 96]>), + Ed25519(Box<[u8; 64]>), + Ed448(Box<[u8; 114]>), +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From 824c8e3256783b310839437ac22fea6c6518ce94 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 09:49:40 +0200 Subject: [PATCH 145/569] Move 'sign' and 'validate' to unstable feature gates --- Cargo.toml | 6 +++--- src/lib.rs | 16 ++++++++-------- src/sign/mod.rs | 4 ++-- src/validate.rs | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7efdc389d..29102648a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,19 +53,19 @@ heapless = ["dep:heapless", "octseq/heapless"] resolv = ["net", "smallvec", "unstable-client-transport"] resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] -sign = ["std", "validate", "dep:openssl"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] net = ["bytes", "futures-util", "rand", "std", "tokio"] tsig = ["bytes", "ring", "smallvec"] -validate = ["bytes", "std", "ring"] 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", "dep:openssl"] unstable-stelline = ["tokio/test-util", "tracing", "tracing-subscriber", "tsig", "unstable-client-transport", "unstable-server-transport", "zonefile"] -unstable-validator = ["validate", "zonefile", "unstable-client-transport"] +unstable-validate = ["bytes", "std", "ring"] +unstable-validator = ["unstable-validate", "zonefile", "unstable-client-transport"] unstable-xfr = ["net"] unstable-zonetree = ["futures-util", "parking_lot", "rustversion", "serde", "std", "tokio", "tracing", "unstable-xfr", "zonefile"] diff --git a/src/lib.rs b/src/lib.rs index 6d6cfd344..119adc66f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -36,14 +36,14 @@ #![cfg_attr(not(feature = "resolv"), doc = "* resolv:")] //! An asynchronous DNS resolver based on the //! [Tokio](https://tokio.rs/) async runtime. -#![cfg_attr(feature = "sign", doc = "* [sign]:")] -#![cfg_attr(not(feature = "sign"), doc = "* sign:")] +#![cfg_attr(feature = "unstable-sign", doc = "* [sign]:")] +#![cfg_attr(not(feature = "unstable-sign"), doc = "* sign:")] //! Experimental support for DNSSEC signing. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig"), doc = "* tsig:")] //! Support for securing DNS transactions with TSIG records. -#![cfg_attr(feature = "validate", doc = "* [validate]:")] -#![cfg_attr(not(feature = "validate"), doc = "* validate:")] +#![cfg_attr(feature = "unstable-validate", doc = "* [validate]:")] +#![cfg_attr(not(feature = "unstable-validate"), doc = "* validate:")] //! Experimental support for DNSSEC validation. #![cfg_attr(feature = "unstable-validator", doc = "* [validator]:")] #![cfg_attr(not(feature = "unstable-validator"), doc = "* validator:")] @@ -86,8 +86,8 @@ //! [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 = "sign", doc = " [sign]")] -#![cfg_attr(not(feature = "sign"), doc = " sign")] +#![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` @@ -108,8 +108,8 @@ //! module and currently pulls in the //! `bytes`, `ring`, and `smallvec` features. //! * `validate`: basic DNSSEC validation support. This feature enables the -#![cfg_attr(feature = "validate", doc = " [validate]")] -#![cfg_attr(not(feature = "validate"), doc = " validate")] +#![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. //! * `zonefile`: reading and writing of zonefiles. This feature enables the diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b9773d7f0..7a96230e3 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -8,8 +8,8 @@ //! "offline" (outside of a name server). Once generated, signatures can be //! serialized as DNS records and stored alongside the authenticated records. -#![cfg(feature = "sign")] -#![cfg_attr(docsrs, doc(cfg(feature = "sign")))] +#![cfg(feature = "unstable-sign")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] use crate::{ base::iana::SecAlg, diff --git a/src/validate.rs b/src/validate.rs index b122c83c9..eb162df8d 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,8 +1,8 @@ //! DNSSEC validation. //! //! **This module is experimental and likely to change significantly.** -#![cfg(feature = "validate")] -#![cfg_attr(docsrs, doc(cfg(feature = "validate")))] +#![cfg(feature = "unstable-validate")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] use crate::base::cmp::CanonicalOrd; use crate::base::iana::{DigestAlg, SecAlg}; From 6d8c29ead85b33a58d7a5290ee080250a22afe3a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 09:54:57 +0200 Subject: [PATCH 146/569] [workflows/ci] Document the vcpkg env vars --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 299da6658..cbad43917 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,8 +10,10 @@ jobs: rust: [1.76.0, stable, beta, nightly] env: RUSTFLAGS: "-D warnings" + # We use 'vcpkg' to install OpenSSL on Windows. VCPKG_ROOT: "${{ github.workspace }}\\vcpkg" VCPKGRS_TRIPLET: x64-windows-release + # Ensure that OpenSSL is dynamically linked. VCPKGRS_DYNAMIC: 1 steps: - name: Checkout repository From 82a05aa7919eb2c5160f9331816eb94dc039765a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:05:27 +0200 Subject: [PATCH 147/569] Rename public/secret key interfaces to '*Raw*' This makes space for higher-level interfaces which track DNSKEY flags information (and possibly key rollover information). --- src/sign/generic.rs | 10 ++++----- src/sign/mod.rs | 18 ++++++++-------- src/sign/openssl.rs | 51 ++++++++++++++++++++++++--------------------- src/sign/ring.rs | 45 ++++++++++++++++++++------------------- src/validate.rs | 10 ++++----- 5 files changed, 69 insertions(+), 65 deletions(-) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 2589a6ab4..f7caaa5a0 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -9,12 +9,10 @@ use crate::validate::RsaPublicKey; /// A generic secret key. /// -/// This type cannot be used for computing signatures, as it does not implement -/// any cryptographic primitives. Instead, it is a generic representation that -/// can be imported/exported or converted into a [`Sign`] (if the underlying -/// cryptographic implementation supports it). -/// -/// [`Sign`]: super::Sign +/// This is a low-level generic representation of a secret key from any one of +/// the commonly supported signature algorithms. It is useful for abstracting +/// over most cryptographic implementations, and it provides functionality for +/// importing and exporting keys from and to the disk. /// /// # Serialization /// diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 7a96230e3..6f31e7887 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -13,26 +13,26 @@ use crate::{ base::iana::SecAlg, - validate::{PublicKey, Signature}, + validate::{RawPublicKey, Signature}, }; pub mod generic; pub mod openssl; pub mod ring; -/// Sign DNS records. +/// Low-level signing functionality. /// /// Types that implement this trait own a private key and can sign arbitrary /// 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()`] to be called on unvalidated keys, it -/// will have to check the validity of the key for every signature; this is +/// 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()`]: Sign::sign() -pub trait Sign { +/// [`sign_raw()`]: SignRaw::sign_raw() +pub trait SignRaw { /// The signature algorithm used. /// /// The following algorithms are known to this crate. Recommendations @@ -54,13 +54,13 @@ pub trait Sign { /// - [`SecAlg::ED448`] fn algorithm(&self) -> SecAlg; - /// The public key. + /// The raw public key. /// /// This can be used to verify produced signatures. It must use the same /// algorithm as returned by [`algorithm()`]. /// /// [`algorithm()`]: Self::algorithm() - fn public_key(&self) -> PublicKey; + fn raw_public_key(&self) -> RawPublicKey; /// Sign the given bytes. /// @@ -84,5 +84,5 @@ pub trait Sign { /// /// None of these are considered likely or recoverable, so panicking is /// the simplest and most ergonomic solution. - fn sign(&self, data: &[u8]) -> Signature; + fn sign_raw(&self, data: &[u8]) -> Signature; } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 5c708f485..990e1c37e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -11,10 +11,10 @@ use openssl::{ use crate::{ base::iana::SecAlg, - validate::{PublicKey, RsaPublicKey, Signature}, + validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, Sign}; +use super::{generic, SignRaw}; /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -33,7 +33,7 @@ impl SecretKey { /// Panics if OpenSSL fails or if memory could not be allocated. pub fn from_generic( secret: &generic::SecretKey, - public: &PublicKey, + public: &RawPublicKey, ) -> Result { fn num(slice: &[u8]) -> BigNum { let mut v = BigNum::new_secure().unwrap(); @@ -42,7 +42,10 @@ impl SecretKey { } let pkey = match (secret, public) { - (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + ( + generic::SecretKey::RsaSha256(s), + RawPublicKey::RsaSha256(p), + ) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromGenericError::InvalidKey); @@ -70,7 +73,7 @@ impl SecretKey { ( generic::SecretKey::EcdsaP256Sha256(s), - PublicKey::EcdsaP256Sha256(p), + RawPublicKey::EcdsaP256Sha256(p), ) => { use openssl::{bn, ec, nid}; @@ -88,7 +91,7 @@ impl SecretKey { ( generic::SecretKey::EcdsaP384Sha384(s), - PublicKey::EcdsaP384Sha384(p), + RawPublicKey::EcdsaP384Sha384(p), ) => { use openssl::{bn, ec, nid}; @@ -104,7 +107,7 @@ impl SecretKey { PKey::from_ec_key(k).unwrap() } - (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; @@ -117,7 +120,7 @@ impl SecretKey { } } - (generic::SecretKey::Ed448(s), PublicKey::Ed448(p)) => { + (generic::SecretKey::Ed448(s), RawPublicKey::Ed448(p)) => { use openssl::memcmp; let id = pkey::Id::ED448; @@ -184,16 +187,16 @@ impl SecretKey { } } -impl Sign for SecretKey { +impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { self.algorithm } - fn public_key(&self) -> PublicKey { + fn raw_public_key(&self) -> RawPublicKey { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - PublicKey::RsaSha256(RsaPublicKey { + RawPublicKey::RsaSha256(RsaPublicKey { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -206,7 +209,7 @@ impl Sign for SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -216,21 +219,21 @@ impl Sign for SecretKey { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - PublicKey::Ed25519(key.try_into().unwrap()) + RawPublicKey::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - PublicKey::Ed448(key.try_into().unwrap()) + RawPublicKey::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } } - fn sign(&self, data: &[u8]) -> Signature { + fn sign_raw(&self, data: &[u8]) -> Signature { use openssl::hash::MessageDigest; use openssl::sign::Signer; @@ -347,8 +350,8 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, Sign}, - validate::PublicKey, + sign::{generic, SignRaw}, + validate::RawPublicKey, }; use super::SecretKey; @@ -373,7 +376,7 @@ mod tests { for &(algorithm, _) in KEYS { let key = super::generate(algorithm).unwrap(); let gen_key = key.to_generic(); - let pub_key = key.public_key(); + let pub_key = key.raw_public_key(); let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } @@ -386,7 +389,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -415,11 +418,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - assert_eq!(key.public_key(), pub_key); + assert_eq!(key.raw_public_key(), pub_key); } } @@ -434,11 +437,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); - let _ = key.sign(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!"); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 2a4867094..051861539 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -10,10 +10,10 @@ use ring::signature::KeyPair; use crate::{ base::iana::SecAlg, - validate::{PublicKey, RsaPublicKey, Signature}, + validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, Sign}; +use super::{generic, SignRaw}; /// A key pair backed by `ring`. pub enum SecretKey<'a> { @@ -43,11 +43,14 @@ impl<'a> SecretKey<'a> { /// Use a generic keypair with `ring`. pub fn from_generic( secret: &generic::SecretKey, - public: &PublicKey, + public: &RawPublicKey, rng: &'a dyn ring::rand::SecureRandom, ) -> Result { match (secret, public) { - (generic::SecretKey::RsaSha256(s), PublicKey::RsaSha256(p)) => { + ( + generic::SecretKey::RsaSha256(s), + RawPublicKey::RsaSha256(p), + ) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromGenericError::InvalidKey); @@ -72,7 +75,7 @@ impl<'a> SecretKey<'a> { ( generic::SecretKey::EcdsaP256Sha256(s), - PublicKey::EcdsaP256Sha256(p), + RawPublicKey::EcdsaP256Sha256(p), ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( @@ -83,7 +86,7 @@ impl<'a> SecretKey<'a> { ( generic::SecretKey::EcdsaP384Sha384(s), - PublicKey::EcdsaP384Sha384(p), + RawPublicKey::EcdsaP384Sha384(p), ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( @@ -92,7 +95,7 @@ impl<'a> SecretKey<'a> { .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - (generic::SecretKey::Ed25519(s), PublicKey::Ed25519(p)) => { + (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { ring::signature::Ed25519KeyPair::from_seed_and_public_key( s.as_slice(), p.as_slice(), @@ -101,7 +104,7 @@ impl<'a> SecretKey<'a> { .map(Self::Ed25519) } - (generic::SecretKey::Ed448(_), PublicKey::Ed448(_)) => { + (generic::SecretKey::Ed448(_), RawPublicKey::Ed448(_)) => { Err(FromGenericError::UnsupportedAlgorithm) } @@ -130,7 +133,7 @@ impl fmt::Display for FromGenericError { } } -impl<'a> Sign for SecretKey<'a> { +impl<'a> SignRaw for SecretKey<'a> { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, @@ -140,12 +143,12 @@ impl<'a> Sign for SecretKey<'a> { } } - fn public_key(&self) -> PublicKey { + fn raw_public_key(&self) -> RawPublicKey { match self { Self::RsaSha256 { key, rng: _ } => { let components: ring::rsa::PublicKeyComponents> = key.public().into(); - PublicKey::RsaSha256(RsaPublicKey { + RawPublicKey::RsaSha256(RsaPublicKey { n: components.n.into(), e: components.e.into(), }) @@ -154,24 +157,24 @@ impl<'a> Sign for SecretKey<'a> { Self::EcdsaP256Sha256 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) } Self::EcdsaP384Sha384 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) } Self::Ed25519(key) => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - PublicKey::Ed25519(key.try_into().unwrap()) + RawPublicKey::Ed25519(key.try_into().unwrap()) } } } - fn sign(&self, data: &[u8]) -> Signature { + fn sign_raw(&self, data: &[u8]) -> Signature { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; @@ -211,8 +214,8 @@ impl<'a> Sign for SecretKey<'a> { mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, Sign}, - validate::PublicKey, + sign::{generic, SignRaw}, + validate::RawPublicKey, }; use super::SecretKey; @@ -232,12 +235,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - assert_eq!(key.public_key(), pub_key); + assert_eq!(key.raw_public_key(), pub_key); } } @@ -253,12 +256,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = PublicKey::from_dnskey_text(&data).unwrap(); + let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); - let _ = key.sign(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!"); } } } diff --git a/src/validate.rs b/src/validate.rs index eb162df8d..2360ee3c8 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -22,7 +22,7 @@ use std::{error, fmt}; /// A generic public key. #[derive(Clone, Debug)] -pub enum PublicKey { +pub enum RawPublicKey { /// An RSA/SHA-1 public key. RsaSha1(RsaPublicKey), @@ -64,7 +64,7 @@ pub enum PublicKey { Ed448(Box<[u8; 57]>), } -impl PublicKey { +impl RawPublicKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -80,7 +80,7 @@ impl PublicKey { } } -impl PublicKey { +impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. pub fn from_dnskey( algorithm: SecAlg, @@ -161,7 +161,7 @@ impl PublicKey { /// [`to_dnskey()`]: Self::to_dnskey() /// /// The `` is any text starting with an ASCII semicolon. - pub fn from_dnskey_text( + pub fn parse_dnskey_text( dnskey: &str, ) -> Result { // Ensure there is a single line in the input. @@ -206,7 +206,7 @@ impl PublicKey { } } -impl PartialEq for PublicKey { +impl PartialEq for RawPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; From 980fe5a355b516e3191c85fb00b2902a06eb5d7a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:21:33 +0200 Subject: [PATCH 148/569] [sign/ring] Store the RNG in an 'Arc' --- src/sign/ring.rs | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 051861539..977db8588 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -4,7 +4,7 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; -use std::{boxed::Box, vec::Vec}; +use std::{boxed::Box, sync::Arc, vec::Vec}; use ring::signature::KeyPair; @@ -16,35 +16,35 @@ use crate::{ use super::{generic, SignRaw}; /// A key pair backed by `ring`. -pub enum SecretKey<'a> { +pub enum SecretKey { /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256 { key: ring::signature::EcdsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An ECDSA P-384/SHA-384 keypair. EcdsaP384Sha384 { key: ring::signature::EcdsaKeyPair, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, }, /// An Ed25519 keypair. Ed25519(ring::signature::Ed25519KeyPair), } -impl<'a> SecretKey<'a> { +impl SecretKey { /// Use a generic keypair with `ring`. pub fn from_generic( secret: &generic::SecretKey, public: &RawPublicKey, - rng: &'a dyn ring::rand::SecureRandom, + rng: Arc, ) -> Result { match (secret, public) { ( @@ -79,7 +79,7 @@ impl<'a> SecretKey<'a> { ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), rng) + alg, s.as_slice(), p.as_slice(), &*rng) .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::EcdsaP256Sha256 { key, rng }) } @@ -90,7 +90,7 @@ impl<'a> SecretKey<'a> { ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), rng) + alg, s.as_slice(), p.as_slice(), &*rng) .map_err(|_| FromGenericError::InvalidKey) .map(|key| Self::EcdsaP384Sha384 { key, rng }) } @@ -133,7 +133,7 @@ impl fmt::Display for FromGenericError { } } -impl<'a> SignRaw for SecretKey<'a> { +impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, @@ -179,14 +179,14 @@ impl<'a> SignRaw for SecretKey<'a> { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, *rng, data, &mut buf) + key.sign(pad, &**rng, data, &mut buf) .expect("random generators do not fail"); Signature::RsaSha256(buf.into_boxed_slice()) } Self::EcdsaP256Sha256 { key, rng } => { let mut buf = Box::new([0u8; 64]); buf.copy_from_slice( - key.sign(*rng, data) + key.sign(&**rng, data) .expect("random generators do not fail") .as_ref(), ); @@ -195,7 +195,7 @@ impl<'a> SignRaw for SecretKey<'a> { Self::EcdsaP384Sha384 { key, rng } => { let mut buf = Box::new([0u8; 96]); buf.copy_from_slice( - key.sign(*rng, data) + key.sign(&**rng, data) .expect("random generators do not fail") .as_ref(), ); @@ -212,6 +212,8 @@ impl<'a> SignRaw for SecretKey<'a> { #[cfg(test)] mod tests { + use std::sync::Arc; + use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, @@ -227,7 +229,7 @@ mod tests { fn public_key() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); - let rng = ring::rand::SystemRandom::new(); + let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -238,7 +240,7 @@ mod tests { let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = - SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); assert_eq!(key.raw_public_key(), pub_key); } @@ -248,7 +250,7 @@ mod tests { fn sign() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); - let rng = ring::rand::SystemRandom::new(); + let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -259,7 +261,7 @@ mod tests { let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); let key = - SecretKey::from_generic(&gen_key, &pub_key, &rng).unwrap(); + SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } From 35ff06c36550eafdd612e4b090f1dc36c794f4fa Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 10:27:13 +0200 Subject: [PATCH 149/569] [validate] Enhance 'Signature' API --- src/validate.rs | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/validate.rs b/src/validate.rs index 2360ee3c8..b584a982a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -354,6 +354,7 @@ pub enum FromDnskeyTextError { /// that are encoded into bytes. /// /// Signatures are too big to pass by value, so they are placed on the heap. +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Signature { RsaSha1(Box<[u8]>), RsaSha1Nsec3Sha1(Box<[u8]>), @@ -365,6 +366,52 @@ pub enum Signature { Ed448(Box<[u8; 114]>), } +impl Signature { + /// The algorithm used to make the signature. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha1(_) => SecAlg::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, + Self::RsaSha256(_) => SecAlg::RSASHA256, + Self::RsaSha512(_) => SecAlg::RSASHA512, + Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, + Self::Ed25519(_) => SecAlg::ED25519, + Self::Ed448(_) => SecAlg::ED448, + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match self { + Self::RsaSha1(s) + | Self::RsaSha1Nsec3Sha1(s) + | Self::RsaSha256(s) + | Self::RsaSha512(s) => s, + Self::EcdsaP256Sha256(s) => &**s, + Self::EcdsaP384Sha384(s) => &**s, + Self::Ed25519(s) => &**s, + Self::Ed448(s) => &**s, + } + } +} + +impl From for Box<[u8]> { + fn from(value: Signature) -> Self { + match value { + Signature::RsaSha1(s) + | Signature::RsaSha1Nsec3Sha1(s) + | Signature::RsaSha256(s) + | Signature::RsaSha512(s) => s, + Signature::EcdsaP256Sha256(s) => s as _, + Signature::EcdsaP384Sha384(s) => s as _, + Signature::Ed25519(s) => s as _, + Signature::Ed448(s) => s as _, + } + } +} + //------------ Dnskey -------------------------------------------------------- /// Extensions for DNSKEY record type. From 95cc462aca08c8e8fe340b031e2d5fe3e3f93d88 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 11:40:21 +0200 Subject: [PATCH 150/569] [validate] Add high-level 'Key' type --- src/sign/openssl.rs | 19 ++-- src/sign/ring.rs | 16 +-- src/validate.rs | 271 +++++++++++++++++++++++++++++++------------- 3 files changed, 212 insertions(+), 94 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 990e1c37e..46553dbad 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -351,7 +351,7 @@ mod tests { use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, - validate::RawPublicKey, + validate::Key, }; use super::SecretKey; @@ -389,13 +389,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); let equiv = key.to_generic(); let mut same = String::new(); @@ -418,11 +419,12 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); - assert_eq!(key.raw_public_key(), pub_key); + assert_eq!(key.raw_public_key(), *pub_key); } } @@ -437,9 +439,10 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 977db8588..e0be1943a 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -212,12 +212,12 @@ impl SignRaw for SecretKey { #[cfg(test)] mod tests { - use std::sync::Arc; + use std::{sync::Arc, vec::Vec}; use crate::{ base::iana::SecAlg, sign::{generic, SignRaw}, - validate::RawPublicKey, + validate::Key, }; use super::SecretKey; @@ -237,12 +237,13 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let key = - SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); + SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); - assert_eq!(key.raw_public_key(), pub_key); + assert_eq!(key.raw_public_key(), *pub_key); } } @@ -258,10 +259,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = RawPublicKey::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = pub_key.raw_public_key(); let key = - SecretKey::from_generic(&gen_key, &pub_key, rng).unwrap(); + SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!"); } diff --git a/src/validate.rs b/src/validate.rs index b584a982a..b040acf9b 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -5,22 +5,197 @@ #![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlg, SecAlg}; +use crate::base::iana::{Class, DigestAlg, SecAlg}; use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; -use crate::base::scan::IterScanner; +use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; +use crate::base::Rtype; use crate::rdata::{Dnskey, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; +use octseq::{EmptyBuilder, FromBuilder}; use ring::{digest, signature}; use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; -/// A generic public key. +/// A DNSSEC key for a particular zone. +#[derive(Clone)] +pub struct Key { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The raw public key. + /// + /// This identifies the key and can be used for signatures. + key: RawPublicKey, +} + +impl Key { + /// Construct a new DNSSEC key manually. + pub fn new(owner: Name, flags: u16, key: RawPublicKey) -> Self { + Self { owner, flags, key } + } + + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw public key. + pub fn raw_public_key(&self) -> &RawPublicKey { + &self.key + } + + /// Whether this is a zone signing key. + /// + /// From RFC 4034, section 2.1.1: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From RFC 4034, section 2.1.1: + /// + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + pub fn is_secure_entry_point(&self) -> bool { + self.flags & (1 << 15) != 0 + } +} + +impl> Key { + /// Deserialize a key from DNSKEY record data. + /// + /// # Errors + /// + /// Fails if the DNSKEY uses an unknown protocol or contains an invalid + /// public key (e.g. one of the wrong size for the signature algorithm). + pub fn from_dnskey( + owner: Name, + dnskey: Dnskey, + ) -> Result { + if dnskey.protocol() != 3 { + return Err(FromDnskeyError::UnsupportedProtocol); + } + + let flags = dnskey.flags(); + let algorithm = dnskey.algorithm(); + let key = dnskey.public_key().as_ref(); + let key = RawPublicKey::from_dnskey_format(algorithm, key)?; + Ok(Self { owner, flags, key }) + } + + /// Parse a DNSSEC key from a DNSKEY record in presentation format. + /// + /// This format is popularized for storing alongside private keys by the + /// BIND name server. This function is convenient for loading such keys. + /// + /// The text should consist of a single line of the following format (each + /// field is separated by a non-zero number of ASCII spaces): + /// + /// ```text + /// DNSKEY [] + /// ``` + /// + /// Where `` consists of the following fields: + /// + /// ```text + /// + /// ``` + /// + /// The first three fields are simple integers, while the last field is + /// Base64 encoded data (with or without padding). The [`from_dnskey()`] + /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data + /// format. + /// + /// [`from_dnskey()`]: Self::from_dnskey() + /// [`to_dnskey()`]: Self::to_dnskey() + /// + /// The `` is any text starting with an ASCII semicolon. + pub fn parse_dnskey_text( + dnskey: &str, + ) -> Result + where + Octs: FromBuilder, + Octs::Builder: EmptyBuilder + Composer, + { + // Ensure there is a single line in the input. + let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); + if !rest.trim().is_empty() { + return Err(ParseDnskeyTextError::Misformatted); + } + + // Strip away any semicolon from the line. + let (line, _) = line.split_once(';').unwrap_or((line, "")); + + // Parse the entire record. + let mut scanner = IterScanner::new(line.split_ascii_whitespace()); + + let name = scanner + .scan_name() + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + let _ = Class::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { + return Err(ParseDnskeyTextError::Misformatted); + } + + let data = Dnskey::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; + + Self::from_dnskey(name, data) + .map_err(ParseDnskeyTextError::FromDnskey) + } + + /// Serialize the key into DNSKEY record data. + /// + /// The owner name can be combined with the returned record to serialize a + /// complete DNS record if necessary. + pub fn to_dnskey(&self) -> Dnskey> { + Dnskey::new( + self.flags, + 3, + self.key.algorithm(), + self.key.to_dnskey_format(), + ) + .expect("long public key") + } +} + +/// A low-level public key. #[derive(Clone, Debug)] pub enum RawPublicKey { /// An RSA/SHA-1 public key. @@ -82,22 +257,23 @@ impl RawPublicKey { impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. - pub fn from_dnskey( + pub fn from_dnskey_format( algorithm: SecAlg, data: &[u8], ) -> Result { match algorithm { SecAlg::RSASHA1 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha1) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha1) } SecAlg::RSASHA1_NSEC3_SHA1 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha1Nsec3Sha1) + RsaPublicKey::from_dnskey_format(data) + .map(Self::RsaSha1Nsec3Sha1) } SecAlg::RSASHA256 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha256) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha256) } SecAlg::RSASHA512 => { - RsaPublicKey::from_dnskey(data).map(Self::RsaSha512) + RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha512) } SecAlg::ECDSAP256SHA256 => { @@ -134,67 +310,13 @@ impl RawPublicKey { } } - /// Parse a public key from a DNSKEY record in presentation format. - /// - /// This format is popularized for storing alongside private keys by the - /// BIND name server. This function is convenient for loading such keys. - /// - /// The text should consist of a single line of the following format (each - /// field is separated by a non-zero number of ASCII spaces): - /// - /// ```text - /// DNSKEY [] - /// ``` - /// - /// Where `` consists of the following fields: - /// - /// ```text - /// - /// ``` - /// - /// The first three fields are simple integers, while the last field is - /// Base64 encoded data (with or without padding). The [`from_dnskey()`] - /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data - /// format. - /// - /// [`from_dnskey()`]: Self::from_dnskey() - /// [`to_dnskey()`]: Self::to_dnskey() - /// - /// The `` is any text starting with an ASCII semicolon. - pub fn parse_dnskey_text( - dnskey: &str, - ) -> Result { - // Ensure there is a single line in the input. - let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); - if !rest.trim().is_empty() { - return Err(FromDnskeyTextError::Misformatted); - } - - // Strip away any semicolon from the line. - let (line, _) = line.split_once(';').unwrap_or((line, "")); - - // Ensure the record header looks reasonable. - let mut words = line.split_ascii_whitespace().skip(2); - if !words.next().unwrap_or("").eq_ignore_ascii_case("DNSKEY") { - return Err(FromDnskeyTextError::Misformatted); - } - - // Parse the DNSKEY record data. - let mut data = IterScanner::new(words); - let dnskey: Dnskey> = Dnskey::scan(&mut data) - .map_err(|_| FromDnskeyTextError::Misformatted)?; - println!("importing {:?}", dnskey); - Self::from_dnskey(dnskey.algorithm(), dnskey.public_key().as_slice()) - .map_err(FromDnskeyTextError::FromDnskey) - } - /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey(&self) -> Box<[u8]> { + pub fn to_dnskey_format(&self) -> Box<[u8]> { match self { Self::RsaSha1(k) | Self::RsaSha1Nsec3Sha1(k) | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.to_dnskey(), + | Self::RsaSha512(k) => k.to_dnskey_format(), // From my reading of RFC 6605, the marker byte is not included. Self::EcdsaP256Sha256(k) => k[1..].into(), @@ -247,7 +369,7 @@ pub struct RsaPublicKey { impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. - pub fn from_dnskey(data: &[u8]) -> Result { + pub fn from_dnskey_format(data: &[u8]) -> Result { if data.len() < 3 { return Err(FromDnskeyError::InvalidKey); } @@ -278,7 +400,7 @@ impl RsaPublicKey { } /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey(&self) -> Box<[u8]> { + pub fn to_dnskey_format(&self) -> Box<[u8]> { let mut key = Vec::new(); // Encode the exponent length. @@ -301,19 +423,10 @@ impl RsaPublicKey { impl PartialEq for RsaPublicKey { fn eq(&self, other: &Self) -> bool { - /// Compare after stripping leading zeros. - fn cmp_without_leading(a: &[u8], b: &[u8]) -> bool { - let a = &a[a.iter().position(|&x| x != 0).unwrap_or(a.len())..]; - let b = &b[b.iter().position(|&x| x != 0).unwrap_or(b.len())..]; - if a.len() == b.len() { - ring::constant_time::verify_slices_are_equal(a, b).is_ok() - } else { - false - } - } + use ring::constant_time::verify_slices_are_equal; - cmp_without_leading(&self.n, &other.n) - && cmp_without_leading(&self.e, &other.e) + verify_slices_are_equal(&self.n, &other.n).is_ok() + && verify_slices_are_equal(&self.e, &other.e).is_ok() } } @@ -325,7 +438,7 @@ pub enum FromDnskeyError { } #[derive(Clone, Debug)] -pub enum FromDnskeyTextError { +pub enum ParseDnskeyTextError { Misformatted, FromDnskey(FromDnskeyError), } From 3cec8cb547d595e19c36cc2af950d883e705910f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 11:59:41 +0200 Subject: [PATCH 151/569] [sign/openssl] Pad ECDSA keys when exporting Tests would spuriously fail when generated keys were only 31 bytes in size. --- src/sign/openssl.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 46553dbad..4086f8947 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -166,12 +166,12 @@ impl SecretKey { } SecAlg::ECDSAP256SHA256 => { let key = self.pkey.ec_key().unwrap(); - let key = key.private_key().to_vec(); + let key = key.private_key().to_vec_padded(32).unwrap(); generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); - let key = key.private_key().to_vec(); + let key = key.private_key().to_vec_padded(48).unwrap(); generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { From 8682b6d99e694229753a4bcf220a315a91e73097 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 13:49:41 +0200 Subject: [PATCH 152/569] [validate] Implement 'Key::key_tag()' This is more efficient than allocating a DNSKEY record and computing the key tag there. --- src/validate.rs | 135 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/src/validate.rs b/src/validate.rs index b040acf9b..303edb4ce 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -60,6 +60,11 @@ impl Key { &self.key } + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg { + self.key.algorithm() + } + /// Whether this is a zone signing key. /// /// From RFC 4034, section 2.1.1: @@ -92,6 +97,26 @@ impl Key { pub fn is_secure_entry_point(&self) -> bool { self.flags & (1 << 15) != 0 } + + /// The key tag. + pub fn key_tag(&self) -> u16 { + // NOTE: RSA/MD5 uses a different algorithm. + + // NOTE: A u32 can fit the sum of 65537 u16s without overflowing. A + // key can never exceed 64KiB anyway, so we won't even get close to + // the limit. Let's just add into a u32 and normalize it after. + let mut res = 0u32; + + // Add basic DNSKEY fields. + res += self.flags as u32; + res += u16::from_be_bytes([3, self.algorithm().to_int()]) as u32; + + // Add the raw key tag from the public key. + res += self.key.raw_key_tag(); + + // Normalize and return the result. + (res as u16).wrapping_add((res >> 16) as u16) + } } impl> Key { @@ -253,6 +278,32 @@ impl RawPublicKey { Self::Ed448(_) => SecAlg::ED448, } } + + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + fn compute(data: &[u8]) -> u32 { + data.chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + // A 0 byte is appended for an incomplete chunk. + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum() + } + + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.raw_key_tag(), + + Self::EcdsaP256Sha256(k) => compute(&k[1..]), + Self::EcdsaP384Sha384(k) => compute(&k[1..]), + Self::Ed25519(k) => compute(&**k), + Self::Ed448(k) => compute(&**k), + } + } } impl RawPublicKey { @@ -367,6 +418,44 @@ pub struct RsaPublicKey { pub e: Box<[u8]>, } +impl RsaPublicKey { + /// The raw key tag computation for this value. + fn raw_key_tag(&self) -> u32 { + let mut res = 0u32; + + // Extended exponent lengths start with '00 (exp_len >> 8)', which is + // just zero for shorter exponents. That doesn't affect the result, + // so let's just do it unconditionally. + res += (self.e.len() >> 8) as u32; + res += u16::from_be_bytes([self.e.len() as u8, self.e[0]]) as u32; + + let mut chunks = self.e[1..].chunks_exact(2); + res += chunks + .by_ref() + .map(|chunk| u16::from_be_bytes(chunk.try_into().unwrap()) as u32) + .sum::(); + + let n = if !chunks.remainder().is_empty() { + res += + u16::from_be_bytes([chunks.remainder()[0], self.n[0]]) as u32; + &self.n[1..] + } else { + &self.n + }; + + res += n + .chunks(2) + .map(|chunk| { + let mut buf = [0u8; 2]; + buf[..chunk.len()].copy_from_slice(chunk); + u16::from_be_bytes(buf) as u32 + }) + .sum::(); + + res + } +} + impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. pub fn from_dnskey_format(data: &[u8]) -> Result { @@ -929,6 +1018,14 @@ mod test { type Dnskey = crate::rdata::Dnskey>; type Rrsig = crate::rdata::Rrsig, Name>; + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 27096), + (SecAlg::ECDSAP256SHA256, 40436), + (SecAlg::ECDSAP384SHA384, 17013), + (SecAlg::ED25519, 43769), + (SecAlg::ED448, 34114), + ]; + // Returns current root KSK/ZSK for testing (2048b) fn root_pubkey() -> (Dnskey, Dnskey) { let ksk = base64::decode::>( @@ -973,6 +1070,44 @@ mod test { ) } + #[test] + fn parse_dnskey_text() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let _ = Key::>::parse_dnskey_text(&data).unwrap(); + } + } + + #[test] + fn key_tag() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + assert_eq!(key.to_dnskey().key_tag(), key_tag); + assert_eq!(key.key_tag(), key_tag); + } + } + + #[test] + fn dnskey_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + let dnskey = key.to_dnskey().convert(); + let same = Key::from_dnskey(key.owner().clone(), dnskey).unwrap(); + assert_eq!(key.to_dnskey(), same.to_dnskey()); + } + } + #[test] fn dnskey_digest() { let (dnskey, _) = root_pubkey(); From 57d20d95d9683e85a3f15573d28037b272f9d26e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 14:14:03 +0200 Subject: [PATCH 153/569] [validate] Correct bit offsets for flags --- src/validate.rs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 303edb4ce..6b48e8f10 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -75,6 +75,19 @@ impl Key { /// > the DNSKEY record holds some other type of DNS public key and MUST /// > NOT be used to verify RRSIGs that cover RRsets. pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From RFC 5011, section 3: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + pub fn is_revoked(&self) -> bool { self.flags & (1 << 7) != 0 } @@ -82,7 +95,6 @@ impl Key { /// /// From RFC 4034, section 2.1.1: /// - /// /// > Bit 15 of the Flags field is the Secure Entry Point flag, described /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a /// > key intended for use as a secure entry point. This flag is only @@ -95,7 +107,7 @@ impl Key { /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs /// > that cover RRsets. pub fn is_secure_entry_point(&self) -> bool { - self.flags & (1 << 15) != 0 + self.flags & 1 != 0 } /// The key tag. From f37c862bedf452849aa6b9c622d3c1803f95eeca Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 15:10:31 +0200 Subject: [PATCH 154/569] [validate] Implement support for digests The test keys have been rotated and replaced with KSKs since they have associated DS records I can verify digests against. I also expanded Ring's testing to include ECDSA keys. The validate module tests SHA-1 keys as well, which aren't supported by 'sign'. --- src/sign/generic.rs | 16 +- src/sign/openssl.rs | 19 +- src/sign/ring.rs | 14 +- src/validate.rs | 239 ++++++++++++++++-- test-data/dnssec-keys/Ktest.+005+00439.ds | 1 + test-data/dnssec-keys/Ktest.+005+00439.key | 1 + .../dnssec-keys/Ktest.+005+00439.private | 10 + test-data/dnssec-keys/Ktest.+007+22204.ds | 1 + test-data/dnssec-keys/Ktest.+007+22204.key | 1 + .../dnssec-keys/Ktest.+007+22204.private | 10 + test-data/dnssec-keys/Ktest.+008+27096.key | 1 - .../dnssec-keys/Ktest.+008+27096.private | 10 - test-data/dnssec-keys/Ktest.+008+60616.ds | 1 + test-data/dnssec-keys/Ktest.+008+60616.key | 1 + .../dnssec-keys/Ktest.+008+60616.private | 10 + test-data/dnssec-keys/Ktest.+013+40436.key | 1 - test-data/dnssec-keys/Ktest.+013+42253.ds | 1 + test-data/dnssec-keys/Ktest.+013+42253.key | 1 + ...40436.private => Ktest.+013+42253.private} | 2 +- test-data/dnssec-keys/Ktest.+014+17013.key | 1 - .../dnssec-keys/Ktest.+014+17013.private | 3 - test-data/dnssec-keys/Ktest.+014+33566.ds | 1 + test-data/dnssec-keys/Ktest.+014+33566.key | 1 + .../dnssec-keys/Ktest.+014+33566.private | 3 + test-data/dnssec-keys/Ktest.+015+43769.key | 1 - .../dnssec-keys/Ktest.+015+43769.private | 3 - test-data/dnssec-keys/Ktest.+015+56037.ds | 1 + test-data/dnssec-keys/Ktest.+015+56037.key | 1 + .../dnssec-keys/Ktest.+015+56037.private | 3 + test-data/dnssec-keys/Ktest.+016+07379.ds | 1 + test-data/dnssec-keys/Ktest.+016+07379.key | 1 + .../dnssec-keys/Ktest.+016+07379.private | 3 + test-data/dnssec-keys/Ktest.+016+34114.key | 1 - .../dnssec-keys/Ktest.+016+34114.private | 3 - 34 files changed, 295 insertions(+), 72 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.ds create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.key create mode 100644 test-data/dnssec-keys/Ktest.+005+00439.private create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.ds create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.key create mode 100644 test-data/dnssec-keys/Ktest.+007+22204.private delete mode 100644 test-data/dnssec-keys/Ktest.+008+27096.key delete mode 100644 test-data/dnssec-keys/Ktest.+008+27096.private create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.ds create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.key create mode 100644 test-data/dnssec-keys/Ktest.+008+60616.private delete mode 100644 test-data/dnssec-keys/Ktest.+013+40436.key create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.ds create mode 100644 test-data/dnssec-keys/Ktest.+013+42253.key rename test-data/dnssec-keys/{Ktest.+013+40436.private => Ktest.+013+42253.private} (50%) delete mode 100644 test-data/dnssec-keys/Ktest.+014+17013.key delete mode 100644 test-data/dnssec-keys/Ktest.+014+17013.private create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.ds create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.key create mode 100644 test-data/dnssec-keys/Ktest.+014+33566.private delete mode 100644 test-data/dnssec-keys/Ktest.+015+43769.key delete mode 100644 test-data/dnssec-keys/Ktest.+015+43769.private create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.ds create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.key create mode 100644 test-data/dnssec-keys/Ktest.+015+56037.private create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.ds create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.key create mode 100644 test-data/dnssec-keys/Ktest.+016+07379.private delete mode 100644 test-data/dnssec-keys/Ktest.+016+34114.key delete mode 100644 test-data/dnssec-keys/Ktest.+016+34114.private diff --git a/src/sign/generic.rs b/src/sign/generic.rs index f7caaa5a0..96a343b1e 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -436,17 +436,18 @@ mod tests { use crate::base::iana::SecAlg; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; #[test] fn secret_from_dns() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKey::parse_from_bind(&data).unwrap(); @@ -457,7 +458,8 @@ mod tests { #[test] fn secret_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKey::parse_from_bind(&data).unwrap(); diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 4086f8947..def9ac40b 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -357,11 +357,11 @@ mod tests { use super::SecretKey; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; #[test] @@ -385,7 +385,8 @@ mod tests { #[test] fn imported_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -411,7 +412,8 @@ mod tests { #[test] fn public_key() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -431,7 +433,8 @@ mod tests { #[test] fn sign() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); diff --git a/src/sign/ring.rs b/src/sign/ring.rs index e0be1943a..67aab7829 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -222,13 +222,18 @@ mod tests { use super::SecretKey; - const KEYS: &[(SecAlg, u16)] = - &[(SecAlg::RSASHA256, 27096), (SecAlg::ED25519, 43769)]; + const KEYS: &[(SecAlg, u16)] = &[ + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + ]; #[test] fn public_key() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); @@ -250,7 +255,8 @@ mod tests { #[test] fn sign() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); diff --git a/src/validate.rs b/src/validate.rs index 6b48e8f10..0670d0030 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -13,7 +13,7 @@ use crate::base::record::Record; use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; use crate::base::Rtype; -use crate::rdata::{Dnskey, Rrsig}; +use crate::rdata::{Dnskey, Ds, Rrsig}; use bytes::Bytes; use octseq::builder::with_infallible; use octseq::{EmptyBuilder, FromBuilder}; @@ -22,6 +22,8 @@ use std::boxed::Box; use std::vec::Vec; use std::{error, fmt}; +//----------- Key ------------------------------------------------------------ + /// A DNSSEC key for a particular zone. #[derive(Clone)] pub struct Key { @@ -39,12 +41,18 @@ pub struct Key { key: RawPublicKey, } +//--- Construction + impl Key { /// Construct a new DNSSEC key manually. pub fn new(owner: Name, flags: u16, key: RawPublicKey) -> Self { Self { owner, flags, key } } +} + +//--- Inspection +impl Key { /// The owner name attached to the key. pub fn owner(&self) -> &Name { &self.owner @@ -129,8 +137,53 @@ impl Key { // Normalize and return the result. (res as u16).wrapping_add((res >> 16) as u16) } + + /// The digest of this key. + pub fn digest( + &self, + algorithm: DigestAlg, + ) -> Result>, DigestError> + where + Octs: AsRef<[u8]>, + { + let mut context = ring::digest::Context::new(match algorithm { + DigestAlg::SHA1 => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, + DigestAlg::SHA256 => &ring::digest::SHA256, + DigestAlg::SHA384 => &ring::digest::SHA384, + _ => return Err(DigestError::UnsupportedAlgorithm), + }); + + // Add the owner name. + if self + .owner + .as_slice() + .iter() + .any(|&b| b.is_ascii_uppercase()) + { + let mut owner = [0u8; 256]; + owner[..self.owner.len()].copy_from_slice(self.owner.as_slice()); + owner.make_ascii_lowercase(); + context.update(&owner[..self.owner.len()]); + } else { + context.update(self.owner.as_slice()); + } + + // Add basic DNSKEY fields. + context.update(&self.flags.to_be_bytes()); + context.update(&[3, self.algorithm().to_int()]); + + // Add the public key. + self.key.digest(&mut context); + + // Finalize the digest. + let digest = context.finish().as_ref().into(); + Ok(Ds::new(self.key_tag(), self.algorithm(), algorithm, digest) + .unwrap()) + } } +//--- Conversion to and from DNSKEYs + impl> Key { /// Deserialize a key from DNSKEY record data. /// @@ -232,6 +285,8 @@ impl> Key { } } +//----------- RsaPublicKey --------------------------------------------------- + /// A low-level public key. #[derive(Clone, Debug)] pub enum RawPublicKey { @@ -276,6 +331,8 @@ pub enum RawPublicKey { Ed448(Box<[u8; 57]>), } +//--- Inspection + impl RawPublicKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { @@ -316,8 +373,25 @@ impl RawPublicKey { Self::Ed448(k) => compute(&**k), } } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + match self { + Self::RsaSha1(k) + | Self::RsaSha1Nsec3Sha1(k) + | Self::RsaSha256(k) + | Self::RsaSha512(k) => k.digest(context), + + Self::EcdsaP256Sha256(k) => context.update(&k[1..]), + Self::EcdsaP384Sha384(k) => context.update(&k[1..]), + Self::Ed25519(k) => context.update(&**k), + Self::Ed448(k) => context.update(&**k), + } + } } +//--- Conversion to and from DNSKEYs + impl RawPublicKey { /// Parse a public key as stored in a DNSKEY record. pub fn from_dnskey_format( @@ -391,6 +465,8 @@ impl RawPublicKey { } } +//--- Comparison + impl PartialEq for RawPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -417,6 +493,10 @@ impl PartialEq for RawPublicKey { } } +impl Eq for RawPublicKey {} + +//----------- RsaPublicKey --------------------------------------------------- + /// A generic RSA public key. /// /// All fields here are arbitrary-precision integers in big-endian format, @@ -430,6 +510,8 @@ pub struct RsaPublicKey { pub e: Box<[u8]>, } +//--- Inspection + impl RsaPublicKey { /// The raw key tag computation for this value. fn raw_key_tag(&self) -> u32 { @@ -466,8 +548,25 @@ impl RsaPublicKey { res } + + /// Compute a digest of this public key. + fn digest(&self, context: &mut ring::digest::Context) { + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(self.e.len()) { + context.update(&[exp_len]); + } else if let Ok(exp_len) = u16::try_from(self.e.len()) { + context.update(&[0u8, (exp_len >> 8) as u8, exp_len as u8]); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + context.update(&self.e); + context.update(&self.n); + } } +//--- Conversion to and from DNSKEYs + impl RsaPublicKey { /// Parse an RSA public key as stored in a DNSKEY record. pub fn from_dnskey_format(data: &[u8]) -> Result { @@ -522,6 +621,8 @@ impl RsaPublicKey { } } +//--- Comparison + impl PartialEq for RsaPublicKey { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -531,18 +632,9 @@ impl PartialEq for RsaPublicKey { } } -#[derive(Clone, Debug)] -pub enum FromDnskeyError { - UnsupportedAlgorithm, - UnsupportedProtocol, - InvalidKey, -} +impl Eq for RsaPublicKey {} -#[derive(Clone, Debug)] -pub enum ParseDnskeyTextError { - Misformatted, - FromDnskey(FromDnskeyError), -} +//----------- Signature ------------------------------------------------------ /// A cryptographic signature. /// @@ -985,6 +1077,71 @@ fn rsa_exponent_modulus( //============ Error Types =================================================== +//----------- DigestError ---------------------------------------------------- + +/// An error when computing a digest. +#[derive(Clone, Debug)] +pub enum DigestError { + UnsupportedAlgorithm, +} + +//--- Display, Error + +impl fmt::Display for DigestError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +impl error::Error for DigestError {} + +//----------- FromDnskeyError ------------------------------------------------ + +/// An error in reading a DNSKEY record. +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + UnsupportedAlgorithm, + UnsupportedProtocol, + InvalidKey, +} + +//--- Display, Error + +impl fmt::Display for FromDnskeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + Self::UnsupportedProtocol => "unsupported protocol", + Self::InvalidKey => "malformed key", + }) + } +} + +impl error::Error for FromDnskeyError {} + +//----------- ParseDnskeyTextError ------------------------------------------- + +#[derive(Clone, Debug)] +pub enum ParseDnskeyTextError { + Misformatted, + FromDnskey(FromDnskeyError), +} + +//--- Display, Error + +impl fmt::Display for ParseDnskeyTextError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Misformatted => "misformatted DNSKEY record", + Self::FromDnskey(e) => return e.fmt(f), + }) + } +} + +impl error::Error for ParseDnskeyTextError {} + //------------ AlgorithmError ------------------------------------------------ /// An algorithm error during verification. @@ -995,17 +1152,15 @@ pub enum AlgorithmError { InvalidData, } -//--- Display and Error +//--- Display, Error impl fmt::Display for AlgorithmError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match *self { - AlgorithmError::Unsupported => { - f.write_str("unsupported algorithm") - } - AlgorithmError::BadSig => f.write_str("bad signature"), - AlgorithmError::InvalidData => f.write_str("invalid data"), - } + f.write_str(match self { + AlgorithmError::Unsupported => "unsupported algorithm", + AlgorithmError::BadSig => "bad signature", + AlgorithmError::InvalidData => "invalid data", + }) } } @@ -1031,11 +1186,13 @@ mod test { type Rrsig = crate::rdata::Rrsig, Name>; const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 27096), - (SecAlg::ECDSAP256SHA256, 40436), - (SecAlg::ECDSAP384SHA384, 17013), - (SecAlg::ED25519, 43769), - (SecAlg::ED448, 34114), + (SecAlg::RSASHA1, 439), + (SecAlg::RSASHA1_NSEC3_SHA1, 22204), + (SecAlg::RSASHA256, 60616), + (SecAlg::ECDSAP256SHA256, 42253), + (SecAlg::ECDSAP384SHA384, 33566), + (SecAlg::ED25519, 56037), + (SecAlg::ED448, 7379), ]; // Returns current root KSK/ZSK for testing (2048b) @@ -1085,7 +1242,8 @@ mod test { #[test] fn parse_dnskey_text() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -1096,7 +1254,8 @@ mod test { #[test] fn key_tag() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -1106,10 +1265,34 @@ mod test { } } + #[test] + fn digest() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_dnskey_text(&data).unwrap(); + + // Scan the DS record from the file. + let path = format!("test-data/dnssec-keys/K{}.ds", name); + let data = std::fs::read_to_string(path).unwrap(); + let mut scanner = IterScanner::new(data.split_ascii_whitespace()); + let _ = scanner.scan_name().unwrap(); + let _ = Class::scan(&mut scanner).unwrap(); + assert_eq!(Rtype::scan(&mut scanner).unwrap(), Rtype::DS); + let ds = Ds::scan(&mut scanner).unwrap(); + + assert_eq!(key.digest(ds.digest_type()).unwrap(), ds); + } + } + #[test] fn dnskey_roundtrip() { for &(algorithm, key_tag) in KEYS { - let name = format!("test.+{:03}+{}", algorithm.to_int(), key_tag); + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); diff --git a/test-data/dnssec-keys/Ktest.+005+00439.ds b/test-data/dnssec-keys/Ktest.+005+00439.ds new file mode 100644 index 000000000..543137100 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.ds @@ -0,0 +1 @@ +test. IN DS 439 5 1 3d54b51d59c71418104ec48bacb3d1a01b8eaa30 diff --git a/test-data/dnssec-keys/Ktest.+005+00439.key b/test-data/dnssec-keys/Ktest.+005+00439.key new file mode 100644 index 000000000..35999a0ae --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 5 AwEAAb5nA65uEYX1bRYwT53jRQqAk/mLbi3SlN3xxkdtn7rTkKgEdiBPIF8+0OVyS3x/OCLPTrto6ojUI5etA1VDZPiTLvuq6rIhn3oNyc5o9Kzl4RX4XptLTrt7ldRcpIjgcgqMJoERUWLQqxoXCfRqClxO2Erk0UZhe3GteCMSEfoGBU5MdPzrrEE6GMxEAKFHabjupQ4GazxfWO7+D38lsmUNJwgCg/B14CIcvTS6cHKFmKJKYEEmAj/kx+LnZd9bmeyagFz8CcgcI/NUiSDgdgx/OeCdSc39OHCp9a0NSJuywbbIxpLPw8cIvgZ8OnHuGjrNTROuyYXVxQM1xe914DM= ;{id = 439 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+005+00439.private b/test-data/dnssec-keys/Ktest.+005+00439.private new file mode 100644 index 000000000..1d8d11ce6 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+005+00439.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 5 (RSASHA1) +Modulus: vmcDrm4RhfVtFjBPneNFCoCT+YtuLdKU3fHGR22futOQqAR2IE8gXz7Q5XJLfH84Is9Ou2jqiNQjl60DVUNk+JMu+6rqsiGfeg3Jzmj0rOXhFfhem0tOu3uV1FykiOByCowmgRFRYtCrGhcJ9GoKXE7YSuTRRmF7ca14IxIR+gYFTkx0/OusQToYzEQAoUdpuO6lDgZrPF9Y7v4PfyWyZQ0nCAKD8HXgIhy9NLpwcoWYokpgQSYCP+TH4udl31uZ7JqAXPwJyBwj81SJIOB2DH854J1Jzf04cKn1rQ1Im7LBtsjGks/Dxwi+Bnw6ce4aOs1NE67JhdXFAzXF73XgMw== +PublicExponent: AQAB +PrivateExponent: CSEarcAR+ltUhK4s/cQKPmurLK7rydSsAKGkFoQCFvd9RcvDojRJDWgPT2vAhNKmGBKFPY/VQa7yRJvYv2YrhDkCarISQ2zrSZ3kTDpUvlQzYQCiAKOGveSPauRE8K8vqKPPANHva2PX9bifEzy2YctXVu1Lv3/TEcCgibCcc2FwrKqzwHZ/AvMeMQD7UjetkpFELqYRHkdFQt+8vFDTmXNhhtm2O5xgYymsaaLW7mOLyR7oo25Uk93ouZx3Ibo9yNHdeJG6S6wFeWQaLGKA78tJK10gaUwiHIdEYh4qQ+pSsjztk6A2ObaWmlbt5Ve9qN1WW+KVizATJIQUQvhocQ== +Prime1: 42WKyzrGcBkhZz8xTvNWzlkhvb6aHgryXlgMP2E1GxRgZDApj6XqFzDHRbC/QaRvZi9skuoEz148xH6Hs2oJQ3I/2+dX/7YmnwPZyxHCx2LUlQ+AqEXXWNGCXQ5I6EvDDFeLSqb7m4sZhnnMaTOpyrmYqFzkxZkWrNiSHJjq5us= +Prime2: 1lo1/h5mxzarMFwfrOI+ErR8bvYrAp8hr33MV58MUwWy2IyUIlJRPJVg6DAaT87jwQuJEVarqq2IB48TI1SKglR5CJNcRuTviHWVViXDY7AVnUvHWiiKncTKDQG7vI4Ffft46qVEdaKLjkPBsapuibt0ocpKszVdmr0usP31qdk= +Exponent1: VIQbD+nqcyOD/MHJ69QZgVwzZDiBQ4VCC7qh4rSYblYmdVZJPDCoTrI8fjRxAU7CcLJTok8ENqaJ42Y7vX09sCm4flz/ofTradKekhEp2b1r0XMPmHtMzKAh2cBDbMMr3Vx0Uuy5O1h5xjdit/8Rrl1I1dqg1KhPezKLK8HSHL0= +Exponent2: QqGALyIcKMjhpgK9Bey+Bup707JJ5GK7AeZE4ufZ2OTol0/7rD+SaRa2LPbm9vAE9Dk1vmIGsuOGaXMcK9tXwvOnO/cytAbuPqjuZv0OI6rUzTSFH42CqVBGzow/Y3lyU5scFzSQd1CzuOFvEF8+RSo0MybC2bo5AqTUIsiO2OE= +Coefficient: wOxhD2sDrZhzWq99qjyaYSZxQrPhJWkLR8LhnZEmPlQwfExz939Qw1TkmBpYcr67sN8UTqY93N7mES2LOJrkE/RzstzaKQS2We8mypovFOwcZu3GfJSsRYJRhsW5dEIiLAVw8a/bnC+K0m2Ahiy8v3GwQVo0u1KZ6oSHmG8IWng= diff --git a/test-data/dnssec-keys/Ktest.+007+22204.ds b/test-data/dnssec-keys/Ktest.+007+22204.ds new file mode 100644 index 000000000..913575095 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.ds @@ -0,0 +1 @@ +test. IN DS 22204 7 1 0783210826bc4a4ab0d4b329458f216bf787a00c diff --git a/test-data/dnssec-keys/Ktest.+007+22204.key b/test-data/dnssec-keys/Ktest.+007+22204.key new file mode 100644 index 000000000..26bf24bfc --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 7 AwEAAcOFirT7uFYwPyEhyio+mb/9yMQH6ENYEOboEX2c0WIPBFr1s34rZ3SWEWsTvxLOMKr3drzSZtcpCQ6vEyPpQpGo1cpWlVSZ7QB73iWw21rZkz/r4MykyloPoJ8ghr4SRSfJx6CjAb+Fhz3bUF4YWofJEshuZMbxLnOEi2hR9T2zTPRjYltA1sfhU478ixh6ddNym+kCIBEhoFIFyKYb5VznOoWcR/mOexQMfUdNqKoIwnhCX8Sg2dKYdgeDDPsZH3AaWp8BY3aqiqOEacSO2XI+7Pdr0rVfszJfcCsf4g+R/7oBt6dtO9WS+0YqVN0J8WQ/9HmWFeCJgY2Rs4c9eDk= ;{id = 22204 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+007+22204.private b/test-data/dnssec-keys/Ktest.+007+22204.private new file mode 100644 index 000000000..ecb576d4c --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+007+22204.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 7 (RSASHA1_NSEC3) +Modulus: w4WKtPu4VjA/ISHKKj6Zv/3IxAfoQ1gQ5ugRfZzRYg8EWvWzfitndJYRaxO/Es4wqvd2vNJm1ykJDq8TI+lCkajVylaVVJntAHveJbDbWtmTP+vgzKTKWg+gnyCGvhJFJ8nHoKMBv4WHPdtQXhhah8kSyG5kxvEuc4SLaFH1PbNM9GNiW0DWx+FTjvyLGHp103Kb6QIgESGgUgXIphvlXOc6hZxH+Y57FAx9R02oqgjCeEJfxKDZ0ph2B4MM+xkfcBpanwFjdqqKo4RpxI7Zcj7s92vStV+zMl9wKx/iD5H/ugG3p2071ZL7RipU3QnxZD/0eZYV4ImBjZGzhz14OQ== +PublicExponent: AQAB +PrivateExponent: VaLpgGCaOgHSvK/AjOUzUVCWPSobdFefu4sckhB78v+R0Ec6cUIQg5NxGJ2i/FkcHt3Zf1WGXqnmAizzbLCvi/3PedqXeGEc2a/nOknuoamXYZFuOiPZTz32A4xrB9gXuxgZXAXZb6nL9O9YkYYILN4IYIpdkHc1ebotlykCiZ14YjS7sFiKNwxk4Pk5HC9qwQlRujO2LZN6Gp5Pqj3i8h/d9/xgCV+IJGwUiy8y0czEJH3f+k76IaM4ZQZiieS/3vXmytHieAVGIZBH5yztgy+p+GJgVXPEb/7WESC38WSn6GwqthcBZXrSOjhqP2PfFuDDfEhglTNSBqhONzE28w== +Prime1: 9trbMq0VgNtsJuyM5CMQa/feEidp51a1POok8pPAZ6SUpno+oNzITCrSga7i08HzBoW22k9jNmIJmpwXDeDoX2TICgDEyzIqzBH+V1zCE1dI8fv9w/hF9mt/qoZ0PN/Jh4Zcu/AHtmRaHAO6lBFblS6EZxdX4lTeVj8toGxR0ms= +Prime2: ysPYyIh9vwN5rKNPKnrjPtMshjFv6CEnXeFDhVvxcutudgayyu0+Gu8g54WjJ/tpEsDENjhi1Da21pn5RxpgCbe/qE+2Z7CGsw+FI+UcOgx8EEm1aGSenC+7AVACarPtU6zr5/kcPiqCm6zPatLJvXRfbQAa/hHdl5Xg28HX8Os= +Exponent1: Da4zV6uf9XQzmjSh2kLXNiSWegsVI2z6vlV7lrX5g8TrOA6uSdvyfcYhxG4cw/+LqGDgsViU9v6X6amc3XgJaL/9FhDU1y4AkS6uGclaOBguQrrkZWfs+KsceCbbakQ8tvYLTZ8PzlvhYowSWwJbQPlC/TOd+z0Y1U7LCIj4P+E= +Exponent2: LnOrqFVMqYP8TgajzlGU2gG7A4sz3fQqdqFyvIyRxggVqEhkkYTEY5tA6Il/FVvNeJRc3ycPzRozzPo9V4K9WbyU1dRdL2gLk94MXGrSiqHtkjWwr5fNlm6A4w4XX6aUykSlTuGNDNjkTxHJ+ukLerG8YtZRWL9zCpU1jGLeO70= +Coefficient: quDhRGQcA/iLpbDJym2ErykV+wsflci0KZIf7/rtCnsDJZSVYQlB/UPY2S5ne+zwuY8/fNYGIVMYN1sV8OPF3AIpTOtte5pc+1V+4rbuQEQhQw9uIvX4205GEc2sjJ637CT46FDP/lnPL7TdvV6NdOuLyDDImbaMqyLtMSJ5IEs= diff --git a/test-data/dnssec-keys/Ktest.+008+27096.key b/test-data/dnssec-keys/Ktest.+008+27096.key deleted file mode 100644 index 5aa614f71..000000000 --- a/test-data/dnssec-keys/Ktest.+008+27096.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 8 AwEAAZNv1qOSZNiRTK1gyMGrikze8q6QtlFaWgJIwhoZ9R1E/AeBCEEeM08WZNrTJZGyLrG+QFrr+eC/iEGjptM0kEEBah7zzvqYEsw7HaUnvomwJ+T9sWepfrbKqRNX9wHz4Mps3jDZNtDZKFxavY9ZDBnOv4jk4bz4xrI0K3yFFLkoxkID2UVCdRzuIodM5SeIROyseYNNMOyygRXSqB5CpKmNO9MgGD3e+7e5eAmtwsxeFJgbYNkcNllO2+vpPwh0p3uHQ7JbCO5IvwC5cvMzebqVJxy/PqL7QyF0HdKKaXi3SXVNu39h7ngsc/ntsPdxNiR3Kqt2FCXKdvp5TBZFouvZ4bvmEGHa9xCnaecx82SUJybyKRM/9GqfNMW5+osy5kyR4xUHjAXZxDO6Vh9fSlnyRZIxfZ+bBTeUZDFPU6zAqCSi8ZrQH0PFdG0I0YQ2QSuIYy57SJZbPVsF21bY5PlJLQwSfZFNGMqPcOjtQeXh4EarpOLQqUmg4hCeWC6gdw== ;{id = 27096 (zsk), size = 3072b} diff --git a/test-data/dnssec-keys/Ktest.+008+27096.private b/test-data/dnssec-keys/Ktest.+008+27096.private deleted file mode 100644 index b5819714f..000000000 --- a/test-data/dnssec-keys/Ktest.+008+27096.private +++ /dev/null @@ -1,10 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 8 (RSASHA256) -Modulus: k2/Wo5Jk2JFMrWDIwauKTN7yrpC2UVpaAkjCGhn1HUT8B4EIQR4zTxZk2tMlkbIusb5AWuv54L+IQaOm0zSQQQFqHvPO+pgSzDsdpSe+ibAn5P2xZ6l+tsqpE1f3AfPgymzeMNk20NkoXFq9j1kMGc6/iOThvPjGsjQrfIUUuSjGQgPZRUJ1HO4ih0zlJ4hE7Kx5g00w7LKBFdKoHkKkqY070yAYPd77t7l4Ca3CzF4UmBtg2Rw2WU7b6+k/CHSne4dDslsI7ki/ALly8zN5upUnHL8+ovtDIXQd0oppeLdJdU27f2HueCxz+e2w93E2JHcqq3YUJcp2+nlMFkWi69nhu+YQYdr3EKdp5zHzZJQnJvIpEz/0ap80xbn6izLmTJHjFQeMBdnEM7pWH19KWfJFkjF9n5sFN5RkMU9TrMCoJKLxmtAfQ8V0bQjRhDZBK4hjLntIlls9WwXbVtjk+UktDBJ9kU0Yyo9w6O1B5eHgRquk4tCpSaDiEJ5YLqB3 -PublicExponent: AQAB -PrivateExponent: B55XVoN5j5FOh4UBSrStBFTe8HNM4H5NOWH+GbAusNEAPvkFbqv7VcJf+si/X7x32jptA+W+t0TeaxnkRHSqYZmLnMbXcq6KBiCl4wNfPqkqHpSXZrZk9FgbjYLVojWyb3NZted7hCY8hi0wL2iYDftXfWDqY0PtrIaympAb5od7WyzsvL325ERP53LrQnQxr5MoAkdqWEjPD8wfYNTrwlEofrvhVM0hb7h3QfTHJJ1V7hg4FG/3RP0ksxeN6MdyTgU7zCnQCsVr4jg6AryMANcsLOJzee5t13iJ5QmC5OlsUa1MXvFxoWSRCV3tr3aYBqV7XZ5YH31T5S2mJdI5IQAo4RPnNe1FJ98uhVp+5yQwj9lV9q3OX7Hfezc3Lgsd93rJKY1auGQ4d8gW+uLBUwj67Jx2kTASP+2y/9fwZqpK6H8HewNMK9M9dpByPZwGOWx5kY6VEamIDXKkyHrRdGF9Es0c5swEmrY0jtFj+0hryKbXJknOl7RWxKu/AaGN -Prime1: wxtTI/kZ0KnsSRc8fGd/QXhIrr2w4ERKiXw/sk/uD/jUQ4z8+wDsXd4z6TRGoLCbmGjk9upfHyJ5VAze64IAHN15EOQ34+SLxpXMFI4NwWRdejVRfCuqgivANUznseXCufaIDUFuzate3/JJgaFr1qJgYOMGb2k6xbeVeB04+7/5OOvMc+9xLY6OMK26HNS6SFvScArDzLutzXMiirW+lQT1SUyfaRu3N3VMNnt/Hsy/MiaLL18DUVtxSooS9zGj -Prime2: wXPHBmFQUtdud/mVErSjswrgULQn3lBUydTqXc6dPk/FNAy2fGFEaUlq5P7h7+xMSfKt8TG7UBmKyL1wWCFqGI4gOxGMJ5j6dENAkxobaZOrldcgFX2DDqUu3AsS1Eom95TrWiHwygt7XOLdj4Md1shu9M1C8PMNYi46Xc6Q4Aujj05fi5YESvK6tVBCJe8gpmtFfMZFWHN5GmPzCJE4XjkljvoM4Y5em+xZwzFBnJsdcjWqdEnIBi+O3AnJhAsd -Exponent1: Rbs7YM0D8/b3Uzwxywi2i7Cw0XtMfysJNNAqd9FndV/qhWYbeJ5g3D+xb/TWFVJpmfRLeRBVBOyuTmL3PVbOMYLaZTYb36BscIJTWTlYIzl6y1XJFMcKftGiNaqR2JwUl6BMCejL8EgCdanDqcgGocSRC6+4OhNzBP1TN4XCOv/m0/g6r2jxm2Wq3i0JKorBNWFT+eVvC3o8aQRwYQEJ53rJK/RtuQRF3FVY8tP6oAhvgT4TWs/rgKVc/VYR5zVf -Exponent2: lZmsKtHspPO2mQ8oajvJcDcT+zUms7RZrW97Aqo6TaqwrSy7nno1xlohUQ+Ot9R7tp/2RdSYrzvhaJWfIHhOrMiUQjmyshiKbohnkpqY4k9xXMHtLNFQHW4+S6pAmGzzr3i5fI1MwWKZtt42SroxxBxiOevWPbEoA2oOdua8gJZfmP4Zwz9y+Ga3Xmm/jchb7nZ8WR6XF+zMlUz/7/slpS/6TJQwi+lmXpwrWlhoDeyim+TGeYFpLuduSdlDvlo9 -Coefficient: NodAWfZD7fkTNsSJavk6RRIZXpoRy4ACyU7zEDtUA9QQokCkG83vGqoO/NK0+UJo7vDgOe/uSZu1qxrtoRa+yamh2Rgeix9tZbKkHLxyADyF/vqNl9vl1w/utHmEmoS0uUCzxtLGMrsxqVKOT4S3IykqxDNDd2gHdPagEdFy81vdlise61FFxcBKO3rNBZA+sSosJWMBaCgPy+7J4adsFG/UOrKEolUCIb0Ze4aS21BYdFdm7vbrP1Wfkqob+Q0X diff --git a/test-data/dnssec-keys/Ktest.+008+60616.ds b/test-data/dnssec-keys/Ktest.+008+60616.ds new file mode 100644 index 000000000..65444f942 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.ds @@ -0,0 +1 @@ +test. IN DS 60616 8 2 6b91f7b7134cf916d909e2905b5707e3ea6c86842339f09d87c858d7ccd620b3 diff --git a/test-data/dnssec-keys/Ktest.+008+60616.key b/test-data/dnssec-keys/Ktest.+008+60616.key new file mode 100644 index 000000000..fa6c03d8a --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 8 AwEAAdaxEmT1eAAnXMGDjYfivh6ax6BOESlNZY85BlVWkCOYV6jf5GcSgweqcCowFW2HtHKiE/FACwG5Wfq/xCDhLHYg4PQIvd5UcrDzj+WBEFe7pVhUjZrMsMRAVy2W4jliat6IrJv+CdycErp4cLxmqfNECIP7i9vI8onruvBe1YWebJN38TxdGCteg5waI27DNaQsXldxZoCfSY7Fkhj7BJ4XxHDeWzE876LmSMkkYFWqEQwesD280piL+4tmySMPxhVC1EUguQyn/Lc9FbEd3h1RyaO8hg8ub/70espLVElE9ImOibaY+gj9jK7HFD/mqdxYdFfr3yiQsGOt2ui4jGM= ;{id = 60616 (ksk), size = 2048b} diff --git a/test-data/dnssec-keys/Ktest.+008+60616.private b/test-data/dnssec-keys/Ktest.+008+60616.private new file mode 100644 index 000000000..8df7cdc20 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+008+60616.private @@ -0,0 +1,10 @@ +Private-key-format: v1.2 +Algorithm: 8 (RSASHA256) +Modulus: 1rESZPV4ACdcwYONh+K+HprHoE4RKU1ljzkGVVaQI5hXqN/kZxKDB6pwKjAVbYe0cqIT8UALAblZ+r/EIOEsdiDg9Ai93lRysPOP5YEQV7ulWFSNmsywxEBXLZbiOWJq3oism/4J3JwSunhwvGap80QIg/uL28jyieu68F7VhZ5sk3fxPF0YK16DnBojbsM1pCxeV3FmgJ9JjsWSGPsEnhfEcN5bMTzvouZIySRgVaoRDB6wPbzSmIv7i2bJIw/GFULURSC5DKf8tz0VsR3eHVHJo7yGDy5v/vR6yktUSUT0iY6Jtpj6CP2MrscUP+ap3Fh0V+vfKJCwY63a6LiMYw== +PublicExponent: AQAB +PrivateExponent: EBBYZ6ofnCYAgGY/J8S6easWdr3V9jjZTtnIdIgxPsiTqTTKGpWTAkwpb66rW8evTnMmz4KoOtfLOMIygvdLjrHabcgIVONitYTJO+CSqs3aiv0V9K2OKGZcCjLjoxbkbNmIeMo4TgPLjvJGFS1lV/4Q2Qya+WCpbSfF6V20gkvQ46dtdRaswFOeav0WIm8LdudWDlYei89EIL243JlDErRmcrh6ZrxIg2TMT+mYJCoM6zfhFkbZuQyagn0Fguymp3Kc31SFqdReF9Q/IIQKwNiW14gdxCEHxq+y7xajCF0bhRZAY/hVyRr4qpx2ZRNMdg5qR2a8IilhH2+YXkHBUQ== +Prime1: 7fuvTpTPTHAQV3nQEW6WLf9xrf0G6ka5E2Lvn+jaawk60VZHoVybpURd0Dq586ZinQpJ2ovEfCd9Os8vn31BNrtulz8mfmKz1rObbdKvo0XRSExcLFx2ZG35Bdo/6H8Ri5e/9gx0m0yJeKspNW20uJX9ndk8Lsm5J9d+8SvcZis= +Prime2: 5vH6ly1VSF1DafdVGMKiHY4icP4OAAPJ/Sl+ihcYzbguhZ82fJ3mZeYLDZWSozwnvhK9PTqGwVRhLJH875AUrU/YA+nEBb5dVHMgGb4Afx2PzOlhgDIhEiRD0QW/9bwq45nITfnFMbYzkE2e08KZ/tjiusQIRZAQCkEBEbNITqk= +Exponent1: pKvW7iUCG/4fEKh1VNqUiFeNLbs7obg2MDfxX1EccZv9WwS8o+cUvBLGZ2N7cCDdc5S+7b5wwwgAG0Vpyo49JcYkC/vigumBTzsQfbmfVvbkjYZo8Tk5otyFx4rxVcs3NMRYS8Tqmtsm9Jxa82Fp/5+p0iOTBT0IJY1zhSW4Z+k= +Exponent2: kvemyxIUVarUPdkiFFG4LSrIjDOA4U2H+02us14jcLcnE+3QFNm/R1Vv70MiQDMF75WpTA+0tc9mz6BP4HxGTEylYUggcK9GYXmqEfeyBTLg0jwqyhQcq5jcd2Y7VLxcZt70c3rhnNMgWVKsIoKS0XVgRA6AXRRiwMPBVGxNNZE= +Coefficient: HsJ5e503CSA3lF3sPrKuL4EuT1Qv0IMHRSd5cZyJj6fCvLYzXi+NtlUX+GMHKuzSm64t6Jrw+FN2I1XTn0QvnpMQqwgou/G79I3dy3a82B+I2qBXgPFqpb/Zj6Eno+aQ+jxD4i6C2b7GhpAxpENwBLIPoIhyJSmWl1o2DDo2irs= diff --git a/test-data/dnssec-keys/Ktest.+013+40436.key b/test-data/dnssec-keys/Ktest.+013+40436.key deleted file mode 100644 index 7f7cd0fcc..000000000 --- a/test-data/dnssec-keys/Ktest.+013+40436.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 13 syG7D2WUTdQEHbNp2G2Pkstb6FXYWu+wz1/07QRsDmPCfFhOBRnhE4dAHxMRqdhkC4nxdKD3vVpMqiJxFPiVLg== ;{id = 40436 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+42253.ds b/test-data/dnssec-keys/Ktest.+013+42253.ds new file mode 100644 index 000000000..8d52a1301 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.ds @@ -0,0 +1 @@ +test. IN DS 42253 13 2 b55c30248246756635ee8eb9ff03a9492df46257f4f6537ea85e579b501765e6 diff --git a/test-data/dnssec-keys/Ktest.+013+42253.key b/test-data/dnssec-keys/Ktest.+013+42253.key new file mode 100644 index 000000000..c9d6127ea --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+013+42253.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 13 /5DQ8gQAUp0yITNeE6p0rKQPblVGKOPAdPKxWLQ/FOrkcax3S7qJZh6Z9ayn+EewnpQcmdexlOvxsMf5q8ppCw== ;{id = 42253 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+013+40436.private b/test-data/dnssec-keys/Ktest.+013+42253.private similarity index 50% rename from test-data/dnssec-keys/Ktest.+013+40436.private rename to test-data/dnssec-keys/Ktest.+013+42253.private index 39f5e8a8d..7b26e96a1 100644 --- a/test-data/dnssec-keys/Ktest.+013+40436.private +++ b/test-data/dnssec-keys/Ktest.+013+42253.private @@ -1,3 +1,3 @@ Private-key-format: v1.2 Algorithm: 13 (ECDSAP256SHA256) -PrivateKey: i9MkBllvhT113NGsyrlixafLigQNFRkiXV6Vhr6An1Y= +PrivateKey: uKp4Xz2aB3/LfLGADBjNYFvAZbDHBCO+uJdL+GFCVOY= diff --git a/test-data/dnssec-keys/Ktest.+014+17013.key b/test-data/dnssec-keys/Ktest.+014+17013.key deleted file mode 100644 index c7b6aa1d4..000000000 --- a/test-data/dnssec-keys/Ktest.+014+17013.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 14 FvRdwSOotny0L51mx270qKyEpBmcwwhXPT++koI1Rb9wYRQHXfFn+8wBh01G4OgF2DDTTkLd5pJKEgoBavuvaAKFkqNAWjMXxqKu4BIJiGSySeNWM6IlRXXldvMZGQto ;{id = 17013 (zsk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+17013.private b/test-data/dnssec-keys/Ktest.+014+17013.private deleted file mode 100644 index 9648a876a..000000000 --- a/test-data/dnssec-keys/Ktest.+014+17013.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 14 (ECDSAP384SHA384) -PrivateKey: S/Q2qvfLTsxBRoTy4OU9QM2qOgbTd4yDNKm5BXFYJi6bWX4/VBjBlWYIBUchK4ZT diff --git a/test-data/dnssec-keys/Ktest.+014+33566.ds b/test-data/dnssec-keys/Ktest.+014+33566.ds new file mode 100644 index 000000000..7e3165c6c --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.ds @@ -0,0 +1 @@ +test. IN DS 33566 14 4 d27e8964b63e8f3db4001834d03f1034669e5d39500b06863cc9f38cd649131421bb78b0b08f0ec61a8c8caf0cf09a19 diff --git a/test-data/dnssec-keys/Ktest.+014+33566.key b/test-data/dnssec-keys/Ktest.+014+33566.key new file mode 100644 index 000000000..dd967bccb --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 14 mce1CBcESReUP0iQYCnnhoWrVYe86PnFHIkKkr7qmO5q7AwAENchMaBPzaPOOuwx8Z8AcqIjXLOGL13RDT1lvLLkH7IJMIPHRwiXiFoj0KXBugvKLmMT3a0Nc8s8Uau9 ;{id = 33566 (ksk), size = 384b} diff --git a/test-data/dnssec-keys/Ktest.+014+33566.private b/test-data/dnssec-keys/Ktest.+014+33566.private new file mode 100644 index 000000000..276b9d315 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+014+33566.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 14 (ECDSAP384SHA384) +PrivateKey: 3e1YdfRwn8YOX3Ai84BWVLl3/SphcQIeCkvQnzszKqR3U2xmq/G5HtiGTnBZ1WSW diff --git a/test-data/dnssec-keys/Ktest.+015+43769.key b/test-data/dnssec-keys/Ktest.+015+43769.key deleted file mode 100644 index 8a1f24f67..000000000 --- a/test-data/dnssec-keys/Ktest.+015+43769.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 15 UCexQp95/u4iayuZwkUDyOQgVT3gewHdk7GZzSnsf+M= ;{id = 43769 (zsk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+43769.private b/test-data/dnssec-keys/Ktest.+015+43769.private deleted file mode 100644 index e178a3bd4..000000000 --- a/test-data/dnssec-keys/Ktest.+015+43769.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 15 (ED25519) -PrivateKey: ajePajntXfFbtfiUgW1quT1EXMdQHalqKbWXBkGy3hc= diff --git a/test-data/dnssec-keys/Ktest.+015+56037.ds b/test-data/dnssec-keys/Ktest.+015+56037.ds new file mode 100644 index 000000000..fb802353f --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.ds @@ -0,0 +1 @@ +test. IN DS 56037 15 2 665c358b671a9ed5310667b2bacfb526ace344f59d085c8331c532e6a7024f75 diff --git a/test-data/dnssec-keys/Ktest.+015+56037.key b/test-data/dnssec-keys/Ktest.+015+56037.key new file mode 100644 index 000000000..38dc516a9 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 15 ml9GKFR/doUuYnnQSPi6uiqvHV4VUGOjD4gmpc5dudc= ;{id = 56037 (ksk), size = 256b} diff --git a/test-data/dnssec-keys/Ktest.+015+56037.private b/test-data/dnssec-keys/Ktest.+015+56037.private new file mode 100644 index 000000000..52c5034aa --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+015+56037.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 15 (ED25519) +PrivateKey: Xg9BfVadQ07eubbyryukpn6lYr9BwDHBLSUOpaGLdrc= diff --git a/test-data/dnssec-keys/Ktest.+016+07379.ds b/test-data/dnssec-keys/Ktest.+016+07379.ds new file mode 100644 index 000000000..a1ca41c42 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.ds @@ -0,0 +1 @@ +test. IN DS 7379 16 2 0ec6db96a33efb0c80c9a90e34e80d32506883d0ed245eefd7bfa4d6e13927c9 diff --git a/test-data/dnssec-keys/Ktest.+016+07379.key b/test-data/dnssec-keys/Ktest.+016+07379.key new file mode 100644 index 000000000..a7eade4f9 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.key @@ -0,0 +1 @@ +test. IN DNSKEY 257 3 16 9tIYxOhfSE0dS7m9mVxjgMeWJ5arrusV9VSvxYrbJVhucOm6I35HpHi4Eau5P06vpHaMdbp3aFOA ;{id = 7379 (ksk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+07379.private b/test-data/dnssec-keys/Ktest.+016+07379.private new file mode 100644 index 000000000..9d837bcc4 --- /dev/null +++ b/test-data/dnssec-keys/Ktest.+016+07379.private @@ -0,0 +1,3 @@ +Private-key-format: v1.2 +Algorithm: 16 (ED448) +PrivateKey: /hmHKRERsvW761FDTmGlCBJNmy1H8pbsU2LeV1NP2wb0xM286RFIyUMAwRmkFqPVZwwfQluIBXqe diff --git a/test-data/dnssec-keys/Ktest.+016+34114.key b/test-data/dnssec-keys/Ktest.+016+34114.key deleted file mode 100644 index fc77e0491..000000000 --- a/test-data/dnssec-keys/Ktest.+016+34114.key +++ /dev/null @@ -1 +0,0 @@ -test. IN DNSKEY 256 3 16 ZT2j/s1s7bjcyondo8Hmz9KelXFeoVItJcjAPUTOXnmhczv8T6OmRSELEXO62dwES/gf6TJ17l0A ;{id = 34114 (zsk), size = 456b} diff --git a/test-data/dnssec-keys/Ktest.+016+34114.private b/test-data/dnssec-keys/Ktest.+016+34114.private deleted file mode 100644 index fca7303dc..000000000 --- a/test-data/dnssec-keys/Ktest.+016+34114.private +++ /dev/null @@ -1,3 +0,0 @@ -Private-key-format: v1.2 -Algorithm: 16 (ED448) -PrivateKey: nqCiPcirogQyUUBNFzF0MtCLTGLkMP74zLroLZyQjzZwZd6fnPgQICrKn5Q3uJTti5YYy+MSUHQV From 7f01a5f910f2bce8a49c16d53b46cddf79709b9e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 18 Oct 2024 11:45:47 +0200 Subject: [PATCH 155/569] [validate] Enhance BIND format conversion for 'Key' Public keys in the BIND format can now have multiple lines (even with comments). Keys can also be directly written into the BIND format and round-trips to and from the BIND format are now tested. --- src/sign/openssl.rs | 6 +- src/sign/ring.rs | 4 +- src/validate.rs | 185 +++++++++++++++++++++++++++++++------------- 3 files changed, 137 insertions(+), 58 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index def9ac40b..c9277e907 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -390,7 +390,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); let path = format!("test-data/dnssec-keys/K{}.private", name); @@ -421,7 +421,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); @@ -442,7 +442,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 67aab7829..9d0ff7ab2 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -242,7 +242,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); let key = @@ -265,7 +265,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_dnskey_text(&data).unwrap(); + let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); let key = diff --git a/src/validate.rs b/src/validate.rs index 0670d0030..b82b456c4 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -25,6 +25,29 @@ use std::{error, fmt}; //----------- Key ------------------------------------------------------------ /// A DNSSEC key for a particular zone. +/// +/// # Serialization +/// +/// Keys can be parsed from or written in the conventional format used by the +/// BIND name server. This is a simplified version of the zonefile format. +/// +/// In this format, a public key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a single DNSKEY record in the +/// presentation format. In either case, the line may end with a comment (an +/// ASCII semicolon followed by arbitrary content until the end of the line). +/// The file must contain a single DNSKEY record line. +/// +/// The DNSKEY record line contains the following fields, separated by ASCII +/// whitespace: +/// +/// - The owner name. This is an absolute name ending with a dot. +/// - Optionally, the class of the record (usually `IN`). +/// - The record type (which must be `DNSKEY`). +/// - The DNSKEY record data, which has the following sub-fields: +/// - The key flags, which describe the key's uses. +/// - The protocol used (expected to be `3`). +/// - The key algorithm (see [`SecAlg`]). +/// - The public key encoded as a Base64 string. #[derive(Clone)] pub struct Key { /// The owner of the key. @@ -75,33 +98,37 @@ impl Key { /// Whether this is a zone signing key. /// - /// From RFC 4034, section 2.1.1: + /// From [RFC 4034, section 2.1.1]: /// /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's /// > owner name MUST be the name of a zone. If bit 7 has value 0, then /// > the DNSKEY record holds some other type of DNS public key and MUST /// > NOT be used to verify RRSIGs that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 pub fn is_zone_signing_key(&self) -> bool { self.flags & (1 << 8) != 0 } /// Whether this key has been revoked. /// - /// From RFC 5011, section 3: + /// From [RFC 5011, section 3]: /// /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) /// > signed by the associated key, then the resolver MUST consider this /// > key permanently invalid for all purposes except for validating the /// > revocation. + /// + /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 pub fn is_revoked(&self) -> bool { self.flags & (1 << 7) != 0 } /// Whether this is a secure entry point. /// - /// From RFC 4034, section 2.1.1: + /// From [RFC 4034, section 2.1.1]: /// /// > Bit 15 of the Flags field is the Secure Entry Point flag, described /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a @@ -114,6 +141,9 @@ impl Key { /// > to be able to generate signatures legally. A DNSKEY RR with the SEP /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs /// > that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 pub fn is_secure_entry_point(&self) -> bool { self.flags & 1 != 0 } @@ -206,48 +236,52 @@ impl> Key { Ok(Self { owner, flags, key }) } - /// Parse a DNSSEC key from a DNSKEY record in presentation format. - /// - /// This format is popularized for storing alongside private keys by the - /// BIND name server. This function is convenient for loading such keys. - /// - /// The text should consist of a single line of the following format (each - /// field is separated by a non-zero number of ASCII spaces): - /// - /// ```text - /// DNSKEY [] - /// ``` - /// - /// Where `` consists of the following fields: - /// - /// ```text - /// - /// ``` - /// - /// The first three fields are simple integers, while the last field is - /// Base64 encoded data (with or without padding). The [`from_dnskey()`] - /// and [`to_dnskey()`] read from and serialize to the Base64-decoded data - /// format. + /// Serialize the key into DNSKEY record data. /// - /// [`from_dnskey()`]: Self::from_dnskey() - /// [`to_dnskey()`]: Self::to_dnskey() + /// The owner name can be combined with the returned record to serialize a + /// complete DNS record if necessary. + pub fn to_dnskey(&self) -> Dnskey> { + Dnskey::new( + self.flags, + 3, + self.key.algorithm(), + self.key.to_dnskey_format(), + ) + .expect("long public key") + } + + /// Parse a DNSSEC key from the conventional format used by BIND. /// - /// The `` is any text starting with an ASCII semicolon. - pub fn parse_dnskey_text( - dnskey: &str, - ) -> Result + /// See the type-level documentation for a description of this format. + pub fn parse_from_bind(data: &str) -> Result where Octs: FromBuilder, Octs::Builder: EmptyBuilder + Composer, { - // Ensure there is a single line in the input. - let (line, rest) = dnskey.split_once('\n').unwrap_or((dnskey, "")); - if !rest.trim().is_empty() { - return Err(ParseDnskeyTextError::Misformatted); + /// Find the next non-blank line in the file. + fn next_line(mut data: &str) -> Option<(&str, &str)> { + let mut line; + while !data.is_empty() { + (line, data) = + data.trim_start().split_once('\n').unwrap_or((data, "")); + if !line.is_empty() && !line.starts_with(';') { + // We found a line that does not start with a comment. + line = line + .split_once(';') + .map_or(line, |(line, _)| line.trim_end()); + return Some((line, data)); + } + } + + None } - // Strip away any semicolon from the line. - let (line, _) = line.split_once(';').unwrap_or((line, "")); + // Ensure there is a single DNSKEY record line in the input. + let (line, rest) = + next_line(data).ok_or(ParseDnskeyTextError::Misformatted)?; + if next_line(rest).is_some() { + return Err(ParseDnskeyTextError::Misformatted); + } // Parse the entire record. let mut scanner = IterScanner::new(line.split_ascii_whitespace()); @@ -270,18 +304,46 @@ impl> Key { .map_err(ParseDnskeyTextError::FromDnskey) } - /// Serialize the key into DNSKEY record data. + /// Serialize this key in the conventional format used by BIND. /// - /// The owner name can be combined with the returned record to serialize a - /// complete DNS record if necessary. - pub fn to_dnskey(&self) -> Dnskey> { - Dnskey::new( - self.flags, - 3, - self.key.algorithm(), - self.key.to_dnskey_format(), + /// A user-specified DNS class can be used in the record; however, this + /// will almost always just be `IN`. + /// + /// See the type-level documentation for a description of this format. + pub fn format_as_bind( + &self, + class: Class, + w: &mut impl fmt::Write, + ) -> fmt::Result { + writeln!( + w, + "{} {} DNSKEY {}", + self.owner().fmt_with_dot(), + class, + self.to_dnskey(), ) - .expect("long public key") + } +} + +//--- Comparison + +impl> PartialEq for Key { + fn eq(&self, other: &Self) -> bool { + self.owner() == other.owner() + && self.flags() == other.flags() + && self.raw_public_key() == other.raw_public_key() + } +} + +//--- Debug + +impl> fmt::Debug for Key { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Key") + .field("owner", self.owner()) + .field("flags", &self.flags()) + .field("raw_public_key", self.raw_public_key()) + .finish() } } @@ -1179,6 +1241,7 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; + use std::string::String; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; @@ -1240,14 +1303,14 @@ mod test { } #[test] - fn parse_dnskey_text() { + fn parse_from_bind() { for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let _ = Key::>::parse_dnskey_text(&data).unwrap(); + let _ = Key::>::parse_from_bind(&data).unwrap(); } } @@ -1259,7 +1322,7 @@ mod test { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_dnskey_text(&data).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); assert_eq!(key.to_dnskey().key_tag(), key_tag); assert_eq!(key.key_tag(), key_tag); } @@ -1273,7 +1336,7 @@ mod test { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_dnskey_text(&data).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); // Scan the DS record from the file. let path = format!("test-data/dnssec-keys/K{}.ds", name); @@ -1296,10 +1359,26 @@ mod test { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_dnskey_text(&data).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); let dnskey = key.to_dnskey().convert(); let same = Key::from_dnskey(key.owner().clone(), dnskey).unwrap(); - assert_eq!(key.to_dnskey(), same.to_dnskey()); + assert_eq!(key, same); + } + } + + #[test] + fn bind_format_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = Key::>::parse_from_bind(&data).unwrap(); + let mut bind_fmt_key = String::new(); + key.format_as_bind(Class::IN, &mut bind_fmt_key).unwrap(); + let same = Key::parse_from_bind(&bind_fmt_key).unwrap(); + assert_eq!(key, same); } } From b4103a308090950f7714bee9627724990a1666a1 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Sun, 20 Oct 2024 15:21:12 +0200 Subject: [PATCH 156/569] [sign] Introduce 'SigningKey' --- src/sign/mod.rs | 136 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 134 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 6f31e7887..5aafc5d15 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -12,14 +12,146 @@ #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] use crate::{ - base::iana::SecAlg, - validate::{RawPublicKey, Signature}, + base::{iana::SecAlg, Name}, + validate::{self, RawPublicKey, Signature}, }; pub mod generic; pub mod openssl; pub mod ring; +//----------- SigningKey ----------------------------------------------------- + +/// A signing key. +/// +/// This associates important metadata with a raw cryptographic secret key. +pub struct SigningKey { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The raw private key. + inner: Inner, +} + +//--- Construction + +impl SigningKey { + /// Construct a new signing key manually. + pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { + Self { + owner, + flags, + inner, + } + } +} + +//--- Inspection + +impl SigningKey { + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw secret key. + pub fn raw_secret_key(&self) -> &Inner { + &self.inner + } + + /// Whether this is a zone signing key. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From [RFC 5011, section 3]: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + /// + /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 + pub fn is_revoked(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 + pub fn is_secure_entry_point(&self) -> bool { + self.flags & 1 != 0 + } + + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg + where + Inner: SignRaw, + { + self.inner.algorithm() + } + + /// The associated public key. + pub fn public_key(&self) -> validate::Key<&Octs> + where + Octs: AsRef<[u8]>, + Inner: SignRaw, + { + let owner = Name::from_octets(self.owner.as_octets()).unwrap(); + validate::Key::new(owner, self.flags, self.inner.raw_public_key()) + } + + /// The associated raw public key. + pub fn raw_public_key(&self) -> RawPublicKey + where + Inner: SignRaw, + { + self.inner.raw_public_key() + } +} + +// TODO: Conversion to and from key files + +//----------- SignRaw -------------------------------------------------------- + /// Low-level signing functionality. /// /// Types that implement this trait own a private key and can sign arbitrary From 81720c3cf4d9410d75d0350529b21d48d6a9c8ca Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 21 Oct 2024 12:03:43 +0200 Subject: [PATCH 157/569] [sign] Handle errors more responsibly The 'openssl' and 'ring' modules should now follow the contributing guidelines regarding module layout and formatting. --- src/sign/mod.rs | 119 ++++++++++++++++++++++++----------- src/sign/openssl.rs | 149 +++++++++++++++++++++++++++++--------------- src/sign/ring.rs | 118 ++++++++++++++++++++++------------- 3 files changed, 255 insertions(+), 131 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 5aafc5d15..137717b30 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -11,6 +11,8 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] +use core::fmt; + use crate::{ base::{iana::SecAlg, Name}, validate::{self, RawPublicKey, Signature}, @@ -167,23 +169,9 @@ impl SigningKey { pub trait SignRaw { /// The signature algorithm used. /// - /// The following algorithms are known to this crate. Recommendations - /// toward or against usage are based on published RFCs, not the crate - /// authors' opinion. Implementing types may choose to support some of - /// the prohibited algorithms anyway. - /// - /// - [`SecAlg::RSAMD5`] (highly insecure, do not use) - /// - [`SecAlg::DSA`] (highly insecure, do not use) - /// - [`SecAlg::RSASHA1`] (insecure, not recommended) - /// - [`SecAlg::DSA_NSEC3_SHA1`] (highly insecure, do not use) - /// - [`SecAlg::RSASHA1_NSEC3_SHA1`] (insecure, not recommended) - /// - [`SecAlg::RSASHA256`] - /// - [`SecAlg::RSASHA512`] (not recommended) - /// - [`SecAlg::ECC_GOST`] (do not use) - /// - [`SecAlg::ECDSAP256SHA256`] - /// - [`SecAlg::ECDSAP384SHA384`] - /// - [`SecAlg::ED25519`] - /// - [`SecAlg::ED448`] + /// See [RFC 8624, section 3.1] for IETF implementation recommendations. + /// + /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 fn algorithm(&self) -> SecAlg; /// The raw public key. @@ -198,23 +186,82 @@ pub trait SignRaw { /// /// # Errors /// - /// There are three expected failure cases for this function: - /// - /// - The secret key was invalid. The implementing type is responsible - /// for validating the secret key during initialization, so that this - /// kind of error does not occur. - /// - /// - Not enough randomness could be obtained. This applies to signature - /// algorithms which use randomization (primarily ECDSA). On common - /// platforms like Linux, Mac OS, and Windows, cryptographically secure - /// pseudo-random number generation is provided by the OS, so this is - /// highly unlikely. - /// - /// - Not enough memory could be obtained. Signature generation does not - /// require significant memory and an out-of-memory condition means that - /// the application will probably panic soon. - /// - /// None of these are considered likely or recoverable, so panicking is - /// the simplest and most ergonomic solution. - fn sign_raw(&self, data: &[u8]) -> Signature; + /// See [`SignError`] for a discussion of possible failure cases. To the + /// greatest extent possible, the implementation should check for failure + /// cases beforehand and prevent them (e.g. when the keypair is created). + fn sign_raw(&self, data: &[u8]) -> Result; } + +//============ Error Types =================================================== + +//----------- SignError ------------------------------------------------------ + +/// A signature failure. +/// +/// In case such an error occurs, callers should stop using the key pair they +/// attempted to sign with. If such an error occurs with every key pair they +/// have available, or if such an error occurs with a freshly-generated key +/// pair, they should use a different cryptographic implementation. If that +/// is not possible, they must forego signing entirely. +/// +/// # Failure Cases +/// +/// Signing should be an infallible process. There are three considerable +/// failure cases for it: +/// +/// - The secret key was invalid (e.g. its parameters were inconsistent). +/// +/// Such a failure would mean that all future signing (with this key) will +/// also fail. In any case, the implementations provided by this crate try +/// to verify the key (e.g. by checking the consistency of the private and +/// public components) before any signing occurs, largely ruling this class +/// of errors out. +/// +/// - Not enough randomness could be obtained. This applies to signature +/// algorithms which use randomization (e.g. RSA and ECDSA). +/// +/// On the vast majority of platforms, randomness can always be obtained. +/// The [`getrandom` crate documentation](getrandom) notes: +/// +/// > If an error does occur, then it is likely that it will occur on every +/// > call to getrandom, hence after the first successful call one can be +/// > reasonably confident that no errors will occur. +/// +/// getrandom: https://docs.rs/getrandom +/// +/// Thus, in case such a failure occurs, all future signing will probably +/// also fail. +/// +/// - Not enough memory could be allocated. +/// +/// Signature algorithms have a small memory overhead, so an out-of-memory +/// condition means that the program is nearly out of allocatable space. +/// +/// Callers who do not expect allocations to fail (i.e. who are using the +/// standard memory allocation routines, not their `try_` variants) will +/// likely panic shortly after such an error. +/// +/// Callers who are aware of their memory usage will likely restrict it far +/// before they get to this point. Systems running at near-maximum load +/// tend to quickly become unresponsive and staggeringly slow. If memory +/// usage is an important consideration, programs will likely cap it before +/// the system reaches e.g. 90% memory use. +/// +/// As such, memory allocation failure should never really occur. It is far +/// more likely that one of the other errors has occurred. +/// +/// It may be reasonable to panic in any such situation, since each kind of +/// error is essentially unrecoverable. However, applications where signing +/// is an optional step, or where crashing is prohibited, may wish to recover +/// from such an error differently (e.g. by foregoing signatures or informing +/// an operator). +#[derive(Clone, Debug)] +pub struct SignError; + +impl fmt::Display for SignError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("could not create a cryptographic signature") + } +} + +impl std::error::Error for SignError {} diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c9277e907..b9a6a4820 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,11 +1,12 @@ //! Key and Signer using OpenSSL. use core::fmt; -use std::boxed::Box; +use std::vec::Vec; use openssl::{ bn::BigNum, ecdsa::EcdsaSig, + error::ErrorStack, pkey::{self, PKey, Private}, }; @@ -14,7 +15,9 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, SignRaw}; +use super::{generic, SignError, SignRaw}; + +//----------- SecretKey ------------------------------------------------------ /// A key pair backed by OpenSSL. pub struct SecretKey { @@ -25,6 +28,8 @@ pub struct SecretKey { pkey: PKey, } +//--- Conversion to and from generic keys + impl SecretKey { /// Use a generic secret key with OpenSSL. /// @@ -187,6 +192,57 @@ impl SecretKey { } } +//--- Signing + +impl SecretKey { + fn sign(&self, data: &[u8]) -> Result, ErrorStack> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + match self.algorithm { + SecAlg::RSASHA256 => { + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) + } + + SecAlg::ECDSAP256SHA256 => { + let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(32)?; + let mut s = signature.s().to_vec_padded(32)?; + r.append(&mut s); + Ok(r) + } + SecAlg::ECDSAP384SHA384 => { + let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(48)?; + let mut s = signature.s().to_vec_padded(48)?; + r.append(&mut s); + Ok(r) + } + + SecAlg::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + SecAlg::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + + _ => unreachable!(), + } + } +} + +//--- SignRaw + impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { self.algorithm @@ -233,56 +289,33 @@ impl SignRaw for SecretKey { } } - fn sign_raw(&self, data: &[u8]) -> Signature { - use openssl::hash::MessageDigest; - use openssl::sign::Signer; + fn sign_raw(&self, data: &[u8]) -> Result { + let signature = self + .sign(data) + .map(Vec::into_boxed_slice) + .map_err(|_| SignError)?; match self.algorithm { - SecAlg::RSASHA256 => { - let mut s = - Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); - s.set_rsa_padding(openssl::rsa::Padding::PKCS1).unwrap(); - let signature = s.sign_oneshot_to_vec(data).unwrap(); - Signature::RsaSha256(signature.into_boxed_slice()) - } - SecAlg::ECDSAP256SHA256 => { - let mut s = - Signer::new(MessageDigest::sha256(), &self.pkey).unwrap(); - let signature = s.sign_oneshot_to_vec(data).unwrap(); - // Convert from DER to the fixed representation. - let signature = EcdsaSig::from_der(&signature).unwrap(); - let r = signature.r().to_vec_padded(32).unwrap(); - let s = signature.s().to_vec_padded(32).unwrap(); - let mut signature = Box::new([0u8; 64]); - signature[..32].copy_from_slice(&r); - signature[32..].copy_from_slice(&s); - Signature::EcdsaP256Sha256(signature) - } - SecAlg::ECDSAP384SHA384 => { - let mut s = - Signer::new(MessageDigest::sha384(), &self.pkey).unwrap(); - let signature = s.sign_oneshot_to_vec(data).unwrap(); - // Convert from DER to the fixed representation. - let signature = EcdsaSig::from_der(&signature).unwrap(); - let r = signature.r().to_vec_padded(48).unwrap(); - let s = signature.s().to_vec_padded(48).unwrap(); - let mut signature = Box::new([0u8; 96]); - signature[..48].copy_from_slice(&r); - signature[48..].copy_from_slice(&s); - Signature::EcdsaP384Sha384(signature) - } - SecAlg::ED25519 => { - let mut s = Signer::new_without_digest(&self.pkey).unwrap(); - let signature = - s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); - Signature::Ed25519(signature.try_into().unwrap()) - } - SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey).unwrap(); - let signature = - s.sign_oneshot_to_vec(data).unwrap().into_boxed_slice(); - Signature::Ed448(signature.try_into().unwrap()) - } + SecAlg::RSASHA256 => Ok(Signature::RsaSha256(signature)), + + SecAlg::ECDSAP256SHA256 => signature + .try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError), + SecAlg::ECDSAP384SHA384 => signature + .try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError), + + SecAlg::ED25519 => signature + .try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError), + SecAlg::ED448 => signature + .try_into() + .map(Signature::Ed448) + .map_err(|_| SignError), + _ => unreachable!(), } } @@ -323,6 +356,10 @@ pub fn generate(algorithm: SecAlg) -> Option { Some(SecretKey { algorithm, pkey }) } +//============ Error Types =================================================== + +//----------- FromGenericError ----------------------------------------------- + /// An error in importing a key into OpenSSL. #[derive(Clone, Debug)] pub enum FromGenericError { @@ -331,19 +368,29 @@ pub enum FromGenericError { /// The key's parameters were invalid. InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, } +//--- Formatting + impl fmt::Display for FromGenericError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", }) } } +//--- Error + impl std::error::Error for FromGenericError {} +//============ Tests ========================================================= + #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; @@ -447,7 +494,7 @@ mod tests { let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); - let _ = key.sign_raw(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!").unwrap(); } } } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 9d0ff7ab2..ccda86a6b 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -13,7 +13,9 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, SignRaw}; +use super::{generic, SignError, SignRaw}; + +//----------- SecretKey ------------------------------------------------------ /// A key pair backed by `ring`. pub enum SecretKey { @@ -39,6 +41,8 @@ pub enum SecretKey { Ed25519(ring::signature::Ed25519KeyPair), } +//--- Conversion from generic keys + impl SecretKey { /// Use a generic keypair with `ring`. pub fn from_generic( @@ -56,6 +60,11 @@ impl SecretKey { return Err(FromGenericError::InvalidKey); } + // Ensure that the key is strong enough. + if p.n.len() < 2048 / 8 { + return Err(FromGenericError::WeakKey); + } + let components = ring::rsa::KeyPairComponents { public_key: ring::rsa::PublicKeyComponents { n: s.n.as_ref(), @@ -114,24 +123,7 @@ impl SecretKey { } } -/// An error in importing a key into `ring`. -#[derive(Clone, Debug)] -pub enum FromGenericError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// The provided keypair was invalid. - InvalidKey, -} - -impl fmt::Display for FromGenericError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::InvalidKey => "malformed or insecure private key", - }) - } -} +//--- SignRaw impl SignRaw for SecretKey { fn algorithm(&self) -> SecAlg { @@ -174,42 +166,80 @@ impl SignRaw for SecretKey { } } - fn sign_raw(&self, data: &[u8]) -> Signature { + fn sign_raw(&self, data: &[u8]) -> Result { match self { Self::RsaSha256 { key, rng } => { let mut buf = vec![0u8; key.public().modulus_len()]; let pad = &ring::signature::RSA_PKCS1_SHA256; key.sign(pad, &**rng, data, &mut buf) - .expect("random generators do not fail"); - Signature::RsaSha256(buf.into_boxed_slice()) - } - Self::EcdsaP256Sha256 { key, rng } => { - let mut buf = Box::new([0u8; 64]); - buf.copy_from_slice( - key.sign(&**rng, data) - .expect("random generators do not fail") - .as_ref(), - ); - Signature::EcdsaP256Sha256(buf) - } - Self::EcdsaP384Sha384 { key, rng } => { - let mut buf = Box::new([0u8; 96]); - buf.copy_from_slice( - key.sign(&**rng, data) - .expect("random generators do not fail") - .as_ref(), - ); - Signature::EcdsaP384Sha384(buf) + .map(|()| Signature::RsaSha256(buf.into_boxed_slice())) + .map_err(|_| SignError) } + + Self::EcdsaP256Sha256 { key, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError) + }), + + Self::EcdsaP384Sha384 { key, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError) + }), + Self::Ed25519(key) => { - let mut buf = Box::new([0u8; 64]); - buf.copy_from_slice(key.sign(data).as_ref()); - Signature::Ed25519(buf) + let sig = key.sign(data); + let buf: Box<[u8]> = sig.as_ref().into(); + buf.try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError) } } } } +//============ Error Types =================================================== + +/// An error in importing a key into `ring`. +#[derive(Clone, Debug)] +pub enum FromGenericError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided keypair was invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, +} + +//--- Formatting + +impl fmt::Display for FromGenericError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + }) + } +} + +//--- Error + +impl std::error::Error for FromGenericError {} + +//============ Tests ========================================================= + #[cfg(test)] mod tests { use std::{sync::Arc, vec::Vec}; @@ -271,7 +301,7 @@ mod tests { let key = SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); - let _ = key.sign_raw(b"Hello, World!"); + let _ = key.sign_raw(b"Hello, World!").unwrap(); } } } From 1e00479a14de02f1aedb5e85b401427a1b2ccee3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 21 Oct 2024 12:50:26 +0200 Subject: [PATCH 158/569] [sign] correct doc link --- src/sign/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 137717b30..4b5497b5f 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -221,13 +221,13 @@ pub trait SignRaw { /// algorithms which use randomization (e.g. RSA and ECDSA). /// /// On the vast majority of platforms, randomness can always be obtained. -/// The [`getrandom` crate documentation](getrandom) notes: +/// The [`getrandom` crate documentation][getrandom] notes: /// /// > If an error does occur, then it is likely that it will occur on every /// > call to getrandom, hence after the first successful call one can be /// > reasonably confident that no errors will occur. /// -/// getrandom: https://docs.rs/getrandom +/// [getrandom]: https://docs.rs/getrandom /// /// Thus, in case such a failure occurs, all future signing will probably /// also fail. From d26a4337b4d19e65595c66543f274f364cc88634 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 23 Oct 2024 20:06:27 +0200 Subject: [PATCH 159/569] [sign/openssl] Replace panics with results --- src/sign/openssl.rs | 167 +++++++++++++++++++++++++++----------------- 1 file changed, 102 insertions(+), 65 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index b9a6a4820..6faddd954 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -32,18 +32,20 @@ pub struct SecretKey { impl SecretKey { /// Use a generic secret key with OpenSSL. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. pub fn from_generic( secret: &generic::SecretKey, public: &RawPublicKey, ) -> Result { - fn num(slice: &[u8]) -> BigNum { - let mut v = BigNum::new_secure().unwrap(); - v.copy_from_slice(slice).unwrap(); - v + fn num(slice: &[u8]) -> Result { + let mut v = BigNum::new()?; + v.copy_from_slice(slice)?; + Ok(v) + } + + fn secure_num(slice: &[u8]) -> Result { + let mut v = BigNum::new_secure()?; + v.copy_from_slice(slice)?; + Ok(v) } let pkey = match (secret, public) { @@ -56,24 +58,28 @@ impl SecretKey { return Err(FromGenericError::InvalidKey); } - let n = BigNum::from_slice(&s.n).unwrap(); - let e = BigNum::from_slice(&s.e).unwrap(); - let d = num(&s.d); - let p = num(&s.p); - let q = num(&s.q); - let d_p = num(&s.d_p); - let d_q = num(&s.d_q); - let q_i = num(&s.q_i); + 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)?; // NOTE: The 'openssl' crate doesn't seem to expose // 'EVP_PKEY_fromdata', which could be used to replace the // deprecated methods called here. - openssl::rsa::Rsa::from_private_components( + let key = openssl::rsa::Rsa::from_private_components( n, e, d, p, q, d_p, d_q, q_i, - ) - .and_then(PKey::from_rsa) - .unwrap() + )?; + + if !key.check_key()? { + return Err(FromGenericError::InvalidKey); + } + + PKey::from_rsa(key)? } ( @@ -82,16 +88,14 @@ impl SecretKey { ) => { use openssl::{bn, ec, nid}; - let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let mut ctx = bn::BigNumContext::new_secure()?; let group = nid::Nid::X9_62_PRIME256V1; - let group = ec::EcGroup::from_curve_name(group).unwrap(); - let n = num(s.as_slice()); - let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) - .map_err(|_| FromGenericError::InvalidKey)?; - let k = ec::EcKey::from_private_components(&group, &n, &p) - .map_err(|_| FromGenericError::InvalidKey)?; + let group = ec::EcGroup::from_curve_name(group)?; + let n = secure_num(s.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(|_| FromGenericError::InvalidKey)?; - PKey::from_ec_key(k).unwrap() + PKey::from_ec_key(k)? } ( @@ -100,24 +104,21 @@ impl SecretKey { ) => { use openssl::{bn, ec, nid}; - let mut ctx = bn::BigNumContext::new_secure().unwrap(); + let mut ctx = bn::BigNumContext::new_secure()?; let group = nid::Nid::SECP384R1; - let group = ec::EcGroup::from_curve_name(group).unwrap(); - let n = num(s.as_slice()); - let p = ec::EcPoint::from_bytes(&group, &**p, &mut ctx) - .map_err(|_| FromGenericError::InvalidKey)?; - let k = ec::EcKey::from_private_components(&group, &n, &p) - .map_err(|_| FromGenericError::InvalidKey)?; + let group = ec::EcGroup::from_curve_name(group)?; + let n = secure_num(s.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(|_| FromGenericError::InvalidKey)?; - PKey::from_ec_key(k).unwrap() + PKey::from_ec_key(k)? } (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; - let k = PKey::private_key_from_raw_bytes(&**s, id) - .map_err(|_| FromGenericError::InvalidKey)?; + let k = PKey::private_key_from_raw_bytes(&**s, id)?; if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { @@ -129,8 +130,7 @@ impl SecretKey { use openssl::memcmp; let id = pkey::Id::ED448; - let k = PKey::private_key_from_raw_bytes(&**s, id) - .map_err(|_| FromGenericError::InvalidKey)?; + let k = PKey::private_key_from_raw_bytes(&**s, id)?; if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { @@ -322,38 +322,28 @@ impl SignRaw for SecretKey { } /// Generate a new secret key for the given algorithm. -/// -/// If the algorithm is not supported, [`None`] is returned. -/// -/// # Panics -/// -/// Panics if OpenSSL fails or if memory could not be allocated. -pub fn generate(algorithm: SecAlg) -> Option { +pub fn generate(algorithm: SecAlg) -> Result { let pkey = match algorithm { // We generate 3072-bit keys for an estimated 128 bits of security. - SecAlg::RSASHA256 => openssl::rsa::Rsa::generate(3072) - .and_then(PKey::from_rsa) - .unwrap(), + SecAlg::RSASHA256 => { + openssl::rsa::Rsa::generate(3072).and_then(PKey::from_rsa)? + } SecAlg::ECDSAP256SHA256 => { let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); - openssl::ec::EcKey::generate(&group) - .and_then(PKey::from_ec_key) - .unwrap() + let group = openssl::ec::EcGroup::from_curve_name(group)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? } SecAlg::ECDSAP384SHA384 => { let group = openssl::nid::Nid::SECP384R1; - let group = openssl::ec::EcGroup::from_curve_name(group).unwrap(); - openssl::ec::EcKey::generate(&group) - .and_then(PKey::from_ec_key) - .unwrap() + let group = openssl::ec::EcGroup::from_curve_name(group)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? } - SecAlg::ED25519 => PKey::generate_ed25519().unwrap(), - SecAlg::ED448 => PKey::generate_ed448().unwrap(), - _ => return None, + SecAlg::ED25519 => PKey::generate_ed25519()?, + SecAlg::ED448 => PKey::generate_ed448()?, + _ => return Err(GenerateError::UnsupportedAlgorithm), }; - Some(SecretKey { algorithm, pkey }) + Ok(SecretKey { algorithm, pkey }) } //============ Error Types =================================================== @@ -369,8 +359,18 @@ pub enum FromGenericError { /// The key's parameters were invalid. InvalidKey, - /// The implementation does not allow such weak keys. - WeakKey, + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for FromGenericError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } } //--- Formatting @@ -380,7 +380,7 @@ impl fmt::Display for FromGenericError { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", Self::InvalidKey => "malformed or insecure private key", - Self::WeakKey => "key too weak to be supported", + Self::Implementation => "an internal error occurred", }) } } @@ -389,6 +389,43 @@ impl fmt::Display for FromGenericError { impl std::error::Error for FromGenericError {} +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key with OpenSSL. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + //============ Tests ========================================================= #[cfg(test)] From 6968cb9bc04824fe9fe7548108eb0b7b9db9d042 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 23 Oct 2024 20:06:57 +0200 Subject: [PATCH 160/569] remove 'sign/key' --- src/sign/key.rs | 53 ------------------------------------------------- 1 file changed, 53 deletions(-) delete mode 100644 src/sign/key.rs diff --git a/src/sign/key.rs b/src/sign/key.rs deleted file mode 100644 index da9385780..000000000 --- a/src/sign/key.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::base::iana::SecAlg; -use crate::base::name::ToName; -use crate::rdata::{Dnskey, Ds}; - -pub trait SigningKey { - type Octets: AsRef<[u8]>; - type Signature: AsRef<[u8]>; - type Error; - - fn dnskey(&self) -> Result, Self::Error>; - fn ds( - &self, - owner: N, - ) -> Result, Self::Error>; - - fn algorithm(&self) -> Result { - self.dnskey().map(|dnskey| dnskey.algorithm()) - } - - fn key_tag(&self) -> Result { - self.dnskey().map(|dnskey| dnskey.key_tag()) - } - - fn sign(&self, data: &[u8]) -> Result; -} - -impl<'a, K: SigningKey> SigningKey for &'a K { - type Octets = K::Octets; - type Signature = K::Signature; - type Error = K::Error; - - fn dnskey(&self) -> Result, Self::Error> { - (*self).dnskey() - } - fn ds( - &self, - owner: N, - ) -> Result, Self::Error> { - (*self).ds(owner) - } - - fn algorithm(&self) -> Result { - (*self).algorithm() - } - - fn key_tag(&self) -> Result { - (*self).key_tag() - } - - fn sign(&self, data: &[u8]) -> Result { - (*self).sign(data) - } -} From 99cb9efa7b09cfb756a3a4576d4bdfaa933b98b3 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 24 Oct 2024 14:37:30 +0200 Subject: [PATCH 161/569] [sign] Introduce 'common' for abstracting backends This is useful for abstracting over OpenSSL and Ring, so that Ring can be used whenever possible while OpenSSL is used as a fallback. This is useful for clients that just wish to support everything. --- src/sign/common.rs | 221 ++++++++++++++++++++++++++++++++++++++++++++ src/sign/generic.rs | 66 +++++++++++++ src/sign/mod.rs | 1 + src/sign/openssl.rs | 52 ++++++++--- 4 files changed, 326 insertions(+), 14 deletions(-) create mode 100644 src/sign/common.rs diff --git a/src/sign/common.rs b/src/sign/common.rs new file mode 100644 index 000000000..8b0b52aa7 --- /dev/null +++ b/src/sign/common.rs @@ -0,0 +1,221 @@ +//! DNSSEC signing using built-in backends. + +use core::fmt; +use std::sync::Arc; + +use ::ring::rand::SystemRandom; + +use crate::{ + base::iana::SecAlg, + validate::{RawPublicKey, Signature}, +}; + +use super::{ + generic::{self, GenerateParams}, + openssl, ring, SignError, SignRaw, +}; + +//----------- SecretKey ------------------------------------------------------ + +/// 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. +pub enum SecretKey { + /// A key backed by Ring. + #[cfg(feature = "ring")] + Ring(ring::SecretKey), + + /// A key backed by OpenSSL. + OpenSSL(openssl::SecretKey), +} + +//--- Conversion to and from generic keys + +impl SecretKey { + /// Use a generic secret key with OpenSSL. + pub fn from_generic( + secret: &generic::SecretKey, + public: &RawPublicKey, + ) -> Result { + // Prefer Ring if it is available. + #[cfg(feature = "ring")] + match public { + RawPublicKey::RsaSha1(k) + | RawPublicKey::RsaSha1Nsec3Sha1(k) + | RawPublicKey::RsaSha256(k) + | RawPublicKey::RsaSha512(k) + if k.n.len() >= 2048 / 8 => + { + let rng = Arc::new(SystemRandom::new()); + let key = ring::SecretKey::from_generic(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + RawPublicKey::EcdsaP256Sha256(_) + | RawPublicKey::EcdsaP384Sha384(_) => { + let rng = Arc::new(SystemRandom::new()); + let key = ring::SecretKey::from_generic(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + RawPublicKey::Ed25519(_) => { + let rng = Arc::new(SystemRandom::new()); + let key = ring::SecretKey::from_generic(secret, public, rng)?; + return Ok(Self::Ring(key)); + } + + _ => {} + } + + // Fall back to OpenSSL. + Ok(Self::OpenSSL(openssl::SecretKey::from_generic( + secret, public, + )?)) + } +} + +//--- SignRaw + +impl SignRaw for SecretKey { + fn algorithm(&self) -> SecAlg { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.algorithm(), + Self::OpenSSL(key) => key.algorithm(), + } + } + + fn raw_public_key(&self) -> RawPublicKey { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.raw_public_key(), + Self::OpenSSL(key) => key.raw_public_key(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.sign_raw(data), + Self::OpenSSL(key) => key.sign_raw(data), + } + } +} + +//----------- generate() ----------------------------------------------------- + +/// Generate a new secret key for the given algorithm. +pub fn generate(params: GenerateParams) -> Result { + // TODO: Support key generation in Ring. + Ok(SecretKey::OpenSSL(openssl::generate(params)?)) +} + +//============ Error Types =================================================== + +//----------- FromGenericError ----------------------------------------------- + +/// An error in importing a key. +#[derive(Clone, Debug)] +pub enum FromGenericError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversions + +impl From for FromGenericError { + fn from(value: ring::FromGenericError) -> Self { + match value { + ring::FromGenericError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::FromGenericError::InvalidKey => Self::InvalidKey, + ring::FromGenericError::WeakKey => Self::WeakKey, + } + } +} + +impl From for FromGenericError { + fn from(value: openssl::FromGenericError) -> Self { + match value { + openssl::FromGenericError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::FromGenericError::InvalidKey => Self::InvalidKey, + openssl::FromGenericError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for FromGenericError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromGenericError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(value: openssl::GenerateError) -> Self { + match value { + openssl::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::GenerateError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 96a343b1e..8717fe711 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -7,6 +7,8 @@ use crate::base::iana::SecAlg; use crate::utils::base64; use crate::validate::RsaPublicKey; +//----------- SecretKey ------------------------------------------------------ + /// A generic secret key. /// /// This is a low-level generic representation of a secret key from any one of @@ -103,6 +105,8 @@ pub enum SecretKey { Ed448(Box<[u8; 57]>), } +//--- Inspection + impl SecretKey { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { @@ -114,7 +118,11 @@ impl SecretKey { Self::Ed448(_) => SecAlg::ED448, } } +} + +//--- Converting to and from the BIND format. +impl SecretKey { /// Serialize this key in the conventional format used by BIND. /// /// The key is formatted in the private key v1.2 format and written to the @@ -222,6 +230,8 @@ impl SecretKey { } } +//--- Drop + impl Drop for SecretKey { fn drop(&mut self) { // Zero the bytes for each field. @@ -235,6 +245,8 @@ impl Drop for SecretKey { } } +//----------- RsaSecretKey --------------------------------------------------- + /// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, @@ -265,6 +277,8 @@ pub struct RsaSecretKey { pub q_i: Box<[u8]>, } +//--- Conversion to and from the BIND format + impl RsaSecretKey { /// Serialize this key in the conventional format used by BIND. /// @@ -356,6 +370,8 @@ impl RsaSecretKey { } } +//--- Into + impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { fn from(value: &'a RsaSecretKey) -> Self { RsaPublicKey { @@ -365,6 +381,8 @@ impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { } } +//--- Drop + impl Drop for RsaSecretKey { fn drop(&mut self) { // Zero the bytes for each field. @@ -379,6 +397,44 @@ impl Drop for RsaSecretKey { } } +//----------- GenerateParams ------------------------------------------------- + +/// Parameters for generating a secret key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GenerateParams { + /// Generate an RSA/SHA-256 keypair. + RsaSha256 { bits: u32 }, + + /// Generate an ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256, + + /// Generate an ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384, + + /// Generate an Ed25519 keypair. + Ed25519, + + /// An Ed448 keypair. + Ed448, +} + +//--- Inspection + +impl GenerateParams { + /// The algorithm of the generated key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, + Self::Ed25519 => SecAlg::ED25519, + Self::Ed448 => SecAlg::ED448, + } + } +} + +//----------- Helpers for parsing the BIND format ---------------------------- + /// Extract the next key-value pair in a DNS private key file. fn parse_dns_pair( data: &str, @@ -404,6 +460,10 @@ fn parse_dns_pair( Ok(Some((key.trim(), val.trim(), rest))) } +//============ Error types =================================================== + +//----------- BindFormatError ------------------------------------------------ + /// An error in loading a [`SecretKey`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BindFormatError { @@ -417,6 +477,8 @@ pub enum BindFormatError { UnsupportedAlgorithm, } +//--- Display + impl fmt::Display for BindFormatError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { @@ -427,8 +489,12 @@ impl fmt::Display for BindFormatError { } } +//--- Error + impl std::error::Error for BindFormatError {} +//============ Tests ========================================================= + #[cfg(test)] mod tests { use std::{string::String, vec::Vec}; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 4b5497b5f..306c9d790 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -18,6 +18,7 @@ use crate::{ validate::{self, RawPublicKey, Signature}, }; +pub mod common; pub mod generic; pub mod openssl; pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 6faddd954..e7822d769 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,4 +1,4 @@ -//! Key and Signer using OpenSSL. +//! DNSSEC signing using OpenSSL. use core::fmt; use std::vec::Vec; @@ -15,7 +15,10 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, SignError, SignRaw}; +use super::{ + generic::{self, GenerateParams}, + SignError, SignRaw, +}; //----------- SecretKey ------------------------------------------------------ @@ -322,25 +325,25 @@ impl SignRaw for SecretKey { } /// Generate a new secret key for the given algorithm. -pub fn generate(algorithm: SecAlg) -> Result { - let pkey = match algorithm { +pub fn generate(params: GenerateParams) -> Result { + let algorithm = params.algorithm(); + let pkey = match params { // We generate 3072-bit keys for an estimated 128 bits of security. - SecAlg::RSASHA256 => { - openssl::rsa::Rsa::generate(3072).and_then(PKey::from_rsa)? + GenerateParams::RsaSha256 { bits } => { + openssl::rsa::Rsa::generate(bits).and_then(PKey::from_rsa)? } - SecAlg::ECDSAP256SHA256 => { + GenerateParams::EcdsaP256Sha256 => { let group = openssl::nid::Nid::X9_62_PRIME256V1; let group = openssl::ec::EcGroup::from_curve_name(group)?; PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? } - SecAlg::ECDSAP384SHA384 => { + GenerateParams::EcdsaP384Sha384 => { let group = openssl::nid::Nid::SECP384R1; let group = openssl::ec::EcGroup::from_curve_name(group)?; PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? } - SecAlg::ED25519 => PKey::generate_ed25519()?, - SecAlg::ED448 => PKey::generate_ed448()?, - _ => return Err(GenerateError::UnsupportedAlgorithm), + GenerateParams::Ed25519 => PKey::generate_ed25519()?, + GenerateParams::Ed448 => PKey::generate_ed448()?, }; Ok(SecretKey { algorithm, pkey }) @@ -434,7 +437,10 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, SignRaw}, + sign::{ + generic::{self, GenerateParams}, + SignRaw, + }, validate::Key, }; @@ -451,14 +457,32 @@ mod tests { #[test] fn generate() { for &(algorithm, _) in KEYS { - let _ = super::generate(algorithm).unwrap(); + let params = match algorithm { + SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, + SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, + SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, + SecAlg::ED25519 => GenerateParams::Ed25519, + SecAlg::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let _ = super::generate(params).unwrap(); } } #[test] fn generated_roundtrip() { for &(algorithm, _) in KEYS { - let key = super::generate(algorithm).unwrap(); + let params = match algorithm { + SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, + SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, + SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, + SecAlg::ED25519 => GenerateParams::Ed25519, + SecAlg::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let key = super::generate(params).unwrap(); let gen_key = key.to_generic(); let pub_key = key.raw_public_key(); let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); From 8321d503f6e2efedc97c2bf99179000a31ddfbdd Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 24 Oct 2024 14:45:32 +0200 Subject: [PATCH 162/569] [sign/generic] add top-level doc comment --- src/sign/generic.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sign/generic.rs b/src/sign/generic.rs index 8717fe711..922a9c79e 100644 --- a/src/sign/generic.rs +++ b/src/sign/generic.rs @@ -1,3 +1,5 @@ +//! A generic representation of secret keys. + use core::{fmt, str}; use std::boxed::Box; From a25be56c173468260ac810e2b0c2bb2c64f5c616 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 24 Oct 2024 15:52:19 +0200 Subject: [PATCH 163/569] [validate] debug bind format errors --- src/validate.rs | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index b82b456c4..db4cdbf60 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -268,7 +268,8 @@ impl> Key { // We found a line that does not start with a comment. line = line .split_once(';') - .map_or(line, |(line, _)| line.trim_end()); + .map_or(line, |(line, _)| line) + .trim_end(); return Some((line, data)); } } @@ -280,25 +281,32 @@ impl> Key { let (line, rest) = next_line(data).ok_or(ParseDnskeyTextError::Misformatted)?; if next_line(rest).is_some() { + eprintln!("DEBUG: next line was Some"); return Err(ParseDnskeyTextError::Misformatted); } // Parse the entire record. let mut scanner = IterScanner::new(line.split_ascii_whitespace()); - let name = scanner - .scan_name() - .map_err(|_| ParseDnskeyTextError::Misformatted)?; + let name = scanner.scan_name().map_err(|_| { + eprintln!("DEBUG: owner name failed"); + ParseDnskeyTextError::Misformatted + })?; - let _ = Class::scan(&mut scanner) - .map_err(|_| ParseDnskeyTextError::Misformatted)?; + let _ = Class::scan(&mut scanner).map_err(|_| { + eprintln!("DEBUG: class parsing failed"); + ParseDnskeyTextError::Misformatted + })?; if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { + eprintln!("DEBUG: rtype parsing failed"); return Err(ParseDnskeyTextError::Misformatted); } - let data = Dnskey::scan(&mut scanner) - .map_err(|_| ParseDnskeyTextError::Misformatted)?; + let data = Dnskey::scan(&mut scanner).map_err(|_| { + eprintln!("DEBUG: record data parsing failed"); + ParseDnskeyTextError::Misformatted + })?; Self::from_dnskey(name, data) .map_err(ParseDnskeyTextError::FromDnskey) From 59650a436edea9436ac6a912ec2857086ebea75e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 24 Oct 2024 16:06:55 +0200 Subject: [PATCH 164/569] [validate] more debug statements --- src/validate.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index db4cdbf60..46709b932 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -288,6 +288,8 @@ impl> Key { // Parse the entire record. let mut scanner = IterScanner::new(line.split_ascii_whitespace()); + eprintln!("DEBUG: line = '{}'", line); + let name = scanner.scan_name().map_err(|_| { eprintln!("DEBUG: owner name failed"); ParseDnskeyTextError::Misformatted @@ -303,8 +305,8 @@ impl> Key { return Err(ParseDnskeyTextError::Misformatted); } - let data = Dnskey::scan(&mut scanner).map_err(|_| { - eprintln!("DEBUG: record data parsing failed"); + let data = Dnskey::scan(&mut scanner).map_err(|err| { + eprintln!("DEBUG: record data parsing failed {err}"); ParseDnskeyTextError::Misformatted })?; From 0f54a8dee480023cbb76ec20e89c9fbced53b556 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 24 Oct 2024 16:21:08 +0200 Subject: [PATCH 165/569] [validate] format DNSKEYs using 'ZonefileFmt' The 'Dnskey' impl of 'fmt::Display' was no longer accurate to the zone file format because 'SecAlg' now prints '()'. --- src/validate.rs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 46709b932..d9ebdf31a 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -12,6 +12,7 @@ use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; +use crate::base::zonefile_fmt::ZonefileFmt; use crate::base::Rtype; use crate::rdata::{Dnskey, Ds, Rrsig}; use bytes::Bytes; @@ -281,34 +282,25 @@ impl> Key { let (line, rest) = next_line(data).ok_or(ParseDnskeyTextError::Misformatted)?; if next_line(rest).is_some() { - eprintln!("DEBUG: next line was Some"); return Err(ParseDnskeyTextError::Misformatted); } // Parse the entire record. let mut scanner = IterScanner::new(line.split_ascii_whitespace()); - eprintln!("DEBUG: line = '{}'", line); + let name = scanner + .scan_name() + .map_err(|_| ParseDnskeyTextError::Misformatted)?; - let name = scanner.scan_name().map_err(|_| { - eprintln!("DEBUG: owner name failed"); - ParseDnskeyTextError::Misformatted - })?; - - let _ = Class::scan(&mut scanner).map_err(|_| { - eprintln!("DEBUG: class parsing failed"); - ParseDnskeyTextError::Misformatted - })?; + let _ = Class::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { - eprintln!("DEBUG: rtype parsing failed"); return Err(ParseDnskeyTextError::Misformatted); } - let data = Dnskey::scan(&mut scanner).map_err(|err| { - eprintln!("DEBUG: record data parsing failed {err}"); - ParseDnskeyTextError::Misformatted - })?; + let data = Dnskey::scan(&mut scanner) + .map_err(|_| ParseDnskeyTextError::Misformatted)?; Self::from_dnskey(name, data) .map_err(ParseDnskeyTextError::FromDnskey) @@ -330,7 +322,7 @@ impl> Key { "{} {} DNSKEY {}", self.owner().fmt_with_dot(), class, - self.to_dnskey(), + self.to_dnskey().display_zonefile(false), ) } } From 5a3de59c2a4d63996fda5de472e1aa659c3f6f83 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 25 Oct 2024 11:59:48 +0200 Subject: [PATCH 166/569] Reorganize crate features in 'Cargo.toml' --- Cargo.toml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 29102648a..279c1d054 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,13 +48,21 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil [features] default = ["std", "rand"] + +# Support for libraries bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] -resolv = ["net", "smallvec", "unstable-client-transport"] -resolv-sync = ["resolv", "tokio/rt"] serde = ["dep:serde", "octseq/serde"] smallvec = ["dep:smallvec", "octseq/smallvec"] std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] + +# Cryptographic backends +ring = ["dep:ring"] +openssl = ["dep:openssl"] + +# Crate features +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"] From 12a70afca2a264d6b8e1662353d2cdf0cb94d62f Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 25 Oct 2024 12:00:09 +0200 Subject: [PATCH 167/569] [sign] Add key generation support for Ring It's a bit hacky because it relies on specific byte indices within the generated PKCS8 documents (internally, Ring basically just concatenates bytes to form the documents, and we use the same indices). However, any change to the document format should be caught by the tests here. --- src/sign/common.rs | 32 ++++++++++- src/sign/openssl.rs | 3 +- src/sign/ring.rs | 136 ++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 162 insertions(+), 9 deletions(-) diff --git a/src/sign/common.rs b/src/sign/common.rs index 8b0b52aa7..9931aba59 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -108,9 +108,24 @@ impl SignRaw for SecretKey { //----------- generate() ----------------------------------------------------- /// Generate a new secret key for the given algorithm. -pub fn generate(params: GenerateParams) -> Result { - // TODO: Support key generation in Ring. - Ok(SecretKey::OpenSSL(openssl::generate(params)?)) +pub fn generate( + params: GenerateParams, +) -> Result<(generic::SecretKey, RawPublicKey), GenerateError> { + // Use Ring if it is available. + #[cfg(feature = "ring")] + if matches!( + ¶ms, + GenerateParams::EcdsaP256Sha256 + | GenerateParams::EcdsaP384Sha384 + | GenerateParams::Ed25519 + ) { + let rng = ::ring::rand::SystemRandom::new(); + return Ok(ring::generate(params, &rng)?); + } + + // Fall back to OpenSSL. + let key = openssl::generate(params)?; + Ok((key.to_generic(), key.raw_public_key())) } //============ Error Types =================================================== @@ -205,6 +220,17 @@ impl From for GenerateError { } } +impl From for GenerateError { + fn from(value: ring::GenerateError) -> Self { + match value { + ring::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::GenerateError::Implementation => Self::Implementation, + } + } +} + //--- Formatting impl fmt::Display for GenerateError { diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e7822d769..d1a0a2392 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -324,11 +324,12 @@ impl SignRaw for SecretKey { } } +//----------- generate() ----------------------------------------------------- + /// Generate a new secret key for the given algorithm. pub fn generate(params: GenerateParams) -> Result { let algorithm = params.algorithm(); let pkey = match params { - // We generate 3072-bit keys for an estimated 128 bits of security. GenerateParams::RsaSha256 { bits } => { openssl::rsa::Rsa::generate(bits).and_then(PKey::from_rsa)? } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index ccda86a6b..9564ed812 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -13,7 +13,10 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{generic, SignError, SignRaw}; +use super::{ + generic::{self, GenerateParams}, + SignError, SignRaw, +}; //----------- SecretKey ------------------------------------------------------ @@ -207,6 +210,73 @@ impl SignRaw for SecretKey { } } +//----------- generate() ----------------------------------------------------- + +/// Generate a new secret key for the given algorithm. +pub fn generate( + params: GenerateParams, + rng: &dyn ring::rand::SecureRandom, +) -> Result<(generic::SecretKey, RawPublicKey), GenerateError> { + use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; + + match params { + GenerateParams::EcdsaP256Sha256 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // 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 = generic::SecretKey::EcdsaP256Sha256(sk); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); + let pk = pk.try_into().unwrap(); + let pk = RawPublicKey::EcdsaP256Sha256(pk); + + Ok((sk, pk)) + } + + GenerateParams::EcdsaP384Sha384 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // 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 = generic::SecretKey::EcdsaP384Sha384(sk); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); + let pk = pk.try_into().unwrap(); + let pk = RawPublicKey::EcdsaP384Sha384(pk); + + Ok((sk, pk)) + } + + GenerateParams::Ed25519 => { + // Generate a key and a PKCS#8 document out of Ring. + let doc = Ed25519KeyPair::generate_pkcs8(rng)?; + + // 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 = generic::SecretKey::Ed25519(sk); + + // Manually parse the PKCS#8 document for the public key. + let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); + let pk = pk.try_into().unwrap(); + let pk = RawPublicKey::Ed25519(pk); + + Ok((sk, pk)) + } + + _ => Err(GenerateError::UnsupportedAlgorithm), + } +} + //============ Error Types =================================================== /// An error in importing a key into `ring`. @@ -238,6 +308,43 @@ impl fmt::Display for FromGenericError { impl std::error::Error for FromGenericError {} +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key with Ring. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ring::error::Unspecified) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + //============ Tests ========================================================= #[cfg(test)] @@ -246,7 +353,10 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{generic, SignRaw}, + sign::{ + generic::{self, GenerateParams}, + SignRaw, + }, validate::Key, }; @@ -259,12 +369,18 @@ mod tests { (SecAlg::ED25519, 56037), ]; + const GENERATE_PARAMS: &[GenerateParams] = &[ + GenerateParams::EcdsaP256Sha256, + GenerateParams::EcdsaP384Sha384, + GenerateParams::Ed25519, + ]; + #[test] fn public_key() { + let rng = Arc::new(ring::rand::SystemRandom::new()); for &(algorithm, key_tag) in KEYS { let name = format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - let rng = Arc::new(ring::rand::SystemRandom::new()); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); @@ -275,13 +391,23 @@ mod tests { let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = - SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); + let key = SecretKey::from_generic(&gen_key, pub_key, rng.clone()) + .unwrap(); assert_eq!(key.raw_public_key(), *pub_key); } } + #[test] + fn generated_roundtrip() { + let rng = Arc::new(ring::rand::SystemRandom::new()); + for params in GENERATE_PARAMS { + let (sk, pk) = super::generate(params.clone(), &*rng).unwrap(); + let key = SecretKey::from_generic(&sk, &pk, rng.clone()).unwrap(); + assert_eq!(key.raw_public_key(), pk); + } + } + #[test] fn sign() { for &(algorithm, key_tag) in KEYS { From 2f2fb58c80c1461e75373049583fd24692ed7a58 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Fri, 25 Oct 2024 12:50:37 +0200 Subject: [PATCH 168/569] [sign] Make OpenSSL support optional Now that Ring and OpenSSL support all mandatory algorithms, OpenSSL is no longer required in order to provide signing functionality. --- Cargo.toml | 2 +- src/sign/common.rs | 52 +++++++++++++++++++++++++++++++++------------ src/sign/openssl.rs | 3 +++ 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 279c1d054..5198b700e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ 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", "dep:openssl"] +unstable-sign = ["std", "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/sign/common.rs b/src/sign/common.rs index 9931aba59..8f03bcfe7 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -12,9 +12,15 @@ use crate::{ use super::{ generic::{self, GenerateParams}, - openssl, ring, SignError, SignRaw, + SignError, SignRaw, }; +#[cfg(feature = "openssl")] +use super::openssl; + +#[cfg(feature = "ring")] +use super::ring; + //----------- SecretKey ------------------------------------------------------ /// A key pair based on a built-in backend. @@ -29,6 +35,7 @@ pub enum SecretKey { Ring(ring::SecretKey), /// A key backed by OpenSSL. + #[cfg(feature = "openssl")] OpenSSL(openssl::SecretKey), } @@ -71,9 +78,14 @@ impl SecretKey { } // Fall back to OpenSSL. - Ok(Self::OpenSSL(openssl::SecretKey::from_generic( + #[cfg(feature = "openssl")] + return Ok(Self::OpenSSL(openssl::SecretKey::from_generic( secret, public, - )?)) + )?)); + + // Otherwise fail. + #[allow(unreachable_code)] + Err(FromGenericError::UnsupportedAlgorithm) } } @@ -84,6 +96,7 @@ impl SignRaw for SecretKey { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.algorithm(), + #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.algorithm(), } } @@ -92,6 +105,7 @@ impl SignRaw for SecretKey { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.raw_public_key(), + #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.raw_public_key(), } } @@ -100,6 +114,7 @@ impl SignRaw for SecretKey { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.sign_raw(data), + #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.sign_raw(data), } } @@ -124,8 +139,15 @@ pub fn generate( } // Fall back to OpenSSL. - let key = openssl::generate(params)?; - Ok((key.to_generic(), key.raw_public_key())) + #[cfg(feature = "openssl")] + { + let key = openssl::generate(params)?; + return Ok((key.to_generic(), key.raw_public_key())); + } + + // Otherwise fail. + #[allow(unreachable_code)] + Err(GenerateError::UnsupportedAlgorithm) } //============ Error Types =================================================== @@ -152,6 +174,7 @@ pub enum FromGenericError { //--- Conversions +#[cfg(feature = "ring")] impl From for FromGenericError { fn from(value: ring::FromGenericError) -> Self { match value { @@ -164,6 +187,7 @@ impl From for FromGenericError { } } +#[cfg(feature = "openssl")] impl From for FromGenericError { fn from(value: openssl::FromGenericError) -> Self { match value { @@ -209,24 +233,26 @@ pub enum GenerateError { //--- Conversion -impl From for GenerateError { - fn from(value: openssl::GenerateError) -> Self { +#[cfg(feature = "ring")] +impl From for GenerateError { + fn from(value: ring::GenerateError) -> Self { match value { - openssl::GenerateError::UnsupportedAlgorithm => { + ring::GenerateError::UnsupportedAlgorithm => { Self::UnsupportedAlgorithm } - openssl::GenerateError::Implementation => Self::Implementation, + ring::GenerateError::Implementation => Self::Implementation, } } } -impl From for GenerateError { - fn from(value: ring::GenerateError) -> Self { +#[cfg(feature = "openssl")] +impl From for GenerateError { + fn from(value: openssl::GenerateError) -> Self { match value { - ring::GenerateError::UnsupportedAlgorithm => { + openssl::GenerateError::UnsupportedAlgorithm => { Self::UnsupportedAlgorithm } - ring::GenerateError::Implementation => Self::Implementation, + openssl::GenerateError::Implementation => Self::Implementation, } } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index d1a0a2392..007908f3d 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,5 +1,8 @@ //! DNSSEC signing using OpenSSL. +#![cfg(feature = "openssl")] +#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] + use core::fmt; use std::vec::Vec; From e0d68ca8dcfecec7a6ef9b646cbddcac0aafed53 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:23:09 +0100 Subject: [PATCH 169/569] FIX: DNSKEY RRs must also be canonically ordered before signing. --- src/sign/records.rs | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 0d65f15a5..0914ace4b 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -100,7 +100,10 @@ impl SortedRecords { ) -> Result>>, ()> where N: ToName + Clone, - D: RecordData + ComposeRecordData + From>, + D: CanonicalOrd + + RecordData + + ComposeRecordData + + From>, SigningKey: SignRaw, Octets: AsRef<[u8]> + Clone @@ -168,18 +171,19 @@ impl SortedRecords { let apex_ttl = families.peek().unwrap().records().next().unwrap().ttl(); - let mut dnskey_rrs: Vec> = - Vec::with_capacity(keys.len()); + let mut dnskey_rrs = SortedRecords::new(); for public_key in keys.iter().map(|(_, public_key)| public_key) { let dnskey: Dnskey = Dnskey::convert(public_key.to_dnskey()); - dnskey_rrs.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - dnskey.clone().into(), - )); + dnskey_rrs + .insert(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + dnskey.clone().into(), + )) + .map_err(|_| ())?; res.push(Record::new( apex.owner().clone(), @@ -189,8 +193,7 @@ impl SortedRecords { )); } - let dnskeys_iter = RecordsIter::new(dnskey_rrs.as_slice()); - let families_iter = dnskeys_iter.chain(families); + let families_iter = dnskey_rrs.families().chain(families); for family in families_iter { // If the owner is out of zone, we have moved out of our zone and @@ -694,7 +697,7 @@ impl SortedRecords { } } -impl Default for SortedRecords { +impl Default for SortedRecords { fn default() -> Self { Self::new() } From 60cff586cc43414813bff37ed90b0da9115c95ef Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:52:16 +0100 Subject: [PATCH 170/569] Extend test file with records useful for manual testing of NSEC3. --- src/net/server/middleware/xfr/tests.rs | 27 ++++++++++++++++++++++++-- test-data/zonefiles/nsd-example.txt | 10 ++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index ec87646a2..a3e6dab2c 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -17,7 +17,7 @@ use octseq::Octets; use tokio::sync::Semaphore; use tokio::time::Instant; -use crate::base::iana::{Class, OptRcode, Rcode}; +use crate::base::iana::{Class, DigestAlg, OptRcode, Rcode, SecAlg}; use crate::base::{ Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl, }; @@ -32,7 +32,7 @@ use crate::net::server::service::{ CallResult, Service, ServiceError, ServiceFeedback, ServiceResult, }; use crate::rdata::{ - Aaaa, AllRecordData, Cname, Mx, Ns, Soa, Txt, ZoneRecordData, A, + Aaaa, AllRecordData, Cname, Ds, Mx, Ns, Soa, Txt, ZoneRecordData, A, }; use crate::tsig::{Algorithm, Key, KeyName}; use crate::zonefile::inplace::Zonefile; @@ -74,6 +74,29 @@ async fn axfr_with_example_zone() { (n("example.com"), Aaaa::new(p("2001:db8::3")).into()), (n("www.example.com"), Cname::new(n("example.com")).into()), (n("mail.example.com"), Mx::new(10, n("example.com")).into()), + (n("a.b.c.mail.example.com"), A::new(p("127.0.0.1")).into()), + ( + n("unsigned.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ds::new( + 60485, + SecAlg::RSASHA1, + DigestAlg::SHA1, + crate::utils::base16::decode( + "2BB183AF5F22588179A53B0A98631FAD1A292118", + ) + .unwrap(), + ) + .unwrap() + .into(), + ), (n("example.com"), zone_soa.into()), ]; diff --git a/test-data/zonefiles/nsd-example.txt b/test-data/zonefiles/nsd-example.txt index bedf91ac6..08e1cf488 100644 --- a/test-data/zonefiles/nsd-example.txt +++ b/test-data/zonefiles/nsd-example.txt @@ -21,3 +21,13 @@ example.com. A 192.0.2.1 www CNAME example.com. mail MX 10 example.com. + +; An ENT for NSEC3 testing purposes. +a.b.c.mail A 127.0.0.1 + +; An unsigned delegation for NSEC3 testing purposes. +unsigned NS some.other.ns.net + +; A signed delegation for NSEC3 testing purposes. +signed NS some.other.ns.net + DS 60485 5 1 ( 2BB183AF5F22588179A53B0A 98631FAD1A292118 ) From a4316b5ff4334478a3dd98ee60e53c1a2a369e4a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 13:59:46 +0100 Subject: [PATCH 171/569] [sign] Rename 'generic::SecretKey' to 'KeyBytes' --- src/sign/{generic.rs => bytes.rs} | 38 ++++++------- src/sign/common.rs | 59 +++++++++---------- src/sign/mod.rs | 21 +++---- src/sign/openssl.rs | 95 ++++++++++++++----------------- src/sign/ring.rs | 76 +++++++++++-------------- 5 files changed, 131 insertions(+), 158 deletions(-) rename src/sign/{generic.rs => bytes.rs} (95%) diff --git a/src/sign/generic.rs b/src/sign/bytes.rs similarity index 95% rename from src/sign/generic.rs rename to src/sign/bytes.rs index 922a9c79e..d2bceeb75 100644 --- a/src/sign/generic.rs +++ b/src/sign/bytes.rs @@ -9,9 +9,9 @@ use crate::base::iana::SecAlg; use crate::utils::base64; use crate::validate::RsaPublicKey; -//----------- SecretKey ------------------------------------------------------ +//----------- KeyBytes ------------------------------------------------------- -/// A generic secret key. +/// A secret key expressed as raw bytes. /// /// This is a low-level generic representation of a secret key from any one of /// the commonly supported signature algorithms. It is useful for abstracting @@ -82,9 +82,9 @@ use crate::validate::RsaPublicKey; /// interpreted as a big-endian integer. /// /// - For EdDSA, the private scalar of the key, as a fixed-width byte string. -pub enum SecretKey { +pub enum KeyBytes { /// An RSA/SHA-256 keypair. - RsaSha256(RsaSecretKey), + RsaSha256(RsaKeyBytes), /// An ECDSA P-256/SHA-256 keypair. /// @@ -109,7 +109,7 @@ pub enum SecretKey { //--- Inspection -impl SecretKey { +impl KeyBytes { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -124,7 +124,7 @@ impl SecretKey { //--- Converting to and from the BIND format. -impl SecretKey { +impl KeyBytes { /// Serialize this key in the conventional format used by BIND. /// /// The key is formatted in the private key v1.2 format and written to the @@ -217,7 +217,7 @@ impl SecretKey { match (code, name) { (8, "(RSASHA256)") => { - RsaSecretKey::parse_from_bind(data).map(Self::RsaSha256) + RsaKeyBytes::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -234,7 +234,7 @@ impl SecretKey { //--- Drop -impl Drop for SecretKey { +impl Drop for KeyBytes { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -247,13 +247,13 @@ impl Drop for SecretKey { } } -//----------- RsaSecretKey --------------------------------------------------- +//----------- RsaKeyBytes --------------------------------------------------- /// A generic RSA private key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. -pub struct RsaSecretKey { +pub struct RsaKeyBytes { /// The public modulus. pub n: Box<[u8]>, @@ -281,12 +281,12 @@ pub struct RsaSecretKey { //--- Conversion to and from the BIND format -impl RsaSecretKey { +impl RsaKeyBytes { /// Serialize this key in the conventional format used by BIND. /// /// The key is formatted in the private key v1.2 format and written to the /// given formatter. Note that the header and algorithm lines are not - /// written. See the type-level documentation of [`SecretKey`] for a + /// written. See the type-level documentation of [`KeyBytes`] for a /// description of this format. pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; @@ -313,7 +313,7 @@ impl RsaSecretKey { /// This parser supports the private key v1.2 format, but it should be /// compatible with any future v1.x key. Note that the header and /// algorithm lines are ignored. See the type-level documentation of - /// [`SecretKey`] for a description of this format. + /// [`KeyBytes`] for a description of this format. pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; @@ -374,8 +374,8 @@ impl RsaSecretKey { //--- Into -impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { - fn from(value: &'a RsaSecretKey) -> Self { +impl<'a> From<&'a RsaKeyBytes> for RsaPublicKey { + fn from(value: &'a RsaKeyBytes) -> Self { RsaPublicKey { n: value.n.clone(), e: value.e.clone(), @@ -385,7 +385,7 @@ impl<'a> From<&'a RsaSecretKey> for RsaPublicKey { //--- Drop -impl Drop for RsaSecretKey { +impl Drop for RsaKeyBytes { fn drop(&mut self) { // Zero the bytes for each field. self.n.fill(0u8); @@ -466,7 +466,7 @@ fn parse_dns_pair( //----------- BindFormatError ------------------------------------------------ -/// An error in loading a [`SecretKey`] from the conventional DNS format. +/// An error in loading a [`KeyBytes`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BindFormatError { /// The key file uses an unsupported version of the format. @@ -518,7 +518,7 @@ mod tests { format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::parse_from_bind(&data).unwrap(); + let key = super::KeyBytes::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -530,7 +530,7 @@ mod tests { format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKey::parse_from_bind(&data).unwrap(); + let key = super::KeyBytes::parse_from_bind(&data).unwrap(); let mut same = String::new(); key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); diff --git a/src/sign/common.rs b/src/sign/common.rs index 8f03bcfe7..516b52201 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -10,10 +10,7 @@ use crate::{ validate::{RawPublicKey, Signature}, }; -use super::{ - generic::{self, GenerateParams}, - SignError, SignRaw, -}; +use super::{GenerateParams, KeyBytes, SignError, SignRaw}; #[cfg(feature = "openssl")] use super::openssl; @@ -39,14 +36,14 @@ pub enum SecretKey { OpenSSL(openssl::SecretKey), } -//--- Conversion to and from generic keys +//--- Conversion to and from bytes keys impl SecretKey { - /// Use a generic secret key with OpenSSL. - pub fn from_generic( - secret: &generic::SecretKey, + /// Import a secret key from bytes. + pub fn from_bytes( + secret: &KeyBytes, public: &RawPublicKey, - ) -> Result { + ) -> Result { // Prefer Ring if it is available. #[cfg(feature = "ring")] match public { @@ -57,20 +54,20 @@ impl SecretKey { if k.n.len() >= 2048 / 8 => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_generic(secret, public, rng)?; + let key = ring::SecretKey::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } RawPublicKey::EcdsaP256Sha256(_) | RawPublicKey::EcdsaP384Sha384(_) => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_generic(secret, public, rng)?; + let key = ring::SecretKey::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } RawPublicKey::Ed25519(_) => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_generic(secret, public, rng)?; + let key = ring::SecretKey::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } @@ -79,13 +76,13 @@ impl SecretKey { // Fall back to OpenSSL. #[cfg(feature = "openssl")] - return Ok(Self::OpenSSL(openssl::SecretKey::from_generic( + return Ok(Self::OpenSSL(openssl::SecretKey::from_bytes( secret, public, )?)); // Otherwise fail. #[allow(unreachable_code)] - Err(FromGenericError::UnsupportedAlgorithm) + Err(FromBytesError::UnsupportedAlgorithm) } } @@ -125,7 +122,7 @@ impl SignRaw for SecretKey { /// Generate a new secret key for the given algorithm. pub fn generate( params: GenerateParams, -) -> Result<(generic::SecretKey, RawPublicKey), GenerateError> { +) -> Result<(KeyBytes, RawPublicKey), GenerateError> { // Use Ring if it is available. #[cfg(feature = "ring")] if matches!( @@ -142,7 +139,7 @@ pub fn generate( #[cfg(feature = "openssl")] { let key = openssl::generate(params)?; - return Ok((key.to_generic(), key.raw_public_key())); + return Ok((key.to_bytes(), key.raw_public_key())); } // Otherwise fail. @@ -152,11 +149,11 @@ pub fn generate( //============ Error Types =================================================== -//----------- FromGenericError ----------------------------------------------- +//----------- FromBytesError ----------------------------------------------- -/// An error in importing a key. +/// An error in importing a key from bytes. #[derive(Clone, Debug)] -pub enum FromGenericError { +pub enum FromBytesError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -175,34 +172,34 @@ pub enum FromGenericError { //--- Conversions #[cfg(feature = "ring")] -impl From for FromGenericError { - fn from(value: ring::FromGenericError) -> Self { +impl From for FromBytesError { + fn from(value: ring::FromBytesError) -> Self { match value { - ring::FromGenericError::UnsupportedAlgorithm => { + ring::FromBytesError::UnsupportedAlgorithm => { Self::UnsupportedAlgorithm } - ring::FromGenericError::InvalidKey => Self::InvalidKey, - ring::FromGenericError::WeakKey => Self::WeakKey, + ring::FromBytesError::InvalidKey => Self::InvalidKey, + ring::FromBytesError::WeakKey => Self::WeakKey, } } } #[cfg(feature = "openssl")] -impl From for FromGenericError { - fn from(value: openssl::FromGenericError) -> Self { +impl From for FromBytesError { + fn from(value: openssl::FromBytesError) -> Self { match value { - openssl::FromGenericError::UnsupportedAlgorithm => { + openssl::FromBytesError::UnsupportedAlgorithm => { Self::UnsupportedAlgorithm } - openssl::FromGenericError::InvalidKey => Self::InvalidKey, - openssl::FromGenericError::Implementation => Self::Implementation, + openssl::FromBytesError::InvalidKey => Self::InvalidKey, + openssl::FromBytesError::Implementation => Self::Implementation, } } } //--- Formatting -impl fmt::Display for FromGenericError { +impl fmt::Display for FromBytesError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -215,7 +212,7 @@ impl fmt::Display for FromGenericError { //--- Error -impl std::error::Error for FromGenericError {} +impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 306c9d790..39d5b2085 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -18,8 +18,10 @@ use crate::{ validate::{self, RawPublicKey, Signature}, }; +mod bytes; +pub use bytes::{GenerateParams, KeyBytes, RsaKeyBytes}; + pub mod common; -pub mod generic; pub mod openssl; pub mod ring; @@ -28,7 +30,7 @@ pub mod ring; /// A signing key. /// /// This associates important metadata with a raw cryptographic secret key. -pub struct SigningKey { +pub struct SigningKey { /// The owner of the key. owner: Name, @@ -43,7 +45,7 @@ pub struct SigningKey { //--- Construction -impl SigningKey { +impl SigningKey { /// Construct a new signing key manually. pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { Self { @@ -56,7 +58,7 @@ impl SigningKey { //--- Inspection -impl SigningKey { +impl SigningKey { /// The owner name attached to the key. pub fn owner(&self) -> &Name { &self.owner @@ -125,10 +127,7 @@ impl SigningKey { } /// The signing algorithm used. - pub fn algorithm(&self) -> SecAlg - where - Inner: SignRaw, - { + pub fn algorithm(&self) -> SecAlg { self.inner.algorithm() } @@ -136,17 +135,13 @@ impl SigningKey { pub fn public_key(&self) -> validate::Key<&Octs> where Octs: AsRef<[u8]>, - Inner: SignRaw, { let owner = Name::from_octets(self.owner.as_octets()).unwrap(); validate::Key::new(owner, self.flags, self.inner.raw_public_key()) } /// The associated raw public key. - pub fn raw_public_key(&self) -> RawPublicKey - where - Inner: SignRaw, - { + pub fn raw_public_key(&self) -> RawPublicKey { self.inner.raw_public_key() } } diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 007908f3d..244a529d1 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -18,10 +18,7 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{ - generic::{self, GenerateParams}, - SignError, SignRaw, -}; +use super::{GenerateParams, KeyBytes, RsaKeyBytes, SignError, SignRaw}; //----------- SecretKey ------------------------------------------------------ @@ -34,34 +31,31 @@ pub struct SecretKey { pkey: PKey, } -//--- Conversion to and from generic keys +//--- Conversion to and from bytes impl SecretKey { - /// Use a generic secret key with OpenSSL. - pub fn from_generic( - secret: &generic::SecretKey, + /// Import a secret key from bytes into OpenSSL. + pub fn from_bytes( + secret: &KeyBytes, public: &RawPublicKey, - ) -> Result { - fn num(slice: &[u8]) -> Result { + ) -> Result { + fn num(slice: &[u8]) -> Result { let mut v = BigNum::new()?; v.copy_from_slice(slice)?; Ok(v) } - fn secure_num(slice: &[u8]) -> Result { + fn secure_num(slice: &[u8]) -> Result { let mut v = BigNum::new_secure()?; v.copy_from_slice(slice)?; Ok(v) } let pkey = match (secret, public) { - ( - generic::SecretKey::RsaSha256(s), - RawPublicKey::RsaSha256(p), - ) => { + (KeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { - return Err(FromGenericError::InvalidKey); + return Err(FromBytesError::InvalidKey); } let n = num(&s.n)?; @@ -82,14 +76,14 @@ impl SecretKey { )?; if !key.check_key()? { - return Err(FromGenericError::InvalidKey); + return Err(FromBytesError::InvalidKey); } PKey::from_rsa(key)? } ( - generic::SecretKey::EcdsaP256Sha256(s), + KeyBytes::EcdsaP256Sha256(s), RawPublicKey::EcdsaP256Sha256(p), ) => { use openssl::{bn, ec, nid}; @@ -100,12 +94,12 @@ impl SecretKey { let n = secure_num(s.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(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; PKey::from_ec_key(k)? } ( - generic::SecretKey::EcdsaP384Sha384(s), + KeyBytes::EcdsaP384Sha384(s), RawPublicKey::EcdsaP384Sha384(p), ) => { use openssl::{bn, ec, nid}; @@ -116,11 +110,11 @@ impl SecretKey { let n = secure_num(s.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(|_| FromGenericError::InvalidKey)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; PKey::from_ec_key(k)? } - (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { + (KeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; @@ -128,11 +122,11 @@ impl SecretKey { if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { - return Err(FromGenericError::InvalidKey); + return Err(FromBytesError::InvalidKey); } } - (generic::SecretKey::Ed448(s), RawPublicKey::Ed448(p)) => { + (KeyBytes::Ed448(s), RawPublicKey::Ed448(p)) => { use openssl::memcmp; let id = pkey::Id::ED448; @@ -140,12 +134,12 @@ impl SecretKey { if memcmp::eq(&k.raw_public_key().unwrap(), &**p) { k } else { - return Err(FromGenericError::InvalidKey); + return Err(FromBytesError::InvalidKey); } } // The public and private key types did not match. - _ => return Err(FromGenericError::InvalidKey), + _ => return Err(FromBytesError::InvalidKey), }; Ok(Self { @@ -154,17 +148,17 @@ impl SecretKey { }) } - /// Export this key into a generic secret key. + /// Export this secret key into bytes. /// /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn to_generic(&self) -> generic::SecretKey { + pub fn to_bytes(&self) -> KeyBytes { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - generic::SecretKey::RsaSha256(generic::RsaSecretKey { + KeyBytes::RsaSha256(RsaKeyBytes { n: key.n().to_vec().into(), e: key.e().to_vec().into(), d: key.d().to_vec().into(), @@ -178,20 +172,20 @@ impl SecretKey { SecAlg::ECDSAP256SHA256 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(32).unwrap(); - generic::SecretKey::EcdsaP256Sha256(key.try_into().unwrap()) + KeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(48).unwrap(); - generic::SecretKey::EcdsaP384Sha384(key.try_into().unwrap()) + KeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); - generic::SecretKey::Ed25519(key.try_into().unwrap()) + KeyBytes::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_private_key().unwrap(); - generic::SecretKey::Ed448(key.try_into().unwrap()) + KeyBytes::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } @@ -355,11 +349,11 @@ pub fn generate(params: GenerateParams) -> Result { //============ Error Types =================================================== -//----------- FromGenericError ----------------------------------------------- +//----------- FromBytesError ----------------------------------------------- -/// An error in importing a key into OpenSSL. +/// An error in importing a key from bytes into OpenSSL. #[derive(Clone, Debug)] -pub enum FromGenericError { +pub enum FromBytesError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -374,7 +368,7 @@ pub enum FromGenericError { //--- Conversion -impl From for FromGenericError { +impl From for FromBytesError { fn from(_: ErrorStack) -> Self { Self::Implementation } @@ -382,7 +376,7 @@ impl From for FromGenericError { //--- Formatting -impl fmt::Display for FromGenericError { +impl fmt::Display for FromBytesError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -394,7 +388,7 @@ impl fmt::Display for FromGenericError { //--- Error -impl std::error::Error for FromGenericError {} +impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- @@ -441,10 +435,7 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{ - generic::{self, GenerateParams}, - SignRaw, - }, + sign::{GenerateParams, KeyBytes, SignRaw}, validate::Key, }; @@ -487,9 +478,9 @@ mod tests { }; let key = super::generate(params).unwrap(); - let gen_key = key.to_generic(); + let gen_key = key.to_bytes(); let pub_key = key.raw_public_key(); - let equiv = SecretKey::from_generic(&gen_key, &pub_key).unwrap(); + let equiv = SecretKey::from_bytes(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } } @@ -507,11 +498,11 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); - let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); + let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); - let equiv = key.to_generic(); + let equiv = key.to_bytes(); let mut same = String::new(); equiv.format_as_bind(&mut same).unwrap(); @@ -529,14 +520,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); + let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); assert_eq!(key.raw_public_key(), *pub_key); } @@ -550,14 +541,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, pub_key).unwrap(); + let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); let _ = key.sign_raw(b"Hello, World!").unwrap(); } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 9564ed812..f53e2ddcd 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -13,10 +13,7 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{ - generic::{self, GenerateParams}, - SignError, SignRaw, -}; +use super::{GenerateParams, KeyBytes, SignError, SignRaw}; //----------- SecretKey ------------------------------------------------------ @@ -44,28 +41,25 @@ pub enum SecretKey { Ed25519(ring::signature::Ed25519KeyPair), } -//--- Conversion from generic keys +//--- Conversion from bytes impl SecretKey { - /// Use a generic keypair with `ring`. - pub fn from_generic( - secret: &generic::SecretKey, + /// Import a secret key from bytes into OpenSSL. + pub fn from_bytes( + secret: &KeyBytes, public: &RawPublicKey, rng: Arc, - ) -> Result { + ) -> Result { match (secret, public) { - ( - generic::SecretKey::RsaSha256(s), - RawPublicKey::RsaSha256(p), - ) => { + (KeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { - return Err(FromGenericError::InvalidKey); + return Err(FromBytesError::InvalidKey); } // Ensure that the key is strong enough. if p.n.len() < 2048 / 8 { - return Err(FromGenericError::WeakKey); + return Err(FromBytesError::WeakKey); } let components = ring::rsa::KeyPairComponents { @@ -81,47 +75,47 @@ impl SecretKey { qInv: s.q_i.as_ref(), }; ring::signature::RsaKeyPair::from_components(&components) - .map_err(|_| FromGenericError::InvalidKey) + .map_err(|_| FromBytesError::InvalidKey) .map(|key| Self::RsaSha256 { key, rng }) } ( - generic::SecretKey::EcdsaP256Sha256(s), + KeyBytes::EcdsaP256Sha256(s), RawPublicKey::EcdsaP256Sha256(p), ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( alg, s.as_slice(), p.as_slice(), &*rng) - .map_err(|_| FromGenericError::InvalidKey) + .map_err(|_| FromBytesError::InvalidKey) .map(|key| Self::EcdsaP256Sha256 { key, rng }) } ( - generic::SecretKey::EcdsaP384Sha384(s), + KeyBytes::EcdsaP384Sha384(s), RawPublicKey::EcdsaP384Sha384(p), ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; ring::signature::EcdsaKeyPair::from_private_key_and_public_key( alg, s.as_slice(), p.as_slice(), &*rng) - .map_err(|_| FromGenericError::InvalidKey) + .map_err(|_| FromBytesError::InvalidKey) .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - (generic::SecretKey::Ed25519(s), RawPublicKey::Ed25519(p)) => { + (KeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { ring::signature::Ed25519KeyPair::from_seed_and_public_key( s.as_slice(), p.as_slice(), ) - .map_err(|_| FromGenericError::InvalidKey) + .map_err(|_| FromBytesError::InvalidKey) .map(Self::Ed25519) } - (generic::SecretKey::Ed448(_), RawPublicKey::Ed448(_)) => { - Err(FromGenericError::UnsupportedAlgorithm) + (KeyBytes::Ed448(_), RawPublicKey::Ed448(_)) => { + Err(FromBytesError::UnsupportedAlgorithm) } // The public and private key types did not match. - _ => Err(FromGenericError::InvalidKey), + _ => Err(FromBytesError::InvalidKey), } } } @@ -216,7 +210,7 @@ impl SignRaw for SecretKey { pub fn generate( params: GenerateParams, rng: &dyn ring::rand::SecureRandom, -) -> Result<(generic::SecretKey, RawPublicKey), GenerateError> { +) -> Result<(KeyBytes, RawPublicKey), GenerateError> { use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; match params { @@ -228,7 +222,7 @@ 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 = generic::SecretKey::EcdsaP256Sha256(sk); + let sk = KeyBytes::EcdsaP256Sha256(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); @@ -246,7 +240,7 @@ 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 = generic::SecretKey::EcdsaP384Sha384(sk); + let sk = KeyBytes::EcdsaP384Sha384(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); @@ -263,7 +257,7 @@ 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 = generic::SecretKey::Ed25519(sk); + let sk = KeyBytes::Ed25519(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); @@ -279,9 +273,9 @@ pub fn generate( //============ Error Types =================================================== -/// An error in importing a key into `ring`. +/// An error in importing a key from bytes into Ring. #[derive(Clone, Debug)] -pub enum FromGenericError { +pub enum FromBytesError { /// The requested algorithm was not supported. UnsupportedAlgorithm, @@ -294,7 +288,7 @@ pub enum FromGenericError { //--- Formatting -impl fmt::Display for FromGenericError { +impl fmt::Display for FromBytesError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match self { Self::UnsupportedAlgorithm => "algorithm not supported", @@ -306,7 +300,7 @@ impl fmt::Display for FromGenericError { //--- Error -impl std::error::Error for FromGenericError {} +impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- @@ -353,10 +347,7 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{ - generic::{self, GenerateParams}, - SignRaw, - }, + sign::{GenerateParams, KeyBytes, SignRaw}, validate::Key, }; @@ -384,14 +375,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_generic(&gen_key, pub_key, rng.clone()) + let key = SecretKey::from_bytes(&gen_key, pub_key, rng.clone()) .unwrap(); assert_eq!(key.raw_public_key(), *pub_key); @@ -403,7 +394,7 @@ mod tests { let rng = Arc::new(ring::rand::SystemRandom::new()); for params in GENERATE_PARAMS { let (sk, pk) = super::generate(params.clone(), &*rng).unwrap(); - let key = SecretKey::from_generic(&sk, &pk, rng.clone()).unwrap(); + let key = SecretKey::from_bytes(&sk, &pk, rng.clone()).unwrap(); assert_eq!(key.raw_public_key(), pk); } } @@ -417,15 +408,14 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = generic::SecretKey::parse_from_bind(&data).unwrap(); + let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = - SecretKey::from_generic(&gen_key, pub_key, rng).unwrap(); + let key = SecretKey::from_bytes(&gen_key, pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!").unwrap(); } From e0a4fc03ef054b45fbef799329a91819f29578ed Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 14:09:14 +0100 Subject: [PATCH 172/569] [sign] Rename 'SecretKey' to 'KeyPair' in all impls --- src/sign/common.rs | 28 ++++++++++++++-------------- src/sign/openssl.rs | 32 ++++++++++++++++---------------- src/sign/ring.rs | 32 ++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/sign/common.rs b/src/sign/common.rs index 516b52201..22ebfd7c2 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -18,7 +18,7 @@ use super::openssl; #[cfg(feature = "ring")] use super::ring; -//----------- SecretKey ------------------------------------------------------ +//----------- KeyPair -------------------------------------------------------- /// A key pair based on a built-in backend. /// @@ -26,20 +26,20 @@ use super::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. -pub enum SecretKey { +pub enum KeyPair { /// A key backed by Ring. #[cfg(feature = "ring")] - Ring(ring::SecretKey), + Ring(ring::KeyPair), /// A key backed by OpenSSL. #[cfg(feature = "openssl")] - OpenSSL(openssl::SecretKey), + OpenSSL(openssl::KeyPair), } -//--- Conversion to and from bytes keys +//--- Conversion to and from bytes -impl SecretKey { - /// Import a secret key from bytes. +impl KeyPair { + /// Import a key pair from bytes. pub fn from_bytes( secret: &KeyBytes, public: &RawPublicKey, @@ -54,20 +54,20 @@ impl SecretKey { if k.n.len() >= 2048 / 8 => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_bytes(secret, public, rng)?; + let key = ring::KeyPair::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } RawPublicKey::EcdsaP256Sha256(_) | RawPublicKey::EcdsaP384Sha384(_) => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_bytes(secret, public, rng)?; + let key = ring::KeyPair::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } RawPublicKey::Ed25519(_) => { let rng = Arc::new(SystemRandom::new()); - let key = ring::SecretKey::from_bytes(secret, public, rng)?; + let key = ring::KeyPair::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } @@ -76,7 +76,7 @@ impl SecretKey { // Fall back to OpenSSL. #[cfg(feature = "openssl")] - return Ok(Self::OpenSSL(openssl::SecretKey::from_bytes( + return Ok(Self::OpenSSL(openssl::KeyPair::from_bytes( secret, public, )?)); @@ -88,7 +88,7 @@ impl SecretKey { //--- SignRaw -impl SignRaw for SecretKey { +impl SignRaw for KeyPair { fn algorithm(&self) -> SecAlg { match self { #[cfg(feature = "ring")] @@ -151,7 +151,7 @@ pub fn generate( //----------- FromBytesError ----------------------------------------------- -/// An error in importing a key from bytes. +/// An error in importing a key pair from bytes. #[derive(Clone, Debug)] pub enum FromBytesError { /// The requested algorithm was not supported. @@ -216,7 +216,7 @@ impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- -/// An error in generating a key. +/// An error in generating a key pair. #[derive(Clone, Debug)] pub enum GenerateError { /// The requested algorithm was not supported. diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 244a529d1..9a7a3e159 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -20,10 +20,10 @@ use crate::{ use super::{GenerateParams, KeyBytes, RsaKeyBytes, SignError, SignRaw}; -//----------- SecretKey ------------------------------------------------------ +//----------- KeyPair -------------------------------------------------------- /// A key pair backed by OpenSSL. -pub struct SecretKey { +pub struct KeyPair { /// The algorithm used by the key. algorithm: SecAlg, @@ -33,8 +33,8 @@ pub struct SecretKey { //--- Conversion to and from bytes -impl SecretKey { - /// Import a secret key from bytes into OpenSSL. +impl KeyPair { + /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( secret: &KeyBytes, public: &RawPublicKey, @@ -148,7 +148,7 @@ impl SecretKey { }) } - /// Export this secret key into bytes. + /// Export the secret key into bytes. /// /// # Panics /// @@ -194,7 +194,7 @@ impl SecretKey { //--- Signing -impl SecretKey { +impl KeyPair { fn sign(&self, data: &[u8]) -> Result, ErrorStack> { use openssl::hash::MessageDigest; use openssl::sign::Signer; @@ -243,7 +243,7 @@ impl SecretKey { //--- SignRaw -impl SignRaw for SecretKey { +impl SignRaw for KeyPair { fn algorithm(&self) -> SecAlg { self.algorithm } @@ -324,7 +324,7 @@ impl SignRaw for SecretKey { //----------- generate() ----------------------------------------------------- /// Generate a new secret key for the given algorithm. -pub fn generate(params: GenerateParams) -> Result { +pub fn generate(params: GenerateParams) -> Result { let algorithm = params.algorithm(); let pkey = match params { GenerateParams::RsaSha256 { bits } => { @@ -344,14 +344,14 @@ pub fn generate(params: GenerateParams) -> Result { GenerateParams::Ed448 => PKey::generate_ed448()?, }; - Ok(SecretKey { algorithm, pkey }) + Ok(KeyPair { algorithm, pkey }) } //============ Error Types =================================================== //----------- FromBytesError ----------------------------------------------- -/// An error in importing a key from bytes into OpenSSL. +/// An error in importing a key pair from bytes into OpenSSL. #[derive(Clone, Debug)] pub enum FromBytesError { /// The requested algorithm was not supported. @@ -392,7 +392,7 @@ impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- -/// An error in generating a key with OpenSSL. +/// An error in generating a key pair with OpenSSL. #[derive(Clone, Debug)] pub enum GenerateError { /// The requested algorithm was not supported. @@ -439,7 +439,7 @@ mod tests { validate::Key, }; - use super::SecretKey; + use super::KeyPair; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), @@ -480,7 +480,7 @@ mod tests { let key = super::generate(params).unwrap(); let gen_key = key.to_bytes(); let pub_key = key.raw_public_key(); - let equiv = SecretKey::from_bytes(&gen_key, &pub_key).unwrap(); + let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } } @@ -500,7 +500,7 @@ mod tests { let data = std::fs::read_to_string(path).unwrap(); let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); - let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); let equiv = key.to_bytes(); let mut same = String::new(); @@ -527,7 +527,7 @@ mod tests { let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); assert_eq!(key.raw_public_key(), *pub_key); } @@ -548,7 +548,7 @@ mod tests { let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_bytes(&gen_key, pub_key).unwrap(); + let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); let _ = key.sign_raw(b"Hello, World!").unwrap(); } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index f53e2ddcd..8caf2c8ec 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -6,7 +6,7 @@ use core::fmt; use std::{boxed::Box, sync::Arc, vec::Vec}; -use ring::signature::KeyPair; +use ring::signature::KeyPair as _; use crate::{ base::iana::SecAlg, @@ -15,10 +15,10 @@ use crate::{ use super::{GenerateParams, KeyBytes, SignError, SignRaw}; -//----------- SecretKey ------------------------------------------------------ +//----------- KeyPair -------------------------------------------------------- /// A key pair backed by `ring`. -pub enum SecretKey { +pub enum KeyPair { /// An RSA/SHA-256 keypair. RsaSha256 { key: ring::signature::RsaKeyPair, @@ -43,8 +43,8 @@ pub enum SecretKey { //--- Conversion from bytes -impl SecretKey { - /// Import a secret key from bytes into OpenSSL. +impl KeyPair { + /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( secret: &KeyBytes, public: &RawPublicKey, @@ -122,7 +122,7 @@ impl SecretKey { //--- SignRaw -impl SignRaw for SecretKey { +impl SignRaw for KeyPair { fn algorithm(&self) -> SecAlg { match self { Self::RsaSha256 { .. } => SecAlg::RSASHA256, @@ -206,7 +206,11 @@ impl SignRaw for SecretKey { //----------- generate() ----------------------------------------------------- -/// Generate a new secret key for the given algorithm. +/// Generate a new key pair for the given algorithm. +/// +/// While this uses Ring internally, the opaque nature of Ring means that it +/// is not possible to export a secret key from [`KeyPair`]. Thus, the bytes +/// of the secret key are returned directly. pub fn generate( params: GenerateParams, rng: &dyn ring::rand::SecureRandom, @@ -273,7 +277,7 @@ pub fn generate( //============ Error Types =================================================== -/// An error in importing a key from bytes into Ring. +/// An error in importing a key pair from bytes into Ring. #[derive(Clone, Debug)] pub enum FromBytesError { /// The requested algorithm was not supported. @@ -304,7 +308,7 @@ impl std::error::Error for FromBytesError {} //----------- GenerateError -------------------------------------------------- -/// An error in generating a key with Ring. +/// An error in generating a key pair with Ring. #[derive(Clone, Debug)] pub enum GenerateError { /// The requested algorithm was not supported. @@ -351,7 +355,7 @@ mod tests { validate::Key, }; - use super::SecretKey; + use super::KeyPair; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), @@ -382,8 +386,8 @@ mod tests { let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_bytes(&gen_key, pub_key, rng.clone()) - .unwrap(); + let key = + KeyPair::from_bytes(&gen_key, pub_key, rng.clone()).unwrap(); assert_eq!(key.raw_public_key(), *pub_key); } @@ -394,7 +398,7 @@ mod tests { let rng = Arc::new(ring::rand::SystemRandom::new()); for params in GENERATE_PARAMS { let (sk, pk) = super::generate(params.clone(), &*rng).unwrap(); - let key = SecretKey::from_bytes(&sk, &pk, rng.clone()).unwrap(); + let key = KeyPair::from_bytes(&sk, &pk, rng.clone()).unwrap(); assert_eq!(key.raw_public_key(), pk); } } @@ -415,7 +419,7 @@ mod tests { let pub_key = Key::>::parse_from_bind(&data).unwrap(); let pub_key = pub_key.raw_public_key(); - let key = SecretKey::from_bytes(&gen_key, pub_key, rng).unwrap(); + let key = KeyPair::from_bytes(&gen_key, pub_key, rng).unwrap(); let _ = key.sign_raw(b"Hello, World!").unwrap(); } From eaea464a283dc74258e1df755819873a2f0875cb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:11:04 +0100 Subject: [PATCH 173/569] Merge fixes missed from the last commit. --- src/sign/records.rs | 60 +++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 0914ace4b..7da6e0b41 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -13,7 +13,7 @@ use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; use tracing::{debug, enabled, Level}; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, Nsec3HashAlg, Rtype, SecAlg}; +use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; use crate::base::name::{ToLabelIter, ToName}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; @@ -24,10 +24,9 @@ use crate::rdata::dnssec::{ use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, ZoneRecordData}; use crate::utils::base32; -use crate::validate; use super::ring::{nsec3_hash, Nsec3HashError}; -use super::SignRaw; +use super::{SignRaw, SigningKey}; //------------ SortedRecords ------------------------------------------------- @@ -91,12 +90,12 @@ impl SortedRecords { /// AND has the SEP flag set, it will be used as a CSK (i.e. both KSK and /// ZSK). #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - keys: &[(SigningKey, validate::Key)], // private, public key pair + keys: &[SigningKey], ) -> Result>>, ()> where N: ToName + Clone, @@ -104,35 +103,16 @@ impl SortedRecords { + RecordData + ComposeRecordData + From>, - SigningKey: SignRaw, + ConcreteSecretKey: SignRaw, Octets: AsRef<[u8]> + Clone + From> + octseq::OctetsFrom>, { - // Per RFC 8624 section 3.1 "DNSSEC Signing" column guidance. - let unsupported_algorithms = [ - SecAlg::RSAMD5, - SecAlg::DSA, - SecAlg::DSA_NSEC3_SHA1, - SecAlg::ECC_GOST, - ]; - - let mut ksks: Vec<&(SigningKey, validate::Key)> = keys + let (mut ksks, mut zsks): (Vec<_>, Vec<_>) = keys .iter() - .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) - .filter(|(_, dk)| { - dk.is_zone_signing_key() && dk.is_secure_entry_point() - }) - .collect(); - - let mut zsks: Vec<&(SigningKey, validate::Key)> = keys - .iter() - .filter(|(k, _)| !unsupported_algorithms.contains(&k.algorithm())) - .filter(|(_, dk)| { - dk.is_zone_signing_key() && !dk.is_secure_entry_point() - }) - .collect(); + .filter(|k| k.is_zone_signing_key()) + .partition(|k| k.is_secure_entry_point()); // CSK? if !ksks.is_empty() && zsks.is_empty() { @@ -144,13 +124,12 @@ impl SortedRecords { if enabled!(Level::DEBUG) { for key in keys { debug!( - "Key : {} [supported={}], owner={}, flags={} (SEP={}, ZSK={}))", - key.0.algorithm(), - !unsupported_algorithms.contains(&key.0.algorithm()), - key.1.owner(), - key.1.flags(), - key.1.is_secure_entry_point(), - key.1.is_zone_signing_key(), + "Key : {}, owner={}, flags={} (SEP={}, ZSK={}))", + key.algorithm(), + key.owner(), + key.flags(), + key.is_secure_entry_point(), + key.is_zone_signing_key(), ) } debug!("# KSKs: {}", ksks.len()); @@ -173,7 +152,7 @@ impl SortedRecords { let mut dnskey_rrs = SortedRecords::new(); - for public_key in keys.iter().map(|(_, public_key)| public_key) { + for public_key in keys.iter().map(|k| k.public_key()) { let dnskey: Dnskey = Dnskey::convert(public_key.to_dnskey()); dnskey_rrs @@ -244,15 +223,15 @@ impl SortedRecords { &zsks }; - for (private_key, public_key) in keys { + for key in keys { let rrsig = ProtoRrsig::new( rrset.rtype(), - private_key.algorithm(), + key.algorithm(), name.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, - public_key.key_tag(), + key.public_key().key_tag(), apex.owner().clone(), ); @@ -261,7 +240,8 @@ impl SortedRecords { for record in rrset.iter() { record.compose_canonical(&mut buf).unwrap(); } - let signature = private_key.sign_raw(&buf); + let signature = + key.raw_secret_key().sign_raw(&buf).unwrap(); let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { return Err(()); From 48e178a1e1541b74ba48b5aa9780e719d5d8cc92 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 14:18:04 +0100 Subject: [PATCH 174/569] [sign] Rename 'KeyBytes' to 'SecretKeyBytes' For consistency with the upcoming 'PublicKeyBytes'. --- src/sign/bytes.rs | 89 ++++++++++++++------------------------------- src/sign/common.rs | 6 +-- src/sign/mod.rs | 38 ++++++++++++++++++- src/sign/openssl.rs | 36 +++++++++--------- src/sign/ring.rs | 66 +++++++++++++++++++-------------- 5 files changed, 124 insertions(+), 111 deletions(-) diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index d2bceeb75..d0d3caab1 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -9,7 +9,7 @@ use crate::base::iana::SecAlg; use crate::utils::base64; use crate::validate::RsaPublicKey; -//----------- KeyBytes ------------------------------------------------------- +//----------- SecretKeyBytes ------------------------------------------------- /// A secret key expressed as raw bytes. /// @@ -82,9 +82,9 @@ use crate::validate::RsaPublicKey; /// interpreted as a big-endian integer. /// /// - For EdDSA, the private scalar of the key, as a fixed-width byte string. -pub enum KeyBytes { +pub enum SecretKeyBytes { /// An RSA/SHA-256 keypair. - RsaSha256(RsaKeyBytes), + RsaSha256(RsaSecretKeyBytes), /// An ECDSA P-256/SHA-256 keypair. /// @@ -109,7 +109,7 @@ pub enum KeyBytes { //--- Inspection -impl KeyBytes { +impl SecretKeyBytes { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -122,10 +122,10 @@ impl KeyBytes { } } -//--- Converting to and from the BIND format. +//--- Converting to and from the BIND format -impl KeyBytes { - /// Serialize this key in the conventional format used by BIND. +impl SecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. /// /// The key is formatted in the private key v1.2 format and written to the /// given formatter. See the type-level documentation for a description @@ -160,7 +160,7 @@ impl KeyBytes { } } - /// Parse a key from the conventional format used by BIND. + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be /// compatible with any future v1.x key. See the type-level documentation @@ -217,7 +217,7 @@ impl KeyBytes { match (code, name) { (8, "(RSASHA256)") => { - RsaKeyBytes::parse_from_bind(data).map(Self::RsaSha256) + RsaSecretKeyBytes::parse_from_bind(data).map(Self::RsaSha256) } (13, "(ECDSAP256SHA256)") => { parse_pkey(data).map(Self::EcdsaP256Sha256) @@ -234,7 +234,7 @@ impl KeyBytes { //--- Drop -impl Drop for KeyBytes { +impl Drop for SecretKeyBytes { fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -247,13 +247,14 @@ impl Drop for KeyBytes { } } -//----------- RsaKeyBytes --------------------------------------------------- +//----------- RsaSecretKeyBytes --------------------------------------------------- -/// A generic RSA private key. +/// An RSA secret key expressed as raw bytes. /// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -pub struct RsaKeyBytes { +/// All fields here are arbitrary-precision integers in big-endian format. +/// The public values, `n` and `e`, must not have leading zeros; the remaining +/// values may be padded with leading zeros. +pub struct RsaSecretKeyBytes { /// The public modulus. pub n: Box<[u8]>, @@ -281,12 +282,12 @@ pub struct RsaKeyBytes { //--- Conversion to and from the BIND format -impl RsaKeyBytes { - /// Serialize this key in the conventional format used by BIND. +impl RsaSecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. /// /// The key is formatted in the private key v1.2 format and written to the /// given formatter. Note that the header and algorithm lines are not - /// written. See the type-level documentation of [`KeyBytes`] for a + /// written. See the type-level documentation of [`SecretKeyBytes`] for a /// description of this format. pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; @@ -308,12 +309,12 @@ impl RsaKeyBytes { Ok(()) } - /// Parse a key from the conventional format used by BIND. + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be /// compatible with any future v1.x key. Note that the header and /// algorithm lines are ignored. See the type-level documentation of - /// [`KeyBytes`] for a description of this format. + /// [`SecretKeyBytes`] for a description of this format. pub fn parse_from_bind(mut data: &str) -> Result { let mut n = None; let mut e = None; @@ -374,8 +375,8 @@ impl RsaKeyBytes { //--- Into -impl<'a> From<&'a RsaKeyBytes> for RsaPublicKey { - fn from(value: &'a RsaKeyBytes) -> Self { +impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKey { + fn from(value: &'a RsaSecretKeyBytes) -> Self { RsaPublicKey { n: value.n.clone(), e: value.e.clone(), @@ -385,7 +386,7 @@ impl<'a> From<&'a RsaKeyBytes> for RsaPublicKey { //--- Drop -impl Drop for RsaKeyBytes { +impl Drop for RsaSecretKeyBytes { fn drop(&mut self) { // Zero the bytes for each field. self.n.fill(0u8); @@ -399,42 +400,6 @@ impl Drop for RsaKeyBytes { } } -//----------- GenerateParams ------------------------------------------------- - -/// Parameters for generating a secret key. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum GenerateParams { - /// Generate an RSA/SHA-256 keypair. - RsaSha256 { bits: u32 }, - - /// Generate an ECDSA P-256/SHA-256 keypair. - EcdsaP256Sha256, - - /// Generate an ECDSA P-384/SHA-384 keypair. - EcdsaP384Sha384, - - /// Generate an Ed25519 keypair. - Ed25519, - - /// An Ed448 keypair. - Ed448, -} - -//--- Inspection - -impl GenerateParams { - /// The algorithm of the generated key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256 { .. } => SecAlg::RSASHA256, - Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, - Self::Ed25519 => SecAlg::ED25519, - Self::Ed448 => SecAlg::ED448, - } - } -} - //----------- Helpers for parsing the BIND format ---------------------------- /// Extract the next key-value pair in a DNS private key file. @@ -466,7 +431,7 @@ fn parse_dns_pair( //----------- BindFormatError ------------------------------------------------ -/// An error in loading a [`KeyBytes`] from the conventional DNS format. +/// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum BindFormatError { /// The key file uses an unsupported version of the format. @@ -518,7 +483,7 @@ mod tests { format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::KeyBytes::parse_from_bind(&data).unwrap(); + let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); assert_eq!(key.algorithm(), algorithm); } } @@ -530,7 +495,7 @@ mod tests { format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let key = super::KeyBytes::parse_from_bind(&data).unwrap(); + let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); let mut same = String::new(); key.format_as_bind(&mut same).unwrap(); let data = data.lines().collect::>(); diff --git a/src/sign/common.rs b/src/sign/common.rs index 22ebfd7c2..4a6a1cd97 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -10,7 +10,7 @@ use crate::{ validate::{RawPublicKey, Signature}, }; -use super::{GenerateParams, KeyBytes, SignError, SignRaw}; +use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; #[cfg(feature = "openssl")] use super::openssl; @@ -41,7 +41,7 @@ pub enum KeyPair { impl KeyPair { /// Import a key pair from bytes. pub fn from_bytes( - secret: &KeyBytes, + secret: &SecretKeyBytes, public: &RawPublicKey, ) -> Result { // Prefer Ring if it is available. @@ -122,7 +122,7 @@ impl SignRaw for KeyPair { /// Generate a new secret key for the given algorithm. pub fn generate( params: GenerateParams, -) -> Result<(KeyBytes, RawPublicKey), GenerateError> { +) -> Result<(SecretKeyBytes, RawPublicKey), GenerateError> { // Use Ring if it is available. #[cfg(feature = "ring")] if matches!( diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 39d5b2085..b2ff17db7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -19,7 +19,7 @@ use crate::{ }; mod bytes; -pub use bytes::{GenerateParams, KeyBytes, RsaKeyBytes}; +pub use bytes::{RsaSecretKeyBytes, SecretKeyBytes}; pub mod common; pub mod openssl; @@ -188,6 +188,42 @@ pub trait SignRaw { fn sign_raw(&self, data: &[u8]) -> Result; } +//----------- GenerateParams ------------------------------------------------- + +/// Parameters for generating a secret key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GenerateParams { + /// Generate an RSA/SHA-256 keypair. + RsaSha256 { bits: u32 }, + + /// Generate an ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256, + + /// Generate an ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384, + + /// Generate an Ed25519 keypair. + Ed25519, + + /// An Ed448 keypair. + Ed448, +} + +//--- Inspection + +impl GenerateParams { + /// The algorithm of the generated key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, + Self::Ed25519 => SecAlg::ED25519, + Self::Ed448 => SecAlg::ED448, + } + } +} + //============ Error Types =================================================== //----------- SignError ------------------------------------------------------ diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 9a7a3e159..4fce3566e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -18,7 +18,9 @@ use crate::{ validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{GenerateParams, KeyBytes, RsaKeyBytes, SignError, SignRaw}; +use super::{ + GenerateParams, RsaSecretKeyBytes, SecretKeyBytes, SignError, SignRaw, +}; //----------- KeyPair -------------------------------------------------------- @@ -36,7 +38,7 @@ pub struct KeyPair { impl KeyPair { /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( - secret: &KeyBytes, + secret: &SecretKeyBytes, public: &RawPublicKey, ) -> Result { fn num(slice: &[u8]) -> Result { @@ -52,7 +54,7 @@ impl KeyPair { } let pkey = match (secret, public) { - (KeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { + (SecretKeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromBytesError::InvalidKey); @@ -83,7 +85,7 @@ impl KeyPair { } ( - KeyBytes::EcdsaP256Sha256(s), + SecretKeyBytes::EcdsaP256Sha256(s), RawPublicKey::EcdsaP256Sha256(p), ) => { use openssl::{bn, ec, nid}; @@ -99,7 +101,7 @@ impl KeyPair { } ( - KeyBytes::EcdsaP384Sha384(s), + SecretKeyBytes::EcdsaP384Sha384(s), RawPublicKey::EcdsaP384Sha384(p), ) => { use openssl::{bn, ec, nid}; @@ -114,7 +116,7 @@ impl KeyPair { PKey::from_ec_key(k)? } - (KeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { + (SecretKeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; @@ -126,7 +128,7 @@ impl KeyPair { } } - (KeyBytes::Ed448(s), RawPublicKey::Ed448(p)) => { + (SecretKeyBytes::Ed448(s), RawPublicKey::Ed448(p)) => { use openssl::memcmp; let id = pkey::Id::ED448; @@ -153,12 +155,12 @@ impl KeyPair { /// # Panics /// /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn to_bytes(&self) -> KeyBytes { + pub fn to_bytes(&self) -> SecretKeyBytes { // TODO: Consider security implications of secret data in 'Vec's. match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - KeyBytes::RsaSha256(RsaKeyBytes { + SecretKeyBytes::RsaSha256(RsaSecretKeyBytes { n: key.n().to_vec().into(), e: key.e().to_vec().into(), d: key.d().to_vec().into(), @@ -172,20 +174,20 @@ impl KeyPair { SecAlg::ECDSAP256SHA256 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(32).unwrap(); - KeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) + SecretKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); let key = key.private_key().to_vec_padded(48).unwrap(); - KeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) + SecretKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_private_key().unwrap(); - KeyBytes::Ed25519(key.try_into().unwrap()) + SecretKeyBytes::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_private_key().unwrap(); - KeyBytes::Ed448(key.try_into().unwrap()) + SecretKeyBytes::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } @@ -435,7 +437,7 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{GenerateParams, KeyBytes, SignRaw}, + sign::{GenerateParams, SecretKeyBytes, SignRaw}, validate::Key, }; @@ -498,7 +500,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); @@ -520,7 +522,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -541,7 +543,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 8caf2c8ec..084786812 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -6,14 +6,16 @@ use core::fmt; use std::{boxed::Box, sync::Arc, vec::Vec}; -use ring::signature::KeyPair as _; +use ring::signature::{ + EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, +}; use crate::{ base::iana::SecAlg, validate::{RawPublicKey, RsaPublicKey, Signature}, }; -use super::{GenerateParams, KeyBytes, SignError, SignRaw}; +use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; //----------- KeyPair -------------------------------------------------------- @@ -21,24 +23,24 @@ use super::{GenerateParams, KeyBytes, SignError, SignRaw}; pub enum KeyPair { /// An RSA/SHA-256 keypair. RsaSha256 { - key: ring::signature::RsaKeyPair, + key: RsaKeyPair, rng: Arc, }, /// An ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256 { - key: ring::signature::EcdsaKeyPair, + key: EcdsaKeyPair, rng: Arc, }, /// An ECDSA P-384/SHA-384 keypair. EcdsaP384Sha384 { - key: ring::signature::EcdsaKeyPair, + key: EcdsaKeyPair, rng: Arc, }, /// An Ed25519 keypair. - Ed25519(ring::signature::Ed25519KeyPair), + Ed25519(Ed25519KeyPair), } //--- Conversion from bytes @@ -46,12 +48,12 @@ pub enum KeyPair { impl KeyPair { /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( - secret: &KeyBytes, + secret: &SecretKeyBytes, public: &RawPublicKey, rng: Arc, ) -> Result { match (secret, public) { - (KeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { + (SecretKeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { // Ensure that the public and private key match. if p != &RsaPublicKey::from(s) { return Err(FromBytesError::InvalidKey); @@ -80,29 +82,37 @@ impl KeyPair { } ( - KeyBytes::EcdsaP256Sha256(s), + SecretKeyBytes::EcdsaP256Sha256(s), RawPublicKey::EcdsaP256Sha256(p), ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; - ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), &*rng) - .map_err(|_| FromBytesError::InvalidKey) - .map(|key| Self::EcdsaP256Sha256 { key, rng }) + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.as_slice(), + p.as_slice(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { key, rng }) } ( - KeyBytes::EcdsaP384Sha384(s), + SecretKeyBytes::EcdsaP384Sha384(s), RawPublicKey::EcdsaP384Sha384(p), ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; - ring::signature::EcdsaKeyPair::from_private_key_and_public_key( - alg, s.as_slice(), p.as_slice(), &*rng) - .map_err(|_| FromBytesError::InvalidKey) - .map(|key| Self::EcdsaP384Sha384 { key, rng }) + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.as_slice(), + p.as_slice(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - (KeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { - ring::signature::Ed25519KeyPair::from_seed_and_public_key( + (SecretKeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { + Ed25519KeyPair::from_seed_and_public_key( s.as_slice(), p.as_slice(), ) @@ -110,7 +120,7 @@ impl KeyPair { .map(Self::Ed25519) } - (KeyBytes::Ed448(_), RawPublicKey::Ed448(_)) => { + (SecretKeyBytes::Ed448(_), RawPublicKey::Ed448(_)) => { Err(FromBytesError::UnsupportedAlgorithm) } @@ -214,7 +224,7 @@ impl SignRaw for KeyPair { pub fn generate( params: GenerateParams, rng: &dyn ring::rand::SecureRandom, -) -> Result<(KeyBytes, RawPublicKey), GenerateError> { +) -> Result<(SecretKeyBytes, RawPublicKey), GenerateError> { use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; match params { @@ -226,7 +236,7 @@ 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 = KeyBytes::EcdsaP256Sha256(sk); + let sk = SecretKeyBytes::EcdsaP256Sha256(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); @@ -244,7 +254,7 @@ 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 = KeyBytes::EcdsaP384Sha384(sk); + let sk = SecretKeyBytes::EcdsaP384Sha384(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); @@ -261,7 +271,7 @@ 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 = KeyBytes::Ed25519(sk); + let sk = SecretKeyBytes::Ed25519(sk); // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); @@ -351,7 +361,7 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{GenerateParams, KeyBytes, SignRaw}, + sign::{GenerateParams, SecretKeyBytes, SignRaw}, validate::Key, }; @@ -379,7 +389,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); @@ -412,7 +422,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); - let gen_key = KeyBytes::parse_from_bind(&data).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); From daa96d86f50952bb0dae708c9b2a6e48f5747bd9 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 14:23:38 +0100 Subject: [PATCH 175/569] [validate] Rename 'RawPublicKey' to 'PublicKeyBytes' --- src/sign/bytes.rs | 8 +++---- src/sign/common.rs | 22 +++++++++--------- src/sign/mod.rs | 8 +++---- src/sign/openssl.rs | 28 +++++++++++------------ src/sign/ring.rs | 34 +++++++++++++-------------- src/validate.rs | 56 ++++++++++++++++++++++----------------------- 6 files changed, 77 insertions(+), 79 deletions(-) diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index d0d3caab1..5b49f3328 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -7,7 +7,7 @@ use std::vec::Vec; use crate::base::iana::SecAlg; use crate::utils::base64; -use crate::validate::RsaPublicKey; +use crate::validate::RsaPublicKeyBytes; //----------- SecretKeyBytes ------------------------------------------------- @@ -373,11 +373,11 @@ impl RsaSecretKeyBytes { } } -//--- Into +//--- Into -impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKey { +impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { fn from(value: &'a RsaSecretKeyBytes) -> Self { - RsaPublicKey { + RsaPublicKeyBytes { n: value.n.clone(), e: value.e.clone(), } diff --git a/src/sign/common.rs b/src/sign/common.rs index 4a6a1cd97..d5aaf5b67 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -7,7 +7,7 @@ use ::ring::rand::SystemRandom; use crate::{ base::iana::SecAlg, - validate::{RawPublicKey, Signature}, + validate::{PublicKeyBytes, Signature}, }; use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; @@ -42,15 +42,15 @@ impl KeyPair { /// Import a key pair from bytes. pub fn from_bytes( secret: &SecretKeyBytes, - public: &RawPublicKey, + public: &PublicKeyBytes, ) -> Result { // Prefer Ring if it is available. #[cfg(feature = "ring")] match public { - RawPublicKey::RsaSha1(k) - | RawPublicKey::RsaSha1Nsec3Sha1(k) - | RawPublicKey::RsaSha256(k) - | RawPublicKey::RsaSha512(k) + PublicKeyBytes::RsaSha1(k) + | PublicKeyBytes::RsaSha1Nsec3Sha1(k) + | PublicKeyBytes::RsaSha256(k) + | PublicKeyBytes::RsaSha512(k) if k.n.len() >= 2048 / 8 => { let rng = Arc::new(SystemRandom::new()); @@ -58,14 +58,14 @@ impl KeyPair { return Ok(Self::Ring(key)); } - RawPublicKey::EcdsaP256Sha256(_) - | RawPublicKey::EcdsaP384Sha384(_) => { + PublicKeyBytes::EcdsaP256Sha256(_) + | PublicKeyBytes::EcdsaP384Sha384(_) => { let rng = Arc::new(SystemRandom::new()); let key = ring::KeyPair::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); } - RawPublicKey::Ed25519(_) => { + PublicKeyBytes::Ed25519(_) => { let rng = Arc::new(SystemRandom::new()); let key = ring::KeyPair::from_bytes(secret, public, rng)?; return Ok(Self::Ring(key)); @@ -98,7 +98,7 @@ impl SignRaw for KeyPair { } } - fn raw_public_key(&self) -> RawPublicKey { + fn raw_public_key(&self) -> PublicKeyBytes { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.raw_public_key(), @@ -122,7 +122,7 @@ impl SignRaw for KeyPair { /// Generate a new secret key for the given algorithm. pub fn generate( params: GenerateParams, -) -> Result<(SecretKeyBytes, RawPublicKey), GenerateError> { +) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { // Use Ring if it is available. #[cfg(feature = "ring")] if matches!( diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b2ff17db7..b365a78f5 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -15,11 +15,11 @@ use core::fmt; use crate::{ base::{iana::SecAlg, Name}, - validate::{self, RawPublicKey, Signature}, + validate::{self, PublicKeyBytes, Signature}, }; mod bytes; -pub use bytes::{RsaSecretKeyBytes, SecretKeyBytes}; +pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; pub mod common; pub mod openssl; @@ -141,7 +141,7 @@ impl SigningKey { } /// The associated raw public key. - pub fn raw_public_key(&self) -> RawPublicKey { + pub fn raw_public_key(&self) -> PublicKeyBytes { self.inner.raw_public_key() } } @@ -176,7 +176,7 @@ pub trait SignRaw { /// algorithm as returned by [`algorithm()`]. /// /// [`algorithm()`]: Self::algorithm() - fn raw_public_key(&self) -> RawPublicKey; + fn raw_public_key(&self) -> PublicKeyBytes; /// Sign the given bytes. /// diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 4fce3566e..c5620c22e 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -15,7 +15,7 @@ use openssl::{ use crate::{ base::iana::SecAlg, - validate::{RawPublicKey, RsaPublicKey, Signature}, + validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, }; use super::{ @@ -39,7 +39,7 @@ impl KeyPair { /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( secret: &SecretKeyBytes, - public: &RawPublicKey, + public: &PublicKeyBytes, ) -> Result { fn num(slice: &[u8]) -> Result { let mut v = BigNum::new()?; @@ -54,9 +54,9 @@ impl KeyPair { } let pkey = match (secret, public) { - (SecretKeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { + (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { // Ensure that the public and private key match. - if p != &RsaPublicKey::from(s) { + if p != &RsaPublicKeyBytes::from(s) { return Err(FromBytesError::InvalidKey); } @@ -86,7 +86,7 @@ impl KeyPair { ( SecretKeyBytes::EcdsaP256Sha256(s), - RawPublicKey::EcdsaP256Sha256(p), + PublicKeyBytes::EcdsaP256Sha256(p), ) => { use openssl::{bn, ec, nid}; @@ -102,7 +102,7 @@ impl KeyPair { ( SecretKeyBytes::EcdsaP384Sha384(s), - RawPublicKey::EcdsaP384Sha384(p), + PublicKeyBytes::EcdsaP384Sha384(p), ) => { use openssl::{bn, ec, nid}; @@ -116,7 +116,7 @@ impl KeyPair { PKey::from_ec_key(k)? } - (SecretKeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { + (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { use openssl::memcmp; let id = pkey::Id::ED25519; @@ -128,7 +128,7 @@ impl KeyPair { } } - (SecretKeyBytes::Ed448(s), RawPublicKey::Ed448(p)) => { + (SecretKeyBytes::Ed448(s), PublicKeyBytes::Ed448(p)) => { use openssl::memcmp; let id = pkey::Id::ED448; @@ -250,11 +250,11 @@ impl SignRaw for KeyPair { self.algorithm } - fn raw_public_key(&self) -> RawPublicKey { + fn raw_public_key(&self) -> PublicKeyBytes { match self.algorithm { SecAlg::RSASHA256 => { let key = self.pkey.rsa().unwrap(); - RawPublicKey::RsaSha256(RsaPublicKey { + PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { n: key.n().to_vec().into(), e: key.e().to_vec().into(), }) @@ -267,7 +267,7 @@ impl SignRaw for KeyPair { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) } SecAlg::ECDSAP384SHA384 => { let key = self.pkey.ec_key().unwrap(); @@ -277,15 +277,15 @@ impl SignRaw for KeyPair { .public_key() .to_bytes(key.group(), form, &mut ctx) .unwrap(); - RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) } SecAlg::ED25519 => { let key = self.pkey.raw_public_key().unwrap(); - RawPublicKey::Ed25519(key.try_into().unwrap()) + PublicKeyBytes::Ed25519(key.try_into().unwrap()) } SecAlg::ED448 => { let key = self.pkey.raw_public_key().unwrap(); - RawPublicKey::Ed448(key.try_into().unwrap()) + PublicKeyBytes::Ed448(key.try_into().unwrap()) } _ => unreachable!(), } diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 084786812..4a0fcf9c2 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -12,7 +12,7 @@ use ring::signature::{ use crate::{ base::iana::SecAlg, - validate::{RawPublicKey, RsaPublicKey, Signature}, + validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, }; use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; @@ -49,13 +49,13 @@ impl KeyPair { /// Import a key pair from bytes into OpenSSL. pub fn from_bytes( secret: &SecretKeyBytes, - public: &RawPublicKey, + public: &PublicKeyBytes, rng: Arc, ) -> Result { match (secret, public) { - (SecretKeyBytes::RsaSha256(s), RawPublicKey::RsaSha256(p)) => { + (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { // Ensure that the public and private key match. - if p != &RsaPublicKey::from(s) { + if p != &RsaPublicKeyBytes::from(s) { return Err(FromBytesError::InvalidKey); } @@ -83,7 +83,7 @@ impl KeyPair { ( SecretKeyBytes::EcdsaP256Sha256(s), - RawPublicKey::EcdsaP256Sha256(p), + PublicKeyBytes::EcdsaP256Sha256(p), ) => { let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; EcdsaKeyPair::from_private_key_and_public_key( @@ -98,7 +98,7 @@ impl KeyPair { ( SecretKeyBytes::EcdsaP384Sha384(s), - RawPublicKey::EcdsaP384Sha384(p), + PublicKeyBytes::EcdsaP384Sha384(p), ) => { let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; EcdsaKeyPair::from_private_key_and_public_key( @@ -111,7 +111,7 @@ impl KeyPair { .map(|key| Self::EcdsaP384Sha384 { key, rng }) } - (SecretKeyBytes::Ed25519(s), RawPublicKey::Ed25519(p)) => { + (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { Ed25519KeyPair::from_seed_and_public_key( s.as_slice(), p.as_slice(), @@ -120,7 +120,7 @@ impl KeyPair { .map(Self::Ed25519) } - (SecretKeyBytes::Ed448(_), RawPublicKey::Ed448(_)) => { + (SecretKeyBytes::Ed448(_), PublicKeyBytes::Ed448(_)) => { Err(FromBytesError::UnsupportedAlgorithm) } @@ -142,12 +142,12 @@ impl SignRaw for KeyPair { } } - fn raw_public_key(&self) -> RawPublicKey { + fn raw_public_key(&self) -> PublicKeyBytes { match self { Self::RsaSha256 { key, rng: _ } => { let components: ring::rsa::PublicKeyComponents> = key.public().into(); - RawPublicKey::RsaSha256(RsaPublicKey { + PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { n: components.n.into(), e: components.e.into(), }) @@ -156,19 +156,19 @@ impl SignRaw for KeyPair { Self::EcdsaP256Sha256 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - RawPublicKey::EcdsaP256Sha256(key.try_into().unwrap()) + PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) } Self::EcdsaP384Sha384 { key, rng: _ } => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - RawPublicKey::EcdsaP384Sha384(key.try_into().unwrap()) + PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) } Self::Ed25519(key) => { let key = key.public_key().as_ref(); let key = Box::<[u8]>::from(key); - RawPublicKey::Ed25519(key.try_into().unwrap()) + PublicKeyBytes::Ed25519(key.try_into().unwrap()) } } } @@ -224,7 +224,7 @@ impl SignRaw for KeyPair { pub fn generate( params: GenerateParams, rng: &dyn ring::rand::SecureRandom, -) -> Result<(SecretKeyBytes, RawPublicKey), GenerateError> { +) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; match params { @@ -241,7 +241,7 @@ pub fn generate( // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[73..138]); let pk = pk.try_into().unwrap(); - let pk = RawPublicKey::EcdsaP256Sha256(pk); + let pk = PublicKeyBytes::EcdsaP256Sha256(pk); Ok((sk, pk)) } @@ -259,7 +259,7 @@ pub fn generate( // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[88..185]); let pk = pk.try_into().unwrap(); - let pk = RawPublicKey::EcdsaP384Sha384(pk); + let pk = PublicKeyBytes::EcdsaP384Sha384(pk); Ok((sk, pk)) } @@ -276,7 +276,7 @@ pub fn generate( // Manually parse the PKCS#8 document for the public key. let pk: Box<[u8]> = Box::from(&doc.as_ref()[51..83]); let pk = pk.try_into().unwrap(); - let pk = RawPublicKey::Ed25519(pk); + let pk = PublicKeyBytes::Ed25519(pk); Ok((sk, pk)) } diff --git a/src/validate.rs b/src/validate.rs index d9ebdf31a..67f248e1f 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -59,17 +59,17 @@ pub struct Key { /// These flags are stored in the DNSKEY record. flags: u16, - /// The raw public key. + /// The public key, in bytes. /// /// This identifies the key and can be used for signatures. - key: RawPublicKey, + key: PublicKeyBytes, } //--- Construction impl Key { /// Construct a new DNSSEC key manually. - pub fn new(owner: Name, flags: u16, key: RawPublicKey) -> Self { + pub fn new(owner: Name, flags: u16, key: PublicKeyBytes) -> Self { Self { owner, flags, key } } } @@ -88,7 +88,7 @@ impl Key { } /// The raw public key. - pub fn raw_public_key(&self) -> &RawPublicKey { + pub fn raw_public_key(&self) -> &PublicKeyBytes { &self.key } @@ -233,7 +233,7 @@ impl> Key { let flags = dnskey.flags(); let algorithm = dnskey.algorithm(); let key = dnskey.public_key().as_ref(); - let key = RawPublicKey::from_dnskey_format(algorithm, key)?; + let key = PublicKeyBytes::from_dnskey_format(algorithm, key)?; Ok(Self { owner, flags, key }) } @@ -349,22 +349,22 @@ impl> fmt::Debug for Key { } } -//----------- RsaPublicKey --------------------------------------------------- +//----------- RsaPublicKeyBytes ---------------------------------------------- /// A low-level public key. #[derive(Clone, Debug)] -pub enum RawPublicKey { +pub enum PublicKeyBytes { /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKey), + RsaSha1(RsaPublicKeyBytes), /// An RSA/SHA-1 with NSEC3 public key. - RsaSha1Nsec3Sha1(RsaPublicKey), + RsaSha1Nsec3Sha1(RsaPublicKeyBytes), /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKey), + RsaSha256(RsaPublicKeyBytes), /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKey), + RsaSha512(RsaPublicKeyBytes), /// An ECDSA P-256/SHA-256 public key. /// @@ -397,7 +397,7 @@ pub enum RawPublicKey { //--- Inspection -impl RawPublicKey { +impl PublicKeyBytes { /// The algorithm used by this key. pub fn algorithm(&self) -> SecAlg { match self { @@ -456,7 +456,7 @@ impl RawPublicKey { //--- Conversion to and from DNSKEYs -impl RawPublicKey { +impl PublicKeyBytes { /// Parse a public key as stored in a DNSKEY record. pub fn from_dnskey_format( algorithm: SecAlg, @@ -464,18 +464,16 @@ impl RawPublicKey { ) -> Result { match algorithm { SecAlg::RSASHA1 => { - RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha1) + RsaPublicKeyBytes::from_dnskey_format(data).map(Self::RsaSha1) } SecAlg::RSASHA1_NSEC3_SHA1 => { - RsaPublicKey::from_dnskey_format(data) + RsaPublicKeyBytes::from_dnskey_format(data) .map(Self::RsaSha1Nsec3Sha1) } - SecAlg::RSASHA256 => { - RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha256) - } - SecAlg::RSASHA512 => { - RsaPublicKey::from_dnskey_format(data).map(Self::RsaSha512) - } + SecAlg::RSASHA256 => RsaPublicKeyBytes::from_dnskey_format(data) + .map(Self::RsaSha256), + SecAlg::RSASHA512 => RsaPublicKeyBytes::from_dnskey_format(data) + .map(Self::RsaSha512), SecAlg::ECDSAP256SHA256 => { let mut key = Box::new([0u8; 65]); @@ -531,7 +529,7 @@ impl RawPublicKey { //--- Comparison -impl PartialEq for RawPublicKey { +impl PartialEq for PublicKeyBytes { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -557,16 +555,16 @@ impl PartialEq for RawPublicKey { } } -impl Eq for RawPublicKey {} +impl Eq for PublicKeyBytes {} -//----------- RsaPublicKey --------------------------------------------------- +//----------- RsaPublicKeyBytes --------------------------------------------------- /// A generic RSA public key. /// /// All fields here are arbitrary-precision integers in big-endian format, /// without any leading zero bytes. #[derive(Clone, Debug)] -pub struct RsaPublicKey { +pub struct RsaPublicKeyBytes { /// The public modulus. pub n: Box<[u8]>, @@ -576,7 +574,7 @@ pub struct RsaPublicKey { //--- Inspection -impl RsaPublicKey { +impl RsaPublicKeyBytes { /// The raw key tag computation for this value. fn raw_key_tag(&self) -> u32 { let mut res = 0u32; @@ -631,7 +629,7 @@ impl RsaPublicKey { //--- Conversion to and from DNSKEYs -impl RsaPublicKey { +impl RsaPublicKeyBytes { /// Parse an RSA public key as stored in a DNSKEY record. pub fn from_dnskey_format(data: &[u8]) -> Result { if data.len() < 3 { @@ -687,7 +685,7 @@ impl RsaPublicKey { //--- Comparison -impl PartialEq for RsaPublicKey { +impl PartialEq for RsaPublicKeyBytes { fn eq(&self, other: &Self) -> bool { use ring::constant_time::verify_slices_are_equal; @@ -696,7 +694,7 @@ impl PartialEq for RsaPublicKey { } } -impl Eq for RsaPublicKey {} +impl Eq for RsaPublicKeyBytes {} //----------- Signature ------------------------------------------------------ From 221f16385fdc3b7bbe5176860c89ffb149e262ac Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 14:46:48 +0100 Subject: [PATCH 176/569] [sign/ring] Remove redundant imports --- src/sign/ring.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 4a0fcf9c2..f1b6cd7b4 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -225,8 +225,6 @@ pub fn generate( params: GenerateParams, rng: &dyn ring::rand::SecureRandom, ) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { - use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; - match params { GenerateParams::EcdsaP256Sha256 => { // Generate a key and a PKCS#8 document out of Ring. From 6d3a602af84d71515065a6644d00d33b9c33addf Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:54:05 +0100 Subject: [PATCH 177/569] Clippy. --- src/sign/records.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 7da6e0b41..0c335631d 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -96,7 +96,10 @@ impl SortedRecords { expiration: Timestamp, inception: Timestamp, keys: &[SigningKey], - ) -> Result>>, ()> + ) -> Result< + Vec>>, + ErrorTypeToBeDetermined, + > where N: ToName + Clone, D: CanonicalOrd @@ -162,7 +165,7 @@ impl SortedRecords { apex_ttl, dnskey.clone().into(), )) - .map_err(|_| ())?; + .map_err(|_| ErrorTypeToBeDetermined)?; res.push(Record::new( apex.owner().clone(), @@ -244,7 +247,7 @@ impl SortedRecords { key.raw_secret_key().sign_raw(&buf).unwrap(); let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { - return Err(()); + return Err(ErrorTypeToBeDetermined); }; let rrsig = @@ -1025,3 +1028,7 @@ where Some(Rrset::new(res)) } } + +//------------ ErrorTypeToBeDetermined ---------------------------------------- + +pub struct ErrorTypeToBeDetermined; From 61bc3aa82fe45a5473cfc33055a41dd399a9eb78 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 10:24:58 +0100 Subject: [PATCH 178/569] [sign,validate] Add 'display_as_bind()' to key bytes types --- src/sign/bytes.rs | 35 ++++++++++++++++++++++++++++++----- src/sign/openssl.rs | 10 +++++----- src/validate.rs | 30 +++++++++++++++++------------- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index 5b49f3328..1187a6dbf 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -130,7 +130,7 @@ impl SecretKeyBytes { /// The key is formatted in the private key v1.2 format and written to the /// given formatter. See the type-level documentation for a description /// of this format. - pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { @@ -160,6 +160,19 @@ impl SecretKeyBytes { } } + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a SecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be @@ -289,7 +302,7 @@ impl RsaSecretKeyBytes { /// given formatter. Note that the header and algorithm lines are not /// written. See the type-level documentation of [`SecretKeyBytes`] for a /// description of this format. - pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -309,6 +322,19 @@ impl RsaSecretKeyBytes { Ok(()) } + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a RsaSecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be @@ -464,7 +490,7 @@ impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { - use std::{string::String, vec::Vec}; + use std::{string::ToString, vec::Vec}; use crate::base::iana::SecAlg; @@ -496,8 +522,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); - let mut same = String::new(); - key.format_as_bind(&mut same).unwrap(); + let same = key.display_as_bind().to_string(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c5620c22e..e1922ffdb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -433,7 +433,10 @@ impl std::error::Error for GenerateError {} #[cfg(test)] mod tests { - use std::{string::String, vec::Vec}; + use std::{ + string::{String, ToString}, + vec::Vec, + }; use crate::{ base::iana::SecAlg, @@ -503,10 +506,7 @@ mod tests { let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); - - let equiv = key.to_bytes(); - let mut same = String::new(); - equiv.format_as_bind(&mut same).unwrap(); + let same = key.to_bytes().display_as_bind().to_string(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); diff --git a/src/validate.rs b/src/validate.rs index 67f248e1f..30104772c 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -308,23 +308,28 @@ impl> Key { /// Serialize this key in the conventional format used by BIND. /// - /// A user-specified DNS class can be used in the record; however, this - /// will almost always just be `IN`. - /// /// See the type-level documentation for a description of this format. - pub fn format_as_bind( - &self, - class: Class, - w: &mut impl fmt::Write, - ) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { writeln!( w, - "{} {} DNSKEY {}", + "{} IN DNSKEY {}", self.owner().fmt_with_dot(), - class, self.to_dnskey().display_zonefile(false), ) } + + /// Display this key in the conventional format used by BIND. + /// + /// See the type-level documentation for a description of this format. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a, Octs>(&'a Key); + impl<'a, Octs: AsRef<[u8]>> fmt::Display for Display<'a, Octs> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } } //--- Comparison @@ -1241,7 +1246,7 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; - use std::string::String; + use std::string::{String, ToString}; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; @@ -1375,8 +1380,7 @@ mod test { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let key = Key::>::parse_from_bind(&data).unwrap(); - let mut bind_fmt_key = String::new(); - key.format_as_bind(Class::IN, &mut bind_fmt_key).unwrap(); + let bind_fmt_key = key.display_as_bind().to_string(); let same = Key::parse_from_bind(&bind_fmt_key).unwrap(); assert_eq!(key, same); } From 55716a4d4bcb084db0af89e804b03b63cc1bdf65 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 11:02:57 +0100 Subject: [PATCH 179/569] [sign,validate] remove unused imports --- src/sign/openssl.rs | 5 +---- src/validate.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e1922ffdb..814a55da2 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -433,10 +433,7 @@ impl std::error::Error for GenerateError {} #[cfg(test)] mod tests { - use std::{ - string::{String, ToString}, - vec::Vec, - }; + use std::{string::ToString, vec::Vec}; use crate::{ base::iana::SecAlg, diff --git a/src/validate.rs b/src/validate.rs index 30104772c..0f307fb42 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1246,7 +1246,7 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; - use std::string::{String, ToString}; + use std::string::ToString; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; From f6c8c7e6479bc1f2eacd272e97a08466efbfaac9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 30 Oct 2024 19:00:09 +0100 Subject: [PATCH 180/569] Emulate ldns-signzone -p behaviour: set NSEC3 opt-out flag but include unsigned delegation NSEC3 RRs in the output. --- src/sign/records.rs | 47 +++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 0c335631d..e6a991830 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -369,7 +369,7 @@ impl SortedRecords { apex: &FamilyName, ttl: Ttl, params: Nsec3param, - opt_out: bool, + opt_out: Nsec3OptOut, ) -> Result, Nsec3HashError> where N: ToName + Clone + From> + Display, @@ -390,6 +390,17 @@ impl SortedRecords { // Reject old algorithms? if not, map 3 to 6 and 5 to 7, or reject // use of 3 and 5? + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, set the Opt-Out bit to one." + let mut nsec3_flags = params.flags(); + if matches!( + opt_out, + Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly + ) { + // Set the Opt-Out flag. + nsec3_flags |= 0b0000_0001; + } + // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." // We store the NSEC3s as we create them in a self-sorting vec. let mut nsec3s = SortedRecords::new(); @@ -437,7 +448,7 @@ impl SortedRecords { // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - if cut.is_some() && !has_ds && opt_out { + if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { continue; } @@ -486,7 +497,7 @@ impl SortedRecords { let rec = Self::mk_nsec3( &name, params.hash_algorithm(), - params.flags(), + nsec3_flags, params.iterations(), params.salt(), &apex_owner, @@ -520,14 +531,6 @@ impl SortedRecords { bitmap.add(Rtype::DNSKEY).unwrap(); } - // RFC 5155 7.1 step 2: - // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = params.flags(); - if opt_out { - // Set the Opt-Out flag. - nsec3_flags |= 0b0000_0001; - } - let rec = Self::mk_nsec3( name.owner(), params.hash_algorithm(), @@ -1029,6 +1032,26 @@ where } } -//------------ ErrorTypeToBeDetermined ---------------------------------------- +//------------ ErrorTypeToBeDetermined --------------------------------------- +#[derive(Debug)] pub struct ErrorTypeToBeDetermined; + +//------------ Nsec3OptOut --------------------------------------------------- + +/// The different types of NSEC3 opt-out. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Nsec3OptOut { + /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure + /// delegations will be included in the NSEC3 chain. + #[default] + NoOptOut, + + /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure + /// delegations will NOT be included in the NSEC3 chain. + OptOut, + + /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and + /// insecure delegations will be included in the NSEC3 chain. + OptOutFlagsOnly, +} From 8bf2c9fbaa5fe96675b6b7413e54fd90642ae06b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:07:22 +0100 Subject: [PATCH 181/569] Move nsec3_hash() back into the validator module per review feedback. --- src/sign/ring.rs | 122 +----------------------------------------- src/validator/mod.rs | 2 + src/validator/nsec.rs | 119 +++++++++++++++++++++++++++++++++++++++- 3 files changed, 120 insertions(+), 123 deletions(-) diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 57f025e17..1b747642f 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -6,15 +6,10 @@ use core::fmt; use std::{boxed::Box, sync::Arc, vec::Vec}; -use octseq::{EmptyBuilder, OctetsBuilder, Truncate}; -use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; use ring::signature::KeyPair as _; use ring::signature::{EcdsaKeyPair, Ed25519KeyPair, RsaKeyPair}; -use crate::base::iana::{Nsec3HashAlg, SecAlg}; -use crate::base::ToName; -use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::Nsec3param; +use crate::base::iana::SecAlg; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; @@ -353,121 +348,6 @@ impl fmt::Display for GenerateError { impl std::error::Error for GenerateError {} -//------------ Nsec3HashError ------------------------------------------------- - -/// An error when creating an NSEC3 hash. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Nsec3HashError { - /// The requested algorithm for NSEC3 hashing is not supported. - UnsupportedAlgorithm, - - /// Data could not be appended to a buffer. - /// - /// This could indicate an out of memory condition. - AppendError, - - /// The hashing process produced an invalid owner hash. - /// - /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) - OwnerHashError, -} - -/// Compute an [RFC 5155] NSEC3 hash using default settings. -/// -/// See: [Nsec3param::default]. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_default_hash( - owner: N, -) -> Result, Nsec3HashError> -where - N: ToName, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - let params = Nsec3param::::default(); - nsec3_hash( - owner, - params.hash_algorithm(), - params.iterations(), - params.salt(), - ) -} - -/// Compute an [RFC 5155] NSEC3 hash. -/// -/// Computes an NSEC3 hash according to [RFC 5155] section 5: -/// -/// > IH(salt, x, 0) = H(x || salt) -/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 -/// -/// Then the calculated hash of an owner name is: -/// -/// > IH(salt, owner name, iterations), -/// -/// Note that the `iterations` parameter is the number of _additional_ -/// iterations as defined in [RFC 5155] section 3.1.3. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_hash( - owner: N, - algorithm: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, -) -> Result, Nsec3HashError> -where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - if algorithm != Nsec3HashAlg::SHA1 { - return Err(Nsec3HashError::UnsupportedAlgorithm); - } - - fn mk_hash( - owner: N, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, - { - let mut buf = HashOcts::empty(); - - owner.compose_canonical(&mut buf)?; - buf.append_slice(salt.as_slice())?; - - let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(buf.as_ref()); - let mut h = ctx.finish(); - - for _ in 0..iterations { - buf.truncate(0); - buf.append_slice(h.as_ref())?; - buf.append_slice(salt.as_slice())?; - - let mut ctx = - ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(buf.as_ref()); - h = ctx.finish(); - } - - Ok(h.as_ref().into()) - } - - let hash = mk_hash(owner, iterations, salt) - .map_err(|_| Nsec3HashError::AppendError)?; - - let owner_hash = OwnerHash::from_octets(hash) - .map_err(|_| Nsec3HashError::OwnerHashError)?; - - Ok(owner_hash) -} - //============ Tests ========================================================= #[cfg(test)] diff --git a/src/validator/mod.rs b/src/validator/mod.rs index af70e18f9..86fe86f41 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -110,3 +110,5 @@ pub mod context; mod group; mod nsec; mod utilities; + +pub use nsec::{nsec3_default_hash, nsec3_hash}; diff --git a/src/validator/nsec.rs b/src/validator/nsec.rs index 81027fc8b..5add99318 100644 --- a/src/validator/nsec.rs +++ b/src/validator/nsec.rs @@ -7,6 +7,8 @@ use std::vec::Vec; use bytes::Bytes; use moka::future::Cache; +use octseq::{EmptyBuilder, OctetsBuilder, Truncate}; +use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; use crate::base::iana::{ExtendedErrorCode, Nsec3HashAlg}; use crate::base::name::{Label, ToName}; @@ -14,8 +16,7 @@ use crate::base::opt::ExtendedError; use crate::base::{Name, ParsedName, Rtype}; use crate::dep::octseq::Octets; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{AllRecordData, Nsec, Nsec3}; -use crate::sign::ring::nsec3_hash; +use crate::rdata::{AllRecordData, Nsec, Nsec3, Nsec3param}; use super::context::{Config, ValidationState}; use super::group::ValidatedGroup; @@ -960,6 +961,120 @@ pub fn supported_nsec3_hash(h: Nsec3HashAlg) -> bool { h == Nsec3HashAlg::SHA1 } +//------------ Nsec3HashError ------------------------------------------------- + +/// An error when creating an NSEC3 hash. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Nsec3HashError { + /// The requested algorithm for NSEC3 hashing is not supported. + UnsupportedAlgorithm, + + /// Data could not be appended to a buffer. + /// + /// This could indicate an out of memory condition. + AppendError, + + /// The hashing process produced an invalid owner hash. + /// + /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) + OwnerHashError, +} + +/// Compute an [RFC 5155] NSEC3 hash using default settings. +/// +/// See: [Nsec3param::default]. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_default_hash( + owner: N, +) -> Result, Nsec3HashError> +where + N: ToName, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + let params = Nsec3param::::default(); + nsec3_hash( + owner, + params.hash_algorithm(), + params.iterations(), + params.salt(), + ) +} + +/// Compute an [RFC 5155] NSEC3 hash. +/// +/// Computes an NSEC3 hash according to [RFC 5155] section 5: +/// +/// > IH(salt, x, 0) = H(x || salt) +/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 +/// +/// Then the calculated hash of an owner name is: +/// +/// > IH(salt, owner name, iterations), +/// +/// Note that the `iterations` parameter is the number of _additional_ +/// iterations as defined in [RFC 5155] section 3.1.3. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_hash( + owner: N, + algorithm: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result, Nsec3HashError> +where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + if algorithm != Nsec3HashAlg::SHA1 { + return Err(Nsec3HashError::UnsupportedAlgorithm); + } + + fn mk_hash( + owner: N, + iterations: u16, + salt: &Nsec3Salt, + ) -> Result + where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, + { + let mut canonical_owner = HashOcts::empty(); + owner.compose_canonical(&mut canonical_owner)?; + + let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(canonical_owner.as_ref()); + ctx.update(salt.as_slice()); + let mut h = ctx.finish(); + + for _ in 0..iterations { + canonical_owner.truncate(0); + canonical_owner.append_slice(h.as_ref())?; + canonical_owner.append_slice(salt.as_slice())?; + + let mut ctx = + ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(canonical_owner.as_ref()); + h = ctx.finish(); + } + + Ok(h.as_ref().into()) + } + + let hash = mk_hash(owner, iterations, salt) + .map_err(|_| Nsec3HashError::AppendError)?; + + let owner_hash = OwnerHash::from_octets(hash) + .map_err(|_| Nsec3HashError::OwnerHashError)?; + + Ok(owner_hash) +} + /// Return an NSEC3 hash using a cache. pub async fn cached_nsec3_hash( owner: &Name, From beb8e529da0997df98c9e0b9a1298469c05fabab Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 00:12:19 +0100 Subject: [PATCH 182/569] Move nsec3_hash() to the validate (not validator!) module per review feedback. --- src/validate.rs | 135 +++++++++++++++++++++++++++++++++++++++--- src/validator/mod.rs | 2 - src/validator/nsec.rs | 119 +------------------------------------ 3 files changed, 128 insertions(+), 128 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 67f248e1f..77c5523ea 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -3,9 +3,18 @@ //! **This module is experimental and likely to change significantly.** #![cfg(feature = "unstable-validate")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] +use std::boxed::Box; +use std::vec::Vec; +use std::{error, fmt}; + +use bytes::Bytes; +use octseq::builder::with_infallible; +use octseq::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; +use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; +use ring::{digest, signature}; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, DigestAlg, SecAlg}; +use crate::base::iana::{Class, DigestAlg, Nsec3HashAlg, SecAlg}; use crate::base::name::Name; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; @@ -14,14 +23,8 @@ use crate::base::scan::{IterScanner, Scanner}; use crate::base::wire::{Compose, Composer}; use crate::base::zonefile_fmt::ZonefileFmt; use crate::base::Rtype; -use crate::rdata::{Dnskey, Ds, Rrsig}; -use bytes::Bytes; -use octseq::builder::with_infallible; -use octseq::{EmptyBuilder, FromBuilder}; -use ring::{digest, signature}; -use std::boxed::Box; -use std::vec::Vec; -use std::{error, fmt}; +use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; +use crate::rdata::{Dnskey, Ds, Nsec3param, Rrsig}; //----------- Key ------------------------------------------------------------ @@ -1682,3 +1685,117 @@ mod test { assert_eq!(rrsig.verify_signed_data(&key, &signed_data), Ok(())); } } + +//------------ Nsec3HashError ------------------------------------------------- + +/// An error when creating an NSEC3 hash. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Nsec3HashError { + /// The requested algorithm for NSEC3 hashing is not supported. + UnsupportedAlgorithm, + + /// Data could not be appended to a buffer. + /// + /// This could indicate an out of memory condition. + AppendError, + + /// The hashing process produced an invalid owner hash. + /// + /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) + OwnerHashError, +} + +/// Compute an [RFC 5155] NSEC3 hash using default settings. +/// +/// See: [Nsec3param::default]. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_default_hash( + owner: N, +) -> Result, Nsec3HashError> +where + N: ToName, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + let params = Nsec3param::::default(); + nsec3_hash( + owner, + params.hash_algorithm(), + params.iterations(), + params.salt(), + ) +} + +/// Compute an [RFC 5155] NSEC3 hash. +/// +/// Computes an NSEC3 hash according to [RFC 5155] section 5: +/// +/// > IH(salt, x, 0) = H(x || salt) +/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 +/// +/// Then the calculated hash of an owner name is: +/// +/// > IH(salt, owner name, iterations), +/// +/// Note that the `iterations` parameter is the number of _additional_ +/// iterations as defined in [RFC 5155] section 3.1.3. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_hash( + owner: N, + algorithm: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result, Nsec3HashError> +where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + if algorithm != Nsec3HashAlg::SHA1 { + return Err(Nsec3HashError::UnsupportedAlgorithm); + } + + fn mk_hash( + owner: N, + iterations: u16, + salt: &Nsec3Salt, + ) -> Result + where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, + { + let mut canonical_owner = HashOcts::empty(); + owner.compose_canonical(&mut canonical_owner)?; + + let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(canonical_owner.as_ref()); + ctx.update(salt.as_slice()); + let mut h = ctx.finish(); + + for _ in 0..iterations { + canonical_owner.truncate(0); + canonical_owner.append_slice(h.as_ref())?; + canonical_owner.append_slice(salt.as_slice())?; + + let mut ctx = + ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); + ctx.update(canonical_owner.as_ref()); + h = ctx.finish(); + } + + Ok(h.as_ref().into()) + } + + let hash = mk_hash(owner, iterations, salt) + .map_err(|_| Nsec3HashError::AppendError)?; + + let owner_hash = OwnerHash::from_octets(hash) + .map_err(|_| Nsec3HashError::OwnerHashError)?; + + Ok(owner_hash) +} diff --git a/src/validator/mod.rs b/src/validator/mod.rs index 86fe86f41..af70e18f9 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -110,5 +110,3 @@ pub mod context; mod group; mod nsec; mod utilities; - -pub use nsec::{nsec3_default_hash, nsec3_hash}; diff --git a/src/validator/nsec.rs b/src/validator/nsec.rs index 5add99318..87ce0e901 100644 --- a/src/validator/nsec.rs +++ b/src/validator/nsec.rs @@ -7,8 +7,6 @@ use std::vec::Vec; use bytes::Bytes; use moka::future::Cache; -use octseq::{EmptyBuilder, OctetsBuilder, Truncate}; -use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; use crate::base::iana::{ExtendedErrorCode, Nsec3HashAlg}; use crate::base::name::{Label, ToName}; @@ -16,7 +14,8 @@ use crate::base::opt::ExtendedError; use crate::base::{Name, ParsedName, Rtype}; use crate::dep::octseq::Octets; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{AllRecordData, Nsec, Nsec3, Nsec3param}; +use crate::rdata::{AllRecordData, Nsec, Nsec3}; +use crate::validate::nsec3_hash; use super::context::{Config, ValidationState}; use super::group::ValidatedGroup; @@ -961,120 +960,6 @@ pub fn supported_nsec3_hash(h: Nsec3HashAlg) -> bool { h == Nsec3HashAlg::SHA1 } -//------------ Nsec3HashError ------------------------------------------------- - -/// An error when creating an NSEC3 hash. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Nsec3HashError { - /// The requested algorithm for NSEC3 hashing is not supported. - UnsupportedAlgorithm, - - /// Data could not be appended to a buffer. - /// - /// This could indicate an out of memory condition. - AppendError, - - /// The hashing process produced an invalid owner hash. - /// - /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) - OwnerHashError, -} - -/// Compute an [RFC 5155] NSEC3 hash using default settings. -/// -/// See: [Nsec3param::default]. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_default_hash( - owner: N, -) -> Result, Nsec3HashError> -where - N: ToName, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - let params = Nsec3param::::default(); - nsec3_hash( - owner, - params.hash_algorithm(), - params.iterations(), - params.salt(), - ) -} - -/// Compute an [RFC 5155] NSEC3 hash. -/// -/// Computes an NSEC3 hash according to [RFC 5155] section 5: -/// -/// > IH(salt, x, 0) = H(x || salt) -/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 -/// -/// Then the calculated hash of an owner name is: -/// -/// > IH(salt, owner name, iterations), -/// -/// Note that the `iterations` parameter is the number of _additional_ -/// iterations as defined in [RFC 5155] section 3.1.3. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_hash( - owner: N, - algorithm: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, -) -> Result, Nsec3HashError> -where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - if algorithm != Nsec3HashAlg::SHA1 { - return Err(Nsec3HashError::UnsupportedAlgorithm); - } - - fn mk_hash( - owner: N, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, - { - let mut canonical_owner = HashOcts::empty(); - owner.compose_canonical(&mut canonical_owner)?; - - let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(canonical_owner.as_ref()); - ctx.update(salt.as_slice()); - let mut h = ctx.finish(); - - for _ in 0..iterations { - canonical_owner.truncate(0); - canonical_owner.append_slice(h.as_ref())?; - canonical_owner.append_slice(salt.as_slice())?; - - let mut ctx = - ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(canonical_owner.as_ref()); - h = ctx.finish(); - } - - Ok(h.as_ref().into()) - } - - let hash = mk_hash(owner, iterations, salt) - .map_err(|_| Nsec3HashError::AppendError)?; - - let owner_hash = OwnerHash::from_octets(hash) - .map_err(|_| Nsec3HashError::OwnerHashError)?; - - Ok(owner_hash) -} - /// Return an NSEC3 hash using a cache. pub async fn cached_nsec3_hash( owner: &Name, From 7831260f07d984a6bca12d01d7fa6291a611832d Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 31 Oct 2024 11:28:37 +0100 Subject: [PATCH 183/569] [sign] Document everything --- src/sign/common.rs | 4 ++ src/sign/mod.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++- src/sign/openssl.rs | 8 ++++ src/sign/ring.rs | 7 ++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/sign/common.rs b/src/sign/common.rs index d5aaf5b67..fc10803e3 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -1,4 +1,8 @@ //! DNSSEC signing using built-in backends. +//! +//! This backend supports all the algorithms supported by Ring and OpenSSL, +//! depending on whether the respective crate features are enabled. See the +//! documentation for each backend for more information. use core::fmt; use std::sync::Arc; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b365a78f5..99bd1f11f 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -7,6 +7,87 @@ //! made "online" (in an authoritative name server while it is running) or //! "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. +//! +//! # Example Usage +//! +//! At the moment, only "low-level" signing is supported. +//! +//! ``` +//! # use domain::sign::*; +//! # use domain::base::Name; +//! // Generate a new ED25519 key. +//! let params = GenerateParams::Ed25519; +//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! +//! // Associate the key with important metadata. +//! let owner: Name> = "www.example.org.".parse().unwrap(); +//! let flags = 257; // key signing key +//! let key = SigningKey::new(owner, flags, key_pair); +//! +//! // Access the public key (with metadata). +//! let pub_key = key.public_key(); +//! println!("{:?}", pub_key); +//! +//! // Sign arbitrary byte sequences with the key. +//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); +//! println!("{:?}", sig); +//! ``` +//! +//! # Cryptography +//! +//! 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. +//! +//! 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. +//! +//! Users can choose to bring their own cryptography by providing their own +//! `KeyPair` type that implements [`SignRaw`]. Note that `async` signing +//! (useful for interacting with cryptographic hardware like HSMs) is not +//! currently supported. +//! +//! While each cryptographic backend can support a limited number of signature +//! algorithms, even the types independent of a cryptographic backend (e.g. +//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of +//! algorithms. They are: +//! +//! - RSA/SHA-256 +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 +//! +//! # Importing and Exporting +//! +//! The [`SecretKeyBytes`] type is a generic representation of a secret key as +//! a byte slice. While it does not offer any cryptographic functionality, it +//! is useful to transfer secret keys stored in memory, independent of any +//! cryptographic backend. +//! +//! The `KeyPair` types of the cryptographic backends in this module each +//! support a `from_bytes()` function that parses the generic representation +//! into a functional cryptographic key. Importantly, these functions require +//! both the public and private keys to be provided -- the pair are verified +//! for consistency. In some cases, it may also be possible to serialize an +//! existing cryptographic key back to the generic bytes representation. +//! +//! [`SecretKeyBytes`] also supports importing and exporting keys from and to +//! the conventional private-key format popularized by BIND. This format is +//! used by a variety of tools for storing DNSSEC keys on disk. See the +//! type-level documentation for a specification of the format. #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] @@ -194,7 +275,14 @@ pub trait SignRaw { #[derive(Clone, Debug, PartialEq, Eq)] pub enum GenerateParams { /// Generate an RSA/SHA-256 keypair. - RsaSha256 { bits: u32 }, + RsaSha256 { + /// The number of bits in the public modulus. + /// + /// 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. + bits: u32, + }, /// Generate an ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256, diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 814a55da2..85257137a 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,4 +1,12 @@ //! DNSSEC signing using OpenSSL. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (512-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] diff --git a/src/sign/ring.rs b/src/sign/ring.rs index f1b6cd7b4..3b97cf006 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,4 +1,11 @@ //! DNSSEC signing using `ring`. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (2048-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] From a04c91704968511b1e09075c3e382131bafe4225 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 28 Oct 2024 13:52:16 +0100 Subject: [PATCH 184/569] Extend test file with records useful for manual testing of NSEC3. --- src/net/server/middleware/xfr/tests.rs | 27 ++++++++++++++++++++++++-- test-data/zonefiles/nsd-example.txt | 10 ++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index ec87646a2..a3e6dab2c 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -17,7 +17,7 @@ use octseq::Octets; use tokio::sync::Semaphore; use tokio::time::Instant; -use crate::base::iana::{Class, OptRcode, Rcode}; +use crate::base::iana::{Class, DigestAlg, OptRcode, Rcode, SecAlg}; use crate::base::{ Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl, }; @@ -32,7 +32,7 @@ use crate::net::server::service::{ CallResult, Service, ServiceError, ServiceFeedback, ServiceResult, }; use crate::rdata::{ - Aaaa, AllRecordData, Cname, Mx, Ns, Soa, Txt, ZoneRecordData, A, + Aaaa, AllRecordData, Cname, Ds, Mx, Ns, Soa, Txt, ZoneRecordData, A, }; use crate::tsig::{Algorithm, Key, KeyName}; use crate::zonefile::inplace::Zonefile; @@ -74,6 +74,29 @@ async fn axfr_with_example_zone() { (n("example.com"), Aaaa::new(p("2001:db8::3")).into()), (n("www.example.com"), Cname::new(n("example.com")).into()), (n("mail.example.com"), Mx::new(10, n("example.com")).into()), + (n("a.b.c.mail.example.com"), A::new(p("127.0.0.1")).into()), + ( + n("unsigned.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ds::new( + 60485, + SecAlg::RSASHA1, + DigestAlg::SHA1, + crate::utils::base16::decode( + "2BB183AF5F22588179A53B0A98631FAD1A292118", + ) + .unwrap(), + ) + .unwrap() + .into(), + ), (n("example.com"), zone_soa.into()), ]; diff --git a/test-data/zonefiles/nsd-example.txt b/test-data/zonefiles/nsd-example.txt index bedf91ac6..08e1cf488 100644 --- a/test-data/zonefiles/nsd-example.txt +++ b/test-data/zonefiles/nsd-example.txt @@ -21,3 +21,13 @@ example.com. A 192.0.2.1 www CNAME example.com. mail MX 10 example.com. + +; An ENT for NSEC3 testing purposes. +a.b.c.mail A 127.0.0.1 + +; An unsigned delegation for NSEC3 testing purposes. +unsigned NS some.other.ns.net + +; A signed delegation for NSEC3 testing purposes. +signed NS some.other.ns.net + DS 60485 5 1 ( 2BB183AF5F22588179A53B0A 98631FAD1A292118 ) From abaab27d5c4960a6a5fe9459ffad1cf4f73de4c4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:31:17 +0100 Subject: [PATCH 185/569] Revert "Extend test file with records useful for manual testing of NSEC3." This reverts commit a04c91704968511b1e09075c3e382131bafe4225. --- src/net/server/middleware/xfr/tests.rs | 27 ++------------------------ test-data/zonefiles/nsd-example.txt | 10 ---------- 2 files changed, 2 insertions(+), 35 deletions(-) diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index a3e6dab2c..ec87646a2 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -17,7 +17,7 @@ use octseq::Octets; use tokio::sync::Semaphore; use tokio::time::Instant; -use crate::base::iana::{Class, DigestAlg, OptRcode, Rcode, SecAlg}; +use crate::base::iana::{Class, OptRcode, Rcode}; use crate::base::{ Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl, }; @@ -32,7 +32,7 @@ use crate::net::server::service::{ CallResult, Service, ServiceError, ServiceFeedback, ServiceResult, }; use crate::rdata::{ - Aaaa, AllRecordData, Cname, Ds, Mx, Ns, Soa, Txt, ZoneRecordData, A, + Aaaa, AllRecordData, Cname, Mx, Ns, Soa, Txt, ZoneRecordData, A, }; use crate::tsig::{Algorithm, Key, KeyName}; use crate::zonefile::inplace::Zonefile; @@ -74,29 +74,6 @@ async fn axfr_with_example_zone() { (n("example.com"), Aaaa::new(p("2001:db8::3")).into()), (n("www.example.com"), Cname::new(n("example.com")).into()), (n("mail.example.com"), Mx::new(10, n("example.com")).into()), - (n("a.b.c.mail.example.com"), A::new(p("127.0.0.1")).into()), - ( - n("unsigned.example.com"), - Ns::new(n("some.other.ns.net.example.com")).into(), - ), - ( - n("signed.example.com"), - Ns::new(n("some.other.ns.net.example.com")).into(), - ), - ( - n("signed.example.com"), - Ds::new( - 60485, - SecAlg::RSASHA1, - DigestAlg::SHA1, - crate::utils::base16::decode( - "2BB183AF5F22588179A53B0A98631FAD1A292118", - ) - .unwrap(), - ) - .unwrap() - .into(), - ), (n("example.com"), zone_soa.into()), ]; diff --git a/test-data/zonefiles/nsd-example.txt b/test-data/zonefiles/nsd-example.txt index 08e1cf488..bedf91ac6 100644 --- a/test-data/zonefiles/nsd-example.txt +++ b/test-data/zonefiles/nsd-example.txt @@ -21,13 +21,3 @@ example.com. A 192.0.2.1 www CNAME example.com. mail MX 10 example.com. - -; An ENT for NSEC3 testing purposes. -a.b.c.mail A 127.0.0.1 - -; An unsigned delegation for NSEC3 testing purposes. -unsigned NS some.other.ns.net - -; A signed delegation for NSEC3 testing purposes. -signed NS some.other.ns.net - DS 60485 5 1 ( 2BB183AF5F22588179A53B0A 98631FAD1A292118 ) From 7a6ec5325f216aca6e211bc7e3c3f9ea3f9d5935 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:44:19 +0100 Subject: [PATCH 186/569] Review feedback. --- src/sign/records.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index e6a991830..cd373938d 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -359,7 +359,7 @@ impl SortedRecords { /// SOA RR and the TTL of the zone SOA RR itself"_. /// /// - The `params` should be set to _"SHA-1, no extra iterations, empty - /// salt"_ and zero flags. See `Nsec3param::default()`. + /// salt"_ and zero flags. See [`Nsec3param::default()`]. /// /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html From 3c53e9e758bbe1e17e8511fe2e404dd1e457c106 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:50:40 +0100 Subject: [PATCH 187/569] Review feedback. --- src/sign/records.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 16cf5ea14..f7dd65017 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -265,7 +265,7 @@ impl SortedRecords { /// SOA RR and the TTL of the zone SOA RR itself"_. /// /// - The `params` should be set to _"SHA-1, no extra iterations, empty - /// salt"_ and zero flags. See `Nsec3param::default()`. + /// salt"_ and zero flags. See [`Nsec3param::default()`]. /// /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html From 50433f0d54131a94cb32a9e7b74728400bf2f440 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:53:25 +0100 Subject: [PATCH 188/569] Review feedback. --- src/validate.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 77c5523ea..c806a48f9 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1778,13 +1778,10 @@ where let mut h = ctx.finish(); for _ in 0..iterations { - canonical_owner.truncate(0); - canonical_owner.append_slice(h.as_ref())?; - canonical_owner.append_slice(salt.as_slice())?; - let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(canonical_owner.as_ref()); + ctx.update(h.as_ref()); + ctx.update(salt.as_slice()); h = ctx.finish(); } From 70e998ad5e2dae0283b21a7c358366a26b163076 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 31 Oct 2024 22:57:25 +0100 Subject: [PATCH 189/569] Review feedback inspired change (though not actually what was suggested). --- src/sign/records.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index f7dd65017..61697f0fe 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -473,7 +473,7 @@ impl SortedRecords { // RFC 5155 7.1 step 8: // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." - let nsec3param_rec = Record::new( + let nsec3param = Record::new( apex.owner().try_to_name::().unwrap().into(), Class::IN, ttl, @@ -486,7 +486,7 @@ impl SortedRecords { // // TODO - Ok(Nsec3Records::new(nsec3s.records, nsec3param_rec)) + Ok(Nsec3Records::new(nsec3s.records, nsec3param)) } pub fn write(&self, target: &mut W) -> Result<(), io::Error> @@ -626,21 +626,18 @@ where /// The set of records created by [`SortedRecords::nsec3s()`]. pub struct Nsec3Records { /// The NSEC3 records. - pub nsec3_recs: Vec>>, + pub nsec3s: Vec>>, /// The NSEC3PARAM record. - pub nsec3param_rec: Record>, + pub nsec3param: Record>, } impl Nsec3Records { pub fn new( - nsec3_recs: Vec>>, - nsec3param_rec: Record>, + nsec3s: Vec>>, + nsec3param: Record>, ) -> Self { - Self { - nsec3_recs, - nsec3param_rec, - } + Self { nsec3s, nsec3param } } } From de7c13fb24a48ca871d3972f3f67ed2f4fefb16d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:08:43 +0100 Subject: [PATCH 190/569] Add a note to self about tests to add. --- src/sign/records.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index fa457b860..2df3aa1ab 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1052,3 +1052,30 @@ pub enum Nsec3OptOut { /// insecure delegations will be included in the NSEC3 chain. OptOutFlagsOnly, } + +// TODO: Add tests for nsec3s() that validate the following from RFC 5155: +// +// 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. +// However, if there is not a corresponding NSEC3 RR, there MUST be +// 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 From 7e9977e9b5a1be2b31660d6af4f17225fb8c4b3f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 1 Nov 2024 09:09:08 +0100 Subject: [PATCH 191/569] More ENT NSEC3 cases to handle. --- test-data/zonefiles/nsd-example.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test-data/zonefiles/nsd-example.txt b/test-data/zonefiles/nsd-example.txt index 08e1cf488..1650961b6 100644 --- a/test-data/zonefiles/nsd-example.txt +++ b/test-data/zonefiles/nsd-example.txt @@ -22,7 +22,9 @@ www CNAME example.com. mail MX 10 example.com. -; An ENT for NSEC3 testing purposes. +; ENTs for NSEC3 testing purposes. +some.ent A 127.0.0.1 +x.y.mail A 127.0.0.1 a.b.c.mail A 127.0.0.1 ; An unsigned delegation for NSEC3 testing purposes. From 7c9ee4c668e88facf0a1829601b4ace0a73ce690 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 4 Nov 2024 09:46:08 +0100 Subject: [PATCH 192/569] [lib] Rewrite feature flag documentation --- Cargo.toml | 2 +- src/lib.rs | 94 +++++++++++++++++++++++++++++++++++------------------- 2 files changed, 62 insertions(+), 34 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78d0f2eda..8eff4e592 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,9 +61,9 @@ 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"] 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.. From cea9ae390860f3b098015336a9c449dac6664e27 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 4 Nov 2024 09:48:49 +0100 Subject: [PATCH 193/569] [workflows/ci] Use 'apt-get' instead of 'apt' --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 354bf0a9a678c1eef5e972005447d4015817ea80 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 4 Nov 2024 10:29:28 +0100 Subject: [PATCH 194/569] [sign] Clarify documentation as per @ximon18 --- src/sign/bytes.rs | 14 ++++++++------ src/sign/common.rs | 8 ++++---- src/sign/mod.rs | 34 ++++++++++++++++++++-------------- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index 1187a6dbf..3326ee086 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -184,7 +184,7 @@ impl SecretKeyBytes { mut data: &str, ) -> 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" { @@ -203,7 +203,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 +213,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)?; @@ -248,6 +248,7 @@ impl SecretKeyBytes { //--- Drop impl Drop for SecretKeyBytes { + /// Securely clear the secret key bytes from memory. fn drop(&mut self) { // Zero the bytes for each field. match self { @@ -351,7 +352,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, @@ -413,6 +414,7 @@ impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { //--- Drop impl Drop for RsaSecretKeyBytes { + /// Securely clear the secret key bytes from memory. fn drop(&mut self) { // Zero the bytes for each field. self.n.fill(0u8); @@ -428,8 +430,8 @@ impl Drop for RsaSecretKeyBytes { //----------- 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 99bd1f11f..b65384945 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 @@ -237,10 +237,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 { @@ -281,6 +282,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, }, From ca10361847a154f77f26e0824139b1ea89f2a862 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 4 Nov 2024 11:03:29 +0100 Subject: [PATCH 195/569] [sign] Use 'secrecy' to protect private keys --- Cargo.lock | 10 +++++ Cargo.toml | 3 +- src/sign/bytes.rs | 104 +++++++++++++++++--------------------------- src/sign/openssl.rs | 37 +++++++++------- src/sign/ring.rs | 31 ++++++------- 5 files changed, 90 insertions(+), 95 deletions(-) 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 8eff4e592..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 } @@ -70,7 +71,7 @@ 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/sign/bytes.rs b/src/sign/bytes.rs index 3326ee086..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,7 +187,7 @@ 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_bind_entry(data)? { data = rest; @@ -191,11 +196,15 @@ impl SecretKeyBytes { 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. @@ -245,22 +254,6 @@ impl SecretKeyBytes { } } -//--- Drop - -impl Drop for SecretKeyBytes { - /// Securely clear the secret key bytes from memory. - 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. @@ -276,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 @@ -309,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(()) } @@ -390,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(), }) } } @@ -411,23 +404,6 @@ impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { } } -//--- Drop - -impl Drop for RsaSecretKeyBytes { - /// Securely clear the secret key bytes from memory. - 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 BIND-format private key file. 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/ring.rs b/src/sign/ring.rs index 3b97cf006..d1e29c395 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -16,6 +16,7 @@ use std::{boxed::Box, sync::Arc, vec::Vec}; use ring::signature::{ EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, }; +use secrecy::ExposeSecret; use crate::{ base::iana::SecAlg, @@ -76,12 +77,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) @@ -95,7 +96,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, ) @@ -110,7 +111,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, ) @@ -120,7 +121,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) @@ -240,8 +241,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]); @@ -258,8 +259,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]); @@ -275,8 +276,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]); From 9268dd3b69c39f81535d46f058db1a9f95cea43c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:33:46 +0100 Subject: [PATCH 196/569] Display NSEC3 without trailing space if the bitmap is empty. --- src/rdata/dnssec.rs | 5 +++++ src/rdata/nsec3.rs | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) 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(()) } } From fb7e9efebd8e77d5470dc81e69b00de205d8c8f5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:27:08 +0100 Subject: [PATCH 197/569] Backport NSEC3 improvements and upstream dnssec-key branch compatibility fixes from the downstream multiple-signing-key branch. --- src/sign/mod.rs | 1 + src/sign/records.rs | 319 ++++++++++++++++++++++++++++++++------------ src/validate.rs | 3 + 3 files changed, 237 insertions(+), 86 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b365a78f5..e5f94a843 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -23,6 +23,7 @@ pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; pub mod common; pub mod openssl; +pub mod records; pub mod ring; //----------- SigningKey ----------------------------------------------------- diff --git a/src/sign/records.rs b/src/sign/records.rs index 61697f0fe..44380347c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -2,13 +2,15 @@ use core::convert::From; use core::fmt::Display; +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}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom}; +use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; @@ -20,11 +22,11 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Ds, Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::rdata::{Nsec, Nsec3, Nsec3param, Rrsig}; use crate::utils::base32; +use crate::validate::{nsec3_hash, Nsec3HashError}; -use super::key::SigningKey; -use super::ring::{nsec3_hash, Nsec3HashError}; +use super::{SignRaw, SigningKey}; //------------ SortedRecords ------------------------------------------------- @@ -75,19 +77,18 @@ impl SortedRecords { } #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, - apex: &FamilyName, + apex: &FamilyName, expiration: Timestamp, inception: Timestamp, - key: Key, - ) -> Result>>, Key::Error> + key: SigningKey, + ) -> Result>>, ErrorTypeToBeDetermined> where N: ToName + Clone, D: RecordData + ComposeRecordData, - Key: SigningKey, - Octets: From + AsRef<[u8]>, - ApexName: ToName + Clone, + ConcreteSecretKey: SignRaw, + Octets: AsRef<[u8]> + OctetsFrom>, { let mut res = Vec::new(); let mut buf = Vec::new(); @@ -148,12 +149,12 @@ impl SortedRecords { buf.clear(); let rrsig = ProtoRrsig::new( rrset.rtype(), - key.algorithm()?, + key.algorithm(), name.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, - key.key_tag()?, + key.public_key().key_tag(), apex.owner().clone(), ); rrsig.compose_canonical(&mut buf).unwrap(); @@ -162,31 +163,34 @@ impl SortedRecords { } // Create and push the RRSIG record. + let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(ErrorTypeToBeDetermined); + }; + res.push(Record::new( name.owner().clone(), name.class(), rrset.ttl(), - rrsig - .into_rrsig(key.sign(&buf)?.into()) - .expect("long signature"), + rrsig.into_rrsig(signature).expect("long signature"), )); } } Ok(res) } - pub fn nsecs( + pub fn nsecs( &self, - apex: &FamilyName, + apex: &FamilyName, ttl: Ttl, ) -> Vec>> where - N: ToName + Clone, + N: ToName + Clone + PartialEq, D: RecordData, Octets: FromBuilder, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, - ::AppendError: fmt::Debug, - ApexName: ToName, + ::AppendError: Debug, { let mut res = Vec::new(); @@ -240,8 +244,13 @@ 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. + bitmap.add(Rtype::DNSKEY).unwrap(); + } + bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { bitmap.add(rrset.rtype()).unwrap() } @@ -275,10 +284,11 @@ impl SortedRecords { apex: &FamilyName, ttl: Ttl, params: Nsec3param, - opt_out: bool, + 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, @@ -289,6 +299,7 @@ impl SortedRecords { + AsMut<[u8]> + EmptyBuilder + FreezeBuilder, + ::Octets: AsRef<[u8]>, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -296,10 +307,23 @@ impl SortedRecords { // Reject old algorithms? if not, map 3 to 6 and 5 to 7, or reject // use of 3 and 5? + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, set the Opt-Out bit to one." + let mut nsec3_flags = params.flags(); + if matches!( + opt_out, + Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly + ) { + // Set the Opt-Out flag. + nsec3_flags |= 0b0000_0001; + } + // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." // 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; @@ -313,6 +337,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. @@ -343,7 +374,7 @@ impl SortedRecords { // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - if cut.is_some() && !has_ds && opt_out { + if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { continue; } @@ -352,9 +383,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 @@ -375,7 +417,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. @@ -386,22 +429,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(), - params.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); + } } } @@ -423,18 +453,42 @@ impl SortedRecords { if distance_to_apex == 0 { bitmap.add(Rtype::NSEC3PARAM).unwrap(); + bitmap.add(Rtype::DNSKEY).unwrap(); } - // RFC 5155 7.1 step 2: - // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = params.flags(); - if opt_out { - // Set the Opt-Out flag. - nsec3_flags |= 0b0000_0001; + let rec = Self::mk_nsec3( + name.owner(), + 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.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.owner(), + &name, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -444,7 +498,14 @@ 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); + } + + // 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: @@ -484,9 +545,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> @@ -495,9 +562,49 @@ impl SortedRecords { D: RecordData + fmt::Display, W: io::Write, { - for record in &self.records { - writeln!(target, "{}", record)?; + for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) + { + writeln!(target, "{record}")?; } + + for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) + { + writeln!(target, "{record}")?; + } + + 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(()) } } @@ -578,7 +685,7 @@ impl SortedRecords { } } -impl Default for SortedRecords { +impl Default for SortedRecords { fn default() -> Self { Self::new() } @@ -623,21 +730,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 } } @@ -719,30 +839,6 @@ impl FamilyName { { Record::new(self.owner.clone(), self.class, ttl, data) } - - pub fn dnskey>( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: Clone, - { - key.dnskey() - .map(|dnskey| self.clone().into_record(ttl, dnskey.convert())) - } - - pub fn ds( - &self, - ttl: Ttl, - key: K, - ) -> Result>, K::Error> - where - N: ToName + Clone, - { - key.ds(&self.owner) - .map(|ds| self.clone().into_record(ttl, ds)) - } } impl<'a, N: Clone> FamilyName<&'a N> { @@ -947,3 +1043,54 @@ where Some(Rrset::new(res)) } } + +//------------ ErrorTypeToBeDetermined --------------------------------------- + +#[derive(Debug)] +pub struct ErrorTypeToBeDetermined; + +//------------ Nsec3OptOut --------------------------------------------------- + +/// The different types of NSEC3 opt-out. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Nsec3OptOut { + /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure + /// delegations will be included in the NSEC3 chain. + #[default] + NoOptOut, + + /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure + /// delegations will NOT be included in the NSEC3 chain. + OptOut, + + /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and + /// insecure delegations will be included in the NSEC3 chain. + OptOutFlagsOnly, +} + +// TODO: Add tests for nsec3s() that validate the following from RFC 5155: +// +// 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. +// However, if there is not a corresponding NSEC3 RR, there MUST be +// 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." diff --git a/src/validate.rs b/src/validate.rs index c806a48f9..612493237 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1703,6 +1703,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. From 414ea6c6b6f0ec1aecb1e1d66e48fcf4405b020e Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 10:24:58 +0100 Subject: [PATCH 198/569] [sign,validate] Add 'display_as_bind()' to key bytes types --- src/sign/bytes.rs | 35 ++++++++++++++++++++++++++++++----- src/sign/openssl.rs | 10 +++++----- src/validate.rs | 30 +++++++++++++++++------------- 3 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/sign/bytes.rs b/src/sign/bytes.rs index 5b49f3328..1187a6dbf 100644 --- a/src/sign/bytes.rs +++ b/src/sign/bytes.rs @@ -130,7 +130,7 @@ impl SecretKeyBytes { /// The key is formatted in the private key v1.2 format and written to the /// given formatter. See the type-level documentation for a description /// of this format. - pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { writeln!(w, "Private-key-format: v1.2")?; match self { Self::RsaSha256(k) => { @@ -160,6 +160,19 @@ impl SecretKeyBytes { } } + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a SecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be @@ -289,7 +302,7 @@ impl RsaSecretKeyBytes { /// given formatter. Note that the header and algorithm lines are not /// written. See the type-level documentation of [`SecretKeyBytes`] for a /// description of this format. - pub fn format_as_bind(&self, w: &mut impl fmt::Write) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { w.write_str("Modulus: ")?; writeln!(w, "{}", base64::encode_display(&self.n))?; w.write_str("PublicExponent: ")?; @@ -309,6 +322,19 @@ impl RsaSecretKeyBytes { Ok(()) } + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a>(&'a RsaSecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + /// Parse a secret key from the conventional format used by BIND. /// /// This parser supports the private key v1.2 format, but it should be @@ -464,7 +490,7 @@ impl std::error::Error for BindFormatError {} #[cfg(test)] mod tests { - use std::{string::String, vec::Vec}; + use std::{string::ToString, vec::Vec}; use crate::base::iana::SecAlg; @@ -496,8 +522,7 @@ mod tests { let path = format!("test-data/dnssec-keys/K{}.private", name); let data = std::fs::read_to_string(path).unwrap(); let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); - let mut same = String::new(); - key.format_as_bind(&mut same).unwrap(); + let same = key.display_as_bind().to_string(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); assert_eq!(data, same); diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index c5620c22e..e1922ffdb 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -433,7 +433,10 @@ impl std::error::Error for GenerateError {} #[cfg(test)] mod tests { - use std::{string::String, vec::Vec}; + use std::{ + string::{String, ToString}, + vec::Vec, + }; use crate::{ base::iana::SecAlg, @@ -503,10 +506,7 @@ mod tests { let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); - - let equiv = key.to_bytes(); - let mut same = String::new(); - equiv.format_as_bind(&mut same).unwrap(); + let same = key.to_bytes().display_as_bind().to_string(); let data = data.lines().collect::>(); let same = same.lines().collect::>(); diff --git a/src/validate.rs b/src/validate.rs index 612493237..37826d796 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -311,23 +311,28 @@ impl> Key { /// Serialize this key in the conventional format used by BIND. /// - /// A user-specified DNS class can be used in the record; however, this - /// will almost always just be `IN`. - /// /// See the type-level documentation for a description of this format. - pub fn format_as_bind( - &self, - class: Class, - w: &mut impl fmt::Write, - ) -> fmt::Result { + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { writeln!( w, - "{} {} DNSKEY {}", + "{} IN DNSKEY {}", self.owner().fmt_with_dot(), - class, self.to_dnskey().display_zonefile(false), ) } + + /// Display this key in the conventional format used by BIND. + /// + /// See the type-level documentation for a description of this format. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + struct Display<'a, Octs>(&'a Key); + impl<'a, Octs: AsRef<[u8]>> fmt::Display for Display<'a, Octs> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } } //--- Comparison @@ -1244,7 +1249,7 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; - use std::string::String; + use std::string::{String, ToString}; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; @@ -1378,8 +1383,7 @@ mod test { let path = format!("test-data/dnssec-keys/K{}.key", name); let data = std::fs::read_to_string(path).unwrap(); let key = Key::>::parse_from_bind(&data).unwrap(); - let mut bind_fmt_key = String::new(); - key.format_as_bind(Class::IN, &mut bind_fmt_key).unwrap(); + let bind_fmt_key = key.display_as_bind().to_string(); let same = Key::parse_from_bind(&bind_fmt_key).unwrap(); assert_eq!(key, same); } From 2bde7aab351fbd9643ac1ffb5c34dc5ab6da474a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 11:02:57 +0100 Subject: [PATCH 199/569] [sign,validate] remove unused imports --- src/sign/openssl.rs | 5 +---- src/validate.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index e1922ffdb..814a55da2 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -433,10 +433,7 @@ impl std::error::Error for GenerateError {} #[cfg(test)] mod tests { - use std::{ - string::{String, ToString}, - vec::Vec, - }; + use std::{string::ToString, vec::Vec}; use crate::{ base::iana::SecAlg, diff --git a/src/validate.rs b/src/validate.rs index 37826d796..3293df0f0 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1249,7 +1249,7 @@ mod test { use crate::utils::base64; use bytes::Bytes; use std::str::FromStr; - use std::string::{String, ToString}; + use std::string::ToString; type Name = crate::base::name::Name>; type Ds = crate::rdata::Ds>; From 98db88b5812aedf4860bee84009b54bc122d6f06 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Thu, 31 Oct 2024 11:28:37 +0100 Subject: [PATCH 200/569] [sign] Document everything --- src/sign/common.rs | 4 ++ src/sign/mod.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++- src/sign/openssl.rs | 8 ++++ src/sign/ring.rs | 7 ++++ 4 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/sign/common.rs b/src/sign/common.rs index d5aaf5b67..fc10803e3 100644 --- a/src/sign/common.rs +++ b/src/sign/common.rs @@ -1,4 +1,8 @@ //! DNSSEC signing using built-in backends. +//! +//! This backend supports all the algorithms supported by Ring and OpenSSL, +//! depending on whether the respective crate features are enabled. See the +//! documentation for each backend for more information. use core::fmt; use std::sync::Arc; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e5f94a843..586bedada 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -7,6 +7,87 @@ //! made "online" (in an authoritative name server while it is running) or //! "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. +//! +//! # Example Usage +//! +//! At the moment, only "low-level" signing is supported. +//! +//! ``` +//! # use domain::sign::*; +//! # use domain::base::Name; +//! // Generate a new ED25519 key. +//! let params = GenerateParams::Ed25519; +//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! +//! // Associate the key with important metadata. +//! let owner: Name> = "www.example.org.".parse().unwrap(); +//! let flags = 257; // key signing key +//! let key = SigningKey::new(owner, flags, key_pair); +//! +//! // Access the public key (with metadata). +//! let pub_key = key.public_key(); +//! println!("{:?}", pub_key); +//! +//! // Sign arbitrary byte sequences with the key. +//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); +//! println!("{:?}", sig); +//! ``` +//! +//! # Cryptography +//! +//! 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. +//! +//! 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. +//! +//! Users can choose to bring their own cryptography by providing their own +//! `KeyPair` type that implements [`SignRaw`]. Note that `async` signing +//! (useful for interacting with cryptographic hardware like HSMs) is not +//! currently supported. +//! +//! While each cryptographic backend can support a limited number of signature +//! algorithms, even the types independent of a cryptographic backend (e.g. +//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of +//! algorithms. They are: +//! +//! - RSA/SHA-256 +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 +//! +//! # Importing and Exporting +//! +//! The [`SecretKeyBytes`] type is a generic representation of a secret key as +//! a byte slice. While it does not offer any cryptographic functionality, it +//! is useful to transfer secret keys stored in memory, independent of any +//! cryptographic backend. +//! +//! The `KeyPair` types of the cryptographic backends in this module each +//! support a `from_bytes()` function that parses the generic representation +//! into a functional cryptographic key. Importantly, these functions require +//! both the public and private keys to be provided -- the pair are verified +//! for consistency. In some cases, it may also be possible to serialize an +//! existing cryptographic key back to the generic bytes representation. +//! +//! [`SecretKeyBytes`] also supports importing and exporting keys from and to +//! the conventional private-key format popularized by BIND. This format is +//! used by a variety of tools for storing DNSSEC keys on disk. See the +//! type-level documentation for a specification of the format. #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] @@ -195,7 +276,14 @@ pub trait SignRaw { #[derive(Clone, Debug, PartialEq, Eq)] pub enum GenerateParams { /// Generate an RSA/SHA-256 keypair. - RsaSha256 { bits: u32 }, + RsaSha256 { + /// The number of bits in the public modulus. + /// + /// 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. + bits: u32, + }, /// Generate an ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256, diff --git a/src/sign/openssl.rs b/src/sign/openssl.rs index 814a55da2..85257137a 100644 --- a/src/sign/openssl.rs +++ b/src/sign/openssl.rs @@ -1,4 +1,12 @@ //! DNSSEC signing using OpenSSL. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (512-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 #![cfg(feature = "openssl")] #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] diff --git a/src/sign/ring.rs b/src/sign/ring.rs index 1b747642f..09435188c 100644 --- a/src/sign/ring.rs +++ b/src/sign/ring.rs @@ -1,4 +1,11 @@ //! DNSSEC signing using `ring`. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (2048-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 #![cfg(feature = "ring")] #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] From 8877c2286a7c6f1f752d6af1905ce86f03ea5450 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:03:07 +0100 Subject: [PATCH 201/569] Update to work with changes in the upstream dnssec-key branch using a partial backport of changes from the downstream multiple-signing-key branch. --- src/sign/records.rs | 50 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 44380347c..facb6ac94 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -157,18 +157,19 @@ impl SortedRecords { key.public_key().key_tag(), apex.owner().clone(), ); + + buf.clear(); rrsig.compose_canonical(&mut buf).unwrap(); for record in rrset.iter() { record.compose_canonical(&mut buf).unwrap(); } - - // Create and push the RRSIG record. let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { return Err(ErrorTypeToBeDetermined); }; + // Create and push the RRSIG record. res.push(Record::new( name.owner().clone(), name.class(), @@ -1094,3 +1095,48 @@ pub enum Nsec3OptOut { // 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." +// #[cfg(test)] +// mod tests { +// use core::str::FromStr; + +// use crate::rdata::A; + +// use super::*; + +// #[test] +// fn nsec3s() { +// fn mk_test_record(name: &str) -> Record>, A> { +// Record::new( +// Name::>::from_str(name).unwrap(), +// Class::IN, +// Ttl::from_days(1), +// A::new("127.0.0.1".parse().unwrap()), +// ) +// } + +// let mut recs = SortedRecords::new(); +// recs.insert(mk_test_record("mail.example.com")).unwrap(); +// recs.insert(mk_test_record("x.y.mail.example.com")).unwrap(); +// recs.insert(mk_test_record("a.b.c.mail.example.com")) +// .unwrap(); +// recs.insert(mk_test_record("a.other.c.mail.example.com")) +// .unwrap(); + +// for rec in recs.families() { +// println!("{}", rec.family_name().owner()); +// } + +// let mut recs = SortedRecords::new(); +// recs.insert(mk_test_record("y.mail.example.com")).unwrap(); +// recs.insert(mk_test_record("c.mail.example.com")).unwrap(); +// recs.insert(mk_test_record("b.c.mail.example.com")).unwrap(); +// recs.insert(mk_test_record("c.mail.example.com")); +// recs.insert(mk_test_record("other.c.mail.example.com")) +// .unwrap(); + +// println!(); +// for rec in recs.families() { +// println!("{}", rec.family_name().owner()); +// } +// } +// } From 40d65ac129ec1e0dbfc1bcc90446975bb00f5014 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 4 Nov 2024 23:45:17 +0100 Subject: [PATCH 202/569] Minor tweaks. --- src/sign/records.rs | 50 ++------------------------------------------- 1 file changed, 2 insertions(+), 48 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index facb6ac94..44380347c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -157,19 +157,18 @@ impl SortedRecords { key.public_key().key_tag(), apex.owner().clone(), ); - - buf.clear(); rrsig.compose_canonical(&mut buf).unwrap(); for record in rrset.iter() { record.compose_canonical(&mut buf).unwrap(); } + + // Create and push the RRSIG record. let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { return Err(ErrorTypeToBeDetermined); }; - // Create and push the RRSIG record. res.push(Record::new( name.owner().clone(), name.class(), @@ -1095,48 +1094,3 @@ pub enum Nsec3OptOut { // 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." -// #[cfg(test)] -// mod tests { -// use core::str::FromStr; - -// use crate::rdata::A; - -// use super::*; - -// #[test] -// fn nsec3s() { -// fn mk_test_record(name: &str) -> Record>, A> { -// Record::new( -// Name::>::from_str(name).unwrap(), -// Class::IN, -// Ttl::from_days(1), -// A::new("127.0.0.1".parse().unwrap()), -// ) -// } - -// let mut recs = SortedRecords::new(); -// recs.insert(mk_test_record("mail.example.com")).unwrap(); -// recs.insert(mk_test_record("x.y.mail.example.com")).unwrap(); -// recs.insert(mk_test_record("a.b.c.mail.example.com")) -// .unwrap(); -// recs.insert(mk_test_record("a.other.c.mail.example.com")) -// .unwrap(); - -// for rec in recs.families() { -// println!("{}", rec.family_name().owner()); -// } - -// let mut recs = SortedRecords::new(); -// recs.insert(mk_test_record("y.mail.example.com")).unwrap(); -// recs.insert(mk_test_record("c.mail.example.com")).unwrap(); -// recs.insert(mk_test_record("b.c.mail.example.com")).unwrap(); -// recs.insert(mk_test_record("c.mail.example.com")); -// recs.insert(mk_test_record("other.c.mail.example.com")) -// .unwrap(); - -// println!(); -// for rec in recs.families() { -// println!("{}", rec.family_name().owner()); -// } -// } -// } From bdedddee187c14379270506e62ad319cdf694073 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:34:50 +0100 Subject: [PATCH 203/569] Add some Arbitrary impls to support cargo-fuzz based fuzz testing. --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 1 + src/base/iana/macros.rs | 1 + src/base/name/absolute.rs | 1 + src/rdata/nsec3.rs | 1 + 5 files changed, 25 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index ca7fb4b69..7f844fa92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -47,6 +47,15 @@ dependencies = [ "libc", ] +[[package]] +name = "arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "arc-swap" version = "1.7.1" @@ -217,10 +226,22 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "derive_arbitrary" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "domain" version = "0.10.3" dependencies = [ + "arbitrary", "arc-swap", "bytes", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 4c60ad9d7..254b4a5c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ name = "domain" path = "src/lib.rs" [dependencies] +arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } octseq = { version = "0.5.2", default-features = false } time = { version = "0.3.1", default-features = false } rand = { version = "0.8", optional = true } diff --git a/src/base/iana/macros.rs b/src/base/iana/macros.rs index 5aa236a82..2c6d13908 100644 --- a/src/base/iana/macros.rs +++ b/src/base/iana/macros.rs @@ -13,6 +13,7 @@ macro_rules! int_enum { $value:expr, $mnemonic:expr) )* ) => { $(#[$attr])* #[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)] + #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct $ianatype($inttype); impl $ianatype { diff --git a/src/base/name/absolute.rs b/src/base/name/absolute.rs index 394521d05..bf8b20d8e 100644 --- a/src/base/name/absolute.rs +++ b/src/base/name/absolute.rs @@ -50,6 +50,7 @@ use std::vec::Vec; /// [`Display`]: std::fmt::Display #[derive(Clone)] #[repr(transparent)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Name(Octs); impl Name<()> { diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index a09e4c309..d80f12b9b 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -752,6 +752,7 @@ impl> ZonefileFmt for Nsec3param { /// no whitespace allowed. #[derive(Clone)] #[repr(transparent)] +#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] pub struct Nsec3Salt(Octs); impl Nsec3Salt<()> { From f2cabc37eff3d85a42058180319926376604b7f6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:35:09 +0100 Subject: [PATCH 204/569] Impl Display for Nsec3HashError. --- src/validate.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/validate.rs b/src/validate.rs index 3293df0f0..37d44836f 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1712,6 +1712,19 @@ pub enum Nsec3HashError { CollisionDetected, } +///--- Display + +impl std::fmt::Display for Nsec3HashError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Nsec3HashError::UnsupportedAlgorithm => f.write_str("Unsupported algorithm"), + Nsec3HashError::AppendError => f.write_str("Append error: out of memory?"), + Nsec3HashError::OwnerHashError => f.write_str("Hashing produced an invalid owner hash"), + Nsec3HashError::CollisionDetected => f.write_str("Hash collision detected"), + } + } +} + /// Compute an [RFC 5155] NSEC3 hash using default settings. /// /// See: [Nsec3param::default]. From 109370d08abbba66fa64faac4bf2eca73f40018a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 10:41:22 +0100 Subject: [PATCH 205/569] Cargo fmt. --- src/validate.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/validate.rs b/src/validate.rs index 37d44836f..0523132ec 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1717,10 +1717,18 @@ pub enum Nsec3HashError { impl std::fmt::Display for Nsec3HashError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - Nsec3HashError::UnsupportedAlgorithm => f.write_str("Unsupported algorithm"), - Nsec3HashError::AppendError => f.write_str("Append error: out of memory?"), - Nsec3HashError::OwnerHashError => f.write_str("Hashing produced an invalid owner hash"), - Nsec3HashError::CollisionDetected => f.write_str("Hash collision detected"), + Nsec3HashError::UnsupportedAlgorithm => { + f.write_str("Unsupported algorithm") + } + Nsec3HashError::AppendError => { + f.write_str("Append error: out of memory?") + } + Nsec3HashError::OwnerHashError => { + f.write_str("Hashing produced an invalid owner hash") + } + Nsec3HashError::CollisionDetected => { + f.write_str("Hash collision detected") + } } } } From 0c26d94688e45e1767f37141af36f74cd7d5bae8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Nov 2024 23:46:15 +0100 Subject: [PATCH 206/569] Use a writer interface for write_with_comments(). --- src/sign/records.rs | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index e3ad7460c..06148fef6 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -662,7 +662,7 @@ impl SortedRecords { Ok(()) } - pub fn write_with_comments( + pub fn write_with_comments( &self, target: &mut W, comment_cb: F, @@ -671,25 +671,20 @@ impl SortedRecords { N: fmt::Display, D: RecordData + fmt::Display, W: io::Write, - C: fmt::Display, - F: Fn(&Record) -> Option, + F: Fn(&Record, &mut W) -> std::io::Result<()>, { 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}")?; - } + write!(target, "{record}")?; + comment_cb(record, target)?; + writeln!(target)?; } 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}")?; - } + write!(target, "{record}")?; + comment_cb(record, target)?; + writeln!(target)?; } Ok(()) From 588fd0f0414cef9b7346a7b805a0f97f1c6e1e68 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 00:34:12 +0100 Subject: [PATCH 207/569] Fix test broken by changed input file. --- src/net/server/middleware/xfr/tests.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index a3e6dab2c..d4849a25b 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -75,6 +75,8 @@ async fn axfr_with_example_zone() { (n("www.example.com"), Cname::new(n("example.com")).into()), (n("mail.example.com"), Mx::new(10, n("example.com")).into()), (n("a.b.c.mail.example.com"), A::new(p("127.0.0.1")).into()), + (n("x.y.mail.example.com"), A::new(p("127.0.0.1")).into()), + (n("some.ent.example.com"), A::new(p("127.0.0.1")).into()), ( n("unsigned.example.com"), Ns::new(n("some.other.ns.net.example.com")).into(), From 9cad710d6c1d4c1be83bc42d7d91e795ddabcaf9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 13:04:37 +0100 Subject: [PATCH 208/569] Add do not add used keys to zone support. --- src/sign/records.rs | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 06148fef6..6f4613bc6 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -98,6 +98,7 @@ impl SortedRecords { expiration: Timestamp, inception: Timestamp, keys: &[SigningKey], + add_used_dnskeys: bool, ) -> Result< Vec>>, ErrorTypeToBeDetermined, @@ -160,6 +161,7 @@ impl SortedRecords { for public_key in keys.iter().map(|k| k.public_key()) { let dnskey: Dnskey = Dnskey::convert(public_key.to_dnskey()); + dnskey_rrs .insert(Record::new( apex.owner().clone(), @@ -169,15 +171,22 @@ impl SortedRecords { )) .map_err(|_| ErrorTypeToBeDetermined)?; - res.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - ZoneRecordData::Dnskey(dnskey), - )); + if add_used_dnskeys { + res.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + ZoneRecordData::Dnskey(dnskey), + )); + } } - let families_iter = dnskey_rrs.families().chain(families); + let dummy_dnskey_rrs = SortedRecords::new(); + let families_iter = if add_used_dnskeys { + dnskey_rrs.families().chain(families) + } else { + dummy_dnskey_rrs.families().chain(families) + }; for family in families_iter { // If the owner is out of zone, we have moved out of our zone and @@ -271,6 +280,7 @@ impl SortedRecords { &self, apex: &FamilyName, ttl: Ttl, + assume_dnskeys_will_be_added: bool, ) -> Vec>> where N: ToName + Clone + PartialEq, @@ -331,9 +341,8 @@ impl SortedRecords { } let mut bitmap = RtypeBitmap::::builder(); - // Assume there's gonna be an RRSIG. bitmap.add(Rtype::RRSIG).unwrap(); - if family.owner() == &apex_owner { + if assume_dnskeys_will_be_added && family.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); } @@ -372,6 +381,7 @@ impl SortedRecords { ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, + assume_dnskeys_will_be_added: bool, capture_hash_to_owner_mappings: bool, ) -> Result, Nsec3HashError> where @@ -540,7 +550,9 @@ impl SortedRecords { if distance_to_apex == 0 { bitmap.add(Rtype::NSEC3PARAM).unwrap(); - bitmap.add(Rtype::DNSKEY).unwrap(); + if assume_dnskeys_will_be_added { + bitmap.add(Rtype::DNSKEY).unwrap(); + } } let rec = Self::mk_nsec3( From 06a9f0ddb91509069480dca32803643b897fd6cf Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 7 Nov 2024 14:31:32 +0100 Subject: [PATCH 209/569] Add SortedRecords::replace_soa(). --- src/sign/records.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 6f4613bc6..b1fd3d50e 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -24,9 +24,11 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, ZoneRecordData}; +use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Soa, ZoneRecordData}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; +use crate::zonetree::types::StoredRecordData; +use crate::zonetree::StoredName; use super::{SignRaw, SigningKey}; @@ -77,7 +79,23 @@ impl SortedRecords { { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } +} +impl SortedRecords { + pub fn replace_soa(&mut self, new_soa: Soa) { + if let Some(soa_rrset) = self + .records + .iter_mut() + .find(|rrset| rrset.rtype() == Rtype::SOA) + { + if let ZoneRecordData::Soa(current_soa) = soa_rrset.data_mut() { + *current_soa = new_soa; + } + } + } +} + +impl SortedRecords { /// Sign a zone using the given keys. /// /// A DNSKEY RR will be output for each key. From 42cbd0d966c0887b05838ff6ec2eaf754b906502 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 20 Nov 2024 17:43:50 +0100 Subject: [PATCH 210/569] Cargo format --- src/rdata/zonemd.rs | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index 66f41d400..e35c7349c 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -13,8 +13,8 @@ use crate::base::iana::Rtype; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; -use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::utils::base16; use core::cmp::Ordering; use core::{fmt, hash}; @@ -233,20 +233,26 @@ impl> ZonefileFmt for Zonemd { p.block(|p| { p.write_token(self.serial)?; p.write_show(self.scheme)?; - p.write_comment(format_args!("scheme ({})", match self.scheme { - Scheme::Reserved => "reserved", - Scheme::Simple => "simple", - Scheme::Unassigned(_) => "unassigned", - Scheme::Private(_) => "private", - }))?; + p.write_comment(format_args!( + "scheme ({})", + match self.scheme { + Scheme::Reserved => "reserved", + Scheme::Simple => "simple", + Scheme::Unassigned(_) => "unassigned", + Scheme::Private(_) => "private", + } + ))?; p.write_show(self.algo)?; - p.write_comment(format_args!("algorithm ({})", match self.algo { - Algorithm::Reserved => "reserved", - Algorithm::Sha384 => "SHA384", - Algorithm::Sha512 => "SHA512", - Algorithm::Unassigned(_) => "unassigned", - Algorithm::Private(_) => "private", - }))?; + p.write_comment(format_args!( + "algorithm ({})", + match self.algo { + Algorithm::Reserved => "reserved", + Algorithm::Sha384 => "SHA384", + Algorithm::Sha512 => "SHA512", + Algorithm::Unassigned(_) => "unassigned", + Algorithm::Private(_) => "private", + } + ))?; p.write_token(base16::encode_display(&self.digest)) }) } From 90aae208a9b59a4e475212057813e200c3397d31 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 20 Nov 2024 17:44:13 +0100 Subject: [PATCH 211/569] Implement FromStr for zonemd Scheme and Algorithm --- src/rdata/zonemd.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index e35c7349c..ed53f94df 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -17,6 +17,7 @@ use crate::base::wire::{Composer, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::utils::base16; use core::cmp::Ordering; +use core::str::FromStr; use core::{fmt, hash}; use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::Parser; @@ -356,12 +357,41 @@ impl From for Scheme { } } +impl FromStr for Scheme { + type Err = SchemeFromStrError; + + // Only implement the actionable variants + fn from_str(s: &str) -> Result { + match s { + "1" | "SIMPLE" => Ok(Self::Simple), + _ => Err(SchemeFromStrError(())), + } + } +} + impl ZonefileFmt for Scheme { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { p.write_token(u8::from(*self)) } } +//------------ SchemeFromStrError --------------------------------------------- + +/// An error occured while reading the scheme from a string. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct SchemeFromStrError(()); + +//--- Display and Error + +impl fmt::Display for SchemeFromStrError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unknown zonemd scheme mnemonic") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for SchemeFromStrError {} + /// The Hash Algorithm used to construct the digest. /// /// This enumeration wraps an 8-bit unsigned integer that identifies @@ -400,12 +430,42 @@ impl From for Algorithm { } } +impl FromStr for Algorithm { + type Err = AlgorithmFromStrError; + + // Only implement the actionable variants + fn from_str(s: &str) -> Result { + match s { + "1" | "SHA384" => Ok(Self::Sha384), + "2" | "SHA512" => Ok(Self::Sha512), + _ => Err(AlgorithmFromStrError(())), + } + } +} + impl ZonefileFmt for Algorithm { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { p.write_token(u8::from(*self)) } } +//------------ AlgorithmFromStrError --------------------------------------------- + +/// An error occured while reading the algorithm from a string. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct AlgorithmFromStrError(()); + +//--- Display and Error + +impl fmt::Display for AlgorithmFromStrError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unknown zonemd hash algorithm mnemonic") + } +} + +#[cfg(feature = "std")] +impl std::error::Error for AlgorithmFromStrError {} + #[cfg(test)] #[cfg(all(feature = "std", feature = "bytes"))] mod test { From d390d15babb641d697458f5cd382ee3f74f760ca Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 21 Nov 2024 13:01:30 +0100 Subject: [PATCH 212/569] Use std::fmt::Write instead of std::io::Write. --- src/sign/records.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 2a79f1440..f14891b6f 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -8,7 +8,7 @@ use std::fmt::Debug; use std::hash::Hash; use std::string::String; use std::vec::Vec; -use std::{fmt, io, slice}; +use std::{fmt, slice}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; @@ -677,20 +677,20 @@ impl SortedRecords { } } - pub fn write(&self, target: &mut W) -> Result<(), io::Error> + pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> where N: fmt::Display, D: RecordData + fmt::Display, - W: io::Write, + W: fmt::Write, { for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) { - writeln!(target, "{record}")?; + write!(target, "{record}")?; } for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) { - writeln!(target, "{record}")?; + write!(target, "{record}")?; } Ok(()) @@ -700,12 +700,12 @@ impl SortedRecords { &self, target: &mut W, comment_cb: F, - ) -> Result<(), io::Error> + ) -> Result<(), fmt::Error> where N: fmt::Display, D: RecordData + fmt::Display, - W: io::Write, - F: Fn(&Record, &mut W) -> std::io::Result<()>, + W: fmt::Write, + F: Fn(&Record, &mut W) -> Result<(), fmt::Error>, { for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) { From e59112145067cbbec7997af531bcfbf67c982c72 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:36:29 +0100 Subject: [PATCH 213/569] Proof of concept, expected to be replaced by a better impl (a) as a separate `FormatWriter` and (b) that only uses tabs between record fields and not between rdata values. --- src/base/dig_printer.rs | 4 ++-- src/base/zonefile_fmt.rs | 39 +++++++++++++++++++++++---------------- src/rdata/nsec3.rs | 4 ++-- src/sign/records.rs | 6 ++++++ src/validate.rs | 2 +- 5 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/base/dig_printer.rs b/src/base/dig_printer.rs index 426b8dc37..f67ad257c 100644 --- a/src/base/dig_printer.rs +++ b/src/base/dig_printer.rs @@ -24,7 +24,7 @@ impl<'a, Octs: AsRef<[u8]>> fmt::Display for DigPrinter<'a, Octs> { writeln!( f, ";; ->>HEADER<<- opcode: {}, rcode: {}, id: {}", - header.opcode().display_zonefile(false), + header.opcode().display_zonefile(false, false), header.rcode(), header.id() )?; @@ -161,7 +161,7 @@ fn write_record_item( let parsed = item.to_any_record::>(); match parsed { - Ok(item) => writeln!(f, "{}", item.display_zonefile(false)), + Ok(item) => writeln!(f, "{}", item.display_zonefile(false, false)), Err(_) => writeln!( f, "; {} {} {} {} ", diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index 8a9e22e75..72b21a0af 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -13,18 +13,19 @@ pub type Result = core::result::Result<(), Error>; pub struct ZoneFileDisplay<'a, T: ?Sized> { inner: &'a T, - pretty: bool, + multiline: bool, + tabbed: bool, } impl fmt::Display for ZoneFileDisplay<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.pretty { + if self.multiline { self.inner .fmt(&mut MultiLineWriter::new(f)) .map_err(|_| fmt::Error) } else { self.inner - .fmt(&mut SimpleWriter::new(f)) + .fmt(&mut SimpleWriter::new(f, self.tabbed)) .map_err(|_| fmt::Error) } } @@ -41,10 +42,11 @@ pub trait ZonefileFmt { /// /// The returned object will be displayed as zonefile when printed or /// written using `fmt::Display`. - fn display_zonefile(&self, pretty: bool) -> ZoneFileDisplay<'_, Self> { + fn display_zonefile(&self, multiline: bool, tabbed: bool) -> ZoneFileDisplay<'_, Self> { ZoneFileDisplay { inner: self, - pretty, + multiline, + tabbed, } } } @@ -88,13 +90,15 @@ pub trait FormatWriter: Sized { struct SimpleWriter { first: bool, writer: W, + tabbed: bool, } impl SimpleWriter { - fn new(writer: W) -> Self { + fn new(writer: W, tabbed: bool) -> Self { Self { first: true, writer, + tabbed, } } } @@ -102,7 +106,10 @@ impl SimpleWriter { impl FormatWriter for SimpleWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - self.writer.write_char(' ')?; + match self.tabbed { + true => self.writer.write_char('\t')?, + false => self.writer.write_char(' ')?, + } } self.first = false; self.writer.write_fmt(args)?; @@ -251,7 +258,7 @@ mod test { let record = create_record(A::new("128.140.76.106".parse().unwrap())); assert_eq!( "example.com. 3600 IN A 128.140.76.106", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -262,7 +269,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN CNAME example.com.", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -279,7 +286,7 @@ mod test { ); assert_eq!( "example.com. 3600 IN DS 5414 15 2 DEADBEEF", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); assert_eq!( [ @@ -289,7 +296,7 @@ mod test { " DEADBEEF )", ] .join("\n"), - record.display_zonefile(true).to_string() + record.display_zonefile(true, false).to_string() ); } @@ -306,7 +313,7 @@ mod test { ); assert_eq!( "example.com. 3600 IN CDS 5414 15 2 DEADBEEF", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -318,7 +325,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN MX 20 example.com.", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -338,7 +345,7 @@ mod test { more like a silly monkey with a typewriter accidentally writing \ some shakespeare along the way but it feels like I have to type \ e\" \"ven longer to hit that limit!\"", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -351,7 +358,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN HINFO \"Windows\" \"Windows Server\"", - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } @@ -368,7 +375,7 @@ mod test { )); assert_eq!( r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#, - record.display_zonefile(false).to_string() + record.display_zonefile(false, false).to_string() ); } } diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index d80f12b9b..f57f48166 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -1608,7 +1608,7 @@ mod test { Nsec3::scan, &rdata, ); - assert_eq!(&format!("{}", rdata.display_zonefile(false)), "1 10 11 626172 CPNMU A SRV"); + assert_eq!(&format!("{}", rdata.display_zonefile(false, false)), "1 10 11 626172 CPNMU A SRV"); } #[test] @@ -1632,7 +1632,7 @@ mod test { Nsec3::scan, &rdata, ); - assert_eq!(&format!("{}", rdata.display_zonefile(false)), "1 10 11 - CPNMU A SRV"); + assert_eq!(&format!("{}", rdata.display_zonefile(false, false)), "1 10 11 - CPNMU A SRV"); } #[test] diff --git a/src/sign/records.rs b/src/sign/records.rs index f14891b6f..8993842e8 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -31,6 +31,7 @@ use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; use super::{SignRaw, SigningKey}; +use core::slice::Iter; //------------ SortedRecords ------------------------------------------------- @@ -79,6 +80,11 @@ impl SortedRecords { { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } + + + pub fn iter(&self) -> Iter<'_, Record> { + self.records.iter() + } } impl SortedRecords { diff --git a/src/validate.rs b/src/validate.rs index cdcc18312..ea1ef8b82 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -322,7 +322,7 @@ impl> Key { w, "{} IN DNSKEY {}", self.owner().fmt_with_dot(), - self.to_dnskey().display_zonefile(false), + self.to_dnskey().display_zonefile(false, false), ) } From b2a2169013c94e0646127b7b51cc5d300e9ee72d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Nov 2024 11:39:08 +0100 Subject: [PATCH 214/569] Cargo fmt. --- src/base/zonefile_fmt.rs | 6 +++++- src/sign/records.rs | 1 - 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index 72b21a0af..dd7860586 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -42,7 +42,11 @@ pub trait ZonefileFmt { /// /// The returned object will be displayed as zonefile when printed or /// written using `fmt::Display`. - fn display_zonefile(&self, multiline: bool, tabbed: bool) -> ZoneFileDisplay<'_, Self> { + fn display_zonefile( + &self, + multiline: bool, + tabbed: bool, + ) -> ZoneFileDisplay<'_, Self> { ZoneFileDisplay { inner: self, multiline, diff --git a/src/sign/records.rs b/src/sign/records.rs index 8993842e8..75a1f252c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -81,7 +81,6 @@ impl SortedRecords { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } - pub fn iter(&self) -> Iter<'_, Record> { self.records.iter() } From 0830acd9e24215c55777230076f1a5052045a151 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sat, 23 Nov 2024 00:10:28 +0100 Subject: [PATCH 215/569] Impl Clone for Family. --- src/sign/records.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index f14891b6f..01a012d93 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -880,6 +880,7 @@ impl Nsec3Records { //------------ Family -------------------------------------------------------- /// A set of records with the same owner name and class. +#[derive(Clone)] pub struct Family<'a, N, D> { slice: &'a [Record], } From 19d8d88d9f050c0b750c21021a1af9032033ec89 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:19:57 +0100 Subject: [PATCH 216/569] Bring your own signing sort impl. Allows consumers to e.g. use Rayon for faster multi-threaded sorting instead of the default slow single-threaded sorting. Also, don't insert into a self-sorting collection while collecting NSEC3s, instead push to an unsorted vec then sort before iterating, as post-sorting is much faster. --- src/sign/records.rs | 130 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 27 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index fe9c4eb1f..bb46975c3 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,6 +1,9 @@ //! Actual signing. +use core::cmp::Ordering; use core::convert::From; use core::fmt::Display; +use core::marker::PhantomData; +use core::slice::Iter; use std::boxed::Box; use std::collections::HashMap; @@ -31,20 +34,74 @@ use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; use super::{SignRaw, SigningKey}; -use core::slice::Iter; + +//------------ Sorter -------------------------------------------------------- + +/// A DNS resource record sorter. +/// +/// Implement this trait to use a different sorting algorithm than that +/// implemented by [`DefaultSorter`], e.g. to use system resources in a +/// different way when sorting. +pub trait Sorter { + /// Sort the given DNS resource records. + /// + /// The imposed order should be compatible with the ordering defined by + /// RFC 8976 section 3.3.1, i.e. _"DNSSEC's canonical on-the-wire RR + /// format (without name compression) and ordering as specified in + /// Sections 6.1, 6.2, and 6.3 of [RFC4034] with the additional provision + /// that RRsets having the same owner name MUST be numerically ordered, in + /// ascending order, by their numeric RR TYPE"_. + fn sort_by(records: &mut Vec>, compare: F) + where + Record: Send, + F: Fn(&Record, &Record) -> Ordering + Sync; +} + +//------------ DefaultSorter ------------------------------------------------- + +/// The default [`Sorter`] implementation used by [`SortedRecords`]. +/// +/// The current implementation is the single threaded sort provided by Rust +/// [`std::vec::Vec::sort_by()`]. +pub struct DefaultSorter; + +impl Sorter for DefaultSorter { + fn sort_by(records: &mut Vec>, compare: F) + where + Record: Send, + F: Fn(&Record, &Record) -> Ordering + Sync, + { + records.sort_by(compare); + } +} //------------ SortedRecords ------------------------------------------------- /// A collection of resource records sorted for signing. +/// +/// The sort algorithm used defaults to [`DefaultSorter`] but can be +/// overridden by being generic over an alternate implementation of +/// [`Sorter`]. #[derive(Clone)] -pub struct SortedRecords { +pub struct SortedRecords +where + Record: Send, + S: Sorter, +{ records: Vec>, + + _phantom: PhantomData, } -impl SortedRecords { +impl SortedRecords +where + Record: Send, + S: Sorter, +{ pub fn new() -> Self { SortedRecords { records: Vec::new(), + _phantom: Default::default(), } } @@ -86,7 +143,7 @@ impl SortedRecords { } } -impl SortedRecords { +impl SortedRecords { pub fn replace_soa(&mut self, new_soa: Soa) { if let Some(soa_rrset) = self .records @@ -100,7 +157,13 @@ impl SortedRecords { } } -impl SortedRecords { +impl SortedRecords +where + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + S: Sorter, + SortedRecords: From>>, +{ /// Sign a zone using the given keys. /// /// A DNSKEY RR will be output for each key. @@ -179,7 +242,7 @@ impl SortedRecords { let apex_ttl = families.peek().unwrap().records().next().unwrap().ttl(); - let mut dnskey_rrs = SortedRecords::new(); + let mut dnskey_rrs = SortedRecords::::new(); for public_key in keys.iter().map(|k| k.public_key()) { let dnskey: Dnskey = @@ -204,7 +267,7 @@ impl SortedRecords { } } - let dummy_dnskey_rrs = SortedRecords::new(); + let dummy_dnskey_rrs = SortedRecords::::new(); let families_iter = if add_used_dnskeys { dnskey_rrs.families().chain(families) } else { @@ -414,8 +477,8 @@ impl SortedRecords { where N: ToName + Clone + From> + Display + Ord + Hash, N: From::Octets>>, - D: RecordData, - Octets: FromBuilder + OctetsFrom> + Clone + Default, + D: RecordData + From>, + Octets: Send + FromBuilder + OctetsFrom> + Clone + Default, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, OctetsMut: OctetsBuilder @@ -444,7 +507,7 @@ impl SortedRecords { // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." // We store the NSEC3s as we create them in a self-sorting vec. - let mut nsec3s = SortedRecords::new(); + let mut nsec3s = Vec::>>::new(); let mut ents = Vec::::new(); @@ -582,7 +645,7 @@ impl SortedRecords { } } - let rec = Self::mk_nsec3( + let rec: Record> = Self::mk_nsec3( name.owner(), params.hash_algorithm(), nsec3_flags, @@ -599,9 +662,7 @@ impl SortedRecords { } // Store the record by order of its owner name. - if nsec3s.insert(rec).is_err() { - return Err(Nsec3HashError::CollisionDetected); - } + nsec3s.push(rec); if let Some(last_nent) = last_nent { last_nent_stack.push(last_nent); @@ -629,9 +690,7 @@ impl SortedRecords { } // Store the record by order of its owner name. - if nsec3s.insert(rec).is_err() { - return Err(Nsec3HashError::CollisionDetected); - } + nsec3s.push(rec); } // RFC 5155 7.1 step 7: @@ -639,7 +698,9 @@ impl SortedRecords { // value of the next NSEC3 RR in hash order. The next hashed owner // name of the last NSEC3 RR in the zone contains the value of the // hashed owner name of the first NSEC3 RR in the hash order." + let mut nsec3s = SortedRecords::, S>::from(nsec3s); for i in 1..=nsec3s.records.len() { + // TODO: Detect duplicate hashes. let next_i = if i == nsec3s.records.len() { 0 } else { i }; let cur_owner = nsec3s.records[next_i].owner(); let name: Name = cur_owner.try_to_name().unwrap(); @@ -731,7 +792,12 @@ impl SortedRecords { } /// Helper functions used to create NSEC3 records per RFC 5155. -impl SortedRecords { +impl SortedRecords +where + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + S: Sorter, +{ #[allow(clippy::too_many_arguments)] fn mk_nsec3( name: &N, @@ -748,6 +814,7 @@ impl SortedRecords { Octets: FromBuilder + Clone + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, + Nsec3: Into, { // Create the base32hex ENT NSEC owner name. let base32hex_label = @@ -806,24 +873,31 @@ impl SortedRecords { } } -impl Default for SortedRecords { +impl Default + for SortedRecords +{ fn default() -> Self { Self::new() } } -impl From>> for SortedRecords +impl From>> for SortedRecords where - N: ToName, - D: RecordData + CanonicalOrd, + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + S: Sorter, { fn from(mut src: Vec>) -> Self { - src.sort_by(CanonicalOrd::canonical_cmp); - SortedRecords { records: src } + S::sort_by(&mut src, CanonicalOrd::canonical_cmp); + SortedRecords { + records: src, + _phantom: Default::default(), + } } } -impl FromIterator> for SortedRecords +impl FromIterator> + for SortedRecords where N: ToName, D: RecordData + CanonicalOrd, @@ -837,15 +911,17 @@ where } } -impl Extend> for SortedRecords +impl Extend> + for SortedRecords where N: ToName, D: RecordData + CanonicalOrd, { fn extend>>(&mut self, iter: T) { for item in iter { - let _ = self.insert(item); + self.records.push(item); } + S::sort_by(&mut self.records, CanonicalOrd::canonical_cmp); } } From 7890d47bb49d647856adff2e72878819a435a1e7 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 27 Nov 2024 11:35:33 +0100 Subject: [PATCH 217/569] Add SortedRecords record deletion and rrsig replace methods --- src/sign/records.rs | 110 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 108 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 01a012d93..b6bc8612e 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,4 +1,5 @@ //! Actual signing. +use core::cmp::Ordering; use core::convert::From; use core::fmt::Display; @@ -10,6 +11,7 @@ use std::string::String; use std::vec::Vec; use std::{fmt, slice}; +use bytes::Bytes; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; use tracing::{debug, enabled, Level}; @@ -24,7 +26,9 @@ use crate::rdata::dnssec::{ ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, }; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Soa, ZoneRecordData}; +use crate::rdata::{ + Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, +}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; use crate::zonetree::types::StoredRecordData; @@ -64,6 +68,83 @@ impl SortedRecords { } } + /// Remove all records matching the owner name, class, and rtype. + /// Class and Rtype can be None to match any. + /// + /// Returns: + /// - Ok: if one or more matching records were found (and removed) + /// - Err: if no matching record was found + pub fn remove_all_by_name_class_rtype( + &mut self, + name: N, + class: Option, + rtype: Option, + ) -> Result<(), ()> + where + N: ToName + Clone, + D: RecordData, + { + let mut found_one = false; + loop { + match self.remove_first_by_name_class_rtype( + name.clone(), + class.clone(), + rtype.clone(), + ) { + Ok(_) => found_one = true, + Err(_) => break, + } + } + + found_one.then_some(()).ok_or(()) + } + + /// Remove first records matching the owner name, class, and rtype. + /// Class and Rtype can be None to match any. + /// + /// Returns: + /// - Ok: if a matching record was found (and removed) + /// - Err: if no matching record was found + pub fn remove_first_by_name_class_rtype( + &mut self, + name: N, + class: Option, + rtype: Option, + ) -> Result<(), ()> + where + N: ToName, + D: RecordData, + { + let idx = self.records.binary_search_by(|stored| { + // Ordering based on base::Record::canonical_cmp excluding comparison of data + + if let Some(class) = class { + match stored.class().cmp(&class) { + Ordering::Equal => {} + res => return res, + } + } + + match stored.owner().name_cmp(&name) { + Ordering::Equal => {} + res => return res, + } + + if let Some(rtype) = rtype { + stored.rtype().cmp(&rtype) + } else { + Ordering::Equal + } + }); + match idx { + Ok(idx) => { + self.records.remove(idx); + return Ok(()); + } + Err(_) => return Err(()), + }; + } + pub fn families(&self) -> RecordsIter { RecordsIter::new(&self.records) } @@ -81,7 +162,7 @@ impl SortedRecords { } } -impl SortedRecords { +impl SortedRecords { pub fn replace_soa(&mut self, new_soa: Soa) { if let Some(soa_rrset) = self .records @@ -93,6 +174,31 @@ impl SortedRecords { } } } + + pub fn replace_rrsig_for_apex_zonemd( + &mut self, + new_rrsig: Rrsig, + apex: &FamilyName, + ) { + if let Some(zonemd_rrsig) = self.records.iter_mut().find(|record| { + if record.rtype() == Rtype::RRSIG + && record.owner().name_cmp(&apex.owner()) == Ordering::Equal + { + if let ZoneRecordData::Rrsig(rrsig) = record.data() { + if rrsig.type_covered() == Rtype::ZONEMD { + return true; + } + } + } + return false; + }) { + if let ZoneRecordData::Rrsig(current_rrsig) = + zonemd_rrsig.data_mut() + { + *current_rrsig = new_rrsig; + } + } + } } impl SortedRecords { From 4808c7033500bca5ed213da89305254a41bbc5fd Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 27 Nov 2024 12:16:38 +0100 Subject: [PATCH 218/569] Return bool from record removal methods --- src/sign/records.rs | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index b6bc8612e..4579c4f36 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -72,45 +72,46 @@ impl SortedRecords { /// Class and Rtype can be None to match any. /// /// Returns: - /// - Ok: if one or more matching records were found (and removed) - /// - Err: if no matching record was found + /// - true: if one or more matching records were found (and removed) + /// - false: if no matching record was found pub fn remove_all_by_name_class_rtype( &mut self, name: N, class: Option, rtype: Option, - ) -> Result<(), ()> + ) -> bool where N: ToName + Clone, D: RecordData, { let mut found_one = false; loop { - match self.remove_first_by_name_class_rtype( + if self.remove_first_by_name_class_rtype( name.clone(), class.clone(), rtype.clone(), ) { - Ok(_) => found_one = true, - Err(_) => break, + found_one = true + } else { + break; } } - found_one.then_some(()).ok_or(()) + found_one } /// Remove first records matching the owner name, class, and rtype. /// Class and Rtype can be None to match any. /// /// Returns: - /// - Ok: if a matching record was found (and removed) - /// - Err: if no matching record was found + /// - true: if a matching record was found (and removed) + /// - false: if no matching record was found pub fn remove_first_by_name_class_rtype( &mut self, name: N, class: Option, rtype: Option, - ) -> Result<(), ()> + ) -> bool where N: ToName, D: RecordData, @@ -139,9 +140,9 @@ impl SortedRecords { match idx { Ok(idx) => { self.records.remove(idx); - return Ok(()); + return true; } - Err(_) => return Err(()), + Err(_) => return false, }; } From 19fac46f59655524a92a7f22fff2d764b25a607a Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 27 Nov 2024 13:25:47 +0100 Subject: [PATCH 219/569] Clippy --- src/sign/records.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 4579c4f36..afe472325 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -88,8 +88,8 @@ impl SortedRecords { loop { if self.remove_first_by_name_class_rtype( name.clone(), - class.clone(), - rtype.clone(), + class, + rtype, ) { found_one = true } else { @@ -140,10 +140,10 @@ impl SortedRecords { match idx { Ok(idx) => { self.records.remove(idx); - return true; + true } - Err(_) => return false, - }; + Err(_) => false, + } } pub fn families(&self) -> RecordsIter { @@ -191,7 +191,7 @@ impl SortedRecords { } } } - return false; + false }) { if let ZoneRecordData::Rrsig(current_rrsig) = zonemd_rrsig.data_mut() From 967c628dd4d79f8ff365cfe09dd4f2855f5b7f49 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:13:09 +0100 Subject: [PATCH 220/569] Breaking change: Update ZONEMD IANA types to use the iana macros to be consistent with how other IANA parameters are codified in domain, and in so doing gain string and mnemonic conversion functions. This is a breaking change because it renames types, moves types within the module tree, and removes support for named variants for reserved and private use ranges for ZONEMD parameters (as we don't do that for other IANA parameters in domain). --- src/base/iana/mod.rs | 2 + src/base/iana/zonemd.rs | 50 +++++++++++ src/rdata/zonemd.rs | 184 +++------------------------------------- 3 files changed, 62 insertions(+), 174 deletions(-) create mode 100644 src/base/iana/zonemd.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 2b73fe624..9d86b2e94 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -35,6 +35,7 @@ pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecAlg; pub use self::svcb::SvcParamKey; +pub use self::zonemd::{ZonemdAlg, ZonemdScheme}; #[macro_use] mod macros; @@ -49,3 +50,4 @@ pub mod rcode; pub mod rtype; pub mod secalg; pub mod svcb; +pub mod zonemd; diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs new file mode 100644 index 000000000..249448477 --- /dev/null +++ b/src/base/iana/zonemd.rs @@ -0,0 +1,50 @@ +//! ZONEMD IANA parameters. + +//------------ ZonemdScheme -------------------------------------------------- + +int_enum! { + /// ZONEMD schemes. + /// + /// This type selects the method by which data is collated and presented + /// as input to the hashing function for use with [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-schemes + => + ZonemdScheme, u8; + + /// Specifies that the SIMPLE scheme is used. + (SIMPLE => 1, "SIMPLE") +} + +int_enum_str_decimal!(ZonemdScheme, u8); +int_enum_zonefile_fmt_decimal!(ZonemdScheme, "scheme"); + +//------------ ZonemdAlg ----------------------------------------------------- + +int_enum! { + /// ZONEMD algorithms. + /// + /// This type selects the algorithm used to hash domain names for use with + /// the [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-hash-algorithms + => + ZonemdAlg, u8; + + /// Specifies that the SHA-384 algorithm is used. + (SHA384 => 1, "SHA-384") + + /// Specifies that the SHA-512 algorithm is used. + (SHA512 => 2, "SHA-512") +} + +int_enum_str_decimal!(ZonemdAlg, u8); +int_enum_zonefile_fmt_decimal!(ZonemdAlg, "hash algorithm"); diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index ed53f94df..f30dcb0b9 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -9,7 +9,7 @@ #![allow(clippy::needless_maybe_sized)] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Rtype, ZonemdAlg, ZonemdScheme}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; @@ -17,7 +17,6 @@ use crate::base::wire::{Composer, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::utils::base16; use core::cmp::Ordering; -use core::str::FromStr; use core::{fmt, hash}; use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::Parser; @@ -30,8 +29,8 @@ const DIGEST_MIN_LEN: usize = 12; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Zonemd { serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, #[cfg_attr( feature = "serde", serde( @@ -55,8 +54,8 @@ impl Zonemd { /// Create a Zonemd record data from provided parameters. pub fn new( serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, digest: Octs, ) -> Self { Self { @@ -73,12 +72,12 @@ impl Zonemd { } /// Get the scheme field. - pub fn scheme(&self) -> Scheme { + pub fn scheme(&self) -> ZonemdScheme { self.scheme } /// Get the hash algorithm field. - pub fn algorithm(&self) -> Algorithm { + pub fn algorithm(&self) -> ZonemdAlg { self.algo } @@ -234,26 +233,7 @@ impl> ZonefileFmt for Zonemd { p.block(|p| { p.write_token(self.serial)?; p.write_show(self.scheme)?; - p.write_comment(format_args!( - "scheme ({})", - match self.scheme { - Scheme::Reserved => "reserved", - Scheme::Simple => "simple", - Scheme::Unassigned(_) => "unassigned", - Scheme::Private(_) => "private", - } - ))?; p.write_show(self.algo)?; - p.write_comment(format_args!( - "algorithm ({})", - match self.algo { - Algorithm::Reserved => "reserved", - Algorithm::Sha384 => "SHA384", - Algorithm::Sha512 => "SHA512", - Algorithm::Unassigned(_) => "unassigned", - Algorithm::Private(_) => "private", - } - ))?; p.write_token(base16::encode_display(&self.digest)) }) } @@ -321,151 +301,6 @@ impl> Ord for Zonemd { } } -/// The data collation scheme. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies the -/// methods by which data is collated and presented as input to the -/// hashing function. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Scheme { - Reserved, - Simple, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(s: Scheme) -> u8 { - match s { - Scheme::Reserved => 0, - Scheme::Simple => 1, - Scheme::Unassigned(n) => n, - Scheme::Private(n) => n, - } - } -} - -impl From for Scheme { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Simple, - 2..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl FromStr for Scheme { - type Err = SchemeFromStrError; - - // Only implement the actionable variants - fn from_str(s: &str) -> Result { - match s { - "1" | "SIMPLE" => Ok(Self::Simple), - _ => Err(SchemeFromStrError(())), - } - } -} - -impl ZonefileFmt for Scheme { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - -//------------ SchemeFromStrError --------------------------------------------- - -/// An error occured while reading the scheme from a string. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct SchemeFromStrError(()); - -//--- Display and Error - -impl fmt::Display for SchemeFromStrError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "unknown zonemd scheme mnemonic") - } -} - -#[cfg(feature = "std")] -impl std::error::Error for SchemeFromStrError {} - -/// The Hash Algorithm used to construct the digest. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies -/// the cryptographic hash algorithm. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Algorithm { - Reserved, - Sha384, - Sha512, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(algo: Algorithm) -> u8 { - match algo { - Algorithm::Reserved => 0, - Algorithm::Sha384 => 1, - Algorithm::Sha512 => 2, - Algorithm::Unassigned(n) => n, - Algorithm::Private(n) => n, - } - } -} - -impl From for Algorithm { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Sha384, - 2 => Self::Sha512, - 3..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl FromStr for Algorithm { - type Err = AlgorithmFromStrError; - - // Only implement the actionable variants - fn from_str(s: &str) -> Result { - match s { - "1" | "SHA384" => Ok(Self::Sha384), - "2" | "SHA512" => Ok(Self::Sha512), - _ => Err(AlgorithmFromStrError(())), - } - } -} - -impl ZonefileFmt for Algorithm { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - -//------------ AlgorithmFromStrError --------------------------------------------- - -/// An error occured while reading the algorithm from a string. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct AlgorithmFromStrError(()); - -//--- Display and Error - -impl fmt::Display for AlgorithmFromStrError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "unknown zonemd hash algorithm mnemonic") - } -} - -#[cfg(feature = "std")] -impl std::error::Error for AlgorithmFromStrError {} - #[cfg(test)] #[cfg(all(feature = "std", feature = "bytes"))] mod test { @@ -503,6 +338,7 @@ mod test { #[cfg(feature = "zonefile")] #[test] fn zonemd_parse_zonefile() { + use crate::base::iana::ZonemdAlg; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; @@ -535,8 +371,8 @@ ns2 3600 IN AAAA 2001:db8::63 match record.into_data() { ZoneRecordData::Zonemd(rd) => { assert_eq!(2018031900, rd.serial().into_int()); - assert_eq!(Scheme::Simple, rd.scheme()); - assert_eq!(Algorithm::Sha384, rd.algorithm()); + assert_eq!(ZonemdScheme::SIMPLE, rd.scheme()); + assert_eq!(ZonemdAlg::SHA384, rd.algorithm()); } _ => panic!(), } From ed76ca99fdc5c70deadee94d5cc47368c0fe3da0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:33:54 +0100 Subject: [PATCH 221/569] Revert "Merge branch 'main' into multiple-key-signing" This reverts commit 75145f5a3e2d2f0ec4056c0d294308fe531ccd71, reversing changes made to a3bac8d8667f9df79bed0e73d2bf2ec7fee8bedd. --- Changelog.md | 9 - examples/client-transports.rs | 62 +- src/net/client/load_balancer.rs | 1128 ------------------------------- src/net/client/mod.rs | 5 - src/net/client/multi_stream.rs | 92 +-- src/net/client/redundant.rs | 58 +- 6 files changed, 39 insertions(+), 1315 deletions(-) delete mode 100644 src/net/client/load_balancer.rs diff --git a/Changelog.md b/Changelog.md index 44c41782f..2ffd13821 100644 --- a/Changelog.md +++ b/Changelog.md @@ -36,21 +36,12 @@ Unstable features * A sample query router, called `QnameRouter`, that routes requests based on the QNAME field in the request ([#353]). -* `unstable-client-transport` - * introduce timeout option in multi_stream ([#424]). - * improve probing in redundant ([#424]). - * restructure configuration for multi_stream and redundant ([#424]). - * introduce a load balancer client transport. This transport tries to - distribute requests equally over upstream transports ([#425]). - Other changes [#353]: https://github.com/NLnetLabs/domain/pull/353 [#396]: https://github.com/NLnetLabs/domain/pull/396 [#417]: https://github.com/NLnetLabs/domain/pull/417 [#421]: https://github.com/NLnetLabs/domain/pull/421 -[#424]: https://github.com/NLnetLabs/domain/pull/424 -[#425]: https://github.com/NLnetLabs/domain/pull/425 [#427]: https://github.com/NLnetLabs/domain/pull/427 [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 diff --git a/examples/client-transports.rs b/examples/client-transports.rs index 5b6832a0d..40f0e9a9a 100644 --- a/examples/client-transports.rs +++ b/examples/client-transports.rs @@ -1,13 +1,4 @@ -//! Using the `domain::net::client` module for sending a query. -use domain::base::{MessageBuilder, Name, Rtype}; -use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; -use domain::net::client::request::{ - RequestMessage, RequestMessageMulti, SendRequest, -}; -use domain::net::client::{ - cache, dgram, dgram_stream, load_balancer, multi_stream, redundant, - stream, -}; +/// Using the `domain::net::client` module for sending a query. use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; #[cfg(feature = "unstable-validator")] @@ -15,6 +6,20 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use domain::base::MessageBuilder; +use domain::base::Name; +use domain::base::Rtype; +use domain::net::client::cache; +use domain::net::client::dgram; +use domain::net::client::dgram_stream; +use domain::net::client::multi_stream; +use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; +use domain::net::client::redundant; +use domain::net::client::request::{ + RequestMessage, RequestMessageMulti, SendRequest, +}; +use domain::net::client::stream; + #[cfg(feature = "tsig")] use domain::net::client::request::SendRequestMulti; #[cfg(feature = "tsig")] @@ -201,9 +206,9 @@ async fn main() { }); // Add the previously created transports. - redun.add(Box::new(udptcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tls_conn.clone())).await.unwrap(); + redun.add(Box::new(udptcp_conn)).await.unwrap(); + redun.add(Box::new(tcp_conn)).await.unwrap(); + redun.add(Box::new(tls_conn)).await.unwrap(); // Start a few queries. for i in 1..10 { @@ -216,37 +221,6 @@ async fn main() { drop(redun); - // Create a transport connection for load balanced connections. - let (lb, transp) = load_balancer::Connection::new(); - - // Start the run function on a separate task. - let run_fut = transp.run(); - tokio::spawn(async move { - run_fut.await; - println!("load_balancer run terminated"); - }); - - // Add the previously created transports. - let mut conn_conf = load_balancer::ConnConfig::new(); - conn_conf.set_max_burst(Some(10)); - conn_conf.set_burst_interval(Duration::from_secs(10)); - lb.add("UDP+TCP", &conn_conf, Box::new(udptcp_conn)) - .await - .unwrap(); - lb.add("TCP", &conn_conf, Box::new(tcp_conn)).await.unwrap(); - lb.add("TLS", &conn_conf, Box::new(tls_conn)).await.unwrap(); - - // Start a few queries. - for i in 1..10 { - let mut request = lb.send_request(req.clone()); - let reply = request.get_response().await; - if i == 2 { - println!("load_balancer connection reply: {reply:?}"); - } - } - - drop(lb); - // Create a new datagram transport connection. Pass the destination address // and port as parameter. This transport does not retry over TCP if the // reply is truncated. This transport does not have a separate run diff --git a/src/net/client/load_balancer.rs b/src/net/client/load_balancer.rs deleted file mode 100644 index 00671bfa6..000000000 --- a/src/net/client/load_balancer.rs +++ /dev/null @@ -1,1128 +0,0 @@ -//! A transport that tries to distribute requests over multiple upstreams. -//! -//! It is assumed that the upstreams have similar performance. use the -//! [super::redundant] transport to forward requests to the best upstream out of -//! upstreams that may have quite different performance. -//! -//! Basic mode of operation -//! -//! Associated with every upstream configured is optionally a burst length -//! and burst interval. Burst length deviced by burst interval gives a -//! queries per second (QPS) value. This be use to limit the rate and -//! especially the bursts that reach upstream servers. Once the burst -//! length has been reach, the upstream receives no new requests until -//! the burst interval has completed. -//! -//! For each upstream the object maintains an estimated response time. -//! with the configuration value slow_rt_factor, the group of upstream -//! that have not exceeded their burst length are divided into a 'fast' -//! and a 'slow' group. The slow group are those upstream that have an -//! estimated response time that is higher than slow_rt_factor times the -//! lowest estimated response time. Slow upstream are considered only when -//! all fast upstream failed to provide a suitable response. -//! -//! Within the group of fast upstreams, the ones with the lower queue -//! length are preferred. This tries to give each of the fast upstreams -//! an equal number of outstanding requests. -//! -//! Within a group of fast upstreams with the same queue length, the -//! one with the lowest estimated response time is preferred. -//! -//! Probing -//! -//! Upstream with high estimated response times may be get any traffic and -//! therefore the estimated response time may remain high. Probing is -//! intended to solve that problem. Using a random number generator, -//! occasionally an upstream is selected for probing. If the selected -//! upstream currently has a non-zero queue then probing is not needed and -//! no probe will happen. -//! Otherwise, the upstream to be probed is selected first with an -//! estimated response time equal to the lowest one. If the probed upstream -//! does not provide a response within that time, the otherwise best upstream -//! also gets the request. If the probes upstream provides a suitable response -//! before the next upstream then its estimated will be updated. - -use crate::base::iana::OptRcode; -use crate::base::iana::Rcode; -use crate::base::opt::AllOptData; -use crate::base::Message; -use crate::base::MessageBuilder; -use crate::base::StaticCompressor; -use crate::dep::octseq::OctetsInto; -use crate::net::client::request::ComposeRequest; -use crate::net::client::request::{Error, GetResponse, SendRequest}; -use crate::utils::config::DefMinMax; -use bytes::Bytes; -use futures_util::stream::FuturesUnordered; -use futures_util::StreamExt; -use octseq::Octets; -use rand::random; -use std::boxed::Box; -use std::cmp::Ordering; -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::pin::Pin; -use std::string::String; -use std::string::ToString; -use std::sync::Arc; -use std::vec::Vec; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::{sleep_until, Duration, Instant}; - -/* -Basic algorithm: -- try to distribute requests over all upstreams subject to some limitations. -- limit bursts - - record the start of a burst interval when a request goes out over an - upstream - - record the number of requests since the start of the burst interval - - in the burst is larger than the maximum configured by the user then the - upstream is no longer available. - - start a new burst interval when enough time has passed. -- prefer fast upstreams over slow upstreams - - maintain a response time estimate for each upstream - - upstreams with an estimate response time larger than slow_rt_factor - times the lowest estimated response time are consider slow. - - 'fast' upstreams are preferred over slow upstream. However slow upstreams - are considered if during a single request all fast upstreams fail. -- prefer fast upstream with a low queue length - - maintain a counter with the number of current outstanding requests on an - upstream. - - prefer the upstream with the lowest count. - - preset the upstream with the lowest estimated response time in case - two or more upstreams have the same count. - -Execution: -- set a timer to the expect response time. -- if the timer expires before reply arrives, send the query to the next lowest - and set a timer -- when a reply arrives update the expected response time for the relevant - upstream and for the ones that failed. - -Probing: -- upstream that currently have outstanding requests do not need to be - probed. -- for idle upstream, based on a random number generator: - - pick a different upstream rather then the best - - but set the timer to the expected response time of the best. - - maybe we need a configuration parameter for the amound of head start - given to the probed upstream. -*/ - -/// Capacity of the channel that transports [ChanReq]. -const DEF_CHAN_CAP: usize = 8; - -/// Time in milliseconds for the initial response time estimate. -const DEFAULT_RT_MS: u64 = 300; - -/// The initial response time estimate for unused connections. -const DEFAULT_RT: Duration = Duration::from_millis(DEFAULT_RT_MS); - -/// Maintain a moving average for the measured response time and the -/// square of that. The window is SMOOTH_N. -const SMOOTH_N: f64 = 8.; - -/// Chance to probe a worse connection. -const PROBE_P: f64 = 0.05; - -//------------ Configuration Constants ---------------------------------------- - -/// Cut off for slow upstreams. -const DEF_SLOW_RT_FACTOR: f64 = 5.0; - -/// Minimum value for the cut off factor. -const MIN_SLOW_RT_FACTOR: f64 = 1.0; - -/// Interval for limiting upstream query bursts. -const BURST_INTERVAL: DefMinMax = DefMinMax::new( - Duration::from_secs(1), - Duration::from_millis(1), - Duration::from_secs(3600), -); - -//------------ Config --------------------------------------------------------- - -/// User configuration variables. -#[derive(Clone, Copy, Debug)] -pub struct Config { - /// Defer transport errors. - defer_transport_error: bool, - - /// Defer replies that report Refused. - defer_refused: bool, - - /// Defer replies that report ServFail. - defer_servfail: bool, - - /// Cut-off for slow upstreams as a factor of the fastest upstream. - slow_rt_factor: f64, -} - -impl Config { - /// Return the value of the defer_transport_error configuration variable. - pub fn defer_transport_error(&self) -> bool { - self.defer_transport_error - } - - /// Set the value of the defer_transport_error configuration variable. - pub fn set_defer_transport_error(&mut self, value: bool) { - self.defer_transport_error = value - } - - /// Return the value of the defer_refused configuration variable. - pub fn defer_refused(&self) -> bool { - self.defer_refused - } - - /// Set the value of the defer_refused configuration variable. - pub fn set_defer_refused(&mut self, value: bool) { - self.defer_refused = value - } - - /// Return the value of the defer_servfail configuration variable. - pub fn defer_servfail(&self) -> bool { - self.defer_servfail - } - - /// Set the value of the defer_servfail configuration variable. - pub fn set_defer_servfail(&mut self, value: bool) { - self.defer_servfail = value - } - - /// Set the value of the slow_rt_factor configuration variable. - pub fn slow_rt_factor(&self) -> f64 { - self.slow_rt_factor - } - - /// Set the value of the slow_rt_factor configuration variable. - pub fn set_slow_rt_factor(&mut self, mut value: f64) { - if value < MIN_SLOW_RT_FACTOR { - value = MIN_SLOW_RT_FACTOR - }; - self.slow_rt_factor = value; - } -} - -impl Default for Config { - fn default() -> Self { - Self { - defer_transport_error: Default::default(), - defer_refused: Default::default(), - defer_servfail: Default::default(), - slow_rt_factor: DEF_SLOW_RT_FACTOR, - } - } -} - -//------------ ConnConfig ----------------------------------------------------- - -/// Configuration variables for each upstream. -#[derive(Clone, Copy, Debug, Default)] -pub struct ConnConfig { - /// Maximum burst of upstream queries. - max_burst: Option, - - /// Interval over which the burst is counted. - burst_interval: Duration, -} - -impl ConnConfig { - /// Create a new ConnConfig object. - pub fn new() -> Self { - Self { - max_burst: None, - burst_interval: BURST_INTERVAL.default(), - } - } - - /// Return the current configuration value for the maximum burst. - /// None means that there is no limit. - pub fn max_burst(&mut self) -> Option { - self.max_burst - } - - /// Set the configuration value for the maximum burst. - /// The value None means no limit. - pub fn set_max_burst(&mut self, max_burst: Option) { - self.max_burst = max_burst; - } - - /// Return the current burst interval. - pub fn burst_interval(&mut self) -> Duration { - self.burst_interval - } - - /// Set a new burst interval. - /// - /// The interval is silently limited to at least 1 millesecond and - /// at most 1 hour. - pub fn set_burst_interval(&mut self, burst_interval: Duration) { - self.burst_interval = BURST_INTERVAL.limit(burst_interval); - } -} - -//------------ Connection ----------------------------------------------------- - -/// This type represents a transport connection. -#[derive(Debug)] -pub struct Connection -where - Req: Send + Sync, -{ - /// User configuation. - config: Config, - - /// To send a request to the runner. - sender: mpsc::Sender>, -} - -impl Connection { - /// Create a new connection. - pub fn new() -> (Self, Transport) { - Self::with_config(Default::default()) - } - - /// Create a new connection with a given config. - pub fn with_config(config: Config) -> (Self, Transport) { - let (sender, receiver) = mpsc::channel(DEF_CHAN_CAP); - (Self { config, sender }, Transport::new(receiver)) - } - - /// Add a transport connection. - pub async fn add( - &self, - label: &str, - config: &ConnConfig, - conn: Box + Send + Sync>, - ) -> Result<(), Error> { - let (tx, rx) = oneshot::channel(); - self.sender - .send(ChanReq::Add(AddReq { - label: label.to_string(), - max_burst: config.max_burst, - burst_interval: config.burst_interval, - conn, - tx, - })) - .await - .expect("send should not fail"); - rx.await.expect("receive should not fail") - } - - /// Implementation of the query method. - async fn request_impl( - self, - request_msg: Req, - ) -> Result, Error> - where - Req: ComposeRequest, - { - let (tx, rx) = oneshot::channel(); - self.sender - .send(ChanReq::GetRT(RTReq { tx })) - .await - .expect("send should not fail"); - let conn_rt = rx.await.expect("receive should not fail")?; - if conn_rt.is_empty() { - return serve_fail(&request_msg.to_message().unwrap()); - } - Query::new(self.config, request_msg, conn_rt, self.sender.clone()) - .get_response() - .await - } -} - -impl Clone for Connection -where - Req: Send + Sync, -{ - fn clone(&self) -> Self { - Self { - config: self.config, - sender: self.sender.clone(), - } - } -} - -impl - SendRequest for Connection -{ - fn send_request( - &self, - request_msg: Req, - ) -> Box { - Box::new(Request { - fut: Box::pin(self.clone().request_impl(request_msg)), - }) - } -} - -//------------ Request ------------------------------------------------------- - -/// An active request. -struct Request { - /// The underlying future. - fut: Pin< - Box, Error>> + Send + Sync>, - >, -} - -impl Request { - /// Async function that waits for the future stored in Query to complete. - async fn get_response_impl(&mut self) -> Result, Error> { - (&mut self.fut).await - } -} - -impl GetResponse for Request { - fn get_response( - &mut self, - ) -> Pin< - Box< - dyn Future, Error>> - + Send - + Sync - + '_, - >, - > { - Box::pin(self.get_response_impl()) - } -} - -impl Debug for Request { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_struct("Request") - .field("fut", &format_args!("_")) - .finish() - } -} - -//------------ Query -------------------------------------------------------- - -/// This type represents an active query request. -#[derive(Debug)] -struct Query -where - Req: Send + Sync, -{ - /// User configuration. - config: Config, - - /// The state of the query - state: QueryState, - - /// The request message - request_msg: Req, - - /// List of connections identifiers and estimated response times. - conn_rt: Vec, - - /// Channel to send requests to the run function. - sender: mpsc::Sender>, - - /// List of futures for outstanding requests. - fut_list: FuturesUnordered< - Pin + Send + Sync>>, - >, - - /// Transport error that should be reported if nothing better shows - /// up. - deferred_transport_error: Option, - - /// Reply that should be returned to the user if nothing better shows - /// up. - deferred_reply: Option>, - - /// The result from one of the connectons. - result: Option, Error>>, - - /// Index of the connection that returned a result. - res_index: usize, -} - -/// The various states a query can be in. -#[derive(Debug)] -enum QueryState { - /// The initial state - Init, - - /// Start a request on a specific connection. - Probe(usize), - - /// Report the response time for a specific index in the list. - Report(usize), - - /// Wait for one of the requests to finish. - Wait, -} - -/// The commands that can be sent to the run function. -enum ChanReq -where - Req: Send + Sync, -{ - /// Add a connection - Add(AddReq), - - /// Get the list of estimated response times for all connections - GetRT(RTReq), - - /// Start a query - Query(RequestReq), - - /// Report how long it took to get a response - Report(TimeReport), - - /// Report that a connection failed to provide a timely response - Failure(TimeReport), -} - -impl Debug for ChanReq -where - Req: Send + Sync, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - f.debug_struct("ChanReq").finish() - } -} - -/// Request to add a new connection -struct AddReq { - /// Name of new connection - label: String, - - /// Maximum length of a burst. - max_burst: Option, - - /// Interval over which bursts are counted. - burst_interval: Duration, - - /// New connection to add - conn: Box + Send + Sync>, - - /// Channel to send the reply to - tx: oneshot::Sender, -} - -/// Reply to an Add request -type AddReply = Result<(), Error>; - -/// Request to give the estimated response times for all connections -struct RTReq /**/ { - /// Channel to send the reply to - tx: oneshot::Sender, -} - -/// Reply to a RT request -type RTReply = Result, Error>; - -/// Request to start a request -struct RequestReq -where - Req: Send + Sync, -{ - /// Identifier of connection - id: u64, - - /// Request message - request_msg: Req, - - /// Channel to send the reply to - tx: oneshot::Sender, -} - -impl Debug for RequestReq -where - Req: Send + Sync, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - f.debug_struct("RequestReq") - .field("id", &self.id) - .field("request_msg", &self.request_msg) - .finish() - } -} - -/// Reply to a request request. -type RequestReply = - Result<(Box, Arc<()>), Error>; - -/// Report the amount of time until success or failure. -#[derive(Debug)] -struct TimeReport { - /// Identifier of the transport connection. - id: u64, - - /// Time spend waiting for a reply. - elapsed: Duration, -} - -/// Connection statistics to compute the estimated response time. -struct ConnStats { - /// Name of the connection. - _label: String, - - /// Aproximation of the windowed average of response times. - mean: f64, - - /// Aproximation of the windowed average of the square of response times. - mean_sq: f64, - - /// Maximum upstream query burst. - max_burst: Option, - - /// burst length, - burst_interval: Duration, - - /// Start of the current burst - burst_start: Instant, - - /// Number of queries since the start of the burst. - burst: u64, - - /// Use the number of references to an Arc as queue length. The number - /// of references is one higher than then actual queue length. - queue_length_plus_one: Arc<()>, -} - -impl ConnStats { - /// Update response time statistics. - fn update(&mut self, elapsed: Duration) { - let elapsed = elapsed.as_secs_f64(); - self.mean += (elapsed - self.mean) / SMOOTH_N; - let elapsed_sq = elapsed * elapsed; - self.mean_sq += (elapsed_sq - self.mean_sq) / SMOOTH_N; - } - - /// Get an estimated response time. - fn est_rt(&self) -> f64 { - let mean = self.mean; - let var = self.mean_sq - mean * mean; - let std_dev = f64::sqrt(var.max(0.)); - mean + 3. * std_dev - } -} - -/// Data required to schedule requests and report timing results. -#[derive(Clone, Debug)] -struct ConnRT { - /// Estimated response time. - est_rt: Duration, - - /// Identifier of the connection. - id: u64, - - /// Start of a request using this connection. - start: Option, - - /// Use the number of references to an Arc as queue length. The number - /// of references is one higher than then actual queue length. - queue_length: usize, -} - -/// Result of the futures in fut_list. -type FutListOutput = (usize, Result, Error>); - -impl Query { - /// Create a new query object. - fn new( - config: Config, - request_msg: Req, - mut conn_rt: Vec, - sender: mpsc::Sender>, - ) -> Self { - let conn_rt_len = conn_rt.len(); - let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); - let slow_rt = min_rt.as_secs_f64() * config.slow_rt_factor; - conn_rt.sort_unstable_by(|e1, e2| conn_rt_cmp(e1, e2, slow_rt)); - - // Do we want to probe a less performant upstream? We only need to - // probe upstreams with a queue length of zero. If the queue length - // is non-zero then the upstream recently got work and does not need - // to be probed. - if conn_rt_len > 1 && random::() < PROBE_P { - let index: usize = 1 + random::() % (conn_rt_len - 1); - - if conn_rt[index].queue_length == 0 { - // Give the probe some head start. We may need a separate - // configuration parameter. A multiple of min_rt. Just use - // min_rt for now. - let mut e = conn_rt.remove(index); - e.est_rt = min_rt; - conn_rt.insert(0, e); - } - } - - Self { - config, - request_msg, - conn_rt, - sender, - state: QueryState::Init, - fut_list: FuturesUnordered::new(), - deferred_transport_error: None, - deferred_reply: None, - result: None, - res_index: 0, - } - } - - /// Implementation of get_response. - async fn get_response(&mut self) -> Result, Error> { - loop { - match self.state { - QueryState::Init => { - if self.conn_rt.is_empty() { - return Err(Error::NoTransportAvailable); - } - self.state = QueryState::Probe(0); - continue; - } - QueryState::Probe(ind) => { - self.conn_rt[ind].start = Some(Instant::now()); - let fut = start_request( - ind, - self.conn_rt[ind].id, - self.sender.clone(), - self.request_msg.clone(), - ); - self.fut_list.push(Box::pin(fut)); - let timeout = Instant::now() + self.conn_rt[ind].est_rt; - loop { - tokio::select! { - res = self.fut_list.next() => { - let res = res.expect("res should not be empty"); - match res.1 { - Err(ref err) => { - if self.config.defer_transport_error { - if self.deferred_transport_error.is_none() { - self.deferred_transport_error = Some(err.clone()); - } - if res.0 == ind { - // The current upstream finished, - // try the next one, if any. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else - { - QueryState::Wait - }; - // Break out of receive loop - break; - } - // Just continue receiving - continue; - } - // Return error to the user. - } - Ok(ref msg) => { - if skip(msg, &self.config) { - if self.deferred_reply.is_none() { - self.deferred_reply = Some(msg.clone()); - } - if res.0 == ind { - // The current upstream finished, - // try the next one, if any. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else - { - QueryState::Wait - }; - // Break out of receive loop - break; - } - // Just continue receiving - continue; - } - // Now we have a reply that can be - // returned to the user. - } - } - self.result = Some(res.1); - self.res_index = res.0; - - self.state = QueryState::Report(0); - // Break out of receive loop - break; - } - _ = sleep_until(timeout) => { - // Move to the next Probe state if there - // are more upstreams to try, otherwise - // move to the Wait state. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else { - QueryState::Wait - }; - // Break out of receive loop - break; - } - } - } - // Continue with state machine loop - continue; - } - QueryState::Report(ind) => { - if ind >= self.conn_rt.len() - || self.conn_rt[ind].start.is_none() - { - // Nothing more to report. Return result. - let res = self - .result - .take() - .expect("result should not be empty"); - return res; - } - - let start = self.conn_rt[ind] - .start - .expect("start time should not be empty"); - let elapsed = start.elapsed(); - let time_report = TimeReport { - id: self.conn_rt[ind].id, - elapsed, - }; - let report = if ind == self.res_index { - // Succesfull entry - ChanReq::Report(time_report) - } else { - // Failed entry - ChanReq::Failure(time_report) - }; - - // Send could fail but we don't care. - let _ = self.sender.send(report).await; - - self.state = QueryState::Report(ind + 1); - continue; - } - QueryState::Wait => { - loop { - if self.fut_list.is_empty() { - // We have nothing left. There should be a reply or - // an error. Prefer a reply over an error. - if self.deferred_reply.is_some() { - let msg = self - .deferred_reply - .take() - .expect("just checked for Some"); - return Ok(msg); - } - if self.deferred_transport_error.is_some() { - let err = self - .deferred_transport_error - .take() - .expect("just checked for Some"); - return Err(err); - } - panic!("either deferred_reply or deferred_error should be present"); - } - let res = self.fut_list.next().await; - let res = res.expect("res should not be empty"); - match res.1 { - Err(ref err) => { - if self.config.defer_transport_error { - if self.deferred_transport_error.is_none() - { - self.deferred_transport_error = - Some(err.clone()); - } - // Just continue with the next future, or - // finish if fut_list is empty. - continue; - } - // Return error to the user. - } - Ok(ref msg) => { - if skip(msg, &self.config) { - if self.deferred_reply.is_none() { - self.deferred_reply = - Some(msg.clone()); - } - // Just continue with the next future, or - // finish if fut_list is empty. - continue; - } - // Return reply to user. - } - } - self.result = Some(res.1); - self.res_index = res.0; - self.state = QueryState::Report(0); - // Break out of loop to continue with the state machine - break; - } - continue; - } - } - } - } -} - -//------------ Transport ----------------------------------------------------- - -/// Type that actually implements the connection. -#[derive(Debug)] -pub struct Transport -where - Req: Send + Sync, -{ - /// Receive side of the channel used by the runner. - receiver: mpsc::Receiver>, -} - -impl<'a, Req: Clone + Send + Sync + 'static> Transport { - /// Implementation of the new method. - fn new(receiver: mpsc::Receiver>) -> Self { - Self { receiver } - } - - /// Run method. - pub async fn run(mut self) { - let mut next_id: u64 = 10; - let mut conn_stats: Vec = Vec::new(); - let mut conn_rt: Vec = Vec::new(); - let mut conns: Vec + Send + Sync>> = - Vec::new(); - - loop { - let req = match self.receiver.recv().await { - Some(req) => req, - None => break, // All references to connection objects are - // dropped. Shutdown. - }; - match req { - ChanReq::Add(add_req) => { - let id = next_id; - next_id += 1; - conn_stats.push(ConnStats { - _label: add_req.label, - mean: (DEFAULT_RT_MS as f64) / 1000., - mean_sq: 0., - max_burst: add_req.max_burst, - burst_interval: add_req.burst_interval, - burst_start: Instant::now(), - burst: 0, - queue_length_plus_one: Arc::new(()), - }); - conn_rt.push(ConnRT { - id, - est_rt: DEFAULT_RT, - start: None, - queue_length: 42, // To spot errors. - }); - conns.push(add_req.conn); - - // Don't care if send fails - let _ = add_req.tx.send(Ok(())); - } - ChanReq::GetRT(rt_req) => { - let mut tmp_conn_rt = conn_rt.clone(); - - // Remove entries that exceed the QPS limit. Loop - // backward to efficiently remove them. - for i in (0..tmp_conn_rt.len()).rev() { - // Fill-in current queue length. - tmp_conn_rt[i].queue_length = Arc::strong_count( - &conn_stats[i].queue_length_plus_one, - ) - 1; - if let Some(max_burst) = conn_stats[i].max_burst { - if conn_stats[i].burst_start.elapsed() - > conn_stats[i].burst_interval - { - conn_stats[i].burst_start = Instant::now(); - conn_stats[i].burst = 0; - } - if conn_stats[i].burst > max_burst { - tmp_conn_rt.swap_remove(i); - } - } else { - // No limit. - } - } - // Don't care if send fails - let _ = rt_req.tx.send(Ok(tmp_conn_rt)); - } - ChanReq::Query(request_req) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == request_req.id); - match opt_ind { - Some(ind) => { - // Leave resetting qps_num to GetRT. - conn_stats[ind].burst += 1; - let query = conns[ind] - .send_request(request_req.request_msg); - // Don't care if send fails - let _ = request_req.tx.send(Ok(( - query, - conn_stats[ind].queue_length_plus_one.clone(), - ))); - } - None => { - // Don't care if send fails - let _ = request_req - .tx - .send(Err(Error::RedundantTransportNotFound)); - } - } - } - ChanReq::Report(time_report) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == time_report.id); - if let Some(ind) = opt_ind { - conn_stats[ind].update(time_report.elapsed); - - let est_rt = conn_stats[ind].est_rt(); - conn_rt[ind].est_rt = Duration::from_secs_f64(est_rt); - } - } - ChanReq::Failure(time_report) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == time_report.id); - if let Some(ind) = opt_ind { - let elapsed = time_report.elapsed.as_secs_f64(); - if elapsed < conn_stats[ind].mean { - // Do not update the mean if a - // failure took less time than the - // current mean. - continue; - } - conn_stats[ind].update(time_report.elapsed); - let est_rt = conn_stats[ind].est_rt(); - conn_rt[ind].est_rt = Duration::from_secs_f64(est_rt); - } - } - } - } - } -} - -//------------ Utility -------------------------------------------------------- - -/// Async function to send a request and wait for the reply. -/// -/// This gives a single future that we can put in a list. -async fn start_request( - index: usize, - id: u64, - sender: mpsc::Sender>, - request_msg: Req, -) -> (usize, Result, Error>) -where - Req: Send + Sync, -{ - let (tx, rx) = oneshot::channel(); - sender - .send(ChanReq::Query(RequestReq { - id, - request_msg, - tx, - })) - .await - .expect("receiver still exists"); - let (mut request, qlp1) = - match rx.await.expect("receive is expected to work") { - Err(err) => return (index, Err(err)), - Ok((request, qlp1)) => (request, qlp1), - }; - let reply = request.get_response().await; - - drop(qlp1); - (index, reply) -} - -/// Compare ConnRT elements based on estimated response time. -fn conn_rt_cmp(e1: &ConnRT, e2: &ConnRT, slow_rt: f64) -> Ordering { - let e1_slow = e1.est_rt.as_secs_f64() > slow_rt; - let e2_slow = e2.est_rt.as_secs_f64() > slow_rt; - - match (e1_slow, e2_slow) { - (true, true) => { - // Normal case. First check queue lengths. Then check est_rt. - e1.queue_length - .cmp(&e2.queue_length) - .then(e1.est_rt.cmp(&e2.est_rt)) - } - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (false, false) => e1.est_rt.cmp(&e2.est_rt), - } -} - -/// Return if this reply should be skipped or not. -fn skip(msg: &Message, config: &Config) -> bool { - // Check if we actually need to check. - if !config.defer_refused && !config.defer_servfail { - return false; - } - - let opt_rcode = msg.opt_rcode(); - // OptRcode needs PartialEq - if let OptRcode::REFUSED = opt_rcode { - if config.defer_refused { - return true; - } - } - if let OptRcode::SERVFAIL = opt_rcode { - if config.defer_servfail { - return true; - } - } - - false -} - -/// Generate a SERVFAIL reply message. -// This needs to be consolodated with the one in validator and the one in -// MessageBuilder. -fn serve_fail(msg: &Message) -> Result, Error> -where - Octs: AsRef<[u8]> + Octets, -{ - let mut target = - MessageBuilder::from_target(StaticCompressor::new(Vec::new())) - .expect("Vec is expected to have enough space"); - - let source = msg; - - *target.header_mut() = msg.header(); - target.header_mut().set_rcode(Rcode::SERVFAIL); - target.header_mut().set_ad(false); - - let source = source.question(); - let mut target = target.question(); - for rr in source { - target.push(rr?).expect("should not fail"); - } - let mut target = target.additional(); - - if let Some(opt) = msg.opt() { - target - .opt(|ob| { - ob.set_dnssec_ok(opt.dnssec_ok()); - // XXX something is missing ob.set_rcode(opt.rcode()); - ob.set_udp_payload_size(opt.udp_payload_size()); - ob.set_version(opt.version()); - for o in opt.opt().iter() { - let x: AllOptData<_, _> = o.expect("should not fail"); - ob.push(&x).expect("should not fail"); - } - Ok(()) - }) - .expect("should not fail"); - } - - let result = target.as_builder().clone(); - let msg = Message::::from_octets( - result.finish().into_target().octets_into(), - ) - .expect("Message should be able to parse output from MessageBuilder"); - Ok(msg) -} diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs index 8b3a48087..89f68fd35 100644 --- a/src/net/client/mod.rs +++ b/src/net/client/mod.rs @@ -21,10 +21,6 @@ //! transport connections. The [redundant] transport favors the connection //! with the lowest response time. Any of the other transports can be added //! as upstream transports. -//! * [load_balancer] This transport distributes requests over a collecton of -//! transport connections. The [load_balancer] transport favors connections -//! with the shortest outstanding request queue. Any of the other transports -//! can be added as upstream transports. //! * [cache] This is a simple message cache provided as a pass through //! transport. The cache works with any of the other transports. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] @@ -226,7 +222,6 @@ pub mod cache; pub mod dgram; pub mod dgram_stream; -pub mod load_balancer; pub mod multi_stream; pub mod protocol; pub mod redundant; diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index c45db3726..d0c65c753 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -9,7 +9,6 @@ use crate::net::client::request::{ ComposeRequest, Error, GetResponse, RequestMessageMulti, SendRequest, }; use crate::net::client::stream; -use crate::utils::config::DefMinMax; use bytes::Bytes; use futures_util::stream::FuturesUnordered; use futures_util::StreamExt; @@ -24,7 +23,6 @@ use std::vec::Vec; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::{mpsc, oneshot}; -use tokio::time::timeout; use tokio::time::{sleep_until, Instant}; //------------ Constants ----------------------------------------------------- @@ -35,42 +33,16 @@ const DEF_CHAN_CAP: usize = 8; /// Error messafe when the connection is closed. const ERR_CONN_CLOSED: &str = "connection closed"; -//------------ Configuration Constants ---------------------------------------- - -/// Default response timeout. -const RESPONSE_TIMEOUT: DefMinMax = DefMinMax::new( - Duration::from_secs(30), - Duration::from_millis(1), - Duration::from_secs(600), -); - //------------ Config --------------------------------------------------------- /// Configuration for an multi-stream transport. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Config { - /// Response timeout currently in effect. - response_timeout: Duration, - /// Configuration of the underlying stream transport. stream: stream::Config, } impl Config { - /// Returns the response timeout. - /// - /// This is the amount of time to wait for a request to complete. - pub fn response_timeout(&self) -> Duration { - self.response_timeout - } - - /// Sets the response timeout. - /// - /// Excessive values are quietly trimmed. - pub fn set_response_timeout(&mut self, timeout: Duration) { - self.response_timeout = RESPONSE_TIMEOUT.limit(timeout); - } - /// Returns the underlying stream config. pub fn stream(&self) -> &stream::Config { &self.stream @@ -84,19 +56,7 @@ impl Config { impl From for Config { fn from(stream: stream::Config) -> Self { - Self { - stream, - response_timeout: RESPONSE_TIMEOUT.default(), - } - } -} - -impl Default for Config { - fn default() -> Self { - Self { - stream: Default::default(), - response_timeout: RESPONSE_TIMEOUT.default(), - } + Self { stream } } } @@ -107,9 +67,6 @@ impl Default for Config { pub struct Connection { /// The sender half of the connection request channel. sender: mpsc::Sender>, - - /// Maximum amount of time to wait for a response. - response_timeout: Duration, } impl Connection { @@ -123,15 +80,8 @@ impl Connection { remote: Remote, config: Config, ) -> (Self, Transport) { - let response_timeout = config.response_timeout; let (sender, transport) = Transport::new(remote, config); - ( - Self { - sender, - response_timeout, - }, - transport, - ) + (Self { sender }, transport) } } @@ -197,7 +147,6 @@ impl Clone for Connection { fn clone(&self) -> Self { Self { sender: self.sender.clone(), - response_timeout: self.response_timeout, } } } @@ -226,9 +175,6 @@ struct Request { /// It is kept so we can compare a response with it. request_msg: Req, - /// Start time of the request. - start: Instant, - /// Current state of the query. state: QueryState, @@ -286,7 +232,6 @@ impl Request { Self { conn, request_msg, - start: Instant::now(), state: QueryState::RequestConn, conn_id: None, delayed_retry_count: 0, @@ -301,20 +246,9 @@ impl Request { /// it is resolved, you can call it again to get a new future. pub async fn get_response(&mut self) -> Result, Error> { loop { - let elapsed = self.start.elapsed(); - if elapsed >= self.conn.response_timeout { - return Err(Error::StreamReadTimeout); - } - let remaining = self.conn.response_timeout - elapsed; - match self.state { QueryState::RequestConn => { - let to = - timeout(remaining, self.conn.new_conn(self.conn_id)) - .await - .map_err(|_| Error::StreamReadTimeout)?; - - let rx = match to { + let rx = match self.conn.new_conn(self.conn_id).await { Ok(rx) => rx, Err(err) => { self.state = QueryState::Done; @@ -324,10 +258,7 @@ impl Request { self.state = QueryState::ReceiveConn(rx); } QueryState::ReceiveConn(ref mut receiver) => { - let to = timeout(remaining, receiver) - .await - .map_err(|_| Error::StreamReadTimeout)?; - let res = match to { + let res = match receiver.await { Ok(res) => res, Err(_) => { // Assume receive error @@ -363,10 +294,8 @@ impl Request { continue; } QueryState::GetResult(ref mut query) => { - let to = timeout(remaining, query.get_response()) - .await - .map_err(|_| Error::StreamReadTimeout)?; - match to { + let res = query.get_response().await; + match res { Ok(reply) => { return Ok(reply); } @@ -403,12 +332,7 @@ impl Request { } } QueryState::Delay(instant, duration) => { - if timeout(remaining, sleep_until(instant + duration)) - .await - .is_err() - { - return Err(Error::StreamReadTimeout); - }; + sleep_until(instant + duration).await; self.state = QueryState::RequestConn; } QueryState::Done => { diff --git a/src/net/client/redundant.rs b/src/net/client/redundant.rs index 4e1f1d51d..fc5677512 100644 --- a/src/net/client/redundant.rs +++ b/src/net/client/redundant.rs @@ -54,51 +54,24 @@ const SMOOTH_N: f64 = 8.; /// Chance to probe a worse connection. const PROBE_P: f64 = 0.05; +/// Avoid sending two requests at the same time. +/// +/// When a worse connection is probed, give it a slight head start. +const PROBE_RT: Duration = Duration::from_millis(1); + //------------ Config --------------------------------------------------------- /// User configuration variables. #[derive(Clone, Copy, Debug, Default)] pub struct Config { /// Defer transport errors. - defer_transport_error: bool, + pub defer_transport_error: bool, /// Defer replies that report Refused. - defer_refused: bool, + pub defer_refused: bool, /// Defer replies that report ServFail. - defer_servfail: bool, -} - -impl Config { - /// Return the value of the defer_transport_error configuration variable. - pub fn defer_transport_error(&self) -> bool { - self.defer_transport_error - } - - /// Set the value of the defer_transport_error configuration variable. - pub fn set_defer_transport_error(&mut self, value: bool) { - self.defer_transport_error = value - } - - /// Return the value of the defer_refused configuration variable. - pub fn defer_refused(&self) -> bool { - self.defer_refused - } - - /// Set the value of the defer_refused configuration variable. - pub fn set_defer_refused(&mut self, value: bool) { - self.defer_refused = value - } - - /// Return the value of the defer_servfail configuration variable. - pub fn defer_servfail(&self) -> bool { - self.defer_servfail - } - - /// Set the value of the defer_servfail configuration variable. - pub fn set_defer_servfail(&mut self, value: bool) { - self.defer_servfail = value - } + pub defer_servfail: bool, } //------------ Connection ----------------------------------------------------- @@ -186,7 +159,7 @@ impl SendRequest //------------ Request ------------------------------------------------------- /// An active request. -struct Request { +pub struct Request { /// The underlying future. fut: Pin< Box, Error>> + Send + Sync>, @@ -227,7 +200,7 @@ impl Debug for Request { /// This type represents an active query request. #[derive(Debug)] -struct Query +pub struct Query where Req: Send + Sync, { @@ -412,15 +385,10 @@ impl Query { // Do we want to probe a less performant upstream? if conn_rt_len > 1 && random::() < PROBE_P { let index: usize = 1 + random::() % (conn_rt_len - 1); + conn_rt[index].est_rt = PROBE_RT; - // Give the probe some head start. We may need a separate - // configuration parameter. A multiple of min_rt. Just use - // min_rt for now. - let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); - - let mut e = conn_rt.remove(index); - e.est_rt = min_rt; - conn_rt.insert(0, e); + // Sort again + conn_rt.sort_unstable_by(conn_rt_cmp); } Self { From b0d14edde4f7827517fd41d6dcebe6ba8bed2d83 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:34:49 +0100 Subject: [PATCH 222/569] Revert "Merge branch 'multiple-key-signing' into sortedrecords-zonemd-remove-replace" This reverts commit d3b9b55c70b4c229606d9937e09804b80b86d5ea, reversing changes made to 2712529125f924ebcfca9cd15ad4e158c0471c53. --- Changelog.md | 9 - examples/client-transports.rs | 62 +- src/net/client/load_balancer.rs | 1128 ------------------------------- src/net/client/mod.rs | 5 - src/net/client/multi_stream.rs | 92 +-- src/net/client/redundant.rs | 58 +- 6 files changed, 39 insertions(+), 1315 deletions(-) delete mode 100644 src/net/client/load_balancer.rs diff --git a/Changelog.md b/Changelog.md index 44c41782f..2ffd13821 100644 --- a/Changelog.md +++ b/Changelog.md @@ -36,21 +36,12 @@ Unstable features * A sample query router, called `QnameRouter`, that routes requests based on the QNAME field in the request ([#353]). -* `unstable-client-transport` - * introduce timeout option in multi_stream ([#424]). - * improve probing in redundant ([#424]). - * restructure configuration for multi_stream and redundant ([#424]). - * introduce a load balancer client transport. This transport tries to - distribute requests equally over upstream transports ([#425]). - Other changes [#353]: https://github.com/NLnetLabs/domain/pull/353 [#396]: https://github.com/NLnetLabs/domain/pull/396 [#417]: https://github.com/NLnetLabs/domain/pull/417 [#421]: https://github.com/NLnetLabs/domain/pull/421 -[#424]: https://github.com/NLnetLabs/domain/pull/424 -[#425]: https://github.com/NLnetLabs/domain/pull/425 [#427]: https://github.com/NLnetLabs/domain/pull/427 [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 diff --git a/examples/client-transports.rs b/examples/client-transports.rs index 5b6832a0d..40f0e9a9a 100644 --- a/examples/client-transports.rs +++ b/examples/client-transports.rs @@ -1,13 +1,4 @@ -//! Using the `domain::net::client` module for sending a query. -use domain::base::{MessageBuilder, Name, Rtype}; -use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; -use domain::net::client::request::{ - RequestMessage, RequestMessageMulti, SendRequest, -}; -use domain::net::client::{ - cache, dgram, dgram_stream, load_balancer, multi_stream, redundant, - stream, -}; +/// Using the `domain::net::client` module for sending a query. use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; #[cfg(feature = "unstable-validator")] @@ -15,6 +6,20 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use domain::base::MessageBuilder; +use domain::base::Name; +use domain::base::Rtype; +use domain::net::client::cache; +use domain::net::client::dgram; +use domain::net::client::dgram_stream; +use domain::net::client::multi_stream; +use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; +use domain::net::client::redundant; +use domain::net::client::request::{ + RequestMessage, RequestMessageMulti, SendRequest, +}; +use domain::net::client::stream; + #[cfg(feature = "tsig")] use domain::net::client::request::SendRequestMulti; #[cfg(feature = "tsig")] @@ -201,9 +206,9 @@ async fn main() { }); // Add the previously created transports. - redun.add(Box::new(udptcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tls_conn.clone())).await.unwrap(); + redun.add(Box::new(udptcp_conn)).await.unwrap(); + redun.add(Box::new(tcp_conn)).await.unwrap(); + redun.add(Box::new(tls_conn)).await.unwrap(); // Start a few queries. for i in 1..10 { @@ -216,37 +221,6 @@ async fn main() { drop(redun); - // Create a transport connection for load balanced connections. - let (lb, transp) = load_balancer::Connection::new(); - - // Start the run function on a separate task. - let run_fut = transp.run(); - tokio::spawn(async move { - run_fut.await; - println!("load_balancer run terminated"); - }); - - // Add the previously created transports. - let mut conn_conf = load_balancer::ConnConfig::new(); - conn_conf.set_max_burst(Some(10)); - conn_conf.set_burst_interval(Duration::from_secs(10)); - lb.add("UDP+TCP", &conn_conf, Box::new(udptcp_conn)) - .await - .unwrap(); - lb.add("TCP", &conn_conf, Box::new(tcp_conn)).await.unwrap(); - lb.add("TLS", &conn_conf, Box::new(tls_conn)).await.unwrap(); - - // Start a few queries. - for i in 1..10 { - let mut request = lb.send_request(req.clone()); - let reply = request.get_response().await; - if i == 2 { - println!("load_balancer connection reply: {reply:?}"); - } - } - - drop(lb); - // Create a new datagram transport connection. Pass the destination address // and port as parameter. This transport does not retry over TCP if the // reply is truncated. This transport does not have a separate run diff --git a/src/net/client/load_balancer.rs b/src/net/client/load_balancer.rs deleted file mode 100644 index 00671bfa6..000000000 --- a/src/net/client/load_balancer.rs +++ /dev/null @@ -1,1128 +0,0 @@ -//! A transport that tries to distribute requests over multiple upstreams. -//! -//! It is assumed that the upstreams have similar performance. use the -//! [super::redundant] transport to forward requests to the best upstream out of -//! upstreams that may have quite different performance. -//! -//! Basic mode of operation -//! -//! Associated with every upstream configured is optionally a burst length -//! and burst interval. Burst length deviced by burst interval gives a -//! queries per second (QPS) value. This be use to limit the rate and -//! especially the bursts that reach upstream servers. Once the burst -//! length has been reach, the upstream receives no new requests until -//! the burst interval has completed. -//! -//! For each upstream the object maintains an estimated response time. -//! with the configuration value slow_rt_factor, the group of upstream -//! that have not exceeded their burst length are divided into a 'fast' -//! and a 'slow' group. The slow group are those upstream that have an -//! estimated response time that is higher than slow_rt_factor times the -//! lowest estimated response time. Slow upstream are considered only when -//! all fast upstream failed to provide a suitable response. -//! -//! Within the group of fast upstreams, the ones with the lower queue -//! length are preferred. This tries to give each of the fast upstreams -//! an equal number of outstanding requests. -//! -//! Within a group of fast upstreams with the same queue length, the -//! one with the lowest estimated response time is preferred. -//! -//! Probing -//! -//! Upstream with high estimated response times may be get any traffic and -//! therefore the estimated response time may remain high. Probing is -//! intended to solve that problem. Using a random number generator, -//! occasionally an upstream is selected for probing. If the selected -//! upstream currently has a non-zero queue then probing is not needed and -//! no probe will happen. -//! Otherwise, the upstream to be probed is selected first with an -//! estimated response time equal to the lowest one. If the probed upstream -//! does not provide a response within that time, the otherwise best upstream -//! also gets the request. If the probes upstream provides a suitable response -//! before the next upstream then its estimated will be updated. - -use crate::base::iana::OptRcode; -use crate::base::iana::Rcode; -use crate::base::opt::AllOptData; -use crate::base::Message; -use crate::base::MessageBuilder; -use crate::base::StaticCompressor; -use crate::dep::octseq::OctetsInto; -use crate::net::client::request::ComposeRequest; -use crate::net::client::request::{Error, GetResponse, SendRequest}; -use crate::utils::config::DefMinMax; -use bytes::Bytes; -use futures_util::stream::FuturesUnordered; -use futures_util::StreamExt; -use octseq::Octets; -use rand::random; -use std::boxed::Box; -use std::cmp::Ordering; -use std::fmt::{Debug, Formatter}; -use std::future::Future; -use std::pin::Pin; -use std::string::String; -use std::string::ToString; -use std::sync::Arc; -use std::vec::Vec; -use tokio::sync::{mpsc, oneshot}; -use tokio::time::{sleep_until, Duration, Instant}; - -/* -Basic algorithm: -- try to distribute requests over all upstreams subject to some limitations. -- limit bursts - - record the start of a burst interval when a request goes out over an - upstream - - record the number of requests since the start of the burst interval - - in the burst is larger than the maximum configured by the user then the - upstream is no longer available. - - start a new burst interval when enough time has passed. -- prefer fast upstreams over slow upstreams - - maintain a response time estimate for each upstream - - upstreams with an estimate response time larger than slow_rt_factor - times the lowest estimated response time are consider slow. - - 'fast' upstreams are preferred over slow upstream. However slow upstreams - are considered if during a single request all fast upstreams fail. -- prefer fast upstream with a low queue length - - maintain a counter with the number of current outstanding requests on an - upstream. - - prefer the upstream with the lowest count. - - preset the upstream with the lowest estimated response time in case - two or more upstreams have the same count. - -Execution: -- set a timer to the expect response time. -- if the timer expires before reply arrives, send the query to the next lowest - and set a timer -- when a reply arrives update the expected response time for the relevant - upstream and for the ones that failed. - -Probing: -- upstream that currently have outstanding requests do not need to be - probed. -- for idle upstream, based on a random number generator: - - pick a different upstream rather then the best - - but set the timer to the expected response time of the best. - - maybe we need a configuration parameter for the amound of head start - given to the probed upstream. -*/ - -/// Capacity of the channel that transports [ChanReq]. -const DEF_CHAN_CAP: usize = 8; - -/// Time in milliseconds for the initial response time estimate. -const DEFAULT_RT_MS: u64 = 300; - -/// The initial response time estimate for unused connections. -const DEFAULT_RT: Duration = Duration::from_millis(DEFAULT_RT_MS); - -/// Maintain a moving average for the measured response time and the -/// square of that. The window is SMOOTH_N. -const SMOOTH_N: f64 = 8.; - -/// Chance to probe a worse connection. -const PROBE_P: f64 = 0.05; - -//------------ Configuration Constants ---------------------------------------- - -/// Cut off for slow upstreams. -const DEF_SLOW_RT_FACTOR: f64 = 5.0; - -/// Minimum value for the cut off factor. -const MIN_SLOW_RT_FACTOR: f64 = 1.0; - -/// Interval for limiting upstream query bursts. -const BURST_INTERVAL: DefMinMax = DefMinMax::new( - Duration::from_secs(1), - Duration::from_millis(1), - Duration::from_secs(3600), -); - -//------------ Config --------------------------------------------------------- - -/// User configuration variables. -#[derive(Clone, Copy, Debug)] -pub struct Config { - /// Defer transport errors. - defer_transport_error: bool, - - /// Defer replies that report Refused. - defer_refused: bool, - - /// Defer replies that report ServFail. - defer_servfail: bool, - - /// Cut-off for slow upstreams as a factor of the fastest upstream. - slow_rt_factor: f64, -} - -impl Config { - /// Return the value of the defer_transport_error configuration variable. - pub fn defer_transport_error(&self) -> bool { - self.defer_transport_error - } - - /// Set the value of the defer_transport_error configuration variable. - pub fn set_defer_transport_error(&mut self, value: bool) { - self.defer_transport_error = value - } - - /// Return the value of the defer_refused configuration variable. - pub fn defer_refused(&self) -> bool { - self.defer_refused - } - - /// Set the value of the defer_refused configuration variable. - pub fn set_defer_refused(&mut self, value: bool) { - self.defer_refused = value - } - - /// Return the value of the defer_servfail configuration variable. - pub fn defer_servfail(&self) -> bool { - self.defer_servfail - } - - /// Set the value of the defer_servfail configuration variable. - pub fn set_defer_servfail(&mut self, value: bool) { - self.defer_servfail = value - } - - /// Set the value of the slow_rt_factor configuration variable. - pub fn slow_rt_factor(&self) -> f64 { - self.slow_rt_factor - } - - /// Set the value of the slow_rt_factor configuration variable. - pub fn set_slow_rt_factor(&mut self, mut value: f64) { - if value < MIN_SLOW_RT_FACTOR { - value = MIN_SLOW_RT_FACTOR - }; - self.slow_rt_factor = value; - } -} - -impl Default for Config { - fn default() -> Self { - Self { - defer_transport_error: Default::default(), - defer_refused: Default::default(), - defer_servfail: Default::default(), - slow_rt_factor: DEF_SLOW_RT_FACTOR, - } - } -} - -//------------ ConnConfig ----------------------------------------------------- - -/// Configuration variables for each upstream. -#[derive(Clone, Copy, Debug, Default)] -pub struct ConnConfig { - /// Maximum burst of upstream queries. - max_burst: Option, - - /// Interval over which the burst is counted. - burst_interval: Duration, -} - -impl ConnConfig { - /// Create a new ConnConfig object. - pub fn new() -> Self { - Self { - max_burst: None, - burst_interval: BURST_INTERVAL.default(), - } - } - - /// Return the current configuration value for the maximum burst. - /// None means that there is no limit. - pub fn max_burst(&mut self) -> Option { - self.max_burst - } - - /// Set the configuration value for the maximum burst. - /// The value None means no limit. - pub fn set_max_burst(&mut self, max_burst: Option) { - self.max_burst = max_burst; - } - - /// Return the current burst interval. - pub fn burst_interval(&mut self) -> Duration { - self.burst_interval - } - - /// Set a new burst interval. - /// - /// The interval is silently limited to at least 1 millesecond and - /// at most 1 hour. - pub fn set_burst_interval(&mut self, burst_interval: Duration) { - self.burst_interval = BURST_INTERVAL.limit(burst_interval); - } -} - -//------------ Connection ----------------------------------------------------- - -/// This type represents a transport connection. -#[derive(Debug)] -pub struct Connection -where - Req: Send + Sync, -{ - /// User configuation. - config: Config, - - /// To send a request to the runner. - sender: mpsc::Sender>, -} - -impl Connection { - /// Create a new connection. - pub fn new() -> (Self, Transport) { - Self::with_config(Default::default()) - } - - /// Create a new connection with a given config. - pub fn with_config(config: Config) -> (Self, Transport) { - let (sender, receiver) = mpsc::channel(DEF_CHAN_CAP); - (Self { config, sender }, Transport::new(receiver)) - } - - /// Add a transport connection. - pub async fn add( - &self, - label: &str, - config: &ConnConfig, - conn: Box + Send + Sync>, - ) -> Result<(), Error> { - let (tx, rx) = oneshot::channel(); - self.sender - .send(ChanReq::Add(AddReq { - label: label.to_string(), - max_burst: config.max_burst, - burst_interval: config.burst_interval, - conn, - tx, - })) - .await - .expect("send should not fail"); - rx.await.expect("receive should not fail") - } - - /// Implementation of the query method. - async fn request_impl( - self, - request_msg: Req, - ) -> Result, Error> - where - Req: ComposeRequest, - { - let (tx, rx) = oneshot::channel(); - self.sender - .send(ChanReq::GetRT(RTReq { tx })) - .await - .expect("send should not fail"); - let conn_rt = rx.await.expect("receive should not fail")?; - if conn_rt.is_empty() { - return serve_fail(&request_msg.to_message().unwrap()); - } - Query::new(self.config, request_msg, conn_rt, self.sender.clone()) - .get_response() - .await - } -} - -impl Clone for Connection -where - Req: Send + Sync, -{ - fn clone(&self) -> Self { - Self { - config: self.config, - sender: self.sender.clone(), - } - } -} - -impl - SendRequest for Connection -{ - fn send_request( - &self, - request_msg: Req, - ) -> Box { - Box::new(Request { - fut: Box::pin(self.clone().request_impl(request_msg)), - }) - } -} - -//------------ Request ------------------------------------------------------- - -/// An active request. -struct Request { - /// The underlying future. - fut: Pin< - Box, Error>> + Send + Sync>, - >, -} - -impl Request { - /// Async function that waits for the future stored in Query to complete. - async fn get_response_impl(&mut self) -> Result, Error> { - (&mut self.fut).await - } -} - -impl GetResponse for Request { - fn get_response( - &mut self, - ) -> Pin< - Box< - dyn Future, Error>> - + Send - + Sync - + '_, - >, - > { - Box::pin(self.get_response_impl()) - } -} - -impl Debug for Request { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - f.debug_struct("Request") - .field("fut", &format_args!("_")) - .finish() - } -} - -//------------ Query -------------------------------------------------------- - -/// This type represents an active query request. -#[derive(Debug)] -struct Query -where - Req: Send + Sync, -{ - /// User configuration. - config: Config, - - /// The state of the query - state: QueryState, - - /// The request message - request_msg: Req, - - /// List of connections identifiers and estimated response times. - conn_rt: Vec, - - /// Channel to send requests to the run function. - sender: mpsc::Sender>, - - /// List of futures for outstanding requests. - fut_list: FuturesUnordered< - Pin + Send + Sync>>, - >, - - /// Transport error that should be reported if nothing better shows - /// up. - deferred_transport_error: Option, - - /// Reply that should be returned to the user if nothing better shows - /// up. - deferred_reply: Option>, - - /// The result from one of the connectons. - result: Option, Error>>, - - /// Index of the connection that returned a result. - res_index: usize, -} - -/// The various states a query can be in. -#[derive(Debug)] -enum QueryState { - /// The initial state - Init, - - /// Start a request on a specific connection. - Probe(usize), - - /// Report the response time for a specific index in the list. - Report(usize), - - /// Wait for one of the requests to finish. - Wait, -} - -/// The commands that can be sent to the run function. -enum ChanReq -where - Req: Send + Sync, -{ - /// Add a connection - Add(AddReq), - - /// Get the list of estimated response times for all connections - GetRT(RTReq), - - /// Start a query - Query(RequestReq), - - /// Report how long it took to get a response - Report(TimeReport), - - /// Report that a connection failed to provide a timely response - Failure(TimeReport), -} - -impl Debug for ChanReq -where - Req: Send + Sync, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - f.debug_struct("ChanReq").finish() - } -} - -/// Request to add a new connection -struct AddReq { - /// Name of new connection - label: String, - - /// Maximum length of a burst. - max_burst: Option, - - /// Interval over which bursts are counted. - burst_interval: Duration, - - /// New connection to add - conn: Box + Send + Sync>, - - /// Channel to send the reply to - tx: oneshot::Sender, -} - -/// Reply to an Add request -type AddReply = Result<(), Error>; - -/// Request to give the estimated response times for all connections -struct RTReq /**/ { - /// Channel to send the reply to - tx: oneshot::Sender, -} - -/// Reply to a RT request -type RTReply = Result, Error>; - -/// Request to start a request -struct RequestReq -where - Req: Send + Sync, -{ - /// Identifier of connection - id: u64, - - /// Request message - request_msg: Req, - - /// Channel to send the reply to - tx: oneshot::Sender, -} - -impl Debug for RequestReq -where - Req: Send + Sync, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), std::fmt::Error> { - f.debug_struct("RequestReq") - .field("id", &self.id) - .field("request_msg", &self.request_msg) - .finish() - } -} - -/// Reply to a request request. -type RequestReply = - Result<(Box, Arc<()>), Error>; - -/// Report the amount of time until success or failure. -#[derive(Debug)] -struct TimeReport { - /// Identifier of the transport connection. - id: u64, - - /// Time spend waiting for a reply. - elapsed: Duration, -} - -/// Connection statistics to compute the estimated response time. -struct ConnStats { - /// Name of the connection. - _label: String, - - /// Aproximation of the windowed average of response times. - mean: f64, - - /// Aproximation of the windowed average of the square of response times. - mean_sq: f64, - - /// Maximum upstream query burst. - max_burst: Option, - - /// burst length, - burst_interval: Duration, - - /// Start of the current burst - burst_start: Instant, - - /// Number of queries since the start of the burst. - burst: u64, - - /// Use the number of references to an Arc as queue length. The number - /// of references is one higher than then actual queue length. - queue_length_plus_one: Arc<()>, -} - -impl ConnStats { - /// Update response time statistics. - fn update(&mut self, elapsed: Duration) { - let elapsed = elapsed.as_secs_f64(); - self.mean += (elapsed - self.mean) / SMOOTH_N; - let elapsed_sq = elapsed * elapsed; - self.mean_sq += (elapsed_sq - self.mean_sq) / SMOOTH_N; - } - - /// Get an estimated response time. - fn est_rt(&self) -> f64 { - let mean = self.mean; - let var = self.mean_sq - mean * mean; - let std_dev = f64::sqrt(var.max(0.)); - mean + 3. * std_dev - } -} - -/// Data required to schedule requests and report timing results. -#[derive(Clone, Debug)] -struct ConnRT { - /// Estimated response time. - est_rt: Duration, - - /// Identifier of the connection. - id: u64, - - /// Start of a request using this connection. - start: Option, - - /// Use the number of references to an Arc as queue length. The number - /// of references is one higher than then actual queue length. - queue_length: usize, -} - -/// Result of the futures in fut_list. -type FutListOutput = (usize, Result, Error>); - -impl Query { - /// Create a new query object. - fn new( - config: Config, - request_msg: Req, - mut conn_rt: Vec, - sender: mpsc::Sender>, - ) -> Self { - let conn_rt_len = conn_rt.len(); - let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); - let slow_rt = min_rt.as_secs_f64() * config.slow_rt_factor; - conn_rt.sort_unstable_by(|e1, e2| conn_rt_cmp(e1, e2, slow_rt)); - - // Do we want to probe a less performant upstream? We only need to - // probe upstreams with a queue length of zero. If the queue length - // is non-zero then the upstream recently got work and does not need - // to be probed. - if conn_rt_len > 1 && random::() < PROBE_P { - let index: usize = 1 + random::() % (conn_rt_len - 1); - - if conn_rt[index].queue_length == 0 { - // Give the probe some head start. We may need a separate - // configuration parameter. A multiple of min_rt. Just use - // min_rt for now. - let mut e = conn_rt.remove(index); - e.est_rt = min_rt; - conn_rt.insert(0, e); - } - } - - Self { - config, - request_msg, - conn_rt, - sender, - state: QueryState::Init, - fut_list: FuturesUnordered::new(), - deferred_transport_error: None, - deferred_reply: None, - result: None, - res_index: 0, - } - } - - /// Implementation of get_response. - async fn get_response(&mut self) -> Result, Error> { - loop { - match self.state { - QueryState::Init => { - if self.conn_rt.is_empty() { - return Err(Error::NoTransportAvailable); - } - self.state = QueryState::Probe(0); - continue; - } - QueryState::Probe(ind) => { - self.conn_rt[ind].start = Some(Instant::now()); - let fut = start_request( - ind, - self.conn_rt[ind].id, - self.sender.clone(), - self.request_msg.clone(), - ); - self.fut_list.push(Box::pin(fut)); - let timeout = Instant::now() + self.conn_rt[ind].est_rt; - loop { - tokio::select! { - res = self.fut_list.next() => { - let res = res.expect("res should not be empty"); - match res.1 { - Err(ref err) => { - if self.config.defer_transport_error { - if self.deferred_transport_error.is_none() { - self.deferred_transport_error = Some(err.clone()); - } - if res.0 == ind { - // The current upstream finished, - // try the next one, if any. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else - { - QueryState::Wait - }; - // Break out of receive loop - break; - } - // Just continue receiving - continue; - } - // Return error to the user. - } - Ok(ref msg) => { - if skip(msg, &self.config) { - if self.deferred_reply.is_none() { - self.deferred_reply = Some(msg.clone()); - } - if res.0 == ind { - // The current upstream finished, - // try the next one, if any. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else - { - QueryState::Wait - }; - // Break out of receive loop - break; - } - // Just continue receiving - continue; - } - // Now we have a reply that can be - // returned to the user. - } - } - self.result = Some(res.1); - self.res_index = res.0; - - self.state = QueryState::Report(0); - // Break out of receive loop - break; - } - _ = sleep_until(timeout) => { - // Move to the next Probe state if there - // are more upstreams to try, otherwise - // move to the Wait state. - self.state = - if ind+1 < self.conn_rt.len() { - QueryState::Probe(ind+1) - } - else { - QueryState::Wait - }; - // Break out of receive loop - break; - } - } - } - // Continue with state machine loop - continue; - } - QueryState::Report(ind) => { - if ind >= self.conn_rt.len() - || self.conn_rt[ind].start.is_none() - { - // Nothing more to report. Return result. - let res = self - .result - .take() - .expect("result should not be empty"); - return res; - } - - let start = self.conn_rt[ind] - .start - .expect("start time should not be empty"); - let elapsed = start.elapsed(); - let time_report = TimeReport { - id: self.conn_rt[ind].id, - elapsed, - }; - let report = if ind == self.res_index { - // Succesfull entry - ChanReq::Report(time_report) - } else { - // Failed entry - ChanReq::Failure(time_report) - }; - - // Send could fail but we don't care. - let _ = self.sender.send(report).await; - - self.state = QueryState::Report(ind + 1); - continue; - } - QueryState::Wait => { - loop { - if self.fut_list.is_empty() { - // We have nothing left. There should be a reply or - // an error. Prefer a reply over an error. - if self.deferred_reply.is_some() { - let msg = self - .deferred_reply - .take() - .expect("just checked for Some"); - return Ok(msg); - } - if self.deferred_transport_error.is_some() { - let err = self - .deferred_transport_error - .take() - .expect("just checked for Some"); - return Err(err); - } - panic!("either deferred_reply or deferred_error should be present"); - } - let res = self.fut_list.next().await; - let res = res.expect("res should not be empty"); - match res.1 { - Err(ref err) => { - if self.config.defer_transport_error { - if self.deferred_transport_error.is_none() - { - self.deferred_transport_error = - Some(err.clone()); - } - // Just continue with the next future, or - // finish if fut_list is empty. - continue; - } - // Return error to the user. - } - Ok(ref msg) => { - if skip(msg, &self.config) { - if self.deferred_reply.is_none() { - self.deferred_reply = - Some(msg.clone()); - } - // Just continue with the next future, or - // finish if fut_list is empty. - continue; - } - // Return reply to user. - } - } - self.result = Some(res.1); - self.res_index = res.0; - self.state = QueryState::Report(0); - // Break out of loop to continue with the state machine - break; - } - continue; - } - } - } - } -} - -//------------ Transport ----------------------------------------------------- - -/// Type that actually implements the connection. -#[derive(Debug)] -pub struct Transport -where - Req: Send + Sync, -{ - /// Receive side of the channel used by the runner. - receiver: mpsc::Receiver>, -} - -impl<'a, Req: Clone + Send + Sync + 'static> Transport { - /// Implementation of the new method. - fn new(receiver: mpsc::Receiver>) -> Self { - Self { receiver } - } - - /// Run method. - pub async fn run(mut self) { - let mut next_id: u64 = 10; - let mut conn_stats: Vec = Vec::new(); - let mut conn_rt: Vec = Vec::new(); - let mut conns: Vec + Send + Sync>> = - Vec::new(); - - loop { - let req = match self.receiver.recv().await { - Some(req) => req, - None => break, // All references to connection objects are - // dropped. Shutdown. - }; - match req { - ChanReq::Add(add_req) => { - let id = next_id; - next_id += 1; - conn_stats.push(ConnStats { - _label: add_req.label, - mean: (DEFAULT_RT_MS as f64) / 1000., - mean_sq: 0., - max_burst: add_req.max_burst, - burst_interval: add_req.burst_interval, - burst_start: Instant::now(), - burst: 0, - queue_length_plus_one: Arc::new(()), - }); - conn_rt.push(ConnRT { - id, - est_rt: DEFAULT_RT, - start: None, - queue_length: 42, // To spot errors. - }); - conns.push(add_req.conn); - - // Don't care if send fails - let _ = add_req.tx.send(Ok(())); - } - ChanReq::GetRT(rt_req) => { - let mut tmp_conn_rt = conn_rt.clone(); - - // Remove entries that exceed the QPS limit. Loop - // backward to efficiently remove them. - for i in (0..tmp_conn_rt.len()).rev() { - // Fill-in current queue length. - tmp_conn_rt[i].queue_length = Arc::strong_count( - &conn_stats[i].queue_length_plus_one, - ) - 1; - if let Some(max_burst) = conn_stats[i].max_burst { - if conn_stats[i].burst_start.elapsed() - > conn_stats[i].burst_interval - { - conn_stats[i].burst_start = Instant::now(); - conn_stats[i].burst = 0; - } - if conn_stats[i].burst > max_burst { - tmp_conn_rt.swap_remove(i); - } - } else { - // No limit. - } - } - // Don't care if send fails - let _ = rt_req.tx.send(Ok(tmp_conn_rt)); - } - ChanReq::Query(request_req) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == request_req.id); - match opt_ind { - Some(ind) => { - // Leave resetting qps_num to GetRT. - conn_stats[ind].burst += 1; - let query = conns[ind] - .send_request(request_req.request_msg); - // Don't care if send fails - let _ = request_req.tx.send(Ok(( - query, - conn_stats[ind].queue_length_plus_one.clone(), - ))); - } - None => { - // Don't care if send fails - let _ = request_req - .tx - .send(Err(Error::RedundantTransportNotFound)); - } - } - } - ChanReq::Report(time_report) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == time_report.id); - if let Some(ind) = opt_ind { - conn_stats[ind].update(time_report.elapsed); - - let est_rt = conn_stats[ind].est_rt(); - conn_rt[ind].est_rt = Duration::from_secs_f64(est_rt); - } - } - ChanReq::Failure(time_report) => { - let opt_ind = - conn_rt.iter().position(|e| e.id == time_report.id); - if let Some(ind) = opt_ind { - let elapsed = time_report.elapsed.as_secs_f64(); - if elapsed < conn_stats[ind].mean { - // Do not update the mean if a - // failure took less time than the - // current mean. - continue; - } - conn_stats[ind].update(time_report.elapsed); - let est_rt = conn_stats[ind].est_rt(); - conn_rt[ind].est_rt = Duration::from_secs_f64(est_rt); - } - } - } - } - } -} - -//------------ Utility -------------------------------------------------------- - -/// Async function to send a request and wait for the reply. -/// -/// This gives a single future that we can put in a list. -async fn start_request( - index: usize, - id: u64, - sender: mpsc::Sender>, - request_msg: Req, -) -> (usize, Result, Error>) -where - Req: Send + Sync, -{ - let (tx, rx) = oneshot::channel(); - sender - .send(ChanReq::Query(RequestReq { - id, - request_msg, - tx, - })) - .await - .expect("receiver still exists"); - let (mut request, qlp1) = - match rx.await.expect("receive is expected to work") { - Err(err) => return (index, Err(err)), - Ok((request, qlp1)) => (request, qlp1), - }; - let reply = request.get_response().await; - - drop(qlp1); - (index, reply) -} - -/// Compare ConnRT elements based on estimated response time. -fn conn_rt_cmp(e1: &ConnRT, e2: &ConnRT, slow_rt: f64) -> Ordering { - let e1_slow = e1.est_rt.as_secs_f64() > slow_rt; - let e2_slow = e2.est_rt.as_secs_f64() > slow_rt; - - match (e1_slow, e2_slow) { - (true, true) => { - // Normal case. First check queue lengths. Then check est_rt. - e1.queue_length - .cmp(&e2.queue_length) - .then(e1.est_rt.cmp(&e2.est_rt)) - } - (true, false) => Ordering::Greater, - (false, true) => Ordering::Less, - (false, false) => e1.est_rt.cmp(&e2.est_rt), - } -} - -/// Return if this reply should be skipped or not. -fn skip(msg: &Message, config: &Config) -> bool { - // Check if we actually need to check. - if !config.defer_refused && !config.defer_servfail { - return false; - } - - let opt_rcode = msg.opt_rcode(); - // OptRcode needs PartialEq - if let OptRcode::REFUSED = opt_rcode { - if config.defer_refused { - return true; - } - } - if let OptRcode::SERVFAIL = opt_rcode { - if config.defer_servfail { - return true; - } - } - - false -} - -/// Generate a SERVFAIL reply message. -// This needs to be consolodated with the one in validator and the one in -// MessageBuilder. -fn serve_fail(msg: &Message) -> Result, Error> -where - Octs: AsRef<[u8]> + Octets, -{ - let mut target = - MessageBuilder::from_target(StaticCompressor::new(Vec::new())) - .expect("Vec is expected to have enough space"); - - let source = msg; - - *target.header_mut() = msg.header(); - target.header_mut().set_rcode(Rcode::SERVFAIL); - target.header_mut().set_ad(false); - - let source = source.question(); - let mut target = target.question(); - for rr in source { - target.push(rr?).expect("should not fail"); - } - let mut target = target.additional(); - - if let Some(opt) = msg.opt() { - target - .opt(|ob| { - ob.set_dnssec_ok(opt.dnssec_ok()); - // XXX something is missing ob.set_rcode(opt.rcode()); - ob.set_udp_payload_size(opt.udp_payload_size()); - ob.set_version(opt.version()); - for o in opt.opt().iter() { - let x: AllOptData<_, _> = o.expect("should not fail"); - ob.push(&x).expect("should not fail"); - } - Ok(()) - }) - .expect("should not fail"); - } - - let result = target.as_builder().clone(); - let msg = Message::::from_octets( - result.finish().into_target().octets_into(), - ) - .expect("Message should be able to parse output from MessageBuilder"); - Ok(msg) -} diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs index 8b3a48087..89f68fd35 100644 --- a/src/net/client/mod.rs +++ b/src/net/client/mod.rs @@ -21,10 +21,6 @@ //! transport connections. The [redundant] transport favors the connection //! with the lowest response time. Any of the other transports can be added //! as upstream transports. -//! * [load_balancer] This transport distributes requests over a collecton of -//! transport connections. The [load_balancer] transport favors connections -//! with the shortest outstanding request queue. Any of the other transports -//! can be added as upstream transports. //! * [cache] This is a simple message cache provided as a pass through //! transport. The cache works with any of the other transports. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] @@ -226,7 +222,6 @@ pub mod cache; pub mod dgram; pub mod dgram_stream; -pub mod load_balancer; pub mod multi_stream; pub mod protocol; pub mod redundant; diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index c45db3726..d0c65c753 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -9,7 +9,6 @@ use crate::net::client::request::{ ComposeRequest, Error, GetResponse, RequestMessageMulti, SendRequest, }; use crate::net::client::stream; -use crate::utils::config::DefMinMax; use bytes::Bytes; use futures_util::stream::FuturesUnordered; use futures_util::StreamExt; @@ -24,7 +23,6 @@ use std::vec::Vec; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::{mpsc, oneshot}; -use tokio::time::timeout; use tokio::time::{sleep_until, Instant}; //------------ Constants ----------------------------------------------------- @@ -35,42 +33,16 @@ const DEF_CHAN_CAP: usize = 8; /// Error messafe when the connection is closed. const ERR_CONN_CLOSED: &str = "connection closed"; -//------------ Configuration Constants ---------------------------------------- - -/// Default response timeout. -const RESPONSE_TIMEOUT: DefMinMax = DefMinMax::new( - Duration::from_secs(30), - Duration::from_millis(1), - Duration::from_secs(600), -); - //------------ Config --------------------------------------------------------- /// Configuration for an multi-stream transport. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Config { - /// Response timeout currently in effect. - response_timeout: Duration, - /// Configuration of the underlying stream transport. stream: stream::Config, } impl Config { - /// Returns the response timeout. - /// - /// This is the amount of time to wait for a request to complete. - pub fn response_timeout(&self) -> Duration { - self.response_timeout - } - - /// Sets the response timeout. - /// - /// Excessive values are quietly trimmed. - pub fn set_response_timeout(&mut self, timeout: Duration) { - self.response_timeout = RESPONSE_TIMEOUT.limit(timeout); - } - /// Returns the underlying stream config. pub fn stream(&self) -> &stream::Config { &self.stream @@ -84,19 +56,7 @@ impl Config { impl From for Config { fn from(stream: stream::Config) -> Self { - Self { - stream, - response_timeout: RESPONSE_TIMEOUT.default(), - } - } -} - -impl Default for Config { - fn default() -> Self { - Self { - stream: Default::default(), - response_timeout: RESPONSE_TIMEOUT.default(), - } + Self { stream } } } @@ -107,9 +67,6 @@ impl Default for Config { pub struct Connection { /// The sender half of the connection request channel. sender: mpsc::Sender>, - - /// Maximum amount of time to wait for a response. - response_timeout: Duration, } impl Connection { @@ -123,15 +80,8 @@ impl Connection { remote: Remote, config: Config, ) -> (Self, Transport) { - let response_timeout = config.response_timeout; let (sender, transport) = Transport::new(remote, config); - ( - Self { - sender, - response_timeout, - }, - transport, - ) + (Self { sender }, transport) } } @@ -197,7 +147,6 @@ impl Clone for Connection { fn clone(&self) -> Self { Self { sender: self.sender.clone(), - response_timeout: self.response_timeout, } } } @@ -226,9 +175,6 @@ struct Request { /// It is kept so we can compare a response with it. request_msg: Req, - /// Start time of the request. - start: Instant, - /// Current state of the query. state: QueryState, @@ -286,7 +232,6 @@ impl Request { Self { conn, request_msg, - start: Instant::now(), state: QueryState::RequestConn, conn_id: None, delayed_retry_count: 0, @@ -301,20 +246,9 @@ impl Request { /// it is resolved, you can call it again to get a new future. pub async fn get_response(&mut self) -> Result, Error> { loop { - let elapsed = self.start.elapsed(); - if elapsed >= self.conn.response_timeout { - return Err(Error::StreamReadTimeout); - } - let remaining = self.conn.response_timeout - elapsed; - match self.state { QueryState::RequestConn => { - let to = - timeout(remaining, self.conn.new_conn(self.conn_id)) - .await - .map_err(|_| Error::StreamReadTimeout)?; - - let rx = match to { + let rx = match self.conn.new_conn(self.conn_id).await { Ok(rx) => rx, Err(err) => { self.state = QueryState::Done; @@ -324,10 +258,7 @@ impl Request { self.state = QueryState::ReceiveConn(rx); } QueryState::ReceiveConn(ref mut receiver) => { - let to = timeout(remaining, receiver) - .await - .map_err(|_| Error::StreamReadTimeout)?; - let res = match to { + let res = match receiver.await { Ok(res) => res, Err(_) => { // Assume receive error @@ -363,10 +294,8 @@ impl Request { continue; } QueryState::GetResult(ref mut query) => { - let to = timeout(remaining, query.get_response()) - .await - .map_err(|_| Error::StreamReadTimeout)?; - match to { + let res = query.get_response().await; + match res { Ok(reply) => { return Ok(reply); } @@ -403,12 +332,7 @@ impl Request { } } QueryState::Delay(instant, duration) => { - if timeout(remaining, sleep_until(instant + duration)) - .await - .is_err() - { - return Err(Error::StreamReadTimeout); - }; + sleep_until(instant + duration).await; self.state = QueryState::RequestConn; } QueryState::Done => { diff --git a/src/net/client/redundant.rs b/src/net/client/redundant.rs index 4e1f1d51d..fc5677512 100644 --- a/src/net/client/redundant.rs +++ b/src/net/client/redundant.rs @@ -54,51 +54,24 @@ const SMOOTH_N: f64 = 8.; /// Chance to probe a worse connection. const PROBE_P: f64 = 0.05; +/// Avoid sending two requests at the same time. +/// +/// When a worse connection is probed, give it a slight head start. +const PROBE_RT: Duration = Duration::from_millis(1); + //------------ Config --------------------------------------------------------- /// User configuration variables. #[derive(Clone, Copy, Debug, Default)] pub struct Config { /// Defer transport errors. - defer_transport_error: bool, + pub defer_transport_error: bool, /// Defer replies that report Refused. - defer_refused: bool, + pub defer_refused: bool, /// Defer replies that report ServFail. - defer_servfail: bool, -} - -impl Config { - /// Return the value of the defer_transport_error configuration variable. - pub fn defer_transport_error(&self) -> bool { - self.defer_transport_error - } - - /// Set the value of the defer_transport_error configuration variable. - pub fn set_defer_transport_error(&mut self, value: bool) { - self.defer_transport_error = value - } - - /// Return the value of the defer_refused configuration variable. - pub fn defer_refused(&self) -> bool { - self.defer_refused - } - - /// Set the value of the defer_refused configuration variable. - pub fn set_defer_refused(&mut self, value: bool) { - self.defer_refused = value - } - - /// Return the value of the defer_servfail configuration variable. - pub fn defer_servfail(&self) -> bool { - self.defer_servfail - } - - /// Set the value of the defer_servfail configuration variable. - pub fn set_defer_servfail(&mut self, value: bool) { - self.defer_servfail = value - } + pub defer_servfail: bool, } //------------ Connection ----------------------------------------------------- @@ -186,7 +159,7 @@ impl SendRequest //------------ Request ------------------------------------------------------- /// An active request. -struct Request { +pub struct Request { /// The underlying future. fut: Pin< Box, Error>> + Send + Sync>, @@ -227,7 +200,7 @@ impl Debug for Request { /// This type represents an active query request. #[derive(Debug)] -struct Query +pub struct Query where Req: Send + Sync, { @@ -412,15 +385,10 @@ impl Query { // Do we want to probe a less performant upstream? if conn_rt_len > 1 && random::() < PROBE_P { let index: usize = 1 + random::() % (conn_rt_len - 1); + conn_rt[index].est_rt = PROBE_RT; - // Give the probe some head start. We may need a separate - // configuration parameter. A multiple of min_rt. Just use - // min_rt for now. - let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); - - let mut e = conn_rt.remove(index); - e.est_rt = min_rt; - conn_rt.insert(0, e); + // Sort again + conn_rt.sort_unstable_by(conn_rt_cmp); } Self { From c71434ef97a84d99f861efba3441ab3a0bb4bfde Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 29 Nov 2024 13:34:53 +0100 Subject: [PATCH 223/569] Revert "Merge branch 'zonemd-from-str' into sortedrecords-zonemd-remove-replace" This reverts commit 2712529125f924ebcfca9cd15ad4e158c0471c53, reversing changes made to 19fac46f59655524a92a7f22fff2d764b25a607a. --- src/base/iana/mod.rs | 2 - src/base/iana/zonemd.rs | 50 ----------------- src/rdata/zonemd.rs | 120 ++++++++++++++++++++++++++++++++++++---- 3 files changed, 109 insertions(+), 63 deletions(-) delete mode 100644 src/base/iana/zonemd.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 9d86b2e94..2b73fe624 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -35,7 +35,6 @@ pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecAlg; pub use self::svcb::SvcParamKey; -pub use self::zonemd::{ZonemdAlg, ZonemdScheme}; #[macro_use] mod macros; @@ -50,4 +49,3 @@ pub mod rcode; pub mod rtype; pub mod secalg; pub mod svcb; -pub mod zonemd; diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs deleted file mode 100644 index 249448477..000000000 --- a/src/base/iana/zonemd.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! ZONEMD IANA parameters. - -//------------ ZonemdScheme -------------------------------------------------- - -int_enum! { - /// ZONEMD schemes. - /// - /// This type selects the method by which data is collated and presented - /// as input to the hashing function for use with [ZONEMD]. - /// - /// For the currently registered values see the [IANA registration]. This - /// type is complete as of 2024-11-29. - /// - /// [ZONEMD]: ../../../rdata/zonemd/index.html - /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-schemes - => - ZonemdScheme, u8; - - /// Specifies that the SIMPLE scheme is used. - (SIMPLE => 1, "SIMPLE") -} - -int_enum_str_decimal!(ZonemdScheme, u8); -int_enum_zonefile_fmt_decimal!(ZonemdScheme, "scheme"); - -//------------ ZonemdAlg ----------------------------------------------------- - -int_enum! { - /// ZONEMD algorithms. - /// - /// This type selects the algorithm used to hash domain names for use with - /// the [ZONEMD]. - /// - /// For the currently registered values see the [IANA registration]. This - /// type is complete as of 2024-11-29. - /// - /// [ZONEMD]: ../../../rdata/zonemd/index.html - /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-hash-algorithms - => - ZonemdAlg, u8; - - /// Specifies that the SHA-384 algorithm is used. - (SHA384 => 1, "SHA-384") - - /// Specifies that the SHA-512 algorithm is used. - (SHA512 => 2, "SHA-512") -} - -int_enum_str_decimal!(ZonemdAlg, u8); -int_enum_zonefile_fmt_decimal!(ZonemdAlg, "hash algorithm"); diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index f30dcb0b9..66f41d400 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -9,12 +9,12 @@ #![allow(clippy::needless_maybe_sized)] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Rtype, ZonemdAlg, ZonemdScheme}; +use crate::base::iana::Rtype; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; -use crate::base::wire::{Composer, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::wire::{Composer, ParseError}; use crate::utils::base16; use core::cmp::Ordering; use core::{fmt, hash}; @@ -29,8 +29,8 @@ const DIGEST_MIN_LEN: usize = 12; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Zonemd { serial: Serial, - scheme: ZonemdScheme, - algo: ZonemdAlg, + scheme: Scheme, + algo: Algorithm, #[cfg_attr( feature = "serde", serde( @@ -54,8 +54,8 @@ impl Zonemd { /// Create a Zonemd record data from provided parameters. pub fn new( serial: Serial, - scheme: ZonemdScheme, - algo: ZonemdAlg, + scheme: Scheme, + algo: Algorithm, digest: Octs, ) -> Self { Self { @@ -72,12 +72,12 @@ impl Zonemd { } /// Get the scheme field. - pub fn scheme(&self) -> ZonemdScheme { + pub fn scheme(&self) -> Scheme { self.scheme } /// Get the hash algorithm field. - pub fn algorithm(&self) -> ZonemdAlg { + pub fn algorithm(&self) -> Algorithm { self.algo } @@ -233,7 +233,20 @@ impl> ZonefileFmt for Zonemd { p.block(|p| { p.write_token(self.serial)?; p.write_show(self.scheme)?; + p.write_comment(format_args!("scheme ({})", match self.scheme { + Scheme::Reserved => "reserved", + Scheme::Simple => "simple", + Scheme::Unassigned(_) => "unassigned", + Scheme::Private(_) => "private", + }))?; p.write_show(self.algo)?; + p.write_comment(format_args!("algorithm ({})", match self.algo { + Algorithm::Reserved => "reserved", + Algorithm::Sha384 => "SHA384", + Algorithm::Sha512 => "SHA512", + Algorithm::Unassigned(_) => "unassigned", + Algorithm::Private(_) => "private", + }))?; p.write_token(base16::encode_display(&self.digest)) }) } @@ -301,6 +314,92 @@ impl> Ord for Zonemd { } } +/// The data collation scheme. +/// +/// This enumeration wraps an 8-bit unsigned integer that identifies the +/// methods by which data is collated and presented as input to the +/// hashing function. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Scheme { + Reserved, + Simple, + Unassigned(u8), + Private(u8), +} + +impl From for u8 { + fn from(s: Scheme) -> u8 { + match s { + Scheme::Reserved => 0, + Scheme::Simple => 1, + Scheme::Unassigned(n) => n, + Scheme::Private(n) => n, + } + } +} + +impl From for Scheme { + fn from(n: u8) -> Self { + match n { + 0 | 255 => Self::Reserved, + 1 => Self::Simple, + 2..=239 => Self::Unassigned(n), + 240..=254 => Self::Private(n), + } + } +} + +impl ZonefileFmt for Scheme { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.write_token(u8::from(*self)) + } +} + +/// The Hash Algorithm used to construct the digest. +/// +/// This enumeration wraps an 8-bit unsigned integer that identifies +/// the cryptographic hash algorithm. +#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum Algorithm { + Reserved, + Sha384, + Sha512, + Unassigned(u8), + Private(u8), +} + +impl From for u8 { + fn from(algo: Algorithm) -> u8 { + match algo { + Algorithm::Reserved => 0, + Algorithm::Sha384 => 1, + Algorithm::Sha512 => 2, + Algorithm::Unassigned(n) => n, + Algorithm::Private(n) => n, + } + } +} + +impl From for Algorithm { + fn from(n: u8) -> Self { + match n { + 0 | 255 => Self::Reserved, + 1 => Self::Sha384, + 2 => Self::Sha512, + 3..=239 => Self::Unassigned(n), + 240..=254 => Self::Private(n), + } + } +} + +impl ZonefileFmt for Algorithm { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.write_token(u8::from(*self)) + } +} + #[cfg(test)] #[cfg(all(feature = "std", feature = "bytes"))] mod test { @@ -338,7 +437,6 @@ mod test { #[cfg(feature = "zonefile")] #[test] fn zonemd_parse_zonefile() { - use crate::base::iana::ZonemdAlg; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; @@ -371,8 +469,8 @@ ns2 3600 IN AAAA 2001:db8::63 match record.into_data() { ZoneRecordData::Zonemd(rd) => { assert_eq!(2018031900, rd.serial().into_int()); - assert_eq!(ZonemdScheme::SIMPLE, rd.scheme()); - assert_eq!(ZonemdAlg::SHA384, rd.algorithm()); + assert_eq!(Scheme::Simple, rd.scheme()); + assert_eq!(Algorithm::Sha384, rd.algorithm()); } _ => panic!(), } From 48d26d8ce15088fe4a8e5e7c0f9e234f8ae320c9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:41:10 +0100 Subject: [PATCH 224/569] Merge PR #444 branch zonemd-from-str into this branch. --- src/base/iana/mod.rs | 2 + src/base/iana/zonemd.rs | 50 +++++++++++++++++ src/rdata/zonemd.rs | 118 ++++------------------------------------ 3 files changed, 62 insertions(+), 108 deletions(-) create mode 100644 src/base/iana/zonemd.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 2b73fe624..9d86b2e94 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -35,6 +35,7 @@ pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecAlg; pub use self::svcb::SvcParamKey; +pub use self::zonemd::{ZonemdAlg, ZonemdScheme}; #[macro_use] mod macros; @@ -49,3 +50,4 @@ pub mod rcode; pub mod rtype; pub mod secalg; pub mod svcb; +pub mod zonemd; diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs new file mode 100644 index 000000000..249448477 --- /dev/null +++ b/src/base/iana/zonemd.rs @@ -0,0 +1,50 @@ +//! ZONEMD IANA parameters. + +//------------ ZonemdScheme -------------------------------------------------- + +int_enum! { + /// ZONEMD schemes. + /// + /// This type selects the method by which data is collated and presented + /// as input to the hashing function for use with [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-schemes + => + ZonemdScheme, u8; + + /// Specifies that the SIMPLE scheme is used. + (SIMPLE => 1, "SIMPLE") +} + +int_enum_str_decimal!(ZonemdScheme, u8); +int_enum_zonefile_fmt_decimal!(ZonemdScheme, "scheme"); + +//------------ ZonemdAlg ----------------------------------------------------- + +int_enum! { + /// ZONEMD algorithms. + /// + /// This type selects the algorithm used to hash domain names for use with + /// the [ZONEMD]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2024-11-29. + /// + /// [ZONEMD]: ../../../rdata/zonemd/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-hash-algorithms + => + ZonemdAlg, u8; + + /// Specifies that the SHA-384 algorithm is used. + (SHA384 => 1, "SHA-384") + + /// Specifies that the SHA-512 algorithm is used. + (SHA512 => 2, "SHA-512") +} + +int_enum_str_decimal!(ZonemdAlg, u8); +int_enum_zonefile_fmt_decimal!(ZonemdAlg, "hash algorithm"); diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index 66f41d400..025dae200 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -9,7 +9,7 @@ #![allow(clippy::needless_maybe_sized)] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Rtype, ZonemdAlg, ZonemdScheme}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; @@ -29,8 +29,8 @@ const DIGEST_MIN_LEN: usize = 12; #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Zonemd { serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, #[cfg_attr( feature = "serde", serde( @@ -54,8 +54,8 @@ impl Zonemd { /// Create a Zonemd record data from provided parameters. pub fn new( serial: Serial, - scheme: Scheme, - algo: Algorithm, + scheme: ZonemdScheme, + algo: ZonemdAlg, digest: Octs, ) -> Self { Self { @@ -72,12 +72,12 @@ impl Zonemd { } /// Get the scheme field. - pub fn scheme(&self) -> Scheme { + pub fn scheme(&self) -> ZonemdScheme { self.scheme } /// Get the hash algorithm field. - pub fn algorithm(&self) -> Algorithm { + pub fn algorithm(&self) -> ZonemdAlg { self.algo } @@ -233,20 +233,7 @@ impl> ZonefileFmt for Zonemd { p.block(|p| { p.write_token(self.serial)?; p.write_show(self.scheme)?; - p.write_comment(format_args!("scheme ({})", match self.scheme { - Scheme::Reserved => "reserved", - Scheme::Simple => "simple", - Scheme::Unassigned(_) => "unassigned", - Scheme::Private(_) => "private", - }))?; p.write_show(self.algo)?; - p.write_comment(format_args!("algorithm ({})", match self.algo { - Algorithm::Reserved => "reserved", - Algorithm::Sha384 => "SHA384", - Algorithm::Sha512 => "SHA512", - Algorithm::Unassigned(_) => "unassigned", - Algorithm::Private(_) => "private", - }))?; p.write_token(base16::encode_display(&self.digest)) }) } @@ -314,92 +301,6 @@ impl> Ord for Zonemd { } } -/// The data collation scheme. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies the -/// methods by which data is collated and presented as input to the -/// hashing function. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Scheme { - Reserved, - Simple, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(s: Scheme) -> u8 { - match s { - Scheme::Reserved => 0, - Scheme::Simple => 1, - Scheme::Unassigned(n) => n, - Scheme::Private(n) => n, - } - } -} - -impl From for Scheme { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Simple, - 2..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl ZonefileFmt for Scheme { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - -/// The Hash Algorithm used to construct the digest. -/// -/// This enumeration wraps an 8-bit unsigned integer that identifies -/// the cryptographic hash algorithm. -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum Algorithm { - Reserved, - Sha384, - Sha512, - Unassigned(u8), - Private(u8), -} - -impl From for u8 { - fn from(algo: Algorithm) -> u8 { - match algo { - Algorithm::Reserved => 0, - Algorithm::Sha384 => 1, - Algorithm::Sha512 => 2, - Algorithm::Unassigned(n) => n, - Algorithm::Private(n) => n, - } - } -} - -impl From for Algorithm { - fn from(n: u8) -> Self { - match n { - 0 | 255 => Self::Reserved, - 1 => Self::Sha384, - 2 => Self::Sha512, - 3..=239 => Self::Unassigned(n), - 240..=254 => Self::Private(n), - } - } -} - -impl ZonefileFmt for Algorithm { - fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - p.write_token(u8::from(*self)) - } -} - #[cfg(test)] #[cfg(all(feature = "std", feature = "bytes"))] mod test { @@ -437,6 +338,7 @@ mod test { #[cfg(feature = "zonefile")] #[test] fn zonemd_parse_zonefile() { + use crate::base::iana::ZonemdAlg; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; @@ -469,8 +371,8 @@ ns2 3600 IN AAAA 2001:db8::63 match record.into_data() { ZoneRecordData::Zonemd(rd) => { assert_eq!(2018031900, rd.serial().into_int()); - assert_eq!(Scheme::Simple, rd.scheme()); - assert_eq!(Algorithm::Sha384, rd.algorithm()); + assert_eq!(ZonemdScheme::SIMPLE, rd.scheme()); + assert_eq!(ZonemdAlg::SHA384, rd.algorithm()); } _ => panic!(), } From 5ede42e24c3cf5e1618d40e1b70334c9decc9030 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 11:24:12 +0100 Subject: [PATCH 225/569] IANA ZONEMD algorithm mnemonics are not hyphenated. --- src/base/iana/zonemd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs index 249448477..cd92a1012 100644 --- a/src/base/iana/zonemd.rs +++ b/src/base/iana/zonemd.rs @@ -40,10 +40,10 @@ int_enum! { ZonemdAlg, u8; /// Specifies that the SHA-384 algorithm is used. - (SHA384 => 1, "SHA-384") + (SHA384 => 1, "SHA384") /// Specifies that the SHA-512 algorithm is used. - (SHA512 => 2, "SHA-512") + (SHA512 => 2, "SHA512") } int_enum_str_decimal!(ZonemdAlg, u8); From a654d959fa1fdd429b1c7ed2af4781ef67a9714c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:01:52 +0100 Subject: [PATCH 226/569] Base use of extra signing keys on a flag, not hard-coded behaviour. --- src/sign/records.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 01a012d93..44e95867a 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -117,6 +117,7 @@ impl SortedRecords { inception: Timestamp, keys: &[SigningKey], add_used_dnskeys: bool, + sign_dnskeys_with_all_keys: bool, ) -> Result< Vec>>, ErrorTypeToBeDetermined, @@ -133,17 +134,19 @@ impl SortedRecords { + From> + octseq::OctetsFrom>, { - let (mut ksks, mut zsks): (Vec<_>, Vec<_>) = keys + let keys_by_ref: Vec<_> = keys.iter().map(|k| k).collect(); + let (ksks, zsks): (Vec<_>, Vec<_>) = keys .iter() .filter(|k| k.is_zone_signing_key()) .partition(|k| k.is_secure_entry_point()); - // CSK? - if !ksks.is_empty() && zsks.is_empty() { - zsks = ksks.clone(); - } else if ksks.is_empty() && !zsks.is_empty() { - ksks = zsks.clone(); - } + let dnskey_signing_keys = if sign_dnskeys_with_all_keys { + &keys_by_ref + } else if ksks.is_empty() { + &zsks + } else { + &ksks + }; if enabled!(Level::DEBUG) { for key in keys { @@ -250,7 +253,7 @@ impl SortedRecords { } let keys = if rrset.rtype() == Rtype::DNSKEY { - &ksks + dnskey_signing_keys } else { &zsks }; From 77b32e3d142df8546baed356b311bfed03609344 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Dec 2024 12:06:59 +0100 Subject: [PATCH 227/569] Clippy. --- src/sign/records.rs | 4 ++-- src/validate.rs | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 44e95867a..6a75a293c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -134,7 +134,7 @@ impl SortedRecords { + From> + octseq::OctetsFrom>, { - let keys_by_ref: Vec<_> = keys.iter().map(|k| k).collect(); + let keys_by_ref: Vec<_> = keys.iter().collect(); let (ksks, zsks): (Vec<_>, Vec<_>) = keys .iter() .filter(|k| k.is_zone_signing_key()) @@ -961,7 +961,7 @@ impl FamilyName { } } -impl<'a, N: Clone> FamilyName<&'a N> { +impl FamilyName<&N> { pub fn cloned(&self) -> FamilyName { FamilyName { owner: (*self.owner).clone(), diff --git a/src/validate.rs b/src/validate.rs index 07bb124b5..84864c123 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1762,7 +1762,6 @@ pub enum Nsec3HashError { } ///--- Display - impl std::fmt::Display for Nsec3HashError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { From 2de0e440a31d155bf2fbc88c7afd0a4ce2c7ecc2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:16:42 +0100 Subject: [PATCH 228/569] Add the signature validity period to SigningKey as "important metadata" related to the key, consistent with how LDNS stores inception and expiration per key, and consistent with how RFC 4033 etc refer to the validity period of a signing key. --- src/sign/mod.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 61549965b..060e9af54 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -113,11 +113,11 @@ #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] use core::fmt; +use core::ops::RangeInclusive; -use crate::{ - base::{iana::SecAlg, Name}, - validate, -}; +use crate::base::{iana::SecAlg, Name}; +use crate::rdata::dnssec::Timestamp; +use crate::validate::Key; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -145,6 +145,13 @@ pub struct SigningKey { /// The raw private key. inner: Inner, + + /// The validity period to assign to any DNSSEC signatures created using + /// this key. + /// + /// The range spans from the inception timestamp up to and including the + /// expiration timestamp. + signature_validity_period: Option>, } //--- Construction @@ -156,8 +163,25 @@ impl SigningKey { owner, flags, inner, + signature_validity_period: None, } } + + pub fn with_validity( + mut self, + inception: Timestamp, + expiration: Timestamp, + ) -> Self { + self.signature_validity_period = + Some(RangeInclusive::new(inception, expiration)); + self + } + + pub fn signature_validity_period( + &self, + ) -> Option> { + self.signature_validity_period.clone() + } } //--- Inspection @@ -236,12 +260,12 @@ impl SigningKey { } /// The associated public key. - pub fn public_key(&self) -> validate::Key<&Octs> + pub fn public_key(&self) -> Key<&Octs> where Octs: AsRef<[u8]>, { let owner = Name::from_octets(self.owner.as_octets()).unwrap(); - validate::Key::new(owner, self.flags, self.inner.raw_public_key()) + Key::new(owner, self.flags, self.inner.raw_public_key()) } /// The associated raw public key. From 685a4022ba2fb0339dc0a72bd6ea7c95241aff07 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 3 Dec 2024 21:16:59 +0100 Subject: [PATCH 229/569] - Move sign() out of SortedRecords into a new Signer type and have it take an iterator as input, which is more flexible than being able to sign only SortedRecords. - Replace `ErrorTypeToBeDetermined` with new enum `SigningError`. - Introduce `IntendedKeyPurpose` to signal intended use of keys explicitly rather than infering intent from key flags. - Introduce `DnssecSigningKey` to associate key intent with a key and provide various constructors to simplify usage. - Introduce `SigningKeyUsageStrategy` to externalize the logic for choosing which keys to use to sign DNSKEY RRSETs and other zone RRSETs. - Use per key signature validity periods instead of per signing operation. - Improved key summary at debug level, including alg name as well as number. --- src/sign/records.rs | 988 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 778 insertions(+), 210 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 6a75a293c..9b1b54adf 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,12 +1,14 @@ //! Actual signing. use core::convert::From; use core::fmt::Display; +use core::marker::PhantomData; +use core::ops::Deref; use std::boxed::Box; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::hash::Hash; -use std::string::String; +use std::string::{String, ToString}; use std::vec::Vec; use std::{fmt, slice}; @@ -20,9 +22,7 @@ use crate::base::name::{ToLabelIter, ToName}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; use crate::base::{Name, NameBuilder, Ttl}; -use crate::rdata::dnssec::{ - ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, -}; +use crate::rdata::dnssec::{ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Dnskey, Nsec, Nsec3, Nsec3param, Soa, ZoneRecordData}; use crate::utils::base32; @@ -95,208 +95,12 @@ impl SortedRecords { } } -impl SortedRecords { - /// Sign a zone using the given keys. - /// - /// A DNSKEY RR will be output for each key. - /// - /// Keys with a supported algorithm with the ZONE flag set will be used as - /// ZSKs. - /// - /// Keys with a supported algorithm with the ZONE flag AND the SEP flag - /// set will be used as KSKs. - /// - /// If only one key has a supported algorithm and has the ZONE flag set - /// AND has the SEP flag set, it will be used as a CSK (i.e. both KSK and - /// ZSK). - #[allow(clippy::type_complexity)] - pub fn sign( - &self, - apex: &FamilyName, - expiration: Timestamp, - inception: Timestamp, - keys: &[SigningKey], - add_used_dnskeys: bool, - sign_dnskeys_with_all_keys: bool, - ) -> Result< - Vec>>, - ErrorTypeToBeDetermined, - > - where - N: ToName + Clone, - D: CanonicalOrd - + RecordData - + ComposeRecordData - + From>, - ConcreteSecretKey: SignRaw, - Octets: AsRef<[u8]> - + Clone - + From> - + octseq::OctetsFrom>, - { - let keys_by_ref: Vec<_> = keys.iter().collect(); - let (ksks, zsks): (Vec<_>, Vec<_>) = keys - .iter() - .filter(|k| k.is_zone_signing_key()) - .partition(|k| k.is_secure_entry_point()); - - let dnskey_signing_keys = if sign_dnskeys_with_all_keys { - &keys_by_ref - } else if ksks.is_empty() { - &zsks - } else { - &ksks - }; - - if enabled!(Level::DEBUG) { - for key in keys { - debug!( - "Key : {}, owner={}, flags={} (SEP={}, ZSK={}))", - key.algorithm(), - key.owner(), - key.flags(), - key.is_secure_entry_point(), - key.is_zone_signing_key(), - ) - } - debug!("# KSKs: {}", ksks.len()); - debug!("# ZSKs: {}", zsks.len()); - } - - let mut res: Vec>> = Vec::new(); - let mut buf = Vec::new(); - let mut cut: Option> = None; - let mut families = self.families(); - - // Since the records are ordered, the first family is the apex -- - // we can skip everything before that. - families.skip_before(apex); - - let mut families = families.peekable(); - - let apex_ttl = - families.peek().unwrap().records().next().unwrap().ttl(); - - let mut dnskey_rrs = SortedRecords::new(); - - for public_key in keys.iter().map(|k| k.public_key()) { - let dnskey: Dnskey = - Dnskey::convert(public_key.to_dnskey()); - - dnskey_rrs - .insert(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - dnskey.clone().into(), - )) - .map_err(|_| ErrorTypeToBeDetermined)?; - - if add_used_dnskeys { - res.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - ZoneRecordData::Dnskey(dnskey), - )); - } - } - - let dummy_dnskey_rrs = SortedRecords::new(); - let families_iter = if add_used_dnskeys { - dnskey_rrs.families().chain(families) - } else { - dummy_dnskey_rrs.families().chain(families) - }; - - for family in families_iter { - // If the owner is out of zone, we have moved out of our zone and - // are done. - if !family.is_in_zone(apex) { - break; - } - - // If the family is below a zone cut, we must ignore it. - if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { - continue; - } - } - - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); - - // If this family is the parent side of a zone cut, we keep the - // family name for later. This also means below that if - // `cut.is_some()` we are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - Some(name.clone()) - } else { - None - }; - - for rrset in family.rrsets() { - if cut.is_some() { - // If we are at a zone cut, we only sign DS and NSEC - // records. NS records we must not sign and everything - // else shouldn’t be here, really. - if rrset.rtype() != Rtype::DS - && rrset.rtype() != Rtype::NSEC - { - continue; - } - } else { - // Otherwise we only ignore RRSIGs. - if rrset.rtype() == Rtype::RRSIG { - continue; - } - } - - let keys = if rrset.rtype() == Rtype::DNSKEY { - dnskey_signing_keys - } else { - &zsks - }; - - for key in keys { - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm(), - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.public_key().key_tag(), - apex.owner().clone(), - ); - - buf.clear(); - rrsig.compose_canonical(&mut buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(&mut buf).unwrap(); - } - let signature = - key.raw_secret_key().sign_raw(&buf).unwrap(); - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(ErrorTypeToBeDetermined); - }; - - let rrsig = - rrsig.into_rrsig(signature).expect("long signature"); - res.push(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - ZoneRecordData::Rrsig(rrsig), - )); - } - } - } - - Ok(res) - } - +impl SortedRecords +where + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + SortedRecords: From>>, +{ pub fn nsecs( &self, apex: &FamilyName, @@ -1164,10 +968,15 @@ where } } -//------------ ErrorTypeToBeDetermined --------------------------------------- +//------------ SigningError -------------------------------------------------- -#[derive(Debug)] -pub struct ErrorTypeToBeDetermined; +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SigningError { + /// One or more keys does not have a signature validity period defined. + KeyLacksSignatureValidityPeriod, + DuplicateDnskey, + OutOfMemory, +} //------------ Nsec3OptOut --------------------------------------------------- @@ -1214,3 +1023,762 @@ pub enum Nsec3OptOut { // 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." + +//------------ IntendedKeyPurpose -------------------------------------------- + +/// The purpose of a DNSSEC key from the perspective of an operator. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IntendedKeyPurpose { + /// A key that signs DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Key signing key (KSK): DNSSEC keys that "only sign the apex DNSKEY + /// RRset in a zone." (Quoted from RFC6781, Section 3.1) + KSK, + + /// A key that signs non-DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Zone signing key (ZSK): "DNSSEC keys that can be used to sign all the + /// RRsets in a zone that require signatures, other than the apex DNSKEY + /// RRset." (Quoted from RFC6781, Section 3.1) Also note that a ZSK is + /// sometimes used to sign the apex DNSKEY RRset. + ZSK, + + /// A key that signs both DNSKEY and other RRSETs. + /// + /// RFC 9499 DNS Terminology: + /// 10. General DNSSEC + /// Combined signing key (CSK): In cases where the differentiation between + /// the KSK and ZSK is not made, i.e., where keys have the role of both + /// KSK and ZSK, we talk about a Single-Type Signing Scheme." (Quoted from + /// [RFC6781], Section 3.1) This is sometimes called a "combined signing + /// key" or "CSK". It is operational practice, not protocol, that + /// determines whether a particular key is a ZSK, a KSK, or a CSK. + CSK, + + /// A key that is not currently used for signing. + /// + /// This key should be added to the zone but not used to sign any RRSETs. + Inactive, +} + +//------------ DnssecSigningKey ---------------------------------------------- + +/// A key to be provided by an operator to a DNSSEC signer. +/// +/// This type carries metadata that signals to a DNSSEC signer how this key +/// should impact the zone to be signed. +pub struct DnssecSigningKey { + /// The key to use to make DNSSEC signatures. + key: SigningKey, + + /// The purpose for which the operator intends the key to be used. + /// + /// Defines explicitly the purpose of the key which should be used instead + /// of attempting to infer the purpose of the key (to sign keys and/or to + /// sign other records) by examining the setting of the Secure Entry Point + /// and Zone Key flags on the key (i.e. whether the key is a KSK or ZSK or + /// something else). + purpose: IntendedKeyPurpose, + + _phantom: PhantomData<(Octs, Inner)>, +} + +impl DnssecSigningKey { + /// Create a new [`DnssecSigningKey`] by assocating intent with a + /// reference to an existing key. + pub fn new( + key: SigningKey, + purpose: IntendedKeyPurpose, + ) -> Self { + Self { + key, + purpose, + _phantom: Default::default(), + } + } + + pub fn into_inner(self) -> SigningKey { + self.key + } +} + +impl Deref for DnssecSigningKey { + type Target = SigningKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +impl DnssecSigningKey { + pub fn key(&self) -> &SigningKey { + &self.key + } + + pub fn purpose(&self) -> IntendedKeyPurpose { + self.purpose + } +} + +impl, Inner: SignRaw> DnssecSigningKey { + pub fn ksk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::KSK, + _phantom: Default::default(), + } + } + + pub fn zsk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::ZSK, + _phantom: Default::default(), + } + } + + pub fn csk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::CSK, + _phantom: Default::default(), + } + } + + pub fn inactive(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::Inactive, + _phantom: Default::default(), + } + } + + pub fn inferred(key: SigningKey) -> Self { + let public_key = key.public_key(); + match ( + public_key.is_secure_entry_point(), + public_key.is_zone_signing_key(), + ) { + (true, _) => Self::ksk(key), + (false, true) => Self::zsk(key), + (false, false) => Self::inactive(key), + } + } +} + +//------------ Operations ---------------------------------------------------- + +// TODO: Move nsecs() and nsecs3() out of SortedRecords and make them also +// take an iterator. This allows callers to pass an iterator over Record +// rather than force them to create the SortedRecords type (which for example +// in the case of a Zone we wouldn't have, but may instead be able to get an +// iterator over the Zone). Also move out the helper functions. Maybe put them +// all into a Signer struct? + +pub trait SigningKeyUsageStrategy { + const NAME: &'static str; + + fn new() -> Self; + + fn filter_ksks( + &mut self, + candidate_key: &DnssecSigningKey, + ) -> bool { + matches!( + candidate_key.purpose(), + IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK + ) + } + + fn filter_zsks( + &mut self, + candidate_key: &DnssecSigningKey, + ) -> bool { + matches!( + candidate_key.purpose(), + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + } +} + +pub struct DefaultSigningKeyUsageStrategy; + +impl SigningKeyUsageStrategy + for DefaultSigningKeyUsageStrategy +{ + const NAME: &'static str = "Default key usage strategy"; + + fn new() -> Self { + Self + } +} + +pub struct Signer +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, +{ + _phantom: PhantomData<(Octs, Inner, KeyStrat)>, +} + +impl Default for Signer +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, +{ + fn default() -> Self { + Self::new() + } +} + +impl Signer +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, +{ + pub fn new() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl Signer +where + Octs: AsRef<[u8]> + From> + OctetsFrom>, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, +{ + /// Sign a zone using the given keys. + /// + /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be + /// added to the given records in order to DNSSEC sign them. + /// + /// The given records MUST be sorted according to [`CanonicalOrd`]. + #[allow(clippy::type_complexity)] + pub fn sign( + &self, + apex: &FamilyName, + mut families: RecordsIter<'_, N, D>, + keys: &[DnssecSigningKey], + add_used_dnskeys: bool, + ) -> Result>>, SigningError> + where + N: ToName + Clone + Send, + D: RecordData + + ComposeRecordData + + From> + + CanonicalOrd + + Send, + { + debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); + + // Work with indices because SigningKey doesn't impl PartialEq so we + // cannot use a HashSet to make a unique set of them. + + let mut key_filter = KeyStrat::new(); + + let dnskey_signing_key_idxs: HashSet = keys + .iter() + .enumerate() + .filter_map(|(i, k)| key_filter.filter_ksks(k).then_some(i)) + .collect(); + + let rrset_signing_key_idxs: HashSet = keys + .iter() + .enumerate() + .filter_map(|(i, k)| key_filter.filter_zsks(k).then_some(i)) + .collect(); + + let keys_in_use_idxs: HashSet<_> = rrset_signing_key_idxs + .iter() + .chain(dnskey_signing_key_idxs.iter()) + .collect(); + + if enabled!(Level::DEBUG) { + fn debug_key, Inner: SignRaw>( + prefix: &str, + key: &SigningKey, + ) { + debug!( + "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}))", + key.algorithm() + .to_mnemonic_str() + .map(|alg| format!("{alg} ({})", key.algorithm())) + .unwrap_or_else(|| key.algorithm().to_string()), + key.owner(), + key.flags(), + key.is_secure_entry_point(), + key.is_zone_signing_key(), + ) + } + + debug!("# Keys: {}", keys_in_use_idxs.len()); + debug!( + "# DNSKEY RR signing keys: {}", + dnskey_signing_key_idxs.len() + ); + debug!("# RRSET signing keys: {}", rrset_signing_key_idxs.len()); + + for idx in &keys_in_use_idxs { + debug_key("Key", keys[**idx].key()); + } + + for idx in &rrset_signing_key_idxs { + debug_key("RRSET Signing Key", keys[*idx].key()); + } + + for idx in &dnskey_signing_key_idxs { + debug_key("DNSKEY Signing Key", keys[*idx].key()); + } + } + + let mut res: Vec>> = Vec::new(); + let mut buf = Vec::new(); + let mut cut: Option> = None; + + // Since the records are ordered, the first family is the apex -- + // we can skip everything before that. + families.skip_before(apex); + + let mut families = families.peekable(); + + let apex_ttl = + families.peek().unwrap().records().next().unwrap().ttl(); + + // Make DNSKEY RRs for all keys that will be used. + let mut dnskey_rrs_to_sign = SortedRecords::::new(); + for public_key in keys_in_use_idxs + .iter() + .map(|&&idx| keys[idx].key().public_key()) + { + let dnskey = public_key.to_dnskey(); + + // Save the DNSKEY RR so that we can generate an RRSIG for it. + dnskey_rrs_to_sign + .insert(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey.clone()).into(), + )) + .map_err(|_| SigningError::DuplicateDnskey)?; + + if add_used_dnskeys { + // Add the DNSKEY RR to the final result so that we not only + // produce an RRSIG for it but tell the caller this is a new + // record to include in the final zone. + res.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey).into(), + )); + } + } + + let dummy_dnskey_rrs = SortedRecords::::new(); + let families_iter = if add_used_dnskeys { + dnskey_rrs_to_sign.families().chain(families) + } else { + dummy_dnskey_rrs.families().chain(families) + }; + + for family in families_iter { + // If the owner is out of zone, we have moved out of our zone and + // are done. + if !family.is_in_zone(apex) { + break; + } + + // If the family is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the + // family name for later. This also means below that if + // `cut.is_some()` we are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + Some(name.clone()) + } else { + None + }; + + for rrset in family.rrsets() { + if cut.is_some() { + // If we are at a zone cut, we only sign DS and NSEC + // records. NS records we must not sign and everything + // else shouldn’t be here, really. + if rrset.rtype() != Rtype::DS + && rrset.rtype() != Rtype::NSEC + { + continue; + } + } else { + // Otherwise we only ignore RRSIGs. + if rrset.rtype() == Rtype::RRSIG { + continue; + } + } + + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + &dnskey_signing_key_idxs + } else { + &rrset_signing_key_idxs + }; + + for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) + { + let (inception, expiration) = key + .signature_validity_period() + .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? + .into_inner(); + + let rrsig = ProtoRrsig::new( + rrset.rtype(), + key.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + key.public_key().key_tag(), + apex.owner().clone(), + ); + + buf.clear(); + rrsig.compose_canonical(&mut buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(&mut buf).unwrap(); + } + let signature = + key.raw_secret_key().sign_raw(&buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + + let rrsig = + rrsig.into_rrsig(signature).expect("long signature"); + res.push(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + ZoneRecordData::Rrsig(rrsig), + )); + debug!( + "Signed {} record with keytag {}", + rrset.rtype(), + key.public_key().key_tag() + ); + } + } + } + + debug!("Returning {} records from signing", res.len()); + + Ok(res) + } +} + +// /// Sign a zone using the given keys. +// /// +// /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be +// /// added to the given records in order to DNSSEC sign them. +// /// +// /// The given records MUST be sorted according to [`CanonicalOrd`]. +// #[allow(clippy::type_complexity)] +// pub fn sign<'a, N, D, S, Octets, ConcreteSecretKey, K>( +// apex: &FamilyName, +// mut families: RecordsIter<'a, N, D>, +// expiration: Timestamp, +// inception: Timestamp, +// keys: &[DnssecSigningKey<'a, Octets, ConcreteSecretKey, K>], +// add_used_dnskeys: bool, +// sign_dnskeys_with_all_keys: bool, +// ) -> Result>>, ErrorTypeToBeDetermined> +// where +// N: ToName + Clone + Send, +// D: CanonicalOrd +// + RecordData +// + ComposeRecordData +// + From> +// + Send, +// K: SigningKey, +// S: Sorter + Send, +// ConcreteSecretKey: SignRaw, +// Octets: AsRef<[u8]> +// + Clone +// + From> +// + octseq::OctetsFrom>, +// { +// debug!("Signing settings: add_used_dnskeys={add_used_dnskeys}, sign_dnskeys_with_all_keys={sign_dnskeys_with_all_keys}"); + +// // Work with indices because SigningKey doesn't impl PartialEq so we +// // cannot use a HashSet to make a unique set of them. +// let mut ksk_idxs = HashSet::::new(); +// let mut zsk_idxs = HashSet::::new(); +// for (idx, key) in keys.iter().enumerate() { +// match key.purpose() { +// IntendedKeyPurpose::KSK => { +// let _ = ksk_idxs.insert(idx); +// } +// IntendedKeyPurpose::ZSK => { +// let _ = zsk_idxs.insert(idx); +// } +// _ => { /* Nothing to do */ } +// } +// } + +// // If the given KSKs (a key with SEP flag set) also have the Zone Key +// // flag set, and lacking any ZSKs (a key with ONLY the Zone Key set), +// // treat the KSKs with the Zone Key flag set as as Combined Signing +// // Keys (CSKs) and so use them to sign the zone RRs as wel as for +// // signing DNSKEY RRs. +// let rrset_signing_key_idxs: HashSet = if !zsk_idxs.is_empty() { +// zsk_idxs.iter().copied().collect() +// } else { +// keys.iter() +// .filter(|k| matches!(k.purpose(), IntendedKeyPurpose::CSK)) +// .collect() +// }; +// let mut rrset_signing_keys = vec![]; +// for idx in &rrset_signing_key_idxs { +// rrset_signing_keys.push(&keys[*idx]); +// } + +// // The set of keys to sign DNSKEY RRs with should be just the KSKs, +// // unless we've been directed to use +// let mut dnskey_signing_keys = vec![]; +// let dnskey_signing_key_idxs: HashSet = +// if sign_dnskeys_with_all_keys { +// debug!("Using all keys to sign DNSKEY RRs"); +// zsk_idxs +// .iter() +// .copied() +// .chain(ksk_idxs.iter().copied()) +// .collect() +// } else if !ksk_idxs.is_empty() { +// // Sign DNSKEY RRs using only SEP keys. +// debug!("Using only SEP keys to sign DNSKEY RRs"); +// ksk_idxs.iter().copied().collect() +// } else { +// // No SEP keys? Sign DNSKEY RRs with all non-SEP keys. +// debug!("Using only the non-SEP keys to sign DNSKEY RRs"); +// keys.iter() +// .enumerate() +// .filter_map(|(i, k)| { +// (!k.is_secure_entry_point()).then_some(i) +// }) +// .collect() +// }; +// for idx in &dnskey_signing_key_idxs { +// dnskey_signing_keys.push(&keys[*idx]); +// } + +// // Determine the total set of keys that we will use. +// let mut keys_to_use: Vec<&SigningKey> = vec![]; +// let keys_to_use_idxs: HashSet = dnskey_signing_key_idxs +// .iter() +// .copied() +// .chain(rrset_signing_key_idxs.iter().copied()) +// .collect(); +// for idx in keys_to_use_idxs { +// keys_to_use.push(&keys[idx]); +// } + +// if enabled!(Level::DEBUG) { +// fn debug_key, C: SignRaw>( +// prefix: &str, +// key: &SigningKey, +// ) { +// debug!( +// "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}))", +// key.algorithm(), +// key.owner(), +// key.flags(), +// key.is_secure_entry_point(), +// key.is_zone_signing_key(), +// ) +// } + +// debug!("# Keys: {}", keys.len()); +// debug!("# KSKs: {}", ksk_idxs.len()); +// debug!("# ZSKs: {}", zsk_idxs.len()); +// debug!("# DNSKEY RR signing keys: {}", dnskey_signing_keys.len()); +// debug!("# RRSET signing keys: {}", rrset_signing_keys.len()); + +// for key in &keys_to_use { +// debug_key("Key", key); +// } + +// for idx in ksk_idxs { +// debug_key("KSK", &keys[idx]); +// } + +// for idx in zsk_idxs { +// debug_key("ZSK", &keys[idx]); +// } + +// for key in dnskey_signing_keys.iter() { +// debug_key("DNSKEY RR signing key", key); +// } + +// for key in rrset_signing_keys.iter() { +// debug_key("RRSET signing key", key); +// } +// } + +// // Shadow the potentially larger collection of given keys with just +// // the set we intend to use, so that we can't accidentally refer to +// // the larger set below this point. +// let keys = keys_to_use; + +// let mut res: Vec>> = Vec::new(); +// let mut buf = Vec::new(); +// let mut cut: Option> = None; + +// // Since the records are ordered, the first family is the apex -- +// // we can skip everything before that. +// families.skip_before(apex); + +// let mut families = families.peekable(); + +// let apex_ttl = families.peek().unwrap().records().next().unwrap().ttl(); + +// // Make DNSKEY RRs for all keys that will be used. +// let mut dnskey_rrs_to_sign = SortedRecords::::new(); +// for public_key in keys.iter().map(|k| k.public_key()) { +// let dnskey: Dnskey = Dnskey::convert(public_key.to_dnskey()); + +// // Save the DNSKEY RR so that we can generate an RRSIG for it. +// dnskey_rrs_to_sign +// .insert(Record::new( +// apex.owner().clone(), +// apex.class(), +// apex_ttl, +// dnskey.clone().into(), +// )) +// .map_err(|_| ErrorTypeToBeDetermined)?; + +// if add_used_dnskeys { +// // Add the DNSKEY RR to the final result so that we not only +// // produce an RRSIG for it but tell the caller this is a new +// // record to include in the final zone. +// res.push(Record::new( +// apex.owner().clone(), +// apex.class(), +// apex_ttl, +// ZoneRecordData::Dnskey(dnskey), +// )); +// } +// } + +// let dummy_dnskey_rrs = SortedRecords::::new(); +// let families_iter = if add_used_dnskeys { +// dnskey_rrs_to_sign.families().chain(families) +// } else { +// dummy_dnskey_rrs.families().chain(families) +// }; + +// for family in families_iter { +// // If the owner is out of zone, we have moved out of our zone and +// // are done. +// if !family.is_in_zone(apex) { +// break; +// } + +// // If the family is below a zone cut, we must ignore it. +// if let Some(ref cut) = cut { +// if family.owner().ends_with(cut.owner()) { +// continue; +// } +// } + +// // A copy of the family name. We’ll need it later. +// let name = family.family_name().cloned(); + +// // If this family is the parent side of a zone cut, we keep the +// // family name for later. This also means below that if +// // `cut.is_some()` we are at the parent side of a zone. +// cut = if family.is_zone_cut(apex) { +// Some(name.clone()) +// } else { +// None +// }; + +// for rrset in family.rrsets() { +// if cut.is_some() { +// // If we are at a zone cut, we only sign DS and NSEC +// // records. NS records we must not sign and everything +// // else shouldn’t be here, really. +// if rrset.rtype() != Rtype::DS && rrset.rtype() != Rtype::NSEC +// { +// continue; +// } +// } else { +// // Otherwise we only ignore RRSIGs. +// if rrset.rtype() == Rtype::RRSIG { +// continue; +// } +// } + +// let signing_keys = if rrset.rtype() == Rtype::DNSKEY { +// &dnskey_signing_keys +// } else { +// &rrset_signing_keys +// }; + +// for key in signing_keys { +// let rrsig = ProtoRrsig::new( +// rrset.rtype(), +// key.algorithm(), +// name.owner().rrsig_label_count(), +// rrset.ttl(), +// expiration, +// inception, +// key.public_key().key_tag(), +// apex.owner().clone(), +// ); + +// buf.clear(); +// rrsig.compose_canonical(&mut buf).unwrap(); +// for record in rrset.iter() { +// record.compose_canonical(&mut buf).unwrap(); +// } +// let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); +// let signature = signature.as_ref().to_vec(); +// let Ok(signature) = signature.try_octets_into() else { +// return Err(ErrorTypeToBeDetermined); +// }; + +// let rrsig = +// rrsig.into_rrsig(signature).expect("long signature"); +// res.push(Record::new( +// name.owner().clone(), +// name.class(), +// rrset.ttl(), +// ZoneRecordData::Rrsig(rrsig), +// )); +// debug!( +// "Signed {} record with keytag {}", +// rrset.rtype(), +// key.public_key().key_tag() +// ); +// } +// } +// } + +// debug!("Returning {} records from signing", res.len()); + +// Ok(res) +// } From 89eb6738969c47da1221484fa64b483852cbac2a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 3 Dec 2024 22:22:57 +0100 Subject: [PATCH 230/569] Cargo fmt. --- src/sign/records.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 873184986..3baf4caff 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -3,8 +3,8 @@ use core::cmp::Ordering; use core::convert::From; use core::fmt::Display; use core::marker::PhantomData; -use core::slice::Iter; use core::ops::Deref; +use core::slice::Iter; use std::boxed::Box; use std::collections::{HashMap, HashSet}; @@ -1292,8 +1292,12 @@ impl SigningKeyUsageStrategy } } -pub struct Signer -where +pub struct Signer< + Octs, + Inner, + KeyStrat = DefaultSigningKeyUsageStrategy, + Sort = DefaultSorter, +> where Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, @@ -1301,7 +1305,8 @@ where _phantom: PhantomData<(Octs, Inner, KeyStrat, Sort)>, } -impl Default for Signer +impl Default + for Signer where Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, From af37a8eee8d107e5bab85ef5dfd1d9587860c199 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 3 Dec 2024 23:19:58 +0100 Subject: [PATCH 231/569] Delete commented out code. --- src/sign/records.rs | 293 -------------------------------------------- 1 file changed, 293 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 9b1b54adf..38f4694bd 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1489,296 +1489,3 @@ where Ok(res) } } - -// /// Sign a zone using the given keys. -// /// -// /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be -// /// added to the given records in order to DNSSEC sign them. -// /// -// /// The given records MUST be sorted according to [`CanonicalOrd`]. -// #[allow(clippy::type_complexity)] -// pub fn sign<'a, N, D, S, Octets, ConcreteSecretKey, K>( -// apex: &FamilyName, -// mut families: RecordsIter<'a, N, D>, -// expiration: Timestamp, -// inception: Timestamp, -// keys: &[DnssecSigningKey<'a, Octets, ConcreteSecretKey, K>], -// add_used_dnskeys: bool, -// sign_dnskeys_with_all_keys: bool, -// ) -> Result>>, ErrorTypeToBeDetermined> -// where -// N: ToName + Clone + Send, -// D: CanonicalOrd -// + RecordData -// + ComposeRecordData -// + From> -// + Send, -// K: SigningKey, -// S: Sorter + Send, -// ConcreteSecretKey: SignRaw, -// Octets: AsRef<[u8]> -// + Clone -// + From> -// + octseq::OctetsFrom>, -// { -// debug!("Signing settings: add_used_dnskeys={add_used_dnskeys}, sign_dnskeys_with_all_keys={sign_dnskeys_with_all_keys}"); - -// // Work with indices because SigningKey doesn't impl PartialEq so we -// // cannot use a HashSet to make a unique set of them. -// let mut ksk_idxs = HashSet::::new(); -// let mut zsk_idxs = HashSet::::new(); -// for (idx, key) in keys.iter().enumerate() { -// match key.purpose() { -// IntendedKeyPurpose::KSK => { -// let _ = ksk_idxs.insert(idx); -// } -// IntendedKeyPurpose::ZSK => { -// let _ = zsk_idxs.insert(idx); -// } -// _ => { /* Nothing to do */ } -// } -// } - -// // If the given KSKs (a key with SEP flag set) also have the Zone Key -// // flag set, and lacking any ZSKs (a key with ONLY the Zone Key set), -// // treat the KSKs with the Zone Key flag set as as Combined Signing -// // Keys (CSKs) and so use them to sign the zone RRs as wel as for -// // signing DNSKEY RRs. -// let rrset_signing_key_idxs: HashSet = if !zsk_idxs.is_empty() { -// zsk_idxs.iter().copied().collect() -// } else { -// keys.iter() -// .filter(|k| matches!(k.purpose(), IntendedKeyPurpose::CSK)) -// .collect() -// }; -// let mut rrset_signing_keys = vec![]; -// for idx in &rrset_signing_key_idxs { -// rrset_signing_keys.push(&keys[*idx]); -// } - -// // The set of keys to sign DNSKEY RRs with should be just the KSKs, -// // unless we've been directed to use -// let mut dnskey_signing_keys = vec![]; -// let dnskey_signing_key_idxs: HashSet = -// if sign_dnskeys_with_all_keys { -// debug!("Using all keys to sign DNSKEY RRs"); -// zsk_idxs -// .iter() -// .copied() -// .chain(ksk_idxs.iter().copied()) -// .collect() -// } else if !ksk_idxs.is_empty() { -// // Sign DNSKEY RRs using only SEP keys. -// debug!("Using only SEP keys to sign DNSKEY RRs"); -// ksk_idxs.iter().copied().collect() -// } else { -// // No SEP keys? Sign DNSKEY RRs with all non-SEP keys. -// debug!("Using only the non-SEP keys to sign DNSKEY RRs"); -// keys.iter() -// .enumerate() -// .filter_map(|(i, k)| { -// (!k.is_secure_entry_point()).then_some(i) -// }) -// .collect() -// }; -// for idx in &dnskey_signing_key_idxs { -// dnskey_signing_keys.push(&keys[*idx]); -// } - -// // Determine the total set of keys that we will use. -// let mut keys_to_use: Vec<&SigningKey> = vec![]; -// let keys_to_use_idxs: HashSet = dnskey_signing_key_idxs -// .iter() -// .copied() -// .chain(rrset_signing_key_idxs.iter().copied()) -// .collect(); -// for idx in keys_to_use_idxs { -// keys_to_use.push(&keys[idx]); -// } - -// if enabled!(Level::DEBUG) { -// fn debug_key, C: SignRaw>( -// prefix: &str, -// key: &SigningKey, -// ) { -// debug!( -// "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}))", -// key.algorithm(), -// key.owner(), -// key.flags(), -// key.is_secure_entry_point(), -// key.is_zone_signing_key(), -// ) -// } - -// debug!("# Keys: {}", keys.len()); -// debug!("# KSKs: {}", ksk_idxs.len()); -// debug!("# ZSKs: {}", zsk_idxs.len()); -// debug!("# DNSKEY RR signing keys: {}", dnskey_signing_keys.len()); -// debug!("# RRSET signing keys: {}", rrset_signing_keys.len()); - -// for key in &keys_to_use { -// debug_key("Key", key); -// } - -// for idx in ksk_idxs { -// debug_key("KSK", &keys[idx]); -// } - -// for idx in zsk_idxs { -// debug_key("ZSK", &keys[idx]); -// } - -// for key in dnskey_signing_keys.iter() { -// debug_key("DNSKEY RR signing key", key); -// } - -// for key in rrset_signing_keys.iter() { -// debug_key("RRSET signing key", key); -// } -// } - -// // Shadow the potentially larger collection of given keys with just -// // the set we intend to use, so that we can't accidentally refer to -// // the larger set below this point. -// let keys = keys_to_use; - -// let mut res: Vec>> = Vec::new(); -// let mut buf = Vec::new(); -// let mut cut: Option> = None; - -// // Since the records are ordered, the first family is the apex -- -// // we can skip everything before that. -// families.skip_before(apex); - -// let mut families = families.peekable(); - -// let apex_ttl = families.peek().unwrap().records().next().unwrap().ttl(); - -// // Make DNSKEY RRs for all keys that will be used. -// let mut dnskey_rrs_to_sign = SortedRecords::::new(); -// for public_key in keys.iter().map(|k| k.public_key()) { -// let dnskey: Dnskey = Dnskey::convert(public_key.to_dnskey()); - -// // Save the DNSKEY RR so that we can generate an RRSIG for it. -// dnskey_rrs_to_sign -// .insert(Record::new( -// apex.owner().clone(), -// apex.class(), -// apex_ttl, -// dnskey.clone().into(), -// )) -// .map_err(|_| ErrorTypeToBeDetermined)?; - -// if add_used_dnskeys { -// // Add the DNSKEY RR to the final result so that we not only -// // produce an RRSIG for it but tell the caller this is a new -// // record to include in the final zone. -// res.push(Record::new( -// apex.owner().clone(), -// apex.class(), -// apex_ttl, -// ZoneRecordData::Dnskey(dnskey), -// )); -// } -// } - -// let dummy_dnskey_rrs = SortedRecords::::new(); -// let families_iter = if add_used_dnskeys { -// dnskey_rrs_to_sign.families().chain(families) -// } else { -// dummy_dnskey_rrs.families().chain(families) -// }; - -// for family in families_iter { -// // If the owner is out of zone, we have moved out of our zone and -// // are done. -// if !family.is_in_zone(apex) { -// break; -// } - -// // If the family is below a zone cut, we must ignore it. -// if let Some(ref cut) = cut { -// if family.owner().ends_with(cut.owner()) { -// continue; -// } -// } - -// // A copy of the family name. We’ll need it later. -// let name = family.family_name().cloned(); - -// // If this family is the parent side of a zone cut, we keep the -// // family name for later. This also means below that if -// // `cut.is_some()` we are at the parent side of a zone. -// cut = if family.is_zone_cut(apex) { -// Some(name.clone()) -// } else { -// None -// }; - -// for rrset in family.rrsets() { -// if cut.is_some() { -// // If we are at a zone cut, we only sign DS and NSEC -// // records. NS records we must not sign and everything -// // else shouldn’t be here, really. -// if rrset.rtype() != Rtype::DS && rrset.rtype() != Rtype::NSEC -// { -// continue; -// } -// } else { -// // Otherwise we only ignore RRSIGs. -// if rrset.rtype() == Rtype::RRSIG { -// continue; -// } -// } - -// let signing_keys = if rrset.rtype() == Rtype::DNSKEY { -// &dnskey_signing_keys -// } else { -// &rrset_signing_keys -// }; - -// for key in signing_keys { -// let rrsig = ProtoRrsig::new( -// rrset.rtype(), -// key.algorithm(), -// name.owner().rrsig_label_count(), -// rrset.ttl(), -// expiration, -// inception, -// key.public_key().key_tag(), -// apex.owner().clone(), -// ); - -// buf.clear(); -// rrsig.compose_canonical(&mut buf).unwrap(); -// for record in rrset.iter() { -// record.compose_canonical(&mut buf).unwrap(); -// } -// let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); -// let signature = signature.as_ref().to_vec(); -// let Ok(signature) = signature.try_octets_into() else { -// return Err(ErrorTypeToBeDetermined); -// }; - -// let rrsig = -// rrsig.into_rrsig(signature).expect("long signature"); -// res.push(Record::new( -// name.owner().clone(), -// name.class(), -// rrset.ttl(), -// ZoneRecordData::Rrsig(rrsig), -// )); -// debug!( -// "Signed {} record with keytag {}", -// rrset.rtype(), -// key.public_key().key_tag() -// ); -// } -// } -// } - -// debug!("Returning {} records from signing", res.len()); - -// Ok(res) -// } From ab9b2193853eae60d32f16bcb0f589128c00540d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 3 Dec 2024 23:45:01 +0100 Subject: [PATCH 232/569] Revert tabbed output changes in preparation to use the PR #446 approach instead. --- src/base/dig_printer.rs | 4 ++-- src/base/zonefile_fmt.rs | 43 +++++++++++++++------------------------- src/rdata/nsec3.rs | 4 ++-- src/validate.rs | 2 +- 4 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/base/dig_printer.rs b/src/base/dig_printer.rs index 69d7545a9..3983f05fa 100644 --- a/src/base/dig_printer.rs +++ b/src/base/dig_printer.rs @@ -24,7 +24,7 @@ impl> fmt::Display for DigPrinter<'_, Octs> { writeln!( f, ";; ->>HEADER<<- opcode: {}, rcode: {}, id: {}", - header.opcode().display_zonefile(false, false), + header.opcode().display_zonefile(false), header.rcode(), header.id() )?; @@ -161,7 +161,7 @@ fn write_record_item( let parsed = item.to_any_record::>(); match parsed { - Ok(item) => writeln!(f, "{}", item.display_zonefile(false, false)), + Ok(item) => writeln!(f, "{}", item.display_zonefile(false)), Err(_) => writeln!( f, "; {} {} {} {} ", diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index dd7860586..8a9e22e75 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -13,19 +13,18 @@ pub type Result = core::result::Result<(), Error>; pub struct ZoneFileDisplay<'a, T: ?Sized> { inner: &'a T, - multiline: bool, - tabbed: bool, + pretty: bool, } impl fmt::Display for ZoneFileDisplay<'_, T> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if self.multiline { + if self.pretty { self.inner .fmt(&mut MultiLineWriter::new(f)) .map_err(|_| fmt::Error) } else { self.inner - .fmt(&mut SimpleWriter::new(f, self.tabbed)) + .fmt(&mut SimpleWriter::new(f)) .map_err(|_| fmt::Error) } } @@ -42,15 +41,10 @@ pub trait ZonefileFmt { /// /// The returned object will be displayed as zonefile when printed or /// written using `fmt::Display`. - fn display_zonefile( - &self, - multiline: bool, - tabbed: bool, - ) -> ZoneFileDisplay<'_, Self> { + fn display_zonefile(&self, pretty: bool) -> ZoneFileDisplay<'_, Self> { ZoneFileDisplay { inner: self, - multiline, - tabbed, + pretty, } } } @@ -94,15 +88,13 @@ pub trait FormatWriter: Sized { struct SimpleWriter { first: bool, writer: W, - tabbed: bool, } impl SimpleWriter { - fn new(writer: W, tabbed: bool) -> Self { + fn new(writer: W) -> Self { Self { first: true, writer, - tabbed, } } } @@ -110,10 +102,7 @@ impl SimpleWriter { impl FormatWriter for SimpleWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - match self.tabbed { - true => self.writer.write_char('\t')?, - false => self.writer.write_char(' ')?, - } + self.writer.write_char(' ')?; } self.first = false; self.writer.write_fmt(args)?; @@ -262,7 +251,7 @@ mod test { let record = create_record(A::new("128.140.76.106".parse().unwrap())); assert_eq!( "example.com. 3600 IN A 128.140.76.106", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -273,7 +262,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN CNAME example.com.", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -290,7 +279,7 @@ mod test { ); assert_eq!( "example.com. 3600 IN DS 5414 15 2 DEADBEEF", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); assert_eq!( [ @@ -300,7 +289,7 @@ mod test { " DEADBEEF )", ] .join("\n"), - record.display_zonefile(true, false).to_string() + record.display_zonefile(true).to_string() ); } @@ -317,7 +306,7 @@ mod test { ); assert_eq!( "example.com. 3600 IN CDS 5414 15 2 DEADBEEF", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -329,7 +318,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN MX 20 example.com.", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -349,7 +338,7 @@ mod test { more like a silly monkey with a typewriter accidentally writing \ some shakespeare along the way but it feels like I have to type \ e\" \"ven longer to hit that limit!\"", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -362,7 +351,7 @@ mod test { )); assert_eq!( "example.com. 3600 IN HINFO \"Windows\" \"Windows Server\"", - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } @@ -379,7 +368,7 @@ mod test { )); assert_eq!( r#"example.com. 3600 IN NAPTR 100 50 "a" "z3950+N2L+N2C" "!^urn:cid:.+@([^\\.]+\\.)(.*)$!\\2!i" cidserver.example.com."#, - record.display_zonefile(false, false).to_string() + record.display_zonefile(false).to_string() ); } } diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index f57f48166..d80f12b9b 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -1608,7 +1608,7 @@ mod test { Nsec3::scan, &rdata, ); - assert_eq!(&format!("{}", rdata.display_zonefile(false, false)), "1 10 11 626172 CPNMU A SRV"); + assert_eq!(&format!("{}", rdata.display_zonefile(false)), "1 10 11 626172 CPNMU A SRV"); } #[test] @@ -1632,7 +1632,7 @@ mod test { Nsec3::scan, &rdata, ); - assert_eq!(&format!("{}", rdata.display_zonefile(false, false)), "1 10 11 - CPNMU A SRV"); + assert_eq!(&format!("{}", rdata.display_zonefile(false)), "1 10 11 - CPNMU A SRV"); } #[test] diff --git a/src/validate.rs b/src/validate.rs index ce13f2ce3..84864c123 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -322,7 +322,7 @@ impl> Key { w, "{} IN DNSKEY {}", self.owner().fmt_with_dot(), - self.to_dnskey().display_zonefile(false, false), + self.to_dnskey().display_zonefile(false), ) } From 623f491a625f52b689759852efc6719309eb21bf Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 5 Dec 2024 10:51:43 +0100 Subject: [PATCH 233/569] Adjust key usage strategy to support LDNS default behaviour of use ZSKs if no KSKs found. --- src/sign/records.rs | 68 +++++++++++++++++++++------------------------ 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 38f4694bd..046e8f52d 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1182,26 +1182,36 @@ impl, Inner: SignRaw> DnssecSigningKey { pub trait SigningKeyUsageStrategy { const NAME: &'static str; - fn new() -> Self; - - fn filter_ksks( - &mut self, - candidate_key: &DnssecSigningKey, - ) -> bool { - matches!( - candidate_key.purpose(), - IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK - ) - } - - fn filter_zsks( - &mut self, - candidate_key: &DnssecSigningKey, - ) -> bool { - matches!( - candidate_key.purpose(), - IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK - ) + fn select_ksks( + candidate_keys: &[DnssecSigningKey], + ) -> HashSet { + candidate_keys + .iter() + .enumerate() + .filter_map(|(i, k)| { + matches!( + k.purpose(), + IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK + ) + .then_some(i) + }) + .collect::>() + } + + fn select_zsks( + candidate_keys: &[DnssecSigningKey], + ) -> HashSet { + candidate_keys + .iter() + .enumerate() + .filter_map(|(i, k)| { + matches!( + k.purpose(), + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + .then_some(i) + }) + .collect::>() } } @@ -1211,10 +1221,6 @@ impl SigningKeyUsageStrategy for DefaultSigningKeyUsageStrategy { const NAME: &'static str = "Default key usage strategy"; - - fn new() -> Self { - Self - } } pub struct Signer @@ -1280,19 +1286,9 @@ where // Work with indices because SigningKey doesn't impl PartialEq so we // cannot use a HashSet to make a unique set of them. - let mut key_filter = KeyStrat::new(); - - let dnskey_signing_key_idxs: HashSet = keys - .iter() - .enumerate() - .filter_map(|(i, k)| key_filter.filter_ksks(k).then_some(i)) - .collect(); + let dnskey_signing_key_idxs = KeyStrat::select_ksks(keys); - let rrset_signing_key_idxs: HashSet = keys - .iter() - .enumerate() - .filter_map(|(i, k)| key_filter.filter_zsks(k).then_some(i)) - .collect(); + let rrset_signing_key_idxs = KeyStrat::select_zsks(keys); let keys_in_use_idxs: HashSet<_> = rrset_signing_key_idxs .iter() From 8c2b140ac5760e3b8eed1cb6a4effd1bd01cab88 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 5 Dec 2024 12:57:36 +0100 Subject: [PATCH 234/569] Rename strategy fns to refer to what they are selecting more accurately. --- src/sign/records.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 046e8f52d..e583dae26 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1182,7 +1182,7 @@ impl, Inner: SignRaw> DnssecSigningKey { pub trait SigningKeyUsageStrategy { const NAME: &'static str; - fn select_ksks( + fn select_dnskey_signing_keys( candidate_keys: &[DnssecSigningKey], ) -> HashSet { candidate_keys @@ -1198,7 +1198,7 @@ pub trait SigningKeyUsageStrategy { .collect::>() } - fn select_zsks( + fn select_non_dnskey_signing_keys( candidate_keys: &[DnssecSigningKey], ) -> HashSet { candidate_keys @@ -1286,9 +1286,11 @@ where // Work with indices because SigningKey doesn't impl PartialEq so we // cannot use a HashSet to make a unique set of them. - let dnskey_signing_key_idxs = KeyStrat::select_ksks(keys); + let dnskey_signing_key_idxs = + KeyStrat::select_dnskey_signing_keys(keys); - let rrset_signing_key_idxs = KeyStrat::select_zsks(keys); + let rrset_signing_key_idxs = + KeyStrat::select_non_dnskey_signing_keys(keys); let keys_in_use_idxs: HashSet<_> = rrset_signing_key_idxs .iter() From bc68b0ba90b2bd99bd162fed7315072f07fbb4cd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 12:49:39 +0100 Subject: [PATCH 235/569] Make key selection more flexible. (#464) E.g. LDNS seems to consider DS and CDS and CDNSKEY resource record types as well as DNSKEY when selecting keys. --- src/sign/records.rs | 59 ++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index e583dae26..984d5f864 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1182,35 +1182,35 @@ impl, Inner: SignRaw> DnssecSigningKey { pub trait SigningKeyUsageStrategy { const NAME: &'static str; - fn select_dnskey_signing_keys( + fn select_signing_keys_for_rtype( candidate_keys: &[DnssecSigningKey], + rtype: Option, ) -> HashSet { - candidate_keys - .iter() - .enumerate() - .filter_map(|(i, k)| { + match rtype { + Some(Rtype::DNSKEY) => Self::filter_keys(candidate_keys, |k| { matches!( k.purpose(), IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK ) - .then_some(i) - }) - .collect::>() + }), + + _ => Self::filter_keys(candidate_keys, |k| { + matches!( + k.purpose(), + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + }), + } } - fn select_non_dnskey_signing_keys( + fn filter_keys( candidate_keys: &[DnssecSigningKey], + filter: fn(&DnssecSigningKey) -> bool, ) -> HashSet { candidate_keys .iter() .enumerate() - .filter_map(|(i, k)| { - matches!( - k.purpose(), - IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK - ) - .then_some(i) - }) + .filter_map(|(i, k)| filter(k).then_some(i)) .collect::>() } } @@ -1286,13 +1286,15 @@ where // Work with indices because SigningKey doesn't impl PartialEq so we // cannot use a HashSet to make a unique set of them. - let dnskey_signing_key_idxs = - KeyStrat::select_dnskey_signing_keys(keys); + let dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype( + keys, + Some(Rtype::DNSKEY), + ); - let rrset_signing_key_idxs = - KeyStrat::select_non_dnskey_signing_keys(keys); + let non_dnskey_signing_key_idxs = + KeyStrat::select_signing_keys_for_rtype(keys, None); - let keys_in_use_idxs: HashSet<_> = rrset_signing_key_idxs + let keys_in_use_idxs: HashSet<_> = non_dnskey_signing_key_idxs .iter() .chain(dnskey_signing_key_idxs.iter()) .collect(); @@ -1320,18 +1322,21 @@ where "# DNSKEY RR signing keys: {}", dnskey_signing_key_idxs.len() ); - debug!("# RRSET signing keys: {}", rrset_signing_key_idxs.len()); + debug!( + "# Non-DNSKEY RR signing keys: {}", + non_dnskey_signing_key_idxs.len() + ); for idx in &keys_in_use_idxs { debug_key("Key", keys[**idx].key()); } - for idx in &rrset_signing_key_idxs { - debug_key("RRSET Signing Key", keys[*idx].key()); + for idx in &dnskey_signing_key_idxs { + debug_key("DNSKEY RR signing key", keys[*idx].key()); } - for idx in &dnskey_signing_key_idxs { - debug_key("DNSKEY Signing Key", keys[*idx].key()); + for idx in &non_dnskey_signing_key_idxs { + debug_key("Non-DNSKEY RR signing key", keys[*idx].key()); } } @@ -1432,7 +1437,7 @@ where let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { &dnskey_signing_key_idxs } else { - &rrset_signing_key_idxs + &non_dnskey_signing_key_idxs }; for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) From 15b72c065a91df5a3e9320fcbe9eeaca094d2713 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:22:03 +0100 Subject: [PATCH 236/569] Add a tab before the RDATA as well as within it, to match LDNS tabbed output format. (#463) --- src/base/zonefile_fmt.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index b48e48e1f..8902d7cf3 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -145,6 +145,7 @@ impl FormatWriter for SimpleWriter { /// A single line writer that puts tabs between ungrouped tokens struct TabbedWriter { first: bool, + first_block: bool, blocks: usize, writer: W, } @@ -153,6 +154,7 @@ impl TabbedWriter { fn new(writer: W) -> Self { Self { first: true, + first_block: true, blocks: 0, writer, } @@ -162,7 +164,14 @@ impl TabbedWriter { impl FormatWriter for TabbedWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - let c = if self.blocks == 0 { '\t' } else { ' ' }; + let c = if self.blocks == 0 { + '\t' + } else if self.first_block { + self.first_block = false; + '\t' + } else { + ' ' + }; self.writer.write_char(c)?; } self.first = false; @@ -439,7 +448,7 @@ mod test { } #[test] - fn aligned() { + fn tabbed() { let record = create_record( Cds::new( 5414, @@ -453,7 +462,7 @@ mod test { // The name, ttl, class and rtype should be separated by \t, but the // rdata shouldn't. assert_eq!( - "example.com.\t3600\tIN\tCDS 5414 15 2 DEADBEEF", + "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF", record.display_zonefile(DisplayKind::Tabbed).to_string() ); } From 5c23fdb1efe296b69f2f02c24823a01857ea140a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:19 +0100 Subject: [PATCH 237/569] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 0845dde68..aabf4b9a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446]) +* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], + [#463]) Bug fixes @@ -48,6 +49,7 @@ Other changes [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 +[#463]: https://github.com/NLnetLabs/domain/pull/463 [@weilence]: https://github.com/weilence ## 0.10.3 From c141bf990900a42a2648c47851f5fa8d11c31a32 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:22:03 +0100 Subject: [PATCH 238/569] Add a tab before the RDATA as well as within it, to match LDNS tabbed output format. (#463) --- src/base/zonefile_fmt.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index b48e48e1f..8902d7cf3 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -145,6 +145,7 @@ impl FormatWriter for SimpleWriter { /// A single line writer that puts tabs between ungrouped tokens struct TabbedWriter { first: bool, + first_block: bool, blocks: usize, writer: W, } @@ -153,6 +154,7 @@ impl TabbedWriter { fn new(writer: W) -> Self { Self { first: true, + first_block: true, blocks: 0, writer, } @@ -162,7 +164,14 @@ impl TabbedWriter { impl FormatWriter for TabbedWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - let c = if self.blocks == 0 { '\t' } else { ' ' }; + let c = if self.blocks == 0 { + '\t' + } else if self.first_block { + self.first_block = false; + '\t' + } else { + ' ' + }; self.writer.write_char(c)?; } self.first = false; @@ -439,7 +448,7 @@ mod test { } #[test] - fn aligned() { + fn tabbed() { let record = create_record( Cds::new( 5414, @@ -453,7 +462,7 @@ mod test { // The name, ttl, class and rtype should be separated by \t, but the // rdata shouldn't. assert_eq!( - "example.com.\t3600\tIN\tCDS 5414 15 2 DEADBEEF", + "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF", record.display_zonefile(DisplayKind::Tabbed).to_string() ); } From 660d2f245667b463b7b625e918184ea70fad1ee0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:19 +0100 Subject: [PATCH 239/569] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 0845dde68..aabf4b9a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446]) +* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], + [#463]) Bug fixes @@ -48,6 +49,7 @@ Other changes [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 +[#463]: https://github.com/NLnetLabs/domain/pull/463 [@weilence]: https://github.com/weilence ## 0.10.3 From 8c583b527c469646d8e0a173242e36ebf20ebb0a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:22:03 +0100 Subject: [PATCH 240/569] Add a tab before the RDATA as well as within it, to match LDNS tabbed output format. (#463) --- src/base/zonefile_fmt.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index b48e48e1f..8902d7cf3 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -145,6 +145,7 @@ impl FormatWriter for SimpleWriter { /// A single line writer that puts tabs between ungrouped tokens struct TabbedWriter { first: bool, + first_block: bool, blocks: usize, writer: W, } @@ -153,6 +154,7 @@ impl TabbedWriter { fn new(writer: W) -> Self { Self { first: true, + first_block: true, blocks: 0, writer, } @@ -162,7 +164,14 @@ impl TabbedWriter { impl FormatWriter for TabbedWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - let c = if self.blocks == 0 { '\t' } else { ' ' }; + let c = if self.blocks == 0 { + '\t' + } else if self.first_block { + self.first_block = false; + '\t' + } else { + ' ' + }; self.writer.write_char(c)?; } self.first = false; @@ -439,7 +448,7 @@ mod test { } #[test] - fn aligned() { + fn tabbed() { let record = create_record( Cds::new( 5414, @@ -453,7 +462,7 @@ mod test { // The name, ttl, class and rtype should be separated by \t, but the // rdata shouldn't. assert_eq!( - "example.com.\t3600\tIN\tCDS 5414 15 2 DEADBEEF", + "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF", record.display_zonefile(DisplayKind::Tabbed).to_string() ); } From 8f97bd3ea3cd7f3caeef8f7230764f6b7ea70129 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:19 +0100 Subject: [PATCH 241/569] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 0845dde68..aabf4b9a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446]) +* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], + [#463]) Bug fixes @@ -48,6 +49,7 @@ Other changes [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 +[#463]: https://github.com/NLnetLabs/domain/pull/463 [@weilence]: https://github.com/weilence ## 0.10.3 From 85ffaf745306fdd405a36f619ecb5593bc6b1bb6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 14:22:03 +0100 Subject: [PATCH 242/569] Add a tab before the RDATA as well as within it, to match LDNS tabbed output format. (#463) --- src/base/zonefile_fmt.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index b48e48e1f..8902d7cf3 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -145,6 +145,7 @@ impl FormatWriter for SimpleWriter { /// A single line writer that puts tabs between ungrouped tokens struct TabbedWriter { first: bool, + first_block: bool, blocks: usize, writer: W, } @@ -153,6 +154,7 @@ impl TabbedWriter { fn new(writer: W) -> Self { Self { first: true, + first_block: true, blocks: 0, writer, } @@ -162,7 +164,14 @@ impl TabbedWriter { impl FormatWriter for TabbedWriter { fn fmt_token(&mut self, args: fmt::Arguments<'_>) -> Result { if !self.first { - let c = if self.blocks == 0 { '\t' } else { ' ' }; + let c = if self.blocks == 0 { + '\t' + } else if self.first_block { + self.first_block = false; + '\t' + } else { + ' ' + }; self.writer.write_char(c)?; } self.first = false; @@ -439,7 +448,7 @@ mod test { } #[test] - fn aligned() { + fn tabbed() { let record = create_record( Cds::new( 5414, @@ -453,7 +462,7 @@ mod test { // The name, ttl, class and rtype should be separated by \t, but the // rdata shouldn't. assert_eq!( - "example.com.\t3600\tIN\tCDS 5414 15 2 DEADBEEF", + "example.com.\t3600\tIN\tCDS\t5414 15 2 DEADBEEF", record.display_zonefile(DisplayKind::Tabbed).to_string() ); } From 254dc9c958206422b70380469dfb362552ef1aff Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Dec 2024 15:27:19 +0100 Subject: [PATCH 243/569] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 0845dde68..aabf4b9a0 100644 --- a/Changelog.md +++ b/Changelog.md @@ -17,7 +17,8 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446]) +* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], + [#463]) Bug fixes @@ -48,6 +49,7 @@ Other changes [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 +[#463]: https://github.com/NLnetLabs/domain/pull/463 [@weilence]: https://github.com/weilence ## 0.10.3 From 235953186104356babaf4266250d852aa60663b7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:21:58 +0100 Subject: [PATCH 244/569] Raise errors instead of unwrapping on missing apex. --- src/sign/records.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 984d5f864..efbf25874 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -976,6 +976,8 @@ pub enum SigningError { KeyLacksSignatureValidityPeriod, DuplicateDnskey, OutOfMemory, + MissingApex, + EmptyApex, } //------------ Nsec3OptOut --------------------------------------------------- @@ -1350,8 +1352,13 @@ where let mut families = families.peekable(); - let apex_ttl = - families.peek().unwrap().records().next().unwrap().ttl(); + let apex_ttl = families + .peek() + .ok_or(SigningError::MissingApex)? + .records() + .next() + .ok_or(SigningError::EmptyApex)? + .ttl(); // Make DNSKEY RRs for all keys that will be used. let mut dnskey_rrs_to_sign = SortedRecords::::new(); From f788ba598aedba7c1491ba269ac17c7ee3e09706 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:22:27 +0100 Subject: [PATCH 245/569] Add a logging related TODO. --- src/sign/records.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index efbf25874..fbfe2a56d 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1301,6 +1301,8 @@ where .chain(dnskey_signing_key_idxs.iter()) .collect(); + // TODO: use log::log_enabled instead. + // See: https://github.com/NLnetLabs/domain/pull/465 if enabled!(Level::DEBUG) { fn debug_key, Inner: SignRaw>( prefix: &str, From dc79547c3e6ae9c240a124714feef471aa8e54c4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:22:50 +0100 Subject: [PATCH 246/569] Also log the key tag when debug logging the keys to use for signing. --- src/sign/records.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index fbfe2a56d..590823def 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1309,7 +1309,7 @@ where key: &SigningKey, ) { debug!( - "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}))", + "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}, Key Tag={}))", key.algorithm() .to_mnemonic_str() .map(|alg| format!("{alg} ({})", key.algorithm())) @@ -1318,6 +1318,7 @@ where key.flags(), key.is_secure_entry_point(), key.is_zone_signing_key(), + key.public_key().key_tag(), ) } From 02f64a456b9ca9e44707558a5c9e55c21bd5c30e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:24:36 +0100 Subject: [PATCH 247/569] Don't emit duplicate DNSKEY RRs for zonefiles that already contain the DNSKEY that matches one of the keys to sign the zone with. Also, only sign DNSKEY RRs with key signing keys if the DNSKEY RR is at the apex. --- src/sign/records.rs | 200 ++++++++++++++++++++++++++------------------ 1 file changed, 120 insertions(+), 80 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 590823def..38dcf96e1 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -828,6 +828,10 @@ impl<'a, N, D> Rrset<'a, N, D> { pub fn iter(&self) -> slice::Iter<'a, Record> { self.slice.iter() } + + pub fn into_inner(self) -> &'a [Record] { + self.slice + } } //------------ RecordsIter --------------------------------------------------- @@ -1276,11 +1280,13 @@ where add_used_dnskeys: bool, ) -> Result>>, SigningError> where - N: ToName + Clone + Send, + N: ToName + Clone + PartialEq + Send, D: RecordData + + Clone + ComposeRecordData + From> + CanonicalOrd + + PartialEq + Send, { debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); @@ -1363,45 +1369,68 @@ where .ok_or(SigningError::EmptyApex)? .ttl(); - // Make DNSKEY RRs for all keys that will be used. - let mut dnskey_rrs_to_sign = SortedRecords::::new(); - for public_key in keys_in_use_idxs - .iter() - .map(|&&idx| keys[idx].key().public_key()) + // Sign the apex + // SAFETY: We just checked above if the apex records existed. + let apex_family = families.next().unwrap(); + let mut dnskey_rrs = vec![]; + for rrset in apex_family + .rrsets() + .filter(|rrset| rrset.rtype() != Rtype::RRSIG) { - let dnskey = public_key.to_dnskey(); - - // Save the DNSKEY RR so that we can generate an RRSIG for it. - dnskey_rrs_to_sign - .insert(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - Dnskey::convert(dnskey.clone()).into(), - )) - .map_err(|_| SigningError::DuplicateDnskey)?; - - if add_used_dnskeys { - // Add the DNSKEY RR to the final result so that we not only - // produce an RRSIG for it but tell the caller this is a new - // record to include in the final zone. - res.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - Dnskey::convert(dnskey).into(), - )); + // If this is the apex DNSKEY RRSET, merge in the DNSKEYs of the + // keys we intend to sign with. + let (signing_key_idxs, rrset) = if rrset.rtype() == Rtype::DNSKEY + { + dnskey_rrs.extend_from_slice(rrset.into_inner()); + + // Make DNSKEY RRs for all new keys that will be used. + for public_key in keys_in_use_idxs + .iter() + .map(|&&idx| keys[idx].key().public_key()) + { + let dnskey = public_key.to_dnskey(); + + let dnskey_rr = Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey.clone()).into(), + ); + + if !dnskey_rrs.contains(&dnskey_rr) { + if add_used_dnskeys { + res.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey).into(), + )); + } + dnskey_rrs.push(dnskey_rr); + } + } + (&dnskey_signing_key_idxs, Rrset::new(&dnskey_rrs)) + } else { + (&non_dnskey_signing_key_idxs, rrset) + }; + + for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) { + // A copy of the family name. We’ll need it later. + let name = apex_family.family_name().cloned(); + + let rrsig_rr = + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRSET at the zone apex with keytag {}", + rrset.rtype(), + key.public_key().key_tag() + ); } } - let dummy_dnskey_rrs = SortedRecords::::new(); - let families_iter = if add_used_dnskeys { - dnskey_rrs_to_sign.families().chain(families) - } else { - dummy_dnskey_rrs.families().chain(families) - }; - - for family in families_iter { + // For all RRSETs below the apex + for family in families { // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { @@ -1444,52 +1473,15 @@ where } } - let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { - &dnskey_signing_key_idxs - } else { - &non_dnskey_signing_key_idxs - }; - - for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) + for key in non_dnskey_signing_key_idxs + .iter() + .map(|&idx| keys[idx].key()) { - let (inception, expiration) = key - .signature_validity_period() - .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? - .into_inner(); - - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm(), - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.public_key().key_tag(), - apex.owner().clone(), - ); - - buf.clear(); - rrsig.compose_canonical(&mut buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(&mut buf).unwrap(); - } - let signature = - key.raw_secret_key().sign_raw(&buf).unwrap(); - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(SigningError::OutOfMemory); - }; - - let rrsig = - rrsig.into_rrsig(signature).expect("long signature"); - res.push(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - ZoneRecordData::Rrsig(rrsig), - )); + let rrsig_rr = + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); debug!( - "Signed {} record with keytag {}", + "Signed {} RRSET with keytag {}", rrset.rtype(), key.public_key().key_tag() ); @@ -1501,4 +1493,52 @@ where Ok(res) } + + fn sign_rrset( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + name: &FamilyName, + apex: &FamilyName, + buf: &mut Vec, + ) -> Result>, SigningError> + where + N: ToName + Clone + Send, + D: RecordData + + ComposeRecordData + + From> + + CanonicalOrd + + Send, + { + let (inception, expiration) = key + .signature_validity_period() + .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? + .into_inner(); + let rrsig = ProtoRrsig::new( + rrset.rtype(), + key.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + key.public_key().key_tag(), + apex.owner().clone(), + ); + buf.clear(); + rrsig.compose_canonical(buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(buf).unwrap(); + } + let signature = key.raw_secret_key().sign_raw(&*buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + Ok(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + ZoneRecordData::Rrsig(rrsig), + )) + } } From 68d714194505b8fe3b48217347db4701e4e39906 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 8 Dec 2024 00:29:16 +0100 Subject: [PATCH 248/569] FIX: When extending SortedRecords, don't permit duplicate RRs to creep in, as insert() would have prevented these. --- src/sign/records.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index dcd44c011..4c3d140b1 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -715,14 +715,15 @@ where impl Extend> for SortedRecords where - N: ToName, - D: RecordData + CanonicalOrd, + N: ToName + PartialEq, + D: RecordData + CanonicalOrd + PartialEq, { fn extend>>(&mut self, iter: T) { for item in iter { self.records.push(item); } S::sort_by(&mut self.records, CanonicalOrd::canonical_cmp); + self.records.dedup(); } } From 9c1cd42102882386e3b12aae62fc576d0ef1c176 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:26:10 +0100 Subject: [PATCH 249/569] Don't attempt to sign a zone or select keys to use if no keys are provided or found to be suitable. --- src/sign/records.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 38dcf96e1..2ea506f1e 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -978,10 +978,22 @@ where pub enum SigningError { /// One or more keys does not have a signature validity period defined. KeyLacksSignatureValidityPeriod, - DuplicateDnskey, + + /// TODO OutOfMemory, + + /// TODO MissingApex, + + /// TODO EmptyApex, + + /// At least one key must be provided to sign with. + NoKeysProvided, + + /// None of the provided keys were deemed suitable by the + /// [`SigningKeyUsageStrategy`] used. + NoSuitableKeysFound, } //------------ Nsec3OptOut --------------------------------------------------- @@ -1291,6 +1303,10 @@ where { debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); + if keys.is_empty() { + return Err(SigningError::NoKeysProvided); + } + // Work with indices because SigningKey doesn't impl PartialEq so we // cannot use a HashSet to make a unique set of them. @@ -1307,6 +1323,10 @@ where .chain(dnskey_signing_key_idxs.iter()) .collect(); + if keys_in_use_idxs.is_empty() { + return Err(SigningError::NoSuitableKeysFound); + } + // TODO: use log::log_enabled instead. // See: https://github.com/NLnetLabs/domain/pull/465 if enabled!(Level::DEBUG) { From 99d4fcc3d353f3824b6ecdee3c099b547208ac69 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:18:52 +0100 Subject: [PATCH 250/569] Improve signing keys debug output. --- src/sign/records.rs | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 2ea506f1e..4b6af87e3 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1335,7 +1335,7 @@ where key: &SigningKey, ) { debug!( - "{prefix}: {}, owner={}, flags={} (SEP={}, ZSK={}, Key Tag={}))", + "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", key.algorithm() .to_mnemonic_str() .map(|alg| format!("{alg} ({})", key.algorithm())) @@ -1348,26 +1348,30 @@ where ) } - debug!("# Keys: {}", keys_in_use_idxs.len()); + let num_keys = keys_in_use_idxs.len(); debug!( - "# DNSKEY RR signing keys: {}", - dnskey_signing_key_idxs.len() - ); - debug!( - "# Non-DNSKEY RR signing keys: {}", - non_dnskey_signing_key_idxs.len() + "Signing with {} {}:", + num_keys, + if num_keys == 1 { "key" } else { "keys" } ); for idx in &keys_in_use_idxs { - debug_key("Key", keys[**idx].key()); - } - - for idx in &dnskey_signing_key_idxs { - debug_key("DNSKEY RR signing key", keys[*idx].key()); - } - - for idx in &non_dnskey_signing_key_idxs { - debug_key("Non-DNSKEY RR signing key", keys[*idx].key()); + let key = keys[**idx].key(); + let is_dnskey_signing_key = + dnskey_signing_key_idxs.contains(idx); + let is_non_dnskey_signing_key = + non_dnskey_signing_key_idxs.contains(idx); + let usage = + if is_dnskey_signing_key && is_non_dnskey_signing_key { + "CSK" + } else if is_dnskey_signing_key { + "KSK" + } else if is_non_dnskey_signing_key { + "ZSK" + } else { + "Unused" + }; + debug_key(&format!("Key[{idx}]: {usage}"), key); } } From b92f2f45dd21776e057cf15a871bdc0747b0ac36 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:19:09 +0100 Subject: [PATCH 251/569] FIX: Only sign the apex if given the apex and remove unnecessary error variants. --- src/sign/records.rs | 148 +++++++++++++++++++++++--------------------- 1 file changed, 78 insertions(+), 70 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 4b6af87e3..a2c1ab57d 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -982,12 +982,6 @@ pub enum SigningError { /// TODO OutOfMemory, - /// TODO - MissingApex, - - /// TODO - EmptyApex, - /// At least one key must be provided to sign with. NoKeysProvided, @@ -1287,7 +1281,7 @@ where pub fn sign( &self, apex: &FamilyName, - mut families: RecordsIter<'_, N, D>, + families: RecordsIter<'_, N, D>, keys: &[DnssecSigningKey], add_used_dnskeys: bool, ) -> Result>>, SigningError> @@ -1378,78 +1372,92 @@ where let mut res: Vec>> = Vec::new(); let mut buf = Vec::new(); let mut cut: Option> = None; - - // Since the records are ordered, the first family is the apex -- - // we can skip everything before that. - families.skip_before(apex); - let mut families = families.peekable(); - let apex_ttl = families - .peek() - .ok_or(SigningError::MissingApex)? - .records() - .next() - .ok_or(SigningError::EmptyApex)? - .ttl(); - - // Sign the apex - // SAFETY: We just checked above if the apex records existed. - let apex_family = families.next().unwrap(); - let mut dnskey_rrs = vec![]; - for rrset in apex_family - .rrsets() - .filter(|rrset| rrset.rtype() != Rtype::RRSIG) - { - // If this is the apex DNSKEY RRSET, merge in the DNSKEYs of the - // keys we intend to sign with. - let (signing_key_idxs, rrset) = if rrset.rtype() == Rtype::DNSKEY - { - dnskey_rrs.extend_from_slice(rrset.into_inner()); + // Are we signing the entire tree from the apex down or just some child records? + let apex_ttl = families.peek().and_then(|first_family| { + first_family + .records() + .find(|rr| rr.rtype() == Rtype::SOA) + .map(|rr| rr.ttl()) + }); + + if let Some(apex_ttl) = apex_ttl { + // Sign the apex + // SAFETY: We just checked above if the apex records existed. + let apex_family = families.next().unwrap(); + + let apex_rrsets = apex_family + .rrsets() + .filter(|rrset| rrset.rtype() != Rtype::RRSIG); + + // Generate or extend the DNSKEY RRSET with the keys that we will sign + // apex DNSKEY RRs and zone RRs with. + let apex_dnskey_rrset = apex_family + .rrsets() + .find(|rrset| rrset.rtype() == Rtype::DNSKEY); + + let mut apex_dnskey_rrs = vec![]; + if let Some(apex_dnskey_rrset) = apex_dnskey_rrset { + apex_dnskey_rrs + .extend_from_slice(apex_dnskey_rrset.into_inner()); + } - // Make DNSKEY RRs for all new keys that will be used. - for public_key in keys_in_use_idxs - .iter() - .map(|&&idx| keys[idx].key().public_key()) - { - let dnskey = public_key.to_dnskey(); + for public_key in keys_in_use_idxs + .iter() + .map(|&&idx| keys[idx].key().public_key()) + { + let dnskey = public_key.to_dnskey(); - let dnskey_rr = Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - Dnskey::convert(dnskey.clone()).into(), - ); + let signing_key_dnskey_rr = Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey.clone()).into(), + ); - if !dnskey_rrs.contains(&dnskey_rr) { - if add_used_dnskeys { - res.push(Record::new( - apex.owner().clone(), - apex.class(), - apex_ttl, - Dnskey::convert(dnskey).into(), - )); - } - dnskey_rrs.push(dnskey_rr); + if !apex_dnskey_rrs.contains(&signing_key_dnskey_rr) { + if add_used_dnskeys { + // Add the DNSKEY RR to the set of new RRs to output for the zone. + res.push(Record::new( + apex.owner().clone(), + apex.class(), + apex_ttl, + Dnskey::convert(dnskey).into(), + )); } + + // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. + apex_dnskey_rrs.push(signing_key_dnskey_rr); } - (&dnskey_signing_key_idxs, Rrset::new(&dnskey_rrs)) - } else { - (&non_dnskey_signing_key_idxs, rrset) - }; + } - for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) { - // A copy of the family name. We’ll need it later. - let name = apex_family.family_name().cloned(); + let apex_dnskey_rrsets = FamilyIter::new(&apex_dnskey_rrs); - let rrsig_rr = - Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; - res.push(rrsig_rr); - debug!( - "Signed {} RRSET at the zone apex with keytag {}", - rrset.rtype(), - key.public_key().key_tag() - ); + for rrset in apex_rrsets.chain(apex_dnskey_rrsets) { + // If this is the apex DNSKEY RRSET, merge in the DNSKEYs of the + // keys we intend to sign with. + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + &dnskey_signing_key_idxs + } else { + &non_dnskey_signing_key_idxs + }; + + for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) + { + // A copy of the family name. We’ll need it later. + let name = apex_family.family_name().cloned(); + + let rrsig_rr = + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRs in RRSET {} at the zone apex with keytag {}", + rrset.iter().len(), + rrset.rtype(), + key.public_key().key_tag() + ); + } } } From 2a80b171ecd2139bdd0e4d4bfe9d18165dd09d8b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 9 Dec 2024 16:26:55 +0100 Subject: [PATCH 252/569] Actually check that we were given THE apex, not AN apex. --- src/sign/records.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index a2c1ab57d..c9018fe51 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1378,7 +1378,9 @@ where let apex_ttl = families.peek().and_then(|first_family| { first_family .records() - .find(|rr| rr.rtype() == Rtype::SOA) + .find(|rr| { + rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA + }) .map(|rr| rr.ttl()) }); From 605efe6148695c4d8d2cd8840b82adb57f89f37c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:44:00 +0100 Subject: [PATCH 253/569] Extend zone parsing to let the caller know when the origin has been detected and with what value, so that the caller can reliably ignore apex records if needed (e.g. when updating NSEC3PARAM or ZONEMD RRs) when signing a loaded zone. This is a breaking change. --- src/sign/records.rs | 27 +++++++++++++++++++++++++++ src/validator/anchor.rs | 18 ++++++------------ src/zonefile/inplace.rs | 8 +++++++- src/zonetree/parsed.rs | 4 ++++ 4 files changed, 44 insertions(+), 13 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index c9018fe51..96f68925b 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -271,15 +271,24 @@ where }; for family in families { + debug!("NSEC3: Family '{}'", family.owner()); // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { + debug!( + "NSEC3: Family '{}' is not in the zone, ignoring", + family.owner() + ); break; } // If the family is below a zone cut, we must ignore it. if let Some(ref cut) = cut { if family.owner().ends_with(cut.owner()) { + debug!( + "NSEC3: Family '{}' is below a zone cut, ignoring", + family.owner() + ); continue; } } @@ -291,6 +300,7 @@ where // family name for later. This also means below that if // `cut.is_some()` we are at the parent side of a zone. cut = if family.is_zone_cut(apex) { + debug!("NSEC3: Family '{}' is a zone cut", family.owner()); Some(name.clone()) } else { None @@ -300,7 +310,15 @@ where // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); + debug!( + "NSEC3: Family '{}' cut={} has_ds={} opt_out={}", + family.owner(), + cut.is_some(), + has_ds, + opt_out == Nsec3OptOut::OptOut + ); if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { + debug!("NSEC3: Family '{}' is an unsigned delegation and should be opted-out, ignoring", family.owner()); continue; } @@ -356,6 +374,7 @@ where builder.append_origin(&apex_owner).unwrap().into(); if let Err(pos) = ents.binary_search(&name) { + debug!("NSEC3: Found ENT '{name}'"); ents.insert(pos, name); } } @@ -367,6 +386,7 @@ where // Authoritative RRsets will be signed. if cut.is_none() || has_ds { + debug!("NSEC3: Family '{}' is not a zone cut and has a DS so add RRSIG to the bitmap", family.owner()); bitmap.add(Rtype::RRSIG).unwrap(); } @@ -374,12 +394,19 @@ where // "For each RRSet at the original owner name, set the // corresponding bit in the Type Bit Maps field." for rrset in family.rrsets() { + debug!( + "NSEC3: Family '{}' adding {} to the bitmap", + family.owner(), + rrset.rtype() + ); bitmap.add(rrset.rtype()).unwrap(); } if distance_to_apex == 0 { + debug!("NSEC3: Family '{}' is at the apex, adding NSEC3PARAM to the bitmap", family.owner()); bitmap.add(Rtype::NSEC3PARAM).unwrap(); if assume_dnskeys_will_be_added { + debug!("NSEC3: Family '{}' is at the apex, adding DNSKEY to the bitmap", family.owner()); bitmap.add(Rtype::DNSKEY).unwrap(); } } diff --git a/src/validator/anchor.rs b/src/validator/anchor.rs index ef6a70042..abd72e8c3 100644 --- a/src/validator/anchor.rs +++ b/src/validator/anchor.rs @@ -103,10 +103,8 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => { - new_self.add(r); - } - Entry::Include { path: _, origin: _ } => continue, // Just ignore include + Entry::Record(r) => new_self.add(r), + Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin } } Ok(new_self) @@ -123,10 +121,8 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => { - new_self.add(r); - } - Entry::Include { path: _, origin: _ } => continue, // Just ignore include + Entry::Record(r) => new_self.add(r), + Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin } } Ok(new_self) @@ -142,10 +138,8 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => { - self.add(r); - } - Entry::Include { path: _, origin: _ } => continue, // Just ignore include + Entry::Record(r) => self.add(r), + Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin } } Ok(()) diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index af4b0c113..5f7a045d5 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -183,7 +183,10 @@ impl Zonefile { loop { match EntryScanner::new(self)?.scan_entry()? { ScannedEntry::Entry(entry) => return Ok(Some(entry)), - ScannedEntry::Origin(origin) => self.origin = Some(origin), + ScannedEntry::Origin(origin) => { + self.origin = Some(origin.clone()); + return Ok(Some(Entry::Origin(origin))); + } ScannedEntry::Ttl(ttl) => self.last_ttl = ttl, ScannedEntry::Empty => {} ScannedEntry::Eof => return Ok(None), @@ -213,6 +216,9 @@ impl Iterator for Zonefile { /// An entry of a zonefile. #[derive(Clone, Debug)] pub enum Entry { + /// The origin has been detected. + Origin(Name), + /// A DNS record. Record(ScannedRecord), diff --git a/src/zonetree/parsed.rs b/src/zonetree/parsed.rs index dc4cab13f..6e51cd547 100644 --- a/src/zonetree/parsed.rs +++ b/src/zonetree/parsed.rs @@ -317,6 +317,10 @@ impl TryFrom for Zonefile { } } + Ok(Entry::Origin(_)) => { + // Nothing to do. + } + Ok(Entry::Include { .. }) => { // Not supported at this time. } From f7b9351648340a546b81224babff5b9a0bc8189b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:34:25 +0100 Subject: [PATCH 254/569] Revert "Extend zone parsing to let the caller know when the origin has been detected and with what value, so that the caller can reliably ignore apex records if needed (e.g. when updating NSEC3PARAM or ZONEMD RRs) when signing a loaded zone. This is a breaking change." This reverts commit 605efe6148695c4d8d2cd8840b82adb57f89f37c. --- src/sign/records.rs | 27 --------------------------- src/validator/anchor.rs | 18 ++++++++++++------ src/zonefile/inplace.rs | 8 +------- src/zonetree/parsed.rs | 4 ---- 4 files changed, 13 insertions(+), 44 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 96f68925b..c9018fe51 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -271,24 +271,15 @@ where }; for family in families { - debug!("NSEC3: Family '{}'", family.owner()); // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { - debug!( - "NSEC3: Family '{}' is not in the zone, ignoring", - family.owner() - ); break; } // If the family is below a zone cut, we must ignore it. if let Some(ref cut) = cut { if family.owner().ends_with(cut.owner()) { - debug!( - "NSEC3: Family '{}' is below a zone cut, ignoring", - family.owner() - ); continue; } } @@ -300,7 +291,6 @@ where // family name for later. This also means below that if // `cut.is_some()` we are at the parent side of a zone. cut = if family.is_zone_cut(apex) { - debug!("NSEC3: Family '{}' is a zone cut", family.owner()); Some(name.clone()) } else { None @@ -310,15 +300,7 @@ where // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - debug!( - "NSEC3: Family '{}' cut={} has_ds={} opt_out={}", - family.owner(), - cut.is_some(), - has_ds, - opt_out == Nsec3OptOut::OptOut - ); if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { - debug!("NSEC3: Family '{}' is an unsigned delegation and should be opted-out, ignoring", family.owner()); continue; } @@ -374,7 +356,6 @@ where builder.append_origin(&apex_owner).unwrap().into(); if let Err(pos) = ents.binary_search(&name) { - debug!("NSEC3: Found ENT '{name}'"); ents.insert(pos, name); } } @@ -386,7 +367,6 @@ where // Authoritative RRsets will be signed. if cut.is_none() || has_ds { - debug!("NSEC3: Family '{}' is not a zone cut and has a DS so add RRSIG to the bitmap", family.owner()); bitmap.add(Rtype::RRSIG).unwrap(); } @@ -394,19 +374,12 @@ where // "For each RRSet at the original owner name, set the // corresponding bit in the Type Bit Maps field." for rrset in family.rrsets() { - debug!( - "NSEC3: Family '{}' adding {} to the bitmap", - family.owner(), - rrset.rtype() - ); bitmap.add(rrset.rtype()).unwrap(); } if distance_to_apex == 0 { - debug!("NSEC3: Family '{}' is at the apex, adding NSEC3PARAM to the bitmap", family.owner()); bitmap.add(Rtype::NSEC3PARAM).unwrap(); if assume_dnskeys_will_be_added { - debug!("NSEC3: Family '{}' is at the apex, adding DNSKEY to the bitmap", family.owner()); bitmap.add(Rtype::DNSKEY).unwrap(); } } diff --git a/src/validator/anchor.rs b/src/validator/anchor.rs index abd72e8c3..ef6a70042 100644 --- a/src/validator/anchor.rs +++ b/src/validator/anchor.rs @@ -103,8 +103,10 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => new_self.add(r), - Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin + Entry::Record(r) => { + new_self.add(r); + } + Entry::Include { path: _, origin: _ } => continue, // Just ignore include } } Ok(new_self) @@ -121,8 +123,10 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => new_self.add(r), - Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin + Entry::Record(r) => { + new_self.add(r); + } + Entry::Include { path: _, origin: _ } => continue, // Just ignore include } } Ok(new_self) @@ -138,8 +142,10 @@ impl TrustAnchors { for e in zonefile { let e = e?; match e { - Entry::Record(r) => self.add(r), - Entry::Origin(_) | Entry::Include { .. } => continue, // Just ignore include and origin + Entry::Record(r) => { + self.add(r); + } + Entry::Include { path: _, origin: _ } => continue, // Just ignore include } } Ok(()) diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index 5f7a045d5..af4b0c113 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -183,10 +183,7 @@ impl Zonefile { loop { match EntryScanner::new(self)?.scan_entry()? { ScannedEntry::Entry(entry) => return Ok(Some(entry)), - ScannedEntry::Origin(origin) => { - self.origin = Some(origin.clone()); - return Ok(Some(Entry::Origin(origin))); - } + ScannedEntry::Origin(origin) => self.origin = Some(origin), ScannedEntry::Ttl(ttl) => self.last_ttl = ttl, ScannedEntry::Empty => {} ScannedEntry::Eof => return Ok(None), @@ -216,9 +213,6 @@ impl Iterator for Zonefile { /// An entry of a zonefile. #[derive(Clone, Debug)] pub enum Entry { - /// The origin has been detected. - Origin(Name), - /// A DNS record. Record(ScannedRecord), diff --git a/src/zonetree/parsed.rs b/src/zonetree/parsed.rs index 6e51cd547..dc4cab13f 100644 --- a/src/zonetree/parsed.rs +++ b/src/zonetree/parsed.rs @@ -317,10 +317,6 @@ impl TryFrom for Zonefile { } } - Ok(Entry::Origin(_)) => { - // Nothing to do. - } - Ok(Entry::Include { .. }) => { // Not supported at this time. } From c0016c168d3bac957577686951fcbbfd15b8d659 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 16 Dec 2024 10:23:50 +0100 Subject: [PATCH 255/569] Use the correct TTL for added DNSKEY RRs when signing. --- src/sign/records.rs | 73 +++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index c9018fe51..80c55b160 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -47,6 +47,11 @@ impl SortedRecords { } } + /// Insert a record in sorted order. + /// + /// If inserting a lot of records at once prefer [`extend()`] instead + /// which will sort once after all insertions rather than once per + /// insertion. pub fn insert(&mut self, record: Record) -> Result<(), Record> where N: ToName, @@ -1278,22 +1283,16 @@ where /// /// The given records MUST be sorted according to [`CanonicalOrd`]. #[allow(clippy::type_complexity)] - pub fn sign( + pub fn sign( &self, apex: &FamilyName, - families: RecordsIter<'_, N, D>, + families: RecordsIter<'_, N, ZoneRecordData>, keys: &[DnssecSigningKey], add_used_dnskeys: bool, ) -> Result>>, SigningError> where - N: ToName + Clone + PartialEq + Send, - D: RecordData - + Clone - + ComposeRecordData - + From> - + CanonicalOrd - + PartialEq - + Send, + N: ToName + Clone + PartialEq + CanonicalOrd + Send, + Octs: Clone + Send, { debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); @@ -1375,16 +1374,20 @@ where let mut families = families.peekable(); // Are we signing the entire tree from the apex down or just some child records? + // Use the first found SOA RR as the apex. If no SOA RR can be found assume that + // we are only signing records below the apex. let apex_ttl = families.peek().and_then(|first_family| { - first_family - .records() - .find(|rr| { - rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA - }) - .map(|rr| rr.ttl()) + first_family.records().find_map(|rr| { + if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { + if let ZoneRecordData::Soa(soa) = rr.data() { + return Some(soa.minimum()); + } + } + None + }) }); - if let Some(apex_ttl) = apex_ttl { + if let Some(soa_minimum_ttl) = apex_ttl { // Sign the apex // SAFETY: We just checked above if the apex records existed. let apex_family = families.next().unwrap(); @@ -1400,10 +1403,27 @@ where .find(|rrset| rrset.rtype() == Rtype::DNSKEY); let mut apex_dnskey_rrs = vec![]; - if let Some(apex_dnskey_rrset) = apex_dnskey_rrset { - apex_dnskey_rrs - .extend_from_slice(apex_dnskey_rrset.into_inner()); - } + + // Determine the TTL of any existing DNSKEY RRSET and use that as the + // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA + // mininmum TTL. + // + // Applicable sections from RFC 1033: + // TTL's (Time To Live) + // "Also, all RRs with the same name, class, and type should + // have the same TTL value." + // + // RESOURCE RECORDS + // "If you leave the TTL field blank it will default to the + // minimum time specified in the SOA record (described + // later)." + let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { + let ttl = rrset.ttl(); + apex_dnskey_rrs.extend_from_slice(rrset.into_inner()); + ttl + } else { + soa_minimum_ttl + }; for public_key in keys_in_use_idxs .iter() @@ -1414,7 +1434,7 @@ where let signing_key_dnskey_rr = Record::new( apex.owner().clone(), apex.class(), - apex_ttl, + dnskey_rrset_ttl, Dnskey::convert(dnskey.clone()).into(), ); @@ -1424,7 +1444,7 @@ where res.push(Record::new( apex.owner().clone(), apex.class(), - apex_ttl, + dnskey_rrset_ttl, Dnskey::convert(dnskey).into(), )); } @@ -1547,6 +1567,13 @@ where .signature_validity_period() .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? .into_inner(); + // RFC 4034 + // 3. The RRSIG Resource Record + // "The TTL value of an RRSIG RR MUST match the TTL value of the + // RRset it covers. This is an exception to the [RFC2181] rules + // for TTL values of individual RRs within a RRset: individual + // RRSIG RRs with the same owner name will have different TTL + // values if the RRsets they cover have different TTL values." let rrsig = ProtoRrsig::new( rrset.rtype(), key.algorithm(), From b17fb854ce420955eddf571f240997a22158bbe5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:32:35 +0100 Subject: [PATCH 256/569] FIX: Don't allow duplicate RRs to be imported via `impl From`. --- src/sign/records.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 80c55b160..9ed2e7b58 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -621,11 +621,12 @@ impl Default for SortedRecords { impl From>> for SortedRecords where - N: ToName, - D: RecordData + CanonicalOrd, + N: ToName + PartialEq, + D: RecordData + CanonicalOrd + PartialEq, { fn from(mut src: Vec>) -> Self { src.sort_by(CanonicalOrd::canonical_cmp); + src.dedup(); SortedRecords { records: src } } } From ed4fb30c350210cd4ff3583a4c77e463386fff83 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:33:03 +0100 Subject: [PATCH 257/569] Add a comment explaining why the apex name we use for an RRSIG meets the RFC requirements for it to be canonical and uncompressed. --- src/sign/records.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index 9ed2e7b58..6539dfc78 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1583,6 +1583,15 @@ where expiration, inception, key.public_key().key_tag(), + // The fns provided by `ToName` state in their RustDoc that they + // "Converts the name into a single, uncompressed name" which + // matches the RFC 4034 section 3.1.7 requirement that "A sender + // MUST NOT use DNS name compression on the Signer's Name field + // when transmitting a RRSIG RR.". + // + // We don't need to make sure here that the signer name is in + // canonical form as required by RFC 4034 as the call to + // `compose_canonical()` below will take care of that. apex.owner().clone(), ); buf.clear(); From 2034f32a5cf2c3677c2a87f160a98e54a5d03855 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 17 Dec 2024 11:07:26 +0100 Subject: [PATCH 258/569] FIX: Sign a merged DNSKEY RR set containing existing and new DNSKEY RRs, not two separate DNSKEY RRsets (existing and new). --- src/sign/records.rs | 62 +++++++++++++++++++++++++++------------------ 1 file changed, 38 insertions(+), 24 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 6539dfc78..e74dc84de 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -84,6 +84,14 @@ impl SortedRecords { { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } + + pub fn as_slice(&self) -> &[Record] { + self.records.as_slice() + } + + pub fn into_inner(self) -> Vec> { + self.records + } } impl SortedRecords { @@ -1403,7 +1411,7 @@ where .rrsets() .find(|rrset| rrset.rtype() == Rtype::DNSKEY); - let mut apex_dnskey_rrs = vec![]; + let mut augmented_apex_dnskey_rrs = SortedRecords::new(); // Determine the TTL of any existing DNSKEY RRSET and use that as the // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA @@ -1420,7 +1428,7 @@ where // later)." let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { let ttl = rrset.ttl(); - apex_dnskey_rrs.extend_from_slice(rrset.into_inner()); + augmented_apex_dnskey_rrs.extend(rrset.iter().cloned()); ttl } else { soa_minimum_ttl @@ -1439,31 +1447,37 @@ where Dnskey::convert(dnskey.clone()).into(), ); - if !apex_dnskey_rrs.contains(&signing_key_dnskey_rr) { - if add_used_dnskeys { - // Add the DNSKEY RR to the set of new RRs to output for the zone. - res.push(Record::new( - apex.owner().clone(), - apex.class(), - dnskey_rrset_ttl, - Dnskey::convert(dnskey).into(), - )); - } - - // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. - apex_dnskey_rrs.push(signing_key_dnskey_rr); + // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. + let is_new_dnskey = augmented_apex_dnskey_rrs + .insert(signing_key_dnskey_rr) + .is_ok(); + + if add_used_dnskeys && is_new_dnskey { + // Add the DNSKEY RR to the set of new RRs to output for the zone. + res.push(Record::new( + apex.owner().clone(), + apex.class(), + dnskey_rrset_ttl, + Dnskey::convert(dnskey).into(), + )); } } - let apex_dnskey_rrsets = FamilyIter::new(&apex_dnskey_rrs); - - for rrset in apex_rrsets.chain(apex_dnskey_rrsets) { - // If this is the apex DNSKEY RRSET, merge in the DNSKEYs of the - // keys we intend to sign with. - let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { - &dnskey_signing_key_idxs + let augmented_apex_dnskey_rrset = + Rrset::new(augmented_apex_dnskey_rrs.as_slice()); + + // Sign the apex RRSETs in canonical order. + for rrset in apex_rrsets { + // For the DNSKEY RRSET, use signing keys chosen for that + // purpose and sign the augmented set of DNSKEY RRs that we + // have generated rather than the original set in the + // zonefile. + let (rrset, signing_key_idxs) = if rrset.rtype() + == Rtype::DNSKEY + { + (&augmented_apex_dnskey_rrset, &dnskey_signing_key_idxs) } else { - &non_dnskey_signing_key_idxs + (&rrset, &non_dnskey_signing_key_idxs) }; for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) @@ -1472,7 +1486,7 @@ where let name = apex_family.family_name().cloned(); let rrsig_rr = - Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + Self::sign_rrset(key, rrset, &name, apex, &mut buf)?; res.push(rrsig_rr); debug!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", From 398e70b1b8900b69c8f53acff9ba4b9d2de030da Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 18 Dec 2024 11:44:20 +0100 Subject: [PATCH 259/569] Clippy-suggested code improvements. --- src/sign/records.rs | 2 +- src/validate.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 46b3d7ddb..5946f5ffd 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -844,7 +844,7 @@ impl FamilyName { } } -impl<'a, N: Clone> FamilyName<&'a N> { +impl FamilyName<&'_ N> { pub fn cloned(&self) -> FamilyName { FamilyName { owner: (*self.owner).clone(), diff --git a/src/validate.rs b/src/validate.rs index 4ee38f79f..135ef66ca 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1761,7 +1761,7 @@ pub enum Nsec3HashError { CollisionDetected, } -///--- Display +//--- Display impl std::fmt::Display for Nsec3HashError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { From f00acc6967414c31064600a2b867fb1d5761030a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:20:29 +0100 Subject: [PATCH 260/569] WIP: Use a hash provider. --- src/sign/records.rs | 213 +++++++++++++++++++++++++++++--------------- src/validate.rs | 6 ++ 2 files changed, 146 insertions(+), 73 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 3565f5c6e..354623cbe 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -7,7 +7,7 @@ use core::ops::Deref; use core::slice::Iter; use std::boxed::Box; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; use std::string::{String, ToString}; @@ -385,14 +385,16 @@ where /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html - pub fn nsec3s( + // TODO: Move to Signer and do HashProvider = OnDemandNsec3HashProvider + // TODO: Does it make sense to take both Nsec3param AND HashProvider as input? + pub fn nsec3s( &self, apex: &FamilyName, ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, assume_dnskeys_will_be_added: bool, - capture_hash_to_owner_mappings: bool, + hash_provider: &mut HashProvider, ) -> Result, Nsec3HashError> where N: ToName + Clone + From> + Display + Ord + Hash, @@ -407,6 +409,7 @@ where + EmptyBuilder + FreezeBuilder, ::Octets: AsRef<[u8]>, + HashProvider: Nsec3HashProvider, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -445,11 +448,11 @@ where 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 - }; + // 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 @@ -567,6 +570,7 @@ where let rec: Record> = Self::mk_nsec3( name.owner(), + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -576,10 +580,10 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map - .insert(rec.owner().clone(), name.owner().clone()); - } + // 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. nsec3s.push(rec); @@ -596,6 +600,7 @@ where let rec = Self::mk_nsec3( &name, + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -605,9 +610,9 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map.insert(rec.owner().clone(), name); - } + // 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. nsec3s.push(rec); @@ -656,11 +661,11 @@ where 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) - } + // 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<(), fmt::Error> @@ -719,13 +724,14 @@ where S: Sorter, { #[allow(clippy::too_many_arguments)] - fn mk_nsec3( + fn mk_nsec3( name: &N, + hash_provider: &mut HashProvider, alg: Nsec3HashAlg, flags: u8, iterations: u16, salt: &Nsec3Salt, - apex_owner: &N, + _apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, ) -> Result>, Nsec3HashError> @@ -735,14 +741,12 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, Nsec3: Into, + HashProvider: Nsec3HashProvider, { - // Create the base32hex ENT NSEC owner name. - let base32hex_label = - Self::mk_base32hex_label_for_name(name, alg, iterations, salt)?; - - // Prepend it to the zone name to create the NSEC3 owner - // name. - let owner_name = Self::append_origin(base32hex_label, apex_owner); + // let owner_name = mk_hashed_nsec3_owner_name( + // name, alg, iterations, salt, apex_owner, + // )?; + let owner_name = hash_provider.get_or_create(name)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -762,35 +766,6 @@ where Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) } - - fn append_origin(base32hex_label: String, apex_owner: &N) -> N - where - N: ToName + From>, - Octets: FromBuilder, - ::Builder: - EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - { - let mut builder = NameBuilder::::new(); - builder.append_label(base32hex_label.as_bytes()).unwrap(); - let owner_name = builder.append_origin(apex_owner).unwrap(); - let owner_name: N = owner_name.into(); - owner_name - } - - fn mk_base32hex_label_for_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - Octets: AsRef<[u8]>, - { - let hash_octets: Vec = - nsec3_hash(name, alg, iterations, salt)?.into_octets(); - Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) - } } impl Default @@ -855,11 +830,6 @@ pub struct Nsec3Records { /// The NSEC3PARAM record. pub param: Record>, - - /// A map of hashes to owner names. - /// - /// For diagnostic purposes. None if not generated. - pub hashes: Option>, } impl Nsec3Records { @@ -867,16 +837,7 @@ impl Nsec3Records { recs: Vec>>, param: Record>, ) -> Self { - Self { - recs, - param, - hashes: None, - } - } - - pub fn with_hashes(mut self, hashes: HashMap) -> Self { - self.hashes = Some(hashes); - self + Self { recs, param } } } @@ -1819,3 +1780,109 @@ where )) } } + +pub fn mk_hashed_nsec3_owner_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, +) -> Result +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + let base32hex_label = + mk_base32hex_label_for_name(name, alg, iterations, salt)?; + Ok(append_origin(base32hex_label, apex_owner)) +} + +fn append_origin(base32hex_label: String, apex_owner: &N) -> N +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name +} + +fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result +where + N: ToName, + SaltOcts: AsRef<[u8]>, +{ + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) +} + +//------------ Nsec3HashProvider --------------------------------------------- + +pub trait Nsec3HashProvider { + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result; +} + +pub struct OnDemandNsec3HashProvider { + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, +} + +impl OnDemandNsec3HashProvider { + pub fn new( + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, + ) -> Self { + Self { + alg, + iterations, + salt, + apex_owner, + } + } + + pub fn algorithm(&self) -> Nsec3HashAlg { + self.alg + } + + pub fn iterations(&self) -> u16 { + self.iterations + } + + pub fn salt(&self) -> &Nsec3Salt { + &self.salt + } +} + +impl Nsec3HashProvider + for OnDemandNsec3HashProvider +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result { + mk_hashed_nsec3_owner_name( + unhashed_owner_name, + self.alg, + self.iterations, + &self.salt, + &self.apex_owner, + ) + } +} diff --git a/src/validate.rs b/src/validate.rs index b6405ff27..d96c8b139 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1759,6 +1759,9 @@ pub enum Nsec3HashError { /// The hashing process produced a hash that already exists. CollisionDetected, + + /// The hash provider did not provide a hash for the given owner name. + MissingHash, } ///--- Display @@ -1777,6 +1780,9 @@ impl std::fmt::Display for Nsec3HashError { Nsec3HashError::CollisionDetected => { f.write_str("Hash collision detected") } + Nsec3HashError::MissingHash => { + f.write_str("Missing hash for owner name") + } } } } From ae9405649aefe6169bbe66f687a59f1f08ee2d6c Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Wed, 18 Dec 2024 14:55:26 +0100 Subject: [PATCH 261/569] Update changelog. --- Changelog.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 4d989ef9d..8d220ed0d 100644 --- a/Changelog.md +++ b/Changelog.md @@ -23,7 +23,8 @@ New Bug fixes * NSEC records should include themselves in the generated bitmap. ([#417]) -* Trailing double quote wrongly preserved when parsing record data. ([#470]) +* Trailing double quote wrongly preserved when parsing record data. ([#470], + [#472]) Unstable features @@ -61,6 +62,7 @@ Other changes [#446]: https://github.com/NLnetLabs/domain/pull/446 [#463]: https://github.com/NLnetLabs/domain/pull/463 [#470]: https://github.com/NLnetLabs/domain/pull/470 +[#472]: https://github.com/NLnetLabs/domain/pull/472 [@weilence]: https://github.com/weilence ## 0.10.3 From 1342d4cf636783561eada249fc66aed7166a6198 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 19 Dec 2024 00:58:59 +0100 Subject: [PATCH 262/569] FIX: Don't omit DNSKEY RRs when signing if there were no pre-exisitng DNSKEY RRs. --- src/sign/records.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index e74dc84de..2ef500d7a 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1467,17 +1467,18 @@ where Rrset::new(augmented_apex_dnskey_rrs.as_slice()); // Sign the apex RRSETs in canonical order. - for rrset in apex_rrsets { + for rrset in apex_rrsets + .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) + .chain(std::iter::once(augmented_apex_dnskey_rrset)) + { // For the DNSKEY RRSET, use signing keys chosen for that // purpose and sign the augmented set of DNSKEY RRs that we // have generated rather than the original set in the // zonefile. - let (rrset, signing_key_idxs) = if rrset.rtype() - == Rtype::DNSKEY - { - (&augmented_apex_dnskey_rrset, &dnskey_signing_key_idxs) + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + &dnskey_signing_key_idxs } else { - (&rrset, &non_dnskey_signing_key_idxs) + &non_dnskey_signing_key_idxs }; for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) @@ -1486,7 +1487,7 @@ where let name = apex_family.family_name().cloned(); let rrsig_rr = - Self::sign_rrset(key, rrset, &name, apex, &mut buf)?; + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; res.push(rrsig_rr); debug!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", From 222d862df3532bb187be1a967945886f6060c1e0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 18 Dec 2024 13:20:29 +0100 Subject: [PATCH 263/569] Don't hard-code NSEC3 hash capture, instead use a HashProvider. --- src/sign/records.rs | 215 +++++++++++++++++++++++++++++--------------- src/validate.rs | 6 ++ 2 files changed, 148 insertions(+), 73 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 2ef500d7a..04ef08f90 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -5,7 +5,7 @@ use core::marker::PhantomData; use core::ops::Deref; use std::boxed::Box; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; use std::string::{String, ToString}; @@ -217,14 +217,16 @@ where /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html - pub fn nsec3s( + // TODO: Move to Signer and do HashProvider = OnDemandNsec3HashProvider + // TODO: Does it make sense to take both Nsec3param AND HashProvider as input? + pub fn nsec3s( &self, apex: &FamilyName, ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, assume_dnskeys_will_be_added: bool, - capture_hash_to_owner_mappings: bool, + hash_provider: &mut HashProvider, ) -> Result, Nsec3HashError> where N: ToName + Clone + From> + Display + Ord + Hash, @@ -239,6 +241,8 @@ where + EmptyBuilder + FreezeBuilder, ::Octets: AsRef<[u8]>, + HashProvider: Nsec3HashProvider, + Nsec3: Into, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -277,11 +281,11 @@ where 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 - }; + // 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 @@ -399,6 +403,7 @@ where let rec = Self::mk_nsec3( name.owner(), + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -408,10 +413,10 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map - .insert(rec.owner().clone(), name.owner().clone()); - } + // 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() { @@ -430,6 +435,7 @@ where let rec = Self::mk_nsec3( &name, + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -439,9 +445,9 @@ where ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map.insert(rec.owner().clone(), name); - } + // 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() { @@ -490,11 +496,11 @@ where 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) - } + // 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<(), fmt::Error> @@ -548,13 +554,14 @@ where /// Helper functions used to create NSEC3 records per RFC 5155. impl SortedRecords { #[allow(clippy::too_many_arguments)] - fn mk_nsec3( + fn mk_nsec3( name: &N, + hash_provider: &mut HashProvider, alg: Nsec3HashAlg, flags: u8, iterations: u16, salt: &Nsec3Salt, - apex_owner: &N, + _apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, ) -> Result>, Nsec3HashError> @@ -563,14 +570,13 @@ impl SortedRecords { Octets: FromBuilder + Clone + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, + Nsec3: Into, + HashProvider: Nsec3HashProvider, { - // Create the base32hex ENT NSEC owner name. - let base32hex_label = - Self::mk_base32hex_label_for_name(name, alg, iterations, salt)?; - - // Prepend it to the zone name to create the NSEC3 owner - // name. - let owner_name = Self::append_origin(base32hex_label, apex_owner); + // let owner_name = mk_hashed_nsec3_owner_name( + // name, alg, iterations, salt, apex_owner, + // )?; + let owner_name = hash_provider.get_or_create(name)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -590,35 +596,6 @@ impl SortedRecords { Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) } - - fn append_origin(base32hex_label: String, apex_owner: &N) -> N - where - N: ToName + From>, - Octets: FromBuilder, - ::Builder: - EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - { - let mut builder = NameBuilder::::new(); - builder.append_label(base32hex_label.as_bytes()).unwrap(); - let owner_name = builder.append_origin(apex_owner).unwrap(); - let owner_name: N = owner_name.into(); - owner_name - } - - fn mk_base32hex_label_for_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - Octets: AsRef<[u8]>, - { - let hash_octets: Vec = - nsec3_hash(name, alg, iterations, salt)?.into_octets(); - Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) - } } impl Default for SortedRecords { @@ -673,11 +650,6 @@ pub struct Nsec3Records { /// The NSEC3PARAM record. pub param: Record>, - - /// A map of hashes to owner names. - /// - /// For diagnostic purposes. None if not generated. - pub hashes: Option>, } impl Nsec3Records { @@ -685,16 +657,7 @@ impl Nsec3Records { recs: Vec>>, param: Record>, ) -> Self { - Self { - recs, - param, - hashes: None, - } - } - - pub fn with_hashes(mut self, hashes: HashMap) -> Self { - self.hashes = Some(hashes); - self + Self { recs, param } } } @@ -1628,3 +1591,109 @@ where )) } } + +pub fn mk_hashed_nsec3_owner_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, +) -> Result +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + let base32hex_label = + mk_base32hex_label_for_name(name, alg, iterations, salt)?; + Ok(append_origin(base32hex_label, apex_owner)) +} + +fn append_origin(base32hex_label: String, apex_owner: &N) -> N +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name +} + +fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result +where + N: ToName, + SaltOcts: AsRef<[u8]>, +{ + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) +} + +//------------ Nsec3HashProvider --------------------------------------------- + +pub trait Nsec3HashProvider { + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result; +} + +pub struct OnDemandNsec3HashProvider { + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, +} + +impl OnDemandNsec3HashProvider { + pub fn new( + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, + ) -> Self { + Self { + alg, + iterations, + salt, + apex_owner, + } + } + + pub fn algorithm(&self) -> Nsec3HashAlg { + self.alg + } + + pub fn iterations(&self) -> u16 { + self.iterations + } + + pub fn salt(&self) -> &Nsec3Salt { + &self.salt + } +} + +impl Nsec3HashProvider + for OnDemandNsec3HashProvider +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result { + mk_hashed_nsec3_owner_name( + unhashed_owner_name, + self.alg, + self.iterations, + &self.salt, + &self.apex_owner, + ) + } +} diff --git a/src/validate.rs b/src/validate.rs index 135ef66ca..4a60876ae 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1759,6 +1759,9 @@ pub enum Nsec3HashError { /// The hashing process produced a hash that already exists. CollisionDetected, + + /// The hash provider did not provide a hash for the given owner name. + MissingHash, } //--- Display @@ -1778,6 +1781,9 @@ impl std::fmt::Display for Nsec3HashError { Nsec3HashError::CollisionDetected => { f.write_str("Hash collision detected") } + Nsec3HashError::MissingHash => { + f.write_str("Missing hash for owner name") + } } } } From 8911c93bbcb90f49b25d1a07d35eb6672dfbf6fb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:23:08 +0100 Subject: [PATCH 264/569] Cargo fmt. --- src/sign/records.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index d6b2c774e..260bdcc64 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1832,7 +1832,10 @@ where //------------ Nsec3HashProvider --------------------------------------------- pub trait Nsec3HashProvider { - fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result; + fn get_or_create( + &mut self, + unhashed_owner_name: &N, + ) -> Result; } pub struct OnDemandNsec3HashProvider { @@ -1878,7 +1881,10 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, SaltOcts: AsRef<[u8]>, { - fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result { + fn get_or_create( + &mut self, + unhashed_owner_name: &N, + ) -> Result { mk_hashed_nsec3_owner_name( unhashed_owner_name, self.alg, From 822c95ad24d57c0f4f31194d9d2c580bf9baadab Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:26:51 +0100 Subject: [PATCH 265/569] Enhanced zone signing. (#418) Co-authored-by: Jannik Peters --- Changelog.md | 9 - examples/client-transports.rs | 62 +- src/net/client/mod.rs | 5 - src/net/client/multi_stream.rs | 92 +- src/net/client/redundant.rs | 58 +- src/net/server/middleware/xfr/tests.rs | 29 +- src/sign/mod.rs | 36 +- src/sign/records.rs | 1233 +++++++++++++++++++----- src/validate.rs | 6 + test-data/zonefiles/nsd-example.txt | 12 + 10 files changed, 1126 insertions(+), 416 deletions(-) diff --git a/Changelog.md b/Changelog.md index 8d220ed0d..03897c059 100644 --- a/Changelog.md +++ b/Changelog.md @@ -40,13 +40,6 @@ Unstable features * A sample query router, called `QnameRouter`, that routes requests based on the QNAME field in the request ([#353]). -* `unstable-client-transport` - * introduce timeout option in multi_stream ([#424]). - * improve probing in redundant ([#424]). - * restructure configuration for multi_stream and redundant ([#424]). - * introduce a load balancer client transport. This transport tries to - distribute requests equally over upstream transports ([#425]). - Other changes [#353]: https://github.com/NLnetLabs/domain/pull/353 @@ -54,8 +47,6 @@ Other changes [#396]: https://github.com/NLnetLabs/domain/pull/396 [#417]: https://github.com/NLnetLabs/domain/pull/417 [#421]: https://github.com/NLnetLabs/domain/pull/421 -[#424]: https://github.com/NLnetLabs/domain/pull/424 -[#425]: https://github.com/NLnetLabs/domain/pull/425 [#427]: https://github.com/NLnetLabs/domain/pull/427 [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 diff --git a/examples/client-transports.rs b/examples/client-transports.rs index 5b6832a0d..40f0e9a9a 100644 --- a/examples/client-transports.rs +++ b/examples/client-transports.rs @@ -1,13 +1,4 @@ -//! Using the `domain::net::client` module for sending a query. -use domain::base::{MessageBuilder, Name, Rtype}; -use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; -use domain::net::client::request::{ - RequestMessage, RequestMessageMulti, SendRequest, -}; -use domain::net::client::{ - cache, dgram, dgram_stream, load_balancer, multi_stream, redundant, - stream, -}; +/// Using the `domain::net::client` module for sending a query. use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; #[cfg(feature = "unstable-validator")] @@ -15,6 +6,20 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use domain::base::MessageBuilder; +use domain::base::Name; +use domain::base::Rtype; +use domain::net::client::cache; +use domain::net::client::dgram; +use domain::net::client::dgram_stream; +use domain::net::client::multi_stream; +use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; +use domain::net::client::redundant; +use domain::net::client::request::{ + RequestMessage, RequestMessageMulti, SendRequest, +}; +use domain::net::client::stream; + #[cfg(feature = "tsig")] use domain::net::client::request::SendRequestMulti; #[cfg(feature = "tsig")] @@ -201,9 +206,9 @@ async fn main() { }); // Add the previously created transports. - redun.add(Box::new(udptcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tcp_conn.clone())).await.unwrap(); - redun.add(Box::new(tls_conn.clone())).await.unwrap(); + redun.add(Box::new(udptcp_conn)).await.unwrap(); + redun.add(Box::new(tcp_conn)).await.unwrap(); + redun.add(Box::new(tls_conn)).await.unwrap(); // Start a few queries. for i in 1..10 { @@ -216,37 +221,6 @@ async fn main() { drop(redun); - // Create a transport connection for load balanced connections. - let (lb, transp) = load_balancer::Connection::new(); - - // Start the run function on a separate task. - let run_fut = transp.run(); - tokio::spawn(async move { - run_fut.await; - println!("load_balancer run terminated"); - }); - - // Add the previously created transports. - let mut conn_conf = load_balancer::ConnConfig::new(); - conn_conf.set_max_burst(Some(10)); - conn_conf.set_burst_interval(Duration::from_secs(10)); - lb.add("UDP+TCP", &conn_conf, Box::new(udptcp_conn)) - .await - .unwrap(); - lb.add("TCP", &conn_conf, Box::new(tcp_conn)).await.unwrap(); - lb.add("TLS", &conn_conf, Box::new(tls_conn)).await.unwrap(); - - // Start a few queries. - for i in 1..10 { - let mut request = lb.send_request(req.clone()); - let reply = request.get_response().await; - if i == 2 { - println!("load_balancer connection reply: {reply:?}"); - } - } - - drop(lb); - // Create a new datagram transport connection. Pass the destination address // and port as parameter. This transport does not retry over TCP if the // reply is truncated. This transport does not have a separate run diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs index 8b3a48087..89f68fd35 100644 --- a/src/net/client/mod.rs +++ b/src/net/client/mod.rs @@ -21,10 +21,6 @@ //! transport connections. The [redundant] transport favors the connection //! with the lowest response time. Any of the other transports can be added //! as upstream transports. -//! * [load_balancer] This transport distributes requests over a collecton of -//! transport connections. The [load_balancer] transport favors connections -//! with the shortest outstanding request queue. Any of the other transports -//! can be added as upstream transports. //! * [cache] This is a simple message cache provided as a pass through //! transport. The cache works with any of the other transports. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] @@ -226,7 +222,6 @@ pub mod cache; pub mod dgram; pub mod dgram_stream; -pub mod load_balancer; pub mod multi_stream; pub mod protocol; pub mod redundant; diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index c45db3726..d0c65c753 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -9,7 +9,6 @@ use crate::net::client::request::{ ComposeRequest, Error, GetResponse, RequestMessageMulti, SendRequest, }; use crate::net::client::stream; -use crate::utils::config::DefMinMax; use bytes::Bytes; use futures_util::stream::FuturesUnordered; use futures_util::StreamExt; @@ -24,7 +23,6 @@ use std::vec::Vec; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::{mpsc, oneshot}; -use tokio::time::timeout; use tokio::time::{sleep_until, Instant}; //------------ Constants ----------------------------------------------------- @@ -35,42 +33,16 @@ const DEF_CHAN_CAP: usize = 8; /// Error messafe when the connection is closed. const ERR_CONN_CLOSED: &str = "connection closed"; -//------------ Configuration Constants ---------------------------------------- - -/// Default response timeout. -const RESPONSE_TIMEOUT: DefMinMax = DefMinMax::new( - Duration::from_secs(30), - Duration::from_millis(1), - Duration::from_secs(600), -); - //------------ Config --------------------------------------------------------- /// Configuration for an multi-stream transport. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Config { - /// Response timeout currently in effect. - response_timeout: Duration, - /// Configuration of the underlying stream transport. stream: stream::Config, } impl Config { - /// Returns the response timeout. - /// - /// This is the amount of time to wait for a request to complete. - pub fn response_timeout(&self) -> Duration { - self.response_timeout - } - - /// Sets the response timeout. - /// - /// Excessive values are quietly trimmed. - pub fn set_response_timeout(&mut self, timeout: Duration) { - self.response_timeout = RESPONSE_TIMEOUT.limit(timeout); - } - /// Returns the underlying stream config. pub fn stream(&self) -> &stream::Config { &self.stream @@ -84,19 +56,7 @@ impl Config { impl From for Config { fn from(stream: stream::Config) -> Self { - Self { - stream, - response_timeout: RESPONSE_TIMEOUT.default(), - } - } -} - -impl Default for Config { - fn default() -> Self { - Self { - stream: Default::default(), - response_timeout: RESPONSE_TIMEOUT.default(), - } + Self { stream } } } @@ -107,9 +67,6 @@ impl Default for Config { pub struct Connection { /// The sender half of the connection request channel. sender: mpsc::Sender>, - - /// Maximum amount of time to wait for a response. - response_timeout: Duration, } impl Connection { @@ -123,15 +80,8 @@ impl Connection { remote: Remote, config: Config, ) -> (Self, Transport) { - let response_timeout = config.response_timeout; let (sender, transport) = Transport::new(remote, config); - ( - Self { - sender, - response_timeout, - }, - transport, - ) + (Self { sender }, transport) } } @@ -197,7 +147,6 @@ impl Clone for Connection { fn clone(&self) -> Self { Self { sender: self.sender.clone(), - response_timeout: self.response_timeout, } } } @@ -226,9 +175,6 @@ struct Request { /// It is kept so we can compare a response with it. request_msg: Req, - /// Start time of the request. - start: Instant, - /// Current state of the query. state: QueryState, @@ -286,7 +232,6 @@ impl Request { Self { conn, request_msg, - start: Instant::now(), state: QueryState::RequestConn, conn_id: None, delayed_retry_count: 0, @@ -301,20 +246,9 @@ impl Request { /// it is resolved, you can call it again to get a new future. pub async fn get_response(&mut self) -> Result, Error> { loop { - let elapsed = self.start.elapsed(); - if elapsed >= self.conn.response_timeout { - return Err(Error::StreamReadTimeout); - } - let remaining = self.conn.response_timeout - elapsed; - match self.state { QueryState::RequestConn => { - let to = - timeout(remaining, self.conn.new_conn(self.conn_id)) - .await - .map_err(|_| Error::StreamReadTimeout)?; - - let rx = match to { + let rx = match self.conn.new_conn(self.conn_id).await { Ok(rx) => rx, Err(err) => { self.state = QueryState::Done; @@ -324,10 +258,7 @@ impl Request { self.state = QueryState::ReceiveConn(rx); } QueryState::ReceiveConn(ref mut receiver) => { - let to = timeout(remaining, receiver) - .await - .map_err(|_| Error::StreamReadTimeout)?; - let res = match to { + let res = match receiver.await { Ok(res) => res, Err(_) => { // Assume receive error @@ -363,10 +294,8 @@ impl Request { continue; } QueryState::GetResult(ref mut query) => { - let to = timeout(remaining, query.get_response()) - .await - .map_err(|_| Error::StreamReadTimeout)?; - match to { + let res = query.get_response().await; + match res { Ok(reply) => { return Ok(reply); } @@ -403,12 +332,7 @@ impl Request { } } QueryState::Delay(instant, duration) => { - if timeout(remaining, sleep_until(instant + duration)) - .await - .is_err() - { - return Err(Error::StreamReadTimeout); - }; + sleep_until(instant + duration).await; self.state = QueryState::RequestConn; } QueryState::Done => { diff --git a/src/net/client/redundant.rs b/src/net/client/redundant.rs index 7ec167cdb..413734b14 100644 --- a/src/net/client/redundant.rs +++ b/src/net/client/redundant.rs @@ -54,51 +54,24 @@ const SMOOTH_N: f64 = 8.; /// Chance to probe a worse connection. const PROBE_P: f64 = 0.05; +/// Avoid sending two requests at the same time. +/// +/// When a worse connection is probed, give it a slight head start. +const PROBE_RT: Duration = Duration::from_millis(1); + //------------ Config --------------------------------------------------------- /// User configuration variables. #[derive(Clone, Copy, Debug, Default)] pub struct Config { /// Defer transport errors. - defer_transport_error: bool, + pub defer_transport_error: bool, /// Defer replies that report Refused. - defer_refused: bool, + pub defer_refused: bool, /// Defer replies that report ServFail. - defer_servfail: bool, -} - -impl Config { - /// Return the value of the defer_transport_error configuration variable. - pub fn defer_transport_error(&self) -> bool { - self.defer_transport_error - } - - /// Set the value of the defer_transport_error configuration variable. - pub fn set_defer_transport_error(&mut self, value: bool) { - self.defer_transport_error = value - } - - /// Return the value of the defer_refused configuration variable. - pub fn defer_refused(&self) -> bool { - self.defer_refused - } - - /// Set the value of the defer_refused configuration variable. - pub fn set_defer_refused(&mut self, value: bool) { - self.defer_refused = value - } - - /// Return the value of the defer_servfail configuration variable. - pub fn defer_servfail(&self) -> bool { - self.defer_servfail - } - - /// Set the value of the defer_servfail configuration variable. - pub fn set_defer_servfail(&mut self, value: bool) { - self.defer_servfail = value - } + pub defer_servfail: bool, } //------------ Connection ----------------------------------------------------- @@ -186,7 +159,7 @@ impl SendRequest //------------ Request ------------------------------------------------------- /// An active request. -struct Request { +pub struct Request { /// The underlying future. fut: Pin< Box, Error>> + Send + Sync>, @@ -227,7 +200,7 @@ impl Debug for Request { /// This type represents an active query request. #[derive(Debug)] -struct Query +pub struct Query where Req: Send + Sync, { @@ -412,15 +385,10 @@ impl Query { // Do we want to probe a less performant upstream? if conn_rt_len > 1 && random::() < PROBE_P { let index: usize = 1 + random::() % (conn_rt_len - 1); + conn_rt[index].est_rt = PROBE_RT; - // Give the probe some head start. We may need a separate - // configuration parameter. A multiple of min_rt. Just use - // min_rt for now. - let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); - - let mut e = conn_rt.remove(index); - e.est_rt = min_rt; - conn_rt.insert(0, e); + // Sort again + conn_rt.sort_unstable_by(conn_rt_cmp); } Self { diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index ec87646a2..d4849a25b 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -17,7 +17,7 @@ use octseq::Octets; use tokio::sync::Semaphore; use tokio::time::Instant; -use crate::base::iana::{Class, OptRcode, Rcode}; +use crate::base::iana::{Class, DigestAlg, OptRcode, Rcode, SecAlg}; use crate::base::{ Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl, }; @@ -32,7 +32,7 @@ use crate::net::server::service::{ CallResult, Service, ServiceError, ServiceFeedback, ServiceResult, }; use crate::rdata::{ - Aaaa, AllRecordData, Cname, Mx, Ns, Soa, Txt, ZoneRecordData, A, + Aaaa, AllRecordData, Cname, Ds, Mx, Ns, Soa, Txt, ZoneRecordData, A, }; use crate::tsig::{Algorithm, Key, KeyName}; use crate::zonefile::inplace::Zonefile; @@ -74,6 +74,31 @@ async fn axfr_with_example_zone() { (n("example.com"), Aaaa::new(p("2001:db8::3")).into()), (n("www.example.com"), Cname::new(n("example.com")).into()), (n("mail.example.com"), Mx::new(10, n("example.com")).into()), + (n("a.b.c.mail.example.com"), A::new(p("127.0.0.1")).into()), + (n("x.y.mail.example.com"), A::new(p("127.0.0.1")).into()), + (n("some.ent.example.com"), A::new(p("127.0.0.1")).into()), + ( + n("unsigned.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ns::new(n("some.other.ns.net.example.com")).into(), + ), + ( + n("signed.example.com"), + Ds::new( + 60485, + SecAlg::RSASHA1, + DigestAlg::SHA1, + crate::utils::base16::decode( + "2BB183AF5F22588179A53B0A98631FAD1A292118", + ) + .unwrap(), + ) + .unwrap() + .into(), + ), (n("example.com"), zone_soa.into()), ]; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 61549965b..060e9af54 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -113,11 +113,11 @@ #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] use core::fmt; +use core::ops::RangeInclusive; -use crate::{ - base::{iana::SecAlg, Name}, - validate, -}; +use crate::base::{iana::SecAlg, Name}; +use crate::rdata::dnssec::Timestamp; +use crate::validate::Key; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -145,6 +145,13 @@ pub struct SigningKey { /// The raw private key. inner: Inner, + + /// The validity period to assign to any DNSSEC signatures created using + /// this key. + /// + /// The range spans from the inception timestamp up to and including the + /// expiration timestamp. + signature_validity_period: Option>, } //--- Construction @@ -156,8 +163,25 @@ impl SigningKey { owner, flags, inner, + signature_validity_period: None, } } + + pub fn with_validity( + mut self, + inception: Timestamp, + expiration: Timestamp, + ) -> Self { + self.signature_validity_period = + Some(RangeInclusive::new(inception, expiration)); + self + } + + pub fn signature_validity_period( + &self, + ) -> Option> { + self.signature_validity_period.clone() + } } //--- Inspection @@ -236,12 +260,12 @@ impl SigningKey { } /// The associated public key. - pub fn public_key(&self) -> validate::Key<&Octs> + pub fn public_key(&self) -> Key<&Octs> where Octs: AsRef<[u8]>, { let owner = Name::from_octets(self.owner.as_octets()).unwrap(); - validate::Key::new(owner, self.flags, self.inner.raw_public_key()) + Key::new(owner, self.flags, self.inner.raw_public_key()) } /// The associated raw public key. diff --git a/src/sign/records.rs b/src/sign/records.rs index 5946f5ffd..d6b2c774e 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,16 +1,23 @@ //! Actual signing. +use core::cmp::Ordering; use core::convert::From; use core::fmt::Display; +use core::marker::PhantomData; +use core::ops::Deref; +use core::slice::Iter; -use std::collections::HashMap; +use std::boxed::Box; +use std::collections::HashSet; use std::fmt::Debug; use std::hash::Hash; -use std::string::String; +use std::string::{String, ToString}; use std::vec::Vec; -use std::{fmt, io, slice}; +use std::{fmt, slice}; +use bytes::Bytes; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; +use tracing::{debug, enabled, Level}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; @@ -18,31 +25,93 @@ use crate::base::name::{ToLabelIter, ToName}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; use crate::base::{Name, NameBuilder, Ttl}; -use crate::rdata::dnssec::{ - ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder, Timestamp, -}; +use crate::rdata::dnssec::{ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Nsec, Nsec3, Nsec3param, Rrsig}; +use crate::rdata::{ + Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, +}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; +use crate::zonetree::types::StoredRecordData; +use crate::zonetree::StoredName; use super::{SignRaw, SigningKey}; +//------------ Sorter -------------------------------------------------------- + +/// A DNS resource record sorter. +/// +/// Implement this trait to use a different sorting algorithm than that +/// implemented by [`DefaultSorter`], e.g. to use system resources in a +/// different way when sorting. +pub trait Sorter { + /// Sort the given DNS resource records. + /// + /// The imposed order should be compatible with the ordering defined by + /// RFC 8976 section 3.3.1, i.e. _"DNSSEC's canonical on-the-wire RR + /// format (without name compression) and ordering as specified in + /// Sections 6.1, 6.2, and 6.3 of [RFC4034] with the additional provision + /// that RRsets having the same owner name MUST be numerically ordered, in + /// ascending order, by their numeric RR TYPE"_. + fn sort_by(records: &mut Vec>, compare: F) + where + Record: Send, + F: Fn(&Record, &Record) -> Ordering + Sync; +} + +//------------ DefaultSorter ------------------------------------------------- + +/// The default [`Sorter`] implementation used by [`SortedRecords`]. +/// +/// The current implementation is the single threaded sort provided by Rust +/// [`std::vec::Vec::sort_by()`]. +pub struct DefaultSorter; + +impl Sorter for DefaultSorter { + fn sort_by(records: &mut Vec>, compare: F) + where + Record: Send, + F: Fn(&Record, &Record) -> Ordering + Sync, + { + records.sort_by(compare); + } +} + //------------ SortedRecords ------------------------------------------------- /// A collection of resource records sorted for signing. +/// +/// The sort algorithm used defaults to [`DefaultSorter`] but can be +/// overridden by being generic over an alternate implementation of +/// [`Sorter`]. #[derive(Clone)] -pub struct SortedRecords { +pub struct SortedRecords +where + Record: Send, + S: Sorter, +{ records: Vec>, + + _phantom: PhantomData, } -impl SortedRecords { +impl SortedRecords +where + Record: Send, + S: Sorter, +{ pub fn new() -> Self { SortedRecords { records: Vec::new(), + _phantom: Default::default(), } } + /// Insert a record in sorted order. + /// + /// If inserting a lot of records at once prefer [`extend()`] instead + /// which will sort once after all insertions rather than once per + /// insertion. pub fn insert(&mut self, record: Record) -> Result<(), Record> where N: ToName, @@ -60,6 +129,84 @@ impl SortedRecords { } } + /// Remove all records matching the owner name, class, and rtype. + /// Class and Rtype can be None to match any. + /// + /// Returns: + /// - true: if one or more matching records were found (and removed) + /// - false: if no matching record was found + pub fn remove_all_by_name_class_rtype( + &mut self, + name: N, + class: Option, + rtype: Option, + ) -> bool + where + N: ToName + Clone, + D: RecordData, + { + let mut found_one = false; + loop { + if self.remove_first_by_name_class_rtype( + name.clone(), + class, + rtype, + ) { + found_one = true + } else { + break; + } + } + + found_one + } + + /// Remove first records matching the owner name, class, and rtype. + /// Class and Rtype can be None to match any. + /// + /// Returns: + /// - true: if a matching record was found (and removed) + /// - false: if no matching record was found + pub fn remove_first_by_name_class_rtype( + &mut self, + name: N, + class: Option, + rtype: Option, + ) -> bool + where + N: ToName, + D: RecordData, + { + let idx = self.records.binary_search_by(|stored| { + // Ordering based on base::Record::canonical_cmp excluding comparison of data + + if let Some(class) = class { + match stored.class().cmp(&class) { + Ordering::Equal => {} + res => return res, + } + } + + match stored.owner().name_cmp(&name) { + Ordering::Equal => {} + res => return res, + } + + if let Some(rtype) = rtype { + stored.rtype().cmp(&rtype) + } else { + Ordering::Equal + } + }); + match idx { + Ok(idx) => { + self.records.remove(idx); + true + } + Err(_) => false, + } + } + pub fn families(&self) -> RecordsIter { RecordsIter::new(&self.records) } @@ -76,114 +223,70 @@ impl SortedRecords { self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } - #[allow(clippy::type_complexity)] - pub fn sign( - &self, - apex: &FamilyName, - expiration: Timestamp, - inception: Timestamp, - key: SigningKey, - ) -> Result>>, ErrorTypeToBeDetermined> - where - N: ToName + Clone, - D: RecordData + ComposeRecordData, - ConcreteSecretKey: SignRaw, - Octets: AsRef<[u8]> + OctetsFrom>, - { - let mut res = Vec::new(); - let mut buf = Vec::new(); - - // The owner name of a zone cut if we currently are at or below one. - let mut cut: Option> = None; - - let mut families = self.families(); + pub fn iter(&self) -> Iter<'_, Record> { + self.records.iter() + } - // Since the records are ordered, the first family is the apex -- - // we can skip everything before that. - families.skip_before(apex); + pub fn as_slice(&self) -> &[Record] { + self.records.as_slice() + } - for family in families { - // If the owner is out of zone, we have moved out of our zone and - // are done. - if !family.is_in_zone(apex) { - break; - } + pub fn into_inner(self) -> Vec> { + self.records + } +} - // If the family is below a zone cut, we must ignore it. - if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { - continue; - } +impl SortedRecords { + pub fn replace_soa(&mut self, new_soa: Soa) { + if let Some(soa_rrset) = self + .records + .iter_mut() + .find(|rrset| rrset.rtype() == Rtype::SOA) + { + if let ZoneRecordData::Soa(current_soa) = soa_rrset.data_mut() { + *current_soa = new_soa; } + } + } - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); - - // If this family is the parent side of a zone cut, we keep the - // family name for later. This also means below that if - // `cut.is_some()` we are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - Some(name.clone()) - } else { - None - }; - - for rrset in family.rrsets() { - if cut.is_some() { - // If we are at a zone cut, we only sign DS and NSEC - // records. NS records we must not sign and everything - // else shouldn’t be here, really. - if rrset.rtype() != Rtype::DS - && rrset.rtype() != Rtype::NSEC - { - continue; - } - } else { - // Otherwise we only ignore RRSIGs. - if rrset.rtype() == Rtype::RRSIG { - continue; + pub fn replace_rrsig_for_apex_zonemd( + &mut self, + new_rrsig: Rrsig, + apex: &FamilyName, + ) { + if let Some(zonemd_rrsig) = self.records.iter_mut().find(|record| { + if record.rtype() == Rtype::RRSIG + && record.owner().name_cmp(&apex.owner()) == Ordering::Equal + { + if let ZoneRecordData::Rrsig(rrsig) = record.data() { + if rrsig.type_covered() == Rtype::ZONEMD { + return true; } } - - // Create the signature. - buf.clear(); - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm(), - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.public_key().key_tag(), - apex.owner().clone(), - ); - rrsig.compose_canonical(&mut buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(&mut buf).unwrap(); - } - - // Create and push the RRSIG record. - let signature = key.raw_secret_key().sign_raw(&buf).unwrap(); - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(ErrorTypeToBeDetermined); - }; - - res.push(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - rrsig.into_rrsig(signature).expect("long signature"), - )); + } + false + }) { + if let ZoneRecordData::Rrsig(current_rrsig) = + zonemd_rrsig.data_mut() + { + *current_rrsig = new_rrsig; } } - Ok(res) } +} +impl SortedRecords +where + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + S: Sorter, + SortedRecords: From>>, +{ pub fn nsecs( &self, apex: &FamilyName, ttl: Ttl, + assume_dnskeys_will_be_added: bool, ) -> Vec>> where N: ToName + Clone + PartialEq, @@ -249,7 +352,7 @@ impl SortedRecords { // zone MUST indicate the presence of both the NSEC record // itself and its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); - if family.owner() == &apex_owner { + if assume_dnskeys_will_be_added && family.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); } @@ -282,19 +385,22 @@ impl SortedRecords { /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html - pub fn nsec3s( + // TODO: Move to Signer and do HashProvider = OnDemandNsec3HashProvider + // TODO: Does it make sense to take both Nsec3param AND HashProvider as input? + pub fn nsec3s( &self, apex: &FamilyName, ttl: Ttl, params: Nsec3param, opt_out: Nsec3OptOut, - capture_hash_to_owner_mappings: bool, + assume_dnskeys_will_be_added: bool, + hash_provider: &mut HashProvider, ) -> Result, Nsec3HashError> where N: ToName + Clone + From> + Display + Ord + Hash, N: From::Octets>>, - D: RecordData, - Octets: FromBuilder + OctetsFrom> + Clone + Default, + D: RecordData + From>, + Octets: Send + FromBuilder + OctetsFrom> + Clone + Default, Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, OctetsMut: OctetsBuilder @@ -303,6 +409,8 @@ impl SortedRecords { + EmptyBuilder + FreezeBuilder, ::Octets: AsRef<[u8]>, + HashProvider: Nsec3HashProvider, + Nsec3: Into, { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) @@ -323,7 +431,7 @@ impl SortedRecords { // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." // We store the NSEC3s as we create them in a self-sorting vec. - let mut nsec3s = SortedRecords::new(); + let mut nsec3s = Vec::>>::new(); let mut ents = Vec::::new(); @@ -341,11 +449,11 @@ impl SortedRecords { 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 - }; + // 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 @@ -456,11 +564,14 @@ impl SortedRecords { if distance_to_apex == 0 { bitmap.add(Rtype::NSEC3PARAM).unwrap(); - bitmap.add(Rtype::DNSKEY).unwrap(); + if assume_dnskeys_will_be_added { + bitmap.add(Rtype::DNSKEY).unwrap(); + } } - let rec = Self::mk_nsec3( + let rec: Record> = Self::mk_nsec3( name.owner(), + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -470,15 +581,13 @@ impl SortedRecords { ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map - .insert(rec.owner().clone(), name.owner().clone()); - } + // 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); - } + nsec3s.push(rec); if let Some(last_nent) = last_nent { last_nent_stack.push(last_nent); @@ -492,6 +601,7 @@ impl SortedRecords { let rec = Self::mk_nsec3( &name, + hash_provider, params.hash_algorithm(), nsec3_flags, params.iterations(), @@ -501,14 +611,12 @@ impl SortedRecords { ttl, )?; - if let Some(nsec3_hash_map) = &mut nsec3_hash_map { - nsec3_hash_map.insert(rec.owner().clone(), name); - } + // 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); - } + nsec3s.push(rec); } // RFC 5155 7.1 step 7: @@ -516,7 +624,9 @@ impl SortedRecords { // value of the next NSEC3 RR in hash order. The next hashed owner // name of the last NSEC3 RR in the zone contains the value of the // hashed owner name of the first NSEC3 RR in the hash order." + let mut nsec3s = SortedRecords::, S>::from(nsec3s); for i in 1..=nsec3s.records.len() { + // TODO: Detect duplicate hashes. let next_i = if i == nsec3s.records.len() { 0 } else { i }; let cur_owner = nsec3s.records[next_i].owner(); let name: Name = cur_owner.try_to_name().unwrap(); @@ -552,60 +662,55 @@ impl SortedRecords { 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) - } + // 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> + pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> where N: fmt::Display, D: RecordData + fmt::Display, - W: io::Write, + W: fmt::Write, { for record in self.records.iter().filter(|r| r.rtype() == Rtype::SOA) { - writeln!(target, "{record}")?; + write!(target, "{record}")?; } for record in self.records.iter().filter(|r| r.rtype() != Rtype::SOA) { - writeln!(target, "{record}")?; + write!(target, "{record}")?; } Ok(()) } - pub fn write_with_comments( + pub fn write_with_comments( &self, target: &mut W, comment_cb: F, - ) -> Result<(), io::Error> + ) -> Result<(), fmt::Error> where N: fmt::Display, D: RecordData + fmt::Display, - W: io::Write, - C: fmt::Display, - F: Fn(&Record) -> Option, + W: fmt::Write, + F: Fn(&Record, &mut W) -> Result<(), fmt::Error>, { 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}")?; - } + write!(target, "{record}")?; + comment_cb(record, target)?; + writeln!(target)?; } 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}")?; - } + write!(target, "{record}")?; + comment_cb(record, target)?; + writeln!(target)?; } Ok(()) @@ -613,15 +718,21 @@ impl SortedRecords { } /// Helper functions used to create NSEC3 records per RFC 5155. -impl SortedRecords { +impl SortedRecords +where + N: ToName + Send, + D: RecordData + CanonicalOrd + Send, + S: Sorter, +{ #[allow(clippy::too_many_arguments)] - fn mk_nsec3( + fn mk_nsec3( name: &N, + hash_provider: &mut HashProvider, alg: Nsec3HashAlg, flags: u8, iterations: u16, salt: &Nsec3Salt, - apex_owner: &N, + _apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, ) -> Result>, Nsec3HashError> @@ -630,14 +741,13 @@ impl SortedRecords { Octets: FromBuilder + Clone + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, + Nsec3: Into, + HashProvider: Nsec3HashProvider, { - // Create the base32hex ENT NSEC owner name. - let base32hex_label = - Self::mk_base32hex_label_for_name(name, alg, iterations, salt)?; - - // Prepend it to the zone name to create the NSEC3 owner - // name. - let owner_name = Self::append_origin(base32hex_label, apex_owner); + // let owner_name = mk_hashed_nsec3_owner_name( + // name, alg, iterations, salt, apex_owner, + // )?; + let owner_name = hash_provider.get_or_create(name)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -657,55 +767,34 @@ impl SortedRecords { Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) } - - fn append_origin(base32hex_label: String, apex_owner: &N) -> N - where - N: ToName + From>, - Octets: FromBuilder, - ::Builder: - EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - { - let mut builder = NameBuilder::::new(); - builder.append_label(base32hex_label.as_bytes()).unwrap(); - let owner_name = builder.append_origin(apex_owner).unwrap(); - let owner_name: N = owner_name.into(); - owner_name - } - - fn mk_base32hex_label_for_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - Octets: AsRef<[u8]>, - { - let hash_octets: Vec = - nsec3_hash(name, alg, iterations, salt)?.into_octets(); - Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) - } } -impl Default for SortedRecords { +impl Default + for SortedRecords +{ fn default() -> Self { Self::new() } } -impl From>> for SortedRecords +impl From>> for SortedRecords where - N: ToName, - D: RecordData + CanonicalOrd, + N: ToName + PartialEq + Send, + D: RecordData + CanonicalOrd + PartialEq + Send, + S: Sorter, { fn from(mut src: Vec>) -> Self { - src.sort_by(CanonicalOrd::canonical_cmp); - SortedRecords { records: src } + S::sort_by(&mut src, CanonicalOrd::canonical_cmp); + src.dedup(); + SortedRecords { + records: src, + _phantom: Default::default(), + } } } -impl FromIterator> for SortedRecords +impl FromIterator> + for SortedRecords where N: ToName, D: RecordData + CanonicalOrd, @@ -719,15 +808,18 @@ where } } -impl Extend> for SortedRecords +impl Extend> + for SortedRecords where - N: ToName, - D: RecordData + CanonicalOrd, + N: ToName + PartialEq, + D: RecordData + CanonicalOrd + PartialEq, { fn extend>>(&mut self, iter: T) { for item in iter { - let _ = self.insert(item); + self.records.push(item); } + S::sort_by(&mut self.records, CanonicalOrd::canonical_cmp); + self.records.dedup(); } } @@ -739,11 +831,6 @@ pub struct Nsec3Records { /// The NSEC3PARAM record. pub param: Record>, - - /// A map of hashes to owner names. - /// - /// For diagnostic purposes. None if not generated. - pub hashes: Option>, } impl Nsec3Records { @@ -751,22 +838,14 @@ impl Nsec3Records { recs: Vec>>, param: Record>, ) -> Self { - Self { - recs, - param, - hashes: None, - } - } - - pub fn with_hashes(mut self, hashes: HashMap) -> Self { - self.hashes = Some(hashes); - self + Self { recs, param } } } //------------ Family -------------------------------------------------------- /// A set of records with the same owner name and class. +#[derive(Clone)] pub struct Family<'a, N, D> { slice: &'a [Record], } @@ -844,7 +923,7 @@ impl FamilyName { } } -impl FamilyName<&'_ N> { +impl FamilyName<&N> { pub fn cloned(&self) -> FamilyName { FamilyName { owner: (*self.owner).clone(), @@ -907,6 +986,10 @@ impl<'a, N, D> Rrset<'a, N, D> { pub fn iter(&self) -> slice::Iter<'a, Record> { self.slice.iter() } + + pub fn into_inner(self) -> &'a [Record] { + self.slice + } } //------------ RecordsIter --------------------------------------------------- @@ -1047,10 +1130,23 @@ where } } -//------------ ErrorTypeToBeDetermined --------------------------------------- +//------------ SigningError -------------------------------------------------- + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SigningError { + /// One or more keys does not have a signature validity period defined. + KeyLacksSignatureValidityPeriod, + + /// TODO + OutOfMemory, -#[derive(Debug)] -pub struct ErrorTypeToBeDetermined; + /// At least one key must be provided to sign with. + NoKeysProvided, + + /// None of the provided keys were deemed suitable by the + /// [`SigningKeyUsageStrategy`] used. + NoSuitableKeysFound, +} //------------ Nsec3OptOut --------------------------------------------------- @@ -1097,3 +1193,698 @@ pub enum Nsec3OptOut { // 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." + +//------------ IntendedKeyPurpose -------------------------------------------- + +/// The purpose of a DNSSEC key from the perspective of an operator. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IntendedKeyPurpose { + /// A key that signs DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Key signing key (KSK): DNSSEC keys that "only sign the apex DNSKEY + /// RRset in a zone." (Quoted from RFC6781, Section 3.1) + KSK, + + /// A key that signs non-DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Zone signing key (ZSK): "DNSSEC keys that can be used to sign all the + /// RRsets in a zone that require signatures, other than the apex DNSKEY + /// RRset." (Quoted from RFC6781, Section 3.1) Also note that a ZSK is + /// sometimes used to sign the apex DNSKEY RRset. + ZSK, + + /// A key that signs both DNSKEY and other RRSETs. + /// + /// RFC 9499 DNS Terminology: + /// 10. General DNSSEC + /// Combined signing key (CSK): In cases where the differentiation between + /// the KSK and ZSK is not made, i.e., where keys have the role of both + /// KSK and ZSK, we talk about a Single-Type Signing Scheme." (Quoted from + /// [RFC6781], Section 3.1) This is sometimes called a "combined signing + /// key" or "CSK". It is operational practice, not protocol, that + /// determines whether a particular key is a ZSK, a KSK, or a CSK. + CSK, + + /// A key that is not currently used for signing. + /// + /// This key should be added to the zone but not used to sign any RRSETs. + Inactive, +} + +//------------ DnssecSigningKey ---------------------------------------------- + +/// A key to be provided by an operator to a DNSSEC signer. +/// +/// This type carries metadata that signals to a DNSSEC signer how this key +/// should impact the zone to be signed. +pub struct DnssecSigningKey { + /// The key to use to make DNSSEC signatures. + key: SigningKey, + + /// The purpose for which the operator intends the key to be used. + /// + /// Defines explicitly the purpose of the key which should be used instead + /// of attempting to infer the purpose of the key (to sign keys and/or to + /// sign other records) by examining the setting of the Secure Entry Point + /// and Zone Key flags on the key (i.e. whether the key is a KSK or ZSK or + /// something else). + purpose: IntendedKeyPurpose, + + _phantom: PhantomData<(Octs, Inner)>, +} + +impl DnssecSigningKey { + /// Create a new [`DnssecSigningKey`] by assocating intent with a + /// reference to an existing key. + pub fn new( + key: SigningKey, + purpose: IntendedKeyPurpose, + ) -> Self { + Self { + key, + purpose, + _phantom: Default::default(), + } + } + + pub fn into_inner(self) -> SigningKey { + self.key + } +} + +impl Deref for DnssecSigningKey { + type Target = SigningKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +impl DnssecSigningKey { + pub fn key(&self) -> &SigningKey { + &self.key + } + + pub fn purpose(&self) -> IntendedKeyPurpose { + self.purpose + } +} + +impl, Inner: SignRaw> DnssecSigningKey { + pub fn ksk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::KSK, + _phantom: Default::default(), + } + } + + pub fn zsk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::ZSK, + _phantom: Default::default(), + } + } + + pub fn csk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::CSK, + _phantom: Default::default(), + } + } + + pub fn inactive(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::Inactive, + _phantom: Default::default(), + } + } + + pub fn inferred(key: SigningKey) -> Self { + let public_key = key.public_key(); + match ( + public_key.is_secure_entry_point(), + public_key.is_zone_signing_key(), + ) { + (true, _) => Self::ksk(key), + (false, true) => Self::zsk(key), + (false, false) => Self::inactive(key), + } + } +} + +//------------ Operations ---------------------------------------------------- + +// TODO: Move nsecs() and nsecs3() out of SortedRecords and make them also +// take an iterator. This allows callers to pass an iterator over Record +// rather than force them to create the SortedRecords type (which for example +// in the case of a Zone we wouldn't have, but may instead be able to get an +// iterator over the Zone). Also move out the helper functions. Maybe put them +// all into a Signer struct? + +pub trait SigningKeyUsageStrategy { + const NAME: &'static str; + + fn select_signing_keys_for_rtype( + candidate_keys: &[DnssecSigningKey], + rtype: Option, + ) -> HashSet { + match rtype { + Some(Rtype::DNSKEY) => Self::filter_keys(candidate_keys, |k| { + matches!( + k.purpose(), + IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK + ) + }), + + _ => Self::filter_keys(candidate_keys, |k| { + matches!( + k.purpose(), + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + }), + } + } + + fn filter_keys( + candidate_keys: &[DnssecSigningKey], + filter: fn(&DnssecSigningKey) -> bool, + ) -> HashSet { + candidate_keys + .iter() + .enumerate() + .filter_map(|(i, k)| filter(k).then_some(i)) + .collect::>() + } +} + +pub struct DefaultSigningKeyUsageStrategy; + +impl SigningKeyUsageStrategy + for DefaultSigningKeyUsageStrategy +{ + const NAME: &'static str = "Default key usage strategy"; +} + +pub struct Signer< + Octs, + Inner, + KeyStrat = DefaultSigningKeyUsageStrategy, + Sort = DefaultSorter, +> where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + _phantom: PhantomData<(Octs, Inner, KeyStrat, Sort)>, +} + +impl Default + for Signer +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + fn default() -> Self { + Self::new() + } +} + +impl Signer +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + pub fn new() -> Self { + Self { + _phantom: PhantomData, + } + } +} + +impl Signer +where + Octs: AsRef<[u8]> + From> + OctetsFrom>, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + /// Sign a zone using the given keys. + /// + /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be + /// added to the given records in order to DNSSEC sign them. + /// + /// The given records MUST be sorted according to [`CanonicalOrd`]. + #[allow(clippy::type_complexity)] + pub fn sign( + &self, + apex: &FamilyName, + families: RecordsIter<'_, N, ZoneRecordData>, + keys: &[DnssecSigningKey], + add_used_dnskeys: bool, + ) -> Result>>, SigningError> + where + N: ToName + Clone + PartialEq + CanonicalOrd + Send, + Octs: Clone + Send, + { + debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); + + if keys.is_empty() { + return Err(SigningError::NoKeysProvided); + } + + // Work with indices because SigningKey doesn't impl PartialEq so we + // cannot use a HashSet to make a unique set of them. + + let dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype( + keys, + Some(Rtype::DNSKEY), + ); + + let non_dnskey_signing_key_idxs = + KeyStrat::select_signing_keys_for_rtype(keys, None); + + let keys_in_use_idxs: HashSet<_> = non_dnskey_signing_key_idxs + .iter() + .chain(dnskey_signing_key_idxs.iter()) + .collect(); + + if keys_in_use_idxs.is_empty() { + return Err(SigningError::NoSuitableKeysFound); + } + + // TODO: use log::log_enabled instead. + // See: https://github.com/NLnetLabs/domain/pull/465 + if enabled!(Level::DEBUG) { + fn debug_key, Inner: SignRaw>( + prefix: &str, + key: &SigningKey, + ) { + debug!( + "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", + key.algorithm() + .to_mnemonic_str() + .map(|alg| format!("{alg} ({})", key.algorithm())) + .unwrap_or_else(|| key.algorithm().to_string()), + key.owner(), + key.flags(), + key.is_secure_entry_point(), + key.is_zone_signing_key(), + key.public_key().key_tag(), + ) + } + + let num_keys = keys_in_use_idxs.len(); + debug!( + "Signing with {} {}:", + num_keys, + if num_keys == 1 { "key" } else { "keys" } + ); + + for idx in &keys_in_use_idxs { + let key = keys[**idx].key(); + let is_dnskey_signing_key = + dnskey_signing_key_idxs.contains(idx); + let is_non_dnskey_signing_key = + non_dnskey_signing_key_idxs.contains(idx); + let usage = + if is_dnskey_signing_key && is_non_dnskey_signing_key { + "CSK" + } else if is_dnskey_signing_key { + "KSK" + } else if is_non_dnskey_signing_key { + "ZSK" + } else { + "Unused" + }; + debug_key(&format!("Key[{idx}]: {usage}"), key); + } + } + + let mut res: Vec>> = Vec::new(); + let mut buf = Vec::new(); + let mut cut: Option> = None; + let mut families = families.peekable(); + + // Are we signing the entire tree from the apex down or just some child records? + // Use the first found SOA RR as the apex. If no SOA RR can be found assume that + // we are only signing records below the apex. + let apex_ttl = families.peek().and_then(|first_family| { + first_family.records().find_map(|rr| { + if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { + if let ZoneRecordData::Soa(soa) = rr.data() { + return Some(soa.minimum()); + } + } + None + }) + }); + + if let Some(soa_minimum_ttl) = apex_ttl { + // Sign the apex + // SAFETY: We just checked above if the apex records existed. + let apex_family = families.next().unwrap(); + + let apex_rrsets = apex_family + .rrsets() + .filter(|rrset| rrset.rtype() != Rtype::RRSIG); + + // Generate or extend the DNSKEY RRSET with the keys that we will sign + // apex DNSKEY RRs and zone RRs with. + let apex_dnskey_rrset = apex_family + .rrsets() + .find(|rrset| rrset.rtype() == Rtype::DNSKEY); + + let mut augmented_apex_dnskey_rrs = + SortedRecords::<_, _, Sort>::new(); + + // Determine the TTL of any existing DNSKEY RRSET and use that as the + // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA + // mininmum TTL. + // + // Applicable sections from RFC 1033: + // TTL's (Time To Live) + // "Also, all RRs with the same name, class, and type should + // have the same TTL value." + // + // RESOURCE RECORDS + // "If you leave the TTL field blank it will default to the + // minimum time specified in the SOA record (described + // later)." + let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { + let ttl = rrset.ttl(); + augmented_apex_dnskey_rrs.extend(rrset.iter().cloned()); + ttl + } else { + soa_minimum_ttl + }; + + for public_key in keys_in_use_idxs + .iter() + .map(|&&idx| keys[idx].key().public_key()) + { + let dnskey = public_key.to_dnskey(); + + let signing_key_dnskey_rr = Record::new( + apex.owner().clone(), + apex.class(), + dnskey_rrset_ttl, + Dnskey::convert(dnskey.clone()).into(), + ); + + // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. + let is_new_dnskey = augmented_apex_dnskey_rrs + .insert(signing_key_dnskey_rr) + .is_ok(); + + if add_used_dnskeys && is_new_dnskey { + // Add the DNSKEY RR to the set of new RRs to output for the zone. + res.push(Record::new( + apex.owner().clone(), + apex.class(), + dnskey_rrset_ttl, + Dnskey::convert(dnskey).into(), + )); + } + } + + let augmented_apex_dnskey_rrset = + Rrset::new(augmented_apex_dnskey_rrs.as_slice()); + + // Sign the apex RRSETs in canonical order. + for rrset in apex_rrsets + .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) + .chain(std::iter::once(augmented_apex_dnskey_rrset)) + { + // For the DNSKEY RRSET, use signing keys chosen for that + // purpose and sign the augmented set of DNSKEY RRs that we + // have generated rather than the original set in the + // zonefile. + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + &dnskey_signing_key_idxs + } else { + &non_dnskey_signing_key_idxs + }; + + for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) + { + // A copy of the family name. We’ll need it later. + let name = apex_family.family_name().cloned(); + + let rrsig_rr = + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRs in RRSET {} at the zone apex with keytag {}", + rrset.iter().len(), + rrset.rtype(), + key.public_key().key_tag() + ); + } + } + } + + // For all RRSETs below the apex + for family in families { + // If the owner is out of zone, we have moved out of our zone and + // are done. + if !family.is_in_zone(apex) { + break; + } + + // If the family is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the + // family name for later. This also means below that if + // `cut.is_some()` we are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + Some(name.clone()) + } else { + None + }; + + for rrset in family.rrsets() { + if cut.is_some() { + // If we are at a zone cut, we only sign DS and NSEC + // records. NS records we must not sign and everything + // else shouldn’t be here, really. + if rrset.rtype() != Rtype::DS + && rrset.rtype() != Rtype::NSEC + { + continue; + } + } else { + // Otherwise we only ignore RRSIGs. + if rrset.rtype() == Rtype::RRSIG { + continue; + } + } + + for key in non_dnskey_signing_key_idxs + .iter() + .map(|&idx| keys[idx].key()) + { + let rrsig_rr = + Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRSET with keytag {}", + rrset.rtype(), + key.public_key().key_tag() + ); + } + } + } + + debug!("Returning {} records from signing", res.len()); + + Ok(res) + } + + fn sign_rrset( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + name: &FamilyName, + apex: &FamilyName, + buf: &mut Vec, + ) -> Result>, SigningError> + where + N: ToName + Clone + Send, + D: RecordData + + ComposeRecordData + + From> + + CanonicalOrd + + Send, + { + let (inception, expiration) = key + .signature_validity_period() + .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? + .into_inner(); + // RFC 4034 + // 3. The RRSIG Resource Record + // "The TTL value of an RRSIG RR MUST match the TTL value of the + // RRset it covers. This is an exception to the [RFC2181] rules + // for TTL values of individual RRs within a RRset: individual + // RRSIG RRs with the same owner name will have different TTL + // values if the RRsets they cover have different TTL values." + let rrsig = ProtoRrsig::new( + rrset.rtype(), + key.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + key.public_key().key_tag(), + // The fns provided by `ToName` state in their RustDoc that they + // "Converts the name into a single, uncompressed name" which + // matches the RFC 4034 section 3.1.7 requirement that "A sender + // MUST NOT use DNS name compression on the Signer's Name field + // when transmitting a RRSIG RR.". + // + // We don't need to make sure here that the signer name is in + // canonical form as required by RFC 4034 as the call to + // `compose_canonical()` below will take care of that. + apex.owner().clone(), + ); + buf.clear(); + rrsig.compose_canonical(buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(buf).unwrap(); + } + let signature = key.raw_secret_key().sign_raw(&*buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + Ok(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + ZoneRecordData::Rrsig(rrsig), + )) + } +} + +pub fn mk_hashed_nsec3_owner_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, +) -> Result +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + let base32hex_label = + mk_base32hex_label_for_name(name, alg, iterations, salt)?; + Ok(append_origin(base32hex_label, apex_owner)) +} + +fn append_origin(base32hex_label: String, apex_owner: &N) -> N +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name +} + +fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result +where + N: ToName, + SaltOcts: AsRef<[u8]>, +{ + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) +} + +//------------ Nsec3HashProvider --------------------------------------------- + +pub trait Nsec3HashProvider { + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result; +} + +pub struct OnDemandNsec3HashProvider { + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, +} + +impl OnDemandNsec3HashProvider { + pub fn new( + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + apex_owner: N, + ) -> Self { + Self { + alg, + iterations, + salt, + apex_owner, + } + } + + pub fn algorithm(&self) -> Nsec3HashAlg { + self.alg + } + + pub fn iterations(&self) -> u16 { + self.iterations + } + + pub fn salt(&self) -> &Nsec3Salt { + &self.salt + } +} + +impl Nsec3HashProvider + for OnDemandNsec3HashProvider +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + fn get_or_create(&mut self, unhashed_owner_name: &N) -> Result { + mk_hashed_nsec3_owner_name( + unhashed_owner_name, + self.alg, + self.iterations, + &self.salt, + &self.apex_owner, + ) + } +} diff --git a/src/validate.rs b/src/validate.rs index 135ef66ca..4a60876ae 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1759,6 +1759,9 @@ pub enum Nsec3HashError { /// The hashing process produced a hash that already exists. CollisionDetected, + + /// The hash provider did not provide a hash for the given owner name. + MissingHash, } //--- Display @@ -1778,6 +1781,9 @@ impl std::fmt::Display for Nsec3HashError { Nsec3HashError::CollisionDetected => { f.write_str("Hash collision detected") } + Nsec3HashError::MissingHash => { + f.write_str("Missing hash for owner name") + } } } } diff --git a/test-data/zonefiles/nsd-example.txt b/test-data/zonefiles/nsd-example.txt index bedf91ac6..1650961b6 100644 --- a/test-data/zonefiles/nsd-example.txt +++ b/test-data/zonefiles/nsd-example.txt @@ -21,3 +21,15 @@ example.com. A 192.0.2.1 www CNAME example.com. mail MX 10 example.com. + +; ENTs for NSEC3 testing purposes. +some.ent A 127.0.0.1 +x.y.mail A 127.0.0.1 +a.b.c.mail A 127.0.0.1 + +; An unsigned delegation for NSEC3 testing purposes. +unsigned NS some.other.ns.net + +; A signed delegation for NSEC3 testing purposes. +signed NS some.other.ns.net + DS 60485 5 1 ( 2BB183AF5F22588179A53B0A 98631FAD1A292118 ) From 40c678cf1f1534b2123d61cbef047501a2bc6f62 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 27 Dec 2024 08:29:17 +0100 Subject: [PATCH 266/569] Correct outdated code comment. --- src/sign/records.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 260bdcc64..c0d7e604c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -429,8 +429,8 @@ where nsec3_flags |= 0b0000_0001; } - // RFC 5155 7.1 step 5: _"Sort the set of NSEC3 RRs into hash order." - // We store the NSEC3s as we create them in a self-sorting vec. + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash order." + // We store the NSEC3s as we create them and sort them afterwards. let mut nsec3s = Vec::>>::new(); let mut ents = Vec::::new(); From 7165146b24b5c10b5390a8511a3727a738e69ed3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 27 Dec 2024 08:30:57 +0100 Subject: [PATCH 267/569] Improved/additional logging during NSEC3 generation. --- src/sign/records.rs | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index c0d7e604c..51b786da4 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -17,7 +17,7 @@ use std::{fmt, slice}; use bytes::Bytes; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; -use tracing::{debug, enabled, Level}; +use tracing::{debug, enabled, trace, Level}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; @@ -456,15 +456,25 @@ where // }; for family in families { + trace!("Family: {}", family.family_name().owner()); + // If the owner is out of zone, we have moved out of our zone and // are done. if !family.is_in_zone(apex) { + debug!( + "Stopping NSEC3 generation at out-of-zone family {}", + family.family_name().owner() + ); break; } // If the family is below a zone cut, we must ignore it. if let Some(ref cut) = cut { if family.owner().ends_with(cut.owner()) { + debug!( + "Excluding family {} as it is below a zone cut", + family.family_name().owner() + ); continue; } } @@ -476,6 +486,10 @@ where // family name for later. This also means below that if // `cut.is_some()` we are at the parent side of a zone. cut = if family.is_zone_cut(apex) { + trace!( + "Zone cut detected at family {}", + family.family_name().owner() + ); Some(name.clone()) } else { None @@ -486,6 +500,7 @@ where // delegations MAY be excluded." let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { + debug!("Excluding family {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",family.family_name().owner()); continue; } @@ -508,6 +523,11 @@ where let distance_to_root = name.owner().iter_labels().count(); let distance_to_apex = distance_to_root - apex_label_count; if distance_to_apex > last_nent_distance_to_apex { + trace!( + "Possible ENT detected at family {}", + family.family_name().owner() + ); + // 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 @@ -541,6 +561,7 @@ where builder.append_origin(&apex_owner).unwrap().into(); if let Err(pos) = ents.binary_search(&name) { + debug!("Found ENT at {name}"); ents.insert(pos, name); } } @@ -552,6 +573,7 @@ where // Authoritative RRsets will be signed. if cut.is_none() || has_ds { + trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut and has DS)"); bitmap.add(Rtype::RRSIG).unwrap(); } @@ -559,12 +581,15 @@ where // "For each RRSet at the original owner name, set the // corresponding bit in the Type Bit Maps field." for rrset in family.rrsets() { + trace!("Adding {} to the bitmap", rrset.rtype()); bitmap.add(rrset.rtype()).unwrap(); } if distance_to_apex == 0 { + trace!("Adding NSEC3PARAM to the bitmap as we are at the apex and RRSIG RRs are expected to be added"); bitmap.add(Rtype::NSEC3PARAM).unwrap(); if assume_dnskeys_will_be_added { + trace!("Adding DNSKEY to the bitmap as we are at the apex and DNSKEY RRs are expected to be added"); bitmap.add(Rtype::DNSKEY).unwrap(); } } @@ -599,6 +624,7 @@ where // Create the type bitmap, empty for an ENT NSEC3. let bitmap = RtypeBitmap::::builder(); + debug!("Generating NSEC3 RR for ENT at {name}"); let rec = Self::mk_nsec3( &name, hash_provider, @@ -624,6 +650,7 @@ where // value of the next NSEC3 RR in hash order. The next hashed owner // name of the last NSEC3 RR in the zone contains the value of the // hashed owner name of the first NSEC3 RR in the hash order." + trace!("Sorting NSEC3 RRs"); let mut nsec3s = SortedRecords::, S>::from(nsec3s); for i in 1..=nsec3s.records.len() { // TODO: Detect duplicate hashes. @@ -1453,7 +1480,7 @@ where add_used_dnskeys: bool, ) -> Result>>, SigningError> where - N: ToName + Clone + PartialEq + CanonicalOrd + Send, + N: ToName + Display + Clone + PartialEq + CanonicalOrd + Send, Octs: Clone + Send, { debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); @@ -1705,8 +1732,9 @@ where Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; res.push(rrsig_rr); debug!( - "Signed {} RRSET with keytag {}", + "Signed {} RRSET at {} with keytag {}", rrset.rtype(), + rrset.family_name().owner(), key.public_key().key_tag() ); } From e0cd6870179f859bcbf47173d1faf7753aa78e72 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 27 Dec 2024 08:31:08 +0100 Subject: [PATCH 268/569] Remove commented out code. --- src/sign/records.rs | 22 +--------------------- 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 51b786da4..2cc2cdf85 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -449,11 +449,6 @@ where 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 { trace!("Family: {}", family.family_name().owner()); @@ -606,11 +601,6 @@ where ttl, )?; - // 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. nsec3s.push(rec); @@ -637,10 +627,6 @@ where 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. nsec3s.push(rec); } @@ -687,13 +673,7 @@ where // // Handled above. - 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) - // } + Ok(Nsec3Records::new(nsec3s.records, nsec3param)) } pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> From f6df4fbbf15bdb74082cf9065e874dea5de36e5e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:07:26 +0100 Subject: [PATCH 269/569] Make signing work with any objects as keys as long as they can answer the basic question are they to be used for signing keys or other zone content, making DnssecSigningKey one possible way to represent the key, not the only possible way. --- src/sign/records.rs | 184 +++++++++++++++++++++++++++++--------------- 1 file changed, 122 insertions(+), 62 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 2cc2cdf85..361b96a7f 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1201,6 +1201,23 @@ pub enum Nsec3OptOut { // itself. Note that this means that the NSEC3 type itself will // never be present in the Type Bit Maps." +//------------ DesignatedSigningKey ------------------------------------------ + +pub trait DesignatedSigningKey: + Deref> +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + /// Should this key be used to "sign one or more other authentication keys + /// for a given zone" (RFC 4033 section 2 "Key Signing Key (KSK)"). + fn signs_keys(&self) -> bool; + + /// Should this key be used to "sign a zone" (RFC 4033 section 2 "Zone + /// Signing Key (ZSK)"). + fn signs_zone_data(&self) -> bool; +} + //------------ IntendedKeyPurpose -------------------------------------------- /// The purpose of a DNSSEC key from the perspective of an operator. @@ -1278,31 +1295,7 @@ impl DnssecSigningKey { } } - pub fn into_inner(self) -> SigningKey { - self.key - } -} - -impl Deref for DnssecSigningKey { - type Target = SigningKey; - - fn deref(&self) -> &Self::Target { - &self.key - } -} - -impl DnssecSigningKey { - pub fn key(&self) -> &SigningKey { - &self.key - } - - pub fn purpose(&self) -> IntendedKeyPurpose { - self.purpose - } -} - -impl, Inner: SignRaw> DnssecSigningKey { - pub fn ksk(key: SigningKey) -> Self { + pub fn new_ksk(key: SigningKey) -> Self { Self { key, purpose: IntendedKeyPurpose::KSK, @@ -1310,7 +1303,7 @@ impl, Inner: SignRaw> DnssecSigningKey { } } - pub fn zsk(key: SigningKey) -> Self { + pub fn new_zsk(key: SigningKey) -> Self { Self { key, purpose: IntendedKeyPurpose::ZSK, @@ -1318,7 +1311,7 @@ impl, Inner: SignRaw> DnssecSigningKey { } } - pub fn csk(key: SigningKey) -> Self { + pub fn new_csk(key: SigningKey) -> Self { Self { key, purpose: IntendedKeyPurpose::CSK, @@ -1326,27 +1319,97 @@ impl, Inner: SignRaw> DnssecSigningKey { } } - pub fn inactive(key: SigningKey) -> Self { + pub fn new_inactive_key(key: SigningKey) -> Self { Self { key, purpose: IntendedKeyPurpose::Inactive, _phantom: Default::default(), } } +} + +impl DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + pub fn purpose(&self) -> IntendedKeyPurpose { + self.purpose + } + + pub fn into_inner(self) -> SigningKey { + self.key + } - pub fn inferred(key: SigningKey) -> Self { + // Note: This cannot be done as impl AsRef because AsRef requires that the + // lifetime of the returned reference be 'static, and we don't do impl Any + // as then the caller has to deal with Option or Result because the type + // might not impl DesignatedSigningKey. + pub fn as_designated_signing_key( + &self, + ) -> &dyn DesignatedSigningKey { + self + } +} + +//--- impl Deref + +impl Deref for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + type Target = SigningKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +//--- impl From + +impl From> + for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + fn from(key: SigningKey) -> Self { let public_key = key.public_key(); match ( public_key.is_secure_entry_point(), public_key.is_zone_signing_key(), ) { - (true, _) => Self::ksk(key), - (false, true) => Self::zsk(key), - (false, false) => Self::inactive(key), + (true, _) => Self::new_ksk(key), + (false, true) => Self::new_zsk(key), + (false, false) => Self::new_inactive_key(key), } } } +//--- impl DesignatedSigningKey + +impl DesignatedSigningKey + for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + fn signs_keys(&self) -> bool { + matches!( + self.purpose, + IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK + ) + } + + fn signs_zone_data(&self) -> bool { + matches!( + self.purpose, + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + } +} + //------------ Operations ---------------------------------------------------- // TODO: Move nsecs() and nsecs3() out of SortedRecords and make them also @@ -1356,46 +1419,43 @@ impl, Inner: SignRaw> DnssecSigningKey { // iterator over the Zone). Also move out the helper functions. Maybe put them // all into a Signer struct? -pub trait SigningKeyUsageStrategy { +pub trait SigningKeyUsageStrategy +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ const NAME: &'static str; fn select_signing_keys_for_rtype( - candidate_keys: &[DnssecSigningKey], + candidate_keys: &[&dyn DesignatedSigningKey], rtype: Option, ) -> HashSet { - match rtype { - Some(Rtype::DNSKEY) => Self::filter_keys(candidate_keys, |k| { - matches!( - k.purpose(), - IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK - ) - }), - - _ => Self::filter_keys(candidate_keys, |k| { - matches!( - k.purpose(), - IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK - ) - }), + if matches!(rtype, Some(Rtype::DNSKEY)) { + Self::filter_keys(candidate_keys, |k| k.signs_keys()) + } else { + Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) } } fn filter_keys( - candidate_keys: &[DnssecSigningKey], - filter: fn(&DnssecSigningKey) -> bool, + candidate_keys: &[&dyn DesignatedSigningKey], + filter: fn(&dyn DesignatedSigningKey) -> bool, ) -> HashSet { candidate_keys .iter() .enumerate() - .filter_map(|(i, k)| filter(k).then_some(i)) + .filter_map(|(i, &k)| filter(k).then_some(i)) .collect::>() } } pub struct DefaultSigningKeyUsageStrategy; -impl SigningKeyUsageStrategy +impl SigningKeyUsageStrategy for DefaultSigningKeyUsageStrategy +where + Octs: AsRef<[u8]>, + Inner: SignRaw, { const NAME: &'static str = "Default key usage strategy"; } @@ -1406,6 +1466,7 @@ pub struct Signer< KeyStrat = DefaultSigningKeyUsageStrategy, Sort = DefaultSorter, > where + Octs: AsRef<[u8]>, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, @@ -1416,6 +1477,7 @@ pub struct Signer< impl Default for Signer where + Octs: AsRef<[u8]>, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, @@ -1427,6 +1489,7 @@ where impl Signer where + Octs: AsRef<[u8]>, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, @@ -1456,7 +1519,7 @@ where &self, apex: &FamilyName, families: RecordsIter<'_, N, ZoneRecordData>, - keys: &[DnssecSigningKey], + keys: &[&dyn DesignatedSigningKey], add_used_dnskeys: bool, ) -> Result>>, SigningError> where @@ -1518,7 +1581,7 @@ where ); for idx in &keys_in_use_idxs { - let key = keys[**idx].key(); + let key = keys[**idx]; let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); let is_non_dnskey_signing_key = @@ -1595,9 +1658,8 @@ where soa_minimum_ttl }; - for public_key in keys_in_use_idxs - .iter() - .map(|&&idx| keys[idx].key().public_key()) + for public_key in + keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) { let dnskey = public_key.to_dnskey(); @@ -1642,8 +1704,7 @@ where &non_dnskey_signing_key_idxs }; - for key in signing_key_idxs.iter().map(|&idx| keys[idx].key()) - { + for key in signing_key_idxs.iter().map(|&idx| keys[idx]) { // A copy of the family name. We’ll need it later. let name = apex_family.family_name().cloned(); @@ -1704,9 +1765,8 @@ where } } - for key in non_dnskey_signing_key_idxs - .iter() - .map(|&idx| keys[idx].key()) + for key in + non_dnskey_signing_key_idxs.iter().map(|&idx| keys[idx]) { let rrsig_rr = Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; From 340a70a5ab5c3ad1268cde3f870aae8fde2eab09 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:07:47 +0100 Subject: [PATCH 270/569] Minor import cleanup. --- src/sign/records.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 361b96a7f..3b60d135e 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -30,13 +30,12 @@ use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{ Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, }; +use crate::sign::{SignRaw, SigningKey}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; -use super::{SignRaw, SigningKey}; - //------------ Sorter -------------------------------------------------------- /// A DNS resource record sorter. From a4492ce85bb05d7af2180600d3503f4369e3dcd0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:08:37 +0100 Subject: [PATCH 271/569] Comment tweaks. --- src/sign/records.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 3b60d135e..509692931 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -347,9 +347,9 @@ where let mut bitmap = RtypeBitmap::::builder(); // RFC 4035 section 2.3: - // "The type bitmap of every NSEC resource record in a signed - // zone MUST indicate the presence of both the NSEC record - // itself and its corresponding RRSIG record." + // "The type bitmap of every NSEC resource record in a signed + // zone MUST indicate the presence of both the NSEC record + // itself and its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); if assume_dnskeys_will_be_added && family.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. @@ -462,7 +462,9 @@ where break; } - // If the family is below a zone cut, we must ignore it. + // If the family is below a zone cut, we must ignore it. As the + // RRs are required to be sorted all RRs below a zone cut should + // be encountered after the cut itself. if let Some(ref cut) = cut { if family.owner().ends_with(cut.owner()) { debug!( @@ -561,8 +563,7 @@ where } } - // Create the type bitmap, assume there will be an RRSIG and an - // NSEC3PARAM. + // Create the type bitmap. let mut bitmap = RtypeBitmap::::builder(); // Authoritative RRsets will be signed. @@ -1260,7 +1261,7 @@ pub enum IntendedKeyPurpose { //------------ DnssecSigningKey ---------------------------------------------- -/// A key to be provided by an operator to a DNSSEC signer. +/// A key that can be used for DNSSEC signing. /// /// This type carries metadata that signals to a DNSSEC signer how this key /// should impact the zone to be signed. From 880f3349bc45128e856a076097bfe4049ce5683c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 1 Jan 2025 16:09:08 +0100 Subject: [PATCH 272/569] FIX: Neither NSEC and NSEC3 nor hashing should include non-authoritative RR types in the Type Bitmap. --- src/sign/records.rs | 120 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 114 insertions(+), 6 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 509692931..f17c1c950 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -357,7 +357,18 @@ where } bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { - bitmap.add(rrset.rtype()).unwrap() + // RFC 4035 section 2.3: + // "The bitmap for the NSEC RR at a delegation point + // requires special attention. Bits corresponding to the + // delegation NS RRset and any RRsets for which the parent + // zone has authoritative data MUST be set; bits + // corresponding to any non-NS RRset for which the parent + // is not authoritative MUST be clear." + if cut.is_none() + || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) + { + bitmap.add(rrset.rtype()).unwrap() + } } prev = Some((name, bitmap.finalize())); @@ -494,8 +505,18 @@ where // RFC 5155 7.1 step 2: // "If Opt-Out is being used, owner names of unsigned // delegations MAY be excluded." + // Note that: + // - A "delegation inherently happens at a zone cut" (RFC 9499). + // - An "unsigned delegation" aka an "insecure delegation" is a + // "signed name containing a delegation (NS RRset), but + // lacking a DS RRset, signifying a delegation to an unsigned + // subzone" (RFC 9499). + // So we need to check for whether Opt-Out is being used at a zone + // cut that lacks a DS RR. We determine whether or not a DS RR is + // present even when Opt-Out is not being used because we also + // need to know there at a later step. let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - if cut.is_some() && !has_ds && opt_out == Nsec3OptOut::OptOut { + if opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { debug!("Excluding family {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",family.family_name().owner()); continue; } @@ -566,18 +587,105 @@ where // Create the type bitmap. let mut bitmap = RtypeBitmap::::builder(); - // Authoritative RRsets will be signed. + // Authoritative RRsets will be signed by `sign()` so add the + // expected future RRSIG type now to the NSEC3 Type Bitmap we are + // constructing. + // + // RFC 4033 section 2: + // 2. Definitions of Important DNSSEC Terms + // Authoritative RRset: Within the context of a particular + // zone, an RRset is "authoritative" if and only if the + // owner name of the RRset lies within the subset of the + // name space that is at or below the zone apex and at or + // above the cuts that separate the zone from its children, + // if any. All RRsets at the zone apex are authoritative, + // except for certain RRsets at this domain name that, if + // present, belong to this zone's parent. These RRset could + // include a DS RRset, the NSEC RRset referencing this DS + // RRset (the "parental NSEC"), and RRSIG RRs associated + // with these RRsets, all of which are authoritative in the + // parent zone. Similarly, if this zone contains any + // delegation points, only the parental NSEC RRset, DS + // RRsets, and any RRSIG RRs associated with these RRsets + // are authoritative for this zone. if cut.is_none() || has_ds { - trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut and has DS)"); + trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut or has a DS RR)"); bitmap.add(Rtype::RRSIG).unwrap(); } // RFC 5155 7.1 step 3: // "For each RRSet at the original owner name, set the // corresponding bit in the Type Bit Maps field." + // + // Note: When generating NSEC RRs (not NSEC3 RRs) RFC 4035 makes + // it clear that non-authoritative RRs should not be represented + // in the Type Bitmap but for NSEC3 generation that's less clear. + // + // RFC 4035 section 2.3: + // 2.3. Including NSEC RRs in a Zone + // ... + // "bits corresponding to any non-NS RRset for which the parent + // is not authoritative MUST be clear." + // + // RFC 5155 section 7.1: + // 7.1. Zone Signing + // ... + // "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." + // + // Thus the rules for the types to include in the Type Bitmap for + // NSEC RRs appear to be different for NSEC3 RRs. However, in + // practice common tooling implementations exclude types from the + // NSEC3 which are non-authoritative (e.g. glue and occluded + // records). One could argue that the following fragments of RFC + // 5155 support this: + // + // RFC 5155 section 7.1. + // 7.1. Zone Signing + // ... + // "Other non-authoritative RRs are not represented by + // NSEC3 RRs." + // ... + // "2. For each unique original owner name in the zone add an + // NSEC3 RR." + // + // (if one reads "in the zone" to exclude data occluded by a zone + // cut or glue records that are only authoritative in the child + // zone and not in the parent zone). + // + // RFC 4033 could also be interpreted as excluding + // non-authoritative data from DNSSEC and thus NSEC3: + // + // RFC 4033 section 9: + // 9. Name Server Considerations + // ... + // "By itself, DNSSEC is not enough to protect the integrity of + // an entire zone during zone transfer operations, as even a + // signed zone contains some unsigned, nonauthoritative data if + // the zone has any children." + // + // As such we exclude non-authoritative RRs from the NSEC3 Type + // Bitmap, with the EXCEPTION of the NS RR at a secure delegation + // as insecure delegations are explicitly included by RFC 5155: + // + // RFC 5155 section 7.1: + // 7.1. Zone Signing + // ... + // "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." for rrset in family.rrsets() { - trace!("Adding {} to the bitmap", rrset.rtype()); - bitmap.add(rrset.rtype()).unwrap(); + if cut.is_none() + || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) + { + trace!("Adding {} to the bitmap", rrset.rtype()); + bitmap.add(rrset.rtype()).unwrap(); + } } if distance_to_apex == 0 { From c90026d80ca01b9d93d32c9b1af0b928618db860 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:10:19 +0100 Subject: [PATCH 273/569] Add Rtype::is_pseudo() for use by NSEC and NSEC3 logic. --- src/base/iana/rtype.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/base/iana/rtype.rs b/src/base/iana/rtype.rs index a6546476a..8f41b08f2 100644 --- a/src/base/iana/rtype.rs +++ b/src/base/iana/rtype.rs @@ -440,4 +440,21 @@ impl Rtype { pub fn is_glue(&self) -> bool { matches!(*self, Rtype::A | Rtype::AAAA) } + + /// Returns true if this record type represents a pseudo-RR. + /// + /// The term "pseudo-RR" appears in [RFC + /// 9499](https://datatracker.ietf.org/doc/rfc9499/) Section 5 "Resource + /// Records" as an alias for "meta-RR" and is referenced by [RFC + /// 4034](https://datatracker.ietf.org/doc/rfc4034)/) in the context of + /// NSEC to denote types that "do not appear in zone data", with [RFC + /// 5155](https://datatracker.ietf.org/doc/rfc5155/) having text with + /// presumably the same goal but defined in terms of "META-TYPE" and + /// "QTYPE", the latter collectively being defined by [RFC + /// 2929](https://datatracker.ietf.org/doc/rfc2929/) and later as having + /// the decimal range 128 - 255 but with section 3.1 explicitly noting OPT + /// (TYPE 41) as an exception. + pub fn is_pseudo(&self) -> bool { + self.0 == 41 || (self.0 >= 128 && self.0 <= 255) + } } From 03b70ca2560ff0fbe083ad17ed5834febc65e2df Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:39:08 +0100 Subject: [PATCH 274/569] Implement MUST constraints from RFC 4034 and RFC 5155 excluding "pseudo" RRtypes from NSEC(3) type bitmaps. --- src/sign/records.rs | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index f17c1c950..f255cf80a 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -357,7 +357,7 @@ where } bitmap.add(Rtype::NSEC).unwrap(); for rrset in family.rrsets() { - // RFC 4035 section 2.3: + // RFC 4034 section 4.1.2: (and also RFC 4035 section 2.3) // "The bitmap for the NSEC RR at a delegation point // requires special attention. Bits corresponding to the // delegation NS RRset and any RRsets for which the parent @@ -367,7 +367,15 @@ where if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) { - bitmap.add(rrset.rtype()).unwrap() + // RFC 4034 section 4.1.2: + // "Bits representing pseudo-types MUST be clear, as + // they do not appear in zone data." + // + // TODO: Should this check be moved into + // RtypeBitmapBuilder itself? + if !rrset.rtype().is_pseudo() { + bitmap.add(rrset.rtype()).unwrap() + } } } @@ -683,8 +691,19 @@ where if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) { - trace!("Adding {} to the bitmap", rrset.rtype()); - bitmap.add(rrset.rtype()).unwrap(); + // RFC 5155 section 3.2: + // "Bits representing Meta-TYPEs or QTYPEs as specified + // in Section 3.1 of [RFC2929] or within the range + // reserved for assignment only to QTYPEs and + // Meta-TYPEs MUST be set to 0, since they do not + // appear in zone data". + // + // TODO: Should this check be moved into + // RtypeBitmapBuilder itself? + if !rrset.rtype().is_pseudo() { + trace!("Adding {} to the bitmap", rrset.rtype()); + bitmap.add(rrset.rtype()).unwrap(); + } } } From 844418e5a904f5ce50ac34a1191d5588432f0d2b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:47:54 +0100 Subject: [PATCH 275/569] Replace the Signer with access to signing via new traits SignableZone and SignableZoneInPlace, removing the need for state to be held, especially as some of that state was better associated with the zone being signed than with the signer. Introduce a new structure under sign/ to hold types extracted by refactoring the long records.rs into separate modules. Add generalized RecordSet::update_data() instead of the very specific replace_soa() and replace_rrsign_for_apex_zonemd() functions. --- examples/keyset.rs | 2 +- src/sign/bin/signzone.rs | 156 --- src/sign/crypto/mod.rs | 2 + src/sign/{ => crypto}/openssl.rs | 9 +- src/sign/{ => crypto}/ring.rs | 9 +- src/sign/error.rs | 49 + src/sign/hashing/config.rs | 116 ++ src/sign/hashing/mod.rs | 3 + src/sign/hashing/nsec.rs | 117 ++ src/sign/hashing/nsec3.rs | 774 ++++++++++++ src/sign/{ => keys}/bytes.rs | 0 src/sign/keys/keymeta.rs | 214 ++++ src/sign/{common.rs => keys/keypair.rs} | 13 +- src/sign/{ => keys}/keyset.rs | 4 +- src/sign/keys/mod.rs | 4 + src/sign/mod.rs | 15 +- src/sign/records.rs | 1530 +---------------------- src/sign/signing/config.rs | 70 ++ src/sign/signing/mod.rs | 4 + src/sign/signing/rrsigs.rs | 389 ++++++ src/sign/signing/strategy.rs | 49 + src/sign/signing/traits.rs | 418 +++++++ src/sign/zone.rs | 1 + 23 files changed, 2278 insertions(+), 1670 deletions(-) delete mode 100644 src/sign/bin/signzone.rs create mode 100644 src/sign/crypto/mod.rs rename src/sign/{ => crypto}/openssl.rs (99%) rename src/sign/{ => crypto}/ring.rs (98%) create mode 100644 src/sign/error.rs create mode 100644 src/sign/hashing/config.rs create mode 100644 src/sign/hashing/mod.rs create mode 100644 src/sign/hashing/nsec.rs create mode 100644 src/sign/hashing/nsec3.rs rename src/sign/{ => keys}/bytes.rs (100%) create mode 100644 src/sign/keys/keymeta.rs rename src/sign/{common.rs => keys/keypair.rs} (97%) rename src/sign/{ => keys}/keyset.rs (99%) create mode 100644 src/sign/keys/mod.rs create mode 100644 src/sign/signing/config.rs create mode 100644 src/sign/signing/mod.rs create mode 100644 src/sign/signing/rrsigs.rs create mode 100644 src/sign/signing/strategy.rs create mode 100644 src/sign/signing/traits.rs create mode 100644 src/sign/zone.rs diff --git a/examples/keyset.rs b/examples/keyset.rs index 2fca00b5d..808cd461a 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -1,6 +1,6 @@ //! Demonstrate the use of key sets. use domain::base::Name; -use domain::sign::keyset::{ +use domain::sign::keys::keyset::{ Action, Error, KeySet, KeyType, RollType, UnixTime, }; use itertools::{Either, Itertools}; diff --git a/src/sign/bin/signzone.rs b/src/sign/bin/signzone.rs deleted file mode 100644 index 23b0d1aa1..000000000 --- a/src/sign/bin/signzone.rs +++ /dev/null @@ -1,156 +0,0 @@ -//! Signs a zone file. - -use std::io; -use std::fs::File; -use bytes::Bytes; -use domain_core::name::Dname; -use domain_core::rdata::MasterRecordData; -use domain_core::master::reader::{Reader, ReaderItem}; -use domain_core::record::Record; -use domain_core::serial::Serial; -use domain_sign::ring; -use domain_sign::sign::{FamilyName, SortedRecords}; -use ::ring::rand::SystemRandom; -use unwrap::unwrap; - - -fn main() { - let mut args = std::env::args(); - let _ = unwrap!(args.next()); - let infile = match args.next() { - Some(infile) => infile, - None => { - eprintln!("Usage: signzone []"); - std::process::exit(1) - } - }; - let outfile = args.next(); - - if let Err(err) = sign_zone(infile, outfile) { - eprintln!("{}", err); - std::process::exit(1) - } - -} - - -type Records = SortedRecords, MasterRecordData>>; - - -fn sign_zone(infile: String, outfile: Option) -> Result<(), io::Error> { - let rng = SystemRandom::new(); - let key = match ring::Key::throwaway_13(256, &rng) { - Ok(key) => key, - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "failed to create key" - )) - } - }; - let mut records = load_zone(infile)?; - let (apex, ttl) = find_apex(&records)?; - let nsecs = records.nsecs(&apex, ttl); - records.extend(nsecs.into_iter().map(Record::from_record)); - match apex.dnskey(ttl, &key) { - Ok(record) => { - let _ = records.insert(Record::from_record(record)); - } - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "Creating DNSKEY record failed." - )) - } - } - let inception = Serial::now().sub(10); - let expiration = inception.add(2592000); // XXX 30 days - let rrsigs = match records.sign(&apex, expiration, inception, &key) { - Ok(rrsigs) => rrsigs, - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "Signing failed." - )) - } - }; - records.extend(rrsigs.into_iter().map(Record::from_record)); - let _ds = match apex.ds(ttl, key) { - Ok(ds) => ds, - Err(_) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "Creating DS record failed." - )) - } - }; - - match outfile { - Some(path) => { - let mut file = File::create(path)?; - records.write(&mut file)?; - } - None => { - { - let stdout = io::stdout(); - let mut stdout = stdout.lock(); - records.write(&mut stdout)?; - } - println!(""); - } - } - //println!("{}", ds); - Ok(()) -} - - -fn load_zone(infile: String) -> Result { - let reader = Reader::open(infile)?; - let mut res = SortedRecords::new(); - for item in reader { - match item { - Ok(ReaderItem::Record(record)) => { - let _ = res.insert(record); - } - Ok(ReaderItem::Include {..}) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "$include not supported" - )) - } - Ok(ReaderItem::Control {name, ..}) => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("${} not supported", name) - )) - } - Err(err) => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("{}", err) - )) - } - } - } - Ok(res) -} - -fn find_apex( - records: &Records -) -> Result<(FamilyName>, u32), io::Error> { - let soa = match records.find_soa() { - Some(soa) => soa, - None => { - return Err(io::Error::new( - io::ErrorKind::Other, - "cannot find SOA record" - )) - } - }; - let ttl = match *soa.first().data() { - MasterRecordData::Soa(ref soa) => soa.minimum(), - _ => unreachable!() - }; - Ok((soa.family_name().cloned(), ttl)) -} - diff --git a/src/sign/crypto/mod.rs b/src/sign/crypto/mod.rs new file mode 100644 index 000000000..a60a4dd8a --- /dev/null +++ b/src/sign/crypto/mod.rs @@ -0,0 +1,2 @@ +pub mod openssl; +pub mod ring; diff --git a/src/sign/openssl.rs b/src/sign/crypto/openssl.rs similarity index 99% rename from src/sign/openssl.rs rename to src/sign/crypto/openssl.rs index a7250081a..20f1185e7 100644 --- a/src/sign/openssl.rs +++ b/src/sign/crypto/openssl.rs @@ -22,14 +22,11 @@ use openssl::{ }; use secrecy::ExposeSecret; -use crate::{ - base::iana::SecAlg, - validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, -}; - -use super::{ +use crate::base::iana::SecAlg; +use crate::sign::{ GenerateParams, RsaSecretKeyBytes, SecretKeyBytes, SignError, SignRaw, }; +use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; //----------- KeyPair -------------------------------------------------------- diff --git a/src/sign/ring.rs b/src/sign/crypto/ring.rs similarity index 98% rename from src/sign/ring.rs rename to src/sign/crypto/ring.rs index d1e29c395..52d1b1901 100644 --- a/src/sign/ring.rs +++ b/src/sign/crypto/ring.rs @@ -18,12 +18,9 @@ use ring::signature::{ }; use secrecy::ExposeSecret; -use crate::{ - base::iana::SecAlg, - validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}, -}; - -use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::base::iana::SecAlg; +use crate::sign::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; //----------- KeyPair -------------------------------------------------------- diff --git a/src/sign/error.rs b/src/sign/error.rs new file mode 100644 index 000000000..dd97a6d45 --- /dev/null +++ b/src/sign/error.rs @@ -0,0 +1,49 @@ +//! Actual signing. +use core::fmt::{Debug, Display}; + +use crate::validate::Nsec3HashError; + +//------------ SigningError -------------------------------------------------- + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum SigningError { + /// One or more keys does not have a signature validity period defined. + KeyLacksSignatureValidityPeriod, + + /// TODO + OutOfMemory, + + /// At least one key must be provided to sign with. + NoKeysProvided, + + /// None of the provided keys were deemed suitable by the + /// [`SigningKeyUsageStrategy`] used. + NoSuitableKeysFound, + + NoSoaFound, + + Nsec3HashingError(Nsec3HashError), + MissingSigningConfiguration, +} + +impl Display for SigningError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SigningError::KeyLacksSignatureValidityPeriod => { + f.write_str("KeyLacksSignatureValidityPeriod") + } + SigningError::OutOfMemory => f.write_str("OutOfMemory"), + SigningError::NoKeysProvided => f.write_str("NoKeysProvided"), + SigningError::NoSuitableKeysFound => { + f.write_str("NoSuitableKeysFound") + } + SigningError::NoSoaFound => f.write_str("NoSoaFound"), + SigningError::Nsec3HashingError(err) => { + f.write_fmt(format_args!("Nsec3HashingError: {err}")) + } + SigningError::MissingSigningConfiguration => { + f.write_str("MissingSigningConfiguration") + } + } + } +} diff --git a/src/sign/hashing/config.rs b/src/sign/hashing/config.rs new file mode 100644 index 000000000..a5717580e --- /dev/null +++ b/src/sign/hashing/config.rs @@ -0,0 +1,116 @@ +use core::convert::From; + +use std::vec::Vec; + +use super::nsec3::{ + Nsec3Config, Nsec3HashProvider, OnDemandNsec3HashProvider, +}; + +//------------ NsecToNsec3TransitionState ------------------------------------ + +/// The current state of an RFC 5155 section 10.4 NSEC to NSEC3 transition. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum NsecToNsec3TransitionState { + /// 1. Transition all DNSKEYs to DNSKEYs using the algorithm aliases + /// described in Section 2. The actual method for safely and securely + /// changing the DNSKEY RRSet of the zone is outside the scope of this + /// specification. However, the end result MUST be that all DS RRs in + /// the parent use the specified algorithm aliases. + /// + /// After this transition is complete, all NSEC3-unaware clients will + /// treat the zone as insecure. At this point, the authoritative + /// server still returns negative and wildcard responses that contain + /// NSEC RRs. + TransitioningDnskeys, + + /// 2. Add signed NSEC3 RRs to the zone, either incrementally or all at + /// once. If adding incrementally, then the last RRSet added MUST be + /// the NSEC3PARAM RRSet. + /// + /// 3. Upon the addition of the NSEC3PARAM RRSet, the server switches to + /// serving negative and wildcard responses with NSEC3 RRs according + /// to this specification. + AddingNsec3Records, + + /// 4. Remove the NSEC RRs either incrementally or all at once. + RemovingNsecRecords, + + /// 5. Done. + Transitioned, +} + +//------------ Nsec3ToNsecTransitionState ------------------------------------ + +/// The current state of an RFC 5155 section 10.5 NSEC3 to NSEC transition. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum Nsec3ToNsecTransitionState { + /// 1. Add NSEC RRs incrementally or all at once. + AddingNsecRecords, + + /// 2. Remove the NSEC3PARAM RRSet. This will signal the server to use + /// the NSEC RRs for negative and wildcard responses. + RemovingNsec3ParamRecord, + + /// 3. Remove the NSEC3 RRs either incrementally or all at once. + RemovingNsec3Records, + + /// 4. Transition all of the DNSKEYs to DNSSEC algorithm identifiers. + /// After this transition is complete, all NSEC3-unaware clients will + /// treat the zone as secure. + TransitioningDnskeys, + + /// 5. Done. + Transitioned, +} + +//------------ HashingConfig ------------------------------------------------- + +/// Hashing configuration for a DNSSEC signed zone. +/// +/// A DNSSEC signed zone must be hashed, either by NSEC or NSEC3. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum HashingConfig> +where + HP: Nsec3HashProvider, + O: AsRef<[u8]> + From<&'static [u8]>, +{ + /// The zone is already hashed. + Prehashed, + + /// The zone is NSEC hashed. + #[default] + Nsec, + + /// The zone is NSEC3 hashed, possibly more than once. + /// + /// https://datatracker.ietf.org/doc/html/rfc5155#section-7.3 + /// 7.3. Secondary Servers + /// ... + /// "If there are multiple NSEC3PARAM RRs present, there are multiple + /// valid NSEC3 chains present. The server must choose one of them, + /// but may use any criteria to do so." + /// + /// https://datatracker.ietf.org/doc/html/rfc5155#section-12.1.3 + /// 12.1.3. Transitioning to a New Hash Algorithm + /// "Although the NSEC3 and NSEC3PARAM RR formats include a hash + /// algorithm parameter, this document does not define a particular + /// mechanism for safely transitioning from one NSEC3 hash algorithm to + /// another. When specifying a new hash algorithm for use with NSEC3, + /// a transition mechanism MUST also be defined. It is possible that + /// the only practical and palatable transition mechanisms may require + /// an intermediate transition to an insecure state, or to a state that + /// uses NSEC records instead of NSEC3." + Nsec3(Nsec3Config, Vec>), + + /// The zone is transitioning from NSEC to NSEC3 hashing. + TransitioningNsecToNsec3( + Nsec3Config, + NsecToNsec3TransitionState, + ), + + /// The zone is transitioning from NSEC3 to NSEC hashing. + TransitioningNsec3ToNsec( + Nsec3Config, + Nsec3ToNsecTransitionState, + ), +} diff --git a/src/sign/hashing/mod.rs b/src/sign/hashing/mod.rs new file mode 100644 index 000000000..4716fcae3 --- /dev/null +++ b/src/sign/hashing/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod nsec; +pub mod nsec3; diff --git a/src/sign/hashing/nsec.rs b/src/sign/hashing/nsec.rs new file mode 100644 index 000000000..c1ecf14ca --- /dev/null +++ b/src/sign/hashing/nsec.rs @@ -0,0 +1,117 @@ +use core::fmt::Debug; + +use std::vec::Vec; + +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; + +use crate::base::iana::Rtype; +use crate::base::name::ToName; +use crate::base::record::Record; +use crate::base::Ttl; +use crate::rdata::dnssec::RtypeBitmap; +use crate::rdata::{Nsec, ZoneRecordData}; +use crate::sign::records::{FamilyName, RecordsIter}; + +// TODO: Add (mutable?) iterator based variant. +pub fn generate_nsecs( + apex: &FamilyName, + ttl: Ttl, + mut families: RecordsIter<'_, N, ZoneRecordData>, + assume_dnskeys_will_be_added: bool, +) -> Vec>> +where + N: ToName + Clone + PartialEq, + Octs: FromBuilder, + Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, + ::AppendError: Debug, +{ + let mut res = Vec::new(); + + // The owner name of a zone cut if we currently are at or below one. + let mut cut: Option> = None; + + // Since the records are ordered, the first family is the apex -- we can + // skip everything before that. + families.skip_before(apex); + + // Because of the next name thing, we need to keep the last NSEC around. + let mut prev: Option<(FamilyName, RtypeBitmap)> = None; + + // We also need the apex for the last NSEC. + let apex_owner = families.first_owner().clone(); + + for family in families { + // If the owner is out of zone, we have moved out of our zone and are + // done. + if !family.is_in_zone(apex) { + break; + } + + // If the family is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the family + // name for later. This also means below that if `cut.is_some()` we + // are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + Some(name.clone()) + } else { + None + }; + + if let Some((prev_name, bitmap)) = prev.take() { + res.push( + prev_name.into_record( + ttl, + Nsec::new(name.owner().clone(), bitmap), + ), + ); + } + + let mut bitmap = RtypeBitmap::::builder(); + // RFC 4035 section 2.3: + // "The type bitmap of every NSEC resource record in a signed zone + // MUST indicate the presence of both the NSEC record itself and + // its corresponding RRSIG record." + bitmap.add(Rtype::RRSIG).unwrap(); + if assume_dnskeys_will_be_added && family.owner() == &apex_owner { + // Assume there's gonna be a DNSKEY. + bitmap.add(Rtype::DNSKEY).unwrap(); + } + bitmap.add(Rtype::NSEC).unwrap(); + for rrset in family.rrsets() { + // RFC 4034 section 4.1.2: (and also RFC 4035 section 2.3) + // "The bitmap for the NSEC RR at a delegation point requires + // special attention. Bits corresponding to the delegation NS + // RRset and any RRsets for which the parent zone has + // authoritative data MUST be set; bits corresponding to any + // non-NS RRset for which the parent is not authoritative MUST + // be clear." + if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) + { + // RFC 4034 section 4.1.2: + // "Bits representing pseudo-types MUST be clear, as they do + // not appear in zone data." + // + // TODO: Should this check be moved into RtypeBitmapBuilder + // itself? + if !rrset.rtype().is_pseudo() { + bitmap.add(rrset.rtype()).unwrap() + } + } + } + + prev = Some((name, bitmap.finalize())); + } + if let Some((prev_name, bitmap)) = prev { + res.push(prev_name.into_record(ttl, Nsec::new(apex_owner, bitmap))); + } + res +} diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs new file mode 100644 index 000000000..0b88ef95d --- /dev/null +++ b/src/sign/hashing/nsec3.rs @@ -0,0 +1,774 @@ +use core::convert::From; +use core::fmt::{Debug, Display}; +use core::marker::{PhantomData, Send}; + +use std::hash::Hash; +use std::string::String; +use std::vec::Vec; + +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; +use octseq::{FreezeBuilder, OctetsFrom}; +use tracing::{debug, trace}; + +use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; +use crate::base::name::{ToLabelIter, ToName}; +use crate::base::{Name, NameBuilder, Record, Ttl}; +use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; +use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; +use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; +use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; +use crate::utils::base32; +use crate::validate::{nsec3_hash, Nsec3HashError}; + +/// Generate [RFC5155] NSEC3 and NSEC3PARAM records for this record set. +/// +/// This function does NOT enforce use of current best practice settings, as +/// defined by [RFC 5155], [RFC 9077] and [RFC 9276] which state that: +/// +/// - The `ttl` should be the _"lesser of the MINIMUM field of the zone SOA RR +/// and the TTL of the zone SOA RR itself"_. +/// +/// - The `params` should be set to _"SHA-1, no extra iterations, empty salt"_ +/// and zero flags. See [`Nsec3param::default()`]. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html +/// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html +/// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html +// TODO: Add mutable iterator based variant. +pub fn generate_nsec3s( + apex: &FamilyName, + ttl: Ttl, + mut families: RecordsIter<'_, N, ZoneRecordData>, + params: Nsec3param, + opt_out: Nsec3OptOut, + assume_dnskeys_will_be_added: bool, + hash_provider: &mut HashProvider, +) -> Result, Nsec3HashError> +where + N: ToName + Clone + Display + Ord + Hash + Send + From>, + N: From::Octets>>, + Octs: FromBuilder + OctetsFrom> + Default + Clone + Send, + Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, + ::AppendError: Debug, + OctsMut: OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder, + ::Octets: AsRef<[u8]>, + HashProvider: Nsec3HashProvider, + Sort: Sorter, +{ + // TODO: + // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) + // - RFC 5155 section 2 Backwards compatibility: Reject old algorithms? + // if not, map 3 to 6 and 5 to 7, or reject use of 3 and 5? + + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, set the Opt-Out bit to one." + let mut nsec3_flags = params.flags(); + if matches!(opt_out, Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly) { + // Set the Opt-Out flag. + nsec3_flags |= 0b0000_0001; + } + + // RFC 5155 7.1 step 5: + // "Sort the set of NSEC3 RRs into hash order." We store the NSEC3s as + // we create them and sort them afterwards. + let mut nsec3s = Vec::>>::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; + + // Since the records are ordered, the first family is the apex -- we can + // skip everything before that. + families.skip_before(apex); + + // We also need the apex for the last NSEC. + let apex_owner = families.first_owner().clone(); + let apex_label_count = apex_owner.iter_labels().count(); + + let mut last_nent_stack: Vec = vec![]; + + for family in families { + trace!("Family: {}", family.family_name().owner()); + + // If the owner is out of zone, we have moved out of our zone and are + // done. + if !family.is_in_zone(apex) { + debug!( + "Stopping NSEC3 generation at out-of-zone family {}", + family.family_name().owner() + ); + break; + } + + // If the family is below a zone cut, we must ignore it. As the RRs + // are required to be sorted all RRs below a zone cut should be + // encountered after the cut itself. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + debug!( + "Excluding family {} as it is below a zone cut", + family.family_name().owner() + ); + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the family + // name for later. This also means below that if `cut.is_some()` we + // are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + trace!( + "Zone cut detected at family {}", + family.family_name().owner() + ); + Some(name.clone()) + } else { + None + }; + + // RFC 5155 7.1 step 2: + // "If Opt-Out is being used, owner names of unsigned delegations + // MAY be excluded." + // Note that: + // - A "delegation inherently happens at a zone cut" (RFC 9499). + // - An "unsigned delegation" aka an "insecure delegation" is a + // "signed name containing a delegation (NS RRset), but lacking a + // DS RRset, signifying a delegation to an unsigned subzone" (RFC + // 9499). + // So we need to check for whether Opt-Out is being used at a zone cut + // that lacks a DS RR. We determine whether or not a DS RR is present + // even when Opt-Out is not being used because we also need to know + // there at a later step. + let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); + if opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { + debug!("Excluding family {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",family.family_name().owner()); + continue; + } + + // RFC 5155 7.1 step 4: + // "If the difference in number of labels between the apex and 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 > last_nent_distance_to_apex { + trace!( + "Possible ENT detected at family {}", + family.family_name().owner() + ); + + // 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 then + // non_empty_label_count would be equal to label_distance. If it + // is less that means there are ENTs between us and the last + // non-empty label in our ancestor path to the apex. + + // Walk from the owner name down the tree of labels from the last + // known non-empty non-terminal label, extending the name each + // time by one label until we get to the current name. + + // Given a.b.c.mail.example.com where: + // - example.com is the apex owner + // - mail.example.com was the last non-empty non-terminal + // This loop will construct the names: + // - c.mail.example.com + // - b.c.mail.example.com + // 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 + 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. + let mut builder = NameBuilder::::new(); + for label in rev_label_it.take(distance_to_apex - n) { + builder.append_label(label.as_slice()).unwrap(); + } + let name = builder.append_origin(&apex_owner).unwrap().into(); + + if let Err(pos) = ents.binary_search(&name) { + debug!("Found ENT at {name}"); + ents.insert(pos, name); + } + } + } + + // Create the type bitmap. + let mut bitmap = RtypeBitmap::::builder(); + + // Authoritative RRsets will be signed by `sign()` so add the expected + // future RRSIG type now to the NSEC3 Type Bitmap we are constructing. + // + // RFC 4033 section 2: + // 2. Definitions of Important DNSSEC Terms + // Authoritative RRset: Within the context of a particular zone, an + // RRset is "authoritative" if and only if the owner name of the + // RRset lies within the subset of the name space that is at or + // below the zone apex and at or above the cuts that separate + // the zone from its children, if any. All RRsets at the zone + // apex are authoritative, except for certain RRsets at this + // domain name that, if present, belong to this zone's parent. + // These RRset could include a DS RRset, the NSEC RRset + // referencing this DS RRset (the "parental NSEC"), and RRSIG + // RRs associated with these RRsets, all of which are + // authoritative in the parent zone. Similarly, if this zone + // contains any delegation points, only the parental NSEC RRset, + // DS RRsets, and any RRSIG RRs associated with these RRsets are + // authoritative for this zone. + if cut.is_none() || has_ds { + trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut or has a DS RR)"); + bitmap.add(Rtype::RRSIG).unwrap(); + } + + // RFC 5155 7.1 step 3: + // "For each RRSet at the original owner name, set the corresponding + // bit in the Type Bit Maps field." + // + // Note: When generating NSEC RRs (not NSEC3 RRs) RFC 4035 makes it + // clear that non-authoritative RRs should not be represented in the + // Type Bitmap but for NSEC3 generation that's less clear. + // + // RFC 4035 section 2.3: + // 2.3. Including NSEC RRs in a Zone + // ... + // "bits corresponding to any non-NS RRset for which the parent is + // not authoritative MUST be clear." + // + // RFC 5155 section 7.1: + // 7.1. Zone Signing + // ... + // "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." + // + // Thus the rules for the types to include in the Type Bitmap for NSEC + // RRs appear to be different for NSEC3 RRs. However, in practice + // common tooling implementations exclude types from the NSEC3 which + // are non-authoritative (e.g. glue and occluded records). One could + // argue that the following fragments of RFC 5155 support this: + // + // RFC 5155 section 7.1. + // 7.1. Zone Signing + // ... + // "Other non-authoritative RRs are not represented by NSEC3 RRs." + // ... + // "2. For each unique original owner name in the zone add an NSEC3 + // RR." + // + // (if one reads "in the zone" to exclude data occluded by a zone cut + // or glue records that are only authoritative in the child zone and + // not in the parent zone). + // + // RFC 4033 could also be interpreted as excluding non-authoritative + // data from DNSSEC and thus NSEC3: + // + // RFC 4033 section 9: + // 9. Name Server Considerations + // ... + // "By itself, DNSSEC is not enough to protect the integrity of an + // entire zone during zone transfer operations, as even a signed + // zone contains some unsigned, nonauthoritative data if the zone + // has any children." + // + // As such we exclude non-authoritative RRs from the NSEC3 Type + // Bitmap, with the EXCEPTION of the NS RR at a secure delegation as + // insecure delegations are explicitly included by RFC 5155: + // + // RFC 5155 section 7.1: + // 7.1. Zone Signing + // ... + // "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." + for rrset in family.rrsets() { + if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) + { + // RFC 5155 section 3.2: + // "Bits representing Meta-TYPEs or QTYPEs as specified in + // Section 3.1 of [RFC2929] or within the range reserved + // for assignment only to QTYPEs and Meta-TYPEs MUST be set + // to 0, since they do not appear in zone data". + // + // TODO: Should this check be moved into RtypeBitmapBuilder + // itself? + if !rrset.rtype().is_pseudo() { + trace!("Adding {} to the bitmap", rrset.rtype()); + bitmap.add(rrset.rtype()).unwrap(); + } + } + } + + if distance_to_apex == 0 { + trace!("Adding NSEC3PARAM to the bitmap as we are at the apex and RRSIG RRs are expected to be added"); + bitmap.add(Rtype::NSEC3PARAM).unwrap(); + if assume_dnskeys_will_be_added { + trace!("Adding DNSKEY to the bitmap as we are at the apex and DNSKEY RRs are expected to be added"); + bitmap.add(Rtype::DNSKEY).unwrap(); + } + } + + let rec: Record> = mk_nsec3( + name.owner(), + hash_provider, + params.hash_algorithm(), + nsec3_flags, + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + // Store the record by order of its owner name. + nsec3s.push(rec); + + 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(); + + debug!("Generating NSEC3 RR for ENT at {name}"); + let rec = mk_nsec3( + &name, + hash_provider, + params.hash_algorithm(), + nsec3_flags, + params.iterations(), + params.salt(), + &apex_owner, + bitmap, + ttl, + )?; + + // Store the record by order of its owner name. + nsec3s.push(rec); + } + + // RFC 5155 7.1 step 7: + // "In each NSEC3 RR, insert the next hashed owner name by using the + // value of the next NSEC3 RR in hash order. The next hashed owner + // name of the last NSEC3 RR in the zone contains the value of the + // hashed owner name of the first NSEC3 RR in the hash order." + trace!("Sorting NSEC3 RRs"); + let mut nsec3s = SortedRecords::, Sort>::from(nsec3s); + let num_nsec3s = nsec3s.len(); + for i in 1..=num_nsec3s { + // TODO: Detect duplicate hashes. + let next_i = if i == num_nsec3s { 0 } else { i }; + let cur_owner = nsec3s.as_slice()[next_i].owner(); + let name: Name = cur_owner.try_to_name().unwrap(); + let label = name.iter_labels().next().unwrap(); + let owner_hash = if let Ok(hash_octets) = + base32::decode_hex(&format!("{label}")) + { + OwnerHash::::from_octets(hash_octets).unwrap() + } else { + OwnerHash::::from_octets(name.as_octets().clone()).unwrap() + }; + let last_rec = &mut nsec3s.as_mut_slice()[i - 1]; + let last_nsec3: &mut Nsec3 = last_rec.data_mut(); + last_nsec3.set_next_owner(owner_hash.clone()); + } + + // RFC 5155 7.1 step 8: + // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, + // Iterations, and Salt fields to the zone apex." + let nsec3param = Record::new( + apex.owner().try_to_name::().unwrap().into(), + Class::IN, + ttl, + params, + ); + + // RFC 5155 7.1 after step 8: + // "If a hash collision is detected, then a new salt has to be + // chosen, and the signing process restarted." + // + // Handled above. + + Ok(Nsec3Records::new(nsec3s.into_inner(), nsec3param)) +} + +#[allow(clippy::too_many_arguments)] +fn mk_nsec3( + name: &N, + hash_provider: &mut HashProvider, + alg: Nsec3HashAlg, + flags: u8, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, + bitmap: RtypeBitmapBuilder<::Builder>, + ttl: Ttl, +) -> Result>, Nsec3HashError> +where + N: ToName + From>, + Octs: FromBuilder + Clone + Default, + ::Builder: + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, + HashProvider: Nsec3HashProvider, +{ + let owner_name = hash_provider.get_or_create(apex_owner, name)?; + + // RFC 5155 7.1. step 2: + // "The Next Hashed Owner Name field is left blank for the moment." + // Create a placeholder next owner, we'll fix it later. + let placeholder_next_owner = + OwnerHash::::from_octets(Octs::default()).unwrap(); + + // Create an NSEC3 record. + let nsec3 = Nsec3::new( + alg, + flags, + iterations, + salt.clone(), + placeholder_next_owner, + bitmap.finalize(), + ); + + Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) +} +pub fn mk_hashed_nsec3_owner_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, + apex_owner: &N, +) -> Result +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]>, +{ + let base32hex_label = + mk_base32hex_label_for_name(name, alg, iterations, salt)?; + Ok(append_origin(base32hex_label, apex_owner)) +} + +fn append_origin(base32hex_label: String, apex_owner: &N) -> N +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + let mut builder = NameBuilder::::new(); + builder.append_label(base32hex_label.as_bytes()).unwrap(); + let owner_name = builder.append_origin(apex_owner).unwrap(); + let owner_name: N = owner_name.into(); + owner_name +} + +fn mk_base32hex_label_for_name( + name: &N, + alg: Nsec3HashAlg, + iterations: u16, + salt: &Nsec3Salt, +) -> Result +where + N: ToName, + SaltOcts: AsRef<[u8]>, +{ + let hash_octets: Vec = + nsec3_hash(name, alg, iterations, salt)?.into_octets(); + Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) +} + +//------------ Nsec3OptOut --------------------------------------------------- + +/// The different types of NSEC3 opt-out. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Nsec3OptOut { + /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure + /// delegations will be included in the NSEC3 chain. + #[default] + NoOptOut, + + /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure + /// delegations will NOT be included in the NSEC3 chain. + OptOut, + + /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and + /// insecure delegations will be included in the NSEC3 chain. + OptOutFlagsOnly, +} + +//------------ Nsec3HashProvider --------------------------------------------- + +pub trait Nsec3HashProvider { + fn get_or_create( + &mut self, + apex_owner: &N, + unhashed_owner_name: &N, + ) -> Result; +} + +pub struct OnDemandNsec3HashProvider { + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + // apex_owner: N, +} + +impl OnDemandNsec3HashProvider { + pub fn new( + alg: Nsec3HashAlg, + iterations: u16, + salt: Nsec3Salt, + // apex_owner: N, + ) -> Self { + Self { + alg, + iterations, + salt, + // apex_owner, + } + } + + pub fn algorithm(&self) -> Nsec3HashAlg { + self.alg + } + + pub fn iterations(&self) -> u16 { + self.iterations + } + + pub fn salt(&self) -> &Nsec3Salt { + &self.salt + } +} + +impl Nsec3HashProvider + for OnDemandNsec3HashProvider +where + N: ToName + From>, + Octs: FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + SaltOcts: AsRef<[u8]> + From<&'static [u8]>, +{ + fn get_or_create( + &mut self, + apex_owner: &N, + unhashed_owner_name: &N, + ) -> Result { + mk_hashed_nsec3_owner_name( + unhashed_owner_name, + self.alg, + self.iterations, + &self.salt, + apex_owner, + ) + } +} + +//----------- Nsec3ParamTtlMode ---------------------------------------------- + +/// The TTL to use for the NSEC3PARAM RR. +/// +/// Per RFC 5155 section 7.3 "Secondary Servers": "Secondary servers (and +/// perhaps other entities) need to reliably determine which NSEC3 +/// parameters (i.e., hash, salt, and iterations) are present at every +/// hashed owner name, in order to be able to choose an appropriate set of +/// NSEC3 RRs for negative responses. This is indicated by an NSEC3PARAM +/// RR present at the zone apex." +/// +/// RFC 5155 does not say anything about the TTL to use for the NSEC3PARAM RR. +/// +/// RFC 1034 says when _"When a name server loads a zone, it forces the TTL of +/// all authoritative RRs to be at least the MINIMUM field of the SOA"_ so an +/// approach used by some zone signers (e.g. PowerDNS [1]) is to use the SOA +/// MINIMUM as the TTL for the NSEC3PARAM. +/// +/// An alternative approach used by some zone signers is to use a fixed TTL +/// for the NSEC3PARAM TTL, e.g. BIND, dnssec-signzone and OpenDNSSEC +/// reportedly use 0 [1] while ldns-signzone uses 3600 [2] (as does an example +/// in the BIND documentation [3]). +/// +/// # Using a zero TTL +/// +/// RFC 1034 section 3.6 "Resource Records" says _"a zero TTL prohibits +/// caching"_. In principle TTLs are used for caching toward clients, RFC 5155 +/// section 4 "The NSEC3PARAM Resource Record" says _"The NSEC3PARAM RR is not +/// used by validators or resolvers"_ and RFC 5155 section 7.3 "Secondary +/// Servers" says that the NSEC3PARAM RR is used by secondary servers. +/// +/// As secondary servers should presumably use the latest version of the +/// NSEC3PARAM RR that they received from the primary without considering its +/// TTL the actual TTL chosen should not matter. +/// +/// However, if resolvers or other clients query the NSEC3PARAM they may +/// honour the TTL when caching the RR, and a value of zero could permit an +/// abusive or broken client to send an abnormally large number of requests +/// for the NSEC3PARAM RR toward authoritative servers. A zero TTL may also be +/// treated specially by resolvers and could lead to unexpected behaviour. +/// +/// [1]: https://github.com/PowerDNS/pdns/issues/2304 +/// [2]: https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/dnssec_sign.c#L1511, +/// https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/rr.c#L75 and +/// https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/ldns/ldns.h#L136 +/// [3]: https://bind9.readthedocs.io/en/v9.18.14/chapter5.html#nsec3 +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Nsec3ParamTtlMode { + /// A user defined TTL value. + Fixed(Ttl), + + #[default] + SoaMinimum, +} + +impl Nsec3ParamTtlMode { + pub fn fixed(ttl: Ttl) -> Self { + Self::Fixed(ttl) + } + + pub fn soa_minimum() -> Self { + Self::SoaMinimum + } + + pub fn bind_and_opendnssec_like() -> Self { + Self::Fixed(Ttl::from_secs(0)) + } + + pub fn ldns_like() -> Self { + Self::Fixed(Ttl::from_secs(3600)) + } +} + +//----------- Nsec3Config ---------------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Nsec3Config +where + HashProvider: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, +{ + pub params: Nsec3param, + pub opt_out: Nsec3OptOut, + pub ttl_mode: Nsec3ParamTtlMode, + pub hash_provider: HashProvider, + _phantom: PhantomData, +} + +impl Nsec3Config +where + HashProvider: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, +{ + pub fn new( + params: Nsec3param, + opt_out: Nsec3OptOut, + hash_provider: HashProvider, + ) -> Self { + Self { + params, + opt_out, + hash_provider, + ttl_mode: Default::default(), + _phantom: Default::default(), + } + } + + pub fn with_ttl_mode(mut self, ttl_mode: Nsec3ParamTtlMode) -> Self { + self.ttl_mode = ttl_mode; + self + } +} + +impl Default + for Nsec3Config> +where + N: ToName + From>, + Octs: AsRef<[u8]> + From<&'static [u8]> + Clone + FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn default() -> Self { + let params = Nsec3param::default(); + let hash_provider = OnDemandNsec3HashProvider::new( + params.hash_algorithm(), + params.iterations(), + params.salt().clone(), + ); + Self { + params, + opt_out: Default::default(), + ttl_mode: Default::default(), + hash_provider, + _phantom: Default::default(), + } + } +} + +//------------ Nsec3Records --------------------------------------------------- + +pub struct Nsec3Records { + /// The NSEC3 records. + pub recs: Vec>>, + + /// The NSEC3PARAM record. + pub param: Record>, +} + +impl Nsec3Records { + pub fn new( + recs: Vec>>, + param: Record>, + ) -> Self { + Self { recs, param } + } +} + +// TODO: Add tests for nsec3s() that validate the following from RFC 5155: +// +// 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. +// However, if there is not a corresponding NSEC3 RR, there MUST be +// 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." diff --git a/src/sign/bytes.rs b/src/sign/keys/bytes.rs similarity index 100% rename from src/sign/bytes.rs rename to src/sign/keys/bytes.rs diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs new file mode 100644 index 000000000..b88ad822e --- /dev/null +++ b/src/sign/keys/keymeta.rs @@ -0,0 +1,214 @@ +use core::convert::From; +use core::marker::PhantomData; +use core::ops::Deref; + +use crate::sign::{SignRaw, SigningKey}; + +//------------ DesignatedSigningKey ------------------------------------------ + +pub trait DesignatedSigningKey: + Deref> +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + /// Should this key be used to "sign one or more other authentication keys + /// for a given zone" (RFC 4033 section 2 "Key Signing Key (KSK)"). + fn signs_keys(&self) -> bool; + + /// Should this key be used to "sign a zone" (RFC 4033 section 2 "Zone + /// Signing Key (ZSK)"). + fn signs_zone_data(&self) -> bool; +} + +//------------ IntendedKeyPurpose -------------------------------------------- + +/// The purpose of a DNSSEC key from the perspective of an operator. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IntendedKeyPurpose { + /// A key that signs DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Key signing key (KSK): DNSSEC keys that "only sign the apex DNSKEY + /// RRset in a zone." (Quoted from RFC6781, Section 3.1) + KSK, + + /// A key that signs non-DNSKEY RRSETs. + /// + /// RFC9499 DNS Terminology: + /// 10. General DNSSEC + /// Zone signing key (ZSK): "DNSSEC keys that can be used to sign all the + /// RRsets in a zone that require signatures, other than the apex DNSKEY + /// RRset." (Quoted from RFC6781, Section 3.1) Also note that a ZSK is + /// sometimes used to sign the apex DNSKEY RRset. + ZSK, + + /// A key that signs both DNSKEY and other RRSETs. + /// + /// RFC 9499 DNS Terminology: + /// 10. General DNSSEC + /// Combined signing key (CSK): In cases where the differentiation between + /// the KSK and ZSK is not made, i.e., where keys have the role of both + /// KSK and ZSK, we talk about a Single-Type Signing Scheme." (Quoted from + /// [RFC6781], Section 3.1) This is sometimes called a "combined signing + /// key" or "CSK". It is operational practice, not protocol, that + /// determines whether a particular key is a ZSK, a KSK, or a CSK. + CSK, + + /// A key that is not currently used for signing. + /// + /// This key should be added to the zone but not used to sign any RRSETs. + Inactive, +} + +//------------ DnssecSigningKey ---------------------------------------------- + +/// A key that can be used for DNSSEC signing. +/// +/// This type carries metadata that signals to a DNSSEC signer how this key +/// should impact the zone to be signed. +pub struct DnssecSigningKey { + /// The key to use to make DNSSEC signatures. + key: SigningKey, + + /// The purpose for which the operator intends the key to be used. + /// + /// Defines explicitly the purpose of the key which should be used instead + /// of attempting to infer the purpose of the key (to sign keys and/or to + /// sign other records) by examining the setting of the Secure Entry Point + /// and Zone Key flags on the key (i.e. whether the key is a KSK or ZSK or + /// something else). + purpose: IntendedKeyPurpose, + + _phantom: PhantomData<(Octs, Inner)>, +} + +impl DnssecSigningKey { + /// Create a new [`DnssecSigningKey`] by assocating intent with a + /// reference to an existing key. + pub fn new( + key: SigningKey, + purpose: IntendedKeyPurpose, + ) -> Self { + Self { + key, + purpose, + _phantom: Default::default(), + } + } + + pub fn new_ksk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::KSK, + _phantom: Default::default(), + } + } + + pub fn new_zsk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::ZSK, + _phantom: Default::default(), + } + } + + pub fn new_csk(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::CSK, + _phantom: Default::default(), + } + } + + pub fn new_inactive_key(key: SigningKey) -> Self { + Self { + key, + purpose: IntendedKeyPurpose::Inactive, + _phantom: Default::default(), + } + } +} + +impl DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + pub fn purpose(&self) -> IntendedKeyPurpose { + self.purpose + } + + pub fn into_inner(self) -> SigningKey { + self.key + } + + // Note: This cannot be done as impl AsRef because AsRef requires that the + // lifetime of the returned reference be 'static, and we don't do impl Any + // as then the caller has to deal with Option or Result because the type + // might not impl DesignatedSigningKey. + pub fn as_designated_signing_key( + &self, + ) -> &dyn DesignatedSigningKey { + self + } +} + +//--- impl Deref + +impl Deref for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + type Target = SigningKey; + + fn deref(&self) -> &Self::Target { + &self.key + } +} + +//--- impl From + +impl From> + for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + fn from(key: SigningKey) -> Self { + let public_key = key.public_key(); + match ( + public_key.is_secure_entry_point(), + public_key.is_zone_signing_key(), + ) { + (true, _) => Self::new_ksk(key), + (false, true) => Self::new_zsk(key), + (false, false) => Self::new_inactive_key(key), + } + } +} + +//--- impl DesignatedSigningKey + +impl DesignatedSigningKey + for DnssecSigningKey +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + fn signs_keys(&self) -> bool { + matches!( + self.purpose, + IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK + ) + } + + fn signs_zone_data(&self) -> bool { + matches!( + self.purpose, + IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK + ) + } +} diff --git a/src/sign/common.rs b/src/sign/keys/keypair.rs similarity index 97% rename from src/sign/common.rs rename to src/sign/keys/keypair.rs index fe0fd1113..27d66a836 100644 --- a/src/sign/common.rs +++ b/src/sign/keys/keypair.rs @@ -9,18 +9,15 @@ use std::sync::Arc; use ::ring::rand::SystemRandom; -use crate::{ - base::iana::SecAlg, - validate::{PublicKeyBytes, Signature}, -}; - -use super::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::base::iana::SecAlg; +use crate::sign::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::validate::{PublicKeyBytes, Signature}; #[cfg(feature = "openssl")] -use super::openssl; +use crate::sign::crypto::openssl; #[cfg(feature = "ring")] -use super::ring; +use crate::sign::crypto::ring; //----------- KeyPair -------------------------------------------------------- diff --git a/src/sign/keyset.rs b/src/sign/keys/keyset.rs similarity index 99% rename from src/sign/keyset.rs rename to src/sign/keys/keyset.rs index b2705657f..5e83eb756 100644 --- a/src/sign/keyset.rs +++ b/src/sign/keys/keyset.rs @@ -1517,7 +1517,9 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { #[cfg(test)] mod tests { use crate::base::Name; - use crate::sign::keyset::{Action, KeySet, KeyType, RollType, UnixTime}; + use crate::sign::keys::keyset::{ + Action, KeySet, KeyType, RollType, UnixTime, + }; use crate::std::string::ToString; use mock_instant::global::MockClock; use std::str::FromStr; diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs new file mode 100644 index 000000000..973c6f236 --- /dev/null +++ b/src/sign/keys/mod.rs @@ -0,0 +1,4 @@ +pub mod bytes; +pub mod keymeta; +pub mod keypair; +pub mod keyset; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e5fc4d431..41579d76c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -127,14 +127,15 @@ use crate::validate::Key; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; -mod bytes; -pub use self::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; - -pub mod common; -pub mod keyset; -pub mod openssl; +pub mod crypto; +pub mod error; +pub mod hashing; +pub mod keys; pub mod records; -pub mod ring; +pub mod signing; +pub mod zone; + +pub use keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; //----------- SigningKey ----------------------------------------------------- diff --git a/src/sign/records.rs b/src/sign/records.rs index f255cf80a..4f2e0f698 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,40 +1,19 @@ //! Actual signing. use core::cmp::Ordering; use core::convert::From; -use core::fmt::Display; -use core::marker::PhantomData; -use core::ops::Deref; +use core::iter::Extend; +use core::marker::{PhantomData, Send}; use core::slice::Iter; -use std::boxed::Box; -use std::collections::HashSet; -use std::fmt::Debug; -use std::hash::Hash; -use std::string::{String, ToString}; use std::vec::Vec; use std::{fmt, slice}; -use bytes::Bytes; -use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom, OctetsInto}; -use tracing::{debug, enabled, trace, Level}; - use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; -use crate::base::name::{ToLabelIter, ToName}; -use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::iana::{Class, Rtype}; +use crate::base::name::ToName; +use crate::base::rdata::RecordData; use crate::base::record::Record; -use crate::base::{Name, NameBuilder, Ttl}; -use crate::rdata::dnssec::{ProtoRrsig, RtypeBitmap, RtypeBitmapBuilder}; -use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{ - Dnskey, Nsec, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, -}; -use crate::sign::{SignRaw, SigningKey}; -use crate::utils::base32; -use crate::validate::{nsec3_hash, Nsec3HashError}; -use crate::zonetree::types::StoredRecordData; -use crate::zonetree::StoredName; +use crate::base::Ttl; //------------ Sorter -------------------------------------------------------- @@ -222,6 +201,33 @@ where self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } + /// Update the data of an existing record. + /// + /// Allowing records to be mutated in-place would not be safe because it + /// could invalidate the sort order so no general method to mutate the + /// records is provided. + /// + /// This method offers a limited ability to mutate records in-place + /// however because it only permits mutating of the resource record data + /// of an existing record which doesn't impact the sort order because the + /// data is not part of the sort key. + pub fn update_data(&mut self, matcher: F, new_data: D) + where + F: Fn(&Record) -> bool, + { + if let Some(rr) = self.records.iter_mut().find(|rr| matcher(rr)) { + *rr.data_mut() = new_data; + } + } + + pub fn len(&self) -> usize { + self.records.len() + } + + pub fn is_empty(&self) -> bool { + self.records.is_empty() + } + pub fn iter(&self) -> Iter<'_, Record> { self.records.iter() } @@ -230,48 +236,21 @@ where self.records.as_slice() } + pub(super) fn as_mut_slice(&mut self) -> &mut [Record] { + self.records.as_mut_slice() + } + pub fn into_inner(self) -> Vec> { self.records } } -impl SortedRecords { - pub fn replace_soa(&mut self, new_soa: Soa) { - if let Some(soa_rrset) = self - .records - .iter_mut() - .find(|rrset| rrset.rtype() == Rtype::SOA) - { - if let ZoneRecordData::Soa(current_soa) = soa_rrset.data_mut() { - *current_soa = new_soa; - } - } - } - - pub fn replace_rrsig_for_apex_zonemd( - &mut self, - new_rrsig: Rrsig, - apex: &FamilyName, - ) { - if let Some(zonemd_rrsig) = self.records.iter_mut().find(|record| { - if record.rtype() == Rtype::RRSIG - && record.owner().name_cmp(&apex.owner()) == Ordering::Equal - { - if let ZoneRecordData::Rrsig(rrsig) = record.data() { - if rrsig.type_covered() == Rtype::ZONEMD { - return true; - } - } - } - false - }) { - if let ZoneRecordData::Rrsig(current_rrsig) = - zonemd_rrsig.data_mut() - { - *current_rrsig = new_rrsig; - } - } - } +impl SortedRecords +where + N: Send, + D: Send, + S: Sorter, +{ } impl SortedRecords @@ -281,528 +260,6 @@ where S: Sorter, SortedRecords: From>>, { - pub fn nsecs( - &self, - apex: &FamilyName, - ttl: Ttl, - assume_dnskeys_will_be_added: bool, - ) -> Vec>> - where - N: ToName + Clone + PartialEq, - D: RecordData, - Octets: FromBuilder, - Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, - ::AppendError: Debug, - { - let mut res = Vec::new(); - - // The owner name of a zone cut if we currently are at or below one. - let mut cut: Option> = None; - - let mut families = self.families(); - - // Since the records are ordered, the first family is the apex -- - // we can skip everything before that. - families.skip_before(apex); - - // Because of the next name thing, we need to keep the last NSEC - // around. - let mut prev: Option<(FamilyName, RtypeBitmap)> = None; - - // We also need the apex for the last NSEC. - let apex_owner = families.first_owner().clone(); - - for family in families { - // If the owner is out of zone, we have moved out of our zone and - // are done. - if !family.is_in_zone(apex) { - break; - } - - // If the family is below a zone cut, we must ignore it. - if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { - continue; - } - } - - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); - - // If this family is the parent side of a zone cut, we keep the - // family name for later. This also means below that if - // `cut.is_some()` we are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - Some(name.clone()) - } else { - None - }; - - if let Some((prev_name, bitmap)) = prev.take() { - res.push(prev_name.into_record( - ttl, - Nsec::new(name.owner().clone(), bitmap), - )); - } - - let mut bitmap = RtypeBitmap::::builder(); - // RFC 4035 section 2.3: - // "The type bitmap of every NSEC resource record in a signed - // zone MUST indicate the presence of both the NSEC record - // itself and its corresponding RRSIG record." - bitmap.add(Rtype::RRSIG).unwrap(); - if assume_dnskeys_will_be_added && family.owner() == &apex_owner { - // Assume there's gonna be a DNSKEY. - bitmap.add(Rtype::DNSKEY).unwrap(); - } - bitmap.add(Rtype::NSEC).unwrap(); - for rrset in family.rrsets() { - // RFC 4034 section 4.1.2: (and also RFC 4035 section 2.3) - // "The bitmap for the NSEC RR at a delegation point - // requires special attention. Bits corresponding to the - // delegation NS RRset and any RRsets for which the parent - // zone has authoritative data MUST be set; bits - // corresponding to any non-NS RRset for which the parent - // is not authoritative MUST be clear." - if cut.is_none() - || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) - { - // RFC 4034 section 4.1.2: - // "Bits representing pseudo-types MUST be clear, as - // they do not appear in zone data." - // - // TODO: Should this check be moved into - // RtypeBitmapBuilder itself? - if !rrset.rtype().is_pseudo() { - bitmap.add(rrset.rtype()).unwrap() - } - } - } - - prev = Some((name, bitmap.finalize())); - } - if let Some((prev_name, bitmap)) = prev { - res.push( - prev_name.into_record(ttl, Nsec::new(apex_owner, bitmap)), - ); - } - res - } - - /// Generate [RFC5155] NSEC3 and NSEC3PARAM records for this record set. - /// - /// This function does NOT enforce use of current best practice settings, - /// as defined by [RFC 5155], [RFC 9077] and [RFC 9276] which state that: - /// - /// - The `ttl` should be the _"lesser of the MINIMUM field of the zone - /// SOA RR and the TTL of the zone SOA RR itself"_. - /// - /// - The `params` should be set to _"SHA-1, no extra iterations, empty - /// salt"_ and zero flags. See [`Nsec3param::default()`]. - /// - /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html - /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html - /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html - // TODO: Move to Signer and do HashProvider = OnDemandNsec3HashProvider - // TODO: Does it make sense to take both Nsec3param AND HashProvider as input? - pub fn nsec3s( - &self, - apex: &FamilyName, - ttl: Ttl, - params: Nsec3param, - opt_out: Nsec3OptOut, - assume_dnskeys_will_be_added: bool, - hash_provider: &mut HashProvider, - ) -> Result, Nsec3HashError> - where - N: ToName + Clone + From> + Display + Ord + Hash, - N: From::Octets>>, - D: RecordData + From>, - Octets: Send + FromBuilder + OctetsFrom> + Clone + Default, - Octets::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, - ::AppendError: Debug, - OctetsMut: OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder, - ::Octets: AsRef<[u8]>, - HashProvider: Nsec3HashProvider, - Nsec3: Into, - { - // TODO: - // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) - // - RFC 5155 section 2 Backwards compatibility: - // Reject old algorithms? if not, map 3 to 6 and 5 to 7, or reject - // use of 3 and 5? - - // RFC 5155 7.1 step 2: - // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = params.flags(); - if matches!( - opt_out, - Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly - ) { - // Set the Opt-Out flag. - nsec3_flags |= 0b0000_0001; - } - - // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash order." - // We store the NSEC3s as we create them and sort them afterwards. - let mut nsec3s = Vec::>>::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; - - let mut families = self.families(); - - // Since the records are ordered, the first family is the apex -- - // we can skip everything before that. - families.skip_before(apex); - - // We also need the apex for the last NSEC. - let apex_owner = families.first_owner().clone(); - let apex_label_count = apex_owner.iter_labels().count(); - - let mut last_nent_stack: Vec = vec![]; - - for family in families { - trace!("Family: {}", family.family_name().owner()); - - // If the owner is out of zone, we have moved out of our zone and - // are done. - if !family.is_in_zone(apex) { - debug!( - "Stopping NSEC3 generation at out-of-zone family {}", - family.family_name().owner() - ); - break; - } - - // If the family is below a zone cut, we must ignore it. As the - // RRs are required to be sorted all RRs below a zone cut should - // be encountered after the cut itself. - if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { - debug!( - "Excluding family {} as it is below a zone cut", - family.family_name().owner() - ); - continue; - } - } - - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); - - // If this family is the parent side of a zone cut, we keep the - // family name for later. This also means below that if - // `cut.is_some()` we are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - trace!( - "Zone cut detected at family {}", - family.family_name().owner() - ); - Some(name.clone()) - } else { - None - }; - - // RFC 5155 7.1 step 2: - // "If Opt-Out is being used, owner names of unsigned - // delegations MAY be excluded." - // Note that: - // - A "delegation inherently happens at a zone cut" (RFC 9499). - // - An "unsigned delegation" aka an "insecure delegation" is a - // "signed name containing a delegation (NS RRset), but - // lacking a DS RRset, signifying a delegation to an unsigned - // subzone" (RFC 9499). - // So we need to check for whether Opt-Out is being used at a zone - // cut that lacks a DS RR. We determine whether or not a DS RR is - // present even when Opt-Out is not being used because we also - // need to know there at a later step. - let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); - if opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { - debug!("Excluding family {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",family.family_name().owner()); - continue; - } - - // RFC 5155 7.1 step 4: - // "If the difference in number of labels between the apex and - // 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 > last_nent_distance_to_apex { - trace!( - "Possible ENT detected at family {}", - family.family_name().owner() - ); - - // 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 - // then non_empty_label_count would be equal to label_distance. - // If it is less that means there are ENTs between us and the - // last non-empty label in our ancestor path to the apex. - - // Walk from the owner name down the tree of labels from the - // last known non-empty non-terminal label, extending the name - // each time by one label until we get to the current name. - - // Given a.b.c.mail.example.com where: - // - example.com is the apex owner - // - mail.example.com was the last non-empty non-terminal - // This loop will construct the names: - // - c.mail.example.com - // - b.c.mail.example.com - // 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 - 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. - let mut builder = NameBuilder::::new(); - for label in rev_label_it.take(distance_to_apex - n) { - builder.append_label(label.as_slice()).unwrap(); - } - let name = - builder.append_origin(&apex_owner).unwrap().into(); - - if let Err(pos) = ents.binary_search(&name) { - debug!("Found ENT at {name}"); - ents.insert(pos, name); - } - } - } - - // Create the type bitmap. - let mut bitmap = RtypeBitmap::::builder(); - - // Authoritative RRsets will be signed by `sign()` so add the - // expected future RRSIG type now to the NSEC3 Type Bitmap we are - // constructing. - // - // RFC 4033 section 2: - // 2. Definitions of Important DNSSEC Terms - // Authoritative RRset: Within the context of a particular - // zone, an RRset is "authoritative" if and only if the - // owner name of the RRset lies within the subset of the - // name space that is at or below the zone apex and at or - // above the cuts that separate the zone from its children, - // if any. All RRsets at the zone apex are authoritative, - // except for certain RRsets at this domain name that, if - // present, belong to this zone's parent. These RRset could - // include a DS RRset, the NSEC RRset referencing this DS - // RRset (the "parental NSEC"), and RRSIG RRs associated - // with these RRsets, all of which are authoritative in the - // parent zone. Similarly, if this zone contains any - // delegation points, only the parental NSEC RRset, DS - // RRsets, and any RRSIG RRs associated with these RRsets - // are authoritative for this zone. - if cut.is_none() || has_ds { - trace!("Adding RRSIG to the bitmap as the RRSET is authoritative (not at zone cut or has a DS RR)"); - bitmap.add(Rtype::RRSIG).unwrap(); - } - - // RFC 5155 7.1 step 3: - // "For each RRSet at the original owner name, set the - // corresponding bit in the Type Bit Maps field." - // - // Note: When generating NSEC RRs (not NSEC3 RRs) RFC 4035 makes - // it clear that non-authoritative RRs should not be represented - // in the Type Bitmap but for NSEC3 generation that's less clear. - // - // RFC 4035 section 2.3: - // 2.3. Including NSEC RRs in a Zone - // ... - // "bits corresponding to any non-NS RRset for which the parent - // is not authoritative MUST be clear." - // - // RFC 5155 section 7.1: - // 7.1. Zone Signing - // ... - // "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." - // - // Thus the rules for the types to include in the Type Bitmap for - // NSEC RRs appear to be different for NSEC3 RRs. However, in - // practice common tooling implementations exclude types from the - // NSEC3 which are non-authoritative (e.g. glue and occluded - // records). One could argue that the following fragments of RFC - // 5155 support this: - // - // RFC 5155 section 7.1. - // 7.1. Zone Signing - // ... - // "Other non-authoritative RRs are not represented by - // NSEC3 RRs." - // ... - // "2. For each unique original owner name in the zone add an - // NSEC3 RR." - // - // (if one reads "in the zone" to exclude data occluded by a zone - // cut or glue records that are only authoritative in the child - // zone and not in the parent zone). - // - // RFC 4033 could also be interpreted as excluding - // non-authoritative data from DNSSEC and thus NSEC3: - // - // RFC 4033 section 9: - // 9. Name Server Considerations - // ... - // "By itself, DNSSEC is not enough to protect the integrity of - // an entire zone during zone transfer operations, as even a - // signed zone contains some unsigned, nonauthoritative data if - // the zone has any children." - // - // As such we exclude non-authoritative RRs from the NSEC3 Type - // Bitmap, with the EXCEPTION of the NS RR at a secure delegation - // as insecure delegations are explicitly included by RFC 5155: - // - // RFC 5155 section 7.1: - // 7.1. Zone Signing - // ... - // "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." - for rrset in family.rrsets() { - if cut.is_none() - || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) - { - // RFC 5155 section 3.2: - // "Bits representing Meta-TYPEs or QTYPEs as specified - // in Section 3.1 of [RFC2929] or within the range - // reserved for assignment only to QTYPEs and - // Meta-TYPEs MUST be set to 0, since they do not - // appear in zone data". - // - // TODO: Should this check be moved into - // RtypeBitmapBuilder itself? - if !rrset.rtype().is_pseudo() { - trace!("Adding {} to the bitmap", rrset.rtype()); - bitmap.add(rrset.rtype()).unwrap(); - } - } - } - - if distance_to_apex == 0 { - trace!("Adding NSEC3PARAM to the bitmap as we are at the apex and RRSIG RRs are expected to be added"); - bitmap.add(Rtype::NSEC3PARAM).unwrap(); - if assume_dnskeys_will_be_added { - trace!("Adding DNSKEY to the bitmap as we are at the apex and DNSKEY RRs are expected to be added"); - bitmap.add(Rtype::DNSKEY).unwrap(); - } - } - - let rec: Record> = Self::mk_nsec3( - name.owner(), - hash_provider, - params.hash_algorithm(), - nsec3_flags, - params.iterations(), - params.salt(), - &apex_owner, - bitmap, - ttl, - )?; - - // Store the record by order of its owner name. - nsec3s.push(rec); - - 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(); - - debug!("Generating NSEC3 RR for ENT at {name}"); - let rec = Self::mk_nsec3( - &name, - hash_provider, - params.hash_algorithm(), - nsec3_flags, - params.iterations(), - params.salt(), - &apex_owner, - bitmap, - ttl, - )?; - - // Store the record by order of its owner name. - nsec3s.push(rec); - } - - // RFC 5155 7.1 step 7: - // "In each NSEC3 RR, insert the next hashed owner name by using the - // value of the next NSEC3 RR in hash order. The next hashed owner - // name of the last NSEC3 RR in the zone contains the value of the - // hashed owner name of the first NSEC3 RR in the hash order." - trace!("Sorting NSEC3 RRs"); - let mut nsec3s = SortedRecords::, S>::from(nsec3s); - for i in 1..=nsec3s.records.len() { - // TODO: Detect duplicate hashes. - let next_i = if i == nsec3s.records.len() { 0 } else { i }; - let cur_owner = nsec3s.records[next_i].owner(); - let name: Name = cur_owner.try_to_name().unwrap(); - let label = name.iter_labels().next().unwrap(); - let owner_hash = if let Ok(hash_octets) = - base32::decode_hex(&format!("{label}")) - { - OwnerHash::::from_octets(hash_octets).unwrap() - } else { - OwnerHash::::from_octets(name.as_octets().clone()) - .unwrap() - }; - let last_rec = &mut nsec3s.records[i - 1]; - let last_nsec3: &mut Nsec3 = last_rec.data_mut(); - last_nsec3.set_next_owner(owner_hash.clone()); - } - - // RFC 5155 7.1 step 8: - // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, - // Iterations, and Salt fields to the zone apex." - let nsec3param = Record::new( - apex.owner().try_to_name::().unwrap().into(), - Class::IN, - ttl, - params, - ); - - // RFC 5155 7.1 after step 8: - // "If a hash collision is detected, then a new salt has to be - // chosen, and the signing process restarted." - // - // Handled above. - - Ok(Nsec3Records::new(nsec3s.records, nsec3param)) - } - pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> where N: fmt::Display, @@ -851,58 +308,6 @@ where } } -/// Helper functions used to create NSEC3 records per RFC 5155. -impl SortedRecords -where - N: ToName + Send, - D: RecordData + CanonicalOrd + Send, - S: Sorter, -{ - #[allow(clippy::too_many_arguments)] - fn mk_nsec3( - name: &N, - hash_provider: &mut HashProvider, - alg: Nsec3HashAlg, - flags: u8, - iterations: u16, - salt: &Nsec3Salt, - _apex_owner: &N, - bitmap: RtypeBitmapBuilder<::Builder>, - ttl: Ttl, - ) -> Result>, Nsec3HashError> - where - N: ToName + From>, - Octets: FromBuilder + Clone + Default, - ::Builder: - EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, - Nsec3: Into, - HashProvider: Nsec3HashProvider, - { - // let owner_name = mk_hashed_nsec3_owner_name( - // name, alg, iterations, salt, apex_owner, - // )?; - let owner_name = hash_provider.get_or_create(name)?; - - // RFC 5155 7.1. step 2: - // "The Next Hashed Owner Name field is left blank for the moment." - // Create a placeholder next owner, we'll fix it later. - let placeholder_next_owner = - OwnerHash::::from_octets(Octets::default()).unwrap(); - - // Create an NSEC3 record. - let nsec3 = Nsec3::new( - alg, - flags, - iterations, - salt.clone(), - placeholder_next_owner, - bitmap.finalize(), - ); - - Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) - } -} - impl Default for SortedRecords { @@ -957,25 +362,6 @@ where } } -//------------ Nsec3Records --------------------------------------------------- - -pub struct Nsec3Records { - /// The NSEC3 records. - pub recs: Vec>>, - - /// The NSEC3PARAM record. - pub param: Record>, -} - -impl Nsec3Records { - pub fn new( - recs: Vec>>, - param: Record>, - ) -> Self { - Self { recs, param } - } -} - //------------ Family -------------------------------------------------------- /// A set of records with the same owner name and class. @@ -1086,7 +472,7 @@ pub struct Rrset<'a, N, D> { } impl<'a, N, D> Rrset<'a, N, D> { - fn new(slice: &'a [Record]) -> Self { + pub fn new(slice: &'a [Record]) -> Self { Rrset { slice } } @@ -1134,7 +520,7 @@ pub struct RecordsIter<'a, N, D> { } impl<'a, N, D> RecordsIter<'a, N, D> { - fn new(slice: &'a [Record]) -> Self { + pub fn new(slice: &'a [Record]) -> Self { RecordsIter { slice } } @@ -1263,829 +649,3 @@ where Some(Rrset::new(res)) } } - -//------------ SigningError -------------------------------------------------- - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum SigningError { - /// One or more keys does not have a signature validity period defined. - KeyLacksSignatureValidityPeriod, - - /// TODO - OutOfMemory, - - /// At least one key must be provided to sign with. - NoKeysProvided, - - /// None of the provided keys were deemed suitable by the - /// [`SigningKeyUsageStrategy`] used. - NoSuitableKeysFound, -} - -//------------ Nsec3OptOut --------------------------------------------------- - -/// The different types of NSEC3 opt-out. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum Nsec3OptOut { - /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure - /// delegations will be included in the NSEC3 chain. - #[default] - NoOptOut, - - /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure - /// delegations will NOT be included in the NSEC3 chain. - OptOut, - - /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and - /// insecure delegations will be included in the NSEC3 chain. - OptOutFlagsOnly, -} - -// TODO: Add tests for nsec3s() that validate the following from RFC 5155: -// -// 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. -// However, if there is not a corresponding NSEC3 RR, there MUST be -// 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." - -//------------ DesignatedSigningKey ------------------------------------------ - -pub trait DesignatedSigningKey: - Deref> -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - /// Should this key be used to "sign one or more other authentication keys - /// for a given zone" (RFC 4033 section 2 "Key Signing Key (KSK)"). - fn signs_keys(&self) -> bool; - - /// Should this key be used to "sign a zone" (RFC 4033 section 2 "Zone - /// Signing Key (ZSK)"). - fn signs_zone_data(&self) -> bool; -} - -//------------ IntendedKeyPurpose -------------------------------------------- - -/// The purpose of a DNSSEC key from the perspective of an operator. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum IntendedKeyPurpose { - /// A key that signs DNSKEY RRSETs. - /// - /// RFC9499 DNS Terminology: - /// 10. General DNSSEC - /// Key signing key (KSK): DNSSEC keys that "only sign the apex DNSKEY - /// RRset in a zone." (Quoted from RFC6781, Section 3.1) - KSK, - - /// A key that signs non-DNSKEY RRSETs. - /// - /// RFC9499 DNS Terminology: - /// 10. General DNSSEC - /// Zone signing key (ZSK): "DNSSEC keys that can be used to sign all the - /// RRsets in a zone that require signatures, other than the apex DNSKEY - /// RRset." (Quoted from RFC6781, Section 3.1) Also note that a ZSK is - /// sometimes used to sign the apex DNSKEY RRset. - ZSK, - - /// A key that signs both DNSKEY and other RRSETs. - /// - /// RFC 9499 DNS Terminology: - /// 10. General DNSSEC - /// Combined signing key (CSK): In cases where the differentiation between - /// the KSK and ZSK is not made, i.e., where keys have the role of both - /// KSK and ZSK, we talk about a Single-Type Signing Scheme." (Quoted from - /// [RFC6781], Section 3.1) This is sometimes called a "combined signing - /// key" or "CSK". It is operational practice, not protocol, that - /// determines whether a particular key is a ZSK, a KSK, or a CSK. - CSK, - - /// A key that is not currently used for signing. - /// - /// This key should be added to the zone but not used to sign any RRSETs. - Inactive, -} - -//------------ DnssecSigningKey ---------------------------------------------- - -/// A key that can be used for DNSSEC signing. -/// -/// This type carries metadata that signals to a DNSSEC signer how this key -/// should impact the zone to be signed. -pub struct DnssecSigningKey { - /// The key to use to make DNSSEC signatures. - key: SigningKey, - - /// The purpose for which the operator intends the key to be used. - /// - /// Defines explicitly the purpose of the key which should be used instead - /// of attempting to infer the purpose of the key (to sign keys and/or to - /// sign other records) by examining the setting of the Secure Entry Point - /// and Zone Key flags on the key (i.e. whether the key is a KSK or ZSK or - /// something else). - purpose: IntendedKeyPurpose, - - _phantom: PhantomData<(Octs, Inner)>, -} - -impl DnssecSigningKey { - /// Create a new [`DnssecSigningKey`] by assocating intent with a - /// reference to an existing key. - pub fn new( - key: SigningKey, - purpose: IntendedKeyPurpose, - ) -> Self { - Self { - key, - purpose, - _phantom: Default::default(), - } - } - - pub fn new_ksk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::KSK, - _phantom: Default::default(), - } - } - - pub fn new_zsk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::ZSK, - _phantom: Default::default(), - } - } - - pub fn new_csk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::CSK, - _phantom: Default::default(), - } - } - - pub fn new_inactive_key(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::Inactive, - _phantom: Default::default(), - } - } -} - -impl DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - pub fn purpose(&self) -> IntendedKeyPurpose { - self.purpose - } - - pub fn into_inner(self) -> SigningKey { - self.key - } - - // Note: This cannot be done as impl AsRef because AsRef requires that the - // lifetime of the returned reference be 'static, and we don't do impl Any - // as then the caller has to deal with Option or Result because the type - // might not impl DesignatedSigningKey. - pub fn as_designated_signing_key( - &self, - ) -> &dyn DesignatedSigningKey { - self - } -} - -//--- impl Deref - -impl Deref for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - type Target = SigningKey; - - fn deref(&self) -> &Self::Target { - &self.key - } -} - -//--- impl From - -impl From> - for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - fn from(key: SigningKey) -> Self { - let public_key = key.public_key(); - match ( - public_key.is_secure_entry_point(), - public_key.is_zone_signing_key(), - ) { - (true, _) => Self::new_ksk(key), - (false, true) => Self::new_zsk(key), - (false, false) => Self::new_inactive_key(key), - } - } -} - -//--- impl DesignatedSigningKey - -impl DesignatedSigningKey - for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - fn signs_keys(&self) -> bool { - matches!( - self.purpose, - IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK - ) - } - - fn signs_zone_data(&self) -> bool { - matches!( - self.purpose, - IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK - ) - } -} - -//------------ Operations ---------------------------------------------------- - -// TODO: Move nsecs() and nsecs3() out of SortedRecords and make them also -// take an iterator. This allows callers to pass an iterator over Record -// rather than force them to create the SortedRecords type (which for example -// in the case of a Zone we wouldn't have, but may instead be able to get an -// iterator over the Zone). Also move out the helper functions. Maybe put them -// all into a Signer struct? - -pub trait SigningKeyUsageStrategy -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - const NAME: &'static str; - - fn select_signing_keys_for_rtype( - candidate_keys: &[&dyn DesignatedSigningKey], - rtype: Option, - ) -> HashSet { - if matches!(rtype, Some(Rtype::DNSKEY)) { - Self::filter_keys(candidate_keys, |k| k.signs_keys()) - } else { - Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) - } - } - - fn filter_keys( - candidate_keys: &[&dyn DesignatedSigningKey], - filter: fn(&dyn DesignatedSigningKey) -> bool, - ) -> HashSet { - candidate_keys - .iter() - .enumerate() - .filter_map(|(i, &k)| filter(k).then_some(i)) - .collect::>() - } -} - -pub struct DefaultSigningKeyUsageStrategy; - -impl SigningKeyUsageStrategy - for DefaultSigningKeyUsageStrategy -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - const NAME: &'static str = "Default key usage strategy"; -} - -pub struct Signer< - Octs, - Inner, - KeyStrat = DefaultSigningKeyUsageStrategy, - Sort = DefaultSorter, -> where - Octs: AsRef<[u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, -{ - _phantom: PhantomData<(Octs, Inner, KeyStrat, Sort)>, -} - -impl Default - for Signer -where - Octs: AsRef<[u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, -{ - fn default() -> Self { - Self::new() - } -} - -impl Signer -where - Octs: AsRef<[u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, -{ - pub fn new() -> Self { - Self { - _phantom: PhantomData, - } - } -} - -impl Signer -where - Octs: AsRef<[u8]> + From> + OctetsFrom>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, -{ - /// Sign a zone using the given keys. - /// - /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be - /// added to the given records in order to DNSSEC sign them. - /// - /// The given records MUST be sorted according to [`CanonicalOrd`]. - #[allow(clippy::type_complexity)] - pub fn sign( - &self, - apex: &FamilyName, - families: RecordsIter<'_, N, ZoneRecordData>, - keys: &[&dyn DesignatedSigningKey], - add_used_dnskeys: bool, - ) -> Result>>, SigningError> - where - N: ToName + Display + Clone + PartialEq + CanonicalOrd + Send, - Octs: Clone + Send, - { - debug!("Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", KeyStrat::NAME); - - if keys.is_empty() { - return Err(SigningError::NoKeysProvided); - } - - // Work with indices because SigningKey doesn't impl PartialEq so we - // cannot use a HashSet to make a unique set of them. - - let dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype( - keys, - Some(Rtype::DNSKEY), - ); - - let non_dnskey_signing_key_idxs = - KeyStrat::select_signing_keys_for_rtype(keys, None); - - let keys_in_use_idxs: HashSet<_> = non_dnskey_signing_key_idxs - .iter() - .chain(dnskey_signing_key_idxs.iter()) - .collect(); - - if keys_in_use_idxs.is_empty() { - return Err(SigningError::NoSuitableKeysFound); - } - - // TODO: use log::log_enabled instead. - // See: https://github.com/NLnetLabs/domain/pull/465 - if enabled!(Level::DEBUG) { - fn debug_key, Inner: SignRaw>( - prefix: &str, - key: &SigningKey, - ) { - debug!( - "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", - key.algorithm() - .to_mnemonic_str() - .map(|alg| format!("{alg} ({})", key.algorithm())) - .unwrap_or_else(|| key.algorithm().to_string()), - key.owner(), - key.flags(), - key.is_secure_entry_point(), - key.is_zone_signing_key(), - key.public_key().key_tag(), - ) - } - - let num_keys = keys_in_use_idxs.len(); - debug!( - "Signing with {} {}:", - num_keys, - if num_keys == 1 { "key" } else { "keys" } - ); - - for idx in &keys_in_use_idxs { - let key = keys[**idx]; - let is_dnskey_signing_key = - dnskey_signing_key_idxs.contains(idx); - let is_non_dnskey_signing_key = - non_dnskey_signing_key_idxs.contains(idx); - let usage = - if is_dnskey_signing_key && is_non_dnskey_signing_key { - "CSK" - } else if is_dnskey_signing_key { - "KSK" - } else if is_non_dnskey_signing_key { - "ZSK" - } else { - "Unused" - }; - debug_key(&format!("Key[{idx}]: {usage}"), key); - } - } - - let mut res: Vec>> = Vec::new(); - let mut buf = Vec::new(); - let mut cut: Option> = None; - let mut families = families.peekable(); - - // Are we signing the entire tree from the apex down or just some child records? - // Use the first found SOA RR as the apex. If no SOA RR can be found assume that - // we are only signing records below the apex. - let apex_ttl = families.peek().and_then(|first_family| { - first_family.records().find_map(|rr| { - if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { - if let ZoneRecordData::Soa(soa) = rr.data() { - return Some(soa.minimum()); - } - } - None - }) - }); - - if let Some(soa_minimum_ttl) = apex_ttl { - // Sign the apex - // SAFETY: We just checked above if the apex records existed. - let apex_family = families.next().unwrap(); - - let apex_rrsets = apex_family - .rrsets() - .filter(|rrset| rrset.rtype() != Rtype::RRSIG); - - // Generate or extend the DNSKEY RRSET with the keys that we will sign - // apex DNSKEY RRs and zone RRs with. - let apex_dnskey_rrset = apex_family - .rrsets() - .find(|rrset| rrset.rtype() == Rtype::DNSKEY); - - let mut augmented_apex_dnskey_rrs = - SortedRecords::<_, _, Sort>::new(); - - // Determine the TTL of any existing DNSKEY RRSET and use that as the - // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA - // mininmum TTL. - // - // Applicable sections from RFC 1033: - // TTL's (Time To Live) - // "Also, all RRs with the same name, class, and type should - // have the same TTL value." - // - // RESOURCE RECORDS - // "If you leave the TTL field blank it will default to the - // minimum time specified in the SOA record (described - // later)." - let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { - let ttl = rrset.ttl(); - augmented_apex_dnskey_rrs.extend(rrset.iter().cloned()); - ttl - } else { - soa_minimum_ttl - }; - - for public_key in - keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) - { - let dnskey = public_key.to_dnskey(); - - let signing_key_dnskey_rr = Record::new( - apex.owner().clone(), - apex.class(), - dnskey_rrset_ttl, - Dnskey::convert(dnskey.clone()).into(), - ); - - // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. - let is_new_dnskey = augmented_apex_dnskey_rrs - .insert(signing_key_dnskey_rr) - .is_ok(); - - if add_used_dnskeys && is_new_dnskey { - // Add the DNSKEY RR to the set of new RRs to output for the zone. - res.push(Record::new( - apex.owner().clone(), - apex.class(), - dnskey_rrset_ttl, - Dnskey::convert(dnskey).into(), - )); - } - } - - let augmented_apex_dnskey_rrset = - Rrset::new(augmented_apex_dnskey_rrs.as_slice()); - - // Sign the apex RRSETs in canonical order. - for rrset in apex_rrsets - .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) - .chain(std::iter::once(augmented_apex_dnskey_rrset)) - { - // For the DNSKEY RRSET, use signing keys chosen for that - // purpose and sign the augmented set of DNSKEY RRs that we - // have generated rather than the original set in the - // zonefile. - let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { - &dnskey_signing_key_idxs - } else { - &non_dnskey_signing_key_idxs - }; - - for key in signing_key_idxs.iter().map(|&idx| keys[idx]) { - // A copy of the family name. We’ll need it later. - let name = apex_family.family_name().cloned(); - - let rrsig_rr = - Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; - res.push(rrsig_rr); - debug!( - "Signed {} RRs in RRSET {} at the zone apex with keytag {}", - rrset.iter().len(), - rrset.rtype(), - key.public_key().key_tag() - ); - } - } - } - - // For all RRSETs below the apex - for family in families { - // If the owner is out of zone, we have moved out of our zone and - // are done. - if !family.is_in_zone(apex) { - break; - } - - // If the family is below a zone cut, we must ignore it. - if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { - continue; - } - } - - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); - - // If this family is the parent side of a zone cut, we keep the - // family name for later. This also means below that if - // `cut.is_some()` we are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - Some(name.clone()) - } else { - None - }; - - for rrset in family.rrsets() { - if cut.is_some() { - // If we are at a zone cut, we only sign DS and NSEC - // records. NS records we must not sign and everything - // else shouldn’t be here, really. - if rrset.rtype() != Rtype::DS - && rrset.rtype() != Rtype::NSEC - { - continue; - } - } else { - // Otherwise we only ignore RRSIGs. - if rrset.rtype() == Rtype::RRSIG { - continue; - } - } - - for key in - non_dnskey_signing_key_idxs.iter().map(|&idx| keys[idx]) - { - let rrsig_rr = - Self::sign_rrset(key, &rrset, &name, apex, &mut buf)?; - res.push(rrsig_rr); - debug!( - "Signed {} RRSET at {} with keytag {}", - rrset.rtype(), - rrset.family_name().owner(), - key.public_key().key_tag() - ); - } - } - } - - debug!("Returning {} records from signing", res.len()); - - Ok(res) - } - - fn sign_rrset( - key: &SigningKey, - rrset: &Rrset<'_, N, D>, - name: &FamilyName, - apex: &FamilyName, - buf: &mut Vec, - ) -> Result>, SigningError> - where - N: ToName + Clone + Send, - D: RecordData - + ComposeRecordData - + From> - + CanonicalOrd - + Send, - { - let (inception, expiration) = key - .signature_validity_period() - .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? - .into_inner(); - // RFC 4034 - // 3. The RRSIG Resource Record - // "The TTL value of an RRSIG RR MUST match the TTL value of the - // RRset it covers. This is an exception to the [RFC2181] rules - // for TTL values of individual RRs within a RRset: individual - // RRSIG RRs with the same owner name will have different TTL - // values if the RRsets they cover have different TTL values." - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm(), - name.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.public_key().key_tag(), - // The fns provided by `ToName` state in their RustDoc that they - // "Converts the name into a single, uncompressed name" which - // matches the RFC 4034 section 3.1.7 requirement that "A sender - // MUST NOT use DNS name compression on the Signer's Name field - // when transmitting a RRSIG RR.". - // - // We don't need to make sure here that the signer name is in - // canonical form as required by RFC 4034 as the call to - // `compose_canonical()` below will take care of that. - apex.owner().clone(), - ); - buf.clear(); - rrsig.compose_canonical(buf).unwrap(); - for record in rrset.iter() { - record.compose_canonical(buf).unwrap(); - } - let signature = key.raw_secret_key().sign_raw(&*buf).unwrap(); - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(SigningError::OutOfMemory); - }; - let rrsig = rrsig.into_rrsig(signature).expect("long signature"); - Ok(Record::new( - name.owner().clone(), - name.class(), - rrset.ttl(), - ZoneRecordData::Rrsig(rrsig), - )) - } -} - -pub fn mk_hashed_nsec3_owner_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, - apex_owner: &N, -) -> Result -where - N: ToName + From>, - Octs: FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - SaltOcts: AsRef<[u8]>, -{ - let base32hex_label = - mk_base32hex_label_for_name(name, alg, iterations, salt)?; - Ok(append_origin(base32hex_label, apex_owner)) -} - -fn append_origin(base32hex_label: String, apex_owner: &N) -> N -where - N: ToName + From>, - Octs: FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, -{ - let mut builder = NameBuilder::::new(); - builder.append_label(base32hex_label.as_bytes()).unwrap(); - let owner_name = builder.append_origin(apex_owner).unwrap(); - let owner_name: N = owner_name.into(); - owner_name -} - -fn mk_base32hex_label_for_name( - name: &N, - alg: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, -) -> Result -where - N: ToName, - SaltOcts: AsRef<[u8]>, -{ - let hash_octets: Vec = - nsec3_hash(name, alg, iterations, salt)?.into_octets(); - Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) -} - -//------------ Nsec3HashProvider --------------------------------------------- - -pub trait Nsec3HashProvider { - fn get_or_create( - &mut self, - unhashed_owner_name: &N, - ) -> Result; -} - -pub struct OnDemandNsec3HashProvider { - alg: Nsec3HashAlg, - iterations: u16, - salt: Nsec3Salt, - apex_owner: N, -} - -impl OnDemandNsec3HashProvider { - pub fn new( - alg: Nsec3HashAlg, - iterations: u16, - salt: Nsec3Salt, - apex_owner: N, - ) -> Self { - Self { - alg, - iterations, - salt, - apex_owner, - } - } - - pub fn algorithm(&self) -> Nsec3HashAlg { - self.alg - } - - pub fn iterations(&self) -> u16 { - self.iterations - } - - pub fn salt(&self) -> &Nsec3Salt { - &self.salt - } -} - -impl Nsec3HashProvider - for OnDemandNsec3HashProvider -where - N: ToName + From>, - Octs: FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - SaltOcts: AsRef<[u8]>, -{ - fn get_or_create( - &mut self, - unhashed_owner_name: &N, - ) -> Result { - mk_hashed_nsec3_owner_name( - unhashed_owner_name, - self.alg, - self.iterations, - &self.salt, - &self.apex_owner, - ) - } -} diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs new file mode 100644 index 000000000..0f5a30d46 --- /dev/null +++ b/src/sign/signing/config.rs @@ -0,0 +1,70 @@ +use core::marker::PhantomData; + +use crate::sign::hashing::config::HashingConfig; +use crate::sign::hashing::nsec3::{ + Nsec3HashProvider, OnDemandNsec3HashProvider, +}; +use crate::sign::records::Sorter; +use crate::sign::signing::strategy::SigningKeyUsageStrategy; +use crate::sign::SignRaw; + +//------------ SigningConfig ------------------------------------------------- + +/// Signing configuration for a DNSSEC signed zone. +pub struct SigningConfig< + N, + Octs: AsRef<[u8]> + From<&'static [u8]>, + Key: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, + HP = OnDemandNsec3HashProvider, +> where + HP: Nsec3HashProvider, +{ + /// Hashing configuration. + pub hashing: HashingConfig, + + /// Should keys used to sign the zone be added as DNSKEY RRs? + pub add_used_dnskeys: bool, + + _phantom: PhantomData<(Key, KeyStrat, Sort)>, +} + +impl + SigningConfig +where + HP: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, + Key: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + pub fn new( + hashing: HashingConfig, + add_used_dnskeys: bool, + ) -> Self { + Self { + hashing, + add_used_dnskeys, + _phantom: PhantomData, + } + } +} + +impl Default + for SigningConfig +where + HP: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, + Key: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, +{ + fn default() -> Self { + Self { + hashing: Default::default(), + add_used_dnskeys: true, + _phantom: Default::default(), + } + } +} diff --git a/src/sign/signing/mod.rs b/src/sign/signing/mod.rs new file mode 100644 index 000000000..7e317b22d --- /dev/null +++ b/src/sign/signing/mod.rs @@ -0,0 +1,4 @@ +pub mod config; +pub mod rrsigs; +pub mod strategy; +pub mod traits; diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs new file mode 100644 index 000000000..8d204e9e8 --- /dev/null +++ b/src/sign/signing/rrsigs.rs @@ -0,0 +1,389 @@ +//! Actual signing. +use core::convert::From; +use core::fmt::Display; +use core::marker::Send; + +use std::boxed::Box; +use std::collections::HashSet; +use std::string::ToString; +use std::vec::Vec; + +use octseq::builder::{EmptyBuilder, FromBuilder}; +use octseq::{OctetsFrom, OctetsInto}; +use tracing::{debug, enabled, Level}; + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::Rtype; +use crate::base::name::ToName; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::record::Record; +use crate::base::Name; +use crate::rdata::dnssec::ProtoRrsig; +use crate::rdata::{Dnskey, ZoneRecordData}; +use crate::sign::error::SigningError; +use crate::sign::keys::keymeta::DesignatedSigningKey; +use crate::sign::records::{ + FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, +}; +use crate::sign::signing::strategy::SigningKeyUsageStrategy; +use crate::sign::signing::traits::SortedExtend; +use crate::sign::{SignRaw, SigningKey}; + +/// Generate RRSIG RRs for a collection of unsigned zone records. +/// +/// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be +/// added to the given records as part of DNSSEC zone signing. +/// +/// The given records MUST be sorted according to [`CanonicalOrd`]. +// TODO: Add mutable iterator based variant. +#[allow(clippy::type_complexity)] +pub fn generate_rrsigs( + apex: &FamilyName, + families: RecordsIter<'_, N, ZoneRecordData>, + keys: &[&dyn DesignatedSigningKey], + add_used_dnskeys: bool, +) -> Result>>, SigningError> +where + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + N: ToName + + PartialEq + + Clone + + Display + + Send + + CanonicalOrd + + From>, + Octs: AsRef<[u8]> + + From> + + Send + + OctetsFrom> + + Clone + + FromBuilder + + From<&'static [u8]>, + Sort: Sorter, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + debug!( + "Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", + KeyStrat::NAME + ); + + if keys.is_empty() { + return Err(SigningError::NoKeysProvided); + } + + // Work with indices because SigningKey doesn't impl PartialEq so we + // cannot use a HashSet to make a unique set of them. + + let dnskey_signing_key_idxs = + KeyStrat::select_signing_keys_for_rtype(keys, Some(Rtype::DNSKEY)); + + let non_dnskey_signing_key_idxs = + KeyStrat::select_signing_keys_for_rtype(keys, None); + + let keys_in_use_idxs: HashSet<_> = non_dnskey_signing_key_idxs + .iter() + .chain(dnskey_signing_key_idxs.iter()) + .collect(); + + if keys_in_use_idxs.is_empty() { + return Err(SigningError::NoSuitableKeysFound); + } + + // TODO: use log::log_enabled instead. + // See: https://github.com/NLnetLabs/domain/pull/465 + if enabled!(Level::DEBUG) { + fn debug_key, Inner: SignRaw>( + prefix: &str, + key: &SigningKey, + ) { + debug!( + "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", + key.algorithm() + .to_mnemonic_str() + .map(|alg| format!("{alg} ({})", key.algorithm())) + .unwrap_or_else(|| key.algorithm().to_string()), + key.owner(), + key.flags(), + key.is_secure_entry_point(), + key.is_zone_signing_key(), + key.public_key().key_tag(), + ) + } + + let num_keys = keys_in_use_idxs.len(); + debug!( + "Signing with {} {}:", + num_keys, + if num_keys == 1 { "key" } else { "keys" } + ); + + for idx in &keys_in_use_idxs { + let key = keys[**idx]; + let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); + let is_non_dnskey_signing_key = + non_dnskey_signing_key_idxs.contains(idx); + let usage = if is_dnskey_signing_key && is_non_dnskey_signing_key + { + "CSK" + } else if is_dnskey_signing_key { + "KSK" + } else if is_non_dnskey_signing_key { + "ZSK" + } else { + "Unused" + }; + debug_key(&format!("Key[{idx}]: {usage}"), key); + } + } + + let mut res: Vec>> = Vec::new(); + let mut buf = Vec::new(); + let mut cut: Option> = None; + let mut families = families.peekable(); + + // Are we signing the entire tree from the apex down or just some child + // records? Use the first found SOA RR as the apex. If no SOA RR can be + // found assume that we are only signing records below the apex. + let apex_ttl = families.peek().and_then(|first_family| { + first_family.records().find_map(|rr| { + if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { + if let ZoneRecordData::Soa(soa) = rr.data() { + return Some(soa.minimum()); + } + } + None + }) + }); + + if let Some(soa_minimum_ttl) = apex_ttl { + // Sign the apex + // SAFETY: We just checked above if the apex records existed. + let apex_family = families.next().unwrap(); + + let apex_rrsets = apex_family + .rrsets() + .filter(|rrset| rrset.rtype() != Rtype::RRSIG); + + // Generate or extend the DNSKEY RRSET with the keys that we will sign + // apex DNSKEY RRs and zone RRs with. + let apex_dnskey_rrset = apex_family + .rrsets() + .find(|rrset| rrset.rtype() == Rtype::DNSKEY); + + let mut augmented_apex_dnskey_rrs = + SortedRecords::<_, _, Sort>::new(); + + // Determine the TTL of any existing DNSKEY RRSET and use that as the + // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA + // mininmum TTL. + // + // Applicable sections from RFC 1033: + // TTL's (Time To Live) + // "Also, all RRs with the same name, class, and type should have + // the same TTL value." + // + // RESOURCE RECORDS + // "If you leave the TTL field blank it will default to the + // minimum time specified in the SOA record (described later)." + let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { + let ttl = rrset.ttl(); + augmented_apex_dnskey_rrs.sorted_extend(rrset.iter().cloned()); + ttl + } else { + soa_minimum_ttl + }; + + for public_key in + keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) + { + let dnskey = public_key.to_dnskey(); + + let signing_key_dnskey_rr = Record::new( + apex.owner().clone(), + apex.class(), + dnskey_rrset_ttl, + Dnskey::convert(dnskey.clone()).into(), + ); + + // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs + // for. + let is_new_dnskey = augmented_apex_dnskey_rrs + .insert(signing_key_dnskey_rr) + .is_ok(); + + if add_used_dnskeys && is_new_dnskey { + // Add the DNSKEY RR to the set of new RRs to output for the + // zone. + res.push(Record::new( + apex.owner().clone(), + apex.class(), + dnskey_rrset_ttl, + Dnskey::convert(dnskey).into(), + )); + } + } + + let augmented_apex_dnskey_rrset = + Rrset::new(augmented_apex_dnskey_rrs.as_slice()); + + // Sign the apex RRSETs in canonical order. + for rrset in apex_rrsets + .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) + .chain(std::iter::once(augmented_apex_dnskey_rrset)) + { + // For the DNSKEY RRSET, use signing keys chosen for that purpose + // and sign the augmented set of DNSKEY RRs that we have generated + // rather than the original set in the zonefile. + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + &dnskey_signing_key_idxs + } else { + &non_dnskey_signing_key_idxs + }; + + for key in signing_key_idxs.iter().map(|&idx| keys[idx]) { + // A copy of the family name. We’ll need it later. + let name = apex_family.family_name().cloned(); + + let rrsig_rr = + sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRs in RRSET {} at the zone apex with keytag {}", + rrset.iter().len(), + rrset.rtype(), + key.public_key().key_tag() + ); + } + } + } + + // For all RRSETs below the apex + for family in families { + // If the owner is out of zone, we have moved out of our zone and are + // done. + if !family.is_in_zone(apex) { + break; + } + + // If the family is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if family.owner().ends_with(cut.owner()) { + continue; + } + } + + // A copy of the family name. We’ll need it later. + let name = family.family_name().cloned(); + + // If this family is the parent side of a zone cut, we keep the family + // name for later. This also means below that if `cut.is_some()` we + // are at the parent side of a zone. + cut = if family.is_zone_cut(apex) { + Some(name.clone()) + } else { + None + }; + + for rrset in family.rrsets() { + if cut.is_some() { + // If we are at a zone cut, we only sign DS and NSEC records. + // NS records we must not sign and everything else shouldn’t + // be here, really. + if rrset.rtype() != Rtype::DS && rrset.rtype() != Rtype::NSEC + { + continue; + } + } else { + // Otherwise we only ignore RRSIGs. + if rrset.rtype() == Rtype::RRSIG { + continue; + } + } + + for key in + non_dnskey_signing_key_idxs.iter().map(|&idx| keys[idx]) + { + let rrsig_rr = + sign_rrset(key, &rrset, &name, apex, &mut buf)?; + res.push(rrsig_rr); + debug!( + "Signed {} RRSET at {} with keytag {}", + rrset.rtype(), + rrset.family_name().owner(), + key.public_key().key_tag() + ); + } + } + } + + debug!("Returning {} records from signing", res.len()); + + Ok(res) +} + +pub fn sign_rrset( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + name: &FamilyName, + apex: &FamilyName, + buf: &mut Vec, +) -> Result>, SigningError> +where + N: ToName + Clone + Send, + D: RecordData + + ComposeRecordData + + From> + + CanonicalOrd + + Send, + Inner: SignRaw, + Octs: AsRef<[u8]> + OctetsFrom>, +{ + let (inception, expiration) = key + .signature_validity_period() + .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? + .into_inner(); + // RFC 4034 + // 3. The RRSIG Resource Record + // "The TTL value of an RRSIG RR MUST match the TTL value of the RRset + // it covers. This is an exception to the [RFC2181] rules for TTL + // values of individual RRs within a RRset: individual RRSIG RRs with + // the same owner name will have different TTL values if the RRsets + // they cover have different TTL values." + let rrsig = ProtoRrsig::new( + rrset.rtype(), + key.algorithm(), + name.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + key.public_key().key_tag(), + // The fns provided by `ToName` state in their RustDoc that they + // "Converts the name into a single, uncompressed name" which matches + // the RFC 4034 section 3.1.7 requirement that "A sender MUST NOT use + // DNS name compression on the Signer's Name field when transmitting a + // RRSIG RR.". + // + // We don't need to make sure here that the signer name is in + // canonical form as required by RFC 4034 as the call to + // `compose_canonical()` below will take care of that. + apex.owner().clone(), + ); + buf.clear(); + rrsig.compose_canonical(buf).unwrap(); + for record in rrset.iter() { + record.compose_canonical(buf).unwrap(); + } + let signature = key.raw_secret_key().sign_raw(&*buf).unwrap(); + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + Ok(Record::new( + name.owner().clone(), + name.class(), + rrset.ttl(), + ZoneRecordData::Rrsig(rrsig), + )) +} diff --git a/src/sign/signing/strategy.rs b/src/sign/signing/strategy.rs new file mode 100644 index 000000000..ce8229d56 --- /dev/null +++ b/src/sign/signing/strategy.rs @@ -0,0 +1,49 @@ +use std::collections::HashSet; + +use crate::base::Rtype; +use crate::sign::keys::keymeta::DesignatedSigningKey; +use crate::sign::SignRaw; + +//------------ SigningKeyUsageStrategy --------------------------------------- + +pub trait SigningKeyUsageStrategy +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + const NAME: &'static str; + + fn select_signing_keys_for_rtype( + candidate_keys: &[&dyn DesignatedSigningKey], + rtype: Option, + ) -> HashSet { + if matches!(rtype, Some(Rtype::DNSKEY)) { + Self::filter_keys(candidate_keys, |k| k.signs_keys()) + } else { + Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) + } + } + + fn filter_keys( + candidate_keys: &[&dyn DesignatedSigningKey], + filter: fn(&dyn DesignatedSigningKey) -> bool, + ) -> HashSet { + candidate_keys + .iter() + .enumerate() + .filter_map(|(i, &k)| filter(k).then_some(i)) + .collect::>() + } +} + +//------------ DefaultSigningKeyUsageStrategy -------------------------------- +pub struct DefaultSigningKeyUsageStrategy; + +impl SigningKeyUsageStrategy + for DefaultSigningKeyUsageStrategy +where + Octs: AsRef<[u8]>, + Inner: SignRaw, +{ + const NAME: &'static str = "Default key usage strategy"; +} diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs new file mode 100644 index 000000000..70027aa59 --- /dev/null +++ b/src/sign/signing/traits.rs @@ -0,0 +1,418 @@ +use core::cmp::min; +use core::convert::From; +use core::fmt::{Debug, Display}; +use core::iter::Extend; +use core::marker::Send; + +use std::boxed::Box; +use std::hash::Hash; +use std::vec::Vec; + +use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; +use octseq::{FreezeBuilder, OctetsFrom}; + +use super::config::SigningConfig; +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::Rtype; +use crate::base::name::ToName; +use crate::base::record::Record; +use crate::base::Name; +use crate::rdata::ZoneRecordData; +use crate::sign::error::SigningError; +use crate::sign::hashing::config::HashingConfig; +use crate::sign::hashing::nsec::generate_nsecs; +use crate::sign::hashing::nsec3::{ + generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, + Nsec3Records, +}; +use crate::sign::keys::keymeta::DesignatedSigningKey; +use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; +use crate::sign::signing::rrsigs::generate_rrsigs; +use crate::sign::signing::strategy::SigningKeyUsageStrategy; +use crate::sign::SignRaw; + +//------------ SortedExtend -------------------------------------------------- + +pub trait SortedExtend { + fn sorted_extend< + T: IntoIterator>>, + >( + &mut self, + iter: T, + ); +} + +impl SortedExtend + for SortedRecords, S> +where + N: Send + PartialEq + ToName, + Octs: Send, + S: Sorter, + ZoneRecordData: CanonicalOrd + PartialEq, +{ + fn sorted_extend< + T: IntoIterator>>, + >( + &mut self, + iter: T, + ) { + self.extend(iter); + } +} + +//------------ SignableZone -------------------------------------------------- + +pub trait SignableZoneInPlace: + SignableZone + SortedExtend +where + N: Clone + ToName + From> + PartialEq + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + Self: SortedExtend, +{ + fn sign( + &mut self, + signing_config: &mut SigningConfig, + signing_keys: &[&dyn DesignatedSigningKey], + ) -> Result<(), SigningError> + where + HP: Nsec3HashProvider, + Key: SignRaw, + N: Display + Send + CanonicalOrd, + ::Builder: Truncate, + <::Builder as OctetsBuilder>::AppendError: Debug, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, + OctsMut: OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder + + Default, + { + let soa = self + .as_slice() + .iter() + .find(|r| r.rtype() == Rtype::SOA) + .ok_or(SigningError::NoSoaFound)?; + let ZoneRecordData::Soa(ref soa_data) = soa.data() else { + return Err(SigningError::NoSoaFound); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + let ttl = min(soa_data.minimum(), soa.ttl()); + + let families = RecordsIter::new(self.as_slice()); + + match &mut signing_config.hashing { + HashingConfig::Prehashed => { + // Nothing to do. + } + + HashingConfig::Nsec => { + let nsecs = generate_nsecs( + &self.apex(), + ttl, + families, + signing_config.add_used_dnskeys, + ); + + self.sorted_extend( + nsecs.into_iter().map(Record::from_record), + ); + } + + HashingConfig::Nsec3( + Nsec3Config { + params, + opt_out, + ttl_mode, + hash_provider, + .. + }, + extra, + ) if extra.is_empty() => { + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash + // order." We store the NSEC3s as we create them and sort them + // afterwards. + let Nsec3Records { recs, mut param } = + generate_nsec3s::( + &self.apex(), + ttl, + families, + params.clone(), + *opt_out, + signing_config.add_used_dnskeys, + hash_provider, + ) + .map_err(SigningError::Nsec3HashingError)?; + + let ttl = match ttl_mode { + Nsec3ParamTtlMode::Fixed(ttl) => *ttl, + Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), + }; + + param.set_ttl(ttl); + + // Add the generated NSEC3 records. + self.sorted_extend( + std::iter::once(Record::from_record(param)) + .chain(recs.into_iter().map(Record::from_record)), + ); + } + + HashingConfig::Nsec3(_nsec3_config, _extra) => { + todo!(); + } + + HashingConfig::TransitioningNsecToNsec3( + _nsec3_config, + _nsec_to_nsec3_transition_state, + ) => { + todo!(); + } + + HashingConfig::TransitioningNsec3ToNsec( + _nsec3_config, + _nsec3_to_nsec_transition_state, + ) => { + todo!(); + } + } + + if !signing_keys.is_empty() { + let families = RecordsIter::new(self.as_slice()); + + let rrsigs_and_dnskeys = + generate_rrsigs::( + &self.apex(), + families, + signing_keys, + signing_config.add_used_dnskeys, + )?; + + self.sorted_extend(rrsigs_and_dnskeys); + } + + Ok(()) + } +} + +//------------ SignableZone -------------------------------------------------- + +pub trait SignableZone +where + N: Clone + ToName + From> + PartialEq + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn apex(&self) -> FamilyName; + + fn as_slice(&self) -> &[Record>]; + + // TODO + // fn iter_mut(&mut self) -> T; + + // TODO: This is almost a duplicate of SignableZoneInPlace::sign(). + // Factor out the common code. + fn sign_into( + &self, + signing_config: &mut SigningConfig, + signing_keys: &[&dyn DesignatedSigningKey], + out: &mut T, + ) -> Result<(), SigningError> + where + HP: Nsec3HashProvider, + Key: SignRaw, + N: Display + Send + CanonicalOrd, + ::Builder: Truncate, + <::Builder as OctetsBuilder>::AppendError: Debug, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, + T: SortedExtend + ?Sized, + OctsMut: Default + + OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder, + { + let soa = self + .as_slice() + .iter() + .find(|r| r.rtype() == Rtype::SOA) + .ok_or(SigningError::NoSoaFound)?; + let ZoneRecordData::Soa(ref soa_data) = soa.data() else { + return Err(SigningError::NoSoaFound); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + let ttl = min(soa_data.minimum(), soa.ttl()); + + let families = RecordsIter::new(self.as_slice()); + + match &mut signing_config.hashing { + HashingConfig::Prehashed => { + // Nothing to do. + } + + HashingConfig::Nsec => { + let nsecs = generate_nsecs( + &self.apex(), + ttl, + families, + signing_config.add_used_dnskeys, + ); + + out.sorted_extend(nsecs.into_iter().map(Record::from_record)); + } + + HashingConfig::Nsec3( + Nsec3Config { + params, + opt_out, + ttl_mode, + hash_provider, + .. + }, + extra, + ) if extra.is_empty() => { + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash + // order." We store the NSEC3s as we create them and sort them + // afterwards. + let Nsec3Records { recs, mut param } = + generate_nsec3s::( + &self.apex(), + ttl, + families, + params.clone(), + *opt_out, + signing_config.add_used_dnskeys, + hash_provider, + ) + .map_err(SigningError::Nsec3HashingError)?; + + let ttl = match ttl_mode { + Nsec3ParamTtlMode::Fixed(ttl) => *ttl, + Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), + }; + + param.set_ttl(ttl); + + // Add the generated NSEC3 records. + out.sorted_extend( + std::iter::once(Record::from_record(param)) + .chain(recs.into_iter().map(Record::from_record)), + ); + } + + HashingConfig::Nsec3(_nsec3_config, _extra) => { + todo!(); + } + + HashingConfig::TransitioningNsecToNsec3( + _nsec3_config, + _nsec_to_nsec3_transition_state, + ) => { + todo!(); + } + + HashingConfig::TransitioningNsec3ToNsec( + _nsec3_config, + _nsec3_to_nsec_transition_state, + ) => { + todo!(); + } + } + + if !signing_keys.is_empty() { + let families = RecordsIter::new(self.as_slice()); + + let rrsigs_and_dnskeys = + generate_rrsigs::( + &self.apex(), + families, + signing_keys, + signing_config.add_used_dnskeys, + )?; + + out.sorted_extend(rrsigs_and_dnskeys); + } + + Ok(()) + } +} + +//--- impl SignableZone for SortedRecords + +impl SignableZone + for SortedRecords, S> +where + N: Clone + + ToName + + From> + + PartialEq + + Send + + CanonicalOrd + + Ord + + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: Sorter, +{ + fn apex(&self) -> FamilyName { + self.find_soa().unwrap().family_name().cloned() + } + + fn as_slice(&self) -> &[Record>] { + SortedRecords::as_slice(self) + } +} + +//--- impl SignableZoneInPlace for SortedRecords + +impl SignableZoneInPlace + for SortedRecords, S> +where + N: Clone + + ToName + + From> + + PartialEq + + Send + + CanonicalOrd + + Hash + + Ord, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + + S: Sorter, +{ +} diff --git a/src/sign/zone.rs b/src/sign/zone.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/src/sign/zone.rs @@ -0,0 +1 @@ + From 35609ccd19de00c123a5237e3c9cc1c2d3efab32 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:51:19 +0100 Subject: [PATCH 276/569] Cargo fmt. --- src/base/iana/rtype.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/base/iana/rtype.rs b/src/base/iana/rtype.rs index 8f41b08f2..127151b5e 100644 --- a/src/base/iana/rtype.rs +++ b/src/base/iana/rtype.rs @@ -442,7 +442,7 @@ impl Rtype { } /// Returns true if this record type represents a pseudo-RR. - /// + /// /// The term "pseudo-RR" appears in [RFC /// 9499](https://datatracker.ietf.org/doc/rfc9499/) Section 5 "Resource /// Records" as an alias for "meta-RR" and is referenced by [RFC From e663e65abad5a3ad7042bde9ac30cb047dd99f92 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 00:59:40 +0100 Subject: [PATCH 277/569] Fix doc tests. --- src/sign/keys/keyset.rs | 2 +- src/sign/mod.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sign/keys/keyset.rs b/src/sign/keys/keyset.rs index 5e83eb756..96edee6f3 100644 --- a/src/sign/keys/keyset.rs +++ b/src/sign/keys/keyset.rs @@ -7,7 +7,7 @@ //! //! ```no_run //! use domain::base::Name; -//! use domain::sign::keyset::{KeySet, RollType, UnixTime}; +//! use domain::sign::keys::keyset::{KeySet, RollType, UnixTime}; //! use std::fs::File; //! use std::io::Write; //! use std::str::FromStr; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 41579d76c..dde2b43b9 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -11,7 +11,7 @@ //! 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`]). +//! to provide the underlying signing operation (e.g. [`keys::keypair::KeyPair`]). //! //! # Example Usage //! @@ -22,10 +22,10 @@ //! # use domain::base::Name; //! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; -//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); +//! let (sec_bytes, pub_bytes) = keys::keypair::generate(params).unwrap(); //! //! // Parse the key into Ring or OpenSSL. -//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! //! // Associate the key with important metadata. //! let owner: Name> = "www.example.org.".parse().unwrap(); @@ -55,7 +55,7 @@ //! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); //! //! // Parse the key into Ring or OpenSSL. -//! let key_pair = common::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); //! //! // Associate the key with important metadata. //! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); From b868b424624d332d92cc1a32813e2e0a5ddd5f19 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:06:09 +0100 Subject: [PATCH 278/569] RustDoc fix. --- src/sign/hashing/nsec3.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 0b88ef95d..512e4525c 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -632,9 +632,7 @@ where /// treated specially by resolvers and could lead to unexpected behaviour. /// /// [1]: https://github.com/PowerDNS/pdns/issues/2304 -/// [2]: https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/dnssec_sign.c#L1511, -/// https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/rr.c#L75 and -/// https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/ldns/ldns.h#L136 +/// [2]: https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/dnssec_sign.c#L1511, https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/rr.c#L75 and https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/ldns/ldns.h#L136 /// [3]: https://bind9.readthedocs.io/en/v9.18.14/chapter5.html#nsec3 #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum Nsec3ParamTtlMode { From c2f1fbd695ad059a5626dc02a3320838070efffc Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:11:05 +0100 Subject: [PATCH 279/569] Better generic type name. --- src/sign/signing/rrsigs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index 8d204e9e8..6a78bca54 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -37,15 +37,15 @@ use crate::sign::{SignRaw, SigningKey}; /// The given records MUST be sorted according to [`CanonicalOrd`]. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -pub fn generate_rrsigs( +pub fn generate_rrsigs( apex: &FamilyName, families: RecordsIter<'_, N, ZoneRecordData>, - keys: &[&dyn DesignatedSigningKey], + keys: &[&dyn DesignatedSigningKey], add_used_dnskeys: bool, ) -> Result>>, SigningError> where - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, + KeyPair: SignRaw, + KeyStrat: SigningKeyUsageStrategy, N: ToName + PartialEq + Clone From 6162b727dd15915931162ff7e12ed16605d81dbb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:12:27 +0100 Subject: [PATCH 280/569] More descriptive and consistent fn name. --- src/sign/signing/traits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 70027aa59..0658864b2 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -76,7 +76,7 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Self: SortedExtend, { - fn sign( + fn sign_zone( &mut self, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], @@ -229,7 +229,7 @@ where // TODO: This is almost a duplicate of SignableZoneInPlace::sign(). // Factor out the common code. - fn sign_into( + fn sign_zone( &self, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], From 28e2144815b3e93f98fcd57065bad3ac9673d1c1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:14:32 +0100 Subject: [PATCH 281/569] Add sorted_records::as_slice(). --- src/sign/records.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index 4f2e0f698..2a1ed3834 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -507,6 +507,10 @@ impl<'a, N, D> Rrset<'a, N, D> { self.slice.iter() } + pub fn as_slice(&self) -> &'a [Record] { + self.slice + } + pub fn into_inner(self) -> &'a [Record] { self.slice } From 0dbeffb1246091d409a448d04b82a2ca359e1973 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:16:05 +0100 Subject: [PATCH 282/569] Also allow RRsets to be signed via trait fn which is simpler than callers having to call generate_rrsigs() manually as it lacks the unnecessary Sorter type and add dnskeys param, and is more consistent with how signing of entire zones is now possible via trait too. --- src/sign/signing/traits.rs | 70 +++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 0658864b2..b68f19cf1 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -26,7 +26,9 @@ use crate::sign::hashing::nsec3::{ Nsec3Records, }; use crate::sign::keys::keymeta::DesignatedSigningKey; -use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; +use crate::sign::records::{ + DefaultSorter, FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, +}; use crate::sign::signing::rrsigs::generate_rrsigs; use crate::sign::signing::strategy::SigningKeyUsageStrategy; use crate::sign::SignRaw; @@ -416,3 +418,69 @@ where S: Sorter, { } + +//------------ Signable ------------------------------------------------------ + +pub trait Signable +where + N: ToName + + CanonicalOrd + + Send + + Display + + Clone + + PartialEq + + From>, + KeyPair: SignRaw, + Octs: From> + + From<&'static [u8]> + + FromBuilder + + Clone + + OctetsFrom> + + Send, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + Sort: Sorter, +{ + fn families(&self) -> RecordsIter<'_, N, ZoneRecordData>; + + fn sign( + &self, + apex: &FamilyName, + keys: &[&dyn DesignatedSigningKey], + ) -> Result>>, SigningError> + where + KeyStrat: SigningKeyUsageStrategy, + { + generate_rrsigs::<_, _, _, KeyStrat, Sort>( + apex, + self.families(), + keys, + false, + ) + } +} + +//--- impl Signable for Rrset + +impl<'a, N, Octs, KeyPair> Signable + for Rrset<'a, N, ZoneRecordData> +where + KeyPair: SignRaw, + N: From> + + PartialEq + + Clone + + Display + + Send + + CanonicalOrd + + ToName, + Octs: octseq::FromBuilder + + Send + + OctetsFrom> + + Clone + + From<&'static [u8]> + + From>, + ::Builder: AsRef<[u8]> + AsMut<[u8]> + EmptyBuilder, +{ + fn families(&self) -> RecordsIter<'_, N, ZoneRecordData> { + RecordsIter::new(self.as_slice()) + } +} From 79d5b91fe9004d65db2f5a15f610ee74b61434ae Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:16:14 +0100 Subject: [PATCH 283/569] Clippy. --- src/sign/signing/traits.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index b68f19cf1..5ba042e28 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -442,6 +442,7 @@ where { fn families(&self) -> RecordsIter<'_, N, ZoneRecordData>; + #[allow(clippy::type_complexity)] fn sign( &self, apex: &FamilyName, @@ -461,8 +462,8 @@ where //--- impl Signable for Rrset -impl<'a, N, Octs, KeyPair> Signable - for Rrset<'a, N, ZoneRecordData> +impl Signable + for Rrset<'_, N, ZoneRecordData> where KeyPair: SignRaw, N: From> From e6d0844030fb6a224bcd88d3928686ac2cf0f89e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:36:17 +0100 Subject: [PATCH 284/569] FIX: Add missing required dependency to fix broken compilation of the keyset example. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index de626bc4d..6293bb843 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ 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", "dep:secrecy", "unstable-validate", "time/formatting"] +unstable-sign = ["std", "dep:secrecy", "unstable-validate", "time/formatting", "tracing"] 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"] From 33beefe5574b5b5218ef2d8f2b184275c3113d9b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 11:51:33 +0100 Subject: [PATCH 285/569] Take out references to BIND and LDNS. --- src/sign/hashing/nsec3.rs | 33 ++------------------------------- 1 file changed, 2 insertions(+), 31 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 512e4525c..5ac718907 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -612,33 +612,12 @@ where /// for the NSEC3PARAM TTL, e.g. BIND, dnssec-signzone and OpenDNSSEC /// reportedly use 0 [1] while ldns-signzone uses 3600 [2] (as does an example /// in the BIND documentation [3]). -/// -/// # Using a zero TTL -/// -/// RFC 1034 section 3.6 "Resource Records" says _"a zero TTL prohibits -/// caching"_. In principle TTLs are used for caching toward clients, RFC 5155 -/// section 4 "The NSEC3PARAM Resource Record" says _"The NSEC3PARAM RR is not -/// used by validators or resolvers"_ and RFC 5155 section 7.3 "Secondary -/// Servers" says that the NSEC3PARAM RR is used by secondary servers. -/// -/// As secondary servers should presumably use the latest version of the -/// NSEC3PARAM RR that they received from the primary without considering its -/// TTL the actual TTL chosen should not matter. -/// -/// However, if resolvers or other clients query the NSEC3PARAM they may -/// honour the TTL when caching the RR, and a value of zero could permit an -/// abusive or broken client to send an abnormally large number of requests -/// for the NSEC3PARAM RR toward authoritative servers. A zero TTL may also be -/// treated specially by resolvers and could lead to unexpected behaviour. -/// -/// [1]: https://github.com/PowerDNS/pdns/issues/2304 -/// [2]: https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/dnssec_sign.c#L1511, https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/rr.c#L75 and https://github.com/NLnetLabs/ldns/blob/310ae27b23e071b20e5010b6916d73ba0435ab79/ldns/ldns.h#L136 -/// [3]: https://bind9.readthedocs.io/en/v9.18.14/chapter5.html#nsec3 #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum Nsec3ParamTtlMode { - /// A user defined TTL value. + /// Use a fixed TTL value. Fixed(Ttl), + /// Use the TTL of the SOA record MINIMUM data field. #[default] SoaMinimum, } @@ -651,14 +630,6 @@ impl Nsec3ParamTtlMode { pub fn soa_minimum() -> Self { Self::SoaMinimum } - - pub fn bind_and_opendnssec_like() -> Self { - Self::Fixed(Ttl::from_secs(0)) - } - - pub fn ldns_like() -> Self { - Self::Fixed(Ttl::from_secs(3600)) - } } //----------- Nsec3Config ---------------------------------------------------- From fc29943d5fcff2ba23b3b64bb7fddb71d77aae7e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:38:21 +0100 Subject: [PATCH 286/569] De-duplicate SignableZone::sign_zone() and SignableZoneInPlace::sign_zone() by introducing SignableZoneInOut and RecordSlice. --- src/sign/hashing/nsec3.rs | 1 + src/sign/records.rs | 11 +- src/sign/signing/rrsigs.rs | 2 +- src/sign/signing/traits.rs | 535 +++++++++++++++++++++---------------- 4 files changed, 315 insertions(+), 234 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 5ac718907..ffcd28991 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -17,6 +17,7 @@ use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; +use crate::sign::signing::traits::RecordSlice; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; diff --git a/src/sign/records.rs b/src/sign/records.rs index 2a1ed3834..38d0be0e1 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -15,6 +15,8 @@ use crate::base::rdata::RecordData; use crate::base::record::Record; use crate::base::Ttl; +use super::signing::traits::RecordSlice; + //------------ Sorter -------------------------------------------------------- /// A DNS resource record sorter. @@ -232,10 +234,6 @@ where self.records.iter() } - pub fn as_slice(&self) -> &[Record] { - self.records.as_slice() - } - pub(super) fn as_mut_slice(&mut self) -> &mut [Record] { self.records.as_mut_slice() } @@ -245,12 +243,15 @@ where } } -impl SortedRecords +impl RecordSlice for SortedRecords where N: Send, D: Send, S: Sorter, { + fn as_slice(&self) -> &[Record] { + self.records.as_slice() + } } impl SortedRecords diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index 6a78bca54..c52ed0b1a 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -26,7 +26,7 @@ use crate::sign::records::{ FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::signing::strategy::SigningKeyUsageStrategy; -use crate::sign::signing::traits::SortedExtend; +use crate::sign::signing::traits::{RecordSlice, SortedExtend}; use crate::sign::{SignRaw, SigningKey}; /// Generate RRSIG RRs for a collection of unsigned zone records. diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 5ba042e28..07a0cc608 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -2,7 +2,7 @@ use core::cmp::min; use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; -use core::marker::Send; +use core::marker::{PhantomData, Send}; use std::boxed::Box; use std::hash::Hash; @@ -62,12 +62,17 @@ where } } -//------------ SignableZone -------------------------------------------------- +//------------ RecordSlice --------------------------------------------------- -pub trait SignableZoneInPlace: - SignableZone + SortedExtend +pub trait RecordSlice { + fn as_slice(&self) -> &[Record]; +} + +//------------ SignableZoneInOut --------------------------------------------- + +enum SignableZoneInOut<'a, 'b, N, Octs, S, T> where - N: Clone + ToName + From> + PartialEq + Ord + Hash, + N: Clone + ToName + From> + Ord + Hash, Octs: Clone + FromBuilder + From<&'static [u8]> @@ -76,141 +81,232 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Self: SortedExtend, + S: SignableZone, + T: SortedExtend + ?Sized, { - fn sign_zone( - &mut self, - signing_config: &mut SigningConfig, - signing_keys: &[&dyn DesignatedSigningKey], - ) -> Result<(), SigningError> - where - HP: Nsec3HashProvider, - Key: SignRaw, - N: Display + Send + CanonicalOrd, - ::Builder: Truncate, - <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, - OctsMut: OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder - + Default, - { - let soa = self - .as_slice() - .iter() - .find(|r| r.rtype() == Rtype::SOA) - .ok_or(SigningError::NoSoaFound)?; - let ZoneRecordData::Soa(ref soa_data) = soa.data() else { - return Err(SigningError::NoSoaFound); - }; + SignInPlace(&'a mut T, PhantomData<(N, Octs)>), + SignInto(&'a S, &'b mut T), +} - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // the MINIMUM field of the SOA record and the TTL of the SOA itself". - let ttl = min(soa_data.minimum(), soa.ttl()); +impl<'a, 'b, N, Octs, S, T> SignableZoneInOut<'a, 'b, N, Octs, S, T> +where + N: Clone + ToName + From> + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: SignableZone, + T: RecordSlice> + + SortedExtend + + ?Sized, +{ + fn new_in_place(signable_zone: &'a mut T) -> Self { + Self::SignInPlace(signable_zone, Default::default()) + } - let families = RecordsIter::new(self.as_slice()); + fn new_into(signable_zone: &'a S, out: &'b mut T) -> Self { + Self::SignInto(signable_zone, out) + } +} - match &mut signing_config.hashing { - HashingConfig::Prehashed => { - // Nothing to do. +impl SignableZoneInOut<'_, '_, N, Octs, S, T> +where + N: Clone + ToName + From> + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: SignableZone, + T: RecordSlice> + + SortedExtend + + ?Sized, +{ + fn as_slice(&self) -> &[Record>] { + match self { + SignableZoneInOut::SignInPlace(input_output, _) => { + input_output.as_slice() } + SignableZoneInOut::SignInto(input, _) => input.as_slice(), + } + } - HashingConfig::Nsec => { - let nsecs = generate_nsecs( - &self.apex(), + fn sorted_extend< + U: IntoIterator>>, + >( + &mut self, + iter: U, + ) { + match self { + SignableZoneInOut::SignInPlace(input_output, _) => { + input_output.sorted_extend(iter) + } + SignableZoneInOut::SignInto(_, output) => { + output.sorted_extend(iter) + } + } + } +} + +//------------ sign_zone() --------------------------------------------------- + +fn sign_zone( + mut in_out: SignableZoneInOut, + apex: &FamilyName, + signing_config: &mut SigningConfig, + signing_keys: &[&dyn DesignatedSigningKey], +) -> Result<(), SigningError> +where + HP: Nsec3HashProvider, + Key: SignRaw, + N: Display + + Send + + CanonicalOrd + + Clone + + ToName + + From> + + Ord + + Hash, + ::Builder: + Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + <::Builder as OctetsBuilder>::AppendError: Debug, + KeyStrat: SigningKeyUsageStrategy, + S: SignableZone, + Sort: Sorter, + T: SortedExtend + ?Sized, + Octs: FromBuilder + + Clone + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + OctsMut: Default + + OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder, + T: RecordSlice>, +{ + let soa = in_out + .as_slice() + .iter() + .find(|r| r.rtype() == Rtype::SOA) + .ok_or(SigningError::NoSoaFound)?; + let ZoneRecordData::Soa(ref soa_data) = soa.data() else { + return Err(SigningError::NoSoaFound); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + let ttl = min(soa_data.minimum(), soa.ttl()); + + let families = RecordsIter::new(in_out.as_slice()); + + match &mut signing_config.hashing { + HashingConfig::Prehashed => { + // Nothing to do. + } + + HashingConfig::Nsec => { + let nsecs = generate_nsecs( + apex, + ttl, + families, + signing_config.add_used_dnskeys, + ); + + in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); + } + + HashingConfig::Nsec3( + Nsec3Config { + params, + opt_out, + ttl_mode, + hash_provider, + .. + }, + extra, + ) if extra.is_empty() => { + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash + // order." We store the NSEC3s as we create them and sort them + // afterwards. + let Nsec3Records { recs, mut param } = + generate_nsec3s::( + apex, ttl, families, + params.clone(), + *opt_out, signing_config.add_used_dnskeys, - ); - - self.sorted_extend( - nsecs.into_iter().map(Record::from_record), - ); - } - - HashingConfig::Nsec3( - Nsec3Config { - params, - opt_out, - ttl_mode, hash_provider, - .. - }, - extra, - ) if extra.is_empty() => { - // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash - // order." We store the NSEC3s as we create them and sort them - // afterwards. - let Nsec3Records { recs, mut param } = - generate_nsec3s::( - &self.apex(), - ttl, - families, - params.clone(), - *opt_out, - signing_config.add_used_dnskeys, - hash_provider, - ) - .map_err(SigningError::Nsec3HashingError)?; - - let ttl = match ttl_mode { - Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), - }; - - param.set_ttl(ttl); - - // Add the generated NSEC3 records. - self.sorted_extend( - std::iter::once(Record::from_record(param)) - .chain(recs.into_iter().map(Record::from_record)), - ); - } + ) + .map_err(SigningError::Nsec3HashingError)?; - HashingConfig::Nsec3(_nsec3_config, _extra) => { - todo!(); - } + let ttl = match ttl_mode { + Nsec3ParamTtlMode::Fixed(ttl) => *ttl, + Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), + }; - HashingConfig::TransitioningNsecToNsec3( - _nsec3_config, - _nsec_to_nsec3_transition_state, - ) => { - todo!(); - } + param.set_ttl(ttl); - HashingConfig::TransitioningNsec3ToNsec( - _nsec3_config, - _nsec3_to_nsec_transition_state, - ) => { - todo!(); - } + // Add the generated NSEC3 records. + in_out.sorted_extend( + std::iter::once(Record::from_record(param)) + .chain(recs.into_iter().map(Record::from_record)), + ); } - if !signing_keys.is_empty() { - let families = RecordsIter::new(self.as_slice()); + HashingConfig::Nsec3(_nsec3_config, _extra) => { + todo!(); + } - let rrsigs_and_dnskeys = - generate_rrsigs::( - &self.apex(), - families, - signing_keys, - signing_config.add_used_dnskeys, - )?; + HashingConfig::TransitioningNsecToNsec3( + _nsec3_config, + _nsec_to_nsec3_transition_state, + ) => { + todo!(); + } - self.sorted_extend(rrsigs_and_dnskeys); + HashingConfig::TransitioningNsec3ToNsec( + _nsec3_config, + _nsec3_to_nsec_transition_state, + ) => { + todo!(); } + } - Ok(()) + if !signing_keys.is_empty() { + let families = RecordsIter::new(in_out.as_slice()); + + let rrsigs_and_dnskeys = + generate_rrsigs::( + apex, + families, + signing_keys, + signing_config.add_used_dnskeys, + )?; + + in_out.sorted_extend(rrsigs_and_dnskeys); } + + Ok(()) } //------------ SignableZone -------------------------------------------------- -pub trait SignableZone +pub trait SignableZone: + RecordSlice> where N: Clone + ToName + From> + PartialEq + Ord + Hash, Octs: Clone @@ -224,13 +320,9 @@ where { fn apex(&self) -> FamilyName; - fn as_slice(&self) -> &[Record>]; - // TODO // fn iter_mut(&mut self) -> T; - // TODO: This is almost a duplicate of SignableZoneInPlace::sign(). - // Factor out the common code. fn sign_zone( &self, signing_config: &mut SigningConfig, @@ -245,119 +337,24 @@ where <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, - T: SortedExtend + ?Sized, + T: SortedExtend + + ?Sized + + RecordSlice>, OctsMut: Default + OctetsBuilder + AsRef<[u8]> + AsMut<[u8]> + EmptyBuilder + FreezeBuilder, + Self: Sized, { - let soa = self - .as_slice() - .iter() - .find(|r| r.rtype() == Rtype::SOA) - .ok_or(SigningError::NoSoaFound)?; - let ZoneRecordData::Soa(ref soa_data) = soa.data() else { - return Err(SigningError::NoSoaFound); - }; - - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // the MINIMUM field of the SOA record and the TTL of the SOA itself". - let ttl = min(soa_data.minimum(), soa.ttl()); - - let families = RecordsIter::new(self.as_slice()); - - match &mut signing_config.hashing { - HashingConfig::Prehashed => { - // Nothing to do. - } - - HashingConfig::Nsec => { - let nsecs = generate_nsecs( - &self.apex(), - ttl, - families, - signing_config.add_used_dnskeys, - ); - - out.sorted_extend(nsecs.into_iter().map(Record::from_record)); - } - - HashingConfig::Nsec3( - Nsec3Config { - params, - opt_out, - ttl_mode, - hash_provider, - .. - }, - extra, - ) if extra.is_empty() => { - // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash - // order." We store the NSEC3s as we create them and sort them - // afterwards. - let Nsec3Records { recs, mut param } = - generate_nsec3s::( - &self.apex(), - ttl, - families, - params.clone(), - *opt_out, - signing_config.add_used_dnskeys, - hash_provider, - ) - .map_err(SigningError::Nsec3HashingError)?; - - let ttl = match ttl_mode { - Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), - }; - - param.set_ttl(ttl); - - // Add the generated NSEC3 records. - out.sorted_extend( - std::iter::once(Record::from_record(param)) - .chain(recs.into_iter().map(Record::from_record)), - ); - } - - HashingConfig::Nsec3(_nsec3_config, _extra) => { - todo!(); - } - - HashingConfig::TransitioningNsecToNsec3( - _nsec3_config, - _nsec_to_nsec3_transition_state, - ) => { - todo!(); - } - - HashingConfig::TransitioningNsec3ToNsec( - _nsec3_config, - _nsec3_to_nsec_transition_state, - ) => { - todo!(); - } - } - - if !signing_keys.is_empty() { - let families = RecordsIter::new(self.as_slice()); - - let rrsigs_and_dnskeys = - generate_rrsigs::( - &self.apex(), - families, - signing_keys, - signing_config.add_used_dnskeys, - )?; - - out.sorted_extend(rrsigs_and_dnskeys); - } - - Ok(()) + let in_out = SignableZoneInOut::new_into(self, out); + sign_zone::( + in_out, + &self.apex(), + signing_config, + signing_keys, + ) } } @@ -387,9 +384,91 @@ where fn apex(&self) -> FamilyName { self.find_soa().unwrap().family_name().cloned() } +} - fn as_slice(&self) -> &[Record>] { - SortedRecords::as_slice(self) +//--- impl RecordSlice for Vec + +impl RecordSlice for Vec> { + fn as_slice(&self) -> &[Record] { + Vec::as_slice(self) + } +} + +//--- impl SignableZone for Vec + +// NOTE: Assumes that the Vec is already sorted according to CanonicalOrd. +impl SignableZone + for Vec>> +where + N: Clone + + ToName + + From> + + PartialEq + + Send + + CanonicalOrd + + Ord + + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn apex(&self) -> FamilyName { + self.iter() + .find(|r| r.rtype() == Rtype::SOA) + .map(|r| FamilyName::new(r.owner().clone(), r.class())) + .unwrap() + } +} + +//------------ SignableZoneInPlace ------------------------------------------- + +pub trait SignableZoneInPlace: + SignableZone + SortedExtend +where + N: Clone + ToName + From> + PartialEq + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + Self: SortedExtend + Sized, +{ + fn sign_zone( + &mut self, + signing_config: &mut SigningConfig, + signing_keys: &[&dyn DesignatedSigningKey], + ) -> Result<(), SigningError> + where + HP: Nsec3HashProvider, + Key: SignRaw, + N: Display + Send + CanonicalOrd, + ::Builder: Truncate, + <::Builder as OctetsBuilder>::AppendError: Debug, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, + OctsMut: OctetsBuilder + + AsRef<[u8]> + + AsMut<[u8]> + + EmptyBuilder + + FreezeBuilder + + Default, + { + let apex = self.apex(); + let in_out = SignableZoneInOut::new_in_place(self); + sign_zone::( + in_out, + &apex, + signing_config, + signing_keys, + ) } } From 2e761c1b85bcf770f51566faee17a81d7774145b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 14:43:20 +0100 Subject: [PATCH 287/569] Remove the confusnig OctsMut generic type. --- src/sign/hashing/nsec3.rs | 13 +++---------- src/sign/signing/traits.rs | 32 +++++++------------------------- 2 files changed, 10 insertions(+), 35 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index ffcd28991..87fc79b25 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -7,7 +7,7 @@ use std::string::String; use std::vec::Vec; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom}; +use octseq::OctetsFrom; use tracing::{debug, trace}; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; @@ -36,7 +36,7 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. -pub fn generate_nsec3s( +pub fn generate_nsec3s( apex: &FamilyName, ttl: Ttl, mut families: RecordsIter<'_, N, ZoneRecordData>, @@ -47,16 +47,9 @@ pub fn generate_nsec3s( ) -> Result, Nsec3HashError> where N: ToName + Clone + Display + Ord + Hash + Send + From>, - N: From::Octets>>, Octs: FromBuilder + OctetsFrom> + Default + Clone + Send, Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, - OctsMut: OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder, - ::Octets: AsRef<[u8]>, HashProvider: Nsec3HashProvider, Sort: Sorter, { @@ -202,7 +195,7 @@ where let rev_label_it = name.owner().iter_labels().skip(n); // Create next longest ENT name. - let mut builder = NameBuilder::::new(); + let mut builder = NameBuilder::::new(); for label in rev_label_it.take(distance_to_apex - n) { builder.append_label(label.as_slice()).unwrap(); } diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 07a0cc608..557f00673 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -9,7 +9,7 @@ use std::hash::Hash; use std::vec::Vec; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use octseq::{FreezeBuilder, OctetsFrom}; +use octseq::OctetsFrom; use super::config::SigningConfig; use crate::base::cmp::CanonicalOrd; @@ -157,7 +157,7 @@ where //------------ sign_zone() --------------------------------------------------- -fn sign_zone( +fn sign_zone( mut in_out: SignableZoneInOut, apex: &FamilyName, signing_config: &mut SigningConfig, @@ -188,12 +188,6 @@ where + OctetsFrom> + From> + Default, - OctsMut: Default - + OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder, T: RecordSlice>, { let soa = in_out @@ -242,7 +236,7 @@ where // order." We store the NSEC3s as we create them and sort them // afterwards. let Nsec3Records { recs, mut param } = - generate_nsec3s::( + generate_nsec3s::( apex, ttl, families, @@ -323,7 +317,7 @@ where // TODO // fn iter_mut(&mut self) -> T; - fn sign_zone( + fn sign_zone( &self, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], @@ -340,16 +334,10 @@ where T: SortedExtend + ?Sized + RecordSlice>, - OctsMut: Default - + OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder, Self: Sized, { let in_out = SignableZoneInOut::new_into(self, out); - sign_zone::( + sign_zone::( in_out, &self.apex(), signing_config, @@ -441,7 +429,7 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Self: SortedExtend + Sized, { - fn sign_zone( + fn sign_zone( &mut self, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], @@ -454,16 +442,10 @@ where <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, - OctsMut: OctetsBuilder - + AsRef<[u8]> - + AsMut<[u8]> - + EmptyBuilder - + FreezeBuilder - + Default, { let apex = self.apex(); let in_out = SignableZoneInOut::new_in_place(self); - sign_zone::( + sign_zone::( in_out, &apex, signing_config, From ceab294cc648d51eb4923e5a52cd898e39652740 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 6 Jan 2025 16:26:04 +0100 Subject: [PATCH 288/569] Default TTL for newly created non-NSEC(3) RRs should be that of the SOA TTL. --- src/sign/hashing/nsec3.rs | 25 ++++++++++++++++++++++++- src/sign/signing/rrsigs.rs | 33 ++++++++++++++++++--------------- src/sign/signing/traits.rs | 10 +++++++++- 3 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 87fc79b25..28e00a25f 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -606,13 +606,32 @@ where /// for the NSEC3PARAM TTL, e.g. BIND, dnssec-signzone and OpenDNSSEC /// reportedly use 0 [1] while ldns-signzone uses 3600 [2] (as does an example /// in the BIND documentation [3]). +/// +/// The default approach used here is to use the TTL of the SOA RR, NOT the +/// SOA MINIMUM. This is consistent with how a TTL is chosen by tools such as +/// dnssec-signzone and ldns-signzone for other non-NSEC(3) records that are +/// added to a zone such as DNSKEY RRs. We do not use a fixed value as the +/// default as that seems strangely inconsistent with the rest of the zone, +/// and especially not zero as that seems to be considered a complex case for +/// resolvers to handle and may potentially lead to unwanted behaviour, and +/// additional load on both authoritatives and resolvers if a (abusive) client +/// should aggressively query the NSEC3PARAM RR. We also do not use the SOA +/// MINIMUM TTL as that concerns (quoting RFC 1034) "the length of time that +/// the negative result may be cached" and the NSEC3PARAM is not related to +/// negative caching. As at least one other implementation uses SOA MINIMUM +/// and this is not a hard-coded value that a caller can supply via the Fixed +/// enum variant, we also support using SOA MINIMUM via the SoaMinimum +/// variant. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub enum Nsec3ParamTtlMode { /// Use a fixed TTL value. Fixed(Ttl), - /// Use the TTL of the SOA record MINIMUM data field. + /// Use the TTL of the SOA record. #[default] + Soa, + + /// Use the TTL of the SOA record MINIMUM data field. SoaMinimum, } @@ -621,6 +640,10 @@ impl Nsec3ParamTtlMode { Self::Fixed(ttl) } + pub fn soa() -> Self { + Self::Soa + } + pub fn soa_minimum() -> Self { Self::SoaMinimum } diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index c52ed0b1a..829c53d23 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -145,18 +145,17 @@ where // Are we signing the entire tree from the apex down or just some child // records? Use the first found SOA RR as the apex. If no SOA RR can be // found assume that we are only signing records below the apex. - let apex_ttl = families.peek().and_then(|first_family| { + let soa_ttl = families.peek().and_then(|first_family| { first_family.records().find_map(|rr| { if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { - if let ZoneRecordData::Soa(soa) = rr.data() { - return Some(soa.minimum()); - } + Some(rr.ttl()) + } else { + None } - None }) }); - if let Some(soa_minimum_ttl) = apex_ttl { + if let Some(soa_ttl) = soa_ttl { // Sign the apex // SAFETY: We just checked above if the apex records existed. let apex_family = families.next().unwrap(); @@ -176,22 +175,26 @@ where // Determine the TTL of any existing DNSKEY RRSET and use that as the // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA - // mininmum TTL. + // TTL. + // + // https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 5.2. TTLs + // of RRs in an RRSet "Consequently the use of differing TTLs in an + // RRSet is hereby deprecated, the TTLs of all RRs in an RRSet must + // be the same." // - // Applicable sections from RFC 1033: - // TTL's (Time To Live) - // "Also, all RRs with the same name, class, and type should have - // the same TTL value." + // Note that while RFC 1033 says: RESOURCE RECORDS "If you leave the + // TTL field blank it will default to the minimum time specified in + // the SOA record (described later)." // - // RESOURCE RECORDS - // "If you leave the TTL field blank it will default to the - // minimum time specified in the SOA record (described later)." + // That RFC pre-dates RFC 1034, and neither dnssec-signzone nor + // ldns-signzone use the SOA MINIMUM as a default TTL, rather they use + // the TTL of the SOA RR as the default and so we will do the same. let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { let ttl = rrset.ttl(); augmented_apex_dnskey_rrs.sorted_extend(rrset.iter().cloned()); ttl } else { - soa_minimum_ttl + soa_ttl }; for public_key in diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 557f00673..1226baf53 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -249,7 +249,15 @@ where let ttl = match ttl_mode { Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::SoaMinimum => soa.ttl(), + Nsec3ParamTtlMode::Soa => soa.ttl(), + Nsec3ParamTtlMode::SoaMinimum => { + if let ZoneRecordData::Soa(soa_data) = soa.data() { + soa_data.minimum() + } else { + // Errm, this is unexpected. + soa.ttl() + } + } }; param.set_ttl(ttl); From 397ade4b4ef15f329869b7baf0949a1430a42f13 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 7 Jan 2025 13:00:12 +0100 Subject: [PATCH 289/569] Add TODO comment. --- src/sign/signing/traits.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 1226baf53..c34f1fea2 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -254,7 +254,8 @@ where if let ZoneRecordData::Soa(soa_data) = soa.data() { soa_data.minimum() } else { - // Errm, this is unexpected. + // Errm, this is unexpected. TODO: Should we abort + // with an error here about a malformed zonefile? soa.ttl() } } From 7e7d3840de54acc91545e922ca150b3209e8b954 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:20:56 +0100 Subject: [PATCH 290/569] Remove unnecessary function. --- src/sign/keys/keymeta.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs index b88ad822e..1a2db9d99 100644 --- a/src/sign/keys/keymeta.rs +++ b/src/sign/keys/keymeta.rs @@ -143,16 +143,6 @@ where pub fn into_inner(self) -> SigningKey { self.key } - - // Note: This cannot be done as impl AsRef because AsRef requires that the - // lifetime of the returned reference be 'static, and we don't do impl Any - // as then the caller has to deal with Option or Result because the type - // might not impl DesignatedSigningKey. - pub fn as_designated_signing_key( - &self, - ) -> &dyn DesignatedSigningKey { - self - } } //--- impl Deref From 70a189407650750fd2ea792488926e199e5661f9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:45:02 +0100 Subject: [PATCH 291/569] Use Deref instead of adding a new RecordSlice trait. --- src/sign/hashing/nsec3.rs | 3 +-- src/sign/records.rs | 11 ++++++----- src/sign/signing/rrsigs.rs | 4 ++-- src/sign/signing/traits.rs | 28 +++++++++++----------------- 4 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 28e00a25f..50d4adeee 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -17,7 +17,6 @@ use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; -use crate::sign::signing::traits::RecordSlice; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; @@ -378,7 +377,7 @@ where for i in 1..=num_nsec3s { // TODO: Detect duplicate hashes. let next_i = if i == num_nsec3s { 0 } else { i }; - let cur_owner = nsec3s.as_slice()[next_i].owner(); + let cur_owner = nsec3s.as_ref()[next_i].owner(); let name: Name = cur_owner.try_to_name().unwrap(); let label = name.iter_labels().next().unwrap(); let owner_hash = if let Ok(hash_octets) = diff --git a/src/sign/records.rs b/src/sign/records.rs index 38d0be0e1..220a1b2b0 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -3,6 +3,7 @@ use core::cmp::Ordering; use core::convert::From; use core::iter::Extend; use core::marker::{PhantomData, Send}; +use core::ops::Deref; use core::slice::Iter; use std::vec::Vec; @@ -15,8 +16,6 @@ use crate::base::rdata::RecordData; use crate::base::record::Record; use crate::base::Ttl; -use super::signing::traits::RecordSlice; - //------------ Sorter -------------------------------------------------------- /// A DNS resource record sorter. @@ -243,14 +242,16 @@ where } } -impl RecordSlice for SortedRecords +impl Deref for SortedRecords where N: Send, D: Send, S: Sorter, { - fn as_slice(&self) -> &[Record] { - self.records.as_slice() + type Target = [Record]; + + fn deref(&self) -> &Self::Target { + &self.records } } diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index 829c53d23..938c1a6bb 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -26,7 +26,7 @@ use crate::sign::records::{ FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::signing::strategy::SigningKeyUsageStrategy; -use crate::sign::signing::traits::{RecordSlice, SortedExtend}; +use crate::sign::signing::traits::SortedExtend; use crate::sign::{SignRaw, SigningKey}; /// Generate RRSIG RRs for a collection of unsigned zone records. @@ -228,7 +228,7 @@ where } let augmented_apex_dnskey_rrset = - Rrset::new(augmented_apex_dnskey_rrs.as_slice()); + Rrset::new(&augmented_apex_dnskey_rrs); // Sign the apex RRSETs in canonical order. for rrset in apex_rrsets diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index c34f1fea2..4be735443 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -3,6 +3,7 @@ use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; use core::marker::{PhantomData, Send}; +use core::ops::Deref; use std::boxed::Box; use std::hash::Hash; @@ -62,12 +63,6 @@ where } } -//------------ RecordSlice --------------------------------------------------- - -pub trait RecordSlice { - fn as_slice(&self) -> &[Record]; -} - //------------ SignableZoneInOut --------------------------------------------- enum SignableZoneInOut<'a, 'b, N, Octs, S, T> @@ -100,7 +95,7 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, S: SignableZone, - T: RecordSlice> + T: Deref>]> + SortedExtend + ?Sized, { @@ -125,16 +120,15 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, S: SignableZone, - T: RecordSlice> + T: Deref>]> + SortedExtend + ?Sized, { fn as_slice(&self) -> &[Record>] { match self { - SignableZoneInOut::SignInPlace(input_output, _) => { - input_output.as_slice() - } - SignableZoneInOut::SignInto(input, _) => input.as_slice(), + SignableZoneInOut::SignInPlace(input_output, _) => input_output, + + SignableZoneInOut::SignInto(input, _) => input, } } @@ -188,7 +182,7 @@ where + OctetsFrom> + From> + Default, - T: RecordSlice>, + T: Deref>]>, { let soa = in_out .as_slice() @@ -309,7 +303,7 @@ where //------------ SignableZone -------------------------------------------------- pub trait SignableZone: - RecordSlice> + Deref>]> where N: Clone + ToName + From> + PartialEq + Ord + Hash, Octs: Clone @@ -340,9 +334,9 @@ where <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, - T: SortedExtend - + ?Sized - + RecordSlice>, + T: Deref>]> + + SortedExtend + + ?Sized, Self: Sized, { let in_out = SignableZoneInOut::new_into(self, out); From b7a65c0d468e12cc88e4a72da37b3b76f6a6d64e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:46:23 +0100 Subject: [PATCH 292/569] Make it possible to construct SortedRecords without specifying the sorter, via Default. --- src/sign/records.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 220a1b2b0..fdd91b134 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -310,8 +310,8 @@ where } } -impl Default - for SortedRecords +impl Default + for SortedRecords { fn default() -> Self { Self::new() From 34f681a3ba6439f92be24d8803ae19428ca407b9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:46:45 +0100 Subject: [PATCH 293/569] Make the Default SigningConfig actually have default behaviour. --- src/sign/signing/config.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs index 0f5a30d46..e039a071f 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/signing/config.rs @@ -1,13 +1,18 @@ use core::marker::PhantomData; +use octseq::{EmptyBuilder, FromBuilder}; + +use crate::base::{Name, ToName}; use crate::sign::hashing::config::HashingConfig; use crate::sign::hashing::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; -use crate::sign::records::Sorter; +use crate::sign::records::{DefaultSorter, Sorter}; use crate::sign::signing::strategy::SigningKeyUsageStrategy; use crate::sign::SignRaw; +use super::strategy::DefaultSigningKeyUsageStrategy; + //------------ SigningConfig ------------------------------------------------- /// Signing configuration for a DNSSEC signed zone. @@ -51,14 +56,20 @@ where } } -impl Default - for SigningConfig +impl Default + for SigningConfig< + N, + Octs, + Key, + DefaultSigningKeyUsageStrategy, + DefaultSorter, + OnDemandNsec3HashProvider, + > where - HP: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, + N: ToName + From>, + Octs: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Key: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, { fn default() -> Self { Self { From 5da1bb0b0bf0aafb7ddd83937d34eb7d8ad9eb7c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:47:04 +0100 Subject: [PATCH 294/569] FIX: Don't panic when signing a zone that lacks a SOA. --- src/sign/signing/traits.rs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 4be735443..c4b7c9e73 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -315,7 +315,7 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, { - fn apex(&self) -> FamilyName; + fn apex(&self) -> Option>; // TODO // fn iter_mut(&mut self) -> T; @@ -342,7 +342,7 @@ where let in_out = SignableZoneInOut::new_into(self, out); sign_zone::( in_out, - &self.apex(), + &self.apex().ok_or(SigningError::NoSoaFound)?, signing_config, signing_keys, ) @@ -372,16 +372,8 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, S: Sorter, { - fn apex(&self) -> FamilyName { - self.find_soa().unwrap().family_name().cloned() - } -} - -//--- impl RecordSlice for Vec - -impl RecordSlice for Vec> { - fn as_slice(&self) -> &[Record] { - Vec::as_slice(self) + fn apex(&self) -> Option> { + self.find_soa().map(|soa| soa.family_name().cloned()) } } @@ -408,11 +400,10 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, { - fn apex(&self) -> FamilyName { + fn apex(&self) -> Option> { self.iter() .find(|r| r.rtype() == Rtype::SOA) .map(|r| FamilyName::new(r.owner().clone(), r.class())) - .unwrap() } } @@ -446,7 +437,7 @@ where KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, { - let apex = self.apex(); + let apex = self.apex().ok_or(SigningError::NoSoaFound)?; let in_out = SignableZoneInOut::new_in_place(self); sign_zone::( in_out, From 955d320f781b859dc4fecd13d3a41d89580002e3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:47:47 +0100 Subject: [PATCH 295/569] Start updating the RustDoc for the sign module. --- src/sign/mod.rs | 181 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 152 insertions(+), 29 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index dde2b43b9..0d5904f3c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,24 +2,80 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! Signatures are at the heart of DNSSEC -- they confirm the authenticity of -//! a DNS record served by a security-aware name server. Signatures can be -//! made "online" (in an authoritative name server while it is running) or -//! "offline" (outside of a name server). Once generated, signatures can be -//! serialized as DNS records and stored alongside the authenticated records. +//! This module provides support for DNSSEC signing of zones. +//! +//! DNSSEC signed zones consist of configuration data such as DNSKEY and +//! NSEC3PARAM records, NSEC(3) chains used to provably deny the existence of +//! records, and signatures that authenticate the authoritative content of the +//! zone. +//! +//! # Overview +//! +//! This module provides support for working with DNSSEC signing keys and +//! using them to DNSSEC sign sorted [`Record`] collections via the traits +//! [`SignableZone`], [`SignableZoneInPlace`] and [`Signable`]. +//! +//!
+//! +//! This module does **NOT** yet support signing of records stored in a +//! [`Zone`]. +//! +//!
//! //! 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. [`keys::keypair::KeyPair`]). +//! to provide the underlying signing operation (e.g. +//! [`keys::keypair::KeyPair`]). +//! +//! While all records in a zone can be signed with a single key, it is useful +//! to use one key, a Key Signing Key (KSK), "to sign the apex DNSKEY RRset in +//! a zone" and another key, a Zone Signing Key (ZSK), "to sign all the RRsets +//! in a zone that require signatures, other than the apex DNSKEY RRset" (see +//! [RFC 6781 section 3.1]). //! -//! # Example Usage +//! Cryptographically there is no difference between these key types, they are +//! assigned by the operator to signal their intended usage. This module +//! provides the [`DnssecSigningKey`] wrapper type around a [`SigningKey`] to +//! allow the intended usage of the key to be signalled by the operator, and +//! [`SigningKeyUsageStrategy`] to allow different key usage strategies to be +//! defined and selected to influence how the different types of key affect +//! signing. //! -//! At the moment, only "low-level" signing is supported. +//! # Importing keys +//! +//! Keys can be imported from files stored on disk in the conventional BIND +//! format. +//! +//! ``` +//! # use domain::base::iana::SecAlg; +//! # use domain::{sign::*, validate}; +//! // Load an Ed25519 key named 'Ktest.+015+56037'. +//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; +//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); +//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); +//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); +//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! +//! // Associate the key with important metadata. +//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); +//! +//! // Check that the owner, algorithm, and key tag matched expectations. +//! assert_eq!(key.owner().to_string(), "test"); +//! assert_eq!(key.algorithm(), SecAlg::ED25519); +//! assert_eq!(key.public_key().key_tag(), 56037); +//! ``` +//! +//! # Generating keys +//! +//! Keys can also be generated. //! //! ``` -//! # use domain::sign::*; //! # use domain::base::Name; +//! # use domain::sign::*; //! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; //! let (sec_bytes, pub_bytes) = keys::keypair::generate(params).unwrap(); @@ -35,36 +91,103 @@ //! // Access the public key (with metadata). //! let pub_key = key.public_key(); //! println!("{:?}", pub_key); +//! ``` +//! +//! # Low level signing //! +//! Given some data and a key, the data can be signed with the key. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::sign::*; +//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let key = SigningKey::new(Name::>::root(), 257, key_pair); //! // Sign arbitrary byte sequences with the key. //! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); //! println!("{:?}", sig); //! ``` //! -//! It is also possible to import keys stored on disk in the conventional BIND -//! format. +//! # High level signing +//! +//! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is +//! implemented, invoke `sign_zone()` on the type. +//! +//!
+//! +//! Currently there is no support for re-signing a zone, i.e. ensuring +//! that any changes to the authoritative records in the zone are reflected +//! by updating the NSEC(3) chain and generating additional signatures or +//! regenerating existing ones that have expired. +//! +//!
//! //! ``` -//! # use domain::base::iana::SecAlg; -//! # use domain::{sign::*, validate}; -//! // Load an Ed25519 key named 'Ktest.+015+56037'. -//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; -//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); -//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); -//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); -//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); +//! # use domain::base::{*, iana::Class}; +//! # use domain::sign::*; +//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let root = Name::>::root(); +//! # let key = SigningKey::new(root.clone(), 257, key_pair); +//! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; +//! use domain::rdata::dnssec::Timestamp; +//! use domain::sign::keys::keymeta::{DnssecSigningKey, DesignatedSigningKey}; +//! use domain::sign::records::{DefaultSorter, SortedRecords}; +//! use domain::sign::signing::config::SigningConfig; +//! use domain::sign::signing::traits::SignableZoneInPlace; //! -//! // Parse the key into Ring or OpenSSL. -//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! // Create a sorted collection of records. +//! let mut records = SortedRecords::default(); //! -//! // Associate the key with important metadata. -//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); +//! // Insert records into the collection. +//! let soa = Soa::new(root.clone(), root.clone(), Serial::now(), Ttl::ZERO, Ttl::ZERO, Ttl::ZERO, Ttl::ZERO); +//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, ZoneRecordData::Soa(soa))); +//! +//! // Generate or import signing keys (see above). +//! +//! // Assign signature validity period and operator intent to the keys. +//! let key = key.with_validity(Timestamp::now(), Timestamp::now()); +//! let dnssec_signing_key = DnssecSigningKey::new_csk(key); +//! let keys = [&dnssec_signing_key as &dyn DesignatedSigningKey<_, _>]; +//! +//! // Create a signing configuration. +//! let mut signing_config = SigningConfig::default(); +//! +//! // Then sign the zone in place. +//! records.sign_zone(&mut signing_config, &keys).unwrap(); +//! ``` +//! +//! If needed, individual RRsets can also be signed:`` //! -//! // Check that the owner, algorithm, and key tag matched expectations. -//! assert_eq!(key.owner().to_string(), "test"); -//! assert_eq!(key.algorithm(), SecAlg::ED25519); -//! assert_eq!(key.public_key().key_tag(), 56037); //! ``` +//! # use domain::base::Name; +//! # use domain::base::iana::Class; +//! # use domain::sign::*; +//! # use domain::sign::keys::keymeta::{DesignatedSigningKey, DnssecSigningKey}; +//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let root = Name::>::root(); +//! # let key = SigningKey::new(root, 257, key_pair); +//! # let dnssec_signing_key = DnssecSigningKey::new_csk(key); +//! # let keys = [&dnssec_signing_key as &dyn DesignatedSigningKey<_, _>]; +//! # let mut records = records::SortedRecords::<_, _, domain::sign::records::DefaultSorter>::new(); +//! use domain::sign::signing::traits::Signable; +//! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; +//! let apex = records::FamilyName::new(Name::>::root(), Class::IN); +//! let rrset = records::Rrset::new(&records); +//! let generated_records = rrset.sign::(&apex, &keys).unwrap(); +//! ``` +//! +//! [`DnssecSigningKey`]: crate::sign::keys::keymeta::DnssecSigningKey +//! [`Record`]: crate::base::record::Record +//! [RFC 6871 section 3.1]: https://rfc-editor.org/rfc/rfc6781#section-3.1 +//! [`SigningKeyUsageStrategy`]: +//! crate::sign::signing::strategy::SigningKeyUsageStrategy +//! [`Signable`]: crate::sign::signing::traits::Signable +//! [`SignableZone`]: crate::sign::signing::traits::SignableZone +//! [`SignableZoneInPlace`]: crate::sign::signing::traits::SignableZoneInPlace +//! [`SortedRecords`]: crate::sign::SortedRecords +//! [`Zone`]: crate::zonetree::Zone //! //! # Cryptography //! @@ -111,8 +234,8 @@ //! //! # Key Sets and Key Lifetime //! The [`keyset`] module provides a way to keep track of the collection of -//! keys that are used to sign a particular zone. In addition, the lifetime -//! of keys can be maintained using key rolls that phase out old keys and +//! keys that are used to sign a particular zone. In addition, the lifetime of +//! keys can be maintained using key rolls that phase out old keys and //! introduce new keys. #![cfg(feature = "unstable-sign")] From 9e9baec936dfa723f31c1fc020b4318da3f2671e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:49:43 +0100 Subject: [PATCH 296/569] RustDoc formatting. --- src/sign/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 0d5904f3c..660d04dcc 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -139,8 +139,15 @@ //! // Create a sorted collection of records. //! let mut records = SortedRecords::default(); //! -//! // Insert records into the collection. -//! let soa = Soa::new(root.clone(), root.clone(), Serial::now(), Ttl::ZERO, Ttl::ZERO, Ttl::ZERO, Ttl::ZERO); +//! // Insert records into the collection. Just a dummy SOA for this example. +//! let soa = Soa::new( +//! root.clone(), +//! root.clone(), +//! Serial::now(), +//! Ttl::ZERO, +//! Ttl::ZERO, +//! Ttl::ZERO, +//! Ttl::ZERO); //! records.insert(Record::new(root, Class::IN, Ttl::ZERO, ZoneRecordData::Soa(soa))); //! //! // Generate or import signing keys (see above). From d45960fb5138ac6b42bc423a3db7c2b0ad855e6e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 8 Jan 2025 13:50:45 +0100 Subject: [PATCH 297/569] Remove errant backticks in RustDoc. --- src/sign/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 660d04dcc..82b88609b 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -164,7 +164,7 @@ //! records.sign_zone(&mut signing_config, &keys).unwrap(); //! ``` //! -//! If needed, individual RRsets can also be signed:`` +//! If needed, individual RRsets can also be signed: //! //! ``` //! # use domain::base::Name; From 51d5bed9dd0f2fa6eb338b44f2f19708e5205e51 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 10:14:08 +0100 Subject: [PATCH 298/569] Use user supplied sort impl everywhere, and require CanonicalOrd. --- src/sign/records.rs | 6 +- src/sign/signing/traits.rs | 109 +++++++++++++++++++++++++------------ 2 files changed, 76 insertions(+), 39 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index fdd91b134..3448c8743 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -34,8 +34,8 @@ pub trait Sorter { /// ascending order, by their numeric RR TYPE"_. fn sort_by(records: &mut Vec>, compare: F) where - Record: Send, - F: Fn(&Record, &Record) -> Ordering + Sync; + F: Fn(&Record, &Record) -> Ordering + Sync, + Record: CanonicalOrd + Send; } //------------ DefaultSorter ------------------------------------------------- @@ -49,8 +49,8 @@ pub struct DefaultSorter; impl Sorter for DefaultSorter { fn sort_by(records: &mut Vec>, compare: F) where - Record: Send, F: Fn(&Record, &Record) -> Ordering + Sync, + Record: CanonicalOrd + Send, { records.sort_by(compare); } diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index c4b7c9e73..50e203b70 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -36,7 +36,10 @@ use crate::sign::SignRaw; //------------ SortedExtend -------------------------------------------------- -pub trait SortedExtend { +pub trait SortedExtend +where + Sort: Sorter, +{ fn sorted_extend< T: IntoIterator>>, >( @@ -45,12 +48,12 @@ pub trait SortedExtend { ); } -impl SortedExtend - for SortedRecords, S> +impl SortedExtend + for SortedRecords, Sort> where N: Send + PartialEq + ToName, Octs: Send, - S: Sorter, + Sort: Sorter, ZoneRecordData: CanonicalOrd + PartialEq, { fn sorted_extend< @@ -59,13 +62,43 @@ where &mut self, iter: T, ) { + // SortedRecords::extend() takes care of sorting and de-duplication so + // we don't have to. self.extend(iter); } } +//---- impl for Vec + +impl SortedExtend + for Vec>> +where + N: Send + PartialEq + ToName, + Octs: Send, + Sort: Sorter, + ZoneRecordData: CanonicalOrd + PartialEq, +{ + fn sorted_extend< + T: IntoIterator>>, + >( + &mut self, + iter: T, + ) { + // This call to extend may add duplicates. + self.extend(iter); + + // Sort the records using the provided sort implementation. + Sort::sort_by(self, CanonicalOrd::canonical_cmp); + + // And remove any duplicates that were created. + // Requires that the vector first be sorted. + self.dedup(); + } +} + //------------ SignableZoneInOut --------------------------------------------- -enum SignableZoneInOut<'a, 'b, N, Octs, S, T> +enum SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, Octs: Clone @@ -76,14 +109,16 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, - T: SortedExtend + ?Sized, + S: SignableZone, + Sort: Sorter, + T: SortedExtend + ?Sized, { - SignInPlace(&'a mut T, PhantomData<(N, Octs)>), + SignInPlace(&'a mut T, PhantomData<(N, Octs, Sort)>), SignInto(&'a S, &'b mut T), } -impl<'a, 'b, N, Octs, S, T> SignableZoneInOut<'a, 'b, N, Octs, S, T> +impl<'a, 'b, N, Octs, S, T, Sort> + SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, Octs: Clone @@ -94,9 +129,10 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, + S: SignableZone, + Sort: Sorter, T: Deref>]> - + SortedExtend + + SortedExtend + ?Sized, { fn new_in_place(signable_zone: &'a mut T) -> Self { @@ -108,7 +144,7 @@ where } } -impl SignableZoneInOut<'_, '_, N, Octs, S, T> +impl SignableZoneInOut<'_, '_, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, Octs: Clone @@ -119,9 +155,10 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, + S: SignableZone, + Sort: Sorter, T: Deref>]> - + SortedExtend + + SortedExtend + ?Sized, { fn as_slice(&self) -> &[Record>] { @@ -152,7 +189,7 @@ where //------------ sign_zone() --------------------------------------------------- fn sign_zone( - mut in_out: SignableZoneInOut, + mut in_out: SignableZoneInOut, apex: &FamilyName, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], @@ -172,9 +209,9 @@ where Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, - S: SignableZone, + S: SignableZone, Sort: Sorter, - T: SortedExtend + ?Sized, + T: SortedExtend + ?Sized, Octs: FromBuilder + Clone + From<&'static [u8]> @@ -302,7 +339,7 @@ where //------------ SignableZone -------------------------------------------------- -pub trait SignableZone: +pub trait SignableZone: Deref>]> where N: Clone + ToName + From> + PartialEq + Ord + Hash, @@ -314,13 +351,14 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + Sort: Sorter, { fn apex(&self) -> Option>; // TODO // fn iter_mut(&mut self) -> T; - fn sign_zone( + fn sign_zone( &self, signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], @@ -333,9 +371,8 @@ where ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, T: Deref>]> - + SortedExtend + + SortedExtend + ?Sized, Self: Sized, { @@ -351,8 +388,8 @@ where //--- impl SignableZone for SortedRecords -impl SignableZone - for SortedRecords, S> +impl SignableZone + for SortedRecords, Sort> where N: Clone + ToName @@ -370,7 +407,7 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: Sorter, + Sort: Sorter, { fn apex(&self) -> Option> { self.find_soa().map(|soa| soa.family_name().cloned()) @@ -380,7 +417,7 @@ where //--- impl SignableZone for Vec // NOTE: Assumes that the Vec is already sorted according to CanonicalOrd. -impl SignableZone +impl SignableZone for Vec>> where N: Clone @@ -399,6 +436,7 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + Sort: Sorter, { fn apex(&self) -> Option> { self.iter() @@ -409,8 +447,8 @@ where //------------ SignableZoneInPlace ------------------------------------------- -pub trait SignableZoneInPlace: - SignableZone + SortedExtend +pub trait SignableZoneInPlace: + SignableZone + SortedExtend where N: Clone + ToName + From> + PartialEq + Ord + Hash, Octs: Clone @@ -421,11 +459,12 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Self: SortedExtend + Sized, + Self: SortedExtend + Sized, + S: Sorter, { - fn sign_zone( + fn sign_zone( &mut self, - signing_config: &mut SigningConfig, + signing_config: &mut SigningConfig, signing_keys: &[&dyn DesignatedSigningKey], ) -> Result<(), SigningError> where @@ -435,11 +474,10 @@ where ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, { let apex = self.apex().ok_or(SigningError::NoSoaFound)?; let in_out = SignableZoneInOut::new_in_place(self); - sign_zone::( + sign_zone::( in_out, &apex, signing_config, @@ -450,8 +488,8 @@ where //--- impl SignableZoneInPlace for SortedRecords -impl SignableZoneInPlace - for SortedRecords, S> +impl SignableZoneInPlace + for SortedRecords, Sort> where N: Clone + ToName @@ -469,8 +507,7 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - - S: Sorter, + Sort: Sorter, { } From 8d49648f9b6fc86cca73b49ccbb6db7a3899326c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:02:22 +0100 Subject: [PATCH 299/569] Group and move things around in the sign module. --- src/sign/crypto/openssl.rs | 10 +- src/sign/crypto/ring.rs | 8 +- src/sign/error.rs | 110 +++++++- src/sign/keys/keymeta.rs | 3 +- src/sign/keys/keypair.rs | 51 +++- src/sign/keys/mod.rs | 1 + src/sign/keys/signingkey.rs | 152 ++++++++++ src/sign/mod.rs | 540 +++++++++++++++++------------------- src/sign/signing/rrsigs.rs | 8 +- src/sign/signing/traits.rs | 300 ++++---------------- 10 files changed, 614 insertions(+), 569 deletions(-) create mode 100644 src/sign/keys/signingkey.rs diff --git a/src/sign/crypto/openssl.rs b/src/sign/crypto/openssl.rs index 20f1185e7..49c0348ef 100644 --- a/src/sign/crypto/openssl.rs +++ b/src/sign/crypto/openssl.rs @@ -12,6 +12,7 @@ #![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] use core::fmt; + use std::{boxed::Box, vec::Vec}; use openssl::{ @@ -23,9 +24,9 @@ use openssl::{ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; -use crate::sign::{ - GenerateParams, RsaSecretKeyBytes, SecretKeyBytes, SignError, SignRaw, -}; +use crate::sign::error::SignError; +use crate::sign::keys::keypair::GenerateParams; +use crate::sign::{RsaSecretKeyBytes, SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; //----------- KeyPair -------------------------------------------------------- @@ -449,11 +450,12 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{GenerateParams, SecretKeyBytes, SignRaw}, + sign::{SecretKeyBytes, SignRaw}, validate::Key, }; use super::KeyPair; + use crate::sign::keys::keypair::GenerateParams; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), diff --git a/src/sign/crypto/ring.rs b/src/sign/crypto/ring.rs index 52d1b1901..52c35c102 100644 --- a/src/sign/crypto/ring.rs +++ b/src/sign/crypto/ring.rs @@ -11,6 +11,7 @@ #![cfg_attr(docsrs, doc(cfg(feature = "ring")))] use core::fmt; + use std::{boxed::Box, sync::Arc, vec::Vec}; use ring::signature::{ @@ -19,7 +20,9 @@ use ring::signature::{ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; -use crate::sign::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::sign::error::SignError; +use crate::sign::keys::keypair::GenerateParams; +use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; //----------- KeyPair -------------------------------------------------------- @@ -364,11 +367,12 @@ mod tests { use crate::{ base::iana::SecAlg, - sign::{GenerateParams, SecretKeyBytes, SignRaw}, + sign::{SecretKeyBytes, SignRaw}, validate::Key, }; use super::KeyPair; + use crate::sign::keys::keypair::GenerateParams; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), diff --git a/src/sign/error.rs b/src/sign/error.rs index dd97a6d45..5d0899087 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -1,5 +1,5 @@ //! Actual signing. -use core::fmt::{Debug, Display}; +use core::fmt::{self, Debug, Display}; use crate::validate::Nsec3HashError; @@ -8,7 +8,7 @@ use crate::validate::Nsec3HashError; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum SigningError { /// One or more keys does not have a signature validity period defined. - KeyLacksSignatureValidityPeriod, + NoSignatureValidityPeriodProvided, /// TODO OutOfMemory, @@ -20,30 +20,116 @@ pub enum SigningError { /// [`SigningKeyUsageStrategy`] used. NoSuitableKeysFound, + // TODO NoSoaFound, + // TODO Nsec3HashingError(Nsec3HashError), - MissingSigningConfiguration, + + // TODO + SigningError(SignError), } impl Display for SigningError { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { - SigningError::KeyLacksSignatureValidityPeriod => { - f.write_str("KeyLacksSignatureValidityPeriod") + SigningError::NoSignatureValidityPeriodProvided => { + f.write_str("No signature validity period found for key") + } + SigningError::OutOfMemory => f.write_str("Out of memory"), + SigningError::NoKeysProvided => { + f.write_str("No signing keys provided") } - SigningError::OutOfMemory => f.write_str("OutOfMemory"), - SigningError::NoKeysProvided => f.write_str("NoKeysProvided"), SigningError::NoSuitableKeysFound => { - f.write_str("NoSuitableKeysFound") + f.write_str("No suitable keys found") + } + SigningError::NoSoaFound => { + f.write_str("nNo apex SOA record found") } - SigningError::NoSoaFound => f.write_str("NoSoaFound"), SigningError::Nsec3HashingError(err) => { - f.write_fmt(format_args!("Nsec3HashingError: {err}")) + f.write_fmt(format_args!("NSEC3 hashing error: {err}")) } - SigningError::MissingSigningConfiguration => { - f.write_str("MissingSigningConfiguration") + SigningError::SigningError(err) => { + f.write_fmt(format_args!("Signing error: {err}")) } } } } + +impl From for SigningError { + fn from(err: SignError) -> Self { + Self::SigningError(err) + } +} + +//----------- SignError ------------------------------------------------------ + +/// A signature failure. +/// +/// In case such an error occurs, callers should stop using the key pair they +/// attempted to sign with. If such an error occurs with every key pair they +/// have available, or if such an error occurs with a freshly-generated key +/// pair, they should use a different cryptographic implementation. If that +/// is not possible, they must forego signing entirely. +/// +/// # Failure Cases +/// +/// Signing should be an infallible process. There are three considerable +/// failure cases for it: +/// +/// - The secret key was invalid (e.g. its parameters were inconsistent). +/// +/// Such a failure would mean that all future signing (with this key) will +/// also fail. In any case, the implementations provided by this crate try +/// to verify the key (e.g. by checking the consistency of the private and +/// public components) before any signing occurs, largely ruling this class +/// of errors out. +/// +/// - Not enough randomness could be obtained. This applies to signature +/// algorithms which use randomization (e.g. RSA and ECDSA). +/// +/// On the vast majority of platforms, randomness can always be obtained. +/// The [`getrandom` crate documentation][getrandom] notes: +/// +/// > If an error does occur, then it is likely that it will occur on every +/// > call to getrandom, hence after the first successful call one can be +/// > reasonably confident that no errors will occur. +/// +/// [getrandom]: https://docs.rs/getrandom +/// +/// Thus, in case such a failure occurs, all future signing will probably +/// also fail. +/// +/// - Not enough memory could be allocated. +/// +/// Signature algorithms have a small memory overhead, so an out-of-memory +/// condition means that the program is nearly out of allocatable space. +/// +/// Callers who do not expect allocations to fail (i.e. who are using the +/// standard memory allocation routines, not their `try_` variants) will +/// likely panic shortly after such an error. +/// +/// Callers who are aware of their memory usage will likely restrict it far +/// before they get to this point. Systems running at near-maximum load +/// tend to quickly become unresponsive and staggeringly slow. If memory +/// usage is an important consideration, programs will likely cap it before +/// the system reaches e.g. 90% memory use. +/// +/// As such, memory allocation failure should never really occur. It is far +/// more likely that one of the other errors has occurred. +/// +/// It may be reasonable to panic in any such situation, since each kind of +/// error is essentially unrecoverable. However, applications where signing +/// is an optional step, or where crashing is prohibited, may wish to recover +/// from such an error differently (e.g. by foregoing signatures or informing +/// an operator). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SignError; + +impl fmt::Display for SignError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("could not create a cryptographic signature") + } +} + +impl std::error::Error for SignError {} diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs index 1a2db9d99..9df2bf0b4 100644 --- a/src/sign/keys/keymeta.rs +++ b/src/sign/keys/keymeta.rs @@ -2,7 +2,8 @@ use core::convert::From; use core::marker::PhantomData; use core::ops::Deref; -use crate::sign::{SignRaw, SigningKey}; +use crate::sign::keys::signingkey::SigningKey; +use crate::sign::SignRaw; //------------ DesignatedSigningKey ------------------------------------------ diff --git a/src/sign/keys/keypair.rs b/src/sign/keys/keypair.rs index 27d66a836..fc4b01745 100644 --- a/src/sign/keys/keypair.rs +++ b/src/sign/keys/keypair.rs @@ -10,7 +10,8 @@ use std::sync::Arc; use ::ring::rand::SystemRandom; use crate::base::iana::SecAlg; -use crate::sign::{GenerateParams, SecretKeyBytes, SignError, SignRaw}; +use crate::sign::error::SignError; +use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, Signature}; #[cfg(feature = "openssl")] @@ -118,6 +119,54 @@ impl SignRaw for KeyPair { } } +//----------- GenerateParams ------------------------------------------------- + +/// Parameters for generating a secret key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GenerateParams { + /// Generate an RSA/SHA-256 keypair. + RsaSha256 { + /// The number of bits in the public modulus. + /// + /// 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, + }, + + /// Generate an ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256, + + /// Generate an ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384, + + /// Generate an Ed25519 keypair. + Ed25519, + + /// An Ed448 keypair. + Ed448, +} + +//--- Inspection + +impl GenerateParams { + /// The algorithm of the generated key. + pub fn algorithm(&self) -> SecAlg { + match self { + Self::RsaSha256 { .. } => SecAlg::RSASHA256, + Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, + Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, + Self::Ed25519 => SecAlg::ED25519, + Self::Ed448 => SecAlg::ED448, + } + } +} + //----------- generate() ----------------------------------------------------- /// Generate a new secret key for the given algorithm. diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index 973c6f236..bab9de0c9 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -2,3 +2,4 @@ pub mod bytes; pub mod keymeta; pub mod keypair; pub mod keyset; +pub mod signingkey; diff --git a/src/sign/keys/signingkey.rs b/src/sign/keys/signingkey.rs new file mode 100644 index 000000000..835f90d86 --- /dev/null +++ b/src/sign/keys/signingkey.rs @@ -0,0 +1,152 @@ +use core::ops::RangeInclusive; + +use crate::base::iana::SecAlg; +use crate::base::Name; +use crate::rdata::dnssec::Timestamp; +use crate::sign::{PublicKeyBytes, SignRaw}; +use crate::validate::Key; + +//----------- SigningKey ----------------------------------------------------- + +/// A signing key. +/// +/// This associates important metadata with a raw cryptographic secret key. +pub struct SigningKey { + /// The owner of the key. + owner: Name, + + /// The flags associated with the key. + /// + /// These flags are stored in the DNSKEY record. + flags: u16, + + /// The raw private key. + inner: Inner, + + /// The validity period to assign to any DNSSEC signatures created using + /// this key. + /// + /// The range spans from the inception timestamp up to and including the + /// expiration timestamp. + signature_validity_period: Option>, +} + +//--- Construction + +impl SigningKey { + /// Construct a new signing key manually. + pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { + Self { + owner, + flags, + inner, + signature_validity_period: None, + } + } + + pub fn with_validity( + mut self, + inception: Timestamp, + expiration: Timestamp, + ) -> Self { + self.signature_validity_period = + Some(RangeInclusive::new(inception, expiration)); + self + } + + pub fn signature_validity_period( + &self, + ) -> Option> { + self.signature_validity_period.clone() + } +} + +//--- Inspection + +impl SigningKey { + /// The owner name attached to the key. + pub fn owner(&self) -> &Name { + &self.owner + } + + /// The flags attached to the key. + pub fn flags(&self) -> u16 { + self.flags + } + + /// The raw secret key. + pub fn raw_secret_key(&self) -> &Inner { + &self.inner + } + + /// Whether this is a zone signing key. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value + /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's + /// > owner name MUST be the name of a zone. If bit 7 has value 0, then + /// > the DNSKEY record holds some other type of DNS public key and MUST + /// > NOT be used to verify RRSIGs that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + pub fn is_zone_signing_key(&self) -> bool { + self.flags & (1 << 8) != 0 + } + + /// Whether this key has been revoked. + /// + /// From [RFC 5011, section 3]: + /// + /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. + /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) + /// > signed by the associated key, then the resolver MUST consider this + /// > key permanently invalid for all purposes except for validating the + /// > revocation. + /// + /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 + pub fn is_revoked(&self) -> bool { + self.flags & (1 << 7) != 0 + } + + /// Whether this is a secure entry point. + /// + /// From [RFC 4034, section 2.1.1]: + /// + /// > Bit 15 of the Flags field is the Secure Entry Point flag, described + /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a + /// > key intended for use as a secure entry point. This flag is only + /// > intended to be a hint to zone signing or debugging software as to + /// > the intended use of this DNSKEY record; validators MUST NOT alter + /// > their behavior during the signature validation process in any way + /// > based on the setting of this bit. This also means that a DNSKEY RR + /// > with the SEP bit set would also need the Zone Key flag set in order + /// > to be able to generate signatures legally. A DNSKEY RR with the SEP + /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs + /// > that cover RRsets. + /// + /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 + /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 + pub fn is_secure_entry_point(&self) -> bool { + self.flags & 1 != 0 + } + + /// The signing algorithm used. + pub fn algorithm(&self) -> SecAlg { + self.inner.algorithm() + } + + /// The associated public key. + pub fn public_key(&self) -> Key<&Octs> + where + Octs: AsRef<[u8]>, + { + let owner = Name::from_octets(self.owner.as_octets()).unwrap(); + Key::new(owner, self.flags, self.inner.raw_public_key()) + } + + /// The associated raw public key. + pub fn raw_public_key(&self) -> PublicKeyBytes { + self.inner.raw_public_key() + } +} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 82b88609b..3a035e0aa 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -248,15 +248,6 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] -use core::fmt; -use core::ops::RangeInclusive; - -use crate::base::{iana::SecAlg, Name}; -use crate::rdata::dnssec::Timestamp; -use crate::validate::Key; - -pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; - pub mod crypto; pub mod error; pub mod hashing; @@ -265,313 +256,278 @@ pub mod records; pub mod signing; pub mod zone; +pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; + pub use keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; -//----------- SigningKey ----------------------------------------------------- - -/// A signing key. -/// -/// This associates important metadata with a raw cryptographic secret key. -pub struct SigningKey { - /// The owner of the key. - owner: Name, - - /// The flags associated with the key. - /// - /// These flags are stored in the DNSKEY record. - flags: u16, - - /// The raw private key. - inner: Inner, - - /// The validity period to assign to any DNSSEC signatures created using - /// this key. - /// - /// The range spans from the inception timestamp up to and including the - /// expiration timestamp. - signature_validity_period: Option>, +use core::cmp::min; +use core::fmt::Display; +use core::hash::Hash; +use core::marker::PhantomData; +use core::ops::Deref; + +use std::boxed::Box; +use std::fmt::Debug; +use std::vec::Vec; + +use crate::base::{CanonicalOrd, ToName}; +use crate::base::{Name, Record, Rtype}; +use crate::rdata::ZoneRecordData; + +use error::SigningError; +use hashing::config::HashingConfig; +use hashing::nsec::generate_nsecs; +use hashing::nsec3::{ + generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, + Nsec3Records, +}; +use keys::keymeta::DesignatedSigningKey; +use octseq::{ + EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, +}; +use records::{FamilyName, RecordsIter, Sorter}; +use signing::config::SigningConfig; +use signing::rrsigs::generate_rrsigs; +use signing::strategy::SigningKeyUsageStrategy; +use signing::traits::{SignRaw, SignableZone, SortedExtend}; + +//------------ SignableZoneInOut --------------------------------------------- + +pub enum SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> +where + N: Clone + ToName + From> + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: SignableZone, + Sort: Sorter, + T: SortedExtend + ?Sized, +{ + SignInPlace(&'a mut T, PhantomData<(N, Octs, Sort)>), + SignInto(&'a S, &'b mut T), } -//--- Construction - -impl SigningKey { - /// Construct a new signing key manually. - pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { - Self { - owner, - flags, - inner, - signature_validity_period: None, - } - } - - pub fn with_validity( - mut self, - inception: Timestamp, - expiration: Timestamp, - ) -> Self { - self.signature_validity_period = - Some(RangeInclusive::new(inception, expiration)); - self +impl<'a, 'b, N, Octs, S, T, Sort> + SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> +where + N: Clone + ToName + From> + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: SignableZone, + Sort: Sorter, + T: Deref>]> + + SortedExtend + + ?Sized, +{ + fn new_in_place(signable_zone: &'a mut T) -> Self { + Self::SignInPlace(signable_zone, Default::default()) } - pub fn signature_validity_period( - &self, - ) -> Option> { - self.signature_validity_period.clone() + fn new_into(signable_zone: &'a S, out: &'b mut T) -> Self { + Self::SignInto(signable_zone, out) } } -//--- Inspection +impl SignableZoneInOut<'_, '_, N, Octs, S, T, Sort> +where + N: Clone + ToName + From> + Ord + Hash, + Octs: Clone + + FromBuilder + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + S: SignableZone, + Sort: Sorter, + T: Deref>]> + + SortedExtend + + ?Sized, +{ + fn as_slice(&self) -> &[Record>] { + match self { + SignableZoneInOut::SignInPlace(input_output, _) => input_output, -impl SigningKey { - /// The owner name attached to the key. - pub fn owner(&self) -> &Name { - &self.owner + SignableZoneInOut::SignInto(input, _) => input, + } } - /// The flags attached to the key. - pub fn flags(&self) -> u16 { - self.flags + fn sorted_extend< + U: IntoIterator>>, + >( + &mut self, + iter: U, + ) { + match self { + SignableZoneInOut::SignInPlace(input_output, _) => { + input_output.sorted_extend(iter) + } + SignableZoneInOut::SignInto(_, output) => { + output.sorted_extend(iter) + } + } } +} - /// The raw secret key. - pub fn raw_secret_key(&self) -> &Inner { - &self.inner - } +//------------ sign_zone() --------------------------------------------------- + +pub fn sign_zone( + mut in_out: SignableZoneInOut, + apex: &FamilyName, + signing_config: &mut SigningConfig, + signing_keys: &[&dyn DesignatedSigningKey], +) -> Result<(), SigningError> +where + HP: Nsec3HashProvider, + Key: SignRaw, + N: Display + + Send + + CanonicalOrd + + Clone + + ToName + + From> + + Ord + + Hash, + ::Builder: + Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, + <::Builder as OctetsBuilder>::AppendError: Debug, + KeyStrat: SigningKeyUsageStrategy, + S: SignableZone, + Sort: Sorter, + T: SortedExtend + ?Sized, + Octs: FromBuilder + + Clone + + From<&'static [u8]> + + Send + + OctetsFrom> + + From> + + Default, + T: Deref>]>, +{ + let soa = in_out + .as_slice() + .iter() + .find(|r| r.rtype() == Rtype::SOA) + .ok_or(SigningError::NoSoaFound)?; + let ZoneRecordData::Soa(ref soa_data) = soa.data() else { + return Err(SigningError::NoSoaFound); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + let ttl = min(soa_data.minimum(), soa.ttl()); + + let families = RecordsIter::new(in_out.as_slice()); + + match &mut signing_config.hashing { + HashingConfig::Prehashed => { + // Nothing to do. + } - /// Whether this is a zone signing key. - /// - /// From [RFC 4034, section 2.1.1]: - /// - /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value - /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's - /// > owner name MUST be the name of a zone. If bit 7 has value 0, then - /// > the DNSKEY record holds some other type of DNS public key and MUST - /// > NOT be used to verify RRSIGs that cover RRsets. - /// - /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 - pub fn is_zone_signing_key(&self) -> bool { - self.flags & (1 << 8) != 0 - } + HashingConfig::Nsec => { + let nsecs = generate_nsecs( + apex, + ttl, + families, + signing_config.add_used_dnskeys, + ); - /// Whether this key has been revoked. - /// - /// From [RFC 5011, section 3]: - /// - /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. - /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) - /// > signed by the associated key, then the resolver MUST consider this - /// > key permanently invalid for all purposes except for validating the - /// > revocation. - /// - /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 - pub fn is_revoked(&self) -> bool { - self.flags & (1 << 7) != 0 - } + in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); + } - /// Whether this is a secure entry point. - /// - /// From [RFC 4034, section 2.1.1]: - /// - /// > Bit 15 of the Flags field is the Secure Entry Point flag, described - /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a - /// > key intended for use as a secure entry point. This flag is only - /// > intended to be a hint to zone signing or debugging software as to - /// > the intended use of this DNSKEY record; validators MUST NOT alter - /// > their behavior during the signature validation process in any way - /// > based on the setting of this bit. This also means that a DNSKEY RR - /// > with the SEP bit set would also need the Zone Key flag set in order - /// > to be able to generate signatures legally. A DNSKEY RR with the SEP - /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs - /// > that cover RRsets. - /// - /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 - /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 - pub fn is_secure_entry_point(&self) -> bool { - self.flags & 1 != 0 - } + HashingConfig::Nsec3( + Nsec3Config { + params, + opt_out, + ttl_mode, + hash_provider, + .. + }, + extra, + ) if extra.is_empty() => { + // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash + // order." We store the NSEC3s as we create them and sort them + // afterwards. + let Nsec3Records { recs, mut param } = + generate_nsec3s::( + apex, + ttl, + families, + params.clone(), + *opt_out, + signing_config.add_used_dnskeys, + hash_provider, + ) + .map_err(SigningError::Nsec3HashingError)?; + + let ttl = match ttl_mode { + Nsec3ParamTtlMode::Fixed(ttl) => *ttl, + Nsec3ParamTtlMode::Soa => soa.ttl(), + Nsec3ParamTtlMode::SoaMinimum => { + if let ZoneRecordData::Soa(soa_data) = soa.data() { + soa_data.minimum() + } else { + // Errm, this is unexpected. TODO: Should we abort + // with an error here about a malformed zonefile? + soa.ttl() + } + } + }; + + param.set_ttl(ttl); + + // Add the generated NSEC3 records. + in_out.sorted_extend( + std::iter::once(Record::from_record(param)) + .chain(recs.into_iter().map(Record::from_record)), + ); + } - /// The signing algorithm used. - pub fn algorithm(&self) -> SecAlg { - self.inner.algorithm() - } + HashingConfig::Nsec3(_nsec3_config, _extra) => { + todo!(); + } - /// The associated public key. - pub fn public_key(&self) -> Key<&Octs> - where - Octs: AsRef<[u8]>, - { - let owner = Name::from_octets(self.owner.as_octets()).unwrap(); - Key::new(owner, self.flags, self.inner.raw_public_key()) - } + HashingConfig::TransitioningNsecToNsec3( + _nsec3_config, + _nsec_to_nsec3_transition_state, + ) => { + todo!(); + } - /// The associated raw public key. - pub fn raw_public_key(&self) -> PublicKeyBytes { - self.inner.raw_public_key() + HashingConfig::TransitioningNsec3ToNsec( + _nsec3_config, + _nsec3_to_nsec_transition_state, + ) => { + todo!(); + } } -} -// TODO: Conversion to and from key files - -//----------- SignRaw -------------------------------------------------------- - -/// Low-level signing functionality. -/// -/// Types that implement this trait own a private key and can sign arbitrary -/// information (in the form of slices of bytes). -/// -/// 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 { - /// The signature algorithm used. - /// - /// See [RFC 8624, section 3.1] for IETF implementation recommendations. - /// - /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 - fn algorithm(&self) -> SecAlg; - - /// The raw public key. - /// - /// This can be used to verify produced signatures. It must use the same - /// algorithm as returned by [`algorithm()`]. - /// - /// [`algorithm()`]: Self::algorithm() - fn raw_public_key(&self) -> PublicKeyBytes; - - /// Sign the given bytes. - /// - /// # Errors - /// - /// See [`SignError`] for a discussion of possible failure cases. To the - /// greatest extent possible, the implementation should check for failure - /// cases beforehand and prevent them (e.g. when the keypair is created). - fn sign_raw(&self, data: &[u8]) -> Result; -} + if !signing_keys.is_empty() { + let families = RecordsIter::new(in_out.as_slice()); -//----------- GenerateParams ------------------------------------------------- - -/// Parameters for generating a secret key. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum GenerateParams { - /// Generate an RSA/SHA-256 keypair. - RsaSha256 { - /// The number of bits in the public modulus. - /// - /// 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, - }, - - /// Generate an ECDSA P-256/SHA-256 keypair. - EcdsaP256Sha256, - - /// Generate an ECDSA P-384/SHA-384 keypair. - EcdsaP384Sha384, - - /// Generate an Ed25519 keypair. - Ed25519, - - /// An Ed448 keypair. - Ed448, -} + let rrsigs_and_dnskeys = + generate_rrsigs::( + apex, + families, + signing_keys, + signing_config.add_used_dnskeys, + )?; -//--- Inspection - -impl GenerateParams { - /// The algorithm of the generated key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256 { .. } => SecAlg::RSASHA256, - Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, - Self::Ed25519 => SecAlg::ED25519, - Self::Ed448 => SecAlg::ED448, - } + in_out.sorted_extend(rrsigs_and_dnskeys); } -} -//============ Error Types =================================================== - -//----------- SignError ------------------------------------------------------ - -/// A signature failure. -/// -/// In case such an error occurs, callers should stop using the key pair they -/// attempted to sign with. If such an error occurs with every key pair they -/// have available, or if such an error occurs with a freshly-generated key -/// pair, they should use a different cryptographic implementation. If that -/// is not possible, they must forego signing entirely. -/// -/// # Failure Cases -/// -/// Signing should be an infallible process. There are three considerable -/// failure cases for it: -/// -/// - The secret key was invalid (e.g. its parameters were inconsistent). -/// -/// Such a failure would mean that all future signing (with this key) will -/// also fail. In any case, the implementations provided by this crate try -/// to verify the key (e.g. by checking the consistency of the private and -/// public components) before any signing occurs, largely ruling this class -/// of errors out. -/// -/// - Not enough randomness could be obtained. This applies to signature -/// algorithms which use randomization (e.g. RSA and ECDSA). -/// -/// On the vast majority of platforms, randomness can always be obtained. -/// The [`getrandom` crate documentation][getrandom] notes: -/// -/// > If an error does occur, then it is likely that it will occur on every -/// > call to getrandom, hence after the first successful call one can be -/// > reasonably confident that no errors will occur. -/// -/// [getrandom]: https://docs.rs/getrandom -/// -/// Thus, in case such a failure occurs, all future signing will probably -/// also fail. -/// -/// - Not enough memory could be allocated. -/// -/// Signature algorithms have a small memory overhead, so an out-of-memory -/// condition means that the program is nearly out of allocatable space. -/// -/// Callers who do not expect allocations to fail (i.e. who are using the -/// standard memory allocation routines, not their `try_` variants) will -/// likely panic shortly after such an error. -/// -/// Callers who are aware of their memory usage will likely restrict it far -/// before they get to this point. Systems running at near-maximum load -/// tend to quickly become unresponsive and staggeringly slow. If memory -/// usage is an important consideration, programs will likely cap it before -/// the system reaches e.g. 90% memory use. -/// -/// As such, memory allocation failure should never really occur. It is far -/// more likely that one of the other errors has occurred. -/// -/// It may be reasonable to panic in any such situation, since each kind of -/// error is essentially unrecoverable. However, applications where signing -/// is an optional step, or where crashing is prohibited, may wish to recover -/// from such an error differently (e.g. by foregoing signatures or informing -/// an operator). -#[derive(Clone, Debug)] -pub struct SignError; - -impl fmt::Display for SignError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("could not create a cryptographic signature") - } + Ok(()) } - -impl std::error::Error for SignError {} diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index 938c1a6bb..d5cdc4aa5 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -22,12 +22,12 @@ use crate::rdata::dnssec::ProtoRrsig; use crate::rdata::{Dnskey, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; +use crate::sign::keys::signingkey::SigningKey; use crate::sign::records::{ FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::signing::strategy::SigningKeyUsageStrategy; -use crate::sign::signing::traits::SortedExtend; -use crate::sign::{SignRaw, SigningKey}; +use crate::sign::signing::traits::{SignRaw, SortedExtend}; /// Generate RRSIG RRs for a collection of unsigned zone records. /// @@ -344,7 +344,7 @@ where { let (inception, expiration) = key .signature_validity_period() - .ok_or(SigningError::KeyLacksSignatureValidityPeriod)? + .ok_or(SigningError::NoSignatureValidityPeriodProvided)? .into_inner(); // RFC 4034 // 3. The RRSIG Resource Record @@ -377,7 +377,7 @@ where for record in rrset.iter() { record.compose_canonical(buf).unwrap(); } - let signature = key.raw_secret_key().sign_raw(&*buf).unwrap(); + let signature = key.raw_secret_key().sign_raw(&*buf)?; let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { return Err(SigningError::OutOfMemory); diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 50e203b70..dc802a519 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -1,8 +1,7 @@ -use core::cmp::min; use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; -use core::marker::{PhantomData, Send}; +use core::marker::Send; use core::ops::Deref; use std::boxed::Box; @@ -12,27 +11,63 @@ use std::vec::Vec; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::OctetsFrom; -use super::config::SigningConfig; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Rtype, SecAlg}; use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; use crate::rdata::ZoneRecordData; -use crate::sign::error::SigningError; -use crate::sign::hashing::config::HashingConfig; -use crate::sign::hashing::nsec::generate_nsecs; -use crate::sign::hashing::nsec3::{ - generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, - Nsec3Records, -}; +use crate::sign::error::{SignError, SigningError}; +use crate::sign::hashing::nsec3::Nsec3HashProvider; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ DefaultSorter, FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, }; +use crate::sign::sign_zone; +use crate::sign::signing::config::SigningConfig; use crate::sign::signing::rrsigs::generate_rrsigs; use crate::sign::signing::strategy::SigningKeyUsageStrategy; -use crate::sign::SignRaw; +use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; + +//----------- SignRaw -------------------------------------------------------- + +/// Low-level signing functionality. +/// +/// Types that implement this trait own a private key and can sign arbitrary +/// information (in the form of slices of bytes). +/// +/// 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 { + /// The signature algorithm used. + /// + /// See [RFC 8624, section 3.1] for IETF implementation recommendations. + /// + /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 + fn algorithm(&self) -> SecAlg; + + /// The raw public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn raw_public_key(&self) -> PublicKeyBytes; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// See [`SignError`] for a discussion of possible failure cases. To the + /// greatest extent possible, the implementation should check for failure + /// cases beforehand and prevent them (e.g. when the keypair is created). + fn sign_raw(&self, data: &[u8]) -> Result; +} //------------ SortedExtend -------------------------------------------------- @@ -96,247 +131,6 @@ where } } -//------------ SignableZoneInOut --------------------------------------------- - -enum SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> -where - N: Clone + ToName + From> + Ord + Hash, - Octs: Clone - + FromBuilder - + From<&'static [u8]> - + Send - + OctetsFrom> - + From> - + Default, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, - Sort: Sorter, - T: SortedExtend + ?Sized, -{ - SignInPlace(&'a mut T, PhantomData<(N, Octs, Sort)>), - SignInto(&'a S, &'b mut T), -} - -impl<'a, 'b, N, Octs, S, T, Sort> - SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> -where - N: Clone + ToName + From> + Ord + Hash, - Octs: Clone - + FromBuilder - + From<&'static [u8]> - + Send - + OctetsFrom> - + From> - + Default, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, - Sort: Sorter, - T: Deref>]> - + SortedExtend - + ?Sized, -{ - fn new_in_place(signable_zone: &'a mut T) -> Self { - Self::SignInPlace(signable_zone, Default::default()) - } - - fn new_into(signable_zone: &'a S, out: &'b mut T) -> Self { - Self::SignInto(signable_zone, out) - } -} - -impl SignableZoneInOut<'_, '_, N, Octs, S, T, Sort> -where - N: Clone + ToName + From> + Ord + Hash, - Octs: Clone - + FromBuilder - + From<&'static [u8]> - + Send - + OctetsFrom> - + From> - + Default, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, - Sort: Sorter, - T: Deref>]> - + SortedExtend - + ?Sized, -{ - fn as_slice(&self) -> &[Record>] { - match self { - SignableZoneInOut::SignInPlace(input_output, _) => input_output, - - SignableZoneInOut::SignInto(input, _) => input, - } - } - - fn sorted_extend< - U: IntoIterator>>, - >( - &mut self, - iter: U, - ) { - match self { - SignableZoneInOut::SignInPlace(input_output, _) => { - input_output.sorted_extend(iter) - } - SignableZoneInOut::SignInto(_, output) => { - output.sorted_extend(iter) - } - } - } -} - -//------------ sign_zone() --------------------------------------------------- - -fn sign_zone( - mut in_out: SignableZoneInOut, - apex: &FamilyName, - signing_config: &mut SigningConfig, - signing_keys: &[&dyn DesignatedSigningKey], -) -> Result<(), SigningError> -where - HP: Nsec3HashProvider, - Key: SignRaw, - N: Display - + Send - + CanonicalOrd - + Clone - + ToName - + From> - + Ord - + Hash, - ::Builder: - Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, - S: SignableZone, - Sort: Sorter, - T: SortedExtend + ?Sized, - Octs: FromBuilder - + Clone - + From<&'static [u8]> - + Send - + OctetsFrom> - + From> - + Default, - T: Deref>]>, -{ - let soa = in_out - .as_slice() - .iter() - .find(|r| r.rtype() == Rtype::SOA) - .ok_or(SigningError::NoSoaFound)?; - let ZoneRecordData::Soa(ref soa_data) = soa.data() else { - return Err(SigningError::NoSoaFound); - }; - - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // the MINIMUM field of the SOA record and the TTL of the SOA itself". - let ttl = min(soa_data.minimum(), soa.ttl()); - - let families = RecordsIter::new(in_out.as_slice()); - - match &mut signing_config.hashing { - HashingConfig::Prehashed => { - // Nothing to do. - } - - HashingConfig::Nsec => { - let nsecs = generate_nsecs( - apex, - ttl, - families, - signing_config.add_used_dnskeys, - ); - - in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); - } - - HashingConfig::Nsec3( - Nsec3Config { - params, - opt_out, - ttl_mode, - hash_provider, - .. - }, - extra, - ) if extra.is_empty() => { - // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash - // order." We store the NSEC3s as we create them and sort them - // afterwards. - let Nsec3Records { recs, mut param } = - generate_nsec3s::( - apex, - ttl, - families, - params.clone(), - *opt_out, - signing_config.add_used_dnskeys, - hash_provider, - ) - .map_err(SigningError::Nsec3HashingError)?; - - let ttl = match ttl_mode { - Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::Soa => soa.ttl(), - Nsec3ParamTtlMode::SoaMinimum => { - if let ZoneRecordData::Soa(soa_data) = soa.data() { - soa_data.minimum() - } else { - // Errm, this is unexpected. TODO: Should we abort - // with an error here about a malformed zonefile? - soa.ttl() - } - } - }; - - param.set_ttl(ttl); - - // Add the generated NSEC3 records. - in_out.sorted_extend( - std::iter::once(Record::from_record(param)) - .chain(recs.into_iter().map(Record::from_record)), - ); - } - - HashingConfig::Nsec3(_nsec3_config, _extra) => { - todo!(); - } - - HashingConfig::TransitioningNsecToNsec3( - _nsec3_config, - _nsec_to_nsec3_transition_state, - ) => { - todo!(); - } - - HashingConfig::TransitioningNsec3ToNsec( - _nsec3_config, - _nsec3_to_nsec_transition_state, - ) => { - todo!(); - } - } - - if !signing_keys.is_empty() { - let families = RecordsIter::new(in_out.as_slice()); - - let rrsigs_and_dnskeys = - generate_rrsigs::( - apex, - families, - signing_keys, - signing_config.add_used_dnskeys, - )?; - - in_out.sorted_extend(rrsigs_and_dnskeys); - } - - Ok(()) -} - //------------ SignableZone -------------------------------------------------- pub trait SignableZone: From 174e694e3aa340e7868d1e240a2fc2db89700a7e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:13:31 +0100 Subject: [PATCH 300/569] Fix doc tests. --- src/sign/mod.rs | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 3a035e0aa..e6d96ad26 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -50,6 +50,7 @@ //! ``` //! # use domain::base::iana::SecAlg; //! # use domain::{sign::*, validate}; +//! # use domain::sign::keys::signingkey::SigningKey; //! // Load an Ed25519 key named 'Ktest.+015+56037'. //! let base = "test-data/dnssec-keys/Ktest.+015+56037"; //! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); @@ -75,13 +76,15 @@ //! //! ``` //! # use domain::base::Name; -//! # use domain::sign::*; +//! # use domain::sign::keys::keypair; +//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::keys::signingkey::SigningKey; //! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; -//! let (sec_bytes, pub_bytes) = keys::keypair::generate(params).unwrap(); +//! let (sec_bytes, pub_bytes) = keypair::generate(params).unwrap(); //! //! // Parse the key into Ring or OpenSSL. -//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! //! // Associate the key with important metadata. //! let owner: Name> = "www.example.org.".parse().unwrap(); @@ -99,9 +102,12 @@ //! //! ``` //! # use domain::base::Name; -//! # use domain::sign::*; -//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # use domain::sign::keys::keypair; +//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::signing::traits::SignRaw; +//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let key = SigningKey::new(Name::>::root(), 257, key_pair); //! // Sign arbitrary byte sequences with the key. //! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); @@ -124,9 +130,11 @@ //! //! ``` //! # use domain::base::{*, iana::Class}; -//! # use domain::sign::*; -//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # use domain::sign::keys::keypair; +//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::keys::signingkey::SigningKey; +//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root.clone(), 257, key_pair); //! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; @@ -169,15 +177,18 @@ //! ``` //! # use domain::base::Name; //! # use domain::base::iana::Class; -//! # use domain::sign::*; +//! # use domain::sign::keys::keypair; +//! # use domain::sign::keys::keypair::GenerateParams; //! # use domain::sign::keys::keymeta::{DesignatedSigningKey, DnssecSigningKey}; -//! # let (sec_bytes, pub_bytes) = keys::keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # use domain::sign::records; +//! # use domain::sign::keys::signingkey::SigningKey; +//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); //! # let dnssec_signing_key = DnssecSigningKey::new_csk(key); //! # let keys = [&dnssec_signing_key as &dyn DesignatedSigningKey<_, _>]; -//! # let mut records = records::SortedRecords::<_, _, domain::sign::records::DefaultSorter>::new(); +//! # let mut records = records::SortedRecords::<_, _, records::DefaultSorter>::new(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; //! let apex = records::FamilyName::new(Name::>::root(), Class::IN); From 7c3c995ee9c03eeace7c23825f8ce56ceb6c792d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:33:51 +0100 Subject: [PATCH 301/569] Use the generic parameter name Inner everywhere for consistency. Replace &dyn DesignatedSigningKey with because the dyn doesn't gain anything as the underlying SigningKey takes a fixed Inner type. --- src/sign/mod.rs | 19 +++++++------- src/sign/signing/config.rs | 28 ++++++++++++--------- src/sign/signing/rrsigs.rs | 15 +++++------ src/sign/signing/strategy.rs | 12 ++++----- src/sign/signing/traits.rs | 48 ++++++++++++++++++++++-------------- 5 files changed, 68 insertions(+), 54 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e6d96ad26..4ae957912 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -162,8 +162,7 @@ //! //! // Assign signature validity period and operator intent to the keys. //! let key = key.with_validity(Timestamp::now(), Timestamp::now()); -//! let dnssec_signing_key = DnssecSigningKey::new_csk(key); -//! let keys = [&dnssec_signing_key as &dyn DesignatedSigningKey<_, _>]; +//! let keys = [DnssecSigningKey::new_csk(key)]; //! //! // Create a signing configuration. //! let mut signing_config = SigningConfig::default(); @@ -186,8 +185,7 @@ //! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); -//! # let dnssec_signing_key = DnssecSigningKey::new_csk(key); -//! # let keys = [&dnssec_signing_key as &dyn DesignatedSigningKey<_, _>]; +//! # let keys = [DnssecSigningKey::new_csk(key)]; //! # let mut records = records::SortedRecords::<_, _, records::DefaultSorter>::new(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; @@ -394,15 +392,16 @@ where //------------ sign_zone() --------------------------------------------------- -pub fn sign_zone( +pub fn sign_zone( mut in_out: SignableZoneInOut, apex: &FamilyName, - signing_config: &mut SigningConfig, - signing_keys: &[&dyn DesignatedSigningKey], + signing_config: &mut SigningConfig, + signing_keys: &[DSK], ) -> Result<(), SigningError> where + DSK: DesignatedSigningKey, HP: Nsec3HashProvider, - Key: SignRaw, + Inner: SignRaw, N: Display + Send + CanonicalOrd @@ -414,7 +413,7 @@ where ::Builder: Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, + KeyStrat: SigningKeyUsageStrategy, S: SignableZone, Sort: Sorter, T: SortedExtend + ?Sized, @@ -530,7 +529,7 @@ where let families = RecordsIter::new(in_out.as_slice()); let rrsigs_and_dnskeys = - generate_rrsigs::( + generate_rrsigs::( apex, families, signing_keys, diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs index e039a071f..2f8120162 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/signing/config.rs @@ -18,13 +18,17 @@ use super::strategy::DefaultSigningKeyUsageStrategy; /// Signing configuration for a DNSSEC signed zone. pub struct SigningConfig< N, - Octs: AsRef<[u8]> + From<&'static [u8]>, - Key: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - Sort: Sorter, + Octs, + Inner, + KeyStrat, + Sort, HP = OnDemandNsec3HashProvider, > where HP: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + Sort: Sorter, { /// Hashing configuration. pub hashing: HashingConfig, @@ -32,16 +36,16 @@ pub struct SigningConfig< /// Should keys used to sign the zone be added as DNSKEY RRs? pub add_used_dnskeys: bool, - _phantom: PhantomData<(Key, KeyStrat, Sort)>, + _phantom: PhantomData<(Inner, KeyStrat, Sort)>, } -impl - SigningConfig +impl + SigningConfig where HP: Nsec3HashProvider, Octs: AsRef<[u8]> + From<&'static [u8]>, - Key: SignRaw, - KeyStrat: SigningKeyUsageStrategy, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, { pub fn new( @@ -56,11 +60,11 @@ where } } -impl Default +impl Default for SigningConfig< N, Octs, - Key, + Inner, DefaultSigningKeyUsageStrategy, DefaultSorter, OnDemandNsec3HashProvider, @@ -69,7 +73,7 @@ where N: ToName + From>, Octs: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Key: SignRaw, + Inner: SignRaw, { fn default() -> Self { Self { diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index d5cdc4aa5..7880c5fd9 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -37,15 +37,16 @@ use crate::sign::signing::traits::{SignRaw, SortedExtend}; /// The given records MUST be sorted according to [`CanonicalOrd`]. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -pub fn generate_rrsigs( +pub fn generate_rrsigs( apex: &FamilyName, families: RecordsIter<'_, N, ZoneRecordData>, - keys: &[&dyn DesignatedSigningKey], + keys: &[DSK], add_used_dnskeys: bool, ) -> Result>>, SigningError> where - KeyPair: SignRaw, - KeyStrat: SigningKeyUsageStrategy, + DSK: DesignatedSigningKey, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, N: ToName + PartialEq + Clone @@ -119,7 +120,7 @@ where ); for idx in &keys_in_use_idxs { - let key = keys[**idx]; + let key = &keys[**idx]; let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); let is_non_dnskey_signing_key = non_dnskey_signing_key_idxs.contains(idx); @@ -244,7 +245,7 @@ where &non_dnskey_signing_key_idxs }; - for key in signing_key_idxs.iter().map(|&idx| keys[idx]) { + for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { // A copy of the family name. We’ll need it later. let name = apex_family.family_name().cloned(); @@ -305,7 +306,7 @@ where } for key in - non_dnskey_signing_key_idxs.iter().map(|&idx| keys[idx]) + non_dnskey_signing_key_idxs.iter().map(|&idx| &keys[idx]) { let rrsig_rr = sign_rrset(key, &rrset, &name, apex, &mut buf)?; diff --git a/src/sign/signing/strategy.rs b/src/sign/signing/strategy.rs index ce8229d56..f0abf034e 100644 --- a/src/sign/signing/strategy.rs +++ b/src/sign/signing/strategy.rs @@ -13,8 +13,8 @@ where { const NAME: &'static str; - fn select_signing_keys_for_rtype( - candidate_keys: &[&dyn DesignatedSigningKey], + fn select_signing_keys_for_rtype>( + candidate_keys: &[K], rtype: Option, ) -> HashSet { if matches!(rtype, Some(Rtype::DNSKEY)) { @@ -24,14 +24,14 @@ where } } - fn filter_keys( - candidate_keys: &[&dyn DesignatedSigningKey], - filter: fn(&dyn DesignatedSigningKey) -> bool, + fn filter_keys>( + candidate_keys: &[K], + filter: fn(&K) -> bool, ) -> HashSet { candidate_keys .iter() .enumerate() - .filter_map(|(i, &k)| filter(k).then_some(i)) + .filter_map(|(i, k)| filter(k).then_some(i)) .collect::>() } } diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index dc802a519..195284c54 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -152,26 +152,34 @@ where // TODO // fn iter_mut(&mut self) -> T; - fn sign_zone( + fn sign_zone( &self, - signing_config: &mut SigningConfig, - signing_keys: &[&dyn DesignatedSigningKey], + signing_config: &mut SigningConfig< + N, + Octs, + Inner, + KeyStrat, + Sort, + HP, + >, + signing_keys: &[DSK], out: &mut T, ) -> Result<(), SigningError> where + DSK: DesignatedSigningKey, HP: Nsec3HashProvider, - Key: SignRaw, + Inner: SignRaw, N: Display + Send + CanonicalOrd, ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, + KeyStrat: SigningKeyUsageStrategy, T: Deref>]> + SortedExtend + ?Sized, Self: Sized, { let in_out = SignableZoneInOut::new_into(self, out); - sign_zone::( + sign_zone::( in_out, &self.apex().ok_or(SigningError::NoSoaFound)?, signing_config, @@ -256,22 +264,23 @@ where Self: SortedExtend + Sized, S: Sorter, { - fn sign_zone( + fn sign_zone( &mut self, - signing_config: &mut SigningConfig, - signing_keys: &[&dyn DesignatedSigningKey], + signing_config: &mut SigningConfig, + signing_keys: &[DSK], ) -> Result<(), SigningError> where + DSK: DesignatedSigningKey, HP: Nsec3HashProvider, - Key: SignRaw, + Inner: SignRaw, N: Display + Send + CanonicalOrd, ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, + KeyStrat: SigningKeyUsageStrategy, { let apex = self.apex().ok_or(SigningError::NoSoaFound)?; let in_out = SignableZoneInOut::new_in_place(self); - sign_zone::( + sign_zone::( in_out, &apex, signing_config, @@ -307,7 +316,7 @@ where //------------ Signable ------------------------------------------------------ -pub trait Signable +pub trait Signable where N: ToName + CanonicalOrd @@ -316,7 +325,7 @@ where + Clone + PartialEq + From>, - KeyPair: SignRaw, + Inner: SignRaw, Octs: From> + From<&'static [u8]> + FromBuilder @@ -332,12 +341,13 @@ where fn sign( &self, apex: &FamilyName, - keys: &[&dyn DesignatedSigningKey], + keys: &[DSK], ) -> Result>>, SigningError> where - KeyStrat: SigningKeyUsageStrategy, + DSK: DesignatedSigningKey, + KeyStrat: SigningKeyUsageStrategy, { - generate_rrsigs::<_, _, _, KeyStrat, Sort>( + generate_rrsigs::<_, _, DSK, _, KeyStrat, Sort>( apex, self.families(), keys, @@ -348,10 +358,10 @@ where //--- impl Signable for Rrset -impl Signable +impl Signable for Rrset<'_, N, ZoneRecordData> where - KeyPair: SignRaw, + Inner: SignRaw, N: From> + PartialEq + Clone From af545ff05363b80acdd606b720f008df8f25c3bb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:37:53 +0100 Subject: [PATCH 302/569] Consistency. --- src/sign/signing/strategy.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sign/signing/strategy.rs b/src/sign/signing/strategy.rs index f0abf034e..a6d75f17c 100644 --- a/src/sign/signing/strategy.rs +++ b/src/sign/signing/strategy.rs @@ -13,8 +13,8 @@ where { const NAME: &'static str; - fn select_signing_keys_for_rtype>( - candidate_keys: &[K], + fn select_signing_keys_for_rtype>( + candidate_keys: &[DSK], rtype: Option, ) -> HashSet { if matches!(rtype, Some(Rtype::DNSKEY)) { @@ -24,9 +24,9 @@ where } } - fn filter_keys>( - candidate_keys: &[K], - filter: fn(&K) -> bool, + fn filter_keys>( + candidate_keys: &[DSK], + filter: fn(&DSK) -> bool, ) -> HashSet { candidate_keys .iter() From d5c31d7055d6e981cc7e218deb3c7398ad19712e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 12:39:50 +0100 Subject: [PATCH 303/569] Cargo fmt. --- src/sign/signing/strategy.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sign/signing/strategy.rs b/src/sign/signing/strategy.rs index a6d75f17c..861d22674 100644 --- a/src/sign/signing/strategy.rs +++ b/src/sign/signing/strategy.rs @@ -13,7 +13,9 @@ where { const NAME: &'static str; - fn select_signing_keys_for_rtype>( + fn select_signing_keys_for_rtype< + DSK: DesignatedSigningKey, + >( candidate_keys: &[DSK], rtype: Option, ) -> HashSet { From 681456aa767a14b7b3c93b498cf1e8e25e9ffb67 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:23:24 +0100 Subject: [PATCH 304/569] Remove FamilyName, rename Family to OwnerRrs, and remove class checks, because all RRs in a zone per RFC 1035 should have the same class. --- src/sign/hashing/nsec.rs | 51 ++++++++++-------- src/sign/hashing/nsec3.rs | 67 +++++++++++------------ src/sign/mod.rs | 4 +- src/sign/records.rs | 107 ++++++++----------------------------- src/sign/signing/rrsigs.rs | 79 ++++++++++++++------------- src/sign/signing/traits.rs | 22 ++++---- 6 files changed, 134 insertions(+), 196 deletions(-) diff --git a/src/sign/hashing/nsec.rs b/src/sign/hashing/nsec.rs index c1ecf14ca..73aabb154 100644 --- a/src/sign/hashing/nsec.rs +++ b/src/sign/hashing/nsec.rs @@ -10,13 +10,13 @@ use crate::base::record::Record; use crate::base::Ttl; use crate::rdata::dnssec::RtypeBitmap; use crate::rdata::{Nsec, ZoneRecordData}; -use crate::sign::records::{FamilyName, RecordsIter}; +use crate::sign::records::RecordsIter; // TODO: Add (mutable?) iterator based variant. pub fn generate_nsecs( - apex: &FamilyName, + expected_apex: &N, ttl: Ttl, - mut families: RecordsIter<'_, N, ZoneRecordData>, + mut records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, ) -> Vec>> where @@ -28,51 +28,53 @@ where let mut res = Vec::new(); // The owner name of a zone cut if we currently are at or below one. - let mut cut: Option> = None; + let mut cut: Option = None; // Since the records are ordered, the first family is the apex -- we can // skip everything before that. - families.skip_before(apex); + records.skip_before(expected_apex); // Because of the next name thing, we need to keep the last NSEC around. - let mut prev: Option<(FamilyName, RtypeBitmap)> = None; + let mut prev: Option<(N, RtypeBitmap)> = None; // We also need the apex for the last NSEC. - let apex_owner = families.first_owner().clone(); + let first_rr = records.first(); + let apex_owner = first_rr.owner().clone(); + let zone_class = first_rr.class(); - for family in families { + for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !family.is_in_zone(apex) { + if !owner_rrs.is_in_zone(expected_apex) { break; } // If the family is below a zone cut, we must ignore it. if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { + if owner_rrs.owner().ends_with(cut) { continue; } } // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); + let name = owner_rrs.owner().clone(); // If this family is the parent side of a zone cut, we keep the family // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { + cut = if owner_rrs.is_zone_cut(expected_apex) { Some(name.clone()) } else { None }; if let Some((prev_name, bitmap)) = prev.take() { - res.push( - prev_name.into_record( - ttl, - Nsec::new(name.owner().clone(), bitmap), - ), - ); + res.push(Record::new( + prev_name.clone(), + zone_class, + ttl, + Nsec::new(name.clone(), bitmap), + )); } let mut bitmap = RtypeBitmap::::builder(); @@ -81,12 +83,12 @@ where // MUST indicate the presence of both the NSEC record itself and // its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); - if assume_dnskeys_will_be_added && family.owner() == &apex_owner { + if assume_dnskeys_will_be_added && owner_rrs.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); } bitmap.add(Rtype::NSEC).unwrap(); - for rrset in family.rrsets() { + for rrset in owner_rrs.rrsets() { // RFC 4034 section 4.1.2: (and also RFC 4035 section 2.3) // "The bitmap for the NSEC RR at a delegation point requires // special attention. Bits corresponding to the delegation NS @@ -110,8 +112,15 @@ where prev = Some((name, bitmap.finalize())); } + if let Some((prev_name, bitmap)) = prev { - res.push(prev_name.into_record(ttl, Nsec::new(apex_owner, bitmap))); + res.push(Record::new( + prev_name.clone(), + zone_class, + ttl, + Nsec::new(apex_owner.clone(), bitmap), + )); } + res } diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 50d4adeee..b996bc5c4 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -16,7 +16,7 @@ use crate::base::{Name, NameBuilder, Record, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; -use crate::sign::records::{FamilyName, RecordsIter, SortedRecords, Sorter}; +use crate::sign::records::{RecordsIter, SortedRecords, Sorter}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; @@ -36,9 +36,9 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. pub fn generate_nsec3s( - apex: &FamilyName, + expected_apex: &N, ttl: Ttl, - mut families: RecordsIter<'_, N, ZoneRecordData>, + mut records: RecordsIter<'_, N, ZoneRecordData>, params: Nsec3param, opt_out: Nsec3OptOut, assume_dnskeys_will_be_added: bool, @@ -73,55 +73,53 @@ where 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; + let mut cut: Option = None; - // Since the records are ordered, the first family is the apex -- we can + // Since the records are ordered, the first owner is the apex -- we can // skip everything before that. - families.skip_before(apex); + records.skip_before(expected_apex); // We also need the apex for the last NSEC. - let apex_owner = families.first_owner().clone(); + let first_rr = records.first(); + let apex_owner = first_rr.owner().clone(); let apex_label_count = apex_owner.iter_labels().count(); let mut last_nent_stack: Vec = vec![]; - for family in families { - trace!("Family: {}", family.family_name().owner()); + for owner_rrs in records { + trace!("Owner: {}", owner_rrs.owner()); // If the owner is out of zone, we have moved out of our zone and are // done. - if !family.is_in_zone(apex) { + if !owner_rrs.is_in_zone(expected_apex) { debug!( - "Stopping NSEC3 generation at out-of-zone family {}", - family.family_name().owner() + "Stopping NSEC3 generation at out-of-zone owner {}", + owner_rrs.owner() ); break; } - // If the family is below a zone cut, we must ignore it. As the RRs + // If the owner is below a zone cut, we must ignore it. As the RRs // are required to be sorted all RRs below a zone cut should be // encountered after the cut itself. if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { + if owner_rrs.owner().ends_with(cut) { debug!( - "Excluding family {} as it is below a zone cut", - family.family_name().owner() + "Excluding owner {} as it is below a zone cut", + owner_rrs.owner() ); continue; } } - // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); + // A copy of the owner name. We’ll need it later. + let name = owner_rrs.owner().clone(); // If this family is the parent side of a zone cut, we keep the family // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { - trace!( - "Zone cut detected at family {}", - family.family_name().owner() - ); + cut = if owner_rrs.is_zone_cut(expected_apex) { + trace!("Zone cut detected at owner {}", owner_rrs.owner()); Some(name.clone()) } else { None @@ -140,9 +138,9 @@ where // that lacks a DS RR. We determine whether or not a DS RR is present // even when Opt-Out is not being used because we also need to know // there at a later step. - let has_ds = family.records().any(|rec| rec.rtype() == Rtype::DS); + let has_ds = owner_rrs.records().any(|rec| rec.rtype() == Rtype::DS); if opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { - debug!("Excluding family {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",family.family_name().owner()); + debug!("Excluding owner {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",owner_rrs.owner()); continue; } @@ -154,20 +152,17 @@ where 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) { + if name.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_root = name.iter_labels().count(); let distance_to_apex = distance_to_root - apex_label_count; if distance_to_apex > last_nent_distance_to_apex { - trace!( - "Possible ENT detected at family {}", - family.family_name().owner() - ); + trace!("Possible ENT detected at family {}", owner_rrs.owner()); // Are there any empty nodes between this node and the apex? The // zone file records are already sorted so if all of the parent @@ -191,7 +186,7 @@ where // - a.b.c.mail.example.com 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); + let rev_label_it = name.iter_labels().skip(n); // Create next longest ENT name. let mut builder = NameBuilder::::new(); @@ -297,7 +292,7 @@ where // RRSets MUST have a corresponding NSEC3 RR. Owner names that // correspond to unsigned delegations MAY have a corresponding // NSEC3 RR." - for rrset in family.rrsets() { + for rrset in owner_rrs.rrsets() { if cut.is_none() || matches!(rrset.rtype(), Rtype::NS | Rtype::DS) { // RFC 5155 section 3.2: @@ -325,7 +320,7 @@ where } let rec: Record> = mk_nsec3( - name.owner(), + &name, hash_provider, params.hash_algorithm(), nsec3_flags, @@ -342,7 +337,7 @@ where if let Some(last_nent) = last_nent { last_nent_stack.push(last_nent); } - last_nent_stack.push(name.owner().clone()); + last_nent_stack.push(name.clone()); } for name in ents { @@ -396,7 +391,7 @@ where // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." let nsec3param = Record::new( - apex.owner().try_to_name::().unwrap().into(), + expected_apex.try_to_name::().unwrap().into(), Class::IN, ttl, params, diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 4ae957912..4a4e3a0a9 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -294,7 +294,7 @@ use keys::keymeta::DesignatedSigningKey; use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, }; -use records::{FamilyName, RecordsIter, Sorter}; +use records::{RecordsIter, Sorter}; use signing::config::SigningConfig; use signing::rrsigs::generate_rrsigs; use signing::strategy::SigningKeyUsageStrategy; @@ -394,7 +394,7 @@ where pub fn sign_zone( mut in_out: SignableZoneInOut, - apex: &FamilyName, + apex: &N, signing_config: &mut SigningConfig, signing_keys: &[DSK], ) -> Result<(), SigningError> diff --git a/src/sign/records.rs b/src/sign/records.rs index 3448c8743..1e0dc34da 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -364,17 +364,17 @@ where } } -//------------ Family -------------------------------------------------------- +//------------ OwnerRrs ------------------------------------------------------ -/// A set of records with the same owner name and class. +/// A set of records with the same owner name. #[derive(Clone)] -pub struct Family<'a, N, D> { +pub struct OwnerRrs<'a, N, D> { slice: &'a [Record], } -impl<'a, N, D> Family<'a, N, D> { +impl<'a, N, D> OwnerRrs<'a, N, D> { fn new(slice: &'a [Record]) -> Self { - Family { slice } + OwnerRrs { slice } } pub fn owner(&self) -> &N { @@ -385,10 +385,6 @@ impl<'a, N, D> Family<'a, N, D> { self.slice[0].class() } - pub fn family_name(&self) -> FamilyName<&N> { - FamilyName::new(self.owner(), self.class()) - } - pub fn rrsets(&self) -> FamilyIter<'a, N, D> { FamilyIter::new(self.slice) } @@ -397,72 +393,20 @@ impl<'a, N, D> Family<'a, N, D> { self.slice.iter() } - pub fn is_zone_cut(&self, apex: &FamilyName) -> bool + pub fn is_zone_cut(&self, apex: &N) -> bool where - N: ToName, - NN: ToName, + N: ToName + PartialEq, D: RecordData, { - self.family_name().ne(apex) + self.owner().ne(apex) && self.records().any(|record| record.rtype() == Rtype::NS) } - pub fn is_in_zone(&self, apex: &FamilyName) -> bool + pub fn is_in_zone(&self, apex: &N) -> bool where N: ToName, { - self.owner().ends_with(&apex.owner) && self.class() == apex.class - } -} - -//------------ FamilyName ---------------------------------------------------- - -/// The identifier for a family, i.e., a owner name and class. -#[derive(Clone)] -pub struct FamilyName { - owner: N, - class: Class, -} - -impl FamilyName { - pub fn new(owner: N, class: Class) -> Self { - FamilyName { owner, class } - } - - pub fn owner(&self) -> &N { - &self.owner - } - - pub fn class(&self) -> Class { - self.class - } - - pub fn into_record(self, ttl: Ttl, data: D) -> Record - where - N: Clone, - { - Record::new(self.owner.clone(), self.class, ttl, data) - } -} - -impl FamilyName<&N> { - pub fn cloned(&self) -> FamilyName { - FamilyName { - owner: (*self.owner).clone(), - class: self.class, - } - } -} - -impl PartialEq> for FamilyName { - fn eq(&self, other: &FamilyName) -> bool { - self.owner.name_eq(&other.owner) && self.class == other.class - } -} - -impl PartialEq> for FamilyName { - fn eq(&self, other: &Record) -> bool { - self.owner.name_eq(other.owner()) && self.class == other.class() + self.owner().ends_with(&apex) } } @@ -486,10 +430,6 @@ impl<'a, N, D> Rrset<'a, N, D> { self.slice[0].class() } - pub fn family_name(&self) -> FamilyName<&N> { - FamilyName::new(self.owner(), self.class()) - } - pub fn rtype(&self) -> Rtype where D: RecordData, @@ -520,7 +460,8 @@ impl<'a, N, D> Rrset<'a, N, D> { //------------ RecordsIter --------------------------------------------------- -/// An iterator that produces families from sorted records. +/// An iterator that produces groups of records belonging to the same owner +/// from sorted records. pub struct RecordsIter<'a, N, D> { slice: &'a [Record], } @@ -530,19 +471,16 @@ impl<'a, N, D> RecordsIter<'a, N, D> { RecordsIter { slice } } - pub fn first_owner(&self) -> &'a N { - self.slice[0].owner() + pub fn first(&self) -> &'a Record { + &self.slice[0] } - pub fn skip_before(&mut self, apex: &FamilyName) + pub fn skip_before(&mut self, apex: &N) where - N: ToName, + N: ToName + PartialEq, { - while let Some(first) = self.slice.first() { - if first.class() != apex.class() { - continue; - } - if apex == first || first.owner().ends_with(apex.owner()) { + while let Some(first) = self.slice.first().map(|r| r.owner()) { + if apex == first || first.ends_with(apex) { break; } self.slice = &self.slice[1..] @@ -555,7 +493,7 @@ where N: ToName + 'a, D: RecordData + 'a, { - type Item = Family<'a, N, D>; + type Item = OwnerRrs<'a, N, D>; fn next(&mut self) -> Option { let first = match self.slice.first() { @@ -564,16 +502,14 @@ where }; let mut end = 1; while let Some(record) = self.slice.get(end) { - if !record.owner().name_eq(first.owner()) - || record.class() != first.class() - { + if !record.owner().name_eq(first.owner()) { break; } end += 1; } let (res, slice) = self.slice.split_at(end); self.slice = slice; - Some(Family::new(res)) + Some(OwnerRrs::new(res)) } } @@ -606,7 +542,6 @@ where while let Some(record) = self.slice.get(end) { if !record.owner().name_eq(first.owner()) || record.rtype() != first.rtype() - || record.class() != first.class() { break; } diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index 7880c5fd9..df545e811 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -13,7 +13,7 @@ use octseq::{OctetsFrom, OctetsInto}; use tracing::{debug, enabled, Level}; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Class, Rtype}; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; @@ -23,9 +23,7 @@ use crate::rdata::{Dnskey, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::keys::signingkey::SigningKey; -use crate::sign::records::{ - FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, -}; +use crate::sign::records::{RecordsIter, Rrset, SortedRecords, Sorter}; use crate::sign::signing::strategy::SigningKeyUsageStrategy; use crate::sign::signing::traits::{SignRaw, SortedExtend}; @@ -38,8 +36,8 @@ use crate::sign::signing::traits::{SignRaw, SortedExtend}; // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_rrsigs( - apex: &FamilyName, - families: RecordsIter<'_, N, ZoneRecordData>, + expected_apex: &N, + records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], add_used_dnskeys: bool, ) -> Result>>, SigningError> @@ -140,34 +138,35 @@ where let mut res: Vec>> = Vec::new(); let mut buf = Vec::new(); - let mut cut: Option> = None; - let mut families = families.peekable(); + let mut cut: Option = None; + let mut records = records.peekable(); // Are we signing the entire tree from the apex down or just some child // records? Use the first found SOA RR as the apex. If no SOA RR can be // found assume that we are only signing records below the apex. - let soa_ttl = families.peek().and_then(|first_family| { - first_family.records().find_map(|rr| { - if rr.owner() == apex.owner() && rr.rtype() == Rtype::SOA { - Some(rr.ttl()) - } else { - None - } - }) - }); + let (soa_ttl, zone_class) = if let Some(rr) = + records.peek().and_then(|first_owner_rrs| { + first_owner_rrs.records().find(|rr| { + rr.owner() == expected_apex && rr.rtype() == Rtype::SOA + }) + }) { + (Some(rr.ttl()), rr.class()) + } else { + (None, Class::IN) + }; if let Some(soa_ttl) = soa_ttl { // Sign the apex // SAFETY: We just checked above if the apex records existed. - let apex_family = families.next().unwrap(); + let apex_owner_rrs = records.next().unwrap(); - let apex_rrsets = apex_family + let apex_rrsets = apex_owner_rrs .rrsets() .filter(|rrset| rrset.rtype() != Rtype::RRSIG); // Generate or extend the DNSKEY RRSET with the keys that we will sign // apex DNSKEY RRs and zone RRs with. - let apex_dnskey_rrset = apex_family + let apex_dnskey_rrset = apex_owner_rrs .rrsets() .find(|rrset| rrset.rtype() == Rtype::DNSKEY); @@ -204,8 +203,8 @@ where let dnskey = public_key.to_dnskey(); let signing_key_dnskey_rr = Record::new( - apex.owner().clone(), - apex.class(), + expected_apex.clone(), + zone_class, dnskey_rrset_ttl, Dnskey::convert(dnskey.clone()).into(), ); @@ -220,8 +219,8 @@ where // Add the DNSKEY RR to the set of new RRs to output for the // zone. res.push(Record::new( - apex.owner().clone(), - apex.class(), + expected_apex.clone(), + zone_class, dnskey_rrset_ttl, Dnskey::convert(dnskey).into(), )); @@ -247,10 +246,10 @@ where for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { // A copy of the family name. We’ll need it later. - let name = apex_family.family_name().cloned(); + let name = apex_owner_rrs.owner().clone(); let rrsig_rr = - sign_rrset(key, &rrset, &name, apex, &mut buf)?; + sign_rrset(key, &rrset, &name, expected_apex, &mut buf)?; res.push(rrsig_rr); debug!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", @@ -263,33 +262,33 @@ where } // For all RRSETs below the apex - for family in families { + for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !family.is_in_zone(apex) { + if !owner_rrs.is_in_zone(expected_apex) { break; } // If the family is below a zone cut, we must ignore it. if let Some(ref cut) = cut { - if family.owner().ends_with(cut.owner()) { + if owner_rrs.owner().ends_with(cut) { continue; } } // A copy of the family name. We’ll need it later. - let name = family.family_name().cloned(); + let name = owner_rrs.owner().clone(); // If this family is the parent side of a zone cut, we keep the family // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if family.is_zone_cut(apex) { + cut = if owner_rrs.is_zone_cut(expected_apex) { Some(name.clone()) } else { None }; - for rrset in family.rrsets() { + for rrset in owner_rrs.rrsets() { if cut.is_some() { // If we are at a zone cut, we only sign DS and NSEC records. // NS records we must not sign and everything else shouldn’t @@ -309,12 +308,12 @@ where non_dnskey_signing_key_idxs.iter().map(|&idx| &keys[idx]) { let rrsig_rr = - sign_rrset(key, &rrset, &name, apex, &mut buf)?; + sign_rrset(key, &rrset, &name, expected_apex, &mut buf)?; res.push(rrsig_rr); debug!( "Signed {} RRSET at {} with keytag {}", rrset.rtype(), - rrset.family_name().owner(), + rrset.owner(), key.public_key().key_tag() ); } @@ -329,8 +328,8 @@ where pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, - name: &FamilyName, - apex: &FamilyName, + rrset_owner: &N, + apex_owner: &N, buf: &mut Vec, ) -> Result>, SigningError> where @@ -357,7 +356,7 @@ where let rrsig = ProtoRrsig::new( rrset.rtype(), key.algorithm(), - name.owner().rrsig_label_count(), + rrset_owner.rrsig_label_count(), rrset.ttl(), expiration, inception, @@ -371,7 +370,7 @@ where // We don't need to make sure here that the signer name is in // canonical form as required by RFC 4034 as the call to // `compose_canonical()` below will take care of that. - apex.owner().clone(), + apex_owner.clone(), ); buf.clear(); rrsig.compose_canonical(buf).unwrap(); @@ -385,8 +384,8 @@ where }; let rrsig = rrsig.into_rrsig(signature).expect("long signature"); Ok(Record::new( - name.owner().clone(), - name.class(), + rrset_owner.clone(), + rrset.class(), rrset.ttl(), ZoneRecordData::Rrsig(rrsig), )) diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 195284c54..56cad5711 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -21,7 +21,7 @@ use crate::sign::error::{SignError, SigningError}; use crate::sign::hashing::nsec3::Nsec3HashProvider; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ - DefaultSorter, FamilyName, RecordsIter, Rrset, SortedRecords, Sorter, + DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::sign_zone; use crate::sign::signing::config::SigningConfig; @@ -147,7 +147,7 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, { - fn apex(&self) -> Option>; + fn apex(&self) -> Option; // TODO // fn iter_mut(&mut self) -> T; @@ -211,8 +211,8 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, { - fn apex(&self) -> Option> { - self.find_soa().map(|soa| soa.family_name().cloned()) + fn apex(&self) -> Option { + self.find_soa().map(|soa| soa.owner().clone()) } } @@ -240,10 +240,10 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, { - fn apex(&self) -> Option> { + fn apex(&self) -> Option { self.iter() .find(|r| r.rtype() == Rtype::SOA) - .map(|r| FamilyName::new(r.owner().clone(), r.class())) + .map(|r| r.owner().clone()) } } @@ -335,12 +335,12 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, { - fn families(&self) -> RecordsIter<'_, N, ZoneRecordData>; + fn owner_rrs(&self) -> RecordsIter<'_, N, ZoneRecordData>; #[allow(clippy::type_complexity)] fn sign( &self, - apex: &FamilyName, + expected_apex: &N, keys: &[DSK], ) -> Result>>, SigningError> where @@ -348,8 +348,8 @@ where KeyStrat: SigningKeyUsageStrategy, { generate_rrsigs::<_, _, DSK, _, KeyStrat, Sort>( - apex, - self.families(), + expected_apex, + self.owner_rrs(), keys, false, ) @@ -377,7 +377,7 @@ where + From>, ::Builder: AsRef<[u8]> + AsMut<[u8]> + EmptyBuilder, { - fn families(&self) -> RecordsIter<'_, N, ZoneRecordData> { + fn owner_rrs(&self) -> RecordsIter<'_, N, ZoneRecordData> { RecordsIter::new(self.as_slice()) } } From b1f7a20df54fe1f2f3c33cc9c926adc033dd30a9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:34:48 +0100 Subject: [PATCH 305/569] As zone signing assumes, but does not check, that the zone is ordered, add a check in debug builds (not in release builds as it is too costly) if the zone is correctly sorted before signing. --- src/sign/mod.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 4a4e3a0a9..5173d4a84 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -392,6 +392,15 @@ where //------------ sign_zone() --------------------------------------------------- +/// DNSSEC sign the given zone records. +/// +/// Assumes that the given zone records are sorted according to +/// [`CanonicalOrd`]. The behaviour is undefined otherwise. +/// +/// # Panics +/// +/// This function will panic in debug builds if the given zone is not sorted +/// according to [`CanonicalOrd`]. pub fn sign_zone( mut in_out: SignableZoneInOut, apex: &N, @@ -435,6 +444,8 @@ where return Err(SigningError::NoSoaFound); }; + debug_assert!(in_out.as_slice().is_sorted_by(CanonicalOrd::canonical_le)); + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of // the MINIMUM field of the SOA record and the TTL of the SOA itself". From 1056703bd4b622d6376ac5b52c95dd235c939aee Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:37:32 +0100 Subject: [PATCH 306/569] Revert "As zone signing assumes, but does not check, that the zone is ordered, add a check in debug builds (not in release builds as it is too costly) if the zone is correctly sorted before signing." This reverts commit b1f7a20df54fe1f2f3c33cc9c926adc033dd30a9. --- src/sign/mod.rs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 5173d4a84..4a4e3a0a9 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -392,15 +392,6 @@ where //------------ sign_zone() --------------------------------------------------- -/// DNSSEC sign the given zone records. -/// -/// Assumes that the given zone records are sorted according to -/// [`CanonicalOrd`]. The behaviour is undefined otherwise. -/// -/// # Panics -/// -/// This function will panic in debug builds if the given zone is not sorted -/// according to [`CanonicalOrd`]. pub fn sign_zone( mut in_out: SignableZoneInOut, apex: &N, @@ -444,8 +435,6 @@ where return Err(SigningError::NoSoaFound); }; - debug_assert!(in_out.as_slice().is_sorted_by(CanonicalOrd::canonical_le)); - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of // the MINIMUM field of the SOA record and the TTL of the SOA itself". From f563f32cae288f3d8ba1edb812ef9d00612266f2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 13:40:25 +0100 Subject: [PATCH 307/569] Fix doc test. --- src/sign/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 4a4e3a0a9..fa7be3194 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -189,7 +189,7 @@ //! # let mut records = records::SortedRecords::<_, _, records::DefaultSorter>::new(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; -//! let apex = records::FamilyName::new(Name::>::root(), Class::IN); +//! let apex = Name::>::root(); //! let rrset = records::Rrset::new(&records); //! let generated_records = rrset.sign::(&apex, &keys).unwrap(); //! ``` From 5549ba7b6cc1203f14fb0d8518c9e9cc35b45f6b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 20:55:35 +0100 Subject: [PATCH 308/569] Pass an is_ent flag to the Nsec3Provider to allow it to be recorded for diagnostics such as those produced by ldns-signzone. --- src/sign/hashing/nsec3.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index b996bc5c4..8d5c423c5 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -329,6 +329,7 @@ where &apex_owner, bitmap, ttl, + false, )?; // Store the record by order of its owner name. @@ -355,6 +356,7 @@ where &apex_owner, bitmap, ttl, + true, )?; // Store the record by order of its owner name. @@ -406,6 +408,12 @@ where Ok(Nsec3Records::new(nsec3s.into_inner(), nsec3param)) } +// unhashed_owner_name_is_ent is used to signal that the unhashed owner name +// is an empty non-terminal, as ldns-signzone for example outputs a comment +// for NSEC3 hashes that are for unhashed empty non-terminal owner names, and +// it can be quite costly to determine later given only a collection of +// records if the unhashed owner name is an ENT or not, so we pass this flag +// to the hash provider and it can record it if wanted. #[allow(clippy::too_many_arguments)] fn mk_nsec3( name: &N, @@ -417,6 +425,7 @@ fn mk_nsec3( apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, + unhashed_owner_name_is_ent: bool, ) -> Result>, Nsec3HashError> where N: ToName + From>, @@ -425,7 +434,7 @@ where EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, HashProvider: Nsec3HashProvider, { - let owner_name = hash_provider.get_or_create(apex_owner, name)?; + let owner_name = hash_provider.get_or_create(apex_owner, name, unhashed_owner_name_is_ent)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -517,6 +526,7 @@ pub trait Nsec3HashProvider { &mut self, apex_owner: &N, unhashed_owner_name: &N, + unhashed_owner_name_is_ent: bool, ) -> Result; } @@ -524,7 +534,6 @@ pub struct OnDemandNsec3HashProvider { alg: Nsec3HashAlg, iterations: u16, salt: Nsec3Salt, - // apex_owner: N, } impl OnDemandNsec3HashProvider { @@ -532,13 +541,11 @@ impl OnDemandNsec3HashProvider { alg: Nsec3HashAlg, iterations: u16, salt: Nsec3Salt, - // apex_owner: N, ) -> Self { Self { alg, iterations, salt, - // apex_owner, } } @@ -567,6 +574,7 @@ where &mut self, apex_owner: &N, unhashed_owner_name: &N, + _unhashed_owner_name_is_ent: bool, ) -> Result { mk_hashed_nsec3_owner_name( unhashed_owner_name, From f128a603d9d400c9cef7ddc1a45e0116b2103693 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:01:31 +0100 Subject: [PATCH 309/569] Rename remaining references to family. --- src/sign/hashing/nsec.rs | 8 ++++---- src/sign/hashing/nsec3.rs | 10 +++++++--- src/sign/mod.rs | 10 +++++----- src/sign/records.rs | 19 ++++++++++--------- src/sign/signing/rrsigs.rs | 8 ++++---- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/sign/hashing/nsec.rs b/src/sign/hashing/nsec.rs index 73aabb154..b49eedc19 100644 --- a/src/sign/hashing/nsec.rs +++ b/src/sign/hashing/nsec.rs @@ -30,7 +30,7 @@ where // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - // Since the records are ordered, the first family is the apex -- we can + // Since the records are ordered, the first owner is the apex -- we can // skip everything before that. records.skip_before(expected_apex); @@ -49,17 +49,17 @@ where break; } - // If the family is below a zone cut, we must ignore it. + // If the owner is below a zone cut, we must ignore it. if let Some(ref cut) = cut { if owner_rrs.owner().ends_with(cut) { continue; } } - // A copy of the family name. We’ll need it later. + // A copy of the owner name. We’ll need it later. let name = owner_rrs.owner().clone(); - // If this family is the parent side of a zone cut, we keep the family + // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. cut = if owner_rrs.is_zone_cut(expected_apex) { diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 8d5c423c5..9d56fc031 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -115,7 +115,7 @@ where // A copy of the owner name. We’ll need it later. let name = owner_rrs.owner().clone(); - // If this family is the parent side of a zone cut, we keep the family + // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. cut = if owner_rrs.is_zone_cut(expected_apex) { @@ -162,7 +162,7 @@ where let distance_to_root = name.iter_labels().count(); let distance_to_apex = distance_to_root - apex_label_count; if distance_to_apex > last_nent_distance_to_apex { - trace!("Possible ENT detected at family {}", owner_rrs.owner()); + trace!("Possible ENT detected at owner {}", owner_rrs.owner()); // Are there any empty nodes between this node and the apex? The // zone file records are already sorted so if all of the parent @@ -434,7 +434,11 @@ where EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, HashProvider: Nsec3HashProvider, { - let owner_name = hash_provider.get_or_create(apex_owner, name, unhashed_owner_name_is_ent)?; + let owner_name = hash_provider.get_or_create( + apex_owner, + name, + unhashed_owner_name_is_ent, + )?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." diff --git a/src/sign/mod.rs b/src/sign/mod.rs index fa7be3194..043bc8623 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -440,7 +440,7 @@ where // the MINIMUM field of the SOA record and the TTL of the SOA itself". let ttl = min(soa_data.minimum(), soa.ttl()); - let families = RecordsIter::new(in_out.as_slice()); + let owner_rrs = RecordsIter::new(in_out.as_slice()); match &mut signing_config.hashing { HashingConfig::Prehashed => { @@ -451,7 +451,7 @@ where let nsecs = generate_nsecs( apex, ttl, - families, + owner_rrs, signing_config.add_used_dnskeys, ); @@ -475,7 +475,7 @@ where generate_nsec3s::( apex, ttl, - families, + owner_rrs, params.clone(), *opt_out, signing_config.add_used_dnskeys, @@ -526,12 +526,12 @@ where } if !signing_keys.is_empty() { - let families = RecordsIter::new(in_out.as_slice()); + let owner_rrs = RecordsIter::new(in_out.as_slice()); let rrsigs_and_dnskeys = generate_rrsigs::( apex, - families, + owner_rrs, signing_keys, signing_config.add_used_dnskeys, )?; diff --git a/src/sign/records.rs b/src/sign/records.rs index 1e0dc34da..d3a1a9a15 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -186,7 +186,7 @@ where } } - pub fn families(&self) -> RecordsIter { + pub fn owner_rrs(&self) -> RecordsIter { RecordsIter::new(&self.records) } @@ -385,8 +385,8 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { self.slice[0].class() } - pub fn rrsets(&self) -> FamilyIter<'a, N, D> { - FamilyIter::new(self.slice) + pub fn rrsets(&self) -> OwnerRrsIter<'a, N, D> { + OwnerRrsIter::new(self.slice) } pub fn records(&self) -> slice::Iter<'a, Record> { @@ -553,20 +553,21 @@ where } } -//------------ FamilyIter ---------------------------------------------------- +//------------ OwnerRrsIter -------------------------------------------------- -/// An iterator that produces RRsets from a record family. -pub struct FamilyIter<'a, N, D> { +/// An iterator that produces RRsets from a set of records with the same owner +/// name. +pub struct OwnerRrsIter<'a, N, D> { slice: &'a [Record], } -impl<'a, N, D> FamilyIter<'a, N, D> { +impl<'a, N, D> OwnerRrsIter<'a, N, D> { fn new(slice: &'a [Record]) -> Self { - FamilyIter { slice } + OwnerRrsIter { slice } } } -impl<'a, N, D> Iterator for FamilyIter<'a, N, D> +impl<'a, N, D> Iterator for OwnerRrsIter<'a, N, D> where N: ToName + 'a, D: RecordData + 'a, diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signing/rrsigs.rs index df545e811..f84295044 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signing/rrsigs.rs @@ -245,7 +245,7 @@ where }; for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { - // A copy of the family name. We’ll need it later. + // A copy of the owner name. We’ll need it later. let name = apex_owner_rrs.owner().clone(); let rrsig_rr = @@ -269,17 +269,17 @@ where break; } - // If the family is below a zone cut, we must ignore it. + // If the owner is below a zone cut, we must ignore it. if let Some(ref cut) = cut { if owner_rrs.owner().ends_with(cut) { continue; } } - // A copy of the family name. We’ll need it later. + // A copy of the owner name. We’ll need it later. let name = owner_rrs.owner().clone(); - // If this family is the parent side of a zone cut, we keep the family + // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. cut = if owner_rrs.is_zone_cut(expected_apex) { From e8bbd08a629b5fdf37db7d3cb0c5cb59699fe0b5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 9 Jan 2025 21:31:21 +0100 Subject: [PATCH 310/569] Clippy. --- src/base/message.rs | 5 +---- src/sign/records.rs | 15 +++------------ src/tsig/mod.rs | 5 +---- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/base/message.rs b/src/base/message.rs index c26839ece..333f89d49 100644 --- a/src/base/message.rs +++ b/src/base/message.rs @@ -517,10 +517,7 @@ impl Message { // iterator would break off in this case and we break out with a None // right away. pub fn canonical_name(&self) -> Option>> { - let question = match self.first_question() { - None => return None, - Some(question) => question, - }; + let question = self.first_question()?; let mut name = question.into_qname(); let answer = match self.answer() { Ok(answer) => answer.limit_to::>(), diff --git a/src/sign/records.rs b/src/sign/records.rs index d3a1a9a15..be7341233 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -496,10 +496,7 @@ where type Item = OwnerRrs<'a, N, D>; fn next(&mut self) -> Option { - let first = match self.slice.first() { - Some(first) => first, - None => return None, - }; + let first = self.slice.first()?; let mut end = 1; while let Some(record) = self.slice.get(end) { if !record.owner().name_eq(first.owner()) { @@ -534,10 +531,7 @@ where type Item = Rrset<'a, N, D>; fn next(&mut self) -> Option { - let first = match self.slice.first() { - Some(first) => first, - None => return None, - }; + let first = self.slice.first()?; let mut end = 1; while let Some(record) = self.slice.get(end) { if !record.owner().name_eq(first.owner()) @@ -575,10 +569,7 @@ where type Item = Rrset<'a, N, D>; fn next(&mut self) -> Option { - let first = match self.slice.first() { - Some(first) => first, - None => return None, - }; + let first = self.slice.first()?; let mut end = 1; while let Some(record) = self.slice.get(end) { if record.rtype() != first.rtype() { diff --git a/src/tsig/mod.rs b/src/tsig/mod.rs index 86a3158bb..6b660b991 100644 --- a/src/tsig/mod.rs +++ b/src/tsig/mod.rs @@ -1653,10 +1653,7 @@ impl Algorithm { /// Returns `None` if the name doesn’t represent a known algorithm. pub fn from_name(name: &N) -> Option { let mut labels = name.iter_labels(); - let first = match labels.next() { - Some(label) => label, - None => return None, - }; + let first = labels.next()?; match labels.next() { Some(label) if label.is_root() => {} _ => return None, From 3fc8c010487b9b1bbd1563fb990c1dfb4ad58fdb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:36:10 +0100 Subject: [PATCH 311/569] Cleanup: - Remove unnecessary apex argument and trait fn as it can be derived from the input. - Remove unnecesarsy skip_before, the input should be the zone and nothing else. - Replace one unwrap() with an error return instead. - The Sort generic argument should be called Sort everywhere, not S. - SignableZone and SignableZoneInPlace don't no longer need specific impls for SortedRecords and Vec but can instead be blanket impls. - Handle the case that the zone has too many apex SOAs. - Factor out apex SOA lookup code. - Added some RustDoc. --- src/sign/error.rs | 8 +-- src/sign/hashing/nsec.rs | 11 +--- src/sign/hashing/nsec3.rs | 16 +++-- src/sign/mod.rs | 116 +++++++++++++++++++++++++++---------- src/sign/records.rs | 46 +++++++++------ src/sign/signing/traits.rs | 71 ++++++----------------- 6 files changed, 149 insertions(+), 119 deletions(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index 5d0899087..1150e5923 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -20,8 +20,8 @@ pub enum SigningError { /// [`SigningKeyUsageStrategy`] used. NoSuitableKeysFound, - // TODO - NoSoaFound, + // The zone either lacks a SOA record or has more than one SOA record. + SoaRecordCouldNotBeDetermined, // TODO Nsec3HashingError(Nsec3HashError), @@ -43,8 +43,8 @@ impl Display for SigningError { SigningError::NoSuitableKeysFound => { f.write_str("No suitable keys found") } - SigningError::NoSoaFound => { - f.write_str("nNo apex SOA record found") + SigningError::SoaRecordCouldNotBeDetermined => { + f.write_str("nNo apex SOA or too many apex SOA records found") } SigningError::Nsec3HashingError(err) => { f.write_fmt(format_args!("NSEC3 hashing error: {err}")) diff --git a/src/sign/hashing/nsec.rs b/src/sign/hashing/nsec.rs index b49eedc19..6b7d0bfe0 100644 --- a/src/sign/hashing/nsec.rs +++ b/src/sign/hashing/nsec.rs @@ -14,9 +14,8 @@ use crate::sign::records::RecordsIter; // TODO: Add (mutable?) iterator based variant. pub fn generate_nsecs( - expected_apex: &N, ttl: Ttl, - mut records: RecordsIter<'_, N, ZoneRecordData>, + records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, ) -> Vec>> where @@ -30,10 +29,6 @@ where // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - // Since the records are ordered, the first owner is the apex -- we can - // skip everything before that. - records.skip_before(expected_apex); - // Because of the next name thing, we need to keep the last NSEC around. let mut prev: Option<(N, RtypeBitmap)> = None; @@ -45,7 +40,7 @@ where for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(expected_apex) { + if !owner_rrs.is_in_zone(&apex_owner) { break; } @@ -62,7 +57,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(expected_apex) { + cut = if owner_rrs.is_zone_cut(&apex_owner) { Some(name.clone()) } else { None diff --git a/src/sign/hashing/nsec3.rs b/src/sign/hashing/nsec3.rs index 9d56fc031..3822ee995 100644 --- a/src/sign/hashing/nsec3.rs +++ b/src/sign/hashing/nsec3.rs @@ -36,9 +36,8 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. pub fn generate_nsec3s( - expected_apex: &N, ttl: Ttl, - mut records: RecordsIter<'_, N, ZoneRecordData>, + records: RecordsIter<'_, N, ZoneRecordData>, params: Nsec3param, opt_out: Nsec3OptOut, assume_dnskeys_will_be_added: bool, @@ -75,10 +74,6 @@ where // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - // Since the records are ordered, the first owner is the apex -- we can - // skip everything before that. - records.skip_before(expected_apex); - // We also need the apex for the last NSEC. let first_rr = records.first(); let apex_owner = first_rr.owner().clone(); @@ -91,7 +86,7 @@ where // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(expected_apex) { + if !owner_rrs.is_in_zone(&apex_owner) { debug!( "Stopping NSEC3 generation at out-of-zone owner {}", owner_rrs.owner() @@ -118,7 +113,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(expected_apex) { + cut = if owner_rrs.is_zone_cut(&apex_owner) { trace!("Zone cut detected at owner {}", owner_rrs.owner()); Some(name.clone()) } else { @@ -393,7 +388,10 @@ where // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." let nsec3param = Record::new( - expected_apex.try_to_name::().unwrap().into(), + apex_owner + .try_to_name::() + .map_err(|_| Nsec3HashError::AppendError)? + .into(), Class::IN, ttl, params, diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 043bc8623..61e833d6b 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -117,13 +117,15 @@ //! # High level signing //! //! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is -//! implemented, invoke `sign_zone()` on the type. +//! implemented, invoke `sign_zone()` on the type to generate, or in the case +//! of [`SignableZoneInPlace`] to add, all records needed to sign the zone, +//! i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. //! //!
//! -//! Currently there is no support for re-signing a zone, i.e. ensuring -//! that any changes to the authoritative records in the zone are reflected -//! by updating the NSEC(3) chain and generating additional signatures or +//! Currently there is no support for re-signing a zone, i.e. ensuring that +//! any changes to the authoritative records in the zone are reflected by +//! updating the NSEC(3) chain and generating additional signatures or //! regenerating existing ones that have expired. //! //!
@@ -171,7 +173,10 @@ //! records.sign_zone(&mut signing_config, &keys).unwrap(); //! ``` //! -//! If needed, individual RRsets can also be signed: +//! If needed, individual RRsets can also be signed but note that this will +//! **only** generate `RRSIG` records, as `NSEC(3)` generation is currently +//! only supported for the zone as a whole and `DNSKEY` records are only +//! generated for the apex of a zone. //! //! ``` //! # use domain::base::Name; @@ -302,6 +307,20 @@ use signing::traits::{SignRaw, SignableZone, SortedExtend}; //------------ SignableZoneInOut --------------------------------------------- +/// Combined in and out input type for use with [`sign_zone()`]. +/// +/// This type exists, similar to [`Cow`], to allow [`sign_zone()`] to operate +/// on both mutable and immutable zones as input, acting as an in-out +/// parameter whereby the same zone is read from and written to, or as +/// separate in and out parameters where one is an in parameter, the zone to +/// read from, and the other is an out parameter, the collection to write +/// generated records to. +/// +/// Prefer signing via the [`SignableZone`] or [`SignableZoneInPlace`] traits +/// as they handle the construction of this type and calling [`sign_zone()`]. +/// +/// [`Cow`]: std::borrow::Cow +/// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace pub enum SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, @@ -313,7 +332,6 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, Sort: Sorter, T: SortedExtend + ?Sized, { @@ -321,6 +339,8 @@ where SignInto(&'a S, &'b mut T), } +//--- Construction + impl<'a, 'b, N, Octs, S, T, Sort> SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> where @@ -333,21 +353,27 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - S: SignableZone, Sort: Sorter, T: Deref>]> + SortedExtend + ?Sized, { + /// Create an input suitable for signing a zone in-place. fn new_in_place(signable_zone: &'a mut T) -> Self { Self::SignInPlace(signable_zone, Default::default()) } + /// Create an input suitable for signing a read-only zone. + /// + /// Records generated by signing should be written into the provided + /// separate collection. fn new_into(signable_zone: &'a S, out: &'b mut T) -> Self { Self::SignInto(signable_zone, out) } } +//--- Accessors + impl SignableZoneInOut<'_, '_, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, @@ -365,6 +391,10 @@ where + SortedExtend + ?Sized, { + /// Read-only slice based access to the zone to be signed. + /// + /// Allows the zone, whether mutable or immutable, to be accessed via + /// an immutable reference. fn as_slice(&self) -> &[Record>] { match self { SignableZoneInOut::SignInPlace(input_output, _) => input_output, @@ -373,6 +403,16 @@ where } } + /// Add records in sort order to the output. + /// + /// For an immutable zone this will cause records to be added to the + /// separate output collection. + /// + /// For a mutable zone this will cause records to be added to the zone + /// itself. + /// + /// The destination type is required via the [`SortedExtend`] trait bound + /// to ensure that the records are added in [`CanonicalOrd`] order. fn sorted_extend< U: IntoIterator>>, >( @@ -392,9 +432,11 @@ where //------------ sign_zone() --------------------------------------------------- +/// DNSSEC sign an unsigned zone using the given configuration and keys. +/// +/// Given an input zone pub fn sign_zone( mut in_out: SignableZoneInOut, - apex: &N, signing_config: &mut SigningConfig, signing_keys: &[DSK], ) -> Result<(), SigningError> @@ -426,19 +468,23 @@ where + Default, T: Deref>]>, { - let soa = in_out - .as_slice() - .iter() - .find(|r| r.rtype() == Rtype::SOA) - .ok_or(SigningError::NoSoaFound)?; - let ZoneRecordData::Soa(ref soa_data) = soa.data() else { - return Err(SigningError::NoSoaFound); + // Iterate over the RR sets of the first owner name (should be the apex as + // the input should be ordered according to [`CanonicalOrd`] and should be + // a complete zone) to find the SOA record. There should be one and only + // one SOA record. + let soa_rr = get_apex_soa_rr(in_out.as_slice())?; + + // Check that the RDATA for the SOA record can be parsed. + let ZoneRecordData::Soa(ref soa_data) = soa_rr.data() else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); }; + let apex_owner = soa_rr.owner().clone(); + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of // the MINIMUM field of the SOA record and the TTL of the SOA itself". - let ttl = min(soa_data.minimum(), soa.ttl()); + let ttl = min(soa_data.minimum(), soa_rr.ttl()); let owner_rrs = RecordsIter::new(in_out.as_slice()); @@ -449,7 +495,6 @@ where HashingConfig::Nsec => { let nsecs = generate_nsecs( - apex, ttl, owner_rrs, signing_config.add_used_dnskeys, @@ -473,7 +518,6 @@ where // afterwards. let Nsec3Records { recs, mut param } = generate_nsec3s::( - apex, ttl, owner_rrs, params.clone(), @@ -485,16 +529,8 @@ where let ttl = match ttl_mode { Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::Soa => soa.ttl(), - Nsec3ParamTtlMode::SoaMinimum => { - if let ZoneRecordData::Soa(soa_data) = soa.data() { - soa_data.minimum() - } else { - // Errm, this is unexpected. TODO: Should we abort - // with an error here about a malformed zonefile? - soa.ttl() - } - } + Nsec3ParamTtlMode::Soa => soa_rr.ttl(), + Nsec3ParamTtlMode::SoaMinimum => soa_data.minimum(), }; param.set_ttl(ttl); @@ -530,7 +566,7 @@ where let rrsigs_and_dnskeys = generate_rrsigs::( - apex, + &apex_owner, owner_rrs, signing_keys, signing_config.add_used_dnskeys, @@ -541,3 +577,25 @@ where Ok(()) } + +// Assumes that the given records are sorted in [`CanonicalOrd`] order. +fn get_apex_soa_rr( + slice: &[Record>], +) -> Result<&Record>, SigningError> +where + N: ToName, +{ + let first_owner_rrs = RecordsIter::new(slice) + .next() + .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; + let mut soa_rrs = first_owner_rrs + .records() + .filter(|rr| rr.rtype() == Rtype::SOA); + let soa_rr = soa_rrs + .next() + .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; + if soa_rrs.next().is_some() { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + } + Ok(soa_rr) +} diff --git a/src/sign/records.rs b/src/sign/records.rs index be7341233..c84645a1b 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -64,20 +64,20 @@ impl Sorter for DefaultSorter { /// overridden by being generic over an alternate implementation of /// [`Sorter`]. #[derive(Clone)] -pub struct SortedRecords +pub struct SortedRecords where Record: Send, - S: Sorter, + Sort: Sorter, { records: Vec>, - _phantom: PhantomData, + _phantom: PhantomData, } -impl SortedRecords +impl SortedRecords where Record: Send, - S: Sorter, + Sort: Sorter, { pub fn new() -> Self { SortedRecords { @@ -242,11 +242,11 @@ where } } -impl Deref for SortedRecords +impl Deref for SortedRecords where N: Send, D: Send, - S: Sorter, + Sort: Sorter, { type Target = [Record]; @@ -255,11 +255,11 @@ where } } -impl SortedRecords +impl SortedRecords where N: ToName + Send, D: RecordData + CanonicalOrd + Send, - S: Sorter, + Sort: Sorter, SortedRecords: From>>, { pub fn write(&self, target: &mut W) -> Result<(), fmt::Error> @@ -318,14 +318,14 @@ impl Default } } -impl From>> for SortedRecords +impl From>> for SortedRecords where N: ToName + PartialEq + Send, D: RecordData + CanonicalOrd + PartialEq + Send, - S: Sorter, + Sort: Sorter, { fn from(mut src: Vec>) -> Self { - S::sort_by(&mut src, CanonicalOrd::canonical_cmp); + Sort::sort_by(&mut src, CanonicalOrd::canonical_cmp); src.dedup(); SortedRecords { records: src, @@ -334,11 +334,13 @@ where } } -impl FromIterator> - for SortedRecords +impl FromIterator> for SortedRecords where N: ToName, D: RecordData + CanonicalOrd, + N: Send, + D: Send, + Sort: Sorter, { fn from_iter>>(iter: T) -> Self { let mut res = Self::new(); @@ -349,17 +351,19 @@ where } } -impl Extend> - for SortedRecords +impl Extend> for SortedRecords where N: ToName + PartialEq, D: RecordData + CanonicalOrd + PartialEq, + N: Send, + D: Send, + Sort: Sorter, { fn extend>>(&mut self, iter: T) { for item in iter { self.records.push(item); } - S::sort_by(&mut self.records, CanonicalOrd::canonical_cmp); + Sort::sort_by(&mut self.records, CanonicalOrd::canonical_cmp); self.records.dedup(); } } @@ -449,6 +453,14 @@ impl<'a, N, D> Rrset<'a, N, D> { self.slice.iter() } + pub fn len(&self) -> usize { + self.slice.len() + } + + pub fn is_empty(&self) -> bool { + self.slice.is_empty() + } + pub fn as_slice(&self) -> &'a [Record] { self.slice } diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 56cad5711..dd7fb41d0 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -12,7 +12,7 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::OctetsFrom; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Rtype, SecAlg}; +use crate::base::iana::SecAlg; use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; @@ -147,8 +147,6 @@ where ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, { - fn apex(&self) -> Option; - // TODO // fn iter_mut(&mut self) -> T; @@ -181,46 +179,13 @@ where let in_out = SignableZoneInOut::new_into(self, out); sign_zone::( in_out, - &self.apex().ok_or(SigningError::NoSoaFound)?, signing_config, signing_keys, ) } } -//--- impl SignableZone for SortedRecords - -impl SignableZone - for SortedRecords, Sort> -where - N: Clone - + ToName - + From> - + PartialEq - + Send - + CanonicalOrd - + Ord - + Hash, - Octs: Clone - + FromBuilder - + From<&'static [u8]> - + Send - + OctetsFrom> - + From> - + Default, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Sort: Sorter, -{ - fn apex(&self) -> Option { - self.find_soa().map(|soa| soa.owner().clone()) - } -} - -//--- impl SignableZone for Vec - -// NOTE: Assumes that the Vec is already sorted according to CanonicalOrd. -impl SignableZone - for Vec>> +impl SignableZone for T where N: Clone + ToName @@ -239,18 +204,14 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, + T: Deref>]>, { - fn apex(&self) -> Option { - self.iter() - .find(|r| r.rtype() == Rtype::SOA) - .map(|r| r.owner().clone()) - } } //------------ SignableZoneInPlace ------------------------------------------- -pub trait SignableZoneInPlace: - SignableZone + SortedExtend +pub trait SignableZoneInPlace: + SignableZone + SortedExtend where N: Clone + ToName + From> + PartialEq + Ord + Hash, Octs: Clone @@ -261,12 +222,19 @@ where + From> + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Self: SortedExtend + Sized, - S: Sorter, + Self: SortedExtend + Sized, + Sort: Sorter, { fn sign_zone( &mut self, - signing_config: &mut SigningConfig, + signing_config: &mut SigningConfig< + N, + Octs, + Inner, + KeyStrat, + Sort, + HP, + >, signing_keys: &[DSK], ) -> Result<(), SigningError> where @@ -278,11 +246,9 @@ where <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, { - let apex = self.apex().ok_or(SigningError::NoSoaFound)?; let in_out = SignableZoneInOut::new_in_place(self); - sign_zone::( + sign_zone::( in_out, - &apex, signing_config, signing_keys, ) @@ -291,8 +257,7 @@ where //--- impl SignableZoneInPlace for SortedRecords -impl SignableZoneInPlace - for SortedRecords, Sort> +impl SignableZoneInPlace for T where N: Clone + ToName @@ -311,6 +276,8 @@ where + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Sort: Sorter, + T: Deref>]>, + T: SortedExtend + Sized, { } From f945240fa6e2b91c6a4b72ae6559efd336ec105a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:49:09 +0100 Subject: [PATCH 312/569] RustDoc tweaks. --- src/sign/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 61e833d6b..7634a6b10 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -147,7 +147,13 @@ //! use domain::sign::signing::traits::SignableZoneInPlace; //! //! // Create a sorted collection of records. -//! let mut records = SortedRecords::default(); +//! // +//! // Note: You can also use a plain Vec here (or any other type that is +//! // compatible with the SignableZone or SignableZoneInPlace trait bounds) +//! // but then you are responsible for ensuring that records in the zone are +//! // in DNSSEC compatible order, e.g. by calling +//! // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. +//! let mut records = SortedRecords::new(); //! //! // Insert records into the collection. Just a dummy SOA for this example. //! let soa = Soa::new( From 87ba5c6ed899371d44e38b1faf30353e9d12ffab Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:50:11 +0100 Subject: [PATCH 313/569] RustDoc tweaks. --- src/sign/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 7634a6b10..8b5c29829 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -156,15 +156,15 @@ //! let mut records = SortedRecords::new(); //! //! // Insert records into the collection. Just a dummy SOA for this example. -//! let soa = Soa::new( +//! let soa = ZoneRecordData::Soa(Soa::new( //! root.clone(), //! root.clone(), //! Serial::now(), //! Ttl::ZERO, //! Ttl::ZERO, //! Ttl::ZERO, -//! Ttl::ZERO); -//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, ZoneRecordData::Soa(soa))); +//! Ttl::ZERO)); +//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); //! //! // Generate or import signing keys (see above). //! From faaa7db7e51eba792f7cb3408677f1a78af5d4fd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:53:52 +0100 Subject: [PATCH 314/569] RustDoc tweaks. --- src/sign/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 8b5c29829..e3c1bed2a 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -141,8 +141,8 @@ //! # let key = SigningKey::new(root.clone(), 257, key_pair); //! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; //! use domain::rdata::dnssec::Timestamp; -//! use domain::sign::keys::keymeta::{DnssecSigningKey, DesignatedSigningKey}; -//! use domain::sign::records::{DefaultSorter, SortedRecords}; +//! use domain::sign::keys::keymeta::DnssecSigningKey; +//! use domain::sign::records::SortedRecords; //! use domain::sign::signing::config::SigningConfig; //! use domain::sign::signing::traits::SignableZoneInPlace; //! @@ -189,7 +189,7 @@ //! # use domain::base::iana::Class; //! # use domain::sign::keys::keypair; //! # use domain::sign::keys::keypair::GenerateParams; -//! # use domain::sign::keys::keymeta::{DesignatedSigningKey, DnssecSigningKey}; +//! # use domain::sign::keys::keymeta::DnssecSigningKey; //! # use domain::sign::records; //! # use domain::sign::keys::signingkey::SigningKey; //! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); @@ -197,7 +197,7 @@ //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); //! # let keys = [DnssecSigningKey::new_csk(key)]; -//! # let mut records = records::SortedRecords::<_, _, records::DefaultSorter>::new(); +//! # let mut records = records::SortedRecords::default(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; //! let apex = Name::>::root(); From d26d62092217078db54c6207268d13cfeef9b683 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:55:53 +0100 Subject: [PATCH 315/569] RustDoc tweaks. --- src/sign/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e3c1bed2a..acd96a174 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -170,7 +170,7 @@ //! //! // Assign signature validity period and operator intent to the keys. //! let key = key.with_validity(Timestamp::now(), Timestamp::now()); -//! let keys = [DnssecSigningKey::new_csk(key)]; +//! let keys = [DnssecSigningKey::from(key)]; //! //! // Create a signing configuration. //! let mut signing_config = SigningConfig::default(); @@ -196,7 +196,7 @@ //! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); -//! # let keys = [DnssecSigningKey::new_csk(key)]; +//! # let keys = [DnssecSigningKey::from(key)]; //! # let mut records = records::SortedRecords::default(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; From d7ee3c08fd0b3861d1666f7d557313ff1c141e69 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:14:24 +0100 Subject: [PATCH 316/569] FIX: When signing to another collection rather than in-place don't neglect to sign the NSEC(3)s. --- src/sign/mod.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index acd96a174..222457c03 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -409,6 +409,15 @@ where } } + /// Read-only slice based access to the record collection being written + /// to. + fn as_out_slice(&self) -> &[Record>] { + match self { + SignableZoneInOut::SignInPlace(input_output, _) => input_output, + SignableZoneInOut::SignInto(_, output) => output, + } + } + /// Add records in sort order to the output. /// /// For an immutable zone this will cause records to be added to the @@ -568,6 +577,22 @@ where } if !signing_keys.is_empty() { + // Sign the NSEC(3)s. + let owner_rrs = RecordsIter::new(in_out.as_out_slice()); + + let nsec_rrsigs = + generate_rrsigs::( + &apex_owner, + owner_rrs, + signing_keys, + signing_config.add_used_dnskeys, + )?; + + // Sorting may not be strictly needed, but we don't have the option to + // extend without sort at the moment. + in_out.sorted_extend(nsec_rrsigs); + + // Sign the original unsigned records. let owner_rrs = RecordsIter::new(in_out.as_slice()); let rrsigs_and_dnskeys = @@ -578,6 +603,8 @@ where signing_config.add_used_dnskeys, )?; + // Sorting may not be strictly needed, but we don't have the option to + // extend without sort at the moment. in_out.sorted_extend(rrsigs_and_dnskeys); } From 55e333acb8e80f89b482bb94375fa73675e21c90 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 10:26:25 +0100 Subject: [PATCH 317/569] Undo unintended changes compared to main. --- Changelog.md | 2 + examples/client-transports.rs | 62 ++++++++++++++++------- src/net/client/mod.rs | 5 ++ src/net/client/multi_stream.rs | 92 +++++++++++++++++++++++++++++++--- src/net/client/redundant.rs | 58 ++++++++++++++++----- 5 files changed, 180 insertions(+), 39 deletions(-) diff --git a/Changelog.md b/Changelog.md index ade4f0001..f500d4b17 100644 --- a/Changelog.md +++ b/Changelog.md @@ -57,6 +57,8 @@ Other changes [#396]: https://github.com/NLnetLabs/domain/pull/396 [#417]: https://github.com/NLnetLabs/domain/pull/417 [#421]: https://github.com/NLnetLabs/domain/pull/421 +[#424]: https://github.com/NLnetLabs/domain/pull/424 +[#425]: https://github.com/NLnetLabs/domain/pull/425 [#427]: https://github.com/NLnetLabs/domain/pull/427 [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 diff --git a/examples/client-transports.rs b/examples/client-transports.rs index 40f0e9a9a..5b6832a0d 100644 --- a/examples/client-transports.rs +++ b/examples/client-transports.rs @@ -1,4 +1,13 @@ -/// Using the `domain::net::client` module for sending a query. +//! Using the `domain::net::client` module for sending a query. +use domain::base::{MessageBuilder, Name, Rtype}; +use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; +use domain::net::client::request::{ + RequestMessage, RequestMessageMulti, SendRequest, +}; +use domain::net::client::{ + cache, dgram, dgram_stream, load_balancer, multi_stream, redundant, + stream, +}; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; #[cfg(feature = "unstable-validator")] @@ -6,20 +15,6 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; -use domain::base::MessageBuilder; -use domain::base::Name; -use domain::base::Rtype; -use domain::net::client::cache; -use domain::net::client::dgram; -use domain::net::client::dgram_stream; -use domain::net::client::multi_stream; -use domain::net::client::protocol::{TcpConnect, TlsConnect, UdpConnect}; -use domain::net::client::redundant; -use domain::net::client::request::{ - RequestMessage, RequestMessageMulti, SendRequest, -}; -use domain::net::client::stream; - #[cfg(feature = "tsig")] use domain::net::client::request::SendRequestMulti; #[cfg(feature = "tsig")] @@ -206,9 +201,9 @@ async fn main() { }); // Add the previously created transports. - redun.add(Box::new(udptcp_conn)).await.unwrap(); - redun.add(Box::new(tcp_conn)).await.unwrap(); - redun.add(Box::new(tls_conn)).await.unwrap(); + redun.add(Box::new(udptcp_conn.clone())).await.unwrap(); + redun.add(Box::new(tcp_conn.clone())).await.unwrap(); + redun.add(Box::new(tls_conn.clone())).await.unwrap(); // Start a few queries. for i in 1..10 { @@ -221,6 +216,37 @@ async fn main() { drop(redun); + // Create a transport connection for load balanced connections. + let (lb, transp) = load_balancer::Connection::new(); + + // Start the run function on a separate task. + let run_fut = transp.run(); + tokio::spawn(async move { + run_fut.await; + println!("load_balancer run terminated"); + }); + + // Add the previously created transports. + let mut conn_conf = load_balancer::ConnConfig::new(); + conn_conf.set_max_burst(Some(10)); + conn_conf.set_burst_interval(Duration::from_secs(10)); + lb.add("UDP+TCP", &conn_conf, Box::new(udptcp_conn)) + .await + .unwrap(); + lb.add("TCP", &conn_conf, Box::new(tcp_conn)).await.unwrap(); + lb.add("TLS", &conn_conf, Box::new(tls_conn)).await.unwrap(); + + // Start a few queries. + for i in 1..10 { + let mut request = lb.send_request(req.clone()); + let reply = request.get_response().await; + if i == 2 { + println!("load_balancer connection reply: {reply:?}"); + } + } + + drop(lb); + // Create a new datagram transport connection. Pass the destination address // and port as parameter. This transport does not retry over TCP if the // reply is truncated. This transport does not have a separate run diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs index 89f68fd35..8b3a48087 100644 --- a/src/net/client/mod.rs +++ b/src/net/client/mod.rs @@ -21,6 +21,10 @@ //! transport connections. The [redundant] transport favors the connection //! with the lowest response time. Any of the other transports can be added //! as upstream transports. +//! * [load_balancer] This transport distributes requests over a collecton of +//! transport connections. The [load_balancer] transport favors connections +//! with the shortest outstanding request queue. Any of the other transports +//! can be added as upstream transports. //! * [cache] This is a simple message cache provided as a pass through //! transport. The cache works with any of the other transports. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] @@ -222,6 +226,7 @@ pub mod cache; pub mod dgram; pub mod dgram_stream; +pub mod load_balancer; pub mod multi_stream; pub mod protocol; pub mod redundant; diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index d0c65c753..c45db3726 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -9,6 +9,7 @@ use crate::net::client::request::{ ComposeRequest, Error, GetResponse, RequestMessageMulti, SendRequest, }; use crate::net::client::stream; +use crate::utils::config::DefMinMax; use bytes::Bytes; use futures_util::stream::FuturesUnordered; use futures_util::StreamExt; @@ -23,6 +24,7 @@ use std::vec::Vec; use tokio::io; use tokio::io::{AsyncRead, AsyncWrite}; use tokio::sync::{mpsc, oneshot}; +use tokio::time::timeout; use tokio::time::{sleep_until, Instant}; //------------ Constants ----------------------------------------------------- @@ -33,16 +35,42 @@ const DEF_CHAN_CAP: usize = 8; /// Error messafe when the connection is closed. const ERR_CONN_CLOSED: &str = "connection closed"; +//------------ Configuration Constants ---------------------------------------- + +/// Default response timeout. +const RESPONSE_TIMEOUT: DefMinMax = DefMinMax::new( + Duration::from_secs(30), + Duration::from_millis(1), + Duration::from_secs(600), +); + //------------ Config --------------------------------------------------------- /// Configuration for an multi-stream transport. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Config { + /// Response timeout currently in effect. + response_timeout: Duration, + /// Configuration of the underlying stream transport. stream: stream::Config, } impl Config { + /// Returns the response timeout. + /// + /// This is the amount of time to wait for a request to complete. + pub fn response_timeout(&self) -> Duration { + self.response_timeout + } + + /// Sets the response timeout. + /// + /// Excessive values are quietly trimmed. + pub fn set_response_timeout(&mut self, timeout: Duration) { + self.response_timeout = RESPONSE_TIMEOUT.limit(timeout); + } + /// Returns the underlying stream config. pub fn stream(&self) -> &stream::Config { &self.stream @@ -56,7 +84,19 @@ impl Config { impl From for Config { fn from(stream: stream::Config) -> Self { - Self { stream } + Self { + stream, + response_timeout: RESPONSE_TIMEOUT.default(), + } + } +} + +impl Default for Config { + fn default() -> Self { + Self { + stream: Default::default(), + response_timeout: RESPONSE_TIMEOUT.default(), + } } } @@ -67,6 +107,9 @@ impl From for Config { pub struct Connection { /// The sender half of the connection request channel. sender: mpsc::Sender>, + + /// Maximum amount of time to wait for a response. + response_timeout: Duration, } impl Connection { @@ -80,8 +123,15 @@ impl Connection { remote: Remote, config: Config, ) -> (Self, Transport) { + let response_timeout = config.response_timeout; let (sender, transport) = Transport::new(remote, config); - (Self { sender }, transport) + ( + Self { + sender, + response_timeout, + }, + transport, + ) } } @@ -147,6 +197,7 @@ impl Clone for Connection { fn clone(&self) -> Self { Self { sender: self.sender.clone(), + response_timeout: self.response_timeout, } } } @@ -175,6 +226,9 @@ struct Request { /// It is kept so we can compare a response with it. request_msg: Req, + /// Start time of the request. + start: Instant, + /// Current state of the query. state: QueryState, @@ -232,6 +286,7 @@ impl Request { Self { conn, request_msg, + start: Instant::now(), state: QueryState::RequestConn, conn_id: None, delayed_retry_count: 0, @@ -246,9 +301,20 @@ impl Request { /// it is resolved, you can call it again to get a new future. pub async fn get_response(&mut self) -> Result, Error> { loop { + let elapsed = self.start.elapsed(); + if elapsed >= self.conn.response_timeout { + return Err(Error::StreamReadTimeout); + } + let remaining = self.conn.response_timeout - elapsed; + match self.state { QueryState::RequestConn => { - let rx = match self.conn.new_conn(self.conn_id).await { + let to = + timeout(remaining, self.conn.new_conn(self.conn_id)) + .await + .map_err(|_| Error::StreamReadTimeout)?; + + let rx = match to { Ok(rx) => rx, Err(err) => { self.state = QueryState::Done; @@ -258,7 +324,10 @@ impl Request { self.state = QueryState::ReceiveConn(rx); } QueryState::ReceiveConn(ref mut receiver) => { - let res = match receiver.await { + let to = timeout(remaining, receiver) + .await + .map_err(|_| Error::StreamReadTimeout)?; + let res = match to { Ok(res) => res, Err(_) => { // Assume receive error @@ -294,8 +363,10 @@ impl Request { continue; } QueryState::GetResult(ref mut query) => { - let res = query.get_response().await; - match res { + let to = timeout(remaining, query.get_response()) + .await + .map_err(|_| Error::StreamReadTimeout)?; + match to { Ok(reply) => { return Ok(reply); } @@ -332,7 +403,12 @@ impl Request { } } QueryState::Delay(instant, duration) => { - sleep_until(instant + duration).await; + if timeout(remaining, sleep_until(instant + duration)) + .await + .is_err() + { + return Err(Error::StreamReadTimeout); + }; self.state = QueryState::RequestConn; } QueryState::Done => { diff --git a/src/net/client/redundant.rs b/src/net/client/redundant.rs index 413734b14..7ec167cdb 100644 --- a/src/net/client/redundant.rs +++ b/src/net/client/redundant.rs @@ -54,24 +54,51 @@ const SMOOTH_N: f64 = 8.; /// Chance to probe a worse connection. const PROBE_P: f64 = 0.05; -/// Avoid sending two requests at the same time. -/// -/// When a worse connection is probed, give it a slight head start. -const PROBE_RT: Duration = Duration::from_millis(1); - //------------ Config --------------------------------------------------------- /// User configuration variables. #[derive(Clone, Copy, Debug, Default)] pub struct Config { /// Defer transport errors. - pub defer_transport_error: bool, + defer_transport_error: bool, /// Defer replies that report Refused. - pub defer_refused: bool, + defer_refused: bool, /// Defer replies that report ServFail. - pub defer_servfail: bool, + defer_servfail: bool, +} + +impl Config { + /// Return the value of the defer_transport_error configuration variable. + pub fn defer_transport_error(&self) -> bool { + self.defer_transport_error + } + + /// Set the value of the defer_transport_error configuration variable. + pub fn set_defer_transport_error(&mut self, value: bool) { + self.defer_transport_error = value + } + + /// Return the value of the defer_refused configuration variable. + pub fn defer_refused(&self) -> bool { + self.defer_refused + } + + /// Set the value of the defer_refused configuration variable. + pub fn set_defer_refused(&mut self, value: bool) { + self.defer_refused = value + } + + /// Return the value of the defer_servfail configuration variable. + pub fn defer_servfail(&self) -> bool { + self.defer_servfail + } + + /// Set the value of the defer_servfail configuration variable. + pub fn set_defer_servfail(&mut self, value: bool) { + self.defer_servfail = value + } } //------------ Connection ----------------------------------------------------- @@ -159,7 +186,7 @@ impl SendRequest //------------ Request ------------------------------------------------------- /// An active request. -pub struct Request { +struct Request { /// The underlying future. fut: Pin< Box, Error>> + Send + Sync>, @@ -200,7 +227,7 @@ impl Debug for Request { /// This type represents an active query request. #[derive(Debug)] -pub struct Query +struct Query where Req: Send + Sync, { @@ -385,10 +412,15 @@ impl Query { // Do we want to probe a less performant upstream? if conn_rt_len > 1 && random::() < PROBE_P { let index: usize = 1 + random::() % (conn_rt_len - 1); - conn_rt[index].est_rt = PROBE_RT; - // Sort again - conn_rt.sort_unstable_by(conn_rt_cmp); + // Give the probe some head start. We may need a separate + // configuration parameter. A multiple of min_rt. Just use + // min_rt for now. + let min_rt = conn_rt.iter().map(|e| e.est_rt).min().unwrap(); + + let mut e = conn_rt.remove(index); + e.est_rt = min_rt; + conn_rt.insert(0, e); } Self { From 28623ddab546fcfb2bce3b3d360def82d4f070f5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:26:24 +0100 Subject: [PATCH 318/569] More RustDoc tweaks for the sign module, and restore the crypto common module by moving keys/keypair.rs to crypto/common.rs. --- .../{keys/keypair.rs => crypto/common.rs} | 0 src/sign/crypto/mod.rs | 1 + src/sign/crypto/openssl.rs | 12 ++- src/sign/crypto/ring.rs | 12 ++- src/sign/keys/mod.rs | 1 - src/sign/mod.rs | 80 ++++++++++++------- 6 files changed, 60 insertions(+), 46 deletions(-) rename src/sign/{keys/keypair.rs => crypto/common.rs} (100%) diff --git a/src/sign/keys/keypair.rs b/src/sign/crypto/common.rs similarity index 100% rename from src/sign/keys/keypair.rs rename to src/sign/crypto/common.rs diff --git a/src/sign/crypto/mod.rs b/src/sign/crypto/mod.rs index a60a4dd8a..0d7dcb33d 100644 --- a/src/sign/crypto/mod.rs +++ b/src/sign/crypto/mod.rs @@ -1,2 +1,3 @@ +pub mod common; pub mod openssl; pub mod ring; diff --git a/src/sign/crypto/openssl.rs b/src/sign/crypto/openssl.rs index 49c0348ef..fe5d3a50a 100644 --- a/src/sign/crypto/openssl.rs +++ b/src/sign/crypto/openssl.rs @@ -25,7 +25,7 @@ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; use crate::sign::error::SignError; -use crate::sign::keys::keypair::GenerateParams; +use crate::sign::crypto::common::GenerateParams; use crate::sign::{RsaSecretKeyBytes, SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -448,14 +448,12 @@ impl std::error::Error for GenerateError {} mod tests { use std::{string::ToString, vec::Vec}; - use crate::{ - base::iana::SecAlg, - sign::{SecretKeyBytes, SignRaw}, - validate::Key, - }; + use crate::base::iana::SecAlg; + use crate::sign::{SecretKeyBytes, SignRaw}; + use crate::validate::Key; + use crate::sign::crypto::common::GenerateParams; use super::KeyPair; - use crate::sign::keys::keypair::GenerateParams; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), diff --git a/src/sign/crypto/ring.rs b/src/sign/crypto/ring.rs index 52c35c102..147fdf4a1 100644 --- a/src/sign/crypto/ring.rs +++ b/src/sign/crypto/ring.rs @@ -21,7 +21,7 @@ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; use crate::sign::error::SignError; -use crate::sign::keys::keypair::GenerateParams; +use crate::sign::crypto::common::GenerateParams; use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -365,14 +365,12 @@ impl std::error::Error for GenerateError {} mod tests { use std::{sync::Arc, vec::Vec}; - use crate::{ - base::iana::SecAlg, - sign::{SecretKeyBytes, SignRaw}, - validate::Key, - }; + use crate::base::iana::SecAlg; + use crate::sign::{SecretKeyBytes, SignRaw}; + use crate::validate::Key; + use crate::sign::crypto::common::GenerateParams; use super::KeyPair; - use crate::sign::keys::keypair::GenerateParams; const KEYS: &[(SecAlg, u16)] = &[ (SecAlg::RSASHA256, 60616), diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index bab9de0c9..432dad726 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -1,5 +1,4 @@ pub mod bytes; pub mod keymeta; -pub mod keypair; pub mod keyset; pub mod signingkey; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 222457c03..ff2cef67c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -24,15 +24,14 @@ //! //! 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. -//! [`keys::keypair::KeyPair`]). +//! the key should be used. [`SigningKey`] relies on a cryptographic backend +//! to provide the underlying signing operation (e.g. `KeyPair`). //! //! While all records in a zone can be signed with a single key, it is useful -//! to use one key, a Key Signing Key (KSK), "to sign the apex DNSKEY RRset in -//! a zone" and another key, a Zone Signing Key (ZSK), "to sign all the RRsets -//! in a zone that require signatures, other than the apex DNSKEY RRset" (see -//! [RFC 6781 section 3.1]). +//! to use one key, a Key Signing Key (KSK), _"to sign the apex DNSKEY RRset +//! in a zone"_ and another key, a Zone Signing Key (ZSK), _"to sign all the +//! RRsets in a zone that require signatures, other than the apex DNSKEY +//! RRset"_ (see [RFC 6781 section 3.1]). //! //! Cryptographically there is no difference between these key types, they are //! assigned by the operator to signal their intended usage. This module @@ -117,14 +116,14 @@ //! # High level signing //! //! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is -//! implemented, invoke `sign_zone()` on the type to generate, or in the case -//! of [`SignableZoneInPlace`] to add, all records needed to sign the zone, -//! i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. +//! implemented, invoke [`sign_zone()`] on the type to generate, or in the +//! case of [`SignableZoneInPlace`] to add, all records needed to sign the +//! zone, i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. //! //!
//! -//! Currently there is no support for re-signing a zone, i.e. ensuring that -//! any changes to the authoritative records in the zone are reflected by +//! This module does **NOT** yet support re-signing of a zone, i.e. ensuring +//! that any changes to the authoritative records in the zone are reflected by //! updating the NSEC(3) chain and generating additional signatures or //! regenerating existing ones that have expired. //! @@ -153,7 +152,7 @@ //! // but then you are responsible for ensuring that records in the zone are //! // in DNSSEC compatible order, e.g. by calling //! // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. -//! let mut records = SortedRecords::new(); +//! let mut records = SortedRecords::default(); //! //! // Insert records into the collection. Just a dummy SOA for this example. //! let soa = ZoneRecordData::Soa(Soa::new( @@ -175,7 +174,15 @@ //! // Create a signing configuration. //! let mut signing_config = SigningConfig::default(); //! -//! // Then sign the zone in place. +//! // Then generate the records which when added to the zone make it signed. +//! let mut signer_generated_records = SortedRecords::default(); +//! records.sign_zone( +//! &mut signing_config, +//! &keys, +//! &mut signer_generated_records).unwrap(); +//! +//! // Or if desired and the underlying collection supports it, sign the zone +//! // in-place. //! records.sign_zone(&mut signing_config, &keys).unwrap(); //! ``` //! @@ -205,17 +212,6 @@ //! let generated_records = rrset.sign::(&apex, &keys).unwrap(); //! ``` //! -//! [`DnssecSigningKey`]: crate::sign::keys::keymeta::DnssecSigningKey -//! [`Record`]: crate::base::record::Record -//! [RFC 6871 section 3.1]: https://rfc-editor.org/rfc/rfc6781#section-3.1 -//! [`SigningKeyUsageStrategy`]: -//! crate::sign::signing::strategy::SigningKeyUsageStrategy -//! [`Signable`]: crate::sign::signing::traits::Signable -//! [`SignableZone`]: crate::sign::signing::traits::SignableZone -//! [`SignableZoneInPlace`]: crate::sign::signing::traits::SignableZoneInPlace -//! [`SortedRecords`]: crate::sign::SortedRecords -//! [`Zone`]: crate::zonetree::Zone -//! //! # Cryptography //! //! This crate supports OpenSSL and Ring for performing cryptography. These @@ -225,14 +221,19 @@ //! weaker key sizes). A [`common`] backend is provided for users that wish //! to use either or both backends at runtime. //! -//! 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. +//! 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 -//! (useful for interacting with cryptographic hardware like HSMs) is not -//! currently supported. +//! `KeyPair` type that implements [`SignRaw`]. +//! +//!
+//! +//! This module does **NOT** yet support `async` signing (useful for +//! interacting with cryptographic hardware like HSMs). +//! +//!
//! //! While each cryptographic backend can support a limited number of signature //! algorithms, even the types independent of a cryptographic backend (e.g. @@ -264,6 +265,23 @@ //! keys that are used to sign a particular zone. In addition, the lifetime of //! keys can be maintained using key rolls that phase out old keys and //! introduce new keys. +//! +//! [`common`]: crate::sign::crypto::common +//! [`openssl`]: crate::sign::crypto::openssl +//! [`ring`]: crate::sign::crypto::ring +//! [`DnssecSigningKey`]: crate::sign::keys::keymeta::DnssecSigningKey +//! [`Record`]: crate::base::record::Record +//! [RFC 6781 section 3.1]: https://rfc-editor.org/rfc/rfc6781#section-3.1 +//! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams +//! [`KeyPair`]: crate::sign::crypto::common::KeyPair +//! [`SigningKeyUsageStrategy`]: +//! crate::sign::signing::strategy::SigningKeyUsageStrategy +//! [`Signable`]: crate::sign::signing::traits::Signable +//! [`SignableZone`]: crate::sign::signing::traits::SignableZone +//! [`SignableZoneInPlace`]: crate::sign::signing::traits::SignableZoneInPlace +//! [`SigningKey`]: crate::sign::keys::signingkey::SigningKey +//! [`SortedRecords`]: crate::sign::SortedRecords +//! [`Zone`]: crate::zonetree::Zone #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] From d20e52e82d1903bb3d0f76a7ae5e967278cf042b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:40:35 +0100 Subject: [PATCH 319/569] Fix broken doc tests. --- src/sign/mod.rs | 70 +++++++++++++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ff2cef67c..6effc2b39 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -48,7 +48,9 @@ //! //! ``` //! # use domain::base::iana::SecAlg; -//! # use domain::{sign::*, validate}; +//! # use domain::validate; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::bytes::SecretKeyBytes; //! # use domain::sign::keys::signingkey::SigningKey; //! // Load an Ed25519 key named 'Ktest.+015+56037'. //! let base = "test-data/dnssec-keys/Ktest.+015+56037"; @@ -58,7 +60,7 @@ //! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); //! //! // Parse the key into Ring or OpenSSL. -//! let key_pair = keys::keypair::KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! let key_pair = KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); //! //! // Associate the key with important metadata. //! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); @@ -75,15 +77,16 @@ //! //! ``` //! # use domain::base::Name; -//! # use domain::sign::keys::keypair; -//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; //! # use domain::sign::keys::signingkey::SigningKey; //! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; -//! let (sec_bytes, pub_bytes) = keypair::generate(params).unwrap(); +//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); //! //! // Parse the key into Ring or OpenSSL. -//! let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! //! // Associate the key with important metadata. //! let owner: Name> = "www.example.org.".parse().unwrap(); @@ -101,12 +104,13 @@ //! //! ``` //! # use domain::base::Name; -//! # use domain::sign::keys::keypair; -//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; //! # use domain::sign::keys::signingkey::SigningKey; //! # use domain::sign::signing::traits::SignRaw; -//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let key = SigningKey::new(Name::>::root(), 257, key_pair); //! // Sign arbitrary byte sequences with the key. //! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); @@ -130,12 +134,14 @@ //!
//! //! ``` -//! # use domain::base::{*, iana::Class}; -//! # use domain::sign::keys::keypair; -//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::base::{Name, Record, Serial, Ttl}; +//! # use domain::base::iana::Class; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; //! # use domain::sign::keys::signingkey::SigningKey; -//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root.clone(), 257, key_pair); //! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; @@ -143,7 +149,6 @@ //! use domain::sign::keys::keymeta::DnssecSigningKey; //! use domain::sign::records::SortedRecords; //! use domain::sign::signing::config::SigningConfig; -//! use domain::sign::signing::traits::SignableZoneInPlace; //! //! // Create a sorted collection of records. //! // @@ -176,14 +181,20 @@ //! //! // Then generate the records which when added to the zone make it signed. //! let mut signer_generated_records = SortedRecords::default(); -//! records.sign_zone( -//! &mut signing_config, -//! &keys, -//! &mut signer_generated_records).unwrap(); -//! +//! { +//! use domain::sign::signing::traits::SignableZone; +//! records.sign_zone( +//! &mut signing_config, +//! &keys, +//! &mut signer_generated_records).unwrap(); +//! } +//! //! // Or if desired and the underlying collection supports it, sign the zone //! // in-place. -//! records.sign_zone(&mut signing_config, &keys).unwrap(); +//! { +//! use domain::sign::signing::traits::SignableZoneInPlace; +//! records.sign_zone(&mut signing_config, &keys).unwrap(); +//! } //! ``` //! //! If needed, individual RRsets can also be signed but note that this will @@ -194,13 +205,14 @@ //! ``` //! # use domain::base::Name; //! # use domain::base::iana::Class; -//! # use domain::sign::keys::keypair; -//! # use domain::sign::keys::keypair::GenerateParams; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; //! # use domain::sign::keys::keymeta::DnssecSigningKey; //! # use domain::sign::records; //! # use domain::sign::keys::signingkey::SigningKey; -//! # let (sec_bytes, pub_bytes) = keypair::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = keypair::KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); //! # let keys = [DnssecSigningKey::from(key)]; @@ -227,12 +239,12 @@ //! //! Users can choose to bring their own cryptography by providing their own //! `KeyPair` type that implements [`SignRaw`]. -//! +//! //!
-//! +//! //! This module does **NOT** yet support `async` signing (useful for //! interacting with cryptographic hardware like HSMs). -//! +//! //!
//! //! While each cryptographic backend can support a limited number of signature From 1aef63f266e9c79f170f6c02d86ffd07408fb5e6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:40:40 +0100 Subject: [PATCH 320/569] Cargo fmt. --- src/sign/crypto/openssl.rs | 4 ++-- src/sign/crypto/ring.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/crypto/openssl.rs b/src/sign/crypto/openssl.rs index fe5d3a50a..2699df447 100644 --- a/src/sign/crypto/openssl.rs +++ b/src/sign/crypto/openssl.rs @@ -24,8 +24,8 @@ use openssl::{ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; -use crate::sign::error::SignError; use crate::sign::crypto::common::GenerateParams; +use crate::sign::error::SignError; use crate::sign::{RsaSecretKeyBytes, SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -449,9 +449,9 @@ mod tests { use std::{string::ToString, vec::Vec}; use crate::base::iana::SecAlg; + use crate::sign::crypto::common::GenerateParams; use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::Key; - use crate::sign::crypto::common::GenerateParams; use super::KeyPair; diff --git a/src/sign/crypto/ring.rs b/src/sign/crypto/ring.rs index 147fdf4a1..6663e61b0 100644 --- a/src/sign/crypto/ring.rs +++ b/src/sign/crypto/ring.rs @@ -20,8 +20,8 @@ use ring::signature::{ use secrecy::ExposeSecret; use crate::base::iana::SecAlg; -use crate::sign::error::SignError; use crate::sign::crypto::common::GenerateParams; +use crate::sign::error::SignError; use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; @@ -366,9 +366,9 @@ mod tests { use std::{sync::Arc, vec::Vec}; use crate::base::iana::SecAlg; + use crate::sign::crypto::common::GenerateParams; use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::Key; - use crate::sign::crypto::common::GenerateParams; use super::KeyPair; From bcac30c33b18b4ec465310a77b39a93602550e3e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:46:48 +0100 Subject: [PATCH 321/569] Move crypto errors in to the main error submodule of sign. --- src/sign/crypto/common.rs | 109 +------------------------------------- src/sign/crypto/mod.rs | 1 + src/sign/error.rs | 108 ++++++++++++++++++++++++++++++++++++- src/sign/mod.rs | 2 + 4 files changed, 111 insertions(+), 109 deletions(-) diff --git a/src/sign/crypto/common.rs b/src/sign/crypto/common.rs index fc4b01745..7442d36a5 100644 --- a/src/sign/crypto/common.rs +++ b/src/sign/crypto/common.rs @@ -10,7 +10,7 @@ use std::sync::Arc; use ::ring::rand::SystemRandom; use crate::base::iana::SecAlg; -use crate::sign::error::SignError; +use crate::sign::error::{FromBytesError, GenerateError, SignError}; use crate::sign::{SecretKeyBytes, SignRaw}; use crate::validate::{PublicKeyBytes, Signature}; @@ -197,113 +197,6 @@ pub fn generate( Err(GenerateError::UnsupportedAlgorithm) } -//============ Error Types =================================================== - -//----------- FromBytesError ----------------------------------------------- - -/// An error in importing a key pair from bytes. -#[derive(Clone, Debug)] -pub enum FromBytesError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// The key's parameters were invalid. - InvalidKey, - - /// The implementation does not allow such weak keys. - WeakKey, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversions - -#[cfg(feature = "ring")] -impl From for FromBytesError { - fn from(value: ring::FromBytesError) -> Self { - match value { - ring::FromBytesError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - ring::FromBytesError::InvalidKey => Self::InvalidKey, - ring::FromBytesError::WeakKey => Self::WeakKey, - } - } -} - -#[cfg(feature = "openssl")] -impl From for FromBytesError { - fn from(value: openssl::FromBytesError) -> Self { - match value { - openssl::FromBytesError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - openssl::FromBytesError::InvalidKey => Self::InvalidKey, - openssl::FromBytesError::Implementation => Self::Implementation, - } - } -} - -//--- Formatting - -impl fmt::Display for FromBytesError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::InvalidKey => "malformed or insecure private key", - Self::WeakKey => "key too weak to be supported", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for FromBytesError {} - -//----------- GenerateError -------------------------------------------------- - -/// An error in generating a key pair. -#[derive(Clone, Debug)] -pub enum GenerateError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversion - -#[cfg(feature = "ring")] -impl From for GenerateError { - fn from(value: ring::GenerateError) -> Self { - match value { - ring::GenerateError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - ring::GenerateError::Implementation => Self::Implementation, - } - } -} - -#[cfg(feature = "openssl")] -impl From for GenerateError { - fn from(value: openssl::GenerateError) -> Self { - match value { - openssl::GenerateError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - openssl::GenerateError::Implementation => Self::Implementation, - } - } -} - //--- Formatting impl fmt::Display for GenerateError { diff --git a/src/sign/crypto/mod.rs b/src/sign/crypto/mod.rs index 0d7dcb33d..ca4a0488d 100644 --- a/src/sign/crypto/mod.rs +++ b/src/sign/crypto/mod.rs @@ -1,3 +1,4 @@ +//! Cryptographic backends. pub mod common; pub mod openssl; pub mod ring; diff --git a/src/sign/error.rs b/src/sign/error.rs index 1150e5923..2790e70f1 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -1,6 +1,7 @@ -//! Actual signing. +//! Types of signing related error. use core::fmt::{self, Debug, Display}; +use crate::sign::crypto::{openssl, ring}; use crate::validate::Nsec3HashError; //------------ SigningError -------------------------------------------------- @@ -133,3 +134,108 @@ impl fmt::Display for SignError { } impl std::error::Error for SignError {} + +//----------- FromBytesError ----------------------------------------------- + +/// An error in importing a key pair from bytes. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversions + +#[cfg(feature = "ring")] +impl From for FromBytesError { + fn from(value: ring::FromBytesError) -> Self { + match value { + ring::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::FromBytesError::InvalidKey => Self::InvalidKey, + ring::FromBytesError::WeakKey => Self::WeakKey, + } + } +} + +#[cfg(feature = "openssl")] +impl From for FromBytesError { + fn from(value: openssl::FromBytesError) -> Self { + match value { + openssl::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::FromBytesError::InvalidKey => Self::InvalidKey, + openssl::FromBytesError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +#[cfg(feature = "ring")] +impl From for GenerateError { + fn from(value: ring::GenerateError) -> Self { + match value { + ring::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::GenerateError::Implementation => Self::Implementation, + } + } +} + +#[cfg(feature = "openssl")] +impl From for GenerateError { + fn from(value: openssl::GenerateError) -> Self { + match value { + openssl::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::GenerateError::Implementation => Self::Implementation, + } + } +} diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 6effc2b39..5399cc5c5 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -273,12 +273,14 @@ //! type-level documentation for a specification of the format. //! //! # Key Sets and Key Lifetime +//! //! The [`keyset`] module provides a way to keep track of the collection of //! keys that are used to sign a particular zone. In addition, the lifetime of //! keys can be maintained using key rolls that phase out old keys and //! introduce new keys. //! //! [`common`]: crate::sign::crypto::common +//! [`keyset`]: crate::sign::keys::keyset //! [`openssl`]: crate::sign::crypto::openssl //! [`ring`]: crate::sign::crypto::ring //! [`DnssecSigningKey`]: crate::sign::keys::keymeta::DnssecSigningKey From bac2e8a1d4bb3fe2fb2cfc21daca87bc8e9ac590 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 11:58:12 +0100 Subject: [PATCH 322/569] Re-export some types that live only in modules by the same name or whose submodule is useful for code structure but not for documentation. --- src/sign/keys/mod.rs | 4 ++++ src/sign/mod.rs | 26 ++++++++++++-------------- src/sign/signing/mod.rs | 2 ++ 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index 432dad726..5a3f3f9e1 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -2,3 +2,7 @@ pub mod bytes; pub mod keymeta; pub mod keyset; pub mod signingkey; + +pub use bytes::SecretKeyBytes; +pub use keymeta::DnssecSigningKey; +pub use signingkey::SigningKey; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 5399cc5c5..b9504c064 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -50,8 +50,7 @@ //! # use domain::base::iana::SecAlg; //! # use domain::validate; //! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::bytes::SecretKeyBytes; -//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::keys::{SecretKeyBytes, SigningKey}; //! // Load an Ed25519 key named 'Ktest.+015+56037'. //! let base = "test-data/dnssec-keys/Ktest.+015+56037"; //! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); @@ -80,7 +79,7 @@ //! # use domain::sign::crypto::common; //! # use domain::sign::crypto::common::GenerateParams; //! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::keys::SigningKey; //! // Generate a new Ed25519 key. //! let params = GenerateParams::Ed25519; //! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); @@ -107,7 +106,7 @@ //! # use domain::sign::crypto::common; //! # use domain::sign::crypto::common::GenerateParams; //! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::keys::SigningKey; //! # use domain::sign::signing::traits::SignRaw; //! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); //! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); @@ -139,16 +138,16 @@ //! # use domain::sign::crypto::common; //! # use domain::sign::crypto::common::GenerateParams; //! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::keys::SigningKey; //! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); //! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root.clone(), 257, key_pair); //! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; //! use domain::rdata::dnssec::Timestamp; -//! use domain::sign::keys::keymeta::DnssecSigningKey; +//! use domain::sign::keys::DnssecSigningKey; //! use domain::sign::records::SortedRecords; -//! use domain::sign::signing::config::SigningConfig; +//! use domain::sign::signing::SigningConfig; //! //! // Create a sorted collection of records. //! // @@ -208,19 +207,18 @@ //! # use domain::sign::crypto::common; //! # use domain::sign::crypto::common::GenerateParams; //! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::keymeta::DnssecSigningKey; -//! # use domain::sign::records; -//! # use domain::sign::keys::signingkey::SigningKey; +//! # use domain::sign::keys::{DnssecSigningKey, SigningKey}; +//! # use domain::sign::records::{Rrset, SortedRecords}; //! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); //! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let root = Name::>::root(); //! # let key = SigningKey::new(root, 257, key_pair); //! # let keys = [DnssecSigningKey::from(key)]; -//! # let mut records = records::SortedRecords::default(); +//! # let mut records = SortedRecords::default(); //! use domain::sign::signing::traits::Signable; //! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; //! let apex = Name::>::root(); -//! let rrset = records::Rrset::new(&records); +//! let rrset = Rrset::new(&records); //! let generated_records = rrset.sign::(&apex, &keys).unwrap(); //! ``` //! @@ -283,7 +281,7 @@ //! [`keyset`]: crate::sign::keys::keyset //! [`openssl`]: crate::sign::crypto::openssl //! [`ring`]: crate::sign::crypto::ring -//! [`DnssecSigningKey`]: crate::sign::keys::keymeta::DnssecSigningKey +//! [`DnssecSigningKey`]: crate::sign::keys::DnssecSigningKey //! [`Record`]: crate::base::record::Record //! [RFC 6781 section 3.1]: https://rfc-editor.org/rfc/rfc6781#section-3.1 //! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams @@ -293,7 +291,7 @@ //! [`Signable`]: crate::sign::signing::traits::Signable //! [`SignableZone`]: crate::sign::signing::traits::SignableZone //! [`SignableZoneInPlace`]: crate::sign::signing::traits::SignableZoneInPlace -//! [`SigningKey`]: crate::sign::keys::signingkey::SigningKey +//! [`SigningKey`]: crate::sign::keys::SigningKey //! [`SortedRecords`]: crate::sign::SortedRecords //! [`Zone`]: crate::zonetree::Zone diff --git a/src/sign/signing/mod.rs b/src/sign/signing/mod.rs index 7e317b22d..ec867506d 100644 --- a/src/sign/signing/mod.rs +++ b/src/sign/signing/mod.rs @@ -2,3 +2,5 @@ pub mod config; pub mod rrsigs; pub mod strategy; pub mod traits; + +pub use config::SigningConfig; From d23c1e8fd2f5383dbb5b8bffe33c92169583bf75 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:00:44 +0100 Subject: [PATCH 323/569] Fix missing feature guards. --- src/sign/error.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index 2790e70f1..258f068c4 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -1,7 +1,12 @@ //! Types of signing related error. use core::fmt::{self, Debug, Display}; -use crate::sign::crypto::{openssl, ring}; +#[cfg(feature = "openssl")] +use crate::sign::crypto::openssl; + +#[cfg(feature = "ring")] +use crate::sign::crypto::ring; + use crate::validate::Nsec3HashError; //------------ SigningError -------------------------------------------------- From 28126000ea7aed6abccf27f4f5181cda9d00aac9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:02:23 +0100 Subject: [PATCH 324/569] Ensure re-exports refer only to descendants of the current module. --- src/sign/keys/mod.rs | 6 +++--- src/sign/mod.rs | 2 +- src/sign/signing/mod.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index 5a3f3f9e1..5d9c57ddf 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -3,6 +3,6 @@ pub mod keymeta; pub mod keyset; pub mod signingkey; -pub use bytes::SecretKeyBytes; -pub use keymeta::DnssecSigningKey; -pub use signingkey::SigningKey; +pub use self::bytes::SecretKeyBytes; +pub use self::keymeta::DnssecSigningKey; +pub use self::signingkey::SigningKey; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b9504c064..09d823976 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -308,7 +308,7 @@ pub mod zone; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; -pub use keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; +pub use self::keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; use core::cmp::min; use core::fmt::Display; diff --git a/src/sign/signing/mod.rs b/src/sign/signing/mod.rs index ec867506d..bcf7b4427 100644 --- a/src/sign/signing/mod.rs +++ b/src/sign/signing/mod.rs @@ -3,4 +3,4 @@ pub mod rrsigs; pub mod strategy; pub mod traits; -pub use config::SigningConfig; +pub use self::config::SigningConfig; From 8c2709a08b3ab3f44e4da62efd07706143c9b596 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:06:00 +0100 Subject: [PATCH 325/569] Add missing RRSIG term in RustDoc comment. --- src/sign/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 09d823976..99a30cf93 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -6,8 +6,8 @@ //! //! DNSSEC signed zones consist of configuration data such as DNSKEY and //! NSEC3PARAM records, NSEC(3) chains used to provably deny the existence of -//! records, and signatures that authenticate the authoritative content of the -//! zone. +//! records, and RRSIG signatures that authenticate the authoritative content +//! of the zone. //! //! # Overview //! From 1f75a00bc3cc5453075f45f37b22cc8a1576930d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:26:42 +0100 Subject: [PATCH 326/569] Rename the hashing module to authnext (authenticated non-existence) as NSEC doesn't do any hashing, only NSEC3 does. --- src/sign/{hashing => authnext}/config.rs | 0 src/sign/authnext/mod.rs | 22 ++++++++++++++++++++++ src/sign/{hashing => authnext}/nsec.rs | 0 src/sign/{hashing => authnext}/nsec3.rs | 0 src/sign/hashing/mod.rs | 3 --- src/sign/mod.rs | 8 ++++---- src/sign/signing/config.rs | 4 ++-- src/sign/signing/traits.rs | 2 +- 8 files changed, 29 insertions(+), 10 deletions(-) rename src/sign/{hashing => authnext}/config.rs (100%) create mode 100644 src/sign/authnext/mod.rs rename src/sign/{hashing => authnext}/nsec.rs (100%) rename src/sign/{hashing => authnext}/nsec3.rs (100%) delete mode 100644 src/sign/hashing/mod.rs diff --git a/src/sign/hashing/config.rs b/src/sign/authnext/config.rs similarity index 100% rename from src/sign/hashing/config.rs rename to src/sign/authnext/config.rs diff --git a/src/sign/authnext/mod.rs b/src/sign/authnext/mod.rs new file mode 100644 index 000000000..5169ca053 --- /dev/null +++ b/src/sign/authnext/mod.rs @@ -0,0 +1,22 @@ +//! Authenticated non-existence mechanisms. +//! +//! In order for a DNSSEC server to deny the existence of a RRSET of the +//! requested type or name the server must have an RRSIG signature that it can +//! include in the response to authenticate it. +//! +//! However, an RRSIG signs an existing RRSET in a zone, it cannot sign a +//! non-existing RRSET. DNSSEC signers must therefore add records to the zone +//! that describe the record types and names that DO exist, which a server can +//! use to determine non-existence and which can signed providing an RRSIG to +//! authenticate the response. +//! +//! This module provides implementations of the zone signing related logic for +//! the NSEC ([RFC 4034]) and NSEC3 ([RFC 5155]) mechanisms which can be used +//! during DNSSEC zone signing to add this missing information to the zone +//! prior to signing. +//! +//! [RFC 4034]: https://www.rfc-editor.org/info/rfc4034 +//! [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 +pub mod config; +pub mod nsec; +pub mod nsec3; diff --git a/src/sign/hashing/nsec.rs b/src/sign/authnext/nsec.rs similarity index 100% rename from src/sign/hashing/nsec.rs rename to src/sign/authnext/nsec.rs diff --git a/src/sign/hashing/nsec3.rs b/src/sign/authnext/nsec3.rs similarity index 100% rename from src/sign/hashing/nsec3.rs rename to src/sign/authnext/nsec3.rs diff --git a/src/sign/hashing/mod.rs b/src/sign/hashing/mod.rs deleted file mode 100644 index 4716fcae3..000000000 --- a/src/sign/hashing/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod config; -pub mod nsec; -pub mod nsec3; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 99a30cf93..f0218b556 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -300,7 +300,7 @@ pub mod crypto; pub mod error; -pub mod hashing; +pub mod authnext; pub mod keys; pub mod records; pub mod signing; @@ -325,9 +325,9 @@ use crate::base::{Name, Record, Rtype}; use crate::rdata::ZoneRecordData; use error::SigningError; -use hashing::config::HashingConfig; -use hashing::nsec::generate_nsecs; -use hashing::nsec3::{ +use authnext::config::HashingConfig; +use authnext::nsec::generate_nsecs; +use authnext::nsec3::{ generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, Nsec3Records, }; diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs index 2f8120162..160ba7879 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/signing/config.rs @@ -3,8 +3,8 @@ use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; use crate::base::{Name, ToName}; -use crate::sign::hashing::config::HashingConfig; -use crate::sign::hashing::nsec3::{ +use crate::sign::authnext::config::HashingConfig; +use crate::sign::authnext::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; use crate::sign::records::{DefaultSorter, Sorter}; diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index dd7fb41d0..e9a14af99 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -18,7 +18,7 @@ use crate::base::record::Record; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::sign::error::{SignError, SigningError}; -use crate::sign::hashing::nsec3::Nsec3HashProvider; +use crate::sign::authnext::nsec3::Nsec3HashProvider; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, From ba144e90ee9a1b88460dc94eaf15d6c60b7b16ec Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:26:49 +0100 Subject: [PATCH 327/569] Minor RustDoc tweaks. --- src/sign/error.rs | 2 +- src/sign/records.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index 258f068c4..c5f458d5e 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -1,4 +1,4 @@ -//! Types of signing related error. +//! Signing related errors. use core::fmt::{self, Debug, Display}; #[cfg(feature = "openssl")] diff --git a/src/sign/records.rs b/src/sign/records.rs index c84645a1b..20d888df3 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -1,4 +1,4 @@ -//! Actual signing. +//! Types for iterating over and storing zone records in canonical sort order. use core::cmp::Ordering; use core::convert::From; use core::iter::Extend; From e843da59eec1e0a92357e70fbdf266ad9e95977c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:26:57 +0100 Subject: [PATCH 328/569] Cargo fmt. --- src/sign/authnext/mod.rs | 8 ++++---- src/sign/mod.rs | 4 ++-- src/sign/signing/traits.rs | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/sign/authnext/mod.rs b/src/sign/authnext/mod.rs index 5169ca053..d6f539b0d 100644 --- a/src/sign/authnext/mod.rs +++ b/src/sign/authnext/mod.rs @@ -1,20 +1,20 @@ //! Authenticated non-existence mechanisms. -//! +//! //! In order for a DNSSEC server to deny the existence of a RRSET of the //! requested type or name the server must have an RRSIG signature that it can //! include in the response to authenticate it. -//! +//! //! However, an RRSIG signs an existing RRSET in a zone, it cannot sign a //! non-existing RRSET. DNSSEC signers must therefore add records to the zone //! that describe the record types and names that DO exist, which a server can //! use to determine non-existence and which can signed providing an RRSIG to //! authenticate the response. -//! +//! //! This module provides implementations of the zone signing related logic for //! the NSEC ([RFC 4034]) and NSEC3 ([RFC 5155]) mechanisms which can be used //! during DNSSEC zone signing to add this missing information to the zone //! prior to signing. -//! +//! //! [RFC 4034]: https://www.rfc-editor.org/info/rfc4034 //! [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 pub mod config; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f0218b556..0c31260c7 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -298,9 +298,9 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] +pub mod authnext; pub mod crypto; pub mod error; -pub mod authnext; pub mod keys; pub mod records; pub mod signing; @@ -324,13 +324,13 @@ use crate::base::{CanonicalOrd, ToName}; use crate::base::{Name, Record, Rtype}; use crate::rdata::ZoneRecordData; -use error::SigningError; use authnext::config::HashingConfig; use authnext::nsec::generate_nsecs; use authnext::nsec3::{ generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, Nsec3Records, }; +use error::SigningError; use keys::keymeta::DesignatedSigningKey; use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index e9a14af99..847ea7fc6 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -17,8 +17,8 @@ use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; use crate::rdata::ZoneRecordData; -use crate::sign::error::{SignError, SigningError}; use crate::sign::authnext::nsec3::Nsec3HashProvider; +use crate::sign::error::{SignError, SigningError}; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, From 5a2959e9504a2172220064f3d5ff383b9f9586df Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:27:55 +0100 Subject: [PATCH 329/569] Rename the authnext module to authnonext which doesn't sound like the word "next". --- src/sign/{authnext => authnonext}/config.rs | 0 src/sign/{authnext => authnonext}/mod.rs | 0 src/sign/{authnext => authnonext}/nsec.rs | 0 src/sign/{authnext => authnonext}/nsec3.rs | 0 src/sign/mod.rs | 8 ++++---- src/sign/signing/config.rs | 4 ++-- src/sign/signing/traits.rs | 4 +++- 7 files changed, 9 insertions(+), 7 deletions(-) rename src/sign/{authnext => authnonext}/config.rs (100%) rename src/sign/{authnext => authnonext}/mod.rs (100%) rename src/sign/{authnext => authnonext}/nsec.rs (100%) rename src/sign/{authnext => authnonext}/nsec3.rs (100%) diff --git a/src/sign/authnext/config.rs b/src/sign/authnonext/config.rs similarity index 100% rename from src/sign/authnext/config.rs rename to src/sign/authnonext/config.rs diff --git a/src/sign/authnext/mod.rs b/src/sign/authnonext/mod.rs similarity index 100% rename from src/sign/authnext/mod.rs rename to src/sign/authnonext/mod.rs diff --git a/src/sign/authnext/nsec.rs b/src/sign/authnonext/nsec.rs similarity index 100% rename from src/sign/authnext/nsec.rs rename to src/sign/authnonext/nsec.rs diff --git a/src/sign/authnext/nsec3.rs b/src/sign/authnonext/nsec3.rs similarity index 100% rename from src/sign/authnext/nsec3.rs rename to src/sign/authnonext/nsec3.rs diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 0c31260c7..b476d6b4c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -298,7 +298,7 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] -pub mod authnext; +pub mod authnonext; pub mod crypto; pub mod error; pub mod keys; @@ -324,9 +324,9 @@ use crate::base::{CanonicalOrd, ToName}; use crate::base::{Name, Record, Rtype}; use crate::rdata::ZoneRecordData; -use authnext::config::HashingConfig; -use authnext::nsec::generate_nsecs; -use authnext::nsec3::{ +use authnonext::config::HashingConfig; +use authnonext::nsec::generate_nsecs; +use authnonext::nsec3::{ generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, Nsec3Records, }; diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs index 160ba7879..6410e8e7a 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/signing/config.rs @@ -3,8 +3,8 @@ use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; use crate::base::{Name, ToName}; -use crate::sign::authnext::config::HashingConfig; -use crate::sign::authnext::nsec3::{ +use crate::sign::authnonext::config::HashingConfig; +use crate::sign::authnonext::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; use crate::sign::records::{DefaultSorter, Sorter}; diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 847ea7fc6..7b0a2897b 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -17,7 +17,7 @@ use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; use crate::rdata::ZoneRecordData; -use crate::sign::authnext::nsec3::Nsec3HashProvider; +use crate::sign::authnonext::nsec3::Nsec3HashProvider; use crate::sign::error::{SignError, SigningError}; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ @@ -75,6 +75,8 @@ pub trait SortedExtend where Sort: Sorter, { + + fn sorted_extend< T: IntoIterator>>, >( From d22880a9bdd1bbdb6fbe7776f04a791d63219bd8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:30:01 +0100 Subject: [PATCH 330/569] Rename authnonext to denial as ext is not really a good abbreviation of non-existence, and the full term is authenticated denial of existence. --- src/sign/{authnonext => denial}/config.rs | 0 src/sign/{authnonext => denial}/mod.rs | 0 src/sign/{authnonext => denial}/nsec.rs | 0 src/sign/{authnonext => denial}/nsec3.rs | 0 src/sign/mod.rs | 8 ++++---- src/sign/signing/config.rs | 4 ++-- src/sign/signing/traits.rs | 4 +--- 7 files changed, 7 insertions(+), 9 deletions(-) rename src/sign/{authnonext => denial}/config.rs (100%) rename src/sign/{authnonext => denial}/mod.rs (100%) rename src/sign/{authnonext => denial}/nsec.rs (100%) rename src/sign/{authnonext => denial}/nsec3.rs (100%) diff --git a/src/sign/authnonext/config.rs b/src/sign/denial/config.rs similarity index 100% rename from src/sign/authnonext/config.rs rename to src/sign/denial/config.rs diff --git a/src/sign/authnonext/mod.rs b/src/sign/denial/mod.rs similarity index 100% rename from src/sign/authnonext/mod.rs rename to src/sign/denial/mod.rs diff --git a/src/sign/authnonext/nsec.rs b/src/sign/denial/nsec.rs similarity index 100% rename from src/sign/authnonext/nsec.rs rename to src/sign/denial/nsec.rs diff --git a/src/sign/authnonext/nsec3.rs b/src/sign/denial/nsec3.rs similarity index 100% rename from src/sign/authnonext/nsec3.rs rename to src/sign/denial/nsec3.rs diff --git a/src/sign/mod.rs b/src/sign/mod.rs index b476d6b4c..8a6b92f0c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -298,8 +298,8 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] -pub mod authnonext; pub mod crypto; +pub mod denial; pub mod error; pub mod keys; pub mod records; @@ -324,9 +324,9 @@ use crate::base::{CanonicalOrd, ToName}; use crate::base::{Name, Record, Rtype}; use crate::rdata::ZoneRecordData; -use authnonext::config::HashingConfig; -use authnonext::nsec::generate_nsecs; -use authnonext::nsec3::{ +use denial::config::HashingConfig; +use denial::nsec::generate_nsecs; +use denial::nsec3::{ generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, Nsec3Records, }; diff --git a/src/sign/signing/config.rs b/src/sign/signing/config.rs index 6410e8e7a..4633dead8 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/signing/config.rs @@ -3,8 +3,8 @@ use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; use crate::base::{Name, ToName}; -use crate::sign::authnonext::config::HashingConfig; -use crate::sign::authnonext::nsec3::{ +use crate::sign::denial::config::HashingConfig; +use crate::sign::denial::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; use crate::sign::records::{DefaultSorter, Sorter}; diff --git a/src/sign/signing/traits.rs b/src/sign/signing/traits.rs index 7b0a2897b..7797211ca 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/signing/traits.rs @@ -17,7 +17,7 @@ use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; use crate::rdata::ZoneRecordData; -use crate::sign::authnonext::nsec3::Nsec3HashProvider; +use crate::sign::denial::nsec3::Nsec3HashProvider; use crate::sign::error::{SignError, SigningError}; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::records::{ @@ -75,8 +75,6 @@ pub trait SortedExtend where Sort: Sorter, { - - fn sorted_extend< T: IntoIterator>>, >( From f4899e16d25d2cd8bb8e6d2fd2f41bf36cd8aa73 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:45:44 +0100 Subject: [PATCH 331/569] Move SigningConfig and signing::traits to the top of the sign module as they relate to signing in general, and rename the signing sub-module as it is specific to signature generation. --- src/sign/{signing => }/config.rs | 5 ++-- src/sign/keys/mod.rs | 1 + src/sign/mod.rs | 24 +++++++++++--------- src/sign/signatures/mod.rs | 3 +++ src/sign/{signing => signatures}/rrsigs.rs | 4 ++-- src/sign/{signing => signatures}/strategy.rs | 0 src/sign/signing/mod.rs | 6 ----- src/sign/{signing => }/traits.rs | 6 ++--- 8 files changed, 24 insertions(+), 25 deletions(-) rename src/sign/{signing => }/config.rs (94%) create mode 100644 src/sign/signatures/mod.rs rename src/sign/{signing => signatures}/rrsigs.rs (99%) rename src/sign/{signing => signatures}/strategy.rs (100%) delete mode 100644 src/sign/signing/mod.rs rename src/sign/{signing => }/traits.rs (98%) diff --git a/src/sign/signing/config.rs b/src/sign/config.rs similarity index 94% rename from src/sign/signing/config.rs rename to src/sign/config.rs index 4633dead8..0e7555d39 100644 --- a/src/sign/signing/config.rs +++ b/src/sign/config.rs @@ -2,17 +2,16 @@ use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; +use super::signatures::strategy::DefaultSigningKeyUsageStrategy; use crate::base::{Name, ToName}; use crate::sign::denial::config::HashingConfig; use crate::sign::denial::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; use crate::sign::records::{DefaultSorter, Sorter}; -use crate::sign::signing::strategy::SigningKeyUsageStrategy; +use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::SignRaw; -use super::strategy::DefaultSigningKeyUsageStrategy; - //------------ SigningConfig ------------------------------------------------- /// Signing configuration for a DNSSEC signed zone. diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index 5d9c57ddf..c06dcd5a3 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -1,3 +1,4 @@ +//! Types for working with DNSSEC signing keys. pub mod bytes; pub mod keymeta; pub mod keyset; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 8a6b92f0c..6c3fa49e1 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -107,7 +107,7 @@ //! # use domain::sign::crypto::common::GenerateParams; //! # use domain::sign::crypto::common::KeyPair; //! # use domain::sign::keys::SigningKey; -//! # use domain::sign::signing::traits::SignRaw; +//! # use domain::sign::traits::SignRaw; //! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); //! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); //! # let key = SigningKey::new(Name::>::root(), 257, key_pair); @@ -147,7 +147,7 @@ //! use domain::rdata::dnssec::Timestamp; //! use domain::sign::keys::DnssecSigningKey; //! use domain::sign::records::SortedRecords; -//! use domain::sign::signing::SigningConfig; +//! use domain::sign::SigningConfig; //! //! // Create a sorted collection of records. //! // @@ -181,7 +181,7 @@ //! // Then generate the records which when added to the zone make it signed. //! let mut signer_generated_records = SortedRecords::default(); //! { -//! use domain::sign::signing::traits::SignableZone; +//! use domain::sign::traits::SignableZone; //! records.sign_zone( //! &mut signing_config, //! &keys, @@ -191,7 +191,7 @@ //! // Or if desired and the underlying collection supports it, sign the zone //! // in-place. //! { -//! use domain::sign::signing::traits::SignableZoneInPlace; +//! use domain::sign::traits::SignableZoneInPlace; //! records.sign_zone(&mut signing_config, &keys).unwrap(); //! } //! ``` @@ -215,8 +215,8 @@ //! # let key = SigningKey::new(root, 257, key_pair); //! # let keys = [DnssecSigningKey::from(key)]; //! # let mut records = SortedRecords::default(); -//! use domain::sign::signing::traits::Signable; -//! use domain::sign::signing::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; +//! use domain::sign::traits::Signable; +//! use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; //! let apex = Name::>::root(); //! let rrset = Rrset::new(&records); //! let generated_records = rrset.sign::(&apex, &keys).unwrap(); @@ -298,16 +298,19 @@ #![cfg(feature = "unstable-sign")] #![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] +pub mod config; pub mod crypto; pub mod denial; pub mod error; pub mod keys; pub mod records; -pub mod signing; +pub mod signatures; +pub mod traits; pub mod zone; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; +pub use self::config::SigningConfig; pub use self::keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; use core::cmp::min; @@ -336,10 +339,9 @@ use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, }; use records::{RecordsIter, Sorter}; -use signing::config::SigningConfig; -use signing::rrsigs::generate_rrsigs; -use signing::strategy::SigningKeyUsageStrategy; -use signing::traits::{SignRaw, SignableZone, SortedExtend}; +use signatures::rrsigs::generate_rrsigs; +use signatures::strategy::SigningKeyUsageStrategy; +use traits::{SignRaw, SignableZone, SortedExtend}; //------------ SignableZoneInOut --------------------------------------------- diff --git a/src/sign/signatures/mod.rs b/src/sign/signatures/mod.rs new file mode 100644 index 000000000..b2750ef45 --- /dev/null +++ b/src/sign/signatures/mod.rs @@ -0,0 +1,3 @@ +//! Signature generation. +pub mod rrsigs; +pub mod strategy; diff --git a/src/sign/signing/rrsigs.rs b/src/sign/signatures/rrsigs.rs similarity index 99% rename from src/sign/signing/rrsigs.rs rename to src/sign/signatures/rrsigs.rs index f84295044..3fe95abdd 100644 --- a/src/sign/signing/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -24,8 +24,8 @@ use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::keys::signingkey::SigningKey; use crate::sign::records::{RecordsIter, Rrset, SortedRecords, Sorter}; -use crate::sign::signing::strategy::SigningKeyUsageStrategy; -use crate::sign::signing::traits::{SignRaw, SortedExtend}; +use crate::sign::signatures::strategy::SigningKeyUsageStrategy; +use crate::sign::traits::{SignRaw, SortedExtend}; /// Generate RRSIG RRs for a collection of unsigned zone records. /// diff --git a/src/sign/signing/strategy.rs b/src/sign/signatures/strategy.rs similarity index 100% rename from src/sign/signing/strategy.rs rename to src/sign/signatures/strategy.rs diff --git a/src/sign/signing/mod.rs b/src/sign/signing/mod.rs deleted file mode 100644 index bcf7b4427..000000000 --- a/src/sign/signing/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod config; -pub mod rrsigs; -pub mod strategy; -pub mod traits; - -pub use self::config::SigningConfig; diff --git a/src/sign/signing/traits.rs b/src/sign/traits.rs similarity index 98% rename from src/sign/signing/traits.rs rename to src/sign/traits.rs index 7797211ca..ff669ece3 100644 --- a/src/sign/signing/traits.rs +++ b/src/sign/traits.rs @@ -24,9 +24,9 @@ use crate::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::sign_zone; -use crate::sign::signing::config::SigningConfig; -use crate::sign::signing::rrsigs::generate_rrsigs; -use crate::sign::signing::strategy::SigningKeyUsageStrategy; +use crate::sign::signatures::rrsigs::generate_rrsigs; +use crate::sign::signatures::strategy::SigningKeyUsageStrategy; +use crate::sign::SigningConfig; use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; //----------- SignRaw -------------------------------------------------------- From 495cc96e84236f30e9a7d93799520842c46f77fb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:47:27 +0100 Subject: [PATCH 332/569] Delete empty sign::zone sub-module. --- src/sign/mod.rs | 1 - src/sign/traits.rs | 1 + src/sign/zone.rs | 1 - 3 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 src/sign/zone.rs diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 6c3fa49e1..ba46107e4 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -306,7 +306,6 @@ pub mod keys; pub mod records; pub mod signatures; pub mod traits; -pub mod zone; pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; diff --git a/src/sign/traits.rs b/src/sign/traits.rs index ff669ece3..7c6e98e4e 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -1,3 +1,4 @@ +//! Signing related traits. use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; diff --git a/src/sign/zone.rs b/src/sign/zone.rs deleted file mode 100644 index 8b1378917..000000000 --- a/src/sign/zone.rs +++ /dev/null @@ -1 +0,0 @@ - From 501ae94c2bd38e1dc90f4f0efc5e8d7eefc5abac Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:47:54 +0100 Subject: [PATCH 333/569] Minor RustDoc tweak. --- src/sign/denial/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/denial/mod.rs b/src/sign/denial/mod.rs index d6f539b0d..8939356e7 100644 --- a/src/sign/denial/mod.rs +++ b/src/sign/denial/mod.rs @@ -1,4 +1,4 @@ -//! Authenticated non-existence mechanisms. +//! Authenticated denial of existence mechanisms. //! //! In order for a DNSSEC server to deny the existence of a RRSET of the //! requested type or name the server must have an RRSIG signature that it can From 2f415a8876c5fc414aa9f847843f4d1da1dac3ca Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:22:05 +0100 Subject: [PATCH 334/569] Add RustDoc for the `sign_zone()` function. --- src/sign/config.rs | 1 + src/sign/mod.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/src/sign/config.rs b/src/sign/config.rs index 0e7555d39..7e834310f 100644 --- a/src/sign/config.rs +++ b/src/sign/config.rs @@ -1,3 +1,4 @@ +//! Types for tuning configurable aspects of DNSSEC signing. use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index ba46107e4..e155a7224 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -480,7 +480,57 @@ where /// DNSSEC sign an unsigned zone using the given configuration and keys. /// -/// Given an input zone +/// An implementation of [RFC 4035 section 2 Zone Signing] with optional +/// support for NSEC3 ([RFC 5155]), i.e. it will generate `DNSKEY` (if +/// configured), `NSEC` or `NSEC3` (and if NSEC3 is in use then also +/// `NSEC3PARAM`), and `RRSIG` records. +/// +/// Signing can either be done in-place (records generated by signing will be +/// added to the record collection being signed) or into some other provided +/// record collection (records generated by signing will be added to the other +/// collection, the record collection being signed will remain untouched). +/// +/// The record collection to be signed is required to implement the +/// [`SortedExtend`] trait, implementations of which are provided for the +/// [`SortedRecords`] and [`Vec`] types. +/// +///
+/// +/// The record collection to be signed must meet the following requiements. +/// +/// Failure to meet these requirements will likely lead to incorrect signing +/// output. +/// +/// 1. The record collection to be signed **MUST** be ordered according to +/// [`CanonicalOrd`]. +/// 2. The record collection to be signed **MUST** be unsigned, i.e. must not +/// contain `DNSKEY`, `NSEC`, `NSEC3`, `NSEC3PARAM`, or `RRSIG` records. +/// +/// [`SortedRecords`] will be sorted at all times and thus is safe to use with +/// this function. [`Vec`] however is safe to use **ONLY IF** the content has +/// been sorted prior to calling this function. +/// +/// This function does **NOT** yet support re-signing, i.e. re-generating +/// expired `RRSIG` signatures, updating the NSEC(3) chain to match added or +/// removed records or adding signatures for another key to an already signed +/// zone e.g. to support key rollover. For the latter case it does however +/// support providing multiple sets of key to sign with the +/// [`SigningKeyUsageStrategy`] implementation being used to determine which +/// keys to use to sign which records. +/// +/// This function does **NOT** yet support signing with multiple NSEC(3) +/// configurations at once, e.g. to migrate from NSEC <-> NSEC3 or between +/// NSEC3 configurations. +/// +///
+/// +/// Various aspects of the signing process are configurable, see +/// [`SigningConfig`] for more information. +/// +/// [RFC 4035 section 2 Zone Signing]: +/// https://www.rfc-editor.org/rfc/rfc4035.html#section-2 +/// [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 +/// [`SortedRecords`]: crate::sign::records::SortedRecords pub fn sign_zone( mut in_out: SignableZoneInOut, signing_config: &mut SigningConfig, From d724fce47b8f60bf0750b3cdc176c26c145fcb25 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:29:19 +0100 Subject: [PATCH 335/569] More `sign_zone()` RustDoc. --- src/sign/mod.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e155a7224..603de12c8 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -522,15 +522,23 @@ where /// configurations at once, e.g. to migrate from NSEC <-> NSEC3 or between /// NSEC3 configurations. /// +/// This function does **NOT** yet support signing of record collections +/// stored in the [`Zone`] type as it currently only supports signing of +/// record slices whereas the records in a [`Zone`] currently only supports a +/// visitor style read interface via [`ReadableZone`] whereby a callback +/// function is invoked for each node that is "walked". +/// /// /// /// Various aspects of the signing process are configurable, see /// [`SigningConfig`] for more information. /// +/// [`ReadableZone`]: crate::zonetree::ReadableZone /// [RFC 4035 section 2 Zone Signing]: /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2 /// [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 /// [`SortedRecords`]: crate::sign::records::SortedRecords +/// [`Zone`]: crate::zonetree::Zone pub fn sign_zone( mut in_out: SignableZoneInOut, signing_config: &mut SigningConfig, From 1db62206e76cb622e854d4b5bc8f3cbde10c6ef8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:30:36 +0100 Subject: [PATCH 336/569] Typo correction. --- src/sign/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 603de12c8..5394da0b2 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -496,7 +496,7 @@ where /// ///
/// -/// The record collection to be signed must meet the following requiements. +/// The record collection to be signed must meet the following requirements. /// /// Failure to meet these requirements will likely lead to incorrect signing /// output. From e8375ee3a22de15029fb2e910c83c4c41d44cca3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:33:04 +0100 Subject: [PATCH 337/569] Typo correction. --- src/sign/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 5394da0b2..10edbc4a6 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -523,10 +523,10 @@ where /// NSEC3 configurations. /// /// This function does **NOT** yet support signing of record collections -/// stored in the [`Zone`] type as it currently only supports signing of -/// record slices whereas the records in a [`Zone`] currently only supports a -/// visitor style read interface via [`ReadableZone`] whereby a callback -/// function is invoked for each node that is "walked". +/// stored in the [`Zone`] type as it currently only support signing of record +/// slices whereas the records in a [`Zone`] currently only supports a visitor +/// style read interface via [`ReadableZone`] whereby a callback function is +/// invoked for each node that is "walked". /// ///
/// From 78b48eb1e1da96b4906c94fac643726f4d81a22f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:39:31 +0100 Subject: [PATCH 338/569] RustDoc correction. --- src/sign/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 10edbc4a6..f1adf8304 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -490,9 +490,14 @@ where /// record collection (records generated by signing will be added to the other /// collection, the record collection being signed will remain untouched). /// +/// Prefer signing via the [`SignableZone`] or [`SignableZoneInPlace`] traits +/// as they handle the construction of the [`SignableZoneInOut`] type and +/// calling of this function for you. +/// /// The record collection to be signed is required to implement the -/// [`SortedExtend`] trait, implementations of which are provided for the -/// [`SortedRecords`] and [`Vec`] types. +/// [`SignableZone`] trait. The collection to extend with generated records is +/// required to implement the [`SortedExtend`] trait, implementations of which +/// are provided for the [`SortedRecords`] and [`Vec`] types. /// ///
/// @@ -537,6 +542,7 @@ where /// [RFC 4035 section 2 Zone Signing]: /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2 /// [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 +/// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace /// [`SortedRecords`]: crate::sign::records::SortedRecords /// [`Zone`]: crate::zonetree::Zone pub fn sign_zone( From 5a82490e75647a9da8d0edb6dbb948aeacb8764a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:43:13 +0100 Subject: [PATCH 339/569] Cargo fmt. --- src/sign/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f1adf8304..373235000 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -493,7 +493,7 @@ where /// Prefer signing via the [`SignableZone`] or [`SignableZoneInPlace`] traits /// as they handle the construction of the [`SignableZoneInOut`] type and /// calling of this function for you. -/// +/// /// The record collection to be signed is required to implement the /// [`SignableZone`] trait. The collection to extend with generated records is /// required to implement the [`SortedExtend`] trait, implementations of which From 5dd9a6f0a16d30cc10fdbf9cf7dd6b9df33bb570 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 16 Jan 2025 23:41:46 +0100 Subject: [PATCH 340/569] More RustDoc. --- src/sign/crypto/mod.rs | 101 +++++++++++- src/sign/keys/keyset.rs | 4 +- src/sign/keys/mod.rs | 31 ++++ src/sign/mod.rs | 357 +++++++++------------------------------- src/sign/traits.rs | 142 ++++++++++++++++ 5 files changed, 355 insertions(+), 280 deletions(-) diff --git a/src/sign/crypto/mod.rs b/src/sign/crypto/mod.rs index ca4a0488d..e6dd0cefd 100644 --- a/src/sign/crypto/mod.rs +++ b/src/sign/crypto/mod.rs @@ -1,4 +1,103 @@ -//! Cryptographic backends. +//! Cryptographic backends, key generation and import. +//! +//! 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 (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 ([`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 the [`SignRaw`] trait. +//! +//! While each cryptographic backend can support a limited number of signature +//! algorithms, even the types independent of a cryptographic backend (e.g. +//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of +//! algorithms. Even with custom cryptographic backends, this module can only +//! support these algorithms. +//! +//! # Importing keys +//! +//! Keys can be imported from files stored on disk in the conventional BIND +//! format. +//! +//! ``` +//! # use domain::base::iana::SecAlg; +//! # use domain::validate; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::{SecretKeyBytes, SigningKey}; +//! // Load an Ed25519 key named 'Ktest.+015+56037'. +//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; +//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); +//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); +//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); +//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); +//! +//! // Associate the key with important metadata. +//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); +//! +//! // Check that the owner, algorithm, and key tag matched expectations. +//! assert_eq!(key.owner().to_string(), "test"); +//! assert_eq!(key.algorithm(), SecAlg::ED25519); +//! assert_eq!(key.public_key().key_tag(), 56037); +//! ``` +//! +//! # Generating keys +//! +//! Keys can also be generated. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::SigningKey; +//! // Generate a new Ed25519 key. +//! let params = GenerateParams::Ed25519; +//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! +//! // Associate the key with important metadata. +//! let owner: Name> = "www.example.org.".parse().unwrap(); +//! let flags = 257; // key signing key +//! let key = SigningKey::new(owner, flags, key_pair); +//! +//! // Access the public key (with metadata). +//! let pub_key = key.public_key(); +//! println!("{:?}", pub_key); +//! ``` +//! +//! # Signing data +//! +//! Given some data and a key, the data can be signed with the key. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::SigningKey; +//! # use domain::sign::traits::SignRaw; +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let key = SigningKey::new(Name::>::root(), 257, key_pair); +//! // Sign arbitrary byte sequences with the key. +//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); +//! println!("{:?}", sig); +//! ``` +//! +//! [`SignRaw`]: crate::sign::traits::SignRaw +//! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams +//! [`SecretKeyBytes`]: crate::sign::keys::SecretKeyBytes pub mod common; pub mod openssl; pub mod ring; diff --git a/src/sign/keys/keyset.rs b/src/sign/keys/keyset.rs index 96edee6f3..4ebeef87c 100644 --- a/src/sign/keys/keyset.rs +++ b/src/sign/keys/keyset.rs @@ -1,7 +1,7 @@ //! Maintain the state of a collection keys used to sign a zone. //! -//! A key set is a collection of keys used to sign a sigle zone. -//! This module support the management of key sets including key rollover. +//! A key set is a collection of keys used to sign a sigle zone. This module +//! supports the management of key sets including key rollover. //! //! # Example //! diff --git a/src/sign/keys/mod.rs b/src/sign/keys/mod.rs index c06dcd5a3..fd297de85 100644 --- a/src/sign/keys/mod.rs +++ b/src/sign/keys/mod.rs @@ -1,4 +1,35 @@ //! Types for working with DNSSEC signing keys. +//! +//! # Importing and Exporting +//! +//! The [`SecretKeyBytes`] type is a generic representation of a secret key as +//! a byte slice. While it does not offer any cryptographic functionality, it +//! is useful to transfer secret keys stored in memory, independent of any +//! cryptographic backend. +//! +//! The `KeyPair` types of the cryptographic backends in this module each +//! support a `from_bytes()` function that parses the generic representation +//! into a functional cryptographic key. Importantly, these functions require +//! both the public and private keys to be provided -- the pair are verified +//! for consistency. In some cases, it may also be possible to serialize an +//! existing cryptographic key back to the generic bytes representation. +//! +//! [`SecretKeyBytes`] also supports importing and exporting keys from and to +//! the conventional private-key format popularized by BIND. This format is +//! used by a variety of tools for storing DNSSEC keys on disk. See the +//! type-level documentation for a specification of the format. +//! +//! # Key Sets and Key Lifetime +//! +//! The [`keyset`] module provides a way to keep track of the collection of +//! keys that are used to sign a particular zone. In addition, the lifetime of +//! keys can be maintained using key rolls that phase out old keys and +//! introduce new keys. +//! +//! # Signing keys +//! +//! + pub mod bytes; pub mod keymeta; pub mod keyset; diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 373235000..fc83f4c02 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -2,295 +2,98 @@ //! //! **This module is experimental and likely to change significantly.** //! -//! This module provides support for DNSSEC signing of zones. -//! -//! DNSSEC signed zones consist of configuration data such as DNSKEY and -//! NSEC3PARAM records, NSEC(3) chains used to provably deny the existence of -//! records, and RRSIG signatures that authenticate the authoritative content -//! of the zone. -//! -//! # Overview -//! -//! This module provides support for working with DNSSEC signing keys and -//! using them to DNSSEC sign sorted [`Record`] collections via the traits -//! [`SignableZone`], [`SignableZoneInPlace`] and [`Signable`]. -//! -//!
-//! -//! This module does **NOT** yet support signing of records stored in a -//! [`Zone`]. -//! -//!
-//! -//! 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. `KeyPair`). -//! -//! While all records in a zone can be signed with a single key, it is useful -//! to use one key, a Key Signing Key (KSK), _"to sign the apex DNSKEY RRset -//! in a zone"_ and another key, a Zone Signing Key (ZSK), _"to sign all the -//! RRsets in a zone that require signatures, other than the apex DNSKEY -//! RRset"_ (see [RFC 6781 section 3.1]). -//! -//! Cryptographically there is no difference between these key types, they are -//! assigned by the operator to signal their intended usage. This module -//! provides the [`DnssecSigningKey`] wrapper type around a [`SigningKey`] to -//! allow the intended usage of the key to be signalled by the operator, and -//! [`SigningKeyUsageStrategy`] to allow different key usage strategies to be -//! defined and selected to influence how the different types of key affect -//! signing. -//! -//! # Importing keys -//! -//! Keys can be imported from files stored on disk in the conventional BIND -//! format. -//! -//! ``` -//! # use domain::base::iana::SecAlg; -//! # use domain::validate; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::{SecretKeyBytes, SigningKey}; -//! // Load an Ed25519 key named 'Ktest.+015+56037'. -//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; -//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); -//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); -//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); -//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); -//! -//! // Parse the key into Ring or OpenSSL. -//! let key_pair = KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); -//! -//! // Associate the key with important metadata. -//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); -//! -//! // Check that the owner, algorithm, and key tag matched expectations. -//! assert_eq!(key.owner().to_string(), "test"); -//! assert_eq!(key.algorithm(), SecAlg::ED25519); -//! assert_eq!(key.public_key().key_tag(), 56037); -//! ``` -//! -//! # Generating keys -//! -//! Keys can also be generated. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! // Generate a new Ed25519 key. -//! let params = GenerateParams::Ed25519; -//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); -//! -//! // Parse the key into Ring or OpenSSL. -//! let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! -//! // Associate the key with important metadata. -//! let owner: Name> = "www.example.org.".parse().unwrap(); -//! let flags = 257; // key signing key -//! let key = SigningKey::new(owner, flags, key_pair); -//! -//! // Access the public key (with metadata). -//! let pub_key = key.public_key(); -//! println!("{:?}", pub_key); -//! ``` -//! -//! # Low level signing -//! -//! Given some data and a key, the data can be signed with the key. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! # use domain::sign::traits::SignRaw; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let key = SigningKey::new(Name::>::root(), 257, key_pair); -//! // Sign arbitrary byte sequences with the key. -//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); -//! println!("{:?}", sig); -//! ``` -//! -//! # High level signing -//! -//! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is -//! implemented, invoke [`sign_zone()`] on the type to generate, or in the -//! case of [`SignableZoneInPlace`] to add, all records needed to sign the -//! zone, i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. -//! -//!
-//! -//! This module does **NOT** yet support re-signing of a zone, i.e. ensuring -//! that any changes to the authoritative records in the zone are reflected by -//! updating the NSEC(3) chain and generating additional signatures or -//! regenerating existing ones that have expired. -//! -//!
-//! -//! ``` -//! # use domain::base::{Name, Record, Serial, Ttl}; -//! # use domain::base::iana::Class; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let root = Name::>::root(); -//! # let key = SigningKey::new(root.clone(), 257, key_pair); -//! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; -//! use domain::rdata::dnssec::Timestamp; -//! use domain::sign::keys::DnssecSigningKey; -//! use domain::sign::records::SortedRecords; -//! use domain::sign::SigningConfig; -//! -//! // Create a sorted collection of records. -//! // -//! // Note: You can also use a plain Vec here (or any other type that is -//! // compatible with the SignableZone or SignableZoneInPlace trait bounds) -//! // but then you are responsible for ensuring that records in the zone are -//! // in DNSSEC compatible order, e.g. by calling -//! // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. -//! let mut records = SortedRecords::default(); -//! -//! // Insert records into the collection. Just a dummy SOA for this example. -//! let soa = ZoneRecordData::Soa(Soa::new( -//! root.clone(), -//! root.clone(), -//! Serial::now(), -//! Ttl::ZERO, -//! Ttl::ZERO, -//! Ttl::ZERO, -//! Ttl::ZERO)); -//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); -//! -//! // Generate or import signing keys (see above). -//! -//! // Assign signature validity period and operator intent to the keys. -//! let key = key.with_validity(Timestamp::now(), Timestamp::now()); -//! let keys = [DnssecSigningKey::from(key)]; -//! -//! // Create a signing configuration. -//! let mut signing_config = SigningConfig::default(); -//! -//! // Then generate the records which when added to the zone make it signed. -//! let mut signer_generated_records = SortedRecords::default(); -//! { -//! use domain::sign::traits::SignableZone; -//! records.sign_zone( -//! &mut signing_config, -//! &keys, -//! &mut signer_generated_records).unwrap(); -//! } -//! -//! // Or if desired and the underlying collection supports it, sign the zone -//! // in-place. -//! { -//! use domain::sign::traits::SignableZoneInPlace; -//! records.sign_zone(&mut signing_config, &keys).unwrap(); -//! } -//! ``` -//! -//! If needed, individual RRsets can also be signed but note that this will -//! **only** generate `RRSIG` records, as `NSEC(3)` generation is currently -//! only supported for the zone as a whole and `DNSKEY` records are only -//! generated for the apex of a zone. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::base::iana::Class; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::{DnssecSigningKey, SigningKey}; -//! # use domain::sign::records::{Rrset, SortedRecords}; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let root = Name::>::root(); -//! # let key = SigningKey::new(root, 257, key_pair); -//! # let keys = [DnssecSigningKey::from(key)]; -//! # let mut records = SortedRecords::default(); -//! use domain::sign::traits::Signable; -//! use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; -//! let apex = Name::>::root(); -//! let rrset = Rrset::new(&records); -//! let generated_records = rrset.sign::(&apex, &keys).unwrap(); -//! ``` -//! -//! # Cryptography -//! -//! 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 (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 ([`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`]. -//! -//!
-//! -//! This module does **NOT** yet support `async` signing (useful for -//! interacting with cryptographic hardware like HSMs). -//! -//!
-//! -//! While each cryptographic backend can support a limited number of signature -//! algorithms, even the types independent of a cryptographic backend (e.g. -//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of -//! algorithms. Even with custom cryptographic backends, this module can only -//! support these algorithms. -//! -//! # Importing and Exporting -//! -//! The [`SecretKeyBytes`] type is a generic representation of a secret key as -//! a byte slice. While it does not offer any cryptographic functionality, it -//! is useful to transfer secret keys stored in memory, independent of any -//! cryptographic backend. -//! -//! The `KeyPair` types of the cryptographic backends in this module each -//! support a `from_bytes()` function that parses the generic representation -//! into a functional cryptographic key. Importantly, these functions require -//! both the public and private keys to be provided -- the pair are verified -//! for consistency. In some cases, it may also be possible to serialize an -//! existing cryptographic key back to the generic bytes representation. -//! -//! [`SecretKeyBytes`] also supports importing and exporting keys from and to -//! the conventional private-key format popularized by BIND. This format is -//! used by a variety of tools for storing DNSSEC keys on disk. See the -//! type-level documentation for a specification of the format. -//! -//! # Key Sets and Key Lifetime -//! -//! The [`keyset`] module provides a way to keep track of the collection of -//! keys that are used to sign a particular zone. In addition, the lifetime of -//! keys can be maintained using key rolls that phase out old keys and -//! introduce new keys. +//! This module provides support for DNSSEC ([RFC 9364]) signing of zones and +//! managing of the keys used for signing, with support for `NSEC3` ([RFC +//! 5155]), [RFC 9077] compliant `NSEC(3)` TTLs, and [RFC 9276] compliant +//! `NSEC3` parameter settings. +//! +//! # Background +//! +//! DNSSEC signed zones are normal DNS zones (i.e. records at the apex such as +//! the `SOA` and `NS` records, records in the zone such as `A` records, and +//! delegations to child zones). What makes them different to non-DNSSEC +//! signed zones is that they also contain additional configuration data such +//! as `DNSKEY` and `NSEC3PARAM` records, a chain of `NSEC(3)` records used to +//! provably deny the existence of records, and `RRSIG` signatures that +//! authenticate the authoritative content of the zone. See "Signed Zone" in +//! [RFC 9499 section 10] for more information. +//! +//! Signatures are generated using private keys produced by cryptographic +//! algorithms in pairs, a public key and a private key. +//! +//! In a DNSSEC signed zone each generated signature covers a single resource +//! record set (a group of records having the same owner name, class and type) +//! and is stored in an `RRSIG` record under the same owner name in the zone. +//! These `RRSIG` records can then be validated later by a resolver using the +//! public key. +//! +//! Private keys must be stored in a safe location by zone signers while +//! public keys can be stored in `DNSKEY` records in public DNS zones. +//! Validating resolvers query the `DNSKEY`s in order to validate `RRSIG`s in +//! the signed zone and thus authenticate the data which the `RRSIG`s cover. +//! `DNSKEY` records can be trusted because a chain of trust is established +//! from a trust anchor to the signed zone with each parent zone in the chain +//! authenticating the public key used to sign a child zone. A `DS` record in +//! the parent zone that refers to a `DNSKEY` record in the child zone +//! establishes this link. +//! +//! For increased security keys are rotated (aka "rolled") over time. This key +//! rolling has to be carefully orchestrated so that at all times the signed +//! zone which the key belongs to remains valid from the perspective of +//! resolvers. +//! +//! # Usage +//! +//! - To generate and/or import signing keys see the [`crypto`] module. +//! - To sign a collection of [`Record`]s that represent a zone see the +//! [`SignableZone`] trait. +//! - To manage the life cycle of signing keys see the [`keyset`] module. +//! +//! # Advanced usage +//! +//! - For more control over the signing process see the [`SigningConfig`] type +//! and the [`SigningKeyUsageStrategy`] and [`DnssecSigningKey`] traits. +//! - For additional ways to sign zones see the [`SignableZoneInPlace`] trait +//! and the [`sign_zone()`] function. +//! - To invoke specific stages of the signing process manually see the +//! [`Signable`] trait and the [`generate_nsecs()`], [`generate_nsec3s()`], +//! [`generate_rrsigs()`] and [`sign_rrset()`] functions. +//! - To generate signatures for arbitrary data see the [`SignRaw`] trait. +//! +//! # Known limitations +//! +//! This module does not yet support : +//! - `async` signing (useful for interacting with cryptographic hardware like +//! Hardware Security Modules (HSMs)). +//! - Re-signing an already signed zone, only unsigned zones can be signed. +//! - Signing of unsorted zones, record collections must be sorted according +//! to [`CanonicalOrd`]. +//! - Signing of [`Zone`] types or via an [`core::iter::Iterator`] over +//! [`Record`]s, only signing of slices is supported. +//! - Signing with both `NSEC` and `NSEC3` or multiple `NSEC3` configurations +//! at once. //! //! [`common`]: crate::sign::crypto::common //! [`keyset`]: crate::sign::keys::keyset //! [`openssl`]: crate::sign::crypto::openssl //! [`ring`]: crate::sign::crypto::ring +//! [`sign_rrset()`]: crate::sign::signatures::sign_rrsets //! [`DnssecSigningKey`]: crate::sign::keys::DnssecSigningKey //! [`Record`]: crate::base::record::Record +//! [RFC 5155]: https://rfc-editor.org/rfc/rfc5155 //! [RFC 6781 section 3.1]: https://rfc-editor.org/rfc/rfc6781#section-3.1 +//! [RFC 9077]: https://rfc-editor.org/rfc/rfc9077 +//! [RFC 9276]: https://rfc-editor.org/rfc/rfc9276 +//! [RFC 9364]: https://rfc-editor.org/rfc/rfc9364 +//! [RFC 9499 section 10]: +//! https://www.rfc-editor.org/rfc/rfc9499.html#section-10 //! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams //! [`KeyPair`]: crate::sign::crypto::common::KeyPair //! [`SigningKeyUsageStrategy`]: //! crate::sign::signing::strategy::SigningKeyUsageStrategy -//! [`Signable`]: crate::sign::signing::traits::Signable -//! [`SignableZone`]: crate::sign::signing::traits::SignableZone -//! [`SignableZoneInPlace`]: crate::sign::signing::traits::SignableZoneInPlace +//! [`Signable`]: crate::sign::traits::Signable +//! [`SignableZone`]: crate::sign::traits::SignableZone +//! [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace //! [`SigningKey`]: crate::sign::keys::SigningKey //! [`SortedRecords`]: crate::sign::SortedRecords //! [`Zone`]: crate::zonetree::Zone diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 7c6e98e4e..213985c25 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -1,4 +1,110 @@ //! Signing related traits. +//! +//! # High level signing +//! +//! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is +//! implemented, invoke [`sign_zone()`] on the type to generate, or in the +//! case of [`SignableZoneInPlace`] to add, all records needed to sign the +//! zone, i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. +//! +//!
+//! +//! This module does **NOT** yet support re-signing of a zone, i.e. ensuring +//! that any changes to the authoritative records in the zone are reflected by +//! updating the NSEC(3) chain and generating additional signatures or +//! regenerating existing ones that have expired. +//! +//!
+//! +//! ``` +//! # use domain::base::{Name, Record, Serial, Ttl}; +//! # use domain::base::iana::Class; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::SigningKey; +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let root = Name::>::root(); +//! # let key = SigningKey::new(root.clone(), 257, key_pair); +//! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; +//! use domain::rdata::dnssec::Timestamp; +//! use domain::sign::keys::DnssecSigningKey; +//! use domain::sign::records::SortedRecords; +//! use domain::sign::SigningConfig; +//! +//! // Create a sorted collection of records. +//! // +//! // Note: You can also use a plain Vec here (or any other type that is +//! // compatible with the SignableZone or SignableZoneInPlace trait bounds) +//! // but then you are responsible for ensuring that records in the zone are +//! // in DNSSEC compatible order, e.g. by calling +//! // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. +//! let mut records = SortedRecords::default(); +//! +//! // Insert records into the collection. Just a dummy SOA for this example. +//! let soa = ZoneRecordData::Soa(Soa::new( +//! root.clone(), +//! root.clone(), +//! Serial::now(), +//! Ttl::ZERO, +//! Ttl::ZERO, +//! Ttl::ZERO, +//! Ttl::ZERO)); +//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); +//! +//! // Generate or import signing keys (see above). +//! +//! // Assign signature validity period and operator intent to the keys. +//! let key = key.with_validity(Timestamp::now(), Timestamp::now()); +//! let keys = [DnssecSigningKey::from(key)]; +//! +//! // Create a signing configuration. +//! let mut signing_config = SigningConfig::default(); +//! +//! // Then generate the records which when added to the zone make it signed. +//! let mut signer_generated_records = SortedRecords::default(); +//! { +//! use domain::sign::traits::SignableZone; +//! records.sign_zone( +//! &mut signing_config, +//! &keys, +//! &mut signer_generated_records).unwrap(); +//! } +//! +//! // Or if desired and the underlying collection supports it, sign the zone +//! // in-place. +//! { +//! use domain::sign::traits::SignableZoneInPlace; +//! records.sign_zone(&mut signing_config, &keys).unwrap(); +//! } +//! ``` +//! +//! If needed, individual RRsets can also be signed but note that this will +//! **only** generate `RRSIG` records, as `NSEC(3)` generation is currently +//! only supported for the zone as a whole and `DNSKEY` records are only +//! generated for the apex of a zone. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::base::iana::Class; +//! # use domain::sign::crypto::common; +//! # use domain::sign::crypto::common::GenerateParams; +//! # use domain::sign::crypto::common::KeyPair; +//! # use domain::sign::keys::{DnssecSigningKey, SigningKey}; +//! # use domain::sign::records::{Rrset, SortedRecords}; +//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! # let root = Name::>::root(); +//! # let key = SigningKey::new(root, 257, key_pair); +//! # let keys = [DnssecSigningKey::from(key)]; +//! # let mut records = SortedRecords::default(); +//! use domain::sign::traits::Signable; +//! use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; +//! let apex = Name::>::root(); +//! let rrset = Rrset::new(&records); +//! let generated_records = rrset.sign::(&apex, &keys).unwrap(); +//! ``` use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; @@ -134,6 +240,13 @@ where //------------ SignableZone -------------------------------------------------- +/// DNSSEC sign an unsigned zone using the given configuration and keys. +/// +/// Types that implement this trait can be signed using the trait provided +/// [`sign_zone()`] function with records generated by signing being appended +/// to the given `out` record collection. +/// +/// [`sign_zone()`]: SignableZone::sign_zone pub trait SignableZone: Deref>]> where @@ -151,6 +264,11 @@ where // TODO // fn iter_mut(&mut self) -> T; + /// DNSSEC sign an unsigned zone using the given configuration and keys. + /// + /// This function is a convenience wrapper around calling + /// [`crate::sign::sign_zone()`] function with enum variant + /// [`SignableZoneInOut::SignInto`]. fn sign_zone( &self, signing_config: &mut SigningConfig< @@ -186,6 +304,10 @@ where } } +/// DNSSEC sign an unsigned zone using the given configuration and keys. +/// +/// Implemented for any type that dereferences to `[Record>]`. impl SignableZone for T where N: Clone @@ -211,6 +333,14 @@ where //------------ SignableZoneInPlace ------------------------------------------- +/// DNSSEC sign an unsigned zone in-place using the given configuration and +/// keys. +/// +/// Types that implement this trait can be signed using the trait provided +/// [`sign_zone()`] function with records generated by signing being appended +/// to the record collection being signed. +/// +/// [`sign_zone()`]: SignableZoneInPlace::sign_zone pub trait SignableZoneInPlace: SignableZone + SortedExtend where @@ -226,6 +356,12 @@ where Self: SortedExtend + Sized, Sort: Sorter, { + /// DNSSEC sign an unsigned zone in-place using the given configuration + /// and keys. + /// + /// This function is a convenience wrapper around calling + /// [`crate::sign::sign_zone()`] function with enum variant + /// [`SignableZoneInOut::SignInPlace`]. fn sign_zone( &mut self, signing_config: &mut SigningConfig< @@ -258,6 +394,12 @@ where //--- impl SignableZoneInPlace for SortedRecords +/// DNSSEC sign an unsigned zone in-place using the given configuration and +/// keys. +/// +/// Implemented for any type that dereferences to `[Record>]` and which implements the [`SortedExtend`] +/// trait. impl SignableZoneInPlace for T where N: Clone From 51f83522210b8ac117ae058f6d167fff67707cbc Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:19:31 +0100 Subject: [PATCH 341/569] Log each signed RRSET at trace level, not debug level. --- src/sign/signatures/rrsigs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 3fe95abdd..4b2f57d68 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -10,7 +10,7 @@ use std::vec::Vec; use octseq::builder::{EmptyBuilder, FromBuilder}; use octseq::{OctetsFrom, OctetsInto}; -use tracing::{debug, enabled, Level}; +use tracing::{debug, enabled, trace, Level}; use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Class, Rtype}; @@ -251,7 +251,7 @@ where let rrsig_rr = sign_rrset(key, &rrset, &name, expected_apex, &mut buf)?; res.push(rrsig_rr); - debug!( + trace!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", rrset.iter().len(), rrset.rtype(), From 2d961d38d512a5a9824d2d359985dad023db2c8e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:21:54 +0100 Subject: [PATCH 342/569] Remove unnecessary owner_name argument from sign_rrset(), don't require all callers to supply a scratch buffer by making sign_rrset() a thin wrapper around new sign_rrset_in() which has the code that was previously sign_rrset(), and add RustDoc. --- src/sign/signatures/rrsigs.rs | 85 ++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 4b2f57d68..4ae58c751 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -137,7 +137,7 @@ where } let mut res: Vec>> = Vec::new(); - let mut buf = Vec::new(); + let mut reusable_scratch = Vec::new(); let mut cut: Option = None; let mut records = records.peekable(); @@ -245,11 +245,12 @@ where }; for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { - // A copy of the owner name. We’ll need it later. - let name = apex_owner_rrs.owner().clone(); - - let rrsig_rr = - sign_rrset(key, &rrset, &name, expected_apex, &mut buf)?; + let rrsig_rr = sign_rrset_in( + key, + &rrset, + expected_apex, + &mut reusable_scratch, + )?; res.push(rrsig_rr); trace!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", @@ -307,8 +308,12 @@ where for key in non_dnskey_signing_key_idxs.iter().map(|&idx| &keys[idx]) { - let rrsig_rr = - sign_rrset(key, &rrset, &name, expected_apex, &mut buf)?; + let rrsig_rr = sign_rrset_in( + key, + &rrset, + expected_apex, + &mut reusable_scratch, + )?; res.push(rrsig_rr); debug!( "Signed {} RRSET at {} with keytag {}", @@ -325,12 +330,57 @@ where Ok(res) } +/// Generate `RRSIG` records for a given RRset. +/// +/// See [`sign_rrset_in()`]. +/// +/// If signing multiple RRsets, calling [`sign_rrset_in()`] directly will be +/// more efficient as you can allocate the scratch buffer once and re-use it +/// across multiple calls. pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, - rrset_owner: &N, apex_owner: &N, - buf: &mut Vec, +) -> Result>, SigningError> +where + N: ToName + Clone + Send, + D: RecordData + + ComposeRecordData + + From> + + CanonicalOrd + + Send, + Inner: SignRaw, + Octs: AsRef<[u8]> + OctetsFrom>, +{ + sign_rrset_in(key, rrset, apex_owner, &mut vec![]) +} + +/// Generate `RRSIG` records for a given RRset. +/// +/// This function generating one or more `RRSIG` records for the given RRset +/// based on the given signing keys, according to the rules defined in [RFC +/// 4034 section 3] _"The RRSIG Resource Record"_, [RFC 4035 section 2.2] +/// _"Including RRSIG RRs in a Zone"_ and [RFC 6840 section 5.11] _"Mandatory +/// Algorithm Rules"_. +/// +/// No checks are done on the given signing key, any key with any algorithm, +/// apex owner and flags may be used to sign the given RRset. +/// +/// When signing multiple RRsets by calling this function multiple times, the +/// `scratch` buffer parameter can be allocated once and re-used for each call +/// to avoid needing to allocate the buffer for each call. +/// +/// [RFC 4034 section 3]: +/// https://www.rfc-editor.org/rfc/rfc4034.html#section-3 +/// [RFC 4035 section 2.2]: +/// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 +/// [RFC 6840 section 5.11]: +/// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 +pub fn sign_rrset_in( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + apex_owner: &N, + scratch: &mut Vec, ) -> Result>, SigningError> where N: ToName + Clone + Send, @@ -356,7 +406,7 @@ where let rrsig = ProtoRrsig::new( rrset.rtype(), key.algorithm(), - rrset_owner.rrsig_label_count(), + rrset.owner().rrsig_label_count(), rrset.ttl(), expiration, inception, @@ -372,19 +422,22 @@ where // `compose_canonical()` below will take care of that. apex_owner.clone(), ); - buf.clear(); - rrsig.compose_canonical(buf).unwrap(); + + scratch.clear(); + + rrsig.compose_canonical(scratch).unwrap(); for record in rrset.iter() { - record.compose_canonical(buf).unwrap(); + record.compose_canonical(scratch).unwrap(); } - let signature = key.raw_secret_key().sign_raw(&*buf)?; + let signature = key.raw_secret_key().sign_raw(&*scratch)?; let signature = signature.as_ref().to_vec(); let Ok(signature) = signature.try_octets_into() else { return Err(SigningError::OutOfMemory); }; + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); Ok(Record::new( - rrset_owner.clone(), + rrset.owner().clone(), rrset.class(), rrset.ttl(), ZoneRecordData::Rrsig(rrsig), From 73e1e780bddab32b6a6addf7c64deac89cfecef7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:22:43 +0100 Subject: [PATCH 343/569] Reject attempts to sign an RRSIG RRset. (a) they should never be signed, and (b) they should not form RRsets. --- src/sign/error.rs | 13 +++++++++++++ src/sign/signatures/rrsigs.rs | 8 ++++++++ 2 files changed, 21 insertions(+) diff --git a/src/sign/error.rs b/src/sign/error.rs index c5f458d5e..5265ca2dd 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -32,6 +32,15 @@ pub enum SigningError { // TODO Nsec3HashingError(Nsec3HashError), + /// TODO + /// + /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 + /// 2.2. Including RRSIG RRs in a Zone + /// ... + /// "An RRSIG RR itself MUST NOT be signed" + RrsigRrsMustNotBeSigned, + + // TODO SigningError(SignError), } @@ -55,6 +64,10 @@ impl Display for SigningError { SigningError::Nsec3HashingError(err) => { f.write_fmt(format_args!("NSEC3 hashing error: {err}")) } + SigningError::RrsigRrsMustNotBeSigned => f.write_str( + "RFC 4035 violation: RRSIG RRs MUST NOT be signed", + ), + SigningError::InvalidSignatureValidityPeriod => { SigningError::SigningError(err) => { f.write_fmt(format_args!("Signing error: {err}")) } diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 4ae58c751..e2a2923f2 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -392,6 +392,14 @@ where Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "An RRSIG RR itself MUST NOT be signed" + if rrset.rtype() == Rtype::RRSIG { + return Err(SigningError::RrsigRrsMustNotBeSigned); + } + let (inception, expiration) = key .signature_validity_period() .ok_or(SigningError::NoSignatureValidityPeriodProvided)? From 6d613770d0c9b1b4951e531ce4c721c448d79b5d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:23:36 +0100 Subject: [PATCH 344/569] Reject invalid signature validity periods in sign_rrset(). --- src/sign/error.rs | 4 ++++ src/sign/signatures/rrsigs.rs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/sign/error.rs b/src/sign/error.rs index 5265ca2dd..b26a47249 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -40,6 +40,8 @@ pub enum SigningError { /// "An RRSIG RR itself MUST NOT be signed" RrsigRrsMustNotBeSigned, + // TODO + InvalidSignatureValidityPeriod, // TODO SigningError(SignError), @@ -68,6 +70,8 @@ impl Display for SigningError { "RFC 4035 violation: RRSIG RRs MUST NOT be signed", ), SigningError::InvalidSignatureValidityPeriod => { + f.write_str("RFC 4034 violation: RRSIG validity period is invalid") + } SigningError::SigningError(err) => { f.write_fmt(format_args!("Signing error: {err}")) } diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index e2a2923f2..8879dd420 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -404,6 +404,11 @@ where .signature_validity_period() .ok_or(SigningError::NoSignatureValidityPeriodProvided)? .into_inner(); + + if expiration < inception { + return Err(SigningError::InvalidSignatureValidityPeriod); + } + // RFC 4034 // 3. The RRSIG Resource Record // "The TTL value of an RRSIG RR MUST match the TTL value of the RRset From 3644ca4d06862c5a9fa203ba4fa5c5d73403ce66 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:23:46 +0100 Subject: [PATCH 345/569] Typo fix in error message. --- src/sign/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index b26a47249..4a1a55b7c 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -61,7 +61,7 @@ impl Display for SigningError { f.write_str("No suitable keys found") } SigningError::SoaRecordCouldNotBeDetermined => { - f.write_str("nNo apex SOA or too many apex SOA records found") + f.write_str("No apex SOA or too many apex SOA records found") } SigningError::Nsec3HashingError(err) => { f.write_fmt(format_args!("NSEC3 hashing error: {err}")) From 0ab62945aee48f7505942c1e381023248189acce Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:24:01 +0100 Subject: [PATCH 346/569] Add a TODO comment. --- src/sign/signatures/rrsigs.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 8879dd420..742293117 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -430,6 +430,9 @@ where // DNS name compression on the Signer's Name field when transmitting a // RRSIG RR.". // + // TODO: However, is this inefficient? The RFC requires it to be + // SENT uncompressed, but doesn't ban storing it in compressed from? + // // We don't need to make sure here that the signer name is in // canonical form as required by RFC 4034 as the call to // `compose_canonical()` below will take care of that. From 4fdf5a5207381b16128600c6b270be696ef6741b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:24:31 +0100 Subject: [PATCH 347/569] Add a debug time assert in sign_rrset() checking the label counts per RFC 4034. --- src/sign/signatures/rrsigs.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 742293117..80a15d57b 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -452,6 +452,16 @@ where }; let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + debug_assert!( + (rrsig.labels() as usize) < rrset.owner().iter_labels().count() + ); + Ok(Record::new( rrset.owner().clone(), rrset.class(), From 01e6b594e474a8cf44aed92692d6a1f195b8d312 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:24:50 +0100 Subject: [PATCH 348/569] Add some RFC 4035 and 4035 based tests of sign_rrset(). --- src/sign/signatures/rrsigs.rs | 282 ++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 80a15d57b..07dc5dbab 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -469,3 +469,285 @@ where ZoneRecordData::Rrsig(rrsig), )) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::base::iana::SecAlg; + use crate::base::{Serial, Ttl}; + use crate::rdata::dnssec::Timestamp; + use crate::rdata::{Rrsig, A}; + use crate::sign::error::SignError; + use crate::sign::{PublicKeyBytes, Signature}; + use bytes::Bytes; + use core::str::FromStr; + use core::u32; + + struct TestKey; + + impl SignRaw for TestKey { + fn algorithm(&self) -> SecAlg { + SecAlg::PRIVATEDNS + } + + fn raw_public_key(&self) -> PublicKeyBytes { + PublicKeyBytes::Ed25519([0_u8; 32].into()) + } + + fn sign_raw(&self, _data: &[u8]) -> Result { + Ok(Signature::Ed25519([0u8; 64].into())) + } + } + + #[test] + fn rrset_sign_adheres_to_rules_in_rfc_4034_and_rfc_4035() { + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "For example, "www.example.com." has a Labels field value of 3" + // We can use any class as RRSIGs are class independent. + let records = [mk_record( + "www.example.com.", + Class::CH, + 12345, + ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), + )]; + let rrset = Rrset::new(&records); + + let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + let ZoneRecordData::Rrsig(rrsig) = rrsig_rr.data() else { + unreachable!(); + }; + + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // "For each authoritative RRset in a signed zone, there MUST be at + // least one RRSIG record that meets the following requirements: + // + // o The RRSIG owner name is equal to the RRset owner name. + assert_eq!(rrsig_rr.owner(), rrset.owner()); + // + // o The RRSIG class is equal to the RRset class. + assert_eq!(rrsig_rr.class(), rrset.class()); + // + // o The RRSIG Type Covered field is equal to the RRset type. + // + assert_eq!(rrsig.type_covered(), rrset.rtype()); + // o The RRSIG Original TTL field is equal to the TTL of the + // RRset. + // + assert_eq!(rrsig.original_ttl(), rrset.ttl()); + // o The RRSIG RR's TTL is equal to the TTL of the RRset. + // + assert_eq!(rrsig_rr.ttl(), rrset.ttl()); + // o The RRSIG Labels field is equal to the number of labels in + // the RRset owner name, not counting the null root label and + // not counting the leftmost label if it is a wildcard. + assert_eq!(rrsig.labels(), 3); + // o The RRSIG Signer's Name field is equal to the name of the + // zone containing the RRset. + // + assert_eq!(rrsig.signer_name(), &apex_owner); + // o The RRSIG Algorithm, Signer's Name, and Key Tag fields + // identify a zone key DNSKEY record at the zone apex." + // ^^^ This is outside the control of the rrset_sign() function. + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + assert!((rrsig.labels() as usize) < rrset.owner().label_count()); + } + + #[test] + fn rrtest_sign_wildcard() { + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // ""*.example.com." has a Labels field value of 2" + // We can use any class as RRSIGs are class independent. + let records = [mk_record( + "*.example.com.", + Class::CH, + 12345, + ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), + )]; + let rrset = Rrset::new(&records); + + let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + let ZoneRecordData::Rrsig(rrsig) = rrsig_rr.data() else { + unreachable!(); + }; + + assert_eq!(rrsig.labels(), 2); + } + + #[test] + fn sign_rrset_must_not_sign_rrsigs() { + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "An RRSIG RR itself MUST NOT be signed" + + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + + let dummy_rrsig = Rrsig::new( + Rtype::A, + SecAlg::PRIVATEDNS, + 0, + Ttl::default(), + 0.into(), + 0.into(), + 0, + Name::root(), + Bytes::new(), + ) + .unwrap(); + + let records = [mk_record( + "any.", + Class::CH, + 12345, + ZoneRecordData::Rrsig(dummy_rrsig), + )]; + let rrset = Rrset::new(&records); + + let res = sign_rrset(&key, &rrset, &apex_owner); + assert_eq!(res, Err(SigningError::RrsigRrsMustNotBeSigned)); + } + + #[test] + fn sign_rrsets_check_validity_period_handling() { + // RFC 4034 + // 3.1.5. Signature Expiration and Inception Fields + // ... + // "The Signature Expiration and Inception field values specify a + // date and time in the form of a 32-bit unsigned number of seconds + // elapsed since 1 January 1970 00:00:00 UTC, ignoring leap + // seconds, in network byte order. The longest interval that can + // be expressed by this format without wrapping is approximately + // 136 years. An RRSIG RR can have an Expiration field value that + // is numerically smaller than the Inception field value if the + // expiration field value is near the 32-bit wrap-around point or + // if the signature is long lived. Because of this, all + // comparisons involving these fields MUST use "Serial number + // arithmetic", as defined in [RFC1982]. As a direct consequence, + // the values contained in these fields cannot refer to dates more + // than 68 years in either the past or the future." + + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + + let records = [mk_record( + "any.", + Class::CH, + 12345, + ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), + )]; + let rrset = Rrset::new(&records); + + fn calc_timestamps( + start: u32, + duration: u32, + ) -> (Timestamp, Timestamp) { + let start_serial = Serial::from(start); + let end = Serial::from(start_serial).add(duration).into_int(); + (Timestamp::from(start), Timestamp::from(end)) + } + + // Good: Expiration > Inception. + let (inception, expiration) = calc_timestamps(5, 5); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Good: Expiration == Inception. + let (inception, expiration) = calc_timestamps(10, 0); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Bad: Expiration < Inception. + let (expiration, inception) = calc_timestamps(5, 10); + let key = key.with_validity(inception, expiration); + let res = sign_rrset(&key, &rrset, &apex_owner); + assert_eq!(res, Err(SigningError::InvalidSignatureValidityPeriod)); + + // Good: Expiration > Inception with Expiration near wrap around + // point. + let (inception, expiration) = calc_timestamps(u32::MAX - 10, 10); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Good: Expiration > Inception with Inception near wrap around point. + let (inception, expiration) = calc_timestamps(0, 10); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Good: Expiration > Inception with Exception crossing the wrap + // around point. + let (inception, expiration) = calc_timestamps(u32::MAX - 10, 20); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Good: Expiration - Inception == 68 years. + let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; + let (inception, expiration) = + calc_timestamps(0, sixty_eight_years_in_secs); + let key = key.with_validity(inception, expiration); + sign_rrset(&key, &rrset, &apex_owner).unwrap(); + + // Bad: Expiration - Inception > 68 years. + // + // I add a rather large amount (A year) because it's unclear where the + // boundary is from the approximate text in the quoted RFC. I think + // it's at 2^31 - 1 so from that you can see how much we need to add + // to cross the boundary: + // + // ``` + // 68 years = 68 * 365 * 24 * 60 * 60 = 2144448000 + // 2^31 - 1 = 2147483647 + // 69 years = 69 * 365 * 24 * 60 * 60 = 2175984000 + // ``` + // + // But as the RFC refers to "dates more than 68 years" a value of 69 + // years is fine to test with. + let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; + let one_year_in_secs = 1 * 365 * 24 * 60 * 60; + let (inception, expiration) = + calc_timestamps(sixty_eight_years_in_secs, one_year_in_secs); + let key = key.with_validity(inception, expiration); + + let key = key + .with_validity(Timestamp::from(0), Timestamp::from(expiration)); + let res = sign_rrset(&key, &rrset, &apex_owner); + assert_eq!(res, Err(SigningError::InvalidSignatureValidityPeriod)); + } + + //------------ Helper fns ------------------------------------------------ + + fn mk_record( + owner: &str, + class: Class, + ttl_secs: u32, + data: ZoneRecordData>, + ) -> Record, ZoneRecordData>> { + Record::new( + Name::from_str(owner).unwrap(), + class, + Ttl::from_secs(ttl_secs), + data, + ) + } +} From 4f155208e66b6322edbc8312fb8ef1021c34e381 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:25:12 +0100 Subject: [PATCH 349/569] RustDoc updates for the sign module. --- src/sign/mod.rs | 28 ++--- src/sign/traits.rs | 264 ++++++++++++++++++++++++++------------------- 2 files changed, 169 insertions(+), 123 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index fc83f4c02..7cbecf15c 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -9,17 +9,17 @@ //! //! # Background //! -//! DNSSEC signed zones are normal DNS zones (i.e. records at the apex such as -//! the `SOA` and `NS` records, records in the zone such as `A` records, and -//! delegations to child zones). What makes them different to non-DNSSEC -//! signed zones is that they also contain additional configuration data such -//! as `DNSKEY` and `NSEC3PARAM` records, a chain of `NSEC(3)` records used to -//! provably deny the existence of records, and `RRSIG` signatures that -//! authenticate the authoritative content of the zone. See "Signed Zone" in -//! [RFC 9499 section 10] for more information. +//! DNSSEC signed zones are normal DNS zones (i.e. with records at the apex +//! such as the `SOA` and `NS` records, records in the zone such as `A` +//! records, and delegations to child zones). What makes them different to +//! non-DNSSEC signed zones is that they also contain additional configuration +//! data such as `DNSKEY` and `NSEC3PARAM` records, a chain of `NSEC(3)` +//! records used to provably deny the existence of records, and `RRSIG` +//! signatures that authenticate the authoritative content of the zone. See +//! "Signed Zone" in [RFC 9499 section 10] for more information. //! -//! Signatures are generated using private keys produced by cryptographic -//! algorithms in pairs, a public key and a private key. +//! Signatures are generated using a private key and can be validated using +//! the corresponding public key. //! //! In a DNSSEC signed zone each generated signature covers a single resource //! record set (a group of records having the same owner name, class and type) @@ -46,15 +46,15 @@ //! //! - To generate and/or import signing keys see the [`crypto`] module. //! - To sign a collection of [`Record`]s that represent a zone see the -//! [`SignableZone`] trait. +//! [`SignableZoneInPlace`] trait. //! - To manage the life cycle of signing keys see the [`keyset`] module. //! //! # Advanced usage //! //! - For more control over the signing process see the [`SigningConfig`] type //! and the [`SigningKeyUsageStrategy`] and [`DnssecSigningKey`] traits. -//! - For additional ways to sign zones see the [`SignableZoneInPlace`] trait -//! and the [`sign_zone()`] function. +//! - For additional ways to sign zones see the [`SignableZone`] trait and the +//! [`sign_zone()`] function. //! - To invoke specific stages of the signing process manually see the //! [`Signable`] trait and the [`generate_nsecs()`], [`generate_nsec3s()`], //! [`generate_rrsigs()`] and [`sign_rrset()`] functions. @@ -77,7 +77,7 @@ //! [`keyset`]: crate::sign::keys::keyset //! [`openssl`]: crate::sign::crypto::openssl //! [`ring`]: crate::sign::crypto::ring -//! [`sign_rrset()`]: crate::sign::signatures::sign_rrsets +//! [`sign_rrset()`]: crate::sign::signatures::rrsigs::sign_rrset //! [`DnssecSigningKey`]: crate::sign::keys::DnssecSigningKey //! [`Record`]: crate::base::record::Record //! [RFC 5155]: https://rfc-editor.org/rfc/rfc5155 diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 213985c25..c0f4b5c68 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -1,110 +1,7 @@ //! Signing related traits. //! -//! # High level signing -//! -//! Given a type for which [`SignableZone`] or [`SignableZoneInPlace`] is -//! implemented, invoke [`sign_zone()`] on the type to generate, or in the -//! case of [`SignableZoneInPlace`] to add, all records needed to sign the -//! zone, i.e. `DNSKEY`, `NSEC` or `NSEC3PARAM` and `NSEC3`, and `RRSIG`. -//! -//!
-//! -//! This module does **NOT** yet support re-signing of a zone, i.e. ensuring -//! that any changes to the authoritative records in the zone are reflected by -//! updating the NSEC(3) chain and generating additional signatures or -//! regenerating existing ones that have expired. -//! -//!
-//! -//! ``` -//! # use domain::base::{Name, Record, Serial, Ttl}; -//! # use domain::base::iana::Class; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let root = Name::>::root(); -//! # let key = SigningKey::new(root.clone(), 257, key_pair); -//! use domain::rdata::{rfc1035::Soa, ZoneRecordData}; -//! use domain::rdata::dnssec::Timestamp; -//! use domain::sign::keys::DnssecSigningKey; -//! use domain::sign::records::SortedRecords; -//! use domain::sign::SigningConfig; -//! -//! // Create a sorted collection of records. -//! // -//! // Note: You can also use a plain Vec here (or any other type that is -//! // compatible with the SignableZone or SignableZoneInPlace trait bounds) -//! // but then you are responsible for ensuring that records in the zone are -//! // in DNSSEC compatible order, e.g. by calling -//! // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. -//! let mut records = SortedRecords::default(); -//! -//! // Insert records into the collection. Just a dummy SOA for this example. -//! let soa = ZoneRecordData::Soa(Soa::new( -//! root.clone(), -//! root.clone(), -//! Serial::now(), -//! Ttl::ZERO, -//! Ttl::ZERO, -//! Ttl::ZERO, -//! Ttl::ZERO)); -//! records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); -//! -//! // Generate or import signing keys (see above). -//! -//! // Assign signature validity period and operator intent to the keys. -//! let key = key.with_validity(Timestamp::now(), Timestamp::now()); -//! let keys = [DnssecSigningKey::from(key)]; -//! -//! // Create a signing configuration. -//! let mut signing_config = SigningConfig::default(); -//! -//! // Then generate the records which when added to the zone make it signed. -//! let mut signer_generated_records = SortedRecords::default(); -//! { -//! use domain::sign::traits::SignableZone; -//! records.sign_zone( -//! &mut signing_config, -//! &keys, -//! &mut signer_generated_records).unwrap(); -//! } -//! -//! // Or if desired and the underlying collection supports it, sign the zone -//! // in-place. -//! { -//! use domain::sign::traits::SignableZoneInPlace; -//! records.sign_zone(&mut signing_config, &keys).unwrap(); -//! } -//! ``` -//! -//! If needed, individual RRsets can also be signed but note that this will -//! **only** generate `RRSIG` records, as `NSEC(3)` generation is currently -//! only supported for the zone as a whole and `DNSKEY` records are only -//! generated for the apex of a zone. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::base::iana::Class; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::{DnssecSigningKey, SigningKey}; -//! # use domain::sign::records::{Rrset, SortedRecords}; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let root = Name::>::root(); -//! # let key = SigningKey::new(root, 257, key_pair); -//! # let keys = [DnssecSigningKey::from(key)]; -//! # let mut records = SortedRecords::default(); -//! use domain::sign::traits::Signable; -//! use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; -//! let apex = Name::>::root(); -//! let rrset = Rrset::new(&records); -//! let generated_records = rrset.sign::(&apex, &keys).unwrap(); -//! ``` +//! This module provides traits which can be used to simplify invocation of +//! [`crate::sign::sign_zone()`] for [`Record`] collection types. use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; @@ -243,9 +140,68 @@ where /// DNSSEC sign an unsigned zone using the given configuration and keys. /// /// Types that implement this trait can be signed using the trait provided -/// [`sign_zone()`] function with records generated by signing being appended -/// to the given `out` record collection. +/// [`sign_zone()`] function which will insert the generated records in order +/// (assuming that it correctly implements [`SortedExtend`]) into the given +/// `out` record collection. /// +/// # Example +/// +/// ``` +/// # use domain::base::{Name, Record, Serial, Ttl}; +/// # use domain::base::iana::Class; +/// # use domain::sign::crypto::common; +/// # use domain::sign::crypto::common::GenerateParams; +/// # use domain::sign::crypto::common::KeyPair; +/// # use domain::sign::keys::SigningKey; +/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +/// # let root = Name::>::root(); +/// # let key = SigningKey::new(root.clone(), 257, key_pair); +/// use domain::rdata::{rfc1035::Soa, ZoneRecordData}; +/// use domain::rdata::dnssec::Timestamp; +/// use domain::sign::keys::DnssecSigningKey; +/// use domain::sign::records::SortedRecords; +/// use domain::sign::traits::SignableZone; +/// use domain::sign::SigningConfig; +/// +/// // Create a sorted collection of records. +/// // +/// // Note: You can also use a plain Vec here (or any other type that is +/// // compatible with the SignableZone or SignableZoneInPlace trait bounds) +/// // but then you are responsible for ensuring that records in the zone are +/// // in DNSSEC compatible order, e.g. by calling +/// // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. +/// let mut records = SortedRecords::default(); +/// +/// // Insert records into the collection. Just a dummy SOA for this example. +/// let soa = ZoneRecordData::Soa(Soa::new( +/// root.clone(), +/// root.clone(), +/// Serial::now(), +/// Ttl::ZERO, +/// Ttl::ZERO, +/// Ttl::ZERO, +/// Ttl::ZERO)); +/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); +/// +/// // Generate or import signing keys (see above). +/// +/// // Assign signature validity period and operator intent to the keys. +/// let key = key.with_validity(Timestamp::now(), Timestamp::now()); +/// let keys = [DnssecSigningKey::from(key)]; +/// +/// // Create a signing configuration. +/// let mut signing_config = SigningConfig::default(); +/// +/// // Then generate the records which when added to the zone make it signed. +/// let mut signer_generated_records = SortedRecords::default(); +/// +/// records.sign_zone( +/// &mut signing_config, +/// &keys, +/// &mut signer_generated_records).unwrap(); +/// ``` +/// /// [`sign_zone()`]: SignableZone::sign_zone pub trait SignableZone: Deref>]> @@ -337,9 +293,63 @@ where /// keys. /// /// Types that implement this trait can be signed using the trait provided -/// [`sign_zone()`] function with records generated by signing being appended -/// to the record collection being signed. +/// [`sign_zone()`] function which will insert the generated records in order +/// (assuming that it correctly implements [`SortedExtend`]) into the +/// collection being signed. /// +/// # Example +/// +/// ``` +/// # use domain::base::{Name, Record, Serial, Ttl}; +/// # use domain::base::iana::Class; +/// # use domain::sign::crypto::common; +/// # use domain::sign::crypto::common::GenerateParams; +/// # use domain::sign::crypto::common::KeyPair; +/// # use domain::sign::keys::SigningKey; +/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +/// # let root = Name::>::root(); +/// # let key = SigningKey::new(root.clone(), 257, key_pair); +/// use domain::rdata::{rfc1035::Soa, ZoneRecordData}; +/// use domain::rdata::dnssec::Timestamp; +/// use domain::sign::keys::DnssecSigningKey; +/// use domain::sign::records::SortedRecords; +/// use domain::sign::traits::SignableZoneInPlace; +/// use domain::sign::SigningConfig; +/// +/// // Create a sorted collection of records. +/// // +/// // Note: You can also use a plain Vec here (or any other type that is +/// // compatible with the SignableZone or SignableZoneInPlace trait bounds) +/// // but then you are responsible for ensuring that records in the zone are +/// // in DNSSEC compatible order, e.g. by calling +/// // `sort_by(CanonicalOrd::canonical_cmp)` before calling `sign_zone()`. +/// let mut records = SortedRecords::default(); +/// +/// // Insert records into the collection. Just a dummy SOA for this example. +/// let soa = ZoneRecordData::Soa(Soa::new( +/// root.clone(), +/// root.clone(), +/// Serial::now(), +/// Ttl::ZERO, +/// Ttl::ZERO, +/// Ttl::ZERO, +/// Ttl::ZERO)); +/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); +/// +/// // Generate or import signing keys (see above). +/// +/// // Assign signature validity period and operator intent to the keys. +/// let key = key.with_validity(Timestamp::now(), Timestamp::now()); +/// let keys = [DnssecSigningKey::from(key)]; +/// +/// // Create a signing configuration. +/// let mut signing_config = SigningConfig::default(); +/// +/// // Then sign the zone in-place. +/// records.sign_zone(&mut signing_config, &keys).unwrap(); +/// ``` +/// /// [`sign_zone()`]: SignableZoneInPlace::sign_zone pub trait SignableZoneInPlace: SignableZone + SortedExtend @@ -426,6 +436,39 @@ where //------------ Signable ------------------------------------------------------ +/// A trait for generating DNSSEC signatures for one or more [`Record`]s. +/// +/// Unlike [`SignableZone`] this trait is intended to be implemented by types +/// that represent one or more [`Record`]s that together do **NOT** constitute +/// a full DNS zone, specifically collections that lack the zone apex records. +/// +/// Functions offered by this trait will **only** generate `RRSIG` records. +/// Other DNSSEC record types such as `NSEC(3)` and `DNSKEY` can only be +/// generated in the context of a full zone and so will **NOT** be generated +/// by the functions offered by this trait. +/// +/// # Example +/// +/// ``` +/// # use domain::base::Name; +/// # use domain::base::iana::Class; +/// # use domain::sign::crypto::common; +/// # use domain::sign::crypto::common::GenerateParams; +/// # use domain::sign::crypto::common::KeyPair; +/// # use domain::sign::keys::{DnssecSigningKey, SigningKey}; +/// # use domain::sign::records::{Rrset, SortedRecords}; +/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +/// # let root = Name::>::root(); +/// # let key = SigningKey::new(root, 257, key_pair); +/// # let keys = [DnssecSigningKey::from(key)]; +/// # let mut records = SortedRecords::default(); +/// use domain::sign::traits::Signable; +/// use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; +/// let apex = Name::>::root(); +/// let rrset = Rrset::new(&records); +/// let generated_records = rrset.sign::(&apex, &keys).unwrap(); +/// ``` pub trait Signable where N: ToName @@ -447,6 +490,9 @@ where { fn owner_rrs(&self) -> RecordsIter<'_, N, ZoneRecordData>; + /// Generate `RRSIG` records for this type. + /// + /// This function is a thin wrapper around [`generate_rrsigs()`]. #[allow(clippy::type_complexity)] fn sign( &self, From b15fab673232a008a9d91de2e860be5042ab3d9a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:25:15 +0100 Subject: [PATCH 350/569] Cargo fmt. --- src/sign/error.rs | 6 +++--- src/sign/traits.rs | 16 ++++++++-------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index 4a1a55b7c..59ab06b37 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -69,9 +69,9 @@ impl Display for SigningError { SigningError::RrsigRrsMustNotBeSigned => f.write_str( "RFC 4035 violation: RRSIG RRs MUST NOT be signed", ), - SigningError::InvalidSignatureValidityPeriod => { - f.write_str("RFC 4034 violation: RRSIG validity period is invalid") - } + SigningError::InvalidSignatureValidityPeriod => f.write_str( + "RFC 4034 violation: RRSIG validity period is invalid", + ), SigningError::SigningError(err) => { f.write_fmt(format_args!("Signing error: {err}")) } diff --git a/src/sign/traits.rs b/src/sign/traits.rs index c0f4b5c68..cfdcdff65 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -145,7 +145,7 @@ where /// `out` record collection. /// /// # Example -/// +/// /// ``` /// # use domain::base::{Name, Record, Serial, Ttl}; /// # use domain::base::iana::Class; @@ -201,7 +201,7 @@ where /// &keys, /// &mut signer_generated_records).unwrap(); /// ``` -/// +/// /// [`sign_zone()`]: SignableZone::sign_zone pub trait SignableZone: Deref>]> @@ -298,7 +298,7 @@ where /// collection being signed. /// /// # Example -/// +/// /// ``` /// # use domain::base::{Name, Record, Serial, Ttl}; /// # use domain::base::iana::Class; @@ -349,7 +349,7 @@ where /// // Then sign the zone in-place. /// records.sign_zone(&mut signing_config, &keys).unwrap(); /// ``` -/// +/// /// [`sign_zone()`]: SignableZoneInPlace::sign_zone pub trait SignableZoneInPlace: SignableZone + SortedExtend @@ -437,18 +437,18 @@ where //------------ Signable ------------------------------------------------------ /// A trait for generating DNSSEC signatures for one or more [`Record`]s. -/// +/// /// Unlike [`SignableZone`] this trait is intended to be implemented by types /// that represent one or more [`Record`]s that together do **NOT** constitute /// a full DNS zone, specifically collections that lack the zone apex records. -/// +/// /// Functions offered by this trait will **only** generate `RRSIG` records. /// Other DNSSEC record types such as `NSEC(3)` and `DNSKEY` can only be /// generated in the context of a full zone and so will **NOT** be generated /// by the functions offered by this trait. /// /// # Example -/// +/// /// ``` /// # use domain::base::Name; /// # use domain::base::iana::Class; @@ -491,7 +491,7 @@ where fn owner_rrs(&self) -> RecordsIter<'_, N, ZoneRecordData>; /// Generate `RRSIG` records for this type. - /// + /// /// This function is a thin wrapper around [`generate_rrsigs()`]. #[allow(clippy::type_complexity)] fn sign( From 6a173411f3ca1730cc96a413eeef41abdc7a7ca7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:37:16 +0100 Subject: [PATCH 351/569] Clippy. --- src/sign/signatures/rrsigs.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 07dc5dbab..b67f3fd3f 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -481,7 +481,6 @@ mod tests { use crate::sign::{PublicKeyBytes, Signature}; use bytes::Bytes; use core::str::FromStr; - use core::u32; struct TestKey; @@ -664,7 +663,7 @@ mod tests { duration: u32, ) -> (Timestamp, Timestamp) { let start_serial = Serial::from(start); - let end = Serial::from(start_serial).add(duration).into_int(); + let end = start_serial.add(duration).into_int(); (Timestamp::from(start), Timestamp::from(end)) } From 47760e88b0f75a4a0e61b8fa29b6a0384dc728b3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:38:13 +0100 Subject: [PATCH 352/569] Fix messed up test code. --- src/sign/signatures/rrsigs.rs | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index b67f3fd3f..9f5943947 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -723,13 +723,28 @@ mod tests { // But as the RFC refers to "dates more than 68 years" a value of 69 // years is fine to test with. let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; - let one_year_in_secs = 1 * 365 * 24 * 60 * 60; - let (inception, expiration) = - calc_timestamps(sixty_eight_years_in_secs, one_year_in_secs); - let key = key.with_validity(inception, expiration); + let one_year_in_secs = 365 * 24 * 60 * 60; - let key = key - .with_validity(Timestamp::from(0), Timestamp::from(expiration)); + // We can't use calc_timestamps() here because the underlying call to + // Serial::add() panics if the value to add is > 2^31 - 1. + // + // calc_timestamps(0, sixty_eight_years_in_secs + one_year_in_secs); + // + // But Timestamp doesn't care, we can construct those just fine. + // However when sign_rrset() compares the Timestamp inception and + // expiration values it will fail because the PartialOrd impl is + // implemented in terms of Serial which detects the wrap around. + // + // I think this is all good because RFC 4034 doesn't prevent creation + // and storage of an arbitrary 32-bit unsigned number of seconds as + // the inception or expiration value, it only mandates that "all + // comparisons involving these fields MUST use "Serial number + // arithmetic", as defined in [RFC1982]" + let (inception, expiration) = ( + Timestamp::from(0), + Timestamp::from(sixty_eight_years_in_secs + one_year_in_secs), + ); + let key = key.with_validity(inception, expiration); let res = sign_rrset(&key, &rrset, &apex_owner); assert_eq!(res, Err(SigningError::InvalidSignatureValidityPeriod)); } From 0e71ecda59bf3265a44fc36a43938e8cf18e5c08 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:44:44 +0100 Subject: [PATCH 353/569] Review feedback. --- src/sign/denial/nsec3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 3822ee995..a45c0ad74 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -74,7 +74,7 @@ where // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - // We also need the apex for the last NSEC. + // We also need the apex for the last NSEC3. let first_rr = records.first(); let apex_owner = first_rr.owner().clone(); let apex_label_count = apex_owner.iter_labels().count(); From 041c92fa1dfdba115f2828f1aae991de0131d3ca Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:47:38 +0100 Subject: [PATCH 354/569] Corrected a RustDoc comment. --- src/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 9f5943947..d651d6742 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1,4 +1,4 @@ -//! Actual signing. +//! DNSSEC RRSIG generation. use core::convert::From; use core::fmt::Display; use core::marker::Send; From b906e5327a5ae6d5b30b8b6739d10d91cc5d7875 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:49:54 +0100 Subject: [PATCH 355/569] Corrected a RustDoc comment. --- src/sign/signatures/rrsigs.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d651d6742..bf5d139ca 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -357,11 +357,10 @@ where /// Generate `RRSIG` records for a given RRset. /// -/// This function generating one or more `RRSIG` records for the given RRset -/// based on the given signing keys, according to the rules defined in [RFC -/// 4034 section 3] _"The RRSIG Resource Record"_, [RFC 4035 section 2.2] -/// _"Including RRSIG RRs in a Zone"_ and [RFC 6840 section 5.11] _"Mandatory -/// Algorithm Rules"_. +/// This function generates an `RRSIG` record for the given RRset based on the +/// given signing key, according to the rules defined in [RFC 4034 section 3] +/// _"The RRSIG Resource Record"_, [RFC 4035 section 2.2] _"Including RRSIG +/// RRs in a Zone"_ and [RFC 6840 section 5.11] _"Mandatory Algorithm Rules"_. /// /// No checks are done on the given signing key, any key with any algorithm, /// apex owner and flags may be used to sign the given RRset. From 287576e687c8444a89799627ccd73465ae34f36c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 12:59:42 +0100 Subject: [PATCH 356/569] Replace incorrect references to hashing which is only true for NSEC3, not for NSEC. --- src/sign/config.rs | 12 ++++++------ src/sign/denial/config.rs | 25 +++++++++++++++---------- src/sign/mod.rs | 16 ++++++++-------- 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/src/sign/config.rs b/src/sign/config.rs index 7e834310f..339b9198c 100644 --- a/src/sign/config.rs +++ b/src/sign/config.rs @@ -5,7 +5,7 @@ use octseq::{EmptyBuilder, FromBuilder}; use super::signatures::strategy::DefaultSigningKeyUsageStrategy; use crate::base::{Name, ToName}; -use crate::sign::denial::config::HashingConfig; +use crate::sign::denial::config::DenialConfig; use crate::sign::denial::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; @@ -30,8 +30,8 @@ pub struct SigningConfig< KeyStrat: SigningKeyUsageStrategy, Sort: Sorter, { - /// Hashing configuration. - pub hashing: HashingConfig, + /// Authenticated denial of existing mechanism configuration. + pub denial: DenialConfig, /// Should keys used to sign the zone be added as DNSKEY RRs? pub add_used_dnskeys: bool, @@ -49,11 +49,11 @@ where Sort: Sorter, { pub fn new( - hashing: HashingConfig, + denial: DenialConfig, add_used_dnskeys: bool, ) -> Self { Self { - hashing, + denial, add_used_dnskeys, _phantom: PhantomData, } @@ -77,7 +77,7 @@ where { fn default() -> Self { Self { - hashing: Default::default(), + denial: Default::default(), add_used_dnskeys: true, _phantom: Default::default(), } diff --git a/src/sign/denial/config.rs b/src/sign/denial/config.rs index a5717580e..ec501a745 100644 --- a/src/sign/denial/config.rs +++ b/src/sign/denial/config.rs @@ -63,25 +63,30 @@ pub enum Nsec3ToNsecTransitionState { Transitioned, } -//------------ HashingConfig ------------------------------------------------- +//------------ DenialConfig -------------------------------------------------- -/// Hashing configuration for a DNSSEC signed zone. +/// Authenticated denial of existence configuration for a DNSSEC signed zone. /// -/// A DNSSEC signed zone must be hashed, either by NSEC or NSEC3. +/// A DNSSEC signed zone must have either `NSEC` or `NSEC3` records to enable +/// the server to authenticate responses for names or record types that are +/// not present in the zone. +/// +/// This type can be used to choose which denial mechanism should be used when +/// DNSSEC signing a zone. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum HashingConfig> +pub enum DenialConfig> where HP: Nsec3HashProvider, O: AsRef<[u8]> + From<&'static [u8]>, { - /// The zone is already hashed. - Prehashed, + /// The zone already has the necessary NSEC(3) records. + AlreadyPresent, - /// The zone is NSEC hashed. + /// The zone already has NSEC records. #[default] Nsec, - /// The zone is NSEC3 hashed, possibly more than once. + /// The zone already has NSEC3 records, possibly more than one set. /// /// https://datatracker.ietf.org/doc/html/rfc5155#section-7.3 /// 7.3. Secondary Servers @@ -102,13 +107,13 @@ where /// uses NSEC records instead of NSEC3." Nsec3(Nsec3Config, Vec>), - /// The zone is transitioning from NSEC to NSEC3 hashing. + /// The zone is transitioning from NSEC to NSEC3. TransitioningNsecToNsec3( Nsec3Config, NsecToNsec3TransitionState, ), - /// The zone is transitioning from NSEC3 to NSEC hashing. + /// The zone is transitioning from NSEC3 to NSEC. TransitioningNsec3ToNsec( Nsec3Config, Nsec3ToNsecTransitionState, diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 7cbecf15c..e172446ec 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -129,7 +129,7 @@ use crate::base::{CanonicalOrd, ToName}; use crate::base::{Name, Record, Rtype}; use crate::rdata::ZoneRecordData; -use denial::config::HashingConfig; +use denial::config::DenialConfig; use denial::nsec::generate_nsecs; use denial::nsec3::{ generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, @@ -401,12 +401,12 @@ where let owner_rrs = RecordsIter::new(in_out.as_slice()); - match &mut signing_config.hashing { - HashingConfig::Prehashed => { + match &mut signing_config.denial { + DenialConfig::AlreadyPresent => { // Nothing to do. } - HashingConfig::Nsec => { + DenialConfig::Nsec => { let nsecs = generate_nsecs( ttl, owner_rrs, @@ -416,7 +416,7 @@ where in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } - HashingConfig::Nsec3( + DenialConfig::Nsec3( Nsec3Config { params, opt_out, @@ -455,18 +455,18 @@ where ); } - HashingConfig::Nsec3(_nsec3_config, _extra) => { + DenialConfig::Nsec3(_nsec3_config, _extra) => { todo!(); } - HashingConfig::TransitioningNsecToNsec3( + DenialConfig::TransitioningNsecToNsec3( _nsec3_config, _nsec_to_nsec3_transition_state, ) => { todo!(); } - HashingConfig::TransitioningNsec3ToNsec( + DenialConfig::TransitioningNsec3ToNsec( _nsec3_config, _nsec3_to_nsec_transition_state, ) => { From fb4f1595e2669465cfb689deb022fe556ce69da2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 17 Jan 2025 13:07:29 +0100 Subject: [PATCH 357/569] Report the invalid signature validity period when sign_rrset() fails. --- src/sign/error.rs | 9 +++++---- src/sign/signatures/rrsigs.rs | 16 ++++++++++++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/sign/error.rs b/src/sign/error.rs index 59ab06b37..545e99f28 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -7,11 +7,12 @@ use crate::sign::crypto::openssl; #[cfg(feature = "ring")] use crate::sign::crypto::ring; +use crate::rdata::dnssec::Timestamp; use crate::validate::Nsec3HashError; //------------ SigningError -------------------------------------------------- -#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[derive(Copy, Clone, Debug)] pub enum SigningError { /// One or more keys does not have a signature validity period defined. NoSignatureValidityPeriodProvided, @@ -41,7 +42,7 @@ pub enum SigningError { RrsigRrsMustNotBeSigned, // TODO - InvalidSignatureValidityPeriod, + InvalidSignatureValidityPeriod(Timestamp, Timestamp), // TODO SigningError(SignError), @@ -69,8 +70,8 @@ impl Display for SigningError { SigningError::RrsigRrsMustNotBeSigned => f.write_str( "RFC 4035 violation: RRSIG RRs MUST NOT be signed", ), - SigningError::InvalidSignatureValidityPeriod => f.write_str( - "RFC 4034 violation: RRSIG validity period is invalid", + SigningError::InvalidSignatureValidityPeriod(inception, expiration) => f.write_fmt( + format_args!("RFC 4034 violation: RRSIG validity period ({inception} <= {expiration}) is invalid"), ), SigningError::SigningError(err) => { f.write_fmt(format_args!("Signing error: {err}")) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index bf5d139ca..78941fcc0 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -405,7 +405,9 @@ where .into_inner(); if expiration < inception { - return Err(SigningError::InvalidSignatureValidityPeriod); + return Err(SigningError::InvalidSignatureValidityPeriod( + inception, expiration, + )); } // RFC 4034 @@ -624,7 +626,7 @@ mod tests { let rrset = Rrset::new(&records); let res = sign_rrset(&key, &rrset, &apex_owner); - assert_eq!(res, Err(SigningError::RrsigRrsMustNotBeSigned)); + assert!(matches!(res, Err(SigningError::RrsigRrsMustNotBeSigned))); } #[test] @@ -680,7 +682,10 @@ mod tests { let (expiration, inception) = calc_timestamps(5, 10); let key = key.with_validity(inception, expiration); let res = sign_rrset(&key, &rrset, &apex_owner); - assert_eq!(res, Err(SigningError::InvalidSignatureValidityPeriod)); + assert!(matches!( + res, + Err(SigningError::InvalidSignatureValidityPeriod(_, _)) + )); // Good: Expiration > Inception with Expiration near wrap around // point. @@ -745,7 +750,10 @@ mod tests { ); let key = key.with_validity(inception, expiration); let res = sign_rrset(&key, &rrset, &apex_owner); - assert_eq!(res, Err(SigningError::InvalidSignatureValidityPeriod)); + assert!(matches!( + res, + Err(SigningError::InvalidSignatureValidityPeriod(_, _)) + )); } //------------ Helper fns ------------------------------------------------ From 8b53b6ce88ba9fc827245eb30672f99863aae854 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:53:34 +0100 Subject: [PATCH 358/569] No need to check for pseudo RTYPEs being added as the input ZoneRecordData type cannot represent them. --- src/base/iana/rtype.rs | 17 ----------------- src/sign/denial/nsec.rs | 10 +++++----- src/sign/denial/nsec3.rs | 12 ++++++------ 3 files changed, 11 insertions(+), 28 deletions(-) diff --git a/src/base/iana/rtype.rs b/src/base/iana/rtype.rs index 127151b5e..a6546476a 100644 --- a/src/base/iana/rtype.rs +++ b/src/base/iana/rtype.rs @@ -440,21 +440,4 @@ impl Rtype { pub fn is_glue(&self) -> bool { matches!(*self, Rtype::A | Rtype::AAAA) } - - /// Returns true if this record type represents a pseudo-RR. - /// - /// The term "pseudo-RR" appears in [RFC - /// 9499](https://datatracker.ietf.org/doc/rfc9499/) Section 5 "Resource - /// Records" as an alias for "meta-RR" and is referenced by [RFC - /// 4034](https://datatracker.ietf.org/doc/rfc4034)/) in the context of - /// NSEC to denote types that "do not appear in zone data", with [RFC - /// 5155](https://datatracker.ietf.org/doc/rfc5155/) having text with - /// presumably the same goal but defined in terms of "META-TYPE" and - /// "QTYPE", the latter collectively being defined by [RFC - /// 2929](https://datatracker.ietf.org/doc/rfc2929/) and later as having - /// the decimal range 128 - 255 but with section 3.1 explicitly noting OPT - /// (TYPE 41) as an exception. - pub fn is_pseudo(&self) -> bool { - self.0 == 41 || (self.0 >= 128 && self.0 <= 255) - } } diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 6b7d0bfe0..c9a0885f1 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -97,11 +97,11 @@ where // "Bits representing pseudo-types MUST be clear, as they do // not appear in zone data." // - // TODO: Should this check be moved into RtypeBitmapBuilder - // itself? - if !rrset.rtype().is_pseudo() { - bitmap.add(rrset.rtype()).unwrap() - } + // We don't need to do a check here as the ZoneRecordData type + // that we require already excludes "pseudo" record types, + // those are only included as member variants of the + // AllRecordData type. + bitmap.add(rrset.rtype()).unwrap() } } diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index a45c0ad74..79e4e0999 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -296,12 +296,12 @@ where // for assignment only to QTYPEs and Meta-TYPEs MUST be set // to 0, since they do not appear in zone data". // - // TODO: Should this check be moved into RtypeBitmapBuilder - // itself? - if !rrset.rtype().is_pseudo() { - trace!("Adding {} to the bitmap", rrset.rtype()); - bitmap.add(rrset.rtype()).unwrap(); - } + // We don't need to do a check here as the ZoneRecordData type + // that we require already excludes "pseudo" record types, + // those are only included as member variants of the + // AllRecordData type. + trace!("Adding {} to the bitmap", rrset.rtype()); + bitmap.add(rrset.rtype()).unwrap(); } } From 0755ee0f2cf4ab30189fdf1980d32748668ac563 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:55:41 +0100 Subject: [PATCH 359/569] Determine the TTL for NSEC records within generate_nsecs() because it is determined by the rules defined in RFC 9077 based on the apex SOA record. Return an error if no or multiple SOAs are found. --- src/sign/denial/nsec.rs | 42 +++++++++++++++++++++++++++++++++++------ src/sign/mod.rs | 3 +-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index c9a0885f1..520e3adf7 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -1,3 +1,4 @@ +use core::cmp::min; use core::fmt::Debug; use std::vec::Vec; @@ -7,17 +8,16 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use crate::base::iana::Rtype; use crate::base::name::ToName; use crate::base::record::Record; -use crate::base::Ttl; use crate::rdata::dnssec::RtypeBitmap; use crate::rdata::{Nsec, ZoneRecordData}; +use crate::sign::error::SigningError; use crate::sign::records::RecordsIter; // TODO: Add (mutable?) iterator based variant. pub fn generate_nsecs( - ttl: Ttl, records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, -) -> Vec>> +) -> Result>>, SigningError> where N: ToName + Clone + PartialEq, Octs: FromBuilder, @@ -36,6 +36,7 @@ where let first_rr = records.first(); let apex_owner = first_rr.owner().clone(); let zone_class = first_rr.class(); + let mut ttl = None; for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are @@ -64,25 +65,30 @@ where }; if let Some((prev_name, bitmap)) = prev.take() { + // SAFETY: ttl will be set below before prev is set to Some. res.push(Record::new( prev_name.clone(), zone_class, - ttl, + ttl.unwrap(), Nsec::new(name.clone(), bitmap), )); } let mut bitmap = RtypeBitmap::::builder(); + // RFC 4035 section 2.3: // "The type bitmap of every NSEC resource record in a signed zone // MUST indicate the presence of both the NSEC record itself and // its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); + if assume_dnskeys_will_be_added && owner_rrs.owner() == &apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); } + bitmap.add(Rtype::NSEC).unwrap(); + for rrset in owner_rrs.rrsets() { // RFC 4034 section 4.1.2: (and also RFC 4035 section 2.3) // "The bitmap for the NSEC RR at a delegation point requires @@ -103,6 +109,29 @@ where // AllRecordData type. bitmap.add(rrset.rtype()).unwrap() } + + if rrset.rtype() == Rtype::SOA { + if rrset.len() > 1 { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + } + + let soa_rr = rrset.first(); + + // Check that the RDATA for the SOA record can be parsed. + let ZoneRecordData::Soa(ref soa_data) = soa_rr.data() else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to + // say that the "TTL of the NSEC(3) RR that is returned MUST + // be the lesser of the MINIMUM field of the SOA record and + // the TTL of the SOA itself". + ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + } + } + + if ttl.is_none() { + return Err(SigningError::SoaRecordCouldNotBeDetermined); } prev = Some((name, bitmap.finalize())); @@ -112,10 +141,11 @@ where res.push(Record::new( prev_name.clone(), zone_class, - ttl, + ttl.unwrap(), Nsec::new(apex_owner.clone(), bitmap), )); } - res + Ok(res) +} } diff --git a/src/sign/mod.rs b/src/sign/mod.rs index e172446ec..140b05496 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -408,10 +408,9 @@ where DenialConfig::Nsec => { let nsecs = generate_nsecs( - ttl, owner_rrs, signing_config.add_used_dnskeys, - ); + )?; in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } From dbd09b24da1205a89e76163d9f209fdab0e6ae21 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:55:49 +0100 Subject: [PATCH 360/569] Add RustDoc for generate_nsecs(). --- src/sign/denial/nsec.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 520e3adf7..61a822515 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -13,6 +13,28 @@ use crate::rdata::{Nsec, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::records::RecordsIter; +/// Generate DNSSEC NSEC records for an unsigned zone. +/// +/// This function returns a collection of generated NSEC records for the given +/// zone, per [RFC 4034 section 4] _"The NSEC Resource Record"_, [RFC 4035 +/// section 2.3] _"Including NSEC RRs in a Zone"_ and [RFC 9077] _"NSEC and +/// NSEC3: TTLs and Aggressive Use"_. +/// +/// Assumes that the given records are in [`CanonicalOrd`] order and start +/// with a complete zone, i.e. including an apex SOA record. If the apex SOA +/// is not found or multiple SOA records are found at the apex error +/// SigningError::SoaRecordCouldNotBeDetermined will be returned. +/// +/// Processing of records will stop at the end of the collection or at the +/// first record that lies outside the zone. +/// +/// If the `assume_dnskeys_will_be_added` parameter is true the generated NSEC +/// at the apex RRset will include the `DNSKEY` record type in the NSEC type +/// bitmap. +/// +/// [RFC 4034 section 4]: https://www.rfc-editor.org/rfc/rfc4034#section-4 +/// [RFC 4035 section 2.3]: https://www.rfc-editor.org/rfc/rfc4035#section-2.3 +/// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077 // TODO: Add (mutable?) iterator based variant. pub fn generate_nsecs( records: RecordsIter<'_, N, ZoneRecordData>, From fe8fc8ec5c7a015db2ef20c57918b6df8939e8d0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:57:17 +0100 Subject: [PATCH 361/569] Add tests for generate_nsecs(). --- Cargo.lock | 23 ++ Cargo.toml | 27 +- src/sign/denial/nsec.rs | 401 ++++++++++++++++++++ test-data/zonefiles/rfc4035-appendix-A.zone | 33 ++ 4 files changed, 471 insertions(+), 13 deletions(-) create mode 100644 test-data/zonefiles/rfc4035-appendix-A.zone diff --git a/Cargo.lock b/Cargo.lock index 6210508fe..e725f5c88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,6 +237,12 @@ dependencies = [ "syn", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "domain" version = "0.10.3" @@ -257,6 +263,7 @@ dependencies = [ "octseq", "openssl", "parking_lot", + "pretty_assertions", "proc-macro2", "rand", "ring", @@ -817,6 +824,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "proc-macro2" version = "1.0.87" @@ -1713,6 +1730,12 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" diff --git a/Cargo.toml b/Cargo.toml index 6293bb843..29ce6ab41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,19 +85,20 @@ unstable-zonetree = ["futures-util", "parking_lot", "rustversion", "serde", "std arbitrary = ["dep:arbitrary"] [dev-dependencies] -itertools = "0.13.0" -lazy_static = { version = "1.4.0" } -rstest = "0.19.0" -rustls-pemfile = { version = "2.1.2" } -serde_test = "1.0.130" -serde_json = "1.0.113" -serde_yaml = "0.9" -socket2 = { version = "0.5.5" } -tokio = { version = "1.37", features = ["rt-multi-thread", "io-util", "net", "test-util"] } -tokio-rustls = { version = "0.26", default-features = false, features = [ "ring", "logging", "tls12" ] } -tokio-test = "0.4" -tokio-tfo = { version = "0.2.0" } -webpki-roots = { version = "0.26" } +itertools = "0.13.0" +lazy_static = { version = "1.4.0" } +pretty_assertions = "1.4.1" +rstest = "0.19.0" +rustls-pemfile = { version = "2.1.2" } +serde_test = "1.0.130" +serde_json = "1.0.113" +serde_yaml = "0.9" +socket2 = { version = "0.5.5" } +tokio = { version = "1.37", features = ["rt-multi-thread", "io-util", "net", "test-util"] } +tokio-rustls = { version = "0.26", default-features = false, features = [ "ring", "logging", "tls12" ] } +tokio-test = "0.4" +tokio-tfo = { version = "0.2.0" } +webpki-roots = { version = "0.26" } # For the "mysql-zone" example #sqlx = { version = "0.6", features = [ "runtime-tokio-native-tls", "mysql" ] } diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 61a822515..ece837696 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -170,4 +170,405 @@ where Ok(res) } + +#[cfg(test)] +mod tests { + use core::str::FromStr; + + use std::io::Read; + + use bytes::Bytes; + use pretty_assertions::assert_eq; + + use crate::base::Serial; + use crate::base::{iana::Class, name::FlattenInto, Name, Ttl}; + use crate::rdata::{Ns, Soa, A}; + use crate::sign::records::SortedRecords; + use crate::zonefile::inplace::{Entry, Zonefile}; + use crate::zonetree::{types::StoredRecordData, StoredName}; + + use octseq::FreezeBuilder; + + use super::*; + + #[test] + fn soa_is_required() { + let mut records = SortedRecords::default(); + records.insert(mk_a("some_a.a.")).unwrap(); + let res = generate_nsecs(records.owner_rrs(), false); + assert!(matches!( + res, + Err(SigningError::SoaRecordCouldNotBeDetermined) + )); + } + + #[test] + fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { + let mut records = SortedRecords::default(); + records.insert(mk_soa("a.", "b.", "c.")).unwrap(); + records.insert(mk_soa("a.", "d.", "e.")).unwrap(); + let res = generate_nsecs(records.owner_rrs(), false); + assert!(matches!( + res, + Err(SigningError::SoaRecordCouldNotBeDetermined) + )); + } + + #[test] + fn records_outside_zone_are_ignored() { + let mut records = SortedRecords::default(); + + records.insert(mk_soa("b.", "d.", "e.")).unwrap(); + records.insert(mk_a("some_a.b.")).unwrap(); + records.insert(mk_soa("a.", "b.", "c.")).unwrap(); + records.insert(mk_a("some_a.a.")).unwrap(); + + // First generate NSECs for the total record collection. As the + // collection is sorted in canonical order the a zone preceeds the b + // zone and NSECs should only be generated for the first zone in the + // collection. + let a_and_b_records = records.owner_rrs(); + let nsecs = generate_nsecs(a_and_b_records, false).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec("a.", Class::IN, 0, "some_a.a.", "SOA RRSIG NSEC"), + mk_nsec("some_a.a.", Class::IN, 0, "a.", "A RRSIG NSEC"), + ] + ); + + // Now skip the a zone in the collection and generate NSECs for the + // remaining records which should only generate NSECs for the b zone. + let mut b_records_only = records.owner_rrs(); + b_records_only.skip_before(&mk_name("b.")); + let nsecs = generate_nsecs(b_records_only, false).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec("b.", Class::IN, 0, "some_a.b.", "SOA RRSIG NSEC"), + mk_nsec("some_a.b.", Class::IN, 0, "b.", "A RRSIG NSEC"), + ] + ); + } + + #[test] + fn occluded_records_are_ignored() { + let mut records = SortedRecords::default(); + + records.insert(mk_soa("a.", "b.", "c.")).unwrap(); + records + .insert(mk_ns("some_ns.a.", "some_a.other.b.")) + .unwrap(); + records.insert(mk_a("some_a.some_ns.a.")).unwrap(); + + let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + + // Implicit negative test. + assert_eq!( + nsecs, + [ + mk_nsec( + "a.", + Class::IN, + 12345, + "some_ns.a.", + "SOA RRSIG NSEC" + ), + mk_nsec( + "some_ns.a.", + Class::IN, + 12345, + "a.", + "NS RRSIG NSEC" + ), + ] + ); + + // Explicit negative test. + assert!(!contains_owner(&nsecs, "some_a.some_ns.a.example.")); + } + + #[test] + fn expect_dnskeys_at_the_apex() { + let mut records = SortedRecords::default(); + + records.insert(mk_soa("a.", "b.", "c.")).unwrap(); + records.insert(mk_a("some_a.a.")).unwrap(); + + let nsecs = generate_nsecs(records.owner_rrs(), true).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec( + "a.", + Class::IN, + 0, + "some_a.a.", + "SOA DNSKEY RRSIG NSEC" + ), + mk_nsec("some_a.a.", Class::IN, 0, "a.", "A RRSIG NSEC"), + ] + ); + } + + #[test] + fn rfc_4034_and_9077_compliant() { + // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A + let zonefile = include_bytes!( + "../../../test-data/zonefiles/rfc4035-appendix-A.zone" + ); + + let records = bytes_to_records(&zonefile[..]); + let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + + assert_eq!(nsecs.len(), 10); + + assert_eq!( + nsecs, + [ + mk_nsec( + "example.", + Class::IN, + 12345, + "a.example", + "NS SOA MX RRSIG NSEC DNSKEY" + ), + mk_nsec( + "a.example.", + Class::IN, + 12345, + "ai.example", + "NS DS RRSIG NSEC" + ), + mk_nsec( + "ai.example.", + Class::IN, + 12345, + "b.example", + "A HINFO AAAA RRSIG NSEC" + ), + mk_nsec( + "b.example.", + Class::IN, + 12345, + "ns1.example", + "NS RRSIG NSEC" + ), + mk_nsec( + "ns1.example.", + Class::IN, + 12345, + "ns2.example", + "A RRSIG NSEC" + ), + // The next record also validates that we comply with + // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 + // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when + // it says: + // "If a wildcard owner name appears in a zone, the wildcard + // label ("*") is treated as a literal symbol and is treated + // the same as any other owner name for the purposes of + // generating NSEC RRs. Wildcard owner names appear in the + // Next Domain Name field without any wildcard expansion. + // [RFC4035] describes the impact of wildcards on + // authenticated denial of existence." + mk_nsec( + "ns2.example.", + Class::IN, + 12345, + "*.w.example", + "A RRSIG NSEC" + ), + mk_nsec( + "*.w.example.", + Class::IN, + 12345, + "x.w.example", + "MX RRSIG NSEC" + ), + mk_nsec( + "x.w.example.", + Class::IN, + 12345, + "x.y.w.example", + "MX RRSIG NSEC" + ), + mk_nsec( + "x.y.w.example.", + Class::IN, + 12345, + "xx.example", + "MX RRSIG NSEC" + ), + mk_nsec( + "xx.example.", + Class::IN, + 12345, + "example", + "A HINFO AAAA RRSIG NSEC" + ) + ], + ); + + // TTLs are not compared by the eq check above so check them + // explicitly now. + // + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + // + // So in our case that is min(1800, 3600) = 1800. + for nsec in &nsecs { + assert_eq!(nsec.ttl(), Ttl::from_secs(1800)); + } + + // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 + // 2.3. Including NSEC RRs in a Zone + // ... + // "The type bitmap of every NSEC resource record in a signed zone + // MUST indicate the presence of both the NSEC record itself and its + // corresponding RRSIG record." + for nsec in &nsecs { + assert!(nsec.data().types().contains(Rtype::NSEC)); + assert!(nsec.data().types().contains(Rtype::RRSIG)); + } + + // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 + // 4.1.2. The Type Bit Maps Field + // "Bits representing pseudo-types MUST be clear, as they do not + // appear in zone data." + // + // There is nothing to test for this as it is excluded at the Rust + // type system level by the generate_nsecs() function taking + // ZoneRecordData (which excludes pseudo record types) as input rather + // than AllRecordData (which includes pseudo record types). + + // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 + // 4.1.2. The Type Bit Maps Field + // ... + // "A zone MUST NOT include an NSEC RR for any domain name that only + // holds glue records." + // + // The "rfc4035-appendix-A.zone" file that we load contains glue A + // records for ns1.example, ns1.a.example, ns1.b.example, ns2.example + // and ns2.a.example all with no other record types at that name. We + // can verify that an NSEC RR was NOT created for those that are not + // within the example zone as we are not authoritative for thos. + assert!(contains_owner(&nsecs, "ns1.example.")); + assert!(!contains_owner(&nsecs, "ns1.a.example.")); + assert!(!contains_owner(&nsecs, "ns1.b.example.")); + assert!(contains_owner(&nsecs, "ns2.example.")); + assert!(!contains_owner(&nsecs, "ns2.a.example.")); + + // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 + // 2.3. Including NSEC RRs in a Zone + // ... + // "The bitmap for the NSEC RR at a delegation point requires special + // attention. Bits corresponding to the delegation NS RRset and any + // RRsets for which the parent zone has authoritative data MUST be + // set; bits corresponding to any non-NS RRset for which the parent + // is not authoritative MUST be clear." + // + // The "rfc4035-appendix-A.zone" file that we load has been modified + // compared to the original to include a glue A record at b.example. + // We can verify that an NSEC RR was NOT created for that name. + let name = mk_name::("b.example."); + let nsec = nsecs.iter().find(|rr| rr.owner() == &name).unwrap(); + assert!(nsec.data().types().contains(Rtype::NSEC)); + assert!(nsec.data().types().contains(Rtype::RRSIG)); + assert!(!nsec.data().types().contains(Rtype::A)); + } + + //------------ Helper fns ------------------------------------------------ + + fn bytes_to_records( + mut zonefile: impl Read, + ) -> SortedRecords { + let reader = Zonefile::load(&mut zonefile).unwrap(); + let mut records = SortedRecords::default(); + for entry in reader { + let entry = entry.unwrap(); + if let Entry::Record(record) = entry { + records.insert(record.flatten_into()).unwrap() + } + } + records + } + + fn mk_nsec( + owner: &str, + class: Class, + ttl_secs: u32, + next_name: &str, + types: &str, + ) -> Record> { + let owner = Name::from_str(owner).unwrap(); + let ttl = Ttl::from_secs(ttl_secs); + let next_name = Name::from_str(next_name).unwrap(); + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + Record::new(owner, class, ttl, Nsec::new(next_name, types)) + } + + fn mk_name(name: &str) -> Name + where + Octs: FromBuilder, + ::Builder: EmptyBuilder + + FreezeBuilder + + AsRef<[u8]> + + AsMut<[u8]>, + { + Name::::from_str(name).unwrap() + } + + fn mk_soa( + name: &str, + mname: &str, + rname: &str, + ) -> Record, ZoneRecordData>> { + let zone_apex_name = mk_name::(name); + let soa = ZoneRecordData::::Soa(Soa::new( + mk_name::(mname), + mk_name::(rname), + Serial::now(), + Ttl::ZERO, + Ttl::ZERO, + Ttl::ZERO, + Ttl::ZERO, + )); + Record::new(zone_apex_name, Class::IN, Ttl::ZERO, soa) + } + + fn mk_a( + name: &str, + ) -> Record, ZoneRecordData>> { + let a_name = mk_name::(name); + let a = + ZoneRecordData::::A(A::from_str("1.2.3.4").unwrap()); + Record::new(a_name, Class::IN, Ttl::ZERO, a) + } + + fn mk_ns( + name: &str, + nsdname: &str, + ) -> Record, ZoneRecordData>> { + let name = mk_name::(name); + let nsdname = mk_name::(nsdname); + let ns = ZoneRecordData::::Ns(Ns::new(nsdname)); + Record::new(name, Class::IN, Ttl::ZERO, ns) + } + + fn contains_owner( + nsecs: &[Record, Nsec>>], + name: &str, + ) -> bool { + let name = mk_name::(name); + nsecs.iter().any(|rr| rr.owner() == &name) + } } diff --git a/test-data/zonefiles/rfc4035-appendix-A.zone b/test-data/zonefiles/rfc4035-appendix-A.zone new file mode 100644 index 000000000..431d99590 --- /dev/null +++ b/test-data/zonefiles/rfc4035-appendix-A.zone @@ -0,0 +1,33 @@ +; Extracted using ldns-readzone -s from the signed zone defined at +; https://datatracker.ietf.org/doc/html/rfc4035#appendix-A +; Keys have been replaced by newer algorithm 8 instead of older algorithm 5 +; which we do not support, and to match key pairs stored alongside this file. +; Contains one extra record compared to that defined in Appendix A of RFC +; 4035, b.example A, for additional testing. +example. 3600 IN SOA ns1.example. bugs.x.w.example. 1081539377 3600 300 3600000 1800 +example. 3600 IN NS ns2.example. +example. 3600 IN NS ns1.example. +example. 3600 IN MX 1 xx.example. +example. 3600 IN DNSKEY 257 3 8 AwEAAaYL5iwWI6UgSQVcDZmH7DrhQU/P6cOfi4wXYDzHypsfZ1D8znPwoAqhj54kTBVqgZDHw8QEnMcS3TWxvHBvncRTIXhCLx0BNK5/6mcTSK2IDbxl0j4vkcQrOxc77tyExuFfuXouuKVtE7rggOJiX6ga5LJW2if6Jxe/Rh8+aJv7 ;{id = 31967 (ksk), size = 1024b} +example. 3600 IN DNSKEY 256 3 8 AwEAAbsD4Tcz8hl2Rldov4CrfYpK3ORIh/giSGDlZaDTZR4gpGxGvMBwu2jzQ3m0iX3PvqPoaybC4tznjlJi8g/qsCRHhOkqWmjtmOYOJXEuUTb+4tPBkiboJM5QchxTfKxkYbJ2AD+VAUX1S6h/0DI0ZCGx1H90QTBE2ymRgHBwUfBt ;{id = 38353 (zsk), size = 1024b} +a.example. 3600 IN NS ns2.a.example. +a.example. 3600 IN NS ns1.a.example. +a.example. 3600 IN DS 57855 5 1 b6dcd485719adca18e5f3d48a2331627fdd3636b +ns1.a.example. 3600 IN A 192.0.2.5 +ns2.a.example. 3600 IN A 192.0.2.6 +ai.example. 3600 IN A 192.0.2.9 +ai.example. 3600 IN HINFO "KLH-10" "ITS" +ai.example. 3600 IN AAAA 2001:db8::f00:baa9 +b.example. 3600 IN NS ns1.b.example. +b.example. 3600 IN NS ns2.b.example. +b.example. 3600 IN A 127.0.0.1 ; not authoritative, should not appear in the NSEC bitmap +ns1.b.example. 3600 IN A 192.0.2.7 +ns2.b.example. 3600 IN A 192.0.2.8 +ns1.example. 3600 IN A 192.0.2.1 +ns2.example. 3600 IN A 192.0.2.2 +*.w.example. 3600 IN MX 1 ai.example. +x.w.example. 3600 IN MX 1 xx.example. +x.y.w.example. 3600 IN MX 1 xx.example. +xx.example. 3600 IN A 192.0.2.10 +xx.example. 3600 IN HINFO "KLH-10" "TOPS-20" +xx.example. 3600 IN AAAA 2001:db8::f00:baaa From df72cb464ad3cbb03fdc1addd600422feb3d3d50 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:57:31 +0100 Subject: [PATCH 362/569] Cargo fmt. --- src/sign/denial/nsec.rs | 4 ++-- src/sign/mod.rs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index ece837696..351a45937 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -24,10 +24,10 @@ use crate::sign::records::RecordsIter; /// with a complete zone, i.e. including an apex SOA record. If the apex SOA /// is not found or multiple SOA records are found at the apex error /// SigningError::SoaRecordCouldNotBeDetermined will be returned. -/// +/// /// Processing of records will stop at the end of the collection or at the /// first record that lies outside the zone. -/// +/// /// If the `assume_dnskeys_will_be_added` parameter is true the generated NSEC /// at the apex RRset will include the `DNSKEY` record type in the NSEC type /// bitmap. diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 140b05496..516786f49 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -407,10 +407,8 @@ where } DenialConfig::Nsec => { - let nsecs = generate_nsecs( - owner_rrs, - signing_config.add_used_dnskeys, - )?; + let nsecs = + generate_nsecs(owner_rrs, signing_config.add_used_dnskeys)?; in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } From b4b7e918d9a1a7497d7b997f9fb99660d2a6c691 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 11:58:23 +0100 Subject: [PATCH 363/569] Clippy. --- src/sign/denial/nsec.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 351a45937..6ff695ad6 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -36,6 +36,7 @@ use crate::sign::records::RecordsIter; /// [RFC 4035 section 2.3]: https://www.rfc-editor.org/rfc/rfc4035#section-2.3 /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077 // TODO: Add (mutable?) iterator based variant. +#[allow(clippy::type_complexity)] pub fn generate_nsecs( records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, @@ -564,6 +565,7 @@ mod tests { Record::new(name, Class::IN, Ttl::ZERO, ns) } + #[allow(clippy::type_complexity)] fn contains_owner( nsecs: &[Record, Nsec>>], name: &str, From 9c6f86625d3191530f224ee43486242534a6987f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:02:07 +0100 Subject: [PATCH 364/569] Fix broken/missing RustDoc links. --- src/sign/denial/nsec.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 6ff695ad6..1d16bc1bd 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -23,7 +23,7 @@ use crate::sign::records::RecordsIter; /// Assumes that the given records are in [`CanonicalOrd`] order and start /// with a complete zone, i.e. including an apex SOA record. If the apex SOA /// is not found or multiple SOA records are found at the apex error -/// SigningError::SoaRecordCouldNotBeDetermined will be returned. +/// [`SigningError::SoaRecordCouldNotBeDetermined`] will be returned. /// /// Processing of records will stop at the end of the collection or at the /// first record that lies outside the zone. @@ -35,6 +35,7 @@ use crate::sign::records::RecordsIter; /// [RFC 4034 section 4]: https://www.rfc-editor.org/rfc/rfc4034#section-4 /// [RFC 4035 section 2.3]: https://www.rfc-editor.org/rfc/rfc4035#section-2.3 /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077 +/// [`CanonicalOrd`]: crate::base::cmp::CanonicalOrd // TODO: Add (mutable?) iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_nsecs( From 6321f737f88dc845c7ac313fde80021e92fbe274 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:04:01 +0100 Subject: [PATCH 365/569] Minor test name corrections. --- src/sign/signatures/rrsigs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 78941fcc0..a68ce4c70 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -500,7 +500,7 @@ mod tests { } #[test] - fn rrset_sign_adheres_to_rules_in_rfc_4034_and_rfc_4035() { + fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); @@ -566,7 +566,7 @@ mod tests { } #[test] - fn rrtest_sign_wildcard() { + fn sign_rrset_with_wildcard() { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); @@ -630,7 +630,7 @@ mod tests { } #[test] - fn sign_rrsets_check_validity_period_handling() { + fn sign_rrset_check_validity_period_handling() { // RFC 4034 // 3.1.5. Signature Expiration and Inception Fields // ... From edc513bc74b8c1e69b93ef1480434f77706296ba Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:37:19 +0100 Subject: [PATCH 366/569] - Make generate_rrsigs() take a config object instead of multiple config arguments, and make the apex optional as it isn't needed for a full zone. - Split generate_rrsigs() apex handling functionality out to generate_apex_rrsigs(). - Split generate_rrsigs() key logging functionality out to log_keys_in_use(). - Use log::log_enabled() now that PR #465 has been merged. - Add some initial generate_rrsigs() tests. - Fix missing unwrap()s after calling .insert() in RustDoc example code. --- src/sign/mod.rs | 12 +- src/sign/signatures/rrsigs.rs | 670 ++++++++++++++++++++++++---------- src/sign/traits.rs | 8 +- 3 files changed, 492 insertions(+), 198 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 516786f49..be2618dcd 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -141,7 +141,7 @@ use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, }; use records::{RecordsIter, Sorter}; -use signatures::rrsigs::generate_rrsigs; +use signatures::rrsigs::{generate_rrsigs, GenerateRrsigConfig}; use signatures::strategy::SigningKeyUsageStrategy; use traits::{SignRaw, SignableZone, SortedExtend}; @@ -472,15 +472,18 @@ where } if !signing_keys.is_empty() { + let mut rrsig_config = GenerateRrsigConfig::new(); + rrsig_config.add_used_dnskeys = signing_config.add_used_dnskeys; + rrsig_config.zone_apex = Some(&apex_owner); + // Sign the NSEC(3)s. let owner_rrs = RecordsIter::new(in_out.as_out_slice()); let nsec_rrsigs = generate_rrsigs::( - &apex_owner, owner_rrs, signing_keys, - signing_config.add_used_dnskeys, + &rrsig_config, )?; // Sorting may not be strictly needed, but we don't have the option to @@ -492,10 +495,9 @@ where let rrsigs_and_dnskeys = generate_rrsigs::( - &apex_owner, owner_rrs, signing_keys, - signing_config.add_used_dnskeys, + &rrsig_config, )?; // Sorting may not be strictly needed, but we don't have the option to diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index a68ce4c70..ae0167be4 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1,19 +1,20 @@ //! DNSSEC RRSIG generation. -use core::convert::From; +use core::convert::{AsRef, From}; use core::fmt::Display; -use core::marker::Send; +use core::marker::{PhantomData, Send}; use std::boxed::Box; use std::collections::HashSet; use std::string::ToString; use std::vec::Vec; -use octseq::builder::{EmptyBuilder, FromBuilder}; +use octseq::builder::FromBuilder; use octseq::{OctetsFrom, OctetsInto}; -use tracing::{debug, enabled, trace, Level}; +use tracing::{debug, trace}; +use super::strategy::DefaultSigningKeyUsageStrategy; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, Rtype}; +use crate::base::iana::Rtype; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; @@ -23,23 +24,73 @@ use crate::rdata::{Dnskey, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::keys::signingkey::SigningKey; -use crate::sign::records::{RecordsIter, Rrset, SortedRecords, Sorter}; +use crate::sign::records::{ + DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, +}; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::traits::{SignRaw, SortedExtend}; +use log::Level; -/// Generate RRSIG RRs for a collection of unsigned zone records. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { + pub add_used_dnskeys: bool, + + pub zone_apex: Option<&'a N>, + + _phantom: PhantomData<(KeyStrat, Sort)>, +} + +impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { + pub fn new() -> Self { + Self { + add_used_dnskeys: false, + zone_apex: None, + _phantom: Default::default(), + } + } + + pub fn with_add_used_dns_keys(mut self) -> Self { + self.add_used_dnskeys = true; + self + } + + pub fn with_zone_apex(mut self, zone_apex: &'a N) -> Self { + self.zone_apex = Some(zone_apex); + self + } +} + +impl Default + for GenerateRrsigConfig< + '_, + N, + DefaultSigningKeyUsageStrategy, + DefaultSorter, + > +{ + fn default() -> Self { + Self { + add_used_dnskeys: true, + zone_apex: None, + _phantom: Default::default(), + } + } +} + +/// Generate RRSIG RRs for a collection of zone records. /// /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be /// added to the given records as part of DNSSEC zone signing. /// /// The given records MUST be sorted according to [`CanonicalOrd`]. +/// +/// Any existing RRSIG records will be ignored. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_rrsigs( - expected_apex: &N, records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], - add_used_dnskeys: bool, + config: &GenerateRrsigConfig<'_, N, KeyStrat, Sort>, ) -> Result>>, SigningError> where DSK: DesignatedSigningKey, @@ -60,20 +111,57 @@ where + FromBuilder + From<&'static [u8]>, Sort: Sorter, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, { debug!( - "Signer settings: add_used_dnskeys={add_used_dnskeys}, strategy: {}", + "Signer settings: add_used_dnskeys={}, strategy: {}", + config.add_used_dnskeys, KeyStrat::NAME ); + // Peek at the records because we need to process the first owner records + // differently if they represent the apex of a zone (i.e. contain the SOA + // record), otherwise we process the first owner records in the same loop + // as the rest of the records beneath the apex. + let mut records = records.peekable(); + + let first_rrs = records.peek(); + + let Some(first_rrs) = first_rrs else { + // No records were provided. As we are able to generate RRSIGs for + // partial zones this is a special case of a partial zone, an empty + // input, for which there is nothing to do. + return Ok(vec![]); + }; + + let first_owner = first_rrs.owner().clone(); + + // If no apex was supplied, assume that because the input should be + // canonically ordered that the first record is part of the apex RRSET. + // Otherwise, check if the first record matches the given apex, if not + // that means that the input starts beneath the apex. + let (zone_apex, at_apex) = match config.zone_apex { + Some(zone_apex) => (zone_apex, first_rrs.owner() == zone_apex), + None => (&first_owner, true), + }; + + // https://www.rfc-editor.org/rfc/rfc1034#section-6.1 + // 6.1. C.ISI.EDU name server + // ... + // "Since the class of all RRs in a zone must be the same..." + // + // We can therefore assume that the class to use for new DNSKEY records + // when we add them will be the same as the class of the first resource + // record in the zone. + let zone_class = first_rrs.class(); + + // Determine which keys to use for what. Work with indices because + // SigningKey doesn't impl PartialEq so we cannot use a HashSet to make a + // unique set of them. + if keys.is_empty() { return Err(SigningError::NoKeysProvided); } - // Work with indices because SigningKey doesn't impl PartialEq so we - // cannot use a HashSet to make a unique set of them. - let dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype(keys, Some(Rtype::DNSKEY)); @@ -89,184 +177,41 @@ where return Err(SigningError::NoSuitableKeysFound); } - // TODO: use log::log_enabled instead. - // See: https://github.com/NLnetLabs/domain/pull/465 - if enabled!(Level::DEBUG) { - fn debug_key, Inner: SignRaw>( - prefix: &str, - key: &SigningKey, - ) { - debug!( - "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", - key.algorithm() - .to_mnemonic_str() - .map(|alg| format!("{alg} ({})", key.algorithm())) - .unwrap_or_else(|| key.algorithm().to_string()), - key.owner(), - key.flags(), - key.is_secure_entry_point(), - key.is_zone_signing_key(), - key.public_key().key_tag(), - ) - } - - let num_keys = keys_in_use_idxs.len(); - debug!( - "Signing with {} {}:", - num_keys, - if num_keys == 1 { "key" } else { "keys" } + if log::log_enabled!(Level::Debug) { + log_keys_in_use( + keys, + &dnskey_signing_key_idxs, + &non_dnskey_signing_key_idxs, + &keys_in_use_idxs, ); - - for idx in &keys_in_use_idxs { - let key = &keys[**idx]; - let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); - let is_non_dnskey_signing_key = - non_dnskey_signing_key_idxs.contains(idx); - let usage = if is_dnskey_signing_key && is_non_dnskey_signing_key - { - "CSK" - } else if is_dnskey_signing_key { - "KSK" - } else if is_non_dnskey_signing_key { - "ZSK" - } else { - "Unused" - }; - debug_key(&format!("Key[{idx}]: {usage}"), key); - } } let mut res: Vec>> = Vec::new(); let mut reusable_scratch = Vec::new(); let mut cut: Option = None; - let mut records = records.peekable(); - - // Are we signing the entire tree from the apex down or just some child - // records? Use the first found SOA RR as the apex. If no SOA RR can be - // found assume that we are only signing records below the apex. - let (soa_ttl, zone_class) = if let Some(rr) = - records.peek().and_then(|first_owner_rrs| { - first_owner_rrs.records().find(|rr| { - rr.owner() == expected_apex && rr.rtype() == Rtype::SOA - }) - }) { - (Some(rr.ttl()), rr.class()) - } else { - (None, Class::IN) - }; - - if let Some(soa_ttl) = soa_ttl { - // Sign the apex - // SAFETY: We just checked above if the apex records existed. - let apex_owner_rrs = records.next().unwrap(); - let apex_rrsets = apex_owner_rrs - .rrsets() - .filter(|rrset| rrset.rtype() != Rtype::RRSIG); - - // Generate or extend the DNSKEY RRSET with the keys that we will sign - // apex DNSKEY RRs and zone RRs with. - let apex_dnskey_rrset = apex_owner_rrs - .rrsets() - .find(|rrset| rrset.rtype() == Rtype::DNSKEY); - - let mut augmented_apex_dnskey_rrs = - SortedRecords::<_, _, Sort>::new(); - - // Determine the TTL of any existing DNSKEY RRSET and use that as the - // TTL for DNSKEY RRs that we add. If none, then fall back to the SOA - // TTL. - // - // https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 5.2. TTLs - // of RRs in an RRSet "Consequently the use of differing TTLs in an - // RRSet is hereby deprecated, the TTLs of all RRs in an RRSet must - // be the same." - // - // Note that while RFC 1033 says: RESOURCE RECORDS "If you leave the - // TTL field blank it will default to the minimum time specified in - // the SOA record (described later)." - // - // That RFC pre-dates RFC 1034, and neither dnssec-signzone nor - // ldns-signzone use the SOA MINIMUM as a default TTL, rather they use - // the TTL of the SOA RR as the default and so we will do the same. - let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { - let ttl = rrset.ttl(); - augmented_apex_dnskey_rrs.sorted_extend(rrset.iter().cloned()); - ttl - } else { - soa_ttl - }; - - for public_key in - keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) - { - let dnskey = public_key.to_dnskey(); - - let signing_key_dnskey_rr = Record::new( - expected_apex.clone(), - zone_class, - dnskey_rrset_ttl, - Dnskey::convert(dnskey.clone()).into(), - ); - - // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs - // for. - let is_new_dnskey = augmented_apex_dnskey_rrs - .insert(signing_key_dnskey_rr) - .is_ok(); - - if add_used_dnskeys && is_new_dnskey { - // Add the DNSKEY RR to the set of new RRs to output for the - // zone. - res.push(Record::new( - expected_apex.clone(), - zone_class, - dnskey_rrset_ttl, - Dnskey::convert(dnskey).into(), - )); - } - } - - let augmented_apex_dnskey_rrset = - Rrset::new(&augmented_apex_dnskey_rrs); - - // Sign the apex RRSETs in canonical order. - for rrset in apex_rrsets - .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) - .chain(std::iter::once(augmented_apex_dnskey_rrset)) - { - // For the DNSKEY RRSET, use signing keys chosen for that purpose - // and sign the augmented set of DNSKEY RRs that we have generated - // rather than the original set in the zonefile. - let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { - &dnskey_signing_key_idxs - } else { - &non_dnskey_signing_key_idxs - }; - - for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { - let rrsig_rr = sign_rrset_in( - key, - &rrset, - expected_apex, - &mut reusable_scratch, - )?; - res.push(rrsig_rr); - trace!( - "Signed {} RRs in RRSET {} at the zone apex with keytag {}", - rrset.iter().len(), - rrset.rtype(), - key.public_key().key_tag() - ); - } - } + if at_apex { + // Sign the apex, if it contains a SOA record, otherwise it's just the + // first in a collection of sorted records but not the apex of a zone. + generate_apex_rrsigs( + keys, + config, + &mut records, + zone_apex, + zone_class, + &dnskey_signing_key_idxs, + &non_dnskey_signing_key_idxs, + keys_in_use_idxs, + &mut res, + &mut reusable_scratch, + )?; } - // For all RRSETs below the apex + // For all records for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(expected_apex) { + if !owner_rrs.is_in_zone(zone_apex) { break; } @@ -283,7 +228,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(expected_apex) { + cut = if owner_rrs.is_zone_cut(zone_apex) { Some(name.clone()) } else { None @@ -311,7 +256,7 @@ where let rrsig_rr = sign_rrset_in( key, &rrset, - expected_apex, + zone_apex, &mut reusable_scratch, )?; res.push(rrsig_rr); @@ -325,11 +270,216 @@ where } } - debug!("Returning {} records from signing", res.len()); + debug!("Returning {} records from signature generation", res.len()); Ok(res) } +fn log_keys_in_use( + keys: &[DSK], + dnskey_signing_key_idxs: &HashSet, + non_dnskey_signing_key_idxs: &HashSet, + keys_in_use_idxs: &HashSet<&usize>, +) where + DSK: DesignatedSigningKey, + Inner: SignRaw, + Octs: AsRef<[u8]>, +{ + fn debug_key, Inner: SignRaw>( + prefix: &str, + key: &SigningKey, + ) { + debug!( + "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", + key.algorithm() + .to_mnemonic_str() + .map(|alg| format!("{alg} ({})", key.algorithm())) + .unwrap_or_else(|| key.algorithm().to_string()), + key.owner(), + key.flags(), + key.is_secure_entry_point(), + key.is_zone_signing_key(), + key.public_key().key_tag(), + ) + } + + let num_keys = keys_in_use_idxs.len(); + debug!( + "Signing with {} {}:", + num_keys, + if num_keys == 1 { "key" } else { "keys" } + ); + + for idx in keys_in_use_idxs { + let key = &keys[**idx]; + let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); + let is_non_dnskey_signing_key = + non_dnskey_signing_key_idxs.contains(idx); + let usage = if is_dnskey_signing_key && is_non_dnskey_signing_key { + "CSK" + } else if is_dnskey_signing_key { + "KSK" + } else if is_non_dnskey_signing_key { + "ZSK" + } else { + "Unused" + }; + debug_key(&format!("Key[{idx}]: {usage}"), key); + } +} + +#[allow(clippy::too_many_arguments)] +fn generate_apex_rrsigs( + keys: &[DSK], + config: &GenerateRrsigConfig<'_, N, KeyStrat, Sort>, + records: &mut core::iter::Peekable< + RecordsIter<'_, N, ZoneRecordData>, + >, + zone_apex: &N, + zone_class: crate::base::iana::Class, + dnskey_signing_key_idxs: &HashSet, + non_dnskey_signing_key_idxs: &HashSet, + keys_in_use_idxs: HashSet<&usize>, + res: &mut Vec>>, + reusable_scratch: &mut Vec, +) -> Result<(), SigningError> +where + DSK: DesignatedSigningKey, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + N: ToName + + PartialEq + + Clone + + Display + + Send + + CanonicalOrd + + From>, + Octs: AsRef<[u8]> + + From> + + Send + + OctetsFrom> + + Clone + + FromBuilder + + From<&'static [u8]>, + Sort: Sorter, +{ + let Some(apex_owner_rrs) = records.peek() else { + // Nothing to do. + return Ok(()); + }; + + let apex_rrsets = apex_owner_rrs + .rrsets() + .filter(|rrset| rrset.rtype() != Rtype::RRSIG); + + let soa_rrs = apex_owner_rrs + .rrsets() + .find(|rrset| rrset.rtype() == Rtype::SOA); + + let Some(soa_rrs) = soa_rrs else { + // Nothing to do, no SOA RR found. + return Ok(()); + }; + + if soa_rrs.len() > 1 { + // Too many SOA RRs found. + return Err(SigningError::SoaRecordCouldNotBeDetermined); + } + + let soa_rr = soa_rrs.first(); + + // Generate or extend the DNSKEY RRSET with the keys that we will sign + // apex DNSKEY RRs and zone RRs with. + let apex_dnskey_rrset = apex_owner_rrs + .rrsets() + .find(|rrset| rrset.rtype() == Rtype::DNSKEY); + + let mut augmented_apex_dnskey_rrs = SortedRecords::<_, _, Sort>::new(); + + // Determine the TTL of any existing DNSKEY RRSET and use that as the TTL + // for DNSKEY RRs that we add. If none, then fall back to the SOA TTL. + // + // https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 + // 5.2. TTLs of RRs in an RRSet + // "Consequently the use of differing TTLs in an RRSet is hereby + // deprecated, the TTLs of all RRs in an RRSet must be the same." + // + // Note that while RFC 1033 says: + // RESOURCE RECORDS + // "If you leave the TTL field blank it will default to the minimum time + // specified in the SOA record (described later)." + // + // That RFC pre-dates RFC 1034, and neither dnssec-signzone nor + // ldns-signzone use the SOA MINIMUM as a default TTL, rather they use the + // TTL of the SOA RR as the default and so we will do the same. + let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { + let ttl = rrset.ttl(); + augmented_apex_dnskey_rrs.sorted_extend(rrset.iter().cloned()); + ttl + } else { + soa_rr.ttl() + }; + + for public_key in + keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) + { + let dnskey = public_key.to_dnskey(); + + let signing_key_dnskey_rr = Record::new( + zone_apex.clone(), + zone_class, + dnskey_rrset_ttl, + Dnskey::convert(dnskey.clone()).into(), + ); + + // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. + let is_new_dnskey = augmented_apex_dnskey_rrs + .insert(signing_key_dnskey_rr) + .is_ok(); + + if config.add_used_dnskeys && is_new_dnskey { + // Add the DNSKEY RR to the set of new RRs to output for the zone. + res.push(Record::new( + zone_apex.clone(), + zone_class, + dnskey_rrset_ttl, + Dnskey::convert(dnskey).into(), + )); + } + } + + let augmented_apex_dnskey_rrset = Rrset::new(&augmented_apex_dnskey_rrs); + + // Sign the apex RRSETs in canonical order. + for rrset in apex_rrsets + .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) + .chain(std::iter::once(augmented_apex_dnskey_rrset)) + { + // For the DNSKEY RRSET, use signing keys chosen for that purpose and + // sign the augmented set of DNSKEY RRs that we have generated rather + // than the original set in the zonefile. + let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { + dnskey_signing_key_idxs + } else { + non_dnskey_signing_key_idxs + }; + + for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { + let rrsig_rr = + sign_rrset_in(key, &rrset, zone_apex, reusable_scratch)?; + res.push(rrsig_rr); + trace!( + "Signed {} RRs in RRSET {} at the zone apex with keytag {}", + rrset.iter().len(), + rrset.rtype(), + key.public_key().key_tag() + ); + } + } + + Ok(()) +} + /// Generate `RRSIG` records for a given RRset. /// /// See [`sign_rrset_in()`]. @@ -473,15 +623,23 @@ where #[cfg(test)] mod tests { - use super::*; - use crate::base::iana::SecAlg; + use core::str::FromStr; + + use bytes::Bytes; + + use crate::base::iana::{Class, SecAlg}; use crate::base::{Serial, Ttl}; - use crate::rdata::dnssec::Timestamp; - use crate::rdata::{Rrsig, A}; + use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; + use crate::rdata::{Nsec, Rrsig, A}; + use crate::sign::crypto::common::{self, GenerateParams, KeyPair}; use crate::sign::error::SignError; + use crate::sign::keys::DnssecSigningKey; use crate::sign::{PublicKeyBytes, Signature}; - use bytes::Bytes; - use core::str::FromStr; + use crate::zonetree::types::StoredRecordData; + use crate::zonetree::StoredName; + + use super::*; + use core::ops::RangeInclusive; struct TestKey; @@ -756,6 +914,85 @@ mod tests { )); } + #[test] + fn generate_rrsigs_with_empty_zone_succeeds() { + let records: [Record; 0] = []; + let no_keys: [DnssecSigningKey; 0] = []; + + generate_rrsigs( + RecordsIter::new(&records), + &no_keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + } + + #[test] + fn generate_rrsigs_without_keys_fails_for_non_empty_zone() { + let records: [Record; 1] = [mk_record( + "example.", + Class::IN, + 0, + ZoneRecordData::A(A::from_str("127.0.0.1").unwrap()), + )]; + let no_keys: [DnssecSigningKey; 0] = []; + + let res = generate_rrsigs( + RecordsIter::new(&records), + &no_keys, + &GenerateRrsigConfig::default(), + ); + + assert!(matches!(res, Err(SigningError::NoKeysProvided))); + } + + #[test] + fn generate_rrsigs_only_for_nsecs() { + let zone_apex = "example."; + + // This is an example of generating RRSIGs for something other than a + // full zone. + let records: [Record; 1] = + [Record::from_record(mk_nsec( + zone_apex, + Class::IN, + 3600, + "next.example.", + "A NSEC RRSIG", + ))]; + + let keys: [TestCSK; 1] = [TestCSK::default()]; + + let rrsigs = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + + assert_eq!(rrsigs.len(), 1); + assert_eq!( + rrsigs[0].owner(), + &Name::::from_str("example.").unwrap() + ); + assert_eq!(rrsigs[0].class(), Class::IN); + let ZoneRecordData::Rrsig(rrsig) = rrsigs[0].data() else { + panic!("RDATA is not RRSIG"); + }; + assert_eq!(rrsig.type_covered(), Rtype::NSEC); + assert_eq!(rrsig.algorithm(), keys[0].algorithm()); + assert_eq!(rrsig.original_ttl(), Ttl::from_secs(3600)); + assert_eq!( + rrsig.signer_name(), + &Name::::from_str(zone_apex).unwrap() + ); + assert_eq!(rrsig.key_tag(), keys[0].public_key().key_tag()); + assert_eq!( + RangeInclusive::new(rrsig.inception(), rrsig.expiration()), + keys[0].signature_validity_period().unwrap() + ); + } + //------------ Helper fns ------------------------------------------------ fn mk_record( @@ -771,4 +1008,59 @@ mod tests { data, ) } + + fn mk_nsec( + owner: &str, + class: Class, + ttl_secs: u32, + next_name: &str, + types: &str, + ) -> Record> { + let owner = Name::from_str(owner).unwrap(); + let ttl = Ttl::from_secs(ttl_secs); + let next_name = Name::from_str(next_name).unwrap(); + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + Record::new(owner, class, ttl, Nsec::new(next_name, types)) + } + + struct TestCSK { + key: SigningKey, + } + + impl Default for TestCSK { + fn default() -> Self { + let (sec_bytes, pub_bytes) = + common::generate(GenerateParams::RsaSha256 { bits: 1024 }) + .unwrap(); + let key_pair = + KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); + let root = Name::::root(); + let key = SigningKey::new(root.clone(), 257, key_pair); + let key = + key.with_validity(Timestamp::from(0), Timestamp::from(100)); + Self { key } + } + } + + impl std::ops::Deref for TestCSK { + type Target = SigningKey; + + fn deref(&self) -> &Self::Target { + &self.key + } + } + + impl DesignatedSigningKey for TestCSK { + fn signs_keys(&self) -> bool { + true + } + + fn signs_zone_data(&self) -> bool { + true + } + } } diff --git a/src/sign/traits.rs b/src/sign/traits.rs index cfdcdff65..ba9a8af57 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -29,6 +29,7 @@ use crate::sign::records::{ }; use crate::sign::sign_zone; use crate::sign::signatures::rrsigs::generate_rrsigs; +use crate::sign::signatures::rrsigs::GenerateRrsigConfig; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::SigningConfig; use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; @@ -182,7 +183,7 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); +/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// @@ -335,7 +336,7 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)); +/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// @@ -504,10 +505,9 @@ where KeyStrat: SigningKeyUsageStrategy, { generate_rrsigs::<_, _, DSK, _, KeyStrat, Sort>( - expected_apex, self.owner_rrs(), keys, - false, + &GenerateRrsigConfig::new().with_zone_apex(expected_apex), ) } } From 3fc07c4b86e0ea362722bb7a09a8b8ccfd6d3373 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:48:04 +0100 Subject: [PATCH 367/569] Minor cleanup of the way test keys are generated and used by generate_rrsig() tests. --- src/sign/signatures/rrsigs.rs | 72 ++++++++++++++++++----------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index ae0167be4..b5e5e2ef1 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -623,6 +623,7 @@ where #[cfg(test)] mod tests { + use core::ops::RangeInclusive; use core::str::FromStr; use bytes::Bytes; @@ -631,7 +632,7 @@ mod tests { use crate::base::{Serial, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; use crate::rdata::{Nsec, Rrsig, A}; - use crate::sign::crypto::common::{self, GenerateParams, KeyPair}; + use crate::sign::crypto::common::KeyPair; use crate::sign::error::SignError; use crate::sign::keys::DnssecSigningKey; use crate::sign::{PublicKeyBytes, Signature}; @@ -639,23 +640,6 @@ mod tests { use crate::zonetree::StoredName; use super::*; - use core::ops::RangeInclusive; - - struct TestKey; - - impl SignRaw for TestKey { - fn algorithm(&self) -> SecAlg { - SecAlg::PRIVATEDNS - } - - fn raw_public_key(&self) -> PublicKeyBytes { - PublicKeyBytes::Ed25519([0_u8; 32].into()) - } - - fn sign_raw(&self, _data: &[u8]) -> Result { - Ok(Signature::Ed25519([0u8; 64].into())) - } - } #[test] fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { @@ -961,7 +945,8 @@ mod tests { "A NSEC RRSIG", ))]; - let keys: [TestCSK; 1] = [TestCSK::default()]; + let keys: [DesignatedTestKey; 1] = + [DesignatedTestKey::new(257, false, true)]; let rrsigs = generate_rrsigs( RecordsIter::new(&records), @@ -1027,40 +1012,57 @@ mod tests { Record::new(owner, class, ttl, Nsec::new(next_name, types)) } - struct TestCSK { - key: SigningKey, + struct TestKey; + + impl SignRaw for TestKey { + fn algorithm(&self) -> SecAlg { + SecAlg::ED25519 + } + + fn raw_public_key(&self) -> PublicKeyBytes { + PublicKeyBytes::Ed25519([0_u8; 32].into()) + } + + fn sign_raw(&self, _data: &[u8]) -> Result { + Ok(Signature::Ed25519([0u8; 64].into())) + } + } + + struct DesignatedTestKey { + key: SigningKey, + signs_keys: bool, + signs_zone_data: bool, } - impl Default for TestCSK { - fn default() -> Self { - let (sec_bytes, pub_bytes) = - common::generate(GenerateParams::RsaSha256 { bits: 1024 }) - .unwrap(); - let key_pair = - KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); + impl DesignatedTestKey { + fn new(flags: u16, signs_keys: bool, signs_zone_data: bool) -> Self { let root = Name::::root(); - let key = SigningKey::new(root.clone(), 257, key_pair); + let key = SigningKey::new(root.clone(), flags, TestKey); let key = key.with_validity(Timestamp::from(0), Timestamp::from(100)); - Self { key } + Self { + key, + signs_keys, + signs_zone_data, + } } } - impl std::ops::Deref for TestCSK { - type Target = SigningKey; + impl std::ops::Deref for DesignatedTestKey { + type Target = SigningKey; fn deref(&self) -> &Self::Target { &self.key } } - impl DesignatedSigningKey for TestCSK { + impl DesignatedSigningKey for DesignatedTestKey { fn signs_keys(&self) -> bool { - true + self.signs_keys } fn signs_zone_data(&self) -> bool { - true + self.signs_zone_data } } } From 5fc894e16985c5bce8e501e310c1e02cd955646c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 09:56:34 +0100 Subject: [PATCH 368/569] Require a version of Bytes that supports From> (as Dnskey uses Box<[u8]> internally). --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 29ce6ab41..3e80e0d07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ octseq = { version = "0.5.2", default-features = false } time = { version = "0.3.1", default-features = false } rand = { version = "0.8", optional = true } arc-swap = { version = "1.7.0", optional = true } -bytes = { version = "1.0", optional = true, default-features = false } +bytes = { version = "1.2", optional = true, default-features = false } chrono = { version = "0.4.35", optional = true, default-features = false } # 0.4.35 deprecates Duration::seconds() futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.14.2", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing From 801fd2d6783ffa9b1b69c46b4fb5f03c73134de0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 11:53:29 +0100 Subject: [PATCH 369/569] FIX: Don't sign the apex twice. --- src/sign/signatures/rrsigs.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index b5e5e2ef1..d47cdf408 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -363,11 +363,13 @@ where + From<&'static [u8]>, Sort: Sorter, { - let Some(apex_owner_rrs) = records.peek() else { + if records.peek().is_none() { // Nothing to do. return Ok(()); }; + let apex_owner_rrs = records.next().unwrap(); + let apex_rrsets = apex_owner_rrs .rrsets() .filter(|rrset| rrset.rtype() != Rtype::RRSIG); From 671da3bf7a57ff3ea6a6857ff2fb7b97af12799c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 12:02:37 +0100 Subject: [PATCH 370/569] FIX: Don't skip signing when the apex isn't matched. --- src/sign/signatures/rrsigs.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d47cdf408..5e337405d 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -363,13 +363,11 @@ where + From<&'static [u8]>, Sort: Sorter, { - if records.peek().is_none() { + let Some(apex_owner_rrs) = records.peek() else { // Nothing to do. return Ok(()); }; - let apex_owner_rrs = records.next().unwrap(); - let apex_rrsets = apex_owner_rrs .rrsets() .filter(|rrset| rrset.rtype() != Rtype::RRSIG); @@ -479,6 +477,9 @@ where } } + // Move the iterator past the processed apex owner RRs. + let _ = records.next(); + Ok(()) } From 48ec28470657c5785803172012ee34c10f65f39f Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 13:38:35 +0100 Subject: [PATCH 371/569] - Move test helper functions to a shared module. - Add a full zone test of generate_rrsigs() with default config. - Change SigningKeyUsageStrategy to return Vec and have generate_rrsigs() sort and dedup the results of its fns so that DNSKEY RRs are generated in a deterministic order and that implementers don't have to create short lived HashSets. (even creating Vec isn't ideal...) --- src/sign/denial/nsec.rs | 233 +++-------------- src/sign/mod.rs | 3 + src/sign/signatures/rrsigs.rs | 439 +++++++++++++++++++++++--------- src/sign/signatures/strategy.rs | 10 +- src/sign/test_util/mod.rs | 142 +++++++++++ 5 files changed, 507 insertions(+), 320 deletions(-) create mode 100644 src/sign/test_util/mod.rs diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 1d16bc1bd..4579805e2 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -175,28 +175,18 @@ where #[cfg(test)] mod tests { - use core::str::FromStr; - - use std::io::Read; - - use bytes::Bytes; use pretty_assertions::assert_eq; - use crate::base::Serial; - use crate::base::{iana::Class, name::FlattenInto, Name, Ttl}; - use crate::rdata::{Ns, Soa, A}; + use crate::base::Ttl; use crate::sign::records::SortedRecords; - use crate::zonefile::inplace::{Entry, Zonefile}; - use crate::zonetree::{types::StoredRecordData, StoredName}; - - use octseq::FreezeBuilder; + use crate::sign::test_util::*; use super::*; #[test] fn soa_is_required() { let mut records = SortedRecords::default(); - records.insert(mk_a("some_a.a.")).unwrap(); + records.insert(mk_a_rr("some_a.a.")).unwrap(); let res = generate_nsecs(records.owner_rrs(), false); assert!(matches!( res, @@ -207,8 +197,8 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let mut records = SortedRecords::default(); - records.insert(mk_soa("a.", "b.", "c.")).unwrap(); - records.insert(mk_soa("a.", "d.", "e.")).unwrap(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); let res = generate_nsecs(records.owner_rrs(), false); assert!(matches!( res, @@ -220,10 +210,10 @@ mod tests { fn records_outside_zone_are_ignored() { let mut records = SortedRecords::default(); - records.insert(mk_soa("b.", "d.", "e.")).unwrap(); - records.insert(mk_a("some_a.b.")).unwrap(); - records.insert(mk_soa("a.", "b.", "c.")).unwrap(); - records.insert(mk_a("some_a.a.")).unwrap(); + records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); + records.insert(mk_a_rr("some_a.b.")).unwrap(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + records.insert(mk_a_rr("some_a.a.")).unwrap(); // First generate NSECs for the total record collection. As the // collection is sorted in canonical order the a zone preceeds the b @@ -235,8 +225,8 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec("a.", Class::IN, 0, "some_a.a.", "SOA RRSIG NSEC"), - mk_nsec("some_a.a.", Class::IN, 0, "a.", "A RRSIG NSEC"), + mk_nsec_rr("a.", "some_a.a.", "SOA RRSIG NSEC"), + mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), ] ); @@ -249,8 +239,8 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec("b.", Class::IN, 0, "some_a.b.", "SOA RRSIG NSEC"), - mk_nsec("some_a.b.", Class::IN, 0, "b.", "A RRSIG NSEC"), + mk_nsec_rr("b.", "some_a.b.", "SOA RRSIG NSEC"), + mk_nsec_rr("some_a.b.", "b.", "A RRSIG NSEC"), ] ); } @@ -259,11 +249,11 @@ mod tests { fn occluded_records_are_ignored() { let mut records = SortedRecords::default(); - records.insert(mk_soa("a.", "b.", "c.")).unwrap(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records - .insert(mk_ns("some_ns.a.", "some_a.other.b.")) + .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) .unwrap(); - records.insert(mk_a("some_a.some_ns.a.")).unwrap(); + records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); @@ -271,20 +261,8 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec( - "a.", - Class::IN, - 12345, - "some_ns.a.", - "SOA RRSIG NSEC" - ), - mk_nsec( - "some_ns.a.", - Class::IN, - 12345, - "a.", - "NS RRSIG NSEC" - ), + mk_nsec_rr("a.", "some_ns.a.", "SOA RRSIG NSEC"), + mk_nsec_rr("some_ns.a.", "a.", "NS RRSIG NSEC"), ] ); @@ -296,22 +274,16 @@ mod tests { fn expect_dnskeys_at_the_apex() { let mut records = SortedRecords::default(); - records.insert(mk_soa("a.", "b.", "c.")).unwrap(); - records.insert(mk_a("some_a.a.")).unwrap(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + records.insert(mk_a_rr("some_a.a.")).unwrap(); let nsecs = generate_nsecs(records.owner_rrs(), true).unwrap(); assert_eq!( nsecs, [ - mk_nsec( - "a.", - Class::IN, - 0, - "some_a.a.", - "SOA DNSKEY RRSIG NSEC" - ), - mk_nsec("some_a.a.", Class::IN, 0, "a.", "A RRSIG NSEC"), + mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"), + mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), ] ); } @@ -331,41 +303,19 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec( + mk_nsec_rr( "example.", - Class::IN, - 12345, "a.example", "NS SOA MX RRSIG NSEC DNSKEY" ), - mk_nsec( - "a.example.", - Class::IN, - 12345, - "ai.example", - "NS DS RRSIG NSEC" - ), - mk_nsec( + mk_nsec_rr("a.example.", "ai.example", "NS DS RRSIG NSEC"), + mk_nsec_rr( "ai.example.", - Class::IN, - 12345, "b.example", "A HINFO AAAA RRSIG NSEC" ), - mk_nsec( - "b.example.", - Class::IN, - 12345, - "ns1.example", - "NS RRSIG NSEC" - ), - mk_nsec( - "ns1.example.", - Class::IN, - 12345, - "ns2.example", - "A RRSIG NSEC" - ), + mk_nsec_rr("b.example.", "ns1.example", "NS RRSIG NSEC"), + mk_nsec_rr("ns1.example.", "ns2.example", "A RRSIG NSEC"), // The next record also validates that we comply with // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when @@ -377,38 +327,12 @@ mod tests { // Next Domain Name field without any wildcard expansion. // [RFC4035] describes the impact of wildcards on // authenticated denial of existence." - mk_nsec( - "ns2.example.", - Class::IN, - 12345, - "*.w.example", - "A RRSIG NSEC" - ), - mk_nsec( - "*.w.example.", - Class::IN, - 12345, - "x.w.example", - "MX RRSIG NSEC" - ), - mk_nsec( - "x.w.example.", - Class::IN, - 12345, - "x.y.w.example", - "MX RRSIG NSEC" - ), - mk_nsec( - "x.y.w.example.", - Class::IN, - 12345, - "xx.example", - "MX RRSIG NSEC" - ), - mk_nsec( + mk_nsec_rr("ns2.example.", "*.w.example", "A RRSIG NSEC"), + mk_nsec_rr("*.w.example.", "x.w.example", "MX RRSIG NSEC"), + mk_nsec_rr("x.w.example.", "x.y.w.example", "MX RRSIG NSEC"), + mk_nsec_rr("x.y.w.example.", "xx.example", "MX RRSIG NSEC"), + mk_nsec_rr( "xx.example.", - Class::IN, - 12345, "example", "A HINFO AAAA RRSIG NSEC" ) @@ -477,101 +401,10 @@ mod tests { // The "rfc4035-appendix-A.zone" file that we load has been modified // compared to the original to include a glue A record at b.example. // We can verify that an NSEC RR was NOT created for that name. - let name = mk_name::("b.example."); + let name = mk_name("b.example."); let nsec = nsecs.iter().find(|rr| rr.owner() == &name).unwrap(); assert!(nsec.data().types().contains(Rtype::NSEC)); assert!(nsec.data().types().contains(Rtype::RRSIG)); assert!(!nsec.data().types().contains(Rtype::A)); } - - //------------ Helper fns ------------------------------------------------ - - fn bytes_to_records( - mut zonefile: impl Read, - ) -> SortedRecords { - let reader = Zonefile::load(&mut zonefile).unwrap(); - let mut records = SortedRecords::default(); - for entry in reader { - let entry = entry.unwrap(); - if let Entry::Record(record) = entry { - records.insert(record.flatten_into()).unwrap() - } - } - records - } - - fn mk_nsec( - owner: &str, - class: Class, - ttl_secs: u32, - next_name: &str, - types: &str, - ) -> Record> { - let owner = Name::from_str(owner).unwrap(); - let ttl = Ttl::from_secs(ttl_secs); - let next_name = Name::from_str(next_name).unwrap(); - let mut builder = RtypeBitmap::::builder(); - for rtype in types.split_whitespace() { - builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); - } - let types = builder.finalize(); - Record::new(owner, class, ttl, Nsec::new(next_name, types)) - } - - fn mk_name(name: &str) -> Name - where - Octs: FromBuilder, - ::Builder: EmptyBuilder - + FreezeBuilder - + AsRef<[u8]> - + AsMut<[u8]>, - { - Name::::from_str(name).unwrap() - } - - fn mk_soa( - name: &str, - mname: &str, - rname: &str, - ) -> Record, ZoneRecordData>> { - let zone_apex_name = mk_name::(name); - let soa = ZoneRecordData::::Soa(Soa::new( - mk_name::(mname), - mk_name::(rname), - Serial::now(), - Ttl::ZERO, - Ttl::ZERO, - Ttl::ZERO, - Ttl::ZERO, - )); - Record::new(zone_apex_name, Class::IN, Ttl::ZERO, soa) - } - - fn mk_a( - name: &str, - ) -> Record, ZoneRecordData>> { - let a_name = mk_name::(name); - let a = - ZoneRecordData::::A(A::from_str("1.2.3.4").unwrap()); - Record::new(a_name, Class::IN, Ttl::ZERO, a) - } - - fn mk_ns( - name: &str, - nsdname: &str, - ) -> Record, ZoneRecordData>> { - let name = mk_name::(name); - let nsdname = mk_name::(nsdname); - let ns = ZoneRecordData::::Ns(Ns::new(nsdname)); - Record::new(name, Class::IN, Ttl::ZERO, ns) - } - - #[allow(clippy::type_complexity)] - fn contains_owner( - nsecs: &[Record, Nsec>>], - name: &str, - ) -> bool { - let name = mk_name::(name); - nsecs.iter().any(|rr| rr.owner() == &name) - } } diff --git a/src/sign/mod.rs b/src/sign/mod.rs index be2618dcd..f8a360a43 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -110,6 +110,9 @@ pub mod records; pub mod signatures; pub mod traits; +#[cfg(test)] +pub mod test_util; + pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; pub use self::config::SigningConfig; diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 5e337405d..4373dd775 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -4,7 +4,6 @@ use core::fmt::Display; use core::marker::{PhantomData, Send}; use std::boxed::Box; -use std::collections::HashSet; use std::string::ToString; use std::vec::Vec; @@ -162,16 +161,22 @@ where return Err(SigningError::NoKeysProvided); } - let dnskey_signing_key_idxs = + let mut dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype(keys, Some(Rtype::DNSKEY)); + dnskey_signing_key_idxs.sort(); + dnskey_signing_key_idxs.dedup(); - let non_dnskey_signing_key_idxs = + let mut non_dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype(keys, None); + non_dnskey_signing_key_idxs.sort(); + non_dnskey_signing_key_idxs.dedup(); - let keys_in_use_idxs: HashSet<_> = non_dnskey_signing_key_idxs + let mut keys_in_use_idxs: Vec<_> = non_dnskey_signing_key_idxs .iter() .chain(dnskey_signing_key_idxs.iter()) .collect(); + keys_in_use_idxs.sort(); + keys_in_use_idxs.dedup(); if keys_in_use_idxs.is_empty() { return Err(SigningError::NoSuitableKeysFound); @@ -201,7 +206,7 @@ where zone_class, &dnskey_signing_key_idxs, &non_dnskey_signing_key_idxs, - keys_in_use_idxs, + &keys_in_use_idxs, &mut res, &mut reusable_scratch, )?; @@ -277,9 +282,9 @@ where fn log_keys_in_use( keys: &[DSK], - dnskey_signing_key_idxs: &HashSet, - non_dnskey_signing_key_idxs: &HashSet, - keys_in_use_idxs: &HashSet<&usize>, + dnskey_signing_key_idxs: &[usize], + non_dnskey_signing_key_idxs: &[usize], + keys_in_use_idxs: &[&usize], ) where DSK: DesignatedSigningKey, Inner: SignRaw, @@ -337,9 +342,9 @@ fn generate_apex_rrsigs( >, zone_apex: &N, zone_class: crate::base::iana::Class, - dnskey_signing_key_idxs: &HashSet, - non_dnskey_signing_key_idxs: &HashSet, - keys_in_use_idxs: HashSet<&usize>, + dnskey_signing_key_idxs: &[usize], + non_dnskey_signing_key_idxs: &[usize], + keys_in_use_idxs: &[&usize], res: &mut Vec>>, reusable_scratch: &mut Vec, ) -> Result<(), SigningError> @@ -630,19 +635,25 @@ mod tests { use core::str::FromStr; use bytes::Bytes; + use pretty_assertions::assert_eq; use crate::base::iana::{Class, SecAlg}; use crate::base::{Serial, Ttl}; - use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; - use crate::rdata::{Nsec, Rrsig, A}; + use crate::rdata::dnssec::Timestamp; + use crate::rdata::{Rrsig, A}; use crate::sign::crypto::common::KeyPair; use crate::sign::error::SignError; use crate::sign::keys::DnssecSigningKey; - use crate::sign::{PublicKeyBytes, Signature}; + use crate::sign::test_util::*; + use crate::sign::{test_util, PublicKeyBytes, Signature}; use crate::zonetree::types::StoredRecordData; - use crate::zonetree::StoredName; + use crate::zonetree::{StoredName, StoredRecord}; use super::*; + use crate::sign::keys::keymeta::IntendedKeyPurpose; + + const TEST_INCEPTION: u32 = 0; + const TEST_EXPIRATION: u32 = 100; #[test] fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { @@ -657,8 +668,6 @@ mod tests { // We can use any class as RRSIGs are class independent. let records = [mk_record( "www.example.com.", - Class::CH, - 12345, ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), )]; let rrset = Rrset::new(&records); @@ -720,11 +729,8 @@ mod tests { // 3.1.3. The Labels Field // ... // ""*.example.com." has a Labels field value of 2" - // We can use any class as RRSIGs are class independent. let records = [mk_record( "*.example.com.", - Class::CH, - 12345, ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), )]; let rrset = Rrset::new(&records); @@ -762,12 +768,7 @@ mod tests { ) .unwrap(); - let records = [mk_record( - "any.", - Class::CH, - 12345, - ZoneRecordData::Rrsig(dummy_rrsig), - )]; + let records = [mk_record("any.", ZoneRecordData::Rrsig(dummy_rrsig))]; let rrset = Rrset::new(&records); let res = sign_rrset(&key, &rrset, &apex_owner); @@ -798,8 +799,6 @@ mod tests { let records = [mk_record( "any.", - Class::CH, - 12345, ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), )]; let rrset = Rrset::new(&records); @@ -902,7 +901,7 @@ mod tests { } #[test] - fn generate_rrsigs_with_empty_zone_succeeds() { + fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { let records: [Record; 0] = []; let no_keys: [DnssecSigningKey; 0] = []; @@ -915,11 +914,9 @@ mod tests { } #[test] - fn generate_rrsigs_without_keys_fails_for_non_empty_zone() { + fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { let records: [Record; 1] = [mk_record( "example.", - Class::IN, - 0, ZoneRecordData::A(A::from_str("127.0.0.1").unwrap()), )]; let no_keys: [DnssecSigningKey; 0] = []; @@ -934,46 +931,48 @@ mod tests { } #[test] - fn generate_rrsigs_only_for_nsecs() { + fn generate_rrsigs_for_partial_zone() { let zone_apex = "example."; // This is an example of generating RRSIGs for something other than a - // full zone. + // full zone, in this case just for NSECs, as is done by sign_zone(). let records: [Record; 1] = - [Record::from_record(mk_nsec( + [Record::from_record(mk_nsec_rr( zone_apex, - Class::IN, - 3600, "next.example.", "A NSEC RRSIG", ))]; - let keys: [DesignatedTestKey; 1] = - [DesignatedTestKey::new(257, false, true)]; + // Prepare a zone signing key and a key signing key. + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + // Generate RRSIGs. Use the default signing config and thus also the + // DefaultSigningKeyUsageStrategy which will honour the purpose of the + // key when selecting a key to use for signing DNSKEY RRs or other + // zone RRs. We supply the zone apex because we are not supplying an + // entire zone complete with SOA. let rrsigs = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default() + .with_zone_apex(&mk_name(zone_apex)), ) .unwrap(); + // Check the generated RRSIG record assert_eq!(rrsigs.len(), 1); - assert_eq!( - rrsigs[0].owner(), - &Name::::from_str("example.").unwrap() - ); + assert_eq!(rrsigs[0].owner(), &mk_name("example.")); assert_eq!(rrsigs[0].class(), Class::IN); + assert_eq!(rrsigs[0].rtype(), Rtype::RRSIG); + + // Check the contained RRSIG RDATA let ZoneRecordData::Rrsig(rrsig) = rrsigs[0].data() else { panic!("RDATA is not RRSIG"); }; assert_eq!(rrsig.type_covered(), Rtype::NSEC); assert_eq!(rrsig.algorithm(), keys[0].algorithm()); - assert_eq!(rrsig.original_ttl(), Ttl::from_secs(3600)); - assert_eq!( - rrsig.signer_name(), - &Name::::from_str(zone_apex).unwrap() - ); + assert_eq!(rrsig.original_ttl(), TEST_TTL); + assert_eq!(rrsig.signer_name(), &mk_name(zone_apex)); assert_eq!(rrsig.key_tag(), keys[0].public_key().key_tag()); assert_eq!( RangeInclusive::new(rrsig.inception(), rrsig.expiration()), @@ -981,91 +980,301 @@ mod tests { ); } - //------------ Helper fns ------------------------------------------------ + #[test] + fn generate_rrsigs_for_complete_zone() { + // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A + let zonefile = include_bytes!( + "../../../test-data/zonefiles/rfc4035-appendix-A.zone" + ); - fn mk_record( - owner: &str, - class: Class, - ttl_secs: u32, - data: ZoneRecordData>, - ) -> Record, ZoneRecordData>> { - Record::new( - Name::from_str(owner).unwrap(), - class, - Ttl::from_secs(ttl_secs), - data, + // Load the zone to generate RRSIGs for. + let records = bytes_to_records(&zonefile[..]); + + // Prepare a zone signing key and a key signing key. + let keys = [ + mk_dnssec_signing_key(IntendedKeyPurpose::KSK), + mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), + ]; + + // Generate DNSKEYs and RRSIGs. Use the default signing config and + // thus also the DefaultSigningKeyUsageStrategy which will honour the + // purpose of the key when selecting a key to use for signing DNSKEY + // RRs or other zone RRs. + let generated_records = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), ) - } + .unwrap(); - fn mk_nsec( - owner: &str, - class: Class, - ttl_secs: u32, - next_name: &str, - types: &str, - ) -> Record> { - let owner = Name::from_str(owner).unwrap(); - let ttl = Ttl::from_secs(ttl_secs); - let next_name = Name::from_str(next_name).unwrap(); - let mut builder = RtypeBitmap::::builder(); - for rtype in types.split_whitespace() { - builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); - } - let types = builder.finalize(); - Record::new(owner, class, ttl, Nsec::new(next_name, types)) + let ksk = keys[0].public_key().to_dnskey().convert(); + let zsk = keys[1].public_key().to_dnskey().convert(); + + // Check the generated records. + // + // The records should be in a fixed canonical order because the input + // records must be in canonical order, with the exception of the added + // DNSKEY RRs which will be ordered in the order in the supplied + // collection of keys to sign with. + // + // We check each record explicitly by index because assert_eq() on an + // array of objects that includes Rrsig produces hard to read output + // due to the large RRSIG signature bytes being printed one byte per + // line. + + // NOTE: As we only invoked generate_rrsigs() and not generate_nsecs() + // there will not be any RRSIGs covering NSEC records. + + // -- example. + + // DNSKEY records should have been generated for the apex for both of + // the keys that we used to sign the zone. + assert_eq!(generated_records[0], mk_dnskey_rr("example.", &ksk)); + assert_eq!(generated_records[1], mk_dnskey_rr("example.", &zsk)); + + // RRSIG records should have been generated for the zone apex records, + // one RRSIG per ZSK used (we used one ZSK so only one RRSIG per + // record). + assert_eq!( + generated_records[2], + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &zsk) + ); + assert_eq!( + generated_records[3], + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &zsk) + ); + assert_eq!( + generated_records[4], + mk_rrsig_rr("example.", Rtype::MX, 1, "example.", &zsk) + ); + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. + // Including RRSIG RRs in a Zone. .. "There MUST be an RRSIG for each + // RRset using at least one DNSKEY of each algorithm in the zone + // apex DNSKEY RRset. The apex DNSKEY RRset itself MUST be signed + // by each algorithm appearing in the DS RRset located at the + // delegating parent (if any)." + // + // In the real world a DNSSEC signed zone is only valid when part of a + // hierarchy such that the signatures can be trusted because there + // exists a valid chain of trust up to a root, and each parent zone + // specifies via one or more DS records which DNSKEY RRs the child + // zone should be signed with. + // + // In our contrived test example we don't have a hierarchy or a parent + // zone so there are no DS RRs to consider. The keys that the zone + // should be signed with are determined by the keys passed to + // generate_rrsigs(). In our case that means that the DNSKEY RR RRSIG + // should have been generated using the KSK because the + // DefaultSigningKeyUsageStrategy selects keys to sign DNSKEY RRs + // based on whether they return true or not from + // `DesignatedSigningKey::signs_keys()` and we are using the + // `DnssecSigningKey` impl of `DesignatedSigningKey` which selects + // keys based on their `IntendedKeyPurpose` which we assigned above + // when creating the keys. + assert_eq!( + generated_records[5], + mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &ksk) + ); + + // -- a.example. + + // NOTE: Per RFC 4035 there is NOT an RRSIG for a.example NS because: + // + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "The NS RRset that appears at the zone apex name MUST be signed, + // but the NS RRsets that appear at delegation points (that is, the + // NS RRsets in the parent zone that delegate the name to the child + // zone's name servers) MUST NOT be signed." + + assert_eq!( + generated_records[6], + mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", &zsk) + ); + + // -- ns1.a.example. + // ns2.a.example. + + // NOTE: Per RFC 4035 there is NOT an RRSIG for ns1.a.example A + // or ns2.a.example because: + // + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. + // Including RRSIG RRs in a Zone "For each authoritative RRset in a + // signed zone, there MUST be at least one RRSIG record..." ... AND + // ... "Glue address RRsets associated with delegations MUST NOT be + // signed." + // + // ns1.a.example is part of the a.example zone which was delegated + // above and so we are not authoritative for it. + // + // Further, ns1.a.example A is a glue record because a.example NS + // refers to it by name but in order for a recursive resolver to + // follow the delegation to the child zones' nameservers it has to + // know their IP address, and in this case the nameserver name falls + // inside the child zone so strictly speaking only the child zone is + // authoritative for it, yet the resolver can't ask the child zone + // nameserver unless it knows its IP address, hence the need for glue + // in the parent zone. + + // -- ai.example. + + assert_eq!( + generated_records[7], + mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", &zsk) + ); + assert_eq!( + generated_records[8], + mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", &zsk) + ); + assert_eq!( + generated_records[9], + mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", &zsk) + ); + + // -- b.example. + + // NOTE: There is no RRSIG for b.example NS for the same reason that + // there is no RRSIG for a.example. + // + // Also, there is no RRSIG for b.example A because b.example is + // delegated and thus we are not authoritative for records in that + // zone. + + // -- ns1.b.example. + // ns2.b.example. + + // NOTE: There is no RRSIG for ns1.b.example or ns2.b.example for + // the same reason that there are no RRSIGs ofr ns1.a.example or + // ns2.a.example, as described above. + + // -- ns1.example. + + assert_eq!( + generated_records[10], + mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", &zsk) + ); + + // -- ns2.example. + + assert_eq!( + generated_records[11], + mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", &zsk) + ); + + // -- *.w.example. + + assert_eq!( + generated_records[12], + mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", &zsk) + ); + + // -- x.w.example. + + assert_eq!( + generated_records[13], + mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", &zsk) + ); + + // -- x.y.w.example. + + assert_eq!( + generated_records[14], + mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", &zsk) + ); + + // -- xx.example. + + assert_eq!( + generated_records[15], + mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", &zsk) + ); + assert_eq!( + generated_records[16], + mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", &zsk) + ); + assert_eq!( + generated_records[17], + mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", &zsk) + ); + + // No other records should have been generated. + + assert_eq!(generated_records.len(), 18); } - struct TestKey; + //------------ Helper fns ------------------------------------------------ - impl SignRaw for TestKey { - fn algorithm(&self) -> SecAlg { - SecAlg::ED25519 - } + fn mk_dnssec_signing_key( + purpose: IntendedKeyPurpose, + ) -> DnssecSigningKey { + // Note: The flags value has no impact on the role the key will play + // in signing, that is instead determined by its designated purpose + // AND the SigningKeyUsageStrategy in use. + let flags = match purpose { + IntendedKeyPurpose::KSK => 257, + IntendedKeyPurpose::ZSK => 256, + IntendedKeyPurpose::CSK => 257, + IntendedKeyPurpose::Inactive => 0, + }; - fn raw_public_key(&self) -> PublicKeyBytes { - PublicKeyBytes::Ed25519([0_u8; 32].into()) - } + let key = SigningKey::new(StoredName::root_bytes(), flags, TestKey); - fn sign_raw(&self, _data: &[u8]) -> Result { - Ok(Signature::Ed25519([0u8; 64].into())) - } + let key = key.with_validity( + Timestamp::from(TEST_INCEPTION), + Timestamp::from(TEST_EXPIRATION), + ); + + DnssecSigningKey::new(key, purpose) } - struct DesignatedTestKey { - key: SigningKey, - signs_keys: bool, - signs_zone_data: bool, + fn mk_dnskey_rr(name: &str, dnskey: &Dnskey) -> StoredRecord { + test_util::mk_dnskey_rr( + name, + dnskey.flags(), + dnskey.algorithm(), + dnskey.public_key(), + ) } - impl DesignatedTestKey { - fn new(flags: u16, signs_keys: bool, signs_zone_data: bool) -> Self { - let root = Name::::root(); - let key = SigningKey::new(root.clone(), flags, TestKey); - let key = - key.with_validity(Timestamp::from(0), Timestamp::from(100)); - Self { - key, - signs_keys, - signs_zone_data, - } - } + fn mk_rrsig_rr( + name: &str, + covered_rtype: Rtype, + labels: u8, + signer_name: &str, + dnskey: &Dnskey, + ) -> StoredRecord { + test_util::mk_rrsig_rr( + name, + covered_rtype, + &dnskey.algorithm(), + labels, + TEST_EXPIRATION, + TEST_INCEPTION, + dnskey.key_tag(), + signer_name, + TEST_SIGNATURE, + ) } - impl std::ops::Deref for DesignatedTestKey { - type Target = SigningKey; + //------------ TestKey --------------------------------------------------- + + const TEST_SIGNATURE_RAW: [u8; 64] = [0u8; 64]; + const TEST_SIGNATURE: Bytes = Bytes::from_static(&TEST_SIGNATURE_RAW); + + struct TestKey; - fn deref(&self) -> &Self::Target { - &self.key + impl SignRaw for TestKey { + fn algorithm(&self) -> SecAlg { + SecAlg::ED25519 } - } - impl DesignatedSigningKey for DesignatedTestKey { - fn signs_keys(&self) -> bool { - self.signs_keys + fn raw_public_key(&self) -> PublicKeyBytes { + PublicKeyBytes::Ed25519([0_u8; 32].into()) } - fn signs_zone_data(&self) -> bool { - self.signs_zone_data + fn sign_raw(&self, _data: &[u8]) -> Result { + Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) } } } diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs index 861d22674..2900ca2d5 100644 --- a/src/sign/signatures/strategy.rs +++ b/src/sign/signatures/strategy.rs @@ -1,11 +1,11 @@ -use std::collections::HashSet; - use crate::base::Rtype; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::SignRaw; +use std::vec::Vec; //------------ SigningKeyUsageStrategy --------------------------------------- +// TODO: Don't return Vec but instead "mark" chosen keys instead ala LDNS? pub trait SigningKeyUsageStrategy where Octs: AsRef<[u8]>, @@ -18,7 +18,7 @@ where >( candidate_keys: &[DSK], rtype: Option, - ) -> HashSet { + ) -> Vec { if matches!(rtype, Some(Rtype::DNSKEY)) { Self::filter_keys(candidate_keys, |k| k.signs_keys()) } else { @@ -29,12 +29,12 @@ where fn filter_keys>( candidate_keys: &[DSK], filter: fn(&DSK) -> bool, - ) -> HashSet { + ) -> Vec { candidate_keys .iter() .enumerate() .filter_map(|(i, k)| filter(k).then_some(i)) - .collect::>() + .collect() } } diff --git a/src/sign/test_util/mod.rs b/src/sign/test_util/mod.rs new file mode 100644 index 000000000..6eeca9c72 --- /dev/null +++ b/src/sign/test_util/mod.rs @@ -0,0 +1,142 @@ +use core::str::FromStr; + +use std::io::Read; + +use bytes::Bytes; + +use crate::base::iana::{Class, SecAlg}; +use crate::base::name::FlattenInto; +use crate::base::{Record, Rtype, Serial, Ttl}; +use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; +use crate::rdata::{Dnskey, Ns, Nsec, Rrsig, Soa, A}; +use crate::zonefile::inplace::{Entry, Zonefile}; +use crate::zonetree::types::StoredRecordData; +use crate::zonetree::{StoredName, StoredRecord}; + +use super::records::SortedRecords; + +pub(crate) const TEST_TTL: Ttl = Ttl::from_secs(3600); + +pub(crate) fn bytes_to_records( + mut zonefile: impl Read, +) -> SortedRecords { + let reader = Zonefile::load(&mut zonefile).unwrap(); + let mut records = SortedRecords::default(); + for entry in reader { + let entry = entry.unwrap(); + if let Entry::Record(record) = entry { + records.insert(record.flatten_into()).unwrap() + } + } + records +} + +pub(crate) fn mk_name(name: &str) -> StoredName { + StoredName::from_str(name).unwrap() +} + +pub(crate) fn mk_record(owner: &str, data: StoredRecordData) -> StoredRecord { + Record::new(mk_name(owner), Class::IN, TEST_TTL, data) +} + +pub(crate) fn mk_nsec_rr( + owner: &str, + next_name: &str, + types: &str, +) -> Record> { + let owner = mk_name(owner); + let next_name = mk_name(next_name); + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + Record::new(owner, Class::IN, TEST_TTL, Nsec::new(next_name, types)) +} + +pub(crate) fn mk_soa_rr( + name: &str, + mname: &str, + rname: &str, +) -> StoredRecord { + let soa = Soa::new( + mk_name(mname), + mk_name(rname), + Serial::now(), + TEST_TTL, + TEST_TTL, + TEST_TTL, + TEST_TTL, + ); + mk_record(name, soa.into()) +} + +pub(crate) fn mk_a_rr(name: &str) -> StoredRecord { + mk_record(name, A::from_str("1.2.3.4").unwrap().into()) +} + +pub(crate) fn mk_ns_rr(name: &str, nsdname: &str) -> StoredRecord { + let nsdname = mk_name(nsdname); + mk_record(name, Ns::new(nsdname).into()) +} + +pub(crate) fn mk_dnskey_rr( + name: &str, + flags: u16, + algorithm: SecAlg, + public_key: &Bytes, +) -> StoredRecord { + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2 + // 2.1.2. The Protocol Field + // "The Protocol Field MUST have value 3, and the DNSKEY RR MUST be + // treated as invalid during signature verification if it is found to + // be some value other than 3." + mk_record( + name, + Dnskey::new(flags, 3, algorithm, public_key.clone()) + .unwrap() + .into(), + ) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn mk_rrsig_rr( + name: &str, + covered_rtype: Rtype, + algorithm: &SecAlg, + labels: u8, + expiration: u32, + inception: u32, + key_tag: u16, + signer_name: &str, + signature: Bytes, +) -> StoredRecord { + let signer_name = mk_name(signer_name); + let expiration = Timestamp::from(expiration); + let inception = Timestamp::from(inception); + mk_record( + name, + Rrsig::new( + covered_rtype, + *algorithm, + labels, + TEST_TTL, + expiration, + inception, + key_tag, + signer_name, + signature, + ) + .unwrap() + .into(), + ) +} + +#[allow(clippy::type_complexity)] +pub(crate) fn contains_owner( + nsecs: &[Record>], + name: &str, +) -> bool { + let name = mk_name(name); + nsecs.iter().any(|rr| rr.owner() == &name) +} From 391d7dca6b6779c4ff6d11294de901d078234695 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:39:06 +0100 Subject: [PATCH 372/569] Use SmallVec instead of Vec, to avoid allocation for a small temporary collection. --- src/sign/signatures/strategy.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs index 2900ca2d5..952baa087 100644 --- a/src/sign/signatures/strategy.rs +++ b/src/sign/signatures/strategy.rs @@ -1,11 +1,11 @@ +use smallvec::SmallVec; + use crate::base::Rtype; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::SignRaw; -use std::vec::Vec; //------------ SigningKeyUsageStrategy --------------------------------------- -// TODO: Don't return Vec but instead "mark" chosen keys instead ala LDNS? pub trait SigningKeyUsageStrategy where Octs: AsRef<[u8]>, @@ -18,7 +18,7 @@ where >( candidate_keys: &[DSK], rtype: Option, - ) -> Vec { + ) -> SmallVec<[usize; 4]> { if matches!(rtype, Some(Rtype::DNSKEY)) { Self::filter_keys(candidate_keys, |k| k.signs_keys()) } else { @@ -29,7 +29,7 @@ where fn filter_keys>( candidate_keys: &[DSK], filter: fn(&DSK) -> bool, - ) -> Vec { + ) -> SmallVec<[usize; 4]> { candidate_keys .iter() .enumerate() From 406818fb380559f9583882a4988ffa670ab1f6e0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:40:22 +0100 Subject: [PATCH 373/569] And missing line break. --- src/sign/signatures/strategy.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs index 952baa087..6ed4d293c 100644 --- a/src/sign/signatures/strategy.rs +++ b/src/sign/signatures/strategy.rs @@ -39,6 +39,7 @@ where } //------------ DefaultSigningKeyUsageStrategy -------------------------------- + pub struct DefaultSigningKeyUsageStrategy; impl SigningKeyUsageStrategy From b2812610c999eccc54e8856fe1829ac31d329c6b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 14:50:59 +0100 Subject: [PATCH 374/569] Fix compilation error. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 3e80e0d07..8ea7bcde2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,7 @@ 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", "dep:secrecy", "unstable-validate", "time/formatting", "tracing"] +unstable-sign = ["std", "dep:secrecy", "dep:smallvec", "time/formatting", "tracing", "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"] From fdb5c66c7c5d4a1f9b99a33aa49eadb5181af4da Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 15:59:44 +0100 Subject: [PATCH 375/569] FIX: At least one key for both roles is needed for signing. --- src/sign/signatures/rrsigs.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 4373dd775..a80e16171 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -163,11 +163,17 @@ where let mut dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype(keys, Some(Rtype::DNSKEY)); + if dnskey_signing_key_idxs.is_empty() { + return Err(SigningError::NoSuitableKeysFound); + } dnskey_signing_key_idxs.sort(); dnskey_signing_key_idxs.dedup(); let mut non_dnskey_signing_key_idxs = KeyStrat::select_signing_keys_for_rtype(keys, None); + if non_dnskey_signing_key_idxs.is_empty() { + return Err(SigningError::NoSuitableKeysFound); + } non_dnskey_signing_key_idxs.sort(); non_dnskey_signing_key_idxs.dedup(); @@ -178,10 +184,6 @@ where keys_in_use_idxs.sort(); keys_in_use_idxs.dedup(); - if keys_in_use_idxs.is_empty() { - return Err(SigningError::NoSuitableKeysFound); - } - if log::log_enabled!(Level::Debug) { log_keys_in_use( keys, From e701addac8781cdf3d4135da3370a59b26145721 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:00:09 +0100 Subject: [PATCH 376/569] Additional RustDoc comments. --- src/sign/signatures/rrsigs.rs | 2 ++ src/sign/signatures/strategy.rs | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index a80e16171..7fc623127 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -40,6 +40,8 @@ pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { } impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { + /// Like [`Self::default()`] but gives control over the SigningKeyStrategy + /// and Sorter used. pub fn new() -> Self { Self { add_used_dnskeys: false, diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs index 6ed4d293c..ba2d0abe4 100644 --- a/src/sign/signatures/strategy.rs +++ b/src/sign/signatures/strategy.rs @@ -6,6 +6,10 @@ use crate::sign::SignRaw; //------------ SigningKeyUsageStrategy --------------------------------------- +// Ala ldns-signzone the default strategy signs with a minimal number of keys +// to keep the response size for the DNSKEY query small, only keys designated +// as being used to sign apex DNSKEY RRs (usually keys with the Secure Entry +// Point (SEP) flag set) will be used to sign DNSKEY RRs. pub trait SigningKeyUsageStrategy where Octs: AsRef<[u8]>, From c5cdf3c88d26d7a7e9e8336e2bdbc92eb5cd1259 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:03:29 +0100 Subject: [PATCH 377/569] FIX: Doc tests broken by recent logic fix. --- src/sign/traits.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/traits.rs b/src/sign/traits.rs index ba9a8af57..49c6d485a 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -189,7 +189,7 @@ where /// /// // Assign signature validity period and operator intent to the keys. /// let key = key.with_validity(Timestamp::now(), Timestamp::now()); -/// let keys = [DnssecSigningKey::from(key)]; +/// let keys = [DnssecSigningKey::new_csk(key)]; /// /// // Create a signing configuration. /// let mut signing_config = SigningConfig::default(); @@ -342,7 +342,7 @@ where /// /// // Assign signature validity period and operator intent to the keys. /// let key = key.with_validity(Timestamp::now(), Timestamp::now()); -/// let keys = [DnssecSigningKey::from(key)]; +/// let keys = [DnssecSigningKey::new_csk(key)]; /// /// // Create a signing configuration. /// let mut signing_config = SigningConfig::default(); From 5b4c4fe285239a11ae5b528932765211a2d0303b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:04:20 +0100 Subject: [PATCH 378/569] Default to adding missing DNSKEY RRs, as RFC 4035 section 2.1 requires it (SHOULD). --- src/sign/signatures/rrsigs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 7fc623127..5f3f04e50 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -44,13 +44,13 @@ impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { /// and Sorter used. pub fn new() -> Self { Self { - add_used_dnskeys: false, + add_used_dnskeys: true, zone_apex: None, _phantom: Default::default(), } } - pub fn with_add_used_dns_keys(mut self) -> Self { + pub fn without_adding_used_dns_keys(mut self) -> Self { self.add_used_dnskeys = true; self } From 294770d71be9003d838ac151e2de6cd757a79b15 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:05:47 +0100 Subject: [PATCH 379/569] FIX: Adding records to SortedRecords via iterator should also use extend, otherwise inserts via a large iterator will sort per insert which is slow comparing to dedup and sort after extend (and will make use of the Sorter too). --- src/sign/records.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/sign/records.rs b/src/sign/records.rs index 20d888df3..ee2519307 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -341,12 +341,11 @@ where N: Send, D: Send, Sort: Sorter, + Self: Extend>, { fn from_iter>>(iter: T) -> Self { let mut res = Self::new(); - for item in iter { - let _ = res.insert(item); - } + res.extend(iter); res } } From 614d81592a8284d63ac412219e06a991c7a668b0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:06:25 +0100 Subject: [PATCH 380/569] Better parameter name. --- src/sign/signatures/rrsigs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 5f3f04e50..c4e97433f 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -349,7 +349,7 @@ fn generate_apex_rrsigs( dnskey_signing_key_idxs: &[usize], non_dnskey_signing_key_idxs: &[usize], keys_in_use_idxs: &[&usize], - res: &mut Vec>>, + generated_rrs: &mut Vec>>, reusable_scratch: &mut Vec, ) -> Result<(), SigningError> where @@ -448,7 +448,7 @@ where if config.add_used_dnskeys && is_new_dnskey { // Add the DNSKEY RR to the set of new RRs to output for the zone. - res.push(Record::new( + generated_rrs.push(Record::new( zone_apex.clone(), zone_class, dnskey_rrset_ttl, @@ -476,7 +476,7 @@ where for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { let rrsig_rr = sign_rrset_in(key, &rrset, zone_apex, reusable_scratch)?; - res.push(rrsig_rr); + generated_rrs.push(rrsig_rr); trace!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", rrset.iter().len(), From f6c2ce5ef70fe0f1cb55a136dfbb3da1404ada6e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:06:59 +0100 Subject: [PATCH 381/569] Minor improvements. --- src/sign/signatures/rrsigs.rs | 34 ++++++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index c4e97433f..0b6c2b458 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -7,6 +7,7 @@ use std::boxed::Box; use std::string::ToString; use std::vec::Vec; +use log::Level; use octseq::builder::FromBuilder; use octseq::{OctetsFrom, OctetsInto}; use tracing::{debug, trace}; @@ -27,8 +28,7 @@ use crate::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; -use crate::sign::traits::{SignRaw, SortedExtend}; -use log::Level; +use crate::sign::traits::SignRaw; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { @@ -395,16 +395,14 @@ where return Err(SigningError::SoaRecordCouldNotBeDetermined); } + // Get the SOA RR. let soa_rr = soa_rrs.first(); - // Generate or extend the DNSKEY RRSET with the keys that we will sign - // apex DNSKEY RRs and zone RRs with. + // Find any existing DNSKEY RRs. let apex_dnskey_rrset = apex_owner_rrs .rrsets() .find(|rrset| rrset.rtype() == Rtype::DNSKEY); - let mut augmented_apex_dnskey_rrs = SortedRecords::<_, _, Sort>::new(); - // Determine the TTL of any existing DNSKEY RRSET and use that as the TTL // for DNSKEY RRs that we add. If none, then fall back to the SOA TTL. // @@ -421,14 +419,30 @@ where // That RFC pre-dates RFC 1034, and neither dnssec-signzone nor // ldns-signzone use the SOA MINIMUM as a default TTL, rather they use the // TTL of the SOA RR as the default and so we will do the same. - let dnskey_rrset_ttl = if let Some(rrset) = apex_dnskey_rrset { - let ttl = rrset.ttl(); - augmented_apex_dnskey_rrs.sorted_extend(rrset.iter().cloned()); - ttl + let dnskey_rrset_ttl = if let Some(rrset) = &apex_dnskey_rrset { + rrset.ttl() } else { soa_rr.ttl() }; + // Generate or extend the DNSKEY RRSET with the keys that we will sign + // apex DNSKEY RRs and zone RRs with. + let mut augmented_apex_dnskey_rrs = if let Some(rrset) = apex_dnskey_rrset + { + SortedRecords::<_, _, Sort>::from_iter(rrset.iter().cloned()) + } else { + SortedRecords::<_, _, Sort>::new() + }; + + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.1 2.1. + // Including DNSKEY RRs in a Zone. .. "For each private key used to create + // RRSIG RRs in a zone, the zone SHOULD include a zone DNSKEY RR + // containing the corresponding public key" + // + // We iterate over the DNSKEY RRs at the apex in the zone converting them + // into the correct output octets form, and if any keys we are going to + // sign the zone with do not exist we add them. + for public_key in keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) { From 94cf97d39fa30c031fc1304df5463187355c38e3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:07:22 +0100 Subject: [PATCH 382/569] Extend testing of generate_rrsigs() with a full zone to cover various key usage strategies. --- src/sign/signatures/rrsigs.rs | 172 +++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 54 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 0b6c2b458..49d3c1d74 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -999,7 +999,74 @@ mod tests { } #[test] - fn generate_rrsigs_for_complete_zone() { + fn generate_rrsigs_for_complete_zone_with_ksk_and_zsk() { + let keys = [ + mk_dnssec_signing_key(IntendedKeyPurpose::KSK), + mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), + ]; + let cfg = GenerateRrsigConfig::default(); + generate_rrsigs_for_complete_zone(&keys, 0, 1, &cfg).unwrap(); + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_csk() { + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + let cfg = GenerateRrsigConfig::default(); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_only_zsk_should_fail_by_default( + ) { + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; + let cfg = GenerateRrsigConfig::default(); + + // This should fail as the DefaultSigningKeyUsageStrategy requires + // both ZSK and KSK, or a CSK. + let res = generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg); + assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_only_zsk_and_fallback_strategy() + { + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; + + // Implement a strategy that falls back to the ZSK for signing zone + // keys if no KSK is available. (ala ldns-sign -A) + struct FallbackStrat; + impl SigningKeyUsageStrategy for FallbackStrat { + const NAME: &'static str = + "Fallback to ZSK usage strategy for testing"; + + fn select_signing_keys_for_rtype< + DSK: DesignatedSigningKey, + >( + candidate_keys: &[DSK], + rtype: Option, + ) -> smallvec::SmallVec<[usize; 4]> { + if core::matches!(rtype, Some(Rtype::DNSKEY)) { + Self::filter_keys(candidate_keys, |_| true) + } else { + Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) + } + } + } + + let fallback_cfg = GenerateRrsigConfig::<_, FallbackStrat, _>::new(); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &fallback_cfg) + .unwrap(); + } + + fn generate_rrsigs_for_complete_zone( + keys: &[DnssecSigningKey], + ksk_idx: usize, + zsk_idx: usize, + config: &GenerateRrsigConfig, + ) -> Result<(), SigningError> + where + KeyStrat: SigningKeyUsageStrategy, + { // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A let zonefile = include_bytes!( "../../../test-data/zonefiles/rfc4035-appendix-A.zone" @@ -1008,28 +1075,21 @@ mod tests { // Load the zone to generate RRSIGs for. let records = bytes_to_records(&zonefile[..]); - // Prepare a zone signing key and a key signing key. - let keys = [ - mk_dnssec_signing_key(IntendedKeyPurpose::KSK), - mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), - ]; + // Generate DNSKEYs and RRSIGs. + let generated_records = + generate_rrsigs(RecordsIter::new(&records), &keys, config)?; - // Generate DNSKEYs and RRSIGs. Use the default signing config and - // thus also the DefaultSigningKeyUsageStrategy which will honour the - // purpose of the key when selecting a key to use for signing DNSKEY - // RRs or other zone RRs. - let generated_records = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(), - ) - .unwrap(); + let dnskeys = keys + .iter() + .map(|k| k.public_key().to_dnskey().convert()) + .collect::>(); - let ksk = keys[0].public_key().to_dnskey().convert(); - let zsk = keys[1].public_key().to_dnskey().convert(); + let ksk = &dnskeys[ksk_idx]; + let zsk = &dnskeys[zsk_idx]; // Check the generated records. - // + let mut iter = generated_records.iter(); + // The records should be in a fixed canonical order because the input // records must be in canonical order, with the exception of the added // DNSKEY RRs which will be ordered in the order in the supplied @@ -1047,23 +1107,25 @@ mod tests { // DNSKEY records should have been generated for the apex for both of // the keys that we used to sign the zone. - assert_eq!(generated_records[0], mk_dnskey_rr("example.", &ksk)); - assert_eq!(generated_records[1], mk_dnskey_rr("example.", &zsk)); + assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", ksk)); + if ksk_idx != zsk_idx { + assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", zsk)); + } // RRSIG records should have been generated for the zone apex records, // one RRSIG per ZSK used (we used one ZSK so only one RRSIG per // record). assert_eq!( - generated_records[2], - mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", zsk) ); assert_eq!( - generated_records[3], - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", zsk) ); assert_eq!( - generated_records[4], - mk_rrsig_rr("example.", Rtype::MX, 1, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::MX, 1, "example.", zsk) ); // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. // Including RRSIG RRs in a Zone. .. "There MUST be an RRSIG for each @@ -1090,8 +1152,8 @@ mod tests { // keys based on their `IntendedKeyPurpose` which we assigned above // when creating the keys. assert_eq!( - generated_records[5], - mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &ksk) + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", ksk) ); // -- a.example. @@ -1107,8 +1169,8 @@ mod tests { // zone's name servers) MUST NOT be signed." assert_eq!( - generated_records[6], - mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", zsk) ); // -- ns1.a.example. @@ -1138,16 +1200,16 @@ mod tests { // -- ai.example. assert_eq!( - generated_records[7], - mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", zsk) ); assert_eq!( - generated_records[8], - mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", zsk) ); assert_eq!( - generated_records[9], - mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", zsk) ); // -- b.example. @@ -1169,56 +1231,58 @@ mod tests { // -- ns1.example. assert_eq!( - generated_records[10], - mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", zsk) ); // -- ns2.example. assert_eq!( - generated_records[11], - mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", zsk) ); // -- *.w.example. assert_eq!( - generated_records[12], - mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", zsk) ); // -- x.w.example. assert_eq!( - generated_records[13], - mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", zsk) ); // -- x.y.w.example. assert_eq!( - generated_records[14], - mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", zsk) ); // -- xx.example. assert_eq!( - generated_records[15], - mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", zsk) ); assert_eq!( - generated_records[16], - mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", zsk) ); assert_eq!( - generated_records[17], - mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", &zsk) + *iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", zsk) ); // No other records should have been generated. - assert_eq!(generated_records.len(), 18); + assert!(iter.next().is_none()); + + Ok(()) } //------------ Helper fns ------------------------------------------------ From 8984921df84db0f49b07c8ea26909bffdfb35e75 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 16:09:16 +0100 Subject: [PATCH 383/569] Clippy. --- src/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 49d3c1d74..d0da6b226 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1077,7 +1077,7 @@ mod tests { // Generate DNSKEYs and RRSIGs. let generated_records = - generate_rrsigs(RecordsIter::new(&records), &keys, config)?; + generate_rrsigs(RecordsIter::new(&records), keys, config)?; let dnskeys = keys .iter() From 019934c5a52af7ed9e2c418893b5b2b00e148e10 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:10:00 +0100 Subject: [PATCH 384/569] FIX: Inverted flag. --- src/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d0da6b226..6eb40b474 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -51,7 +51,7 @@ impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { } pub fn without_adding_used_dns_keys(mut self) -> Self { - self.add_used_dnskeys = true; + self.add_used_dnskeys = false; self } From ab40d90c475d163159d6e0e1424f467f8f777832 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:06:03 +0100 Subject: [PATCH 385/569] Organize imports. --- src/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 6eb40b474..879323001 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -661,6 +661,7 @@ mod tests { use crate::rdata::{Rrsig, A}; use crate::sign::crypto::common::KeyPair; use crate::sign::error::SignError; + use crate::sign::keys::keymeta::IntendedKeyPurpose; use crate::sign::keys::DnssecSigningKey; use crate::sign::test_util::*; use crate::sign::{test_util, PublicKeyBytes, Signature}; @@ -668,7 +669,6 @@ mod tests { use crate::zonetree::{StoredName, StoredRecord}; use super::*; - use crate::sign::keys::keymeta::IntendedKeyPurpose; const TEST_INCEPTION: u32 = 0; const TEST_EXPIRATION: u32 = 100; From 0f7ca2b32e6a976f52b5bc8aea6250eb655cb630 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:08:25 +0100 Subject: [PATCH 386/569] Add test for generating RRSIGs without adding DNSKEYs. --- src/sign/signatures/rrsigs.rs | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 879323001..7b566d557 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1015,6 +1015,14 @@ mod tests { generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); } + #[test] + fn generate_rrsigs_for_complete_zone_with_csk_without_adding_dnskeys() { + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + let cfg = + GenerateRrsigConfig::default().without_adding_used_dns_keys(); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); + } + #[test] fn generate_rrsigs_for_complete_zone_with_only_zsk_should_fail_by_default( ) { @@ -1105,11 +1113,16 @@ mod tests { // -- example. - // DNSKEY records should have been generated for the apex for both of - // the keys that we used to sign the zone. - assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", ksk)); - if ksk_idx != zsk_idx { - assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", zsk)); + if cfg.add_used_dnskeys { + // DNSKEY records should have been generated for the apex for both + // of the keys that we used to sign the zone. + assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", ksk)); + if ksk_idx != zsk_idx { + assert_eq!( + *iter.next().unwrap(), + mk_dnskey_rr("example.", zsk) + ); + } } // RRSIG records should have been generated for the zone apex records, From e7d2460a7cef35319d698216452a34226fae541d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:08:37 +0100 Subject: [PATCH 387/569] Rename parameter. --- src/sign/signatures/rrsigs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 7b566d557..9d50901f5 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1070,7 +1070,7 @@ mod tests { keys: &[DnssecSigningKey], ksk_idx: usize, zsk_idx: usize, - config: &GenerateRrsigConfig, + cfg: &GenerateRrsigConfig, ) -> Result<(), SigningError> where KeyStrat: SigningKeyUsageStrategy, @@ -1085,7 +1085,7 @@ mod tests { // Generate DNSKEYs and RRSIGs. let generated_records = - generate_rrsigs(RecordsIter::new(&records), keys, config)?; + generate_rrsigs(RecordsIter::new(&records), keys, cfg)?; let dnskeys = keys .iter() From 976b83ec1638705680d70520c0e10fd521cb8dc4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:20:15 +0100 Subject: [PATCH 388/569] Use existing helper fns to simplify test code. --- src/sign/signatures/rrsigs.rs | 51 +++++++++-------------------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 9d50901f5..310b75b6d 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -650,15 +650,13 @@ where #[cfg(test)] mod tests { use core::ops::RangeInclusive; - use core::str::FromStr; use bytes::Bytes; use pretty_assertions::assert_eq; use crate::base::iana::{Class, SecAlg}; - use crate::base::{Serial, Ttl}; + use crate::base::Serial; use crate::rdata::dnssec::Timestamp; - use crate::rdata::{Rrsig, A}; use crate::sign::crypto::common::KeyPair; use crate::sign::error::SignError; use crate::sign::keys::keymeta::IntendedKeyPurpose; @@ -684,10 +682,7 @@ mod tests { // ... // "For example, "www.example.com." has a Labels field value of 3" // We can use any class as RRSIGs are class independent. - let records = [mk_record( - "www.example.com.", - ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), - )]; + let records = [mk_a_rr("www.example.com.")]; let rrset = Rrset::new(&records); let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); @@ -747,10 +742,7 @@ mod tests { // 3.1.3. The Labels Field // ... // ""*.example.com." has a Labels field value of 2" - let records = [mk_record( - "*.example.com.", - ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), - )]; + let records = [mk_a_rr("*.example.com.")]; let rrset = Rrset::new(&records); let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); @@ -772,21 +764,9 @@ mod tests { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + let dnskey = key.public_key().to_dnskey().convert(); - let dummy_rrsig = Rrsig::new( - Rtype::A, - SecAlg::PRIVATEDNS, - 0, - Ttl::default(), - 0.into(), - 0.into(), - 0, - Name::root(), - Bytes::new(), - ) - .unwrap(); - - let records = [mk_record("any.", ZoneRecordData::Rrsig(dummy_rrsig))]; + let records = [mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)]; let rrset = Rrset::new(&records); let res = sign_rrset(&key, &rrset, &apex_owner); @@ -815,10 +795,7 @@ mod tests { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey); - let records = [mk_record( - "any.", - ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()), - )]; + let records = [mk_a_rr("any.")]; let rrset = Rrset::new(&records); fn calc_timestamps( @@ -933,10 +910,7 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { - let records: [Record; 1] = [mk_record( - "example.", - ZoneRecordData::A(A::from_str("127.0.0.1").unwrap()), - )]; + let records = [mk_a_rr("example.")]; let no_keys: [DnssecSigningKey; 0] = []; let res = generate_rrsigs( @@ -954,12 +928,11 @@ mod tests { // This is an example of generating RRSIGs for something other than a // full zone, in this case just for NSECs, as is done by sign_zone(). - let records: [Record; 1] = - [Record::from_record(mk_nsec_rr( - zone_apex, - "next.example.", - "A NSEC RRSIG", - ))]; + let records = [Record::from_record(mk_nsec_rr( + zone_apex, + "next.example.", + "A NSEC RRSIG", + ))]; // Prepare a zone signing key and a key signing key. let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; From 26911fdde1dea7de4a667d0ecec107bfe48c1414 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:30:39 +0100 Subject: [PATCH 389/569] Add missing [must_use] attributes. --- src/sign/denial/nsec.rs | 1 + src/sign/denial/nsec3.rs | 1 + src/sign/signatures/rrsigs.rs | 3 +++ src/sign/traits.rs | 1 + 4 files changed, 6 insertions(+) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 4579805e2..67ef6b971 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -38,6 +38,7 @@ use crate::sign::records::RecordsIter; /// [`CanonicalOrd`]: crate::base::cmp::CanonicalOrd // TODO: Add (mutable?) iterator based variant. #[allow(clippy::type_complexity)] +#[must_use] pub fn generate_nsecs( records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 79e4e0999..0df2c976f 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -35,6 +35,7 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. +#[must_use] pub fn generate_nsec3s( ttl: Ttl, records: RecordsIter<'_, N, ZoneRecordData>, diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 310b75b6d..b89b451b9 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -88,6 +88,7 @@ impl Default /// Any existing RRSIG records will be ignored. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] +#[must_use] pub fn generate_rrsigs( records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], @@ -513,6 +514,7 @@ where /// If signing multiple RRsets, calling [`sign_rrset_in()`] directly will be /// more efficient as you can allocate the scratch buffer once and re-use it /// across multiple calls. +#[must_use] pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, @@ -551,6 +553,7 @@ where /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 /// [RFC 6840 section 5.11]: /// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 +#[must_use] pub fn sign_rrset_in( key: &SigningKey, rrset: &Rrset<'_, N, D>, diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 49c6d485a..4ac150d1c 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -495,6 +495,7 @@ where /// /// This function is a thin wrapper around [`generate_rrsigs()`]. #[allow(clippy::type_complexity)] + #[must_use] fn sign( &self, expected_apex: &N, From 0bd93ec3560253fc29316e826bfa7f2bd7d0d47b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:34:43 +0100 Subject: [PATCH 390/569] Correct / generalize old comments. --- src/sign/signatures/rrsigs.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index b89b451b9..48baa9307 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1082,7 +1082,8 @@ mod tests { // We check each record explicitly by index because assert_eq() on an // array of objects that includes Rrsig produces hard to read output // due to the large RRSIG signature bytes being printed one byte per - // line. + // line. It also wouldn't support dynamically checking for certain + // records based on the signing configuration used. // NOTE: As we only invoked generate_rrsigs() and not generate_nsecs() // there will not be any RRSIGs covering NSEC records. @@ -1102,8 +1103,7 @@ mod tests { } // RRSIG records should have been generated for the zone apex records, - // one RRSIG per ZSK used (we used one ZSK so only one RRSIG per - // record). + // one RRSIG per ZSK used. assert_eq!( *iter.next().unwrap(), mk_rrsig_rr("example.", Rtype::NS, 1, "example.", zsk) From ffa16b3465b28775b11975a2f9cb786b5c3c0cb4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:36:34 +0100 Subject: [PATCH 391/569] Ah, the [must_use] are already inffered and duplicate and annoy Clippy, remove them again. --- src/sign/denial/nsec.rs | 1 - src/sign/denial/nsec3.rs | 1 - src/sign/signatures/rrsigs.rs | 3 --- src/sign/traits.rs | 1 - 4 files changed, 6 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 67ef6b971..4579805e2 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -38,7 +38,6 @@ use crate::sign::records::RecordsIter; /// [`CanonicalOrd`]: crate::base::cmp::CanonicalOrd // TODO: Add (mutable?) iterator based variant. #[allow(clippy::type_complexity)] -#[must_use] pub fn generate_nsecs( records: RecordsIter<'_, N, ZoneRecordData>, assume_dnskeys_will_be_added: bool, diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 0df2c976f..79e4e0999 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -35,7 +35,6 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. -#[must_use] pub fn generate_nsec3s( ttl: Ttl, records: RecordsIter<'_, N, ZoneRecordData>, diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 48baa9307..4669b519b 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -88,7 +88,6 @@ impl Default /// Any existing RRSIG records will be ignored. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -#[must_use] pub fn generate_rrsigs( records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], @@ -514,7 +513,6 @@ where /// If signing multiple RRsets, calling [`sign_rrset_in()`] directly will be /// more efficient as you can allocate the scratch buffer once and re-use it /// across multiple calls. -#[must_use] pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, @@ -553,7 +551,6 @@ where /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 /// [RFC 6840 section 5.11]: /// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 -#[must_use] pub fn sign_rrset_in( key: &SigningKey, rrset: &Rrset<'_, N, D>, diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 4ac150d1c..49c6d485a 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -495,7 +495,6 @@ where /// /// This function is a thin wrapper around [`generate_rrsigs()`]. #[allow(clippy::type_complexity)] - #[must_use] fn sign( &self, expected_apex: &N, From 3717c668c0803de907464cdb312b2d04e31f1ab6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:08:30 +0100 Subject: [PATCH 392/569] Corrections and additions to the RustDoc for generate_rrsigs(). --- src/sign/signatures/rrsigs.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 4669b519b..1efb5cbe2 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -81,11 +81,20 @@ impl Default /// Generate RRSIG RRs for a collection of zone records. /// /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be -/// added to the given records as part of DNSSEC zone signing. +/// added to the input records as part of DNSSEC zone signing. /// -/// The given records MUST be sorted according to [`CanonicalOrd`]. +/// The input records MUST be sorted according to [`CanonicalOrd`]. /// -/// Any existing RRSIG records will be ignored. +/// Any RRSIG records in the input will be ignored. New, and replacement (if +/// already present), RRSIGs will be generated and included in the output. +/// +/// If [`GenerateRrsigConfig::add_used_dnskeys`] is true, for the subset of +/// the input keys that are used to sign records, if they lack a corresponding +/// DNSKEY RR in the input records the missing DNSKEY RR will be generated and +/// included in the output. +/// +/// Note that the order of the output records should not be relied upon and is +/// subject to change. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_rrsigs( From 1887d7eea8cb639c7aeca720c07a05ca288f3b35 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 11:09:02 +0100 Subject: [PATCH 393/569] Add a test of calling generate_rrsigs() on an already signed zone. --- src/sign/signatures/rrsigs.rs | 100 +++++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 1efb5cbe2..e797e3497 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1083,7 +1083,10 @@ mod tests { // The records should be in a fixed canonical order because the input // records must be in canonical order, with the exception of the added // DNSKEY RRs which will be ordered in the order in the supplied - // collection of keys to sign with. + // collection of keys to sign with. While we tell users of + // generate_rrsigs() not to rely on the order of the output, we assume + // that we know what that order is for this test, but would have to + // update this test if that order later changes. // // We check each record explicitly by index because assert_eq() on an // array of objects that includes Rrsig produces hard to read output @@ -1280,6 +1283,101 @@ mod tests { Ok(()) } + #[test] + fn generate_rrsigs_for_already_signed_zone() { + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + + let dnskey = keys[0].public_key().to_dnskey().convert(); + + let records = [ + // -- example. + mk_soa_rr("example.", "some.mname.", "some.rname."), + mk_ns_rr("example.", "ns.example."), + mk_dnskey_rr("example.", &dnskey), + Record::from_record(mk_nsec_rr( + "example", + "ns.example.", + "SOA NS DNSKEY NSEC RRSIG", + )), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey), + // -- ns.example. + mk_a_rr("ns.example."), + Record::from_record(mk_nsec_rr( + "ns.example", + "example.", + "A NSEC RRSIG", + )), + mk_rrsig_rr("ns.example.", Rtype::A, 1, "example.", &dnskey), + mk_rrsig_rr("ns.example.", Rtype::NSEC, 1, "example.", &dnskey), + ]; + + let generated_records = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + + // Check the generated records. + let mut iter = generated_records.iter(); + + // The records should be in a fixed canonical order because the input + // records must be in canonical order, with the exception of the added + // DNSKEY RRs which will be ordered in the order in the supplied + // collection of keys to sign with. While we tell users of + // generate_rrsigs() not to rely on the order of the output, we assume + // that we know what that order is for this test, but would have to + // update this test if that order later changes. + // + // We check each record explicitly by index because assert_eq() on an + // array of objects that includes Rrsig produces hard to read output + // due to the large RRSIG signature bytes being printed one byte per + // line. It also wouldn't support dynamically checking for certain + // records based on the signing configuration used. + + // -- example. + + // The DNSKEY was already present in the zone so we do NOT expect a + // DNSKEY to be included in the output. + + // RRSIG records should have been generated for the zone apex records, + // one RRSIG per ZSK used, even if RRSIG RRs already exist. + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey) + ); + + // -- ns.example. + + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("ns.example.", Rtype::A, 2, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("ns.example.", Rtype::NSEC, 2, "example.", &dnskey) + ); + + // No other records should have been generated. + + assert!(iter.next().is_none()); + } + //------------ Helper fns ------------------------------------------------ fn mk_dnssec_signing_key( From 7764e6bfb82504802141b5b029561c69d02470ee Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 12:53:22 +0100 Subject: [PATCH 394/569] - Remove the DNSKEY RRs from the input test zonefile as it is assumed to be unsigned. - Add more tests and simplify some existing tests. --- src/sign/denial/nsec.rs | 6 +- src/sign/signatures/rrsigs.rs | 176 ++++++++++++++++---- test-data/zonefiles/rfc4035-appendix-A.zone | 2 - 3 files changed, 148 insertions(+), 36 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 4579805e2..cc363ec43 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -303,11 +303,7 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec_rr( - "example.", - "a.example", - "NS SOA MX RRSIG NSEC DNSKEY" - ), + mk_nsec_rr("example.", "a.example", "NS SOA MX RRSIG NSEC"), mk_nsec_rr("a.example.", "ai.example", "NS DS RRSIG NSEC"), mk_nsec_rr( "ai.example.", diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index e797e3497..f8086ebf9 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -658,12 +658,10 @@ where #[cfg(test)] mod tests { - use core::ops::RangeInclusive; - use bytes::Bytes; use pretty_assertions::assert_eq; - use crate::base::iana::{Class, SecAlg}; + use crate::base::iana::SecAlg; use crate::base::Serial; use crate::rdata::dnssec::Timestamp; use crate::sign::crypto::common::KeyPair; @@ -919,7 +917,10 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { - let records = [mk_a_rr("example.")]; + let records = [ + mk_soa_rr("example.", "mname.", "rname."), + mk_a_rr("example."), + ]; let no_keys: [DnssecSigningKey; 0] = []; let res = generate_rrsigs( @@ -932,26 +933,61 @@ mod tests { } #[test] - fn generate_rrsigs_for_partial_zone() { - let zone_apex = "example."; + fn generate_rrsigs_without_suitable_keys_should_fail_for_non_empty_zone() + { + let records = [ + mk_soa_rr("example.", "mname.", "rname."), + mk_a_rr("example."), + ]; + let res = generate_rrsigs( + RecordsIter::new(&records), + &[mk_dnssec_signing_key(IntendedKeyPurpose::KSK)], + &GenerateRrsigConfig::default(), + ); + assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); + + let res = generate_rrsigs( + RecordsIter::new(&records), + &[mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)], + &GenerateRrsigConfig::default(), + ); + assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); + + let res = generate_rrsigs( + RecordsIter::new(&records), + &[mk_dnssec_signing_key(IntendedKeyPurpose::Inactive)], + &GenerateRrsigConfig::default(), + ); + assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); + } + + #[test] + fn generate_rrsigs_for_partial_zone_at_apex() { + generate_rrsigs_for_partial_zone("example.", "example."); + } + + #[test] + fn generate_rrsigs_for_partial_zone_beneath_apex() { + generate_rrsigs_for_partial_zone("example.", "in.example."); + } + + fn generate_rrsigs_for_partial_zone(zone_apex: &str, record_owner: &str) { // This is an example of generating RRSIGs for something other than a - // full zone, in this case just for NSECs, as is done by sign_zone(). - let records = [Record::from_record(mk_nsec_rr( - zone_apex, - "next.example.", - "A NSEC RRSIG", - ))]; + // full zone, in this case just for an A record. This test + // deliberately does not include a SOA record as the zone is partial. + let records = [mk_a_rr(record_owner)]; // Prepare a zone signing key and a key signing key. let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + let dnskey = keys[0].public_key().to_dnskey().convert(); // Generate RRSIGs. Use the default signing config and thus also the // DefaultSigningKeyUsageStrategy which will honour the purpose of the // key when selecting a key to use for signing DNSKEY RRs or other // zone RRs. We supply the zone apex because we are not supplying an // entire zone complete with SOA. - let rrsigs = generate_rrsigs( + let generated_records = generate_rrsigs( RecordsIter::new(&records), &keys, &GenerateRrsigConfig::default() @@ -959,25 +995,107 @@ mod tests { ) .unwrap(); - // Check the generated RRSIG record - assert_eq!(rrsigs.len(), 1); - assert_eq!(rrsigs[0].owner(), &mk_name("example.")); - assert_eq!(rrsigs[0].class(), Class::IN); - assert_eq!(rrsigs[0].rtype(), Rtype::RRSIG); + // Check the generated RRSIG records + let expected_labels = mk_name(record_owner).rrsig_label_count(); + assert_eq!(generated_records.len(), 1); + assert_eq!( + generated_records[0], + mk_rrsig_rr( + record_owner, + Rtype::A, + expected_labels, + zone_apex, + &dnskey + ) + ); + } - // Check the contained RRSIG RDATA - let ZoneRecordData::Rrsig(rrsig) = rrsigs[0].data() else { - panic!("RDATA is not RRSIG"); - }; - assert_eq!(rrsig.type_covered(), Rtype::NSEC); - assert_eq!(rrsig.algorithm(), keys[0].algorithm()); - assert_eq!(rrsig.original_ttl(), TEST_TTL); - assert_eq!(rrsig.signer_name(), &mk_name(zone_apex)); - assert_eq!(rrsig.key_tag(), keys[0].public_key().key_tag()); + #[test] + fn generate_rrsigs_ignores_records_outside_the_zone() { + let records = [ + mk_soa_rr("example.", "mname.", "rname."), + mk_a_rr("in_zone.example."), + mk_a_rr("out_of_zone."), + ]; + + // Prepare a zone signing key and a key signing key. + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + let dnskey = keys[0].public_key().to_dnskey().convert(); + + let generated_records = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + + // Check the generated records + assert_eq!( + generated_records, + [ + mk_dnskey_rr("example.", &dnskey), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr( + "example.", + Rtype::DNSKEY, + 1, + "example.", + &dnskey + ), + mk_rrsig_rr( + "in_zone.example.", + Rtype::A, + 2, + "example.", + &dnskey + ), + ] + ); + + // Repeat but this time passing only the out-of-zone record in and + // show that it DOES get signed if not passed together with the first + // zone. + let generated_records = generate_rrsigs( + RecordsIter::new(&records[2..]), + &keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + + // Check the generated RRSIG records assert_eq!( - RangeInclusive::new(rrsig.inception(), rrsig.expiration()), - keys[0].signature_validity_period().unwrap() + generated_records, + [mk_rrsig_rr( + "out_of_zone.", + Rtype::A, + 1, + "out_of_zone.", + &dnskey + )] + ); + } + + #[test] + fn generate_rrsigs_fails_with_multiple_soas_at_apex() { + let records = [ + mk_soa_rr("example.", "mname.", "rname."), + mk_soa_rr("example.", "other.mname.", "other.rname."), + mk_a_rr("in_zone.example."), + ]; + + // Prepare a zone signing key and a key signing key. + let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; + + let res = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), ); + + assert!(matches!( + res, + Err(SigningError::SoaRecordCouldNotBeDetermined) + )); } #[test] diff --git a/test-data/zonefiles/rfc4035-appendix-A.zone b/test-data/zonefiles/rfc4035-appendix-A.zone index 431d99590..adbb4f7ab 100644 --- a/test-data/zonefiles/rfc4035-appendix-A.zone +++ b/test-data/zonefiles/rfc4035-appendix-A.zone @@ -8,8 +8,6 @@ example. 3600 IN SOA ns1.example. bugs.x.w.example. 108153937 example. 3600 IN NS ns2.example. example. 3600 IN NS ns1.example. example. 3600 IN MX 1 xx.example. -example. 3600 IN DNSKEY 257 3 8 AwEAAaYL5iwWI6UgSQVcDZmH7DrhQU/P6cOfi4wXYDzHypsfZ1D8znPwoAqhj54kTBVqgZDHw8QEnMcS3TWxvHBvncRTIXhCLx0BNK5/6mcTSK2IDbxl0j4vkcQrOxc77tyExuFfuXouuKVtE7rggOJiX6ga5LJW2if6Jxe/Rh8+aJv7 ;{id = 31967 (ksk), size = 1024b} -example. 3600 IN DNSKEY 256 3 8 AwEAAbsD4Tcz8hl2Rldov4CrfYpK3ORIh/giSGDlZaDTZR4gpGxGvMBwu2jzQ3m0iX3PvqPoaybC4tznjlJi8g/qsCRHhOkqWmjtmOYOJXEuUTb+4tPBkiboJM5QchxTfKxkYbJ2AD+VAUX1S6h/0DI0ZCGx1H90QTBE2ymRgHBwUfBt ;{id = 38353 (zsk), size = 1024b} a.example. 3600 IN NS ns2.a.example. a.example. 3600 IN NS ns1.a.example. a.example. 3600 IN DS 57855 5 1 b6dcd485719adca18e5f3d48a2331627fdd3636b From d807d4b507882354808e1880403c021366fb88fe Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 22 Jan 2025 13:40:55 +0100 Subject: [PATCH 395/569] - Also use SmallVec here. - Add a test of generate_rrsigs() with mutliple KSKs and ZSKs. --- src/sign/signatures/rrsigs.rs | 129 ++++++++++++++++++++++++++++------ 1 file changed, 106 insertions(+), 23 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index f8086ebf9..52c778036 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -29,6 +29,7 @@ use crate::sign::records::{ }; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::traits::SignRaw; +use smallvec::SmallVec; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { @@ -188,10 +189,12 @@ where non_dnskey_signing_key_idxs.sort(); non_dnskey_signing_key_idxs.dedup(); - let mut keys_in_use_idxs: Vec<_> = non_dnskey_signing_key_idxs - .iter() - .chain(dnskey_signing_key_idxs.iter()) - .collect(); + let mut keys_in_use_idxs: SmallVec<[usize; 4]> = + non_dnskey_signing_key_idxs + .iter() + .chain(dnskey_signing_key_idxs.iter()) + .copied() + .collect(); keys_in_use_idxs.sort(); keys_in_use_idxs.dedup(); @@ -297,7 +300,7 @@ fn log_keys_in_use( keys: &[DSK], dnskey_signing_key_idxs: &[usize], non_dnskey_signing_key_idxs: &[usize], - keys_in_use_idxs: &[&usize], + keys_in_use_idxs: &[usize], ) where DSK: DesignatedSigningKey, Inner: SignRaw, @@ -329,7 +332,7 @@ fn log_keys_in_use( ); for idx in keys_in_use_idxs { - let key = &keys[**idx]; + let key = &keys[*idx]; let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); let is_non_dnskey_signing_key = non_dnskey_signing_key_idxs.contains(idx); @@ -357,7 +360,7 @@ fn generate_apex_rrsigs( zone_class: crate::base::iana::Class, dnskey_signing_key_idxs: &[usize], non_dnskey_signing_key_idxs: &[usize], - keys_in_use_idxs: &[&usize], + keys_in_use_idxs: &[usize], generated_rrs: &mut Vec>>, reusable_scratch: &mut Vec, ) -> Result<(), SigningError> @@ -453,7 +456,7 @@ where // sign the zone with do not exist we add them. for public_key in - keys_in_use_idxs.iter().map(|&&idx| keys[idx].public_key()) + keys_in_use_idxs.iter().map(|&idx| keys[idx].public_key()) { let dnskey = public_key.to_dnskey(); @@ -674,6 +677,7 @@ mod tests { use crate::zonetree::{StoredName, StoredRecord}; use super::*; + use rand::Rng; const TEST_INCEPTION: u32 = 0; const TEST_EXPIRATION: u32 = 100; @@ -681,7 +685,7 @@ mod tests { #[test] fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); // RFC 4034 @@ -742,7 +746,7 @@ mod tests { #[test] fn sign_rrset_with_wildcard() { let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); // RFC 4034 @@ -769,7 +773,7 @@ mod tests { // "An RRSIG RR itself MUST NOT be signed" let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); let dnskey = key.public_key().to_dnskey().convert(); @@ -800,7 +804,7 @@ mod tests { // than 68 years in either the past or the future." let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let records = [mk_a_rr("any.")]; let rrset = Rrset::new(&records); @@ -917,10 +921,7 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { - let records = [ - mk_soa_rr("example.", "mname.", "rname."), - mk_a_rr("example."), - ]; + let records = [mk_a_rr("example.")]; let no_keys: [DnssecSigningKey; 0] = []; let res = generate_rrsigs( @@ -935,10 +936,7 @@ mod tests { #[test] fn generate_rrsigs_without_suitable_keys_should_fail_for_non_empty_zone() { - let records = [ - mk_soa_rr("example.", "mname.", "rname."), - mk_a_rr("example."), - ]; + let records = [mk_a_rr("example.")]; let res = generate_rrsigs( RecordsIter::new(&records), @@ -1401,6 +1399,81 @@ mod tests { Ok(()) } + #[test] + fn generate_rrsigs_for_complete_zone_with_multiple_ksks_and_zsks() { + let apex = "example."; + let records = [ + mk_soa_rr(apex, "some.mname.", "some.rname."), + mk_ns_rr(apex, "ns.example."), + mk_a_rr("ns.example."), + ]; + + let keys = [ + mk_dnssec_signing_key(IntendedKeyPurpose::KSK), + mk_dnssec_signing_key(IntendedKeyPurpose::KSK), + mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), + mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), + ]; + + let ksk1 = keys[0].public_key().to_dnskey().convert(); + let ksk2 = keys[1].public_key().to_dnskey().convert(); + let zsk1 = keys[2].public_key().to_dnskey().convert(); + let zsk2 = keys[3].public_key().to_dnskey().convert(); + + let generated_records = generate_rrsigs( + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::default(), + ) + .unwrap(); + + // Check the generated records. + assert_eq!(generated_records.len(), 12); + + // Filter out the records one by one until there should be none left. + + let it = generated_records + .iter() + .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk1)) + .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk2)) + .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk1)) + .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk2)) + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk1) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk2) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::DNSKEY, 1, apex, &ksk1) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::DNSKEY, 1, apex, &ksk2) + }) + .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk1)) + .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk2)) + .filter(|&rr| { + rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk1) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk2) + }); + + let mut it = it.inspect(|rr| { + eprint!( + "Warning: Unexpected record remaining after filtering: {} {}", + rr.owner(), + rr.rtype() + ); + if let ZoneRecordData::Rrsig(rrsig) = rr.data() { + eprint!(" => {:?}", rrsig); + } + eprintln!(); + }); + + assert!(it.next().is_none()); + } + #[test] fn generate_rrsigs_for_already_signed_zone() { let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; @@ -1511,7 +1584,11 @@ mod tests { IntendedKeyPurpose::Inactive => 0, }; - let key = SigningKey::new(StoredName::root_bytes(), flags, TestKey); + let key = SigningKey::new( + StoredName::root_bytes(), + flags, + TestKey::default(), + ); let key = key.with_validity( Timestamp::from(TEST_INCEPTION), @@ -1555,7 +1632,7 @@ mod tests { const TEST_SIGNATURE_RAW: [u8; 64] = [0u8; 64]; const TEST_SIGNATURE: Bytes = Bytes::from_static(&TEST_SIGNATURE_RAW); - struct TestKey; + struct TestKey([u8; 32]); impl SignRaw for TestKey { fn algorithm(&self) -> SecAlg { @@ -1563,11 +1640,17 @@ mod tests { } fn raw_public_key(&self) -> PublicKeyBytes { - PublicKeyBytes::Ed25519([0_u8; 32].into()) + PublicKeyBytes::Ed25519(self.0.into()) } fn sign_raw(&self, _data: &[u8]) -> Result { Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) } } + + impl Default for TestKey { + fn default() -> Self { + Self(rand::thread_rng().gen()) + } + } } From 56ce3b04b4f4a91a1dd2894e5dde2aafa713a392 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:05:49 +0100 Subject: [PATCH 396/569] Normalize the generate_xxx interfaces to take config objects and return just the record types they produce. --- src/sign/config.rs | 4 +- src/sign/denial/config.rs | 20 ++- src/sign/denial/nsec3.rs | 247 +++++++++++++++++++------------ src/sign/error.rs | 6 + src/sign/mod.rs | 73 +++------- src/sign/records.rs | 13 +- src/sign/signatures/rrsigs.rs | 263 ++++++++++++++++++++++------------ src/sign/test_util/mod.rs | 115 ++++++++------- src/sign/traits.rs | 5 +- 9 files changed, 448 insertions(+), 298 deletions(-) diff --git a/src/sign/config.rs b/src/sign/config.rs index 339b9198c..ea49553bd 100644 --- a/src/sign/config.rs +++ b/src/sign/config.rs @@ -31,7 +31,7 @@ pub struct SigningConfig< Sort: Sorter, { /// Authenticated denial of existing mechanism configuration. - pub denial: DenialConfig, + pub denial: DenialConfig, /// Should keys used to sign the zone be added as DNSKEY RRs? pub add_used_dnskeys: bool, @@ -49,7 +49,7 @@ where Sort: Sorter, { pub fn new( - denial: DenialConfig, + denial: DenialConfig, add_used_dnskeys: bool, ) -> Self { Self { diff --git a/src/sign/denial/config.rs b/src/sign/denial/config.rs index ec501a745..a7952c428 100644 --- a/src/sign/denial/config.rs +++ b/src/sign/denial/config.rs @@ -3,8 +3,9 @@ use core::convert::From; use std::vec::Vec; use super::nsec3::{ - Nsec3Config, Nsec3HashProvider, OnDemandNsec3HashProvider, + GenerateNsec3Config, Nsec3HashProvider, OnDemandNsec3HashProvider, }; +use crate::sign::records::DefaultSorter; //------------ NsecToNsec3TransitionState ------------------------------------ @@ -74,8 +75,12 @@ pub enum Nsec3ToNsecTransitionState { /// This type can be used to choose which denial mechanism should be used when /// DNSSEC signing a zone. #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub enum DenialConfig> -where +pub enum DenialConfig< + N, + O, + HP = OnDemandNsec3HashProvider, + Sort = DefaultSorter, +> where HP: Nsec3HashProvider, O: AsRef<[u8]> + From<&'static [u8]>, { @@ -105,17 +110,20 @@ where /// the only practical and palatable transition mechanisms may require /// an intermediate transition to an insecure state, or to a state that /// uses NSEC records instead of NSEC3." - Nsec3(Nsec3Config, Vec>), + Nsec3( + GenerateNsec3Config, + Vec>, + ), /// The zone is transitioning from NSEC to NSEC3. TransitioningNsecToNsec3( - Nsec3Config, + GenerateNsec3Config, NsecToNsec3TransitionState, ), /// The zone is transitioning from NSEC3 to NSEC. TransitioningNsec3ToNsec( - Nsec3Config, + GenerateNsec3Config, Nsec3ToNsecTransitionState, ), } diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 79e4e0999..d0ea0f0fc 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -1,3 +1,4 @@ +use core::cmp::min; use core::convert::From; use core::fmt::{Debug, Display}; use core::marker::{PhantomData, Send}; @@ -16,10 +17,91 @@ use crate::base::{Name, NameBuilder, Record, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; -use crate::sign::records::{RecordsIter, SortedRecords, Sorter}; +use crate::sign::error::SigningError; +use crate::sign::records::{ + DefaultSorter, RecordsIter, SortedRecords, Sorter, +}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; +//----------- GenerateNsec3Config -------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GenerateNsec3Config +where + HashProvider: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, +{ + pub assume_dnskeys_will_be_added: bool, + pub params: Nsec3param, + pub opt_out: Nsec3OptOut, + pub nsec3param_ttl_mode: Nsec3ParamTtlMode, + pub hash_provider: HashProvider, + _phantom: PhantomData<(N, Sort)>, +} + +impl + GenerateNsec3Config +where + HashProvider: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, +{ + pub fn new( + params: Nsec3param, + opt_out: Nsec3OptOut, + hash_provider: HashProvider, + ) -> Self { + Self { + assume_dnskeys_will_be_added: true, + params, + opt_out, + hash_provider, + nsec3param_ttl_mode: Default::default(), + _phantom: Default::default(), + } + } + + pub fn with_ttl_mode(mut self, ttl_mode: Nsec3ParamTtlMode) -> Self { + self.nsec3param_ttl_mode = ttl_mode; + self + } + + pub fn without_assuming_dnskeys_will_be_added(mut self) -> Self { + self.assume_dnskeys_will_be_added = false; + self + } +} + +impl Default + for GenerateNsec3Config< + N, + Octs, + OnDemandNsec3HashProvider, + DefaultSorter, + > +where + N: ToName + From>, + Octs: AsRef<[u8]> + From<&'static [u8]> + Clone + FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn default() -> Self { + let params = Nsec3param::default(); + let hash_provider = OnDemandNsec3HashProvider::new( + params.hash_algorithm(), + params.iterations(), + params.salt().clone(), + ); + Self { + assume_dnskeys_will_be_added: true, + params, + opt_out: Default::default(), + nsec3param_ttl_mode: Default::default(), + hash_provider, + _phantom: Default::default(), + } + } +} + /// Generate [RFC5155] NSEC3 and NSEC3PARAM records for this record set. /// /// This function does NOT enforce use of current best practice settings, as @@ -35,17 +117,19 @@ use crate::validate::{nsec3_hash, Nsec3HashError}; /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. +// TODO: Get rid of &mut for GenerateNsec3Config. pub fn generate_nsec3s( - ttl: Ttl, records: RecordsIter<'_, N, ZoneRecordData>, - params: Nsec3param, - opt_out: Nsec3OptOut, - assume_dnskeys_will_be_added: bool, - hash_provider: &mut HashProvider, -) -> Result, Nsec3HashError> + config: &mut GenerateNsec3Config, +) -> Result, SigningError> where N: ToName + Clone + Display + Ord + Hash + Send + From>, - Octs: FromBuilder + OctetsFrom> + Default + Clone + Send, + Octs: FromBuilder + + From<&'static [u8]> + + OctetsFrom> + + Default + + Clone + + Send, Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, HashProvider: Nsec3HashProvider, @@ -58,8 +142,11 @@ where // RFC 5155 7.1 step 2: // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = params.flags(); - if matches!(opt_out, Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly) { + let mut nsec3_flags = config.params.flags(); + if matches!( + config.opt_out, + Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly + ) { // Set the Opt-Out flag. nsec3_flags |= 0b0000_0001; } @@ -80,6 +167,8 @@ where let apex_label_count = apex_owner.iter_labels().count(); let mut last_nent_stack: Vec = vec![]; + let mut ttl = None; + let mut nsec3param_ttl = None; for owner_rrs in records { trace!("Owner: {}", owner_rrs.owner()); @@ -134,7 +223,7 @@ where // even when Opt-Out is not being used because we also need to know // there at a later step. let has_ds = owner_rrs.records().any(|rec| rec.rtype() == Rtype::DS); - if opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { + if config.opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { debug!("Excluding owner {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",owner_rrs.owner()); continue; } @@ -303,27 +392,57 @@ where trace!("Adding {} to the bitmap", rrset.rtype()); bitmap.add(rrset.rtype()).unwrap(); } + + if rrset.rtype() == Rtype::SOA { + if rrset.len() > 1 { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + } + + let soa_rr = rrset.first(); + + // Check that the RDATA for the SOA record can be parsed. + let ZoneRecordData::Soa(ref soa_data) = soa_rr.data() else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + }; + + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to + // say that the "TTL of the NSEC(3) RR that is returned MUST + // be the lesser of the MINIMUM field of the SOA record and + // the TTL of the SOA itself". + ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + + nsec3param_ttl = match config.nsec3param_ttl_mode { + Nsec3ParamTtlMode::Fixed(ttl) => Some(ttl), + Nsec3ParamTtlMode::Soa => Some(soa_rr.ttl()), + Nsec3ParamTtlMode::SoaMinimum => Some(soa_data.minimum()), + }; + } + } + + if ttl.is_none() { + return Err(SigningError::SoaRecordCouldNotBeDetermined); } if distance_to_apex == 0 { trace!("Adding NSEC3PARAM to the bitmap as we are at the apex and RRSIG RRs are expected to be added"); bitmap.add(Rtype::NSEC3PARAM).unwrap(); - if assume_dnskeys_will_be_added { + if config.assume_dnskeys_will_be_added { trace!("Adding DNSKEY to the bitmap as we are at the apex and DNSKEY RRs are expected to be added"); bitmap.add(Rtype::DNSKEY).unwrap(); } } + // SAFETY: ttl will be set above before we get here. let rec: Record> = mk_nsec3( &name, - hash_provider, - params.hash_algorithm(), + &mut config.hash_provider, + config.params.hash_algorithm(), nsec3_flags, - params.iterations(), - params.salt(), + config.params.iterations(), + config.params.salt(), &apex_owner, bitmap, - ttl, + ttl.unwrap(), false, )?; @@ -341,16 +460,17 @@ where let bitmap = RtypeBitmap::::builder(); debug!("Generating NSEC3 RR for ENT at {name}"); + // SAFETY: ttl will be set below before prev is set to Some. let rec = mk_nsec3( &name, - hash_provider, - params.hash_algorithm(), + &mut config.hash_provider, + config.params.hash_algorithm(), nsec3_flags, - params.iterations(), - params.salt(), + config.params.iterations(), + config.params.salt(), &apex_owner, bitmap, - ttl, + ttl.unwrap(), true, )?; @@ -384,17 +504,22 @@ where last_nsec3.set_next_owner(owner_hash.clone()); } + let Some(nsec3param_ttl) = nsec3param_ttl else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + }; + // RFC 5155 7.1 step 8: // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." + // SAFETY: nsec3param_ttl will be set above before we get here. let nsec3param = Record::new( apex_owner .try_to_name::() .map_err(|_| Nsec3HashError::AppendError)? .into(), Class::IN, - ttl, - params, + nsec3param_ttl, + config.params.clone(), ); // RFC 5155 7.1 after step 8: @@ -653,86 +778,22 @@ impl Nsec3ParamTtlMode { } } -//----------- Nsec3Config ---------------------------------------------------- - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Nsec3Config -where - HashProvider: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, -{ - pub params: Nsec3param, - pub opt_out: Nsec3OptOut, - pub ttl_mode: Nsec3ParamTtlMode, - pub hash_provider: HashProvider, - _phantom: PhantomData, -} - -impl Nsec3Config -where - HashProvider: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, -{ - pub fn new( - params: Nsec3param, - opt_out: Nsec3OptOut, - hash_provider: HashProvider, - ) -> Self { - Self { - params, - opt_out, - hash_provider, - ttl_mode: Default::default(), - _phantom: Default::default(), - } - } - - pub fn with_ttl_mode(mut self, ttl_mode: Nsec3ParamTtlMode) -> Self { - self.ttl_mode = ttl_mode; - self - } -} - -impl Default - for Nsec3Config> -where - N: ToName + From>, - Octs: AsRef<[u8]> + From<&'static [u8]> + Clone + FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, -{ - fn default() -> Self { - let params = Nsec3param::default(); - let hash_provider = OnDemandNsec3HashProvider::new( - params.hash_algorithm(), - params.iterations(), - params.salt().clone(), - ); - Self { - params, - opt_out: Default::default(), - ttl_mode: Default::default(), - hash_provider, - _phantom: Default::default(), - } - } -} - //------------ Nsec3Records --------------------------------------------------- pub struct Nsec3Records { /// The NSEC3 records. - pub recs: Vec>>, + pub nsec3s: Vec>>, /// The NSEC3PARAM record. - pub param: Record>, + pub nsec3param: Record>, } impl Nsec3Records { pub fn new( - recs: Vec>>, - param: Record>, + nsec3s: Vec>>, + nsec3param: Record>, ) -> Self { - Self { recs, param } + Self { nsec3s, nsec3param } } } diff --git a/src/sign/error.rs b/src/sign/error.rs index 545e99f28..d0a4889cc 100644 --- a/src/sign/error.rs +++ b/src/sign/error.rs @@ -86,6 +86,12 @@ impl From for SigningError { } } +impl From for SigningError { + fn from(err: Nsec3HashError) -> Self { + Self::Nsec3HashingError(err) + } +} + //----------- SignError ------------------------------------------------------ /// A signature failure. diff --git a/src/sign/mod.rs b/src/sign/mod.rs index f8a360a43..901ff9288 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -118,7 +118,6 @@ pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; pub use self::config::SigningConfig; pub use self::keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; -use core::cmp::min; use core::fmt::Display; use core::hash::Hash; use core::marker::PhantomData; @@ -134,17 +133,16 @@ use crate::rdata::ZoneRecordData; use denial::config::DenialConfig; use denial::nsec::generate_nsecs; -use denial::nsec3::{ - generate_nsec3s, Nsec3Config, Nsec3HashProvider, Nsec3ParamTtlMode, - Nsec3Records, -}; +use denial::nsec3::{generate_nsec3s, Nsec3HashProvider, Nsec3Records}; use error::SigningError; use keys::keymeta::DesignatedSigningKey; use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, }; use records::{RecordsIter, Sorter}; -use signatures::rrsigs::{generate_rrsigs, GenerateRrsigConfig}; +use signatures::rrsigs::{ + generate_rrsigs, GenerateRrsigConfig, RrsigRecords, +}; use signatures::strategy::SigningKeyUsageStrategy; use traits::{SignRaw, SignableZone, SortedExtend}; @@ -390,18 +388,8 @@ where // one SOA record. let soa_rr = get_apex_soa_rr(in_out.as_slice())?; - // Check that the RDATA for the SOA record can be parsed. - let ZoneRecordData::Soa(ref soa_data) = soa_rr.data() else { - return Err(SigningError::SoaRecordCouldNotBeDetermined); - }; - let apex_owner = soa_rr.owner().clone(); - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // the MINIMUM field of the SOA record and the TTL of the SOA itself". - let ttl = min(soa_data.minimum(), soa_rr.ttl()); - let owner_rrs = RecordsIter::new(in_out.as_slice()); match &mut signing_config.denial { @@ -416,42 +404,17 @@ where in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } - DenialConfig::Nsec3( - Nsec3Config { - params, - opt_out, - ttl_mode, - hash_provider, - .. - }, - extra, - ) if extra.is_empty() => { + DenialConfig::Nsec3(ref mut config, extra) if extra.is_empty() => { // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash // order." We store the NSEC3s as we create them and sort them // afterwards. - let Nsec3Records { recs, mut param } = - generate_nsec3s::( - ttl, - owner_rrs, - params.clone(), - *opt_out, - signing_config.add_used_dnskeys, - hash_provider, - ) - .map_err(SigningError::Nsec3HashingError)?; - - let ttl = match ttl_mode { - Nsec3ParamTtlMode::Fixed(ttl) => *ttl, - Nsec3ParamTtlMode::Soa => soa_rr.ttl(), - Nsec3ParamTtlMode::SoaMinimum => soa_data.minimum(), - }; - - param.set_ttl(ttl); + let Nsec3Records { nsec3s, nsec3param } = + generate_nsec3s::(owner_rrs, config)?; // Add the generated NSEC3 records. in_out.sorted_extend( - std::iter::once(Record::from_record(param)) - .chain(recs.into_iter().map(Record::from_record)), + std::iter::once(Record::from_record(nsec3param)) + .chain(nsec3s.into_iter().map(Record::from_record)), ); } @@ -482,7 +445,7 @@ where // Sign the NSEC(3)s. let owner_rrs = RecordsIter::new(in_out.as_out_slice()); - let nsec_rrsigs = + let RrsigRecords { rrsigs, dnskeys } = generate_rrsigs::( owner_rrs, signing_keys, @@ -491,12 +454,17 @@ where // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. - in_out.sorted_extend(nsec_rrsigs); + in_out.sorted_extend( + dnskeys + .into_iter() + .map(Record::from_record) + .chain(rrsigs.into_iter().map(Record::from_record)), + ); // Sign the original unsigned records. let owner_rrs = RecordsIter::new(in_out.as_slice()); - let rrsigs_and_dnskeys = + let RrsigRecords { rrsigs, dnskeys } = generate_rrsigs::( owner_rrs, signing_keys, @@ -505,7 +473,12 @@ where // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. - in_out.sorted_extend(rrsigs_and_dnskeys); + in_out.sorted_extend( + dnskeys + .into_iter() + .map(Record::from_record) + .chain(rrsigs.into_iter().map(Record::from_record)), + ); } Ok(()) diff --git a/src/sign/records.rs b/src/sign/records.rs index ee2519307..3122d32c3 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -15,6 +15,8 @@ use crate::base::name::ToName; use crate::base::rdata::RecordData; use crate::base::record::Record; use crate::base::Ttl; +use crate::zonetree::types::StoredRecordData; +use crate::zonetree::StoredName; //------------ Sorter -------------------------------------------------------- @@ -64,8 +66,11 @@ impl Sorter for DefaultSorter { /// overridden by being generic over an alternate implementation of /// [`Sorter`]. #[derive(Clone)] -pub struct SortedRecords -where +pub struct SortedRecords< + N = StoredName, + D = StoredRecordData, + Sort = DefaultSorter, +> where Record: Send, Sort: Sorter, { @@ -310,9 +315,7 @@ where } } -impl Default - for SortedRecords -{ +impl Default for SortedRecords { fn default() -> Self { Self::new() } diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 52c778036..d7636b0e6 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -20,7 +20,7 @@ use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; use crate::base::Name; use crate::rdata::dnssec::ProtoRrsig; -use crate::rdata::{Dnskey, ZoneRecordData}; +use crate::rdata::{Dnskey, Rrsig, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::keys::signingkey::SigningKey; @@ -31,6 +31,8 @@ use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::traits::SignRaw; use smallvec::SmallVec; +//----------- GenerateRrsigConfig -------------------------------------------- + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { pub add_used_dnskeys: bool, @@ -79,6 +81,46 @@ impl Default } } +//------------ RrsigRecords -------------------------------------------------- + +#[derive(Clone, Debug)] +pub struct RrsigRecords +where + Octs: AsRef<[u8]>, +{ + /// The NSEC3 records. + pub rrsigs: Vec>>, + + /// The DNSKEY records. + pub dnskeys: Vec>>, +} + +impl RrsigRecords +where + Octs: AsRef<[u8]>, +{ + pub fn new( + rrsigs: Vec>>, + dnskeys: Vec>>, + ) -> Self { + Self { rrsigs, dnskeys } + } +} + +impl Default for RrsigRecords +where + Octs: AsRef<[u8]>, +{ + fn default() -> Self { + Self { + rrsigs: Default::default(), + dnskeys: Default::default(), + } + } +} + +//------------ generate_rrsigs() --------------------------------------------- + /// Generate RRSIG RRs for a collection of zone records. /// /// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be @@ -102,7 +144,7 @@ pub fn generate_rrsigs( records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], config: &GenerateRrsigConfig<'_, N, KeyStrat, Sort>, -) -> Result>>, SigningError> +) -> Result, SigningError> where DSK: DesignatedSigningKey, Inner: SignRaw, @@ -141,7 +183,7 @@ where // No records were provided. As we are able to generate RRSIGs for // partial zones this is a special case of a partial zone, an empty // input, for which there is nothing to do. - return Ok(vec![]); + return Ok(RrsigRecords::default()); }; let first_owner = first_rrs.owner().clone(); @@ -207,7 +249,7 @@ where ); } - let mut res: Vec>> = Vec::new(); + let mut out = RrsigRecords::default(); let mut reusable_scratch = Vec::new(); let mut cut: Option = None; @@ -223,7 +265,7 @@ where &dnskey_signing_key_idxs, &non_dnskey_signing_key_idxs, &keys_in_use_idxs, - &mut res, + &mut out, &mut reusable_scratch, )?; } @@ -280,7 +322,7 @@ where zone_apex, &mut reusable_scratch, )?; - res.push(rrsig_rr); + out.rrsigs.push(rrsig_rr); debug!( "Signed {} RRSET at {} with keytag {}", rrset.rtype(), @@ -291,9 +333,13 @@ where } } - debug!("Returning {} records from signature generation", res.len()); + debug!( + "Returning {} RRSIG RRs and {} DNSKEY RRs from signature generation", + out.rrsigs.len(), + out.dnskeys.len(), + ); - Ok(res) + Ok(out) } fn log_keys_in_use( @@ -361,7 +407,7 @@ fn generate_apex_rrsigs( dnskey_signing_key_idxs: &[usize], non_dnskey_signing_key_idxs: &[usize], keys_in_use_idxs: &[usize], - generated_rrs: &mut Vec>>, + out: &mut RrsigRecords, reusable_scratch: &mut Vec, ) -> Result<(), SigningError> where @@ -474,11 +520,11 @@ where if config.add_used_dnskeys && is_new_dnskey { // Add the DNSKEY RR to the set of new RRs to output for the zone. - generated_rrs.push(Record::new( + out.dnskeys.push(Record::new( zone_apex.clone(), zone_class, dnskey_rrset_ttl, - Dnskey::convert(dnskey).into(), + Dnskey::convert(dnskey), )); } } @@ -502,7 +548,7 @@ where for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { let rrsig_rr = sign_rrset_in(key, &rrset, zone_apex, reusable_scratch)?; - generated_rrs.push(rrsig_rr); + out.rrsigs.push(rrsig_rr); trace!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", rrset.iter().len(), @@ -529,7 +575,7 @@ pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, apex_owner: &N, -) -> Result>, SigningError> +) -> Result>, SigningError> where N: ToName + Clone + Send, D: RecordData @@ -568,7 +614,7 @@ pub fn sign_rrset_in( rrset: &Rrset<'_, N, D>, apex_owner: &N, scratch: &mut Vec, -) -> Result>, SigningError> +) -> Result>, SigningError> where N: ToName + Clone + Send, D: RecordData @@ -655,7 +701,7 @@ where rrset.owner().clone(), rrset.class(), rrset.ttl(), - ZoneRecordData::Rrsig(rrsig), + rrsig, )) } @@ -673,8 +719,7 @@ mod tests { use crate::sign::keys::DnssecSigningKey; use crate::sign::test_util::*; use crate::sign::{test_util, PublicKeyBytes, Signature}; - use crate::zonetree::types::StoredRecordData; - use crate::zonetree::{StoredName, StoredRecord}; + use crate::zonetree::StoredName; use super::*; use rand::Rng; @@ -693,14 +738,12 @@ mod tests { // ... // "For example, "www.example.com." has a Labels field value of 3" // We can use any class as RRSIGs are class independent. - let records = [mk_a_rr("www.example.com.")]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("www.example.com.")).unwrap(); let rrset = Rrset::new(&records); let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); - - let ZoneRecordData::Rrsig(rrsig) = rrsig_rr.data() else { - unreachable!(); - }; + let rrsig = rrsig_rr.data(); // RFC 4035 // 2.2. Including RRSIG RRs in a Zone @@ -753,14 +796,12 @@ mod tests { // 3.1.3. The Labels Field // ... // ""*.example.com." has a Labels field value of 2" - let records = [mk_a_rr("*.example.com.")]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("*.example.com.")).unwrap(); let rrset = Rrset::new(&records); let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); - - let ZoneRecordData::Rrsig(rrsig) = rrsig_rr.data() else { - unreachable!(); - }; + let rrsig = rrsig_rr.data(); assert_eq!(rrsig.labels(), 2); } @@ -777,7 +818,10 @@ mod tests { let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); let dnskey = key.public_key().to_dnskey().convert(); - let records = [mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)]; + let mut records = SortedRecords::default(); + records + .insert(mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)) + .unwrap(); let rrset = Rrset::new(&records); let res = sign_rrset(&key, &rrset, &apex_owner); @@ -806,7 +850,8 @@ mod tests { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let records = [mk_a_rr("any.")]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("any.")).unwrap(); let rrset = Rrset::new(&records); fn calc_timestamps( @@ -908,7 +953,7 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { - let records: [Record; 0] = []; + let records = SortedRecords::default(); let no_keys: [DnssecSigningKey; 0] = []; generate_rrsigs( @@ -921,7 +966,8 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { - let records = [mk_a_rr("example.")]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("example.")).unwrap(); let no_keys: [DnssecSigningKey; 0] = []; let res = generate_rrsigs( @@ -936,7 +982,8 @@ mod tests { #[test] fn generate_rrsigs_without_suitable_keys_should_fail_for_non_empty_zone() { - let records = [mk_a_rr("example.")]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("example.")).unwrap(); let res = generate_rrsigs( RecordsIter::new(&records), @@ -974,7 +1021,8 @@ mod tests { // This is an example of generating RRSIGs for something other than a // full zone, in this case just for an A record. This test // deliberately does not include a SOA record as the zone is partial. - let records = [mk_a_rr(record_owner)]; + let mut records = SortedRecords::default(); + records.insert(mk_a_rr(record_owner)).unwrap(); // Prepare a zone signing key and a key signing key. let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; @@ -995,9 +1043,10 @@ mod tests { // Check the generated RRSIG records let expected_labels = mk_name(record_owner).rrsig_label_count(); - assert_eq!(generated_records.len(), 1); + assert_eq!(generated_records.rrsigs.len(), 1); + assert!(generated_records.dnskeys.is_empty()); assert_eq!( - generated_records[0], + generated_records.rrsigs[0], mk_rrsig_rr( record_owner, Rtype::A, @@ -1010,11 +1059,12 @@ mod tests { #[test] fn generate_rrsigs_ignores_records_outside_the_zone() { - let records = [ + let mut records = SortedRecords::default(); + records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_a_rr("in_zone.example."), mk_a_rr("out_of_zone."), - ]; + ]); // Prepare a zone signing key and a key signing key. let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; @@ -1029,9 +1079,13 @@ mod tests { // Check the generated records assert_eq!( - generated_records, + generated_records.dnskeys, + [mk_dnskey_rr("example.", &dnskey),] + ); + + assert_eq!( + generated_records.rrsigs, [ - mk_dnskey_rr("example.", &dnskey), mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), mk_rrsig_rr( "example.", @@ -1060,9 +1114,12 @@ mod tests { ) .unwrap(); + // Check the generated DNSKEY records + assert!(generated_records.dnskeys.is_empty()); + // Check the generated RRSIG records assert_eq!( - generated_records, + generated_records.rrsigs, [mk_rrsig_rr( "out_of_zone.", Rtype::A, @@ -1075,11 +1132,12 @@ mod tests { #[test] fn generate_rrsigs_fails_with_multiple_soas_at_apex() { - let records = [ + let mut records = SortedRecords::default(); + records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_soa_rr("example.", "other.mname.", "other.rname."), mk_a_rr("in_zone.example."), - ]; + ]); // Prepare a zone signing key and a key signing key. let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; @@ -1194,7 +1252,8 @@ mod tests { let zsk = &dnskeys[zsk_idx]; // Check the generated records. - let mut iter = generated_records.iter(); + let mut dnskey_iter = generated_records.dnskeys.iter(); + let mut rrsig_iter = generated_records.rrsigs.iter(); // The records should be in a fixed canonical order because the input // records must be in canonical order, with the exception of the added @@ -1218,10 +1277,13 @@ mod tests { if cfg.add_used_dnskeys { // DNSKEY records should have been generated for the apex for both // of the keys that we used to sign the zone. - assert_eq!(*iter.next().unwrap(), mk_dnskey_rr("example.", ksk)); + assert_eq!( + *dnskey_iter.next().unwrap(), + mk_dnskey_rr("example.", ksk) + ); if ksk_idx != zsk_idx { assert_eq!( - *iter.next().unwrap(), + *dnskey_iter.next().unwrap(), mk_dnskey_rr("example.", zsk) ); } @@ -1230,15 +1292,15 @@ mod tests { // RRSIG records should have been generated for the zone apex records, // one RRSIG per ZSK used. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("example.", Rtype::NS, 1, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("example.", Rtype::MX, 1, "example.", zsk) ); // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. @@ -1266,7 +1328,7 @@ mod tests { // keys based on their `IntendedKeyPurpose` which we assigned above // when creating the keys. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", ksk) ); @@ -1283,7 +1345,7 @@ mod tests { // zone's name servers) MUST NOT be signed." assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", zsk) ); @@ -1314,15 +1376,15 @@ mod tests { // -- ai.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", zsk) ); @@ -1345,56 +1407,56 @@ mod tests { // -- ns1.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", zsk) ); // -- ns2.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", zsk) ); // -- *.w.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", zsk) ); // -- x.w.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", zsk) ); // -- x.y.w.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", zsk) ); // -- xx.example. assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", zsk) ); assert_eq!( - *iter.next().unwrap(), + *rrsig_iter.next().unwrap(), mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", zsk) ); // No other records should have been generated. - assert!(iter.next().is_none()); + assert!(rrsig_iter.next().is_none()); Ok(()) } @@ -1402,11 +1464,13 @@ mod tests { #[test] fn generate_rrsigs_for_complete_zone_with_multiple_ksks_and_zsks() { let apex = "example."; - let records = [ + + let mut records = SortedRecords::default(); + records.extend([ mk_soa_rr(apex, "some.mname.", "some.rname."), mk_ns_rr(apex, "ns.example."), mk_a_rr("ns.example."), - ]; + ]); let keys = [ mk_dnssec_signing_key(IntendedKeyPurpose::KSK), @@ -1428,16 +1492,33 @@ mod tests { .unwrap(); // Check the generated records. - assert_eq!(generated_records.len(), 12); + assert_eq!(generated_records.dnskeys.len(), 4); + assert_eq!(generated_records.rrsigs.len(), 8); // Filter out the records one by one until there should be none left. let it = generated_records + .dnskeys .iter() .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk1)) .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk2)) .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk1)) - .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk2)) + .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk2)); + + let mut it = it.inspect(|rr| { + eprintln!( + "Warning: Unexpected DNSKEY RRs remaining after filtering: {} {} => {:?}", + rr.owner(), + rr.rtype(), + rr.data(), + ); + }); + + assert!(it.next().is_none()); + + let it = generated_records + .rrsigs + .iter() .filter(|&rr| { rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk1) }) @@ -1460,15 +1541,12 @@ mod tests { }); let mut it = it.inspect(|rr| { - eprint!( - "Warning: Unexpected record remaining after filtering: {} {}", + eprintln!( + "Warning: Unexpected RRSIG RRs remaining after filtering: {} {} => {:?}", rr.owner(), - rr.rtype() + rr.rtype(), + rr.data(), ); - if let ZoneRecordData::Rrsig(rrsig) = rr.data() { - eprint!(" => {:?}", rrsig); - } - eprintln!(); }); assert!(it.next().is_none()); @@ -1480,30 +1558,23 @@ mod tests { let dnskey = keys[0].public_key().to_dnskey().convert(); - let records = [ + let mut records = SortedRecords::default(); + records.extend([ // -- example. mk_soa_rr("example.", "some.mname.", "some.rname."), mk_ns_rr("example.", "ns.example."), mk_dnskey_rr("example.", &dnskey), - Record::from_record(mk_nsec_rr( - "example", - "ns.example.", - "SOA NS DNSKEY NSEC RRSIG", - )), + mk_nsec_rr("example", "ns.example.", "SOA NS DNSKEY NSEC RRSIG"), mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey), mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey), // -- ns.example. mk_a_rr("ns.example."), - Record::from_record(mk_nsec_rr( - "ns.example", - "example.", - "A NSEC RRSIG", - )), + mk_nsec_rr("ns.example", "example.", "A NSEC RRSIG"), mk_rrsig_rr("ns.example.", Rtype::A, 1, "example.", &dnskey), mk_rrsig_rr("ns.example.", Rtype::NSEC, 1, "example.", &dnskey), - ]; + ]); let generated_records = generate_rrsigs( RecordsIter::new(&records), @@ -1513,7 +1584,7 @@ mod tests { .unwrap(); // Check the generated records. - let mut iter = generated_records.iter(); + let mut iter = generated_records.rrsigs.iter(); // The records should be in a fixed canonical order because the input // records must be in canonical order, with the exception of the added @@ -1533,16 +1604,17 @@ mod tests { // The DNSKEY was already present in the zone so we do NOT expect a // DNSKEY to be included in the output. + assert!(generated_records.dnskeys.is_empty()); // RRSIG records should have been generated for the zone apex records, // one RRSIG per ZSK used, even if RRSIG RRs already exist. assert_eq!( *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey) + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey) ); assert_eq!( *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey) + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey) ); assert_eq!( *iter.next().unwrap(), @@ -1598,7 +1670,13 @@ mod tests { DnssecSigningKey::new(key, purpose) } - fn mk_dnskey_rr(name: &str, dnskey: &Dnskey) -> StoredRecord { + fn mk_dnskey_rr( + name: &str, + dnskey: &Dnskey, + ) -> Record + where + R: From>, + { test_util::mk_dnskey_rr( name, dnskey.flags(), @@ -1607,13 +1685,16 @@ mod tests { ) } - fn mk_rrsig_rr( + fn mk_rrsig_rr( name: &str, covered_rtype: Rtype, labels: u8, signer_name: &str, dnskey: &Dnskey, - ) -> StoredRecord { + ) -> Record + where + R: From>, + { test_util::mk_rrsig_rr( name, covered_rtype, diff --git a/src/sign/test_util/mod.rs b/src/sign/test_util/mod.rs index 6eeca9c72..eb14767bc 100644 --- a/src/sign/test_util/mod.rs +++ b/src/sign/test_util/mod.rs @@ -11,7 +11,7 @@ use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; use crate::rdata::{Dnskey, Ns, Nsec, Rrsig, Soa, A}; use crate::zonefile::inplace::{Entry, Zonefile}; use crate::zonetree::types::StoredRecordData; -use crate::zonetree::{StoredName, StoredRecord}; +use crate::zonetree::StoredName; use super::records::SortedRecords; @@ -35,73 +35,67 @@ pub(crate) fn mk_name(name: &str) -> StoredName { StoredName::from_str(name).unwrap() } -pub(crate) fn mk_record(owner: &str, data: StoredRecordData) -> StoredRecord { +pub(crate) fn mk_record(owner: &str, data: D) -> Record { Record::new(mk_name(owner), Class::IN, TEST_TTL, data) } -pub(crate) fn mk_nsec_rr( - owner: &str, - next_name: &str, - types: &str, -) -> Record> { - let owner = mk_name(owner); - let next_name = mk_name(next_name); - let mut builder = RtypeBitmap::::builder(); - for rtype in types.split_whitespace() { - builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); - } - let types = builder.finalize(); - Record::new(owner, Class::IN, TEST_TTL, Nsec::new(next_name, types)) -} - -pub(crate) fn mk_soa_rr( - name: &str, - mname: &str, - rname: &str, -) -> StoredRecord { - let soa = Soa::new( - mk_name(mname), - mk_name(rname), - Serial::now(), - TEST_TTL, - TEST_TTL, - TEST_TTL, - TEST_TTL, - ); - mk_record(name, soa.into()) +pub(crate) fn mk_a_rr(owner: &str) -> Record +where + R: From, +{ + mk_record(owner, A::from_str("1.2.3.4").unwrap().into()) } -pub(crate) fn mk_a_rr(name: &str) -> StoredRecord { - mk_record(name, A::from_str("1.2.3.4").unwrap().into()) -} - -pub(crate) fn mk_ns_rr(name: &str, nsdname: &str) -> StoredRecord { - let nsdname = mk_name(nsdname); - mk_record(name, Ns::new(nsdname).into()) -} - -pub(crate) fn mk_dnskey_rr( - name: &str, +pub(crate) fn mk_dnskey_rr( + owner: &str, flags: u16, algorithm: SecAlg, public_key: &Bytes, -) -> StoredRecord { +) -> Record +where + R: From>, +{ // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.2 // 2.1.2. The Protocol Field // "The Protocol Field MUST have value 3, and the DNSKEY RR MUST be // treated as invalid during signature verification if it is found to // be some value other than 3." mk_record( - name, + owner, Dnskey::new(flags, 3, algorithm, public_key.clone()) .unwrap() .into(), ) } +pub(crate) fn mk_ns_rr(owner: &str, nsdname: &str) -> Record +where + R: From>, +{ + let nsdname = mk_name(nsdname); + mk_record(owner, Ns::new(nsdname).into()) +} + +pub(crate) fn mk_nsec_rr( + owner: &str, + next_name: &str, + types: &str, +) -> Record +where + R: From>, +{ + let next_name = mk_name(next_name); + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + mk_record(owner, Nsec::new(next_name, types).into()) +} + #[allow(clippy::too_many_arguments)] -pub(crate) fn mk_rrsig_rr( - name: &str, +pub(crate) fn mk_rrsig_rr( + owner: &str, covered_rtype: Rtype, algorithm: &SecAlg, labels: u8, @@ -110,12 +104,15 @@ pub(crate) fn mk_rrsig_rr( key_tag: u16, signer_name: &str, signature: Bytes, -) -> StoredRecord { +) -> Record +where + R: From>, +{ let signer_name = mk_name(signer_name); let expiration = Timestamp::from(expiration); let inception = Timestamp::from(inception); mk_record( - name, + owner, Rrsig::new( covered_rtype, *algorithm, @@ -132,6 +129,26 @@ pub(crate) fn mk_rrsig_rr( ) } +pub(crate) fn mk_soa_rr( + owner: &str, + mname: &str, + rname: &str, +) -> Record +where + R: From>, +{ + let soa = Soa::new( + mk_name(mname), + mk_name(rname), + Serial::now(), + TEST_TTL, + TEST_TTL, + TEST_TTL, + TEST_TTL, + ); + mk_record(owner, soa.into()) +} + #[allow(clippy::type_complexity)] pub(crate) fn contains_owner( nsecs: &[Record>], diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 49c6d485a..5ee34e98d 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -30,6 +30,7 @@ use crate::sign::records::{ use crate::sign::sign_zone; use crate::sign::signatures::rrsigs::generate_rrsigs; use crate::sign::signatures::rrsigs::GenerateRrsigConfig; +use crate::sign::signatures::rrsigs::RrsigRecords; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::SigningConfig; use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; @@ -499,12 +500,12 @@ where &self, expected_apex: &N, keys: &[DSK], - ) -> Result>>, SigningError> + ) -> Result, SigningError> where DSK: DesignatedSigningKey, KeyStrat: SigningKeyUsageStrategy, { - generate_rrsigs::<_, _, DSK, _, KeyStrat, Sort>( + generate_rrsigs::( self.owner_rrs(), keys, &GenerateRrsigConfig::new().with_zone_apex(expected_apex), From 0680c1f1a21a9f56b6f88e546111ef15b91478b2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:14:11 +0100 Subject: [PATCH 397/569] Fix broken doc test, restore flexible signature for Default impl for SortedRecords. --- src/sign/denial/nsec.rs | 14 ++++++++++---- src/sign/records.rs | 11 +++-------- src/sign/signatures/rrsigs.rs | 16 +++++++++++----- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index cc363ec43..26084bf48 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -180,12 +180,15 @@ mod tests { use crate::base::Ttl; use crate::sign::records::SortedRecords; use crate::sign::test_util::*; + use crate::zonetree::types::StoredRecordData; + use crate::zonetree::StoredName; use super::*; #[test] fn soa_is_required() { - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_a_rr("some_a.a.")).unwrap(); let res = generate_nsecs(records.owner_rrs(), false); assert!(matches!( @@ -196,7 +199,8 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); let res = generate_nsecs(records.owner_rrs(), false); @@ -208,7 +212,8 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); records.insert(mk_a_rr("some_a.b.")).unwrap(); @@ -272,7 +277,8 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records.insert(mk_a_rr("some_a.a.")).unwrap(); diff --git a/src/sign/records.rs b/src/sign/records.rs index 3122d32c3..ed70ad64c 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -15,8 +15,6 @@ use crate::base::name::ToName; use crate::base::rdata::RecordData; use crate::base::record::Record; use crate::base::Ttl; -use crate::zonetree::types::StoredRecordData; -use crate::zonetree::StoredName; //------------ Sorter -------------------------------------------------------- @@ -66,11 +64,8 @@ impl Sorter for DefaultSorter { /// overridden by being generic over an alternate implementation of /// [`Sorter`]. #[derive(Clone)] -pub struct SortedRecords< - N = StoredName, - D = StoredRecordData, - Sort = DefaultSorter, -> where +pub struct SortedRecords +where Record: Send, Sort: Sorter, { @@ -315,7 +310,7 @@ where } } -impl Default for SortedRecords { +impl Default for SortedRecords { fn default() -> Self { Self::new() } diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d7636b0e6..cda1d0e41 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -722,6 +722,7 @@ mod tests { use crate::zonetree::StoredName; use super::*; + use crate::zonetree::types::StoredRecordData; use rand::Rng; const TEST_INCEPTION: u32 = 0; @@ -738,7 +739,8 @@ mod tests { // ... // "For example, "www.example.com." has a Labels field value of 3" // We can use any class as RRSIGs are class independent. - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_a_rr("www.example.com.")).unwrap(); let rrset = Rrset::new(&records); @@ -796,7 +798,8 @@ mod tests { // 3.1.3. The Labels Field // ... // ""*.example.com." has a Labels field value of 2" - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_a_rr("*.example.com.")).unwrap(); let rrset = Rrset::new(&records); @@ -818,7 +821,8 @@ mod tests { let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); let dnskey = key.public_key().to_dnskey().convert(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records .insert(mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)) .unwrap(); @@ -850,7 +854,8 @@ mod tests { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_a_rr("any.")).unwrap(); let rrset = Rrset::new(&records); @@ -953,7 +958,8 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { - let records = SortedRecords::default(); + let records = + SortedRecords::::default(); let no_keys: [DnssecSigningKey; 0] = []; generate_rrsigs( From 14cd78f100f5830797ddc845632641e8b2746589 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:52:16 +0100 Subject: [PATCH 398/569] More normalization of the generate_xxx interfaces to take config objects. --- src/sign/denial/config.rs | 22 ++- src/sign/denial/nsec.rs | 61 ++++++-- src/sign/denial/nsec3.rs | 290 ++++++++++++++++++++++++++++++++++---- src/sign/mod.rs | 11 +- 4 files changed, 341 insertions(+), 43 deletions(-) diff --git a/src/sign/denial/config.rs b/src/sign/denial/config.rs index a7952c428..9a271246b 100644 --- a/src/sign/denial/config.rs +++ b/src/sign/denial/config.rs @@ -2,10 +2,13 @@ use core::convert::From; use std::vec::Vec; +use super::nsec::GenerateNsecConfig; use super::nsec3::{ GenerateNsec3Config, Nsec3HashProvider, OnDemandNsec3HashProvider, }; +use crate::base::{Name, ToName}; use crate::sign::records::DefaultSorter; +use octseq::{EmptyBuilder, FromBuilder}; //------------ NsecToNsec3TransitionState ------------------------------------ @@ -74,7 +77,7 @@ pub enum Nsec3ToNsecTransitionState { /// /// This type can be used to choose which denial mechanism should be used when /// DNSSEC signing a zone. -#[derive(Clone, Debug, Default, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum DenialConfig< N, O, @@ -88,8 +91,7 @@ pub enum DenialConfig< AlreadyPresent, /// The zone already has NSEC records. - #[default] - Nsec, + Nsec(GenerateNsecConfig), /// The zone already has NSEC3 records, possibly more than one set. /// @@ -117,13 +119,27 @@ pub enum DenialConfig< /// The zone is transitioning from NSEC to NSEC3. TransitioningNsecToNsec3( + GenerateNsecConfig, GenerateNsec3Config, NsecToNsec3TransitionState, ), /// The zone is transitioning from NSEC3 to NSEC. TransitioningNsec3ToNsec( + GenerateNsecConfig, GenerateNsec3Config, Nsec3ToNsecTransitionState, ), } + +impl Default + for DenialConfig, DefaultSorter> +where + N: ToName + From>, + O: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn default() -> Self { + Self::Nsec(GenerateNsecConfig::default()) + } +} diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 26084bf48..f1b1e1b72 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -13,6 +13,34 @@ use crate::rdata::{Nsec, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::records::RecordsIter; +//----------- GenerateNsec3Config -------------------------------------------- + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct GenerateNsecConfig { + pub assume_dnskeys_will_be_added: bool, +} + +impl GenerateNsecConfig { + pub fn new() -> Self { + Self { + assume_dnskeys_will_be_added: true, + } + } + + pub fn without_assuming_dnskeys_will_be_added(mut self) -> Self { + self.assume_dnskeys_will_be_added = false; + self + } +} + +impl Default for GenerateNsecConfig { + fn default() -> Self { + Self { + assume_dnskeys_will_be_added: true, + } + } +} + /// Generate DNSSEC NSEC records for an unsigned zone. /// /// This function returns a collection of generated NSEC records for the given @@ -40,7 +68,7 @@ use crate::sign::records::RecordsIter; #[allow(clippy::type_complexity)] pub fn generate_nsecs( records: RecordsIter<'_, N, ZoneRecordData>, - assume_dnskeys_will_be_added: bool, + config: &GenerateNsecConfig, ) -> Result>>, SigningError> where N: ToName + Clone + PartialEq, @@ -106,7 +134,9 @@ where // its corresponding RRSIG record." bitmap.add(Rtype::RRSIG).unwrap(); - if assume_dnskeys_will_be_added && owner_rrs.owner() == &apex_owner { + if config.assume_dnskeys_will_be_added + && owner_rrs.owner() == &apex_owner + { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); } @@ -187,10 +217,12 @@ mod tests { #[test] fn soa_is_required() { + let cfg = GenerateNsecConfig::new() + .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); records.insert(mk_a_rr("some_a.a.")).unwrap(); - let res = generate_nsecs(records.owner_rrs(), false); + let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -199,11 +231,13 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { + let cfg = GenerateNsecConfig::new() + .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); - let res = generate_nsecs(records.owner_rrs(), false); + let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -212,6 +246,8 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { + let cfg = GenerateNsecConfig::new() + .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); @@ -225,7 +261,7 @@ mod tests { // zone and NSECs should only be generated for the first zone in the // collection. let a_and_b_records = records.owner_rrs(); - let nsecs = generate_nsecs(a_and_b_records, false).unwrap(); + let nsecs = generate_nsecs(a_and_b_records, &cfg).unwrap(); assert_eq!( nsecs, @@ -239,7 +275,7 @@ mod tests { // remaining records which should only generate NSECs for the b zone. let mut b_records_only = records.owner_rrs(); b_records_only.skip_before(&mk_name("b.")); - let nsecs = generate_nsecs(b_records_only, false).unwrap(); + let nsecs = generate_nsecs(b_records_only, &cfg).unwrap(); assert_eq!( nsecs, @@ -252,6 +288,8 @@ mod tests { #[test] fn occluded_records_are_ignored() { + let cfg = GenerateNsecConfig::new() + .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); @@ -260,7 +298,7 @@ mod tests { .unwrap(); records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); - let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); // Implicit negative test. assert_eq!( @@ -277,13 +315,15 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { + let cfg = GenerateNsecConfig::new(); + let mut records = SortedRecords::::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records.insert(mk_a_rr("some_a.a.")).unwrap(); - let nsecs = generate_nsecs(records.owner_rrs(), true).unwrap(); + let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -296,13 +336,16 @@ mod tests { #[test] fn rfc_4034_and_9077_compliant() { + let cfg = GenerateNsecConfig::new() + .without_assuming_dnskeys_will_be_added(); + // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A let zonefile = include_bytes!( "../../../test-data/zonefiles/rfc4035-appendix-A.zone" ); let records = bytes_to_records(&zonefile[..]); - let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); assert_eq!(nsecs.len(), 10); diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index d0ea0f0fc..24263c71d 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -797,29 +797,267 @@ impl Nsec3Records { } } -// TODO: Add tests for nsec3s() that validate the following from RFC 5155: -// -// 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. -// However, if there is not a corresponding NSEC3 RR, there MUST be -// 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." +// #[cfg(test)] +// mod tests { +// use pretty_assertions::assert_eq; + +// use crate::base::Ttl; +// use crate::sign::records::SortedRecords; +// use crate::sign::test_util::*; +// use crate::zonetree::types::StoredRecordData; +// use crate::zonetree::StoredName; + +// use super::*; + +// #[test] +// fn soa_is_required() { +// let mut records = +// SortedRecords::::default(); +// records.insert(mk_a_rr("some_a.a.")).unwrap(); +// let res = generate_nsec3s(records.owner_rrs(), false); +// assert!(matches!( +// res, +// Err(SigningError::SoaRecordCouldNotBeDetermined) +// )); +// } + +// #[test] +// fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { +// let mut records = +// SortedRecords::::default(); +// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); +// records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); +// let res = generate_nsecs(records.owner_rrs(), false); +// assert!(matches!( +// res, +// Err(SigningError::SoaRecordCouldNotBeDetermined) +// )); +// } + +// #[test] +// fn records_outside_zone_are_ignored() { +// let mut records = +// SortedRecords::::default(); + +// records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); +// records.insert(mk_a_rr("some_a.b.")).unwrap(); +// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); +// records.insert(mk_a_rr("some_a.a.")).unwrap(); + +// // First generate NSECs for the total record collection. As the +// // collection is sorted in canonical order the a zone preceeds the b +// // zone and NSECs should only be generated for the first zone in the +// // collection. +// let a_and_b_records = records.owner_rrs(); +// let nsecs = generate_nsecs(a_and_b_records, false).unwrap(); + +// assert_eq!( +// nsecs, +// [ +// mk_nsec_rr("a.", "some_a.a.", "SOA RRSIG NSEC"), +// mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), +// ] +// ); + +// // Now skip the a zone in the collection and generate NSECs for the +// // remaining records which should only generate NSECs for the b zone. +// let mut b_records_only = records.owner_rrs(); +// b_records_only.skip_before(&mk_name("b.")); +// let nsecs = generate_nsecs(b_records_only, false).unwrap(); + +// assert_eq!( +// nsecs, +// [ +// mk_nsec_rr("b.", "some_a.b.", "SOA RRSIG NSEC"), +// mk_nsec_rr("some_a.b.", "b.", "A RRSIG NSEC"), +// ] +// ); +// } + +// #[test] +// fn occluded_records_are_ignored() { +// let mut records = SortedRecords::default(); + +// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); +// records +// .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) +// .unwrap(); +// records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + +// let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + +// // Implicit negative test. +// assert_eq!( +// nsecs, +// [ +// mk_nsec_rr("a.", "some_ns.a.", "SOA RRSIG NSEC"), +// mk_nsec_rr("some_ns.a.", "a.", "NS RRSIG NSEC"), +// ] +// ); + +// // Explicit negative test. +// assert!(!contains_owner(&nsecs, "some_a.some_ns.a.example.")); +// } + +// #[test] +// fn expect_dnskeys_at_the_apex() { +// let mut records = +// SortedRecords::::default(); + +// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); +// records.insert(mk_a_rr("some_a.a.")).unwrap(); + +// let nsecs = generate_nsecs(records.owner_rrs(), true).unwrap(); + +// assert_eq!( +// nsecs, +// [ +// mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"), +// mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), +// ] +// ); +// } + +// #[test] +// fn rfc_4034_and_9077_compliant() { +// // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A +// let zonefile = include_bytes!( +// "../../../test-data/zonefiles/rfc4035-appendix-A.zone" +// ); + +// let records = bytes_to_records(&zonefile[..]); +// let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); + +// assert_eq!(nsecs.len(), 10); + +// assert_eq!( +// nsecs, +// [ +// mk_nsec_rr("example.", "a.example", "NS SOA MX RRSIG NSEC"), +// mk_nsec_rr("a.example.", "ai.example", "NS DS RRSIG NSEC"), +// mk_nsec_rr( +// "ai.example.", +// "b.example", +// "A HINFO AAAA RRSIG NSEC" +// ), +// mk_nsec_rr("b.example.", "ns1.example", "NS RRSIG NSEC"), +// mk_nsec_rr("ns1.example.", "ns2.example", "A RRSIG NSEC"), +// // The next record also validates that we comply with +// // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 +// // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when +// // it says: +// // "If a wildcard owner name appears in a zone, the wildcard +// // label ("*") is treated as a literal symbol and is treated +// // the same as any other owner name for the purposes of +// // generating NSEC RRs. Wildcard owner names appear in the +// // Next Domain Name field without any wildcard expansion. +// // [RFC4035] describes the impact of wildcards on +// // authenticated denial of existence." +// mk_nsec_rr("ns2.example.", "*.w.example", "A RRSIG NSEC"), +// mk_nsec_rr("*.w.example.", "x.w.example", "MX RRSIG NSEC"), +// mk_nsec_rr("x.w.example.", "x.y.w.example", "MX RRSIG NSEC"), +// mk_nsec_rr("x.y.w.example.", "xx.example", "MX RRSIG NSEC"), +// mk_nsec_rr( +// "xx.example.", +// "example", +// "A HINFO AAAA RRSIG NSEC" +// ) +// ], +// ); + +// // TTLs are not compared by the eq check above so check them +// // explicitly now. +// // +// // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that +// // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of +// // the MINIMUM field of the SOA record and the TTL of the SOA itself". +// // +// // So in our case that is min(1800, 3600) = 1800. +// for nsec in &nsecs { +// assert_eq!(nsec.ttl(), Ttl::from_secs(1800)); +// } + +// // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 +// // 2.3. Including NSEC RRs in a Zone +// // ... +// // "The type bitmap of every NSEC resource record in a signed zone +// // MUST indicate the presence of both the NSEC record itself and its +// // corresponding RRSIG record." +// for nsec in &nsecs { +// assert!(nsec.data().types().contains(Rtype::NSEC)); +// assert!(nsec.data().types().contains(Rtype::RRSIG)); +// } + +// // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 +// // 4.1.2. The Type Bit Maps Field +// // "Bits representing pseudo-types MUST be clear, as they do not +// // appear in zone data." +// // +// // There is nothing to test for this as it is excluded at the Rust +// // type system level by the generate_nsecs() function taking +// // ZoneRecordData (which excludes pseudo record types) as input rather +// // than AllRecordData (which includes pseudo record types). + +// // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 +// // 4.1.2. The Type Bit Maps Field +// // ... +// // "A zone MUST NOT include an NSEC RR for any domain name that only +// // holds glue records." +// // +// // The "rfc4035-appendix-A.zone" file that we load contains glue A +// // records for ns1.example, ns1.a.example, ns1.b.example, ns2.example +// // and ns2.a.example all with no other record types at that name. We +// // can verify that an NSEC RR was NOT created for those that are not +// // within the example zone as we are not authoritative for thos. +// assert!(contains_owner(&nsecs, "ns1.example.")); +// assert!(!contains_owner(&nsecs, "ns1.a.example.")); +// assert!(!contains_owner(&nsecs, "ns1.b.example.")); +// assert!(contains_owner(&nsecs, "ns2.example.")); +// assert!(!contains_owner(&nsecs, "ns2.a.example.")); + +// // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 +// // 2.3. Including NSEC RRs in a Zone +// // ... +// // "The bitmap for the NSEC RR at a delegation point requires special +// // attention. Bits corresponding to the delegation NS RRset and any +// // RRsets for which the parent zone has authoritative data MUST be +// // set; bits corresponding to any non-NS RRset for which the parent +// // is not authoritative MUST be clear." +// // +// // The "rfc4035-appendix-A.zone" file that we load has been modified +// // compared to the original to include a glue A record at b.example. +// // We can verify that an NSEC RR was NOT created for that name. +// let name = mk_name("b.example."); +// let nsec = nsecs.iter().find(|rr| rr.owner() == &name).unwrap(); +// assert!(nsec.data().types().contains(Rtype::NSEC)); +// assert!(nsec.data().types().contains(Rtype::RRSIG)); +// assert!(!nsec.data().types().contains(Rtype::A)); +// } +// } + +// // TODO: Add tests for nsec3s() that validate the following from RFC 5155: +// // +// // 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. +// // However, if there is not a corresponding NSEC3 RR, there MUST be +// // 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." diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 901ff9288..30ebb3d37 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -397,19 +397,18 @@ where // Nothing to do. } - DenialConfig::Nsec => { - let nsecs = - generate_nsecs(owner_rrs, signing_config.add_used_dnskeys)?; + DenialConfig::Nsec(ref cfg) => { + let nsecs = generate_nsecs(owner_rrs, cfg)?; in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } - DenialConfig::Nsec3(ref mut config, extra) if extra.is_empty() => { + DenialConfig::Nsec3(ref mut cfg, extra) if extra.is_empty() => { // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash // order." We store the NSEC3s as we create them and sort them // afterwards. let Nsec3Records { nsec3s, nsec3param } = - generate_nsec3s::(owner_rrs, config)?; + generate_nsec3s::(owner_rrs, cfg)?; // Add the generated NSEC3 records. in_out.sorted_extend( @@ -423,6 +422,7 @@ where } DenialConfig::TransitioningNsecToNsec3( + _nsec_config, _nsec3_config, _nsec_to_nsec3_transition_state, ) => { @@ -430,6 +430,7 @@ where } DenialConfig::TransitioningNsec3ToNsec( + _nsec_config, _nsec3_config, _nsec3_to_nsec_transition_state, ) => { From 5efcccfabd4535a40c8981fc36f89ca73899c3b4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 23 Jan 2025 23:42:46 +0100 Subject: [PATCH 399/569] Initial NSEC3 unit tests based on existing NSEC tests. --- src/sign/denial/nsec.rs | 15 +- src/sign/denial/nsec3.rs | 616 +++++++++++++++++++--------------- src/sign/signatures/rrsigs.rs | 1 - src/sign/test_util/mod.rs | 73 +++- 4 files changed, 428 insertions(+), 277 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index f1b1e1b72..f7479ffa8 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -217,7 +217,7 @@ mod tests { #[test] fn soa_is_required() { - let cfg = GenerateNsecConfig::new() + let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); @@ -231,7 +231,7 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { - let cfg = GenerateNsecConfig::new() + let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); @@ -246,7 +246,7 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { - let cfg = GenerateNsecConfig::new() + let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); let mut records = SortedRecords::::default(); @@ -288,9 +288,10 @@ mod tests { #[test] fn occluded_records_are_ignored() { - let cfg = GenerateNsecConfig::new() + let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::::default(); records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); records @@ -315,7 +316,7 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { - let cfg = GenerateNsecConfig::new(); + let cfg = GenerateNsecConfig::default(); let mut records = SortedRecords::::default(); @@ -336,7 +337,7 @@ mod tests { #[test] fn rfc_4034_and_9077_compliant() { - let cfg = GenerateNsecConfig::new() + let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 24263c71d..24bb39bc3 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -581,6 +581,7 @@ where Ok(Record::new(owner_name, Class::IN, ttl, nsec3)) } + pub fn mk_hashed_nsec3_owner_name( name: &N, alg: Nsec3HashAlg, @@ -797,267 +798,354 @@ impl Nsec3Records { } } -// #[cfg(test)] -// mod tests { -// use pretty_assertions::assert_eq; - -// use crate::base::Ttl; -// use crate::sign::records::SortedRecords; -// use crate::sign::test_util::*; -// use crate::zonetree::types::StoredRecordData; -// use crate::zonetree::StoredName; - -// use super::*; - -// #[test] -// fn soa_is_required() { -// let mut records = -// SortedRecords::::default(); -// records.insert(mk_a_rr("some_a.a.")).unwrap(); -// let res = generate_nsec3s(records.owner_rrs(), false); -// assert!(matches!( -// res, -// Err(SigningError::SoaRecordCouldNotBeDetermined) -// )); -// } - -// #[test] -// fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { -// let mut records = -// SortedRecords::::default(); -// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); -// records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); -// let res = generate_nsecs(records.owner_rrs(), false); -// assert!(matches!( -// res, -// Err(SigningError::SoaRecordCouldNotBeDetermined) -// )); -// } - -// #[test] -// fn records_outside_zone_are_ignored() { -// let mut records = -// SortedRecords::::default(); - -// records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); -// records.insert(mk_a_rr("some_a.b.")).unwrap(); -// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); -// records.insert(mk_a_rr("some_a.a.")).unwrap(); - -// // First generate NSECs for the total record collection. As the -// // collection is sorted in canonical order the a zone preceeds the b -// // zone and NSECs should only be generated for the first zone in the -// // collection. -// let a_and_b_records = records.owner_rrs(); -// let nsecs = generate_nsecs(a_and_b_records, false).unwrap(); - -// assert_eq!( -// nsecs, -// [ -// mk_nsec_rr("a.", "some_a.a.", "SOA RRSIG NSEC"), -// mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), -// ] -// ); - -// // Now skip the a zone in the collection and generate NSECs for the -// // remaining records which should only generate NSECs for the b zone. -// let mut b_records_only = records.owner_rrs(); -// b_records_only.skip_before(&mk_name("b.")); -// let nsecs = generate_nsecs(b_records_only, false).unwrap(); - -// assert_eq!( -// nsecs, -// [ -// mk_nsec_rr("b.", "some_a.b.", "SOA RRSIG NSEC"), -// mk_nsec_rr("some_a.b.", "b.", "A RRSIG NSEC"), -// ] -// ); -// } - -// #[test] -// fn occluded_records_are_ignored() { -// let mut records = SortedRecords::default(); - -// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); -// records -// .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) -// .unwrap(); -// records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); - -// let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); - -// // Implicit negative test. -// assert_eq!( -// nsecs, -// [ -// mk_nsec_rr("a.", "some_ns.a.", "SOA RRSIG NSEC"), -// mk_nsec_rr("some_ns.a.", "a.", "NS RRSIG NSEC"), -// ] -// ); - -// // Explicit negative test. -// assert!(!contains_owner(&nsecs, "some_a.some_ns.a.example.")); -// } - -// #[test] -// fn expect_dnskeys_at_the_apex() { -// let mut records = -// SortedRecords::::default(); - -// records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); -// records.insert(mk_a_rr("some_a.a.")).unwrap(); - -// let nsecs = generate_nsecs(records.owner_rrs(), true).unwrap(); - -// assert_eq!( -// nsecs, -// [ -// mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"), -// mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), -// ] -// ); -// } - -// #[test] -// fn rfc_4034_and_9077_compliant() { -// // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A -// let zonefile = include_bytes!( -// "../../../test-data/zonefiles/rfc4035-appendix-A.zone" -// ); - -// let records = bytes_to_records(&zonefile[..]); -// let nsecs = generate_nsecs(records.owner_rrs(), false).unwrap(); - -// assert_eq!(nsecs.len(), 10); - -// assert_eq!( -// nsecs, -// [ -// mk_nsec_rr("example.", "a.example", "NS SOA MX RRSIG NSEC"), -// mk_nsec_rr("a.example.", "ai.example", "NS DS RRSIG NSEC"), -// mk_nsec_rr( -// "ai.example.", -// "b.example", -// "A HINFO AAAA RRSIG NSEC" -// ), -// mk_nsec_rr("b.example.", "ns1.example", "NS RRSIG NSEC"), -// mk_nsec_rr("ns1.example.", "ns2.example", "A RRSIG NSEC"), -// // The next record also validates that we comply with -// // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 -// // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when -// // it says: -// // "If a wildcard owner name appears in a zone, the wildcard -// // label ("*") is treated as a literal symbol and is treated -// // the same as any other owner name for the purposes of -// // generating NSEC RRs. Wildcard owner names appear in the -// // Next Domain Name field without any wildcard expansion. -// // [RFC4035] describes the impact of wildcards on -// // authenticated denial of existence." -// mk_nsec_rr("ns2.example.", "*.w.example", "A RRSIG NSEC"), -// mk_nsec_rr("*.w.example.", "x.w.example", "MX RRSIG NSEC"), -// mk_nsec_rr("x.w.example.", "x.y.w.example", "MX RRSIG NSEC"), -// mk_nsec_rr("x.y.w.example.", "xx.example", "MX RRSIG NSEC"), -// mk_nsec_rr( -// "xx.example.", -// "example", -// "A HINFO AAAA RRSIG NSEC" -// ) -// ], -// ); - -// // TTLs are not compared by the eq check above so check them -// // explicitly now. -// // -// // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that -// // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of -// // the MINIMUM field of the SOA record and the TTL of the SOA itself". -// // -// // So in our case that is min(1800, 3600) = 1800. -// for nsec in &nsecs { -// assert_eq!(nsec.ttl(), Ttl::from_secs(1800)); -// } - -// // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 -// // 2.3. Including NSEC RRs in a Zone -// // ... -// // "The type bitmap of every NSEC resource record in a signed zone -// // MUST indicate the presence of both the NSEC record itself and its -// // corresponding RRSIG record." -// for nsec in &nsecs { -// assert!(nsec.data().types().contains(Rtype::NSEC)); -// assert!(nsec.data().types().contains(Rtype::RRSIG)); -// } - -// // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 -// // 4.1.2. The Type Bit Maps Field -// // "Bits representing pseudo-types MUST be clear, as they do not -// // appear in zone data." -// // -// // There is nothing to test for this as it is excluded at the Rust -// // type system level by the generate_nsecs() function taking -// // ZoneRecordData (which excludes pseudo record types) as input rather -// // than AllRecordData (which includes pseudo record types). - -// // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 -// // 4.1.2. The Type Bit Maps Field -// // ... -// // "A zone MUST NOT include an NSEC RR for any domain name that only -// // holds glue records." -// // -// // The "rfc4035-appendix-A.zone" file that we load contains glue A -// // records for ns1.example, ns1.a.example, ns1.b.example, ns2.example -// // and ns2.a.example all with no other record types at that name. We -// // can verify that an NSEC RR was NOT created for those that are not -// // within the example zone as we are not authoritative for thos. -// assert!(contains_owner(&nsecs, "ns1.example.")); -// assert!(!contains_owner(&nsecs, "ns1.a.example.")); -// assert!(!contains_owner(&nsecs, "ns1.b.example.")); -// assert!(contains_owner(&nsecs, "ns2.example.")); -// assert!(!contains_owner(&nsecs, "ns2.a.example.")); - -// // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 -// // 2.3. Including NSEC RRs in a Zone -// // ... -// // "The bitmap for the NSEC RR at a delegation point requires special -// // attention. Bits corresponding to the delegation NS RRset and any -// // RRsets for which the parent zone has authoritative data MUST be -// // set; bits corresponding to any non-NS RRset for which the parent -// // is not authoritative MUST be clear." -// // -// // The "rfc4035-appendix-A.zone" file that we load has been modified -// // compared to the original to include a glue A record at b.example. -// // We can verify that an NSEC RR was NOT created for that name. -// let name = mk_name("b.example."); -// let nsec = nsecs.iter().find(|rr| rr.owner() == &name).unwrap(); -// assert!(nsec.data().types().contains(Rtype::NSEC)); -// assert!(nsec.data().types().contains(Rtype::RRSIG)); -// assert!(!nsec.data().types().contains(Rtype::A)); -// } -// } - -// // TODO: Add tests for nsec3s() that validate the following from RFC 5155: -// // -// // 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. -// // However, if there is not a corresponding NSEC3 RR, there MUST be -// // 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." +#[cfg(test)] +mod tests { + use pretty_assertions::assert_eq; + + use crate::sign::records::SortedRecords; + use crate::sign::test_util::*; + use crate::zonetree::types::StoredRecordData; + use crate::zonetree::StoredName; + + use super::*; + + #[test] + fn soa_is_required() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let mut records = + SortedRecords::::default(); + records.insert(mk_a_rr("some_a.a.")).unwrap(); + let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + assert!(matches!( + res, + Err(SigningError::SoaRecordCouldNotBeDetermined) + )); + } + + #[test] + fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let mut records = + SortedRecords::::default(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); + let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + assert!(matches!( + res, + Err(SigningError::SoaRecordCouldNotBeDetermined) + )); + } + + #[test] + fn records_outside_zone_are_ignored() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let mut records = + SortedRecords::::default(); + + records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); + records.insert(mk_a_rr("some_a.b.")).unwrap(); + records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + records.insert(mk_a_rr("some_a.a.")).unwrap(); + + // First generate NSECs for the total record collection. As the + // collection is sorted in canonical order the a zone preceeds the b + // zone and NSECs should only be generated for the first zone in the + // collection. + let a_and_b_records = records.owner_rrs(); + + let generated_records = + generate_nsec3s(a_and_b_records, &mut cfg).unwrap(); + + let mut expected_records = SortedRecords::default(); + expected_records.extend([ + mk_nsec3_rr( + "a.", + "a.", + "some_a.a.", + "SOA RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr("a.", "some_a.a.", "a.", "A RRSIG", &cfg), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + + // Now skip the a zone in the collection and generate NSECs for the + // remaining records which should only generate NSECs for the b zone. + let mut b_records_only = records.owner_rrs(); + b_records_only.skip_before(&mk_name("b.")); + + let generated_records = + generate_nsec3s(b_records_only, &mut cfg).unwrap(); + + let mut expected_records = SortedRecords::default(); + expected_records.extend([ + mk_nsec3_rr( + "b.", + "b.", + "some_a.b.", + "SOA RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr("b.", "some_a.b.", "b.", "A RRSIG", &cfg), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } + + // #[test] + // fn occluded_records_are_ignored() { + // let mut cfg = GenerateNsec3Config::default() + // .without_assuming_dnskeys_will_be_added(); + // let mut records = SortedRecords::default(); + + // records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + // records + // .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) + // .unwrap(); + // records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + + // let generated_records = + // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // // Implicit negative test. + // assert_eq!( + // generated_records.nsec3s, + // [ + // mk_nsec3_rr( + // "a.", + // "a.", + // "some_ns.a.", + // "SOA RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "a.", + // "some_ns.a.", + // "a.", + // "NS RRSIG NSEC", + // &mut cfg + // ), + // ] + // ); + + // // Explicit negative test. + // assert!(!contains_owner( + // &generated_records.nsec3s, + // "some_a.some_ns.a.example." + // )); + // } + + // #[test] + // fn expect_dnskeys_at_the_apex() { + // let mut cfg = GenerateNsec3Config::default(); + + // let mut records = + // SortedRecords::::default(); + + // records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); + // records.insert(mk_a_rr("some_a.a.")).unwrap(); + + // let generated_records = + // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // assert_eq!( + // generated_records.nsec3s, + // [ + // mk_nsec3_rr( + // "a.", + // "a.", + // "some_a.a.", + // "SOA DNSKEY RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "a.", + // "some_a.a.", + // "a.", + // "A RRSIG NSEC", + // &mut cfg + // ), + // ] + // ); + // } + + // #[test] + // fn rfc_4034_and_9077_compliant() { + // let mut cfg = GenerateNsec3Config::default() + // .without_assuming_dnskeys_will_be_added(); + + // // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A + // let zonefile = include_bytes!( + // "../../../test-data/zonefiles/rfc4035-appendix-A.zone" + // ); + + // let records = bytes_to_records(&zonefile[..]); + // let generated_records = + // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // assert_eq!(generated_records.nsec3s.len(), 10); + + // assert_eq!( + // generated_records.nsec3s, + // [ + // mk_nsec3_rr( + // "example.", + // "example.", + // "a.example", + // "NS SOA MX RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "a.example.", + // "ai.example", + // "NS DS RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "ai.example.", + // "b.example", + // "A HINFO AAAA RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "b.example.", + // "ns1.example", + // "NS RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "ns1.example.", + // "ns2.example", + // "A RRSIG NSEC", + // &mut cfg + // ), + // // The next record also validates that we comply with + // // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 + // // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when + // // it says: + // // "If a wildcard owner name appears in a zone, the wildcard + // // label ("*") is treated as a literal symbol and is treated + // // the same as any other owner name for the purposes of + // // generating NSEC RRs. Wildcard owner names appear in the + // // Next Domain Name field without any wildcard expansion. + // // [RFC4035] describes the impact of wildcards on + // // authenticated denial of existence." + // mk_nsec3_rr( + // "example.", + // "ns2.example.", + // "*.w.example", + // "A RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "*.w.example.", + // "x.w.example", + // "MX RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "x.w.example.", + // "x.y.w.example", + // "MX RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "x.y.w.example.", + // "xx.example", + // "MX RRSIG NSEC", + // &mut cfg + // ), + // mk_nsec3_rr( + // "example.", + // "xx.example.", + // "example", + // "A HINFO AAAA RRSIG NSEC", + // &mut cfg + // ) + // ], + // ); + + // // TTLs are not compared by the eq check above so check them + // // explicitly now. + // // + // // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // // the MINIMUM field of the SOA record and the TTL of the SOA itself". + // // + // // So in our case that is min(1800, 3600) = 1800. + // for nsec3 in &generated_records.nsec3s { + // assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); + // } + + // // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 + // // 2.3. Including NSEC RRs in a Zone + // // ... + // // "The type bitmap of every NSEC resource record in a signed zone + // // MUST indicate the presence of both the NSEC record itself and its + // // corresponding RRSIG record." + // for nsec3 in &generated_records.nsec3s { + // assert!(nsec3.data().types().contains(Rtype::NSEC)); + // assert!(nsec3.data().types().contains(Rtype::RRSIG)); + // } + + // // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 + // // 4.1.2. The Type Bit Maps Field + // // "Bits representing pseudo-types MUST be clear, as they do not + // // appear in zone data." + // // + // // There is nothing to test for this as it is excluded at the Rust + // // type system level by the generate_nsecs() function taking + // // ZoneRecordData (which excludes pseudo record types) as input rather + // // than AllRecordData (which includes pseudo record types). + + // // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 + // // 4.1.2. The Type Bit Maps Field + // // ... + // // "A zone MUST NOT include an NSEC RR for any domain name that only + // // holds glue records." + // // + // // The "rfc4035-appendix-A.zone" file that we load contains glue A + // // records for ns1.example, ns1.a.example, ns1.b.example, ns2.example + // // and ns2.a.example all with no other record types at that name. We + // // can verify that an NSEC RR was NOT created for those that are not + // // within the example zone as we are not authoritative for thos. + // assert!(contains_owner(&generated_records.nsec3s, "ns1.example.")); + // assert!(!contains_owner(&generated_records.nsec3s, "ns1.a.example.")); + // assert!(!contains_owner(&generated_records.nsec3s, "ns1.b.example.")); + // assert!(contains_owner(&generated_records.nsec3s, "ns2.example.")); + // assert!(!contains_owner(&generated_records.nsec3s, "ns2.a.example.")); + + // // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 + // // 2.3. Including NSEC RRs in a Zone + // // ... + // // "The bitmap for the NSEC RR at a delegation point requires special + // // attention. Bits corresponding to the delegation NS RRset and any + // // RRsets for which the parent zone has authoritative data MUST be + // // set; bits corresponding to any non-NS RRset for which the parent + // // is not authoritative MUST be clear." + // // + // // The "rfc4035-appendix-A.zone" file that we load has been modified + // // compared to the original to include a glue A record at b.example. + // // We can verify that an NSEC RR was NOT created for that name. + // let name = mk_name("b.example."); + // let nsec3 = generated_records + // .nsec3s + // .iter() + // .find(|rr| rr.owner() == &name) + // .unwrap(); + // assert!(nsec3.data().types().contains(Rtype::NSEC)); + // assert!(nsec3.data().types().contains(Rtype::RRSIG)); + // assert!(!nsec3.data().types().contains(Rtype::A)); + // } +} diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index cda1d0e41..e48a92339 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -1502,7 +1502,6 @@ mod tests { assert_eq!(generated_records.rrsigs.len(), 8); // Filter out the records one by one until there should be none left. - let it = generated_records .dnskeys .iter() diff --git a/src/sign/test_util/mod.rs b/src/sign/test_util/mod.rs index eb14767bc..38e555699 100644 --- a/src/sign/test_util/mod.rs +++ b/src/sign/test_util/mod.rs @@ -1,18 +1,26 @@ use core::str::FromStr; +use std::fmt::Debug; use std::io::Read; +use std::string::ToString; +use std::vec::Vec; use bytes::Bytes; use crate::base::iana::{Class, SecAlg}; use crate::base::name::FlattenInto; -use crate::base::{Record, Rtype, Serial, Ttl}; +use crate::base::{Name, Record, Rtype, Serial, ToName, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; -use crate::rdata::{Dnskey, Ns, Nsec, Rrsig, Soa, A}; +use crate::rdata::nsec3::OwnerHash; +use crate::rdata::{Dnskey, Ns, Nsec, Nsec3, Rrsig, Soa, A}; +use crate::sign::denial::nsec3::mk_hashed_nsec3_owner_name; +use crate::utils::base32; +use crate::validate::nsec3_hash; use crate::zonefile::inplace::{Entry, Zonefile}; use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; +use super::denial::nsec3::{GenerateNsec3Config, Nsec3HashProvider}; use super::records::SortedRecords; pub(crate) const TEST_TTL: Ttl = Ttl::from_secs(3600); @@ -93,6 +101,61 @@ where mk_record(owner, Nsec::new(next_name, types).into()) } +pub(crate) fn mk_nsec3_rr( + apex_owner: &str, + owner: &str, + next_owner: &str, + types: &str, + cfg: &GenerateNsec3Config, +) -> Record +where + HP: Nsec3HashProvider, + N: FromStr + ToName + From>, + ::Err: Debug, + R: From>, +{ + let hashed_owner_name = mk_hashed_nsec3_owner_name( + &N::from_str(owner).unwrap(), + cfg.params.hash_algorithm(), + cfg.params.iterations(), + cfg.params.salt(), + &N::from_str(apex_owner).unwrap(), + ) + .unwrap() + .to_name::() + .to_string(); + + let next_owner_hash_octets: Vec = nsec3_hash( + N::from_str(next_owner).unwrap(), + cfg.params.hash_algorithm(), + cfg.params.iterations(), + cfg.params.salt(), + ) + .unwrap() + .into_octets(); + let next_owner_hash = base32::encode_string_hex(&next_owner_hash_octets) + .to_ascii_lowercase(); + + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + + mk_record( + &hashed_owner_name, + Nsec3::new( + cfg.params.hash_algorithm(), + cfg.params.flags(), + cfg.params.iterations(), + cfg.params.salt().clone(), + OwnerHash::from_str(&next_owner_hash).unwrap(), + types, + ) + .into(), + ) +} + #[allow(clippy::too_many_arguments)] pub(crate) fn mk_rrsig_rr( owner: &str, @@ -150,10 +213,10 @@ where } #[allow(clippy::type_complexity)] -pub(crate) fn contains_owner( - nsecs: &[Record>], +pub(crate) fn contains_owner( + recs: &[Record], name: &str, ) -> bool { let name = mk_name(name); - nsecs.iter().any(|rr| rr.owner() == &name) + recs.iter().any(|rr| rr.owner() == &name) } From 1660cbadb9e80892adbc2b5c6422746fcf9f1ce6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 27 Jan 2025 15:49:48 +0100 Subject: [PATCH 400/569] Fix missing feature dependency. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 921f01e84..f88db4464 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,7 @@ 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", "dep:secrecy", "dep:smallvec", "time/formatting", "tracing", "unstable-validate"] +unstable-sign = ["std", "dep:secrecy", "dep:smallvec", "dep:serde", "time/formatting", "tracing", "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"] From 45ade90297a47c01ab802eea6c58cfa130c50bc6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:57:23 +0100 Subject: [PATCH 401/569] - Remove defaults from trait generic type parameters (i.e. TraitName) to prevent intermediate types that impl the trait from not themselves allowing the default type parameter to be overridden. - Move common bounds up from implementers of the Service trait to the Service trait itself. This both reduces boilerplate and reduces the chances of a type having stricter bounds on the struct and fns (such as fn new()) than its Service trait impl, making it hard to find the cause of a bounds mismatch. - Align bounds on all middleware struct blocks to match the bounds on the Service trait impls to catch mismatched bounds earlier. - Remove unnecessary ?Sized bound on impl Service for U where U: Deref. --- examples/query-routing.rs | 6 +- examples/serve-zone.rs | 27 +++++--- examples/server-transports.rs | 65 +++++++++--------- src/net/server/adapter.rs | 43 +++++++++--- src/net/server/connection.rs | 34 +++++----- src/net/server/dgram.rs | 43 ++---------- src/net/server/message.rs | 10 +-- src/net/server/middleware/cookies.rs | 28 +++++--- src/net/server/middleware/edns.rs | 29 +++++--- src/net/server/middleware/mandatory.rs | 30 +++++--- src/net/server/middleware/notify.rs | 43 +++++++----- src/net/server/middleware/tsig.rs | 68 ++++++++++++------- .../server/middleware/xfr/data_provider.rs | 2 +- src/net/server/middleware/xfr/service.rs | 52 +++++++++----- src/net/server/middleware/xfr/tests.rs | 41 ++++++----- src/net/server/qname_router.rs | 30 +++++--- src/net/server/service.rs | 20 +++--- src/net/server/single_service.rs | 4 +- src/net/server/stream.rs | 31 ++------- src/net/server/tests/integration.rs | 8 +-- src/net/server/tests/unit.rs | 8 ++- src/net/server/util.rs | 3 +- 22 files changed, 353 insertions(+), 272 deletions(-) diff --git a/examples/query-routing.rs b/examples/query-routing.rs index a0b829133..fc1b2b2da 100644 --- a/examples/query-routing.rs +++ b/examples/query-routing.rs @@ -39,7 +39,7 @@ async fn main() { .ok(); // Start building the query router plus upstreams. - let mut qr: QnameRouter, Vec, ReplyMessage> = + let mut qr: QnameRouter, Vec, (), ReplyMessage> = QnameRouter::new(); // Queries to the root go to 2606:4700:4700::1111 and 1.1.1.1. @@ -57,8 +57,8 @@ async fn main() { let conn_service = ClientTransportToSingleService::new(redun); qr.add(Name::>::from_str("nl").unwrap(), conn_service); - let srv = SingleServiceToService::new(qr); - let srv = MandatoryMiddlewareSvc::, _, _>::new(srv); + let srv = SingleServiceToService::<_, _, _, _>::new(qr); + let srv = MandatoryMiddlewareSvc::new(srv); let my_svc = Arc::new(srv); let udpsocket = UdpSocket::bind("[::1]:8053").await.unwrap(); diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index c82b30ceb..109676e24 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -118,16 +118,16 @@ async fn main() { let svc = service_fn(my_service, zones.clone()); #[cfg(feature = "siphasher")] - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); let svc = XfrMiddlewareSvc::, _, _, _>::new( svc, zones_and_diffs.clone(), 1, ); let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); + let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); + let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::, _, _>::new(svc); let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); - let svc = TsigMiddlewareSvc::new(svc, key_store); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); @@ -135,15 +135,18 @@ async fn main() { let mut udp_metrics = vec![]; let num_cores = std::thread::available_parallelism().unwrap().get(); for _i in 0..num_cores { - let udp_srv = - DgramServer::new(sock.clone(), VecBufSource, svc.clone()); + let udp_srv = DgramServer::<_, _, _>::new( + sock.clone(), + VecBufSource, + svc.clone(), + ); let metrics = udp_srv.metrics(); udp_metrics.push(metrics); tokio::spawn(async move { udp_srv.run().await }); } let sock = TcpListener::bind(addr).await.unwrap(); - let tcp_srv = StreamServer::new(sock, VecBufSource, svc); + let tcp_srv = StreamServer::<_, _, _>::new(sock, VecBufSource, svc); let tcp_metrics = tcp_srv.metrics(); tokio::spawn(async move { tcp_srv.run().await }); @@ -240,8 +243,8 @@ async fn main() { } #[allow(clippy::type_complexity)] -fn my_service( - request: Request>, +fn my_service( + request: Request, RequestMeta>, zones: Arc, ) -> ServiceResult> { let question = request.message().sole_question().unwrap(); @@ -317,12 +320,12 @@ impl ZoneTreeWithDiffs { } } -impl XfrDataProvider for ZoneTreeWithDiffs { +impl XfrDataProvider> for ZoneTreeWithDiffs { type Diff = InMemoryZoneDiff; fn request( &self, - req: &Request, + req: &Request>, diff_from: Option, ) -> Pin< Box< @@ -338,6 +341,10 @@ impl XfrDataProvider for ZoneTreeWithDiffs { where Octs: Octets + Send + Sync, { + if req.metadata().is_none() { + eprintln!("Rejecting"); + return Box::pin(ready(Err(XfrDataProviderError::Refused))); + } let res = req .message() .sole_question() diff --git a/examples/server-transports.rs b/examples/server-transports.rs index 16bcff007..159d6a691 100644 --- a/examples/server-transports.rs +++ b/examples/server-transports.rs @@ -7,6 +7,7 @@ use core::time::Duration; use std::fs::File; use std::io; use std::io::BufReader; +use std::marker::Unpin; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; @@ -50,7 +51,7 @@ use domain::rdata::{Soa, A}; // Helper fn to create a dummy response to send back to the client fn mk_answer( - msg: &Request>, + msg: &Request, ()>, builder: MessageBuilder>, ) -> Result>, PushError> where @@ -69,7 +70,7 @@ where } fn mk_soa_answer( - msg: &Request>, + msg: &Request, ()>, builder: MessageBuilder>, ) -> Result>, PushError> where @@ -100,6 +101,7 @@ where //--- MySingleResultService +#[derive(Clone)] struct MySingleResultService; /// This example shows how to implement the [`Service`] trait directly. @@ -116,12 +118,12 @@ struct MySingleResultService; /// /// See [`query`] and [`name_to_ip`] for ways of implementing the [`Service`] /// trait for a function instead of a struct. -impl Service> for MySingleResultService { +impl Service, ()> for MySingleResultService { type Target = Vec; type Stream = Once>>; type Future = Ready; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { let builder = mk_builder_for_target(); let additional = mk_answer(&request, builder).unwrap(); let item = Ok(CallResult::new(additional)); @@ -131,6 +133,7 @@ impl Service> for MySingleResultService { //--- MyAsyncStreamingService +#[derive(Clone)] struct MyAsyncStreamingService; /// This example also shows how to implement the [`Service`] trait directly. @@ -147,13 +150,13 @@ struct MyAsyncStreamingService; /// and/or Stream implementations that actually wait and/or stream, e.g. /// making the Stream type be UnboundedReceiver instead of Pin>. -impl Service> for MyAsyncStreamingService { +impl Service, ()> for MyAsyncStreamingService { type Target = Vec; type Stream = Pin> + Send>>; type Future = Pin + Send>>; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { Box::pin(async move { if !matches!( request @@ -209,7 +212,10 @@ impl Service> for MyAsyncStreamingService { /// The function signature is slightly more complex than when using /// [`service_fn`] (see the [`query`] example below). #[allow(clippy::type_complexity)] -fn name_to_ip(request: Request>, _: ()) -> ServiceResult> { +fn name_to_ip( + request: Request, ()>, + _: (), +) -> ServiceResult> { let mut out_answer = None; if let Ok(question) = request.message().sole_question() { let qname = question.qname(); @@ -257,7 +263,7 @@ fn name_to_ip(request: Request>, _: ()) -> ServiceResult> { /// [`service_fn`] and supports passing in meta data without any extra /// boilerplate. fn query( - request: Request>, + request: Request, ()>, count: Arc, ) -> ServiceResult> { let cnt = count @@ -455,6 +461,7 @@ impl std::fmt::Display for Stats { } } +#[derive(Clone)] pub struct StatsMiddlewareSvc { svc: Svc, stats: Arc>, @@ -467,7 +474,7 @@ impl StatsMiddlewareSvc { Self { svc, stats } } - fn preprocess(&self, request: &Request) + fn preprocess(&self, request: &Request) where RequestOctets: Octets + Send + Sync + Unpin, { @@ -488,12 +495,12 @@ impl StatsMiddlewareSvc { } fn postprocess( - request: &Request, + request: &Request, response: &AdditionalBuilder>, stats: &RwLock, ) where RequestOctets: Octets + Send + Sync + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, { let duration = Instant::now().duration_since(request.received_at()); @@ -510,13 +517,13 @@ impl StatsMiddlewareSvc { } fn map_stream_item( - request: Request, + request: Request, stream_item: ServiceResult, stats: &mut Arc>, ) -> ServiceResult where RequestOctets: Octets + Send + Sync + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, { if let Ok(cr) = &stream_item { @@ -528,10 +535,11 @@ impl StatsMiddlewareSvc { } } -impl Service for StatsMiddlewareSvc +impl Service + for StatsMiddlewareSvc where RequestOctets: Octets + Send + Sync + 'static + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, Svc::Future: Unpin, { @@ -551,7 +559,7 @@ where >; type Future = Ready; - fn call(&self, request: Request) -> Self::Future { + fn call(&self, request: Request) -> Self::Future { self.preprocess(&request); let svc_call_fut = self.svc.call(request.clone()); let map = PostprocessingStream::new( @@ -567,24 +575,19 @@ where //------------ build_middleware_chain() -------------------------------------- #[allow(clippy::type_complexity)] -fn build_middleware_chain( +fn build_middleware_chain( svc: Svc, stats: Arc>, -) -> StatsMiddlewareSvc< - MandatoryMiddlewareSvc< - Vec, - EdnsMiddlewareSvc< - Vec, - CookiesMiddlewareSvc, Svc, ()>, - (), - >, - (), - >, -> { +) -> impl Service +where + Octs: Octets + Send + Sync + Clone + Unpin + 'static, + Svc: Service, + >::Future: Unpin, +{ #[cfg(feature = "siphasher")] - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); - let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); + let svc = CookiesMiddlewareSvc::::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::new(svc); + let svc = MandatoryMiddlewareSvc::new(svc); StatsMiddlewareSvc::new(svc, stats.clone()) } diff --git a/src/net/server/adapter.rs b/src/net/server/adapter.rs index a28c1d80d..5a9c785d6 100644 --- a/src/net/server/adapter.rs +++ b/src/net/server/adapter.rs @@ -31,15 +31,30 @@ use std::string::ToString; use std::vec::Vec; /// Provide a [Service] trait for an object that implements [SingleService]. -pub struct SingleServiceToService { +pub struct SingleServiceToService +where + RequestMeta: Clone + Default, + RequestOcts: Octets + Send + Sync, + SVC: SingleService, + CR: ComposeReply + 'static, + Self: Send + Sync + 'static, +{ /// Service that is wrapped by this object. service: SVC, /// Phantom field for RequestOcts and CR. - _phantom: PhantomData<(RequestOcts, CR)>, + _phantom: PhantomData<(RequestOcts, CR, RequestMeta)>, } -impl SingleServiceToService { +impl + SingleServiceToService +where + RequestMeta: Clone + Default, + RequestOcts: Octets + Send + Sync, + SVC: SingleService, + CR: ComposeReply + 'static, + Self: Send + Sync + 'static, +{ /// Create a new [SingleServiceToService] object. pub fn new(service: SVC) -> Self { Self { @@ -49,18 +64,23 @@ impl SingleServiceToService { } } -impl Service - for SingleServiceToService +impl Service + for SingleServiceToService where + RequestMeta: Clone + Default, RequestOcts: Octets + Send + Sync, - SVC: SingleService, + SVC: SingleService, CR: ComposeReply + 'static, + Self: Send + Sync + 'static, { type Target = Vec; type Stream = Once>>; type Future = Pin + Send>>; - fn call(&self, request: Request) -> Self::Future { + fn call( + &self, + request: Request, + ) -> Self::Future { let fut = self.service.call(request); let fut = async move { let reply = match fut.await { @@ -114,7 +134,8 @@ where } } -impl SingleService +impl + SingleService for ClientTransportToSingleService where RequestOcts: AsRef<[u8]> + Clone + Debug + Octets + Send + Sync, @@ -123,7 +144,7 @@ where { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]>, @@ -194,7 +215,7 @@ where } } -impl SingleService +impl SingleService for BoxClientTransportToSingleService where RequestOcts: AsRef<[u8]> + Clone + Debug + Octets + Send + Sync, @@ -202,7 +223,7 @@ where { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]>, diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index a09609a05..e200e7e80 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -21,7 +21,6 @@ use tokio::time::{sleep_until, timeout}; use tracing::{debug, error, trace, warn}; use crate::base::message_builder::AdditionalBuilder; -use crate::base::wire::Composer; use crate::base::{Message, StreamTarget}; use crate::net::server::buf::BufSource; use crate::net::server::message::Request; @@ -219,11 +218,12 @@ impl Clone for Config { //------------ Connection ---------------------------------------------------- /// A handler for a single stream connection between client and server. -pub struct Connection +pub struct Connection where + RequestMeta: Default + Clone + Send + 'static, Buf: BufSource, Buf::Output: Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { /// Flag used by the Drop impl to track if the metric count has to be /// decreased or not. @@ -270,12 +270,13 @@ where /// Creation /// -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite, Buf: BufSource, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { /// Creates a new handler for an accepted stream connection. #[must_use] @@ -340,14 +341,13 @@ where /// Control /// -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite + Send + Sync + 'static, Buf: BufSource + Send + Sync + Clone + 'static, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone + Send + Sync + 'static, - Svc::Target: Composer + Send, - Svc::Stream: Send, + Svc: Service + Clone, { /// Start reading requests and writing responses to the stream. /// @@ -377,15 +377,13 @@ where //--- Internal details -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite + Send + Sync + 'static, Buf: BufSource + Send + Sync + Clone + 'static, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone + Send + Sync + 'static, - Svc::Target: Composer + Send, - Svc::Future: Send, - Svc::Stream: Send, + Svc: Service + Clone, { /// Connection handler main loop. async fn run_until_error( @@ -685,7 +683,7 @@ where received_at, msg, ctx, - (), + Default::default(), ); let svc = self.service.clone(); @@ -799,11 +797,13 @@ where //--- Drop -impl Drop for Connection +impl Drop + for Connection where + RequestMeta: Default + Clone + Send + 'static, Buf: BufSource, Buf::Output: Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { fn drop(&mut self) { if self.active { diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 4da2e261c..f9d896700 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -33,7 +33,6 @@ use tokio::time::Instant; use tokio::time::MissedTickBehavior; use tracing::{error, trace, warn}; -use crate::base::wire::Composer; use crate::base::Message; use crate::net::server::buf::BufSource; use crate::net::server::error::Error; @@ -251,14 +250,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin + 'static, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// The configuration of the server. config: Arc>, @@ -295,10 +287,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Constructs a new [`DgramServer`] with default configuration. /// @@ -352,10 +341,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Get a reference to the network source being used to receive messages. #[must_use] @@ -377,14 +363,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + 'static + Unpin, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Start the server. /// @@ -465,10 +444,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Receive incoming messages until shutdown or fatal error. async fn run_until_error(&self) -> Result<(), String> { @@ -678,14 +654,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin + 'static, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { fn drop(&mut self) { // Shutdown the DgramServer. Don't handle the failure case here as diff --git a/src/net/server/message.rs b/src/net/server/message.rs index 197ab0718..6e7687daf 100644 --- a/src/net/server/message.rs +++ b/src/net/server/message.rs @@ -166,7 +166,7 @@ impl From for TransportSpecificContext { /// message itself but also on the circumstances surrounding its creation and /// delivery. #[derive(Debug)] -pub struct Request +pub struct Request where Octs: AsRef<[u8]> + Send + Sync, { @@ -191,7 +191,7 @@ where /// still possible to generate responses that ignore this value. num_reserved_bytes: u16, - /// user defined metadata to associate with the request. + /// User defined metadata to associate with the request. /// /// For example this could be used to pass data from one [middleware] /// [`Service`] impl to another. @@ -298,12 +298,12 @@ where //--- TryFrom> for RequestMessage> -impl TryFrom> - for RequestMessage +impl + TryFrom> for RequestMessage { type Error = request::Error; - fn try_from(req: Request) -> Result { + fn try_from(req: Request) -> Result { // Copy the ECS option from the message. This is just an example, // there should be a separate plugin that deals with ECS. diff --git a/src/net/server/middleware/cookies.rs b/src/net/server/middleware/cookies.rs index fd6b089be..4344cdc64 100644 --- a/src/net/server/middleware/cookies.rs +++ b/src/net/server/middleware/cookies.rs @@ -14,7 +14,7 @@ use crate::base::iana::{OptRcode, Rcode}; use crate::base::message_builder::AdditionalBuilder; use crate::base::net::IpAddr; use crate::base::opt; -use crate::base::wire::{Composer, ParseError}; +use crate::base::wire::ParseError; use crate::base::{Serial, StreamTarget}; use crate::net::server::message::Request; use crate::net::server::middleware::stream::MiddlewareStream; @@ -46,7 +46,13 @@ const ONE_HOUR_AS_SECS: u32 = 60 * 60; /// [7873]: https://datatracker.ietf.org/doc/html/rfc7873 /// [9018]: https://datatracker.ietf.org/doc/html/rfc7873 #[derive(Clone, Debug)] -pub struct CookiesMiddlewareSvc { +pub struct CookiesMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -70,6 +76,11 @@ pub struct CookiesMiddlewareSvc { impl CookiesMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { /// Creates an instance of this middleware service. #[must_use] @@ -108,10 +119,10 @@ impl impl CookiesMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, - RequestMeta: Clone + Default, NextSvc: Service, - NextSvc::Target: Composer + Default, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { /// Get the DNS COOKIE, if any, for the given message. /// @@ -457,11 +468,10 @@ where impl Service for CookiesMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, - RequestMeta: Clone + Default, NextSvc: Service, - NextSvc::Target: Composer + Default, NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -534,7 +544,7 @@ mod tests { ); fn my_service( - _req: Request>, + _req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/edns.rs b/src/net/server/middleware/edns.rs index 880a12b8c..75df8dc04 100644 --- a/src/net/server/middleware/edns.rs +++ b/src/net/server/middleware/edns.rs @@ -12,7 +12,6 @@ use crate::base::iana::OptRcode; use crate::base::message_builder::AdditionalBuilder; use crate::base::opt::keepalive::IdleTimeout; use crate::base::opt::{ComposeOptData, Opt, OptRecord, TcpKeepalive}; -use crate::base::wire::Composer; use crate::base::{Message, Name, StreamTarget}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::middleware::stream::MiddlewareStream; @@ -47,7 +46,13 @@ const EDNS_VERSION_ZERO: u8 = 0; /// [7828]: https://datatracker.ietf.org/doc/html/rfc7828 /// [9210]: https://datatracker.ietf.org/doc/html/rfc9210 #[derive(Clone, Debug, Default)] -pub struct EdnsMiddlewareSvc { +pub struct EdnsMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -63,6 +68,11 @@ pub struct EdnsMiddlewareSvc { impl EdnsMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { /// Creates an instance of this middleware service. #[must_use] @@ -83,10 +93,10 @@ impl impl EdnsMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { fn preprocess( &self, @@ -411,11 +421,10 @@ where impl Service for EdnsMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, - RequestMeta: Clone + Default + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -431,7 +440,7 @@ where Once::Item>>, ::Item, >; - type Future = core::future::Ready; + type Future = Ready; fn call( &self, @@ -568,7 +577,7 @@ mod tests { ); fn my_service( - req: Request>, + req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/mandatory.rs b/src/net/server/middleware/mandatory.rs index 933b21afb..5174090b9 100644 --- a/src/net/server/middleware/mandatory.rs +++ b/src/net/server/middleware/mandatory.rs @@ -11,7 +11,7 @@ use tracing::{debug, error, trace, warn}; use crate::base::iana::{Opcode, OptRcode}; use crate::base::message_builder::{AdditionalBuilder, PushError}; -use crate::base::wire::{Composer, ParseError}; +use crate::base::wire::ParseError; use crate::base::{Message, StreamTarget}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::service::{CallResult, Service, ServiceResult}; @@ -41,7 +41,13 @@ pub const MINIMUM_RESPONSE_BYTE_LEN: u16 = 512; /// [2181]: https://datatracker.ietf.org/doc/html/rfc2181 /// [9619]: https://datatracker.ietf.org/doc/html/rfc9619 #[derive(Clone, Debug)] -pub struct MandatoryMiddlewareSvc { +pub struct MandatoryMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -55,6 +61,11 @@ pub struct MandatoryMiddlewareSvc { impl MandatoryMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { /// Creates an instance of this middleware service. /// @@ -84,10 +95,10 @@ impl impl MandatoryMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { /// Truncate the given response message if it is too large. /// @@ -303,11 +314,10 @@ where impl Service for MandatoryMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, NextSvc: Service, NextSvc::Future: Unpin, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default + Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -331,6 +341,7 @@ where ) -> Self::Future { match self.preprocess(request.message()) { ControlFlow::Continue(()) => { + let request = request.with_new_metadata(Default::default()); let svc_call_fut = self.next_svc.call(request.clone()); let map = PostprocessingStream::new( svc_call_fut, @@ -341,6 +352,7 @@ where ready(MiddlewareStream::Map(map)) } ControlFlow::Break(mut response) => { + let request = request.with_new_metadata(Default::default()); Self::postprocess(&request, &mut response, self.strict); ready(MiddlewareStream::Result(once(ready(Ok( CallResult::new(response), @@ -457,7 +469,7 @@ mod tests { ); fn my_service( - req: Request>, + req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/notify.rs b/src/net/server/middleware/notify.rs index 2eef038ef..fa0e465f9 100644 --- a/src/net/server/middleware/notify.rs +++ b/src/net/server/middleware/notify.rs @@ -61,7 +61,6 @@ use crate::base::message::CopyRecordsError; use crate::base::message_builder::AdditionalBuilder; use crate::base::name::Name; use crate::base::net::IpAddr; -use crate::base::wire::Composer; use crate::base::{ Message, ParsedName, Question, Rtype, StreamTarget, ToName, }; @@ -80,7 +79,15 @@ use crate::rdata::AllRecordData; /// /// [RFC 1996]: https://www.rfc-editor.org/info/rfc1996 #[derive(Clone, Debug)] -pub struct NotifyMiddlewareSvc { +pub struct NotifyMiddlewareSvc +where + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -93,6 +100,13 @@ pub struct NotifyMiddlewareSvc { impl NotifyMiddlewareSvc +where + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, { /// Creates an instance of this middleware service. /// @@ -111,11 +125,12 @@ impl impl NotifyMiddlewareSvc where - RequestOctets: Octets + Send + Sync, - RequestMeta: Clone + Default, - NextSvc: Service, - NextSvc::Target: Composer + Default, - N: Clone + Notifiable + Sync + Send, + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, { /// Pre-process received DNS NOTIFY queries. /// @@ -326,18 +341,12 @@ impl Service for NotifyMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static, + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, RequestMeta: Clone + Default + Sync + Send + 'static, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service - + Clone - + 'static - + Send - + Sync - + Unpin, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - N: Notifiable + Clone + Sync + Send + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< diff --git a/src/net/server/middleware/tsig.rs b/src/net/server/middleware/tsig.rs index 195fe3484..4edb9add3 100644 --- a/src/net/server/middleware/tsig.rs +++ b/src/net/server/middleware/tsig.rs @@ -66,20 +66,33 @@ use futures_util::Stream; /// Upstream services can detect whether a request is signed and with which /// key by consuming the `Option` metadata output by this service. #[derive(Clone, Debug)] -pub struct TsigMiddlewareSvc +pub struct TsigMiddlewareSvc where + Infallible: From<>>::Error>, KS: Clone + KeyStore, + KS::Key: Clone, + NextSvc: Service>, + NextSvc::Target: Composer + Default, + RequestOctets: Octets + OctetsFrom> + Send + Sync + Unpin, { next_svc: NextSvc, key_store: KS, - _phantom: PhantomData, + _phantom: PhantomData<(RequestOctets, IgnoredRequestMeta)>, } -impl TsigMiddlewareSvc +impl + TsigMiddlewareSvc where - KS: Clone + KeyStore, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, + Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, + NextSvc: Service>, + NextSvc::Future: Unpin, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { /// Creates an instance of this middleware service. /// @@ -95,18 +108,21 @@ where } } -impl TsigMiddlewareSvc +impl + TsigMiddlewareSvc where - RequestOctets: Octets + OctetsFrom> + Send + Sync + Unpin, - NextSvc: Service>, - NextSvc::Target: Composer + Default, - KS: Clone + KeyStore, - KS::Key: Clone, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, + NextSvc: Service>, + NextSvc::Future: Unpin, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { #[allow(clippy::type_complexity)] fn preprocess( - req: &Request, + req: &Request, key_store: &KS, ) -> Result< ControlFlow< @@ -188,7 +204,7 @@ where /// Sign the given response, or if necessary construct and return an /// alternate response. fn postprocess( - request: &Request, + request: &Request, response: &mut AdditionalBuilder>, state: &mut PostprocessingState, ) -> Result< @@ -272,7 +288,7 @@ where } fn mk_signed_truncated_response( - request: &Request, + request: &Request, truncation_ctx: TruncationContext, ) -> Result>, ServiceError> { @@ -334,7 +350,7 @@ where } fn map_stream_item( - request: Request, + request: Request, stream_item: ServiceResult, pp_config: &mut PostprocessingState, ) -> ServiceResult { @@ -396,17 +412,18 @@ where /// and (b) because this service does not propagate the metadata it receives /// from downstream but instead outputs [`Option`] metadata to /// upstream services. -impl Service - for TsigMiddlewareSvc +impl + Service + for TsigMiddlewareSvc where - RequestOctets: - Octets + OctetsFrom> + Send + Sync + 'static + Unpin, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, + Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, NextSvc: Service>, NextSvc::Future: Unpin, - NextSvc::Target: Composer + Default, - KS: Clone + KeyStore + Unpin, - KS::Key: Clone + Unpin, - Infallible: From<>>::Error>, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -416,7 +433,7 @@ where RequestOctets, NextSvc::Future, NextSvc::Stream, - (), + IgnoredRequestMeta, PostprocessingState, >, Once>>, @@ -424,7 +441,10 @@ where >; type Future = Ready; - fn call(&self, request: Request) -> Self::Future { + fn call( + &self, + request: Request, + ) -> Self::Future { match Self::preprocess(&request, &self.key_store) { Ok(ControlFlow::Continue(Some((modified_req, signer)))) => { let pp_config = PostprocessingState::new(signer); diff --git a/src/net/server/middleware/xfr/data_provider.rs b/src/net/server/middleware/xfr/data_provider.rs index 65aae2619..a274da5d8 100644 --- a/src/net/server/middleware/xfr/data_provider.rs +++ b/src/net/server/middleware/xfr/data_provider.rs @@ -85,7 +85,7 @@ impl XfrData { //------------ XfrDataProvider ------------------------------------------------ /// A provider of data needed for responding to XFR requests. -pub trait XfrDataProvider { +pub trait XfrDataProvider { type Diff: ZoneDiff + Send + Sync; /// Request data needed to respond to an XFR request. diff --git a/src/net/server/middleware/xfr/service.rs b/src/net/server/middleware/xfr/service.rs index d0862f5e7..8f330eaba 100644 --- a/src/net/server/middleware/xfr/service.rs +++ b/src/net/server/middleware/xfr/service.rs @@ -16,7 +16,6 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, info, trace, warn}; use crate::base::iana::{Opcode, OptRcode}; -use crate::base::wire::Composer; use crate::base::{Message, ParsedName, Question, Rtype, Serial, ToName}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::middleware::stream::MiddlewareStream; @@ -55,7 +54,18 @@ const MAX_TCP_MSG_BYTE_LEN: u16 = u16::MAX; /// /// [module documentation]: crate::net::server::middleware::xfr #[derive(Clone, Debug)] -pub struct XfrMiddlewareSvc { +pub struct XfrMiddlewareSvc +where + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, + for<'a> ::Range<'a>: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -78,7 +88,15 @@ pub struct XfrMiddlewareSvc { impl XfrMiddlewareSvc where - XDP: XfrDataProvider, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, + for<'a> ::Range<'a>: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, { /// Creates a new instance of this middleware. /// @@ -110,14 +128,15 @@ where impl XfrMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service + Clone + Send + Sync + 'static, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - NextSvc::Stream: Send + Sync, - XDP: XfrDataProvider, - XDP::Diff: Debug + 'static, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, { /// Pre-process received DNS XFR queries. /// @@ -684,12 +703,12 @@ impl Service for XfrMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin + 'static, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service + Clone + Send + Sync + 'static, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - NextSvc::Stream: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, XDP: XfrDataProvider + Clone + Sync + Send + 'static, XDP::Diff: Debug + Sync, RequestMeta: Clone + Default + Sync + Send + 'static, @@ -721,7 +740,8 @@ where .await { Ok(ControlFlow::Continue(())) => { - let request = request.with_new_metadata(()); + let request = + request.with_new_metadata(Default::default()); let stream = next_svc.call(request).await; MiddlewareStream::IdentityStream(stream) } diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index d4849a25b..ddad7e7b3 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -57,7 +57,7 @@ async fn axfr_with_example_zone() { "../../../../../test-data/zonefiles/nsd-example.txt" )); - let req = mk_axfr_request(zone.apex_name(), ()); + let req = mk_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -125,7 +125,7 @@ async fn axfr_multi_response() { "../../../../../test-data/zonefiles/big.example.com.txt" )); - let req = mk_axfr_request(zone.apex_name(), ()); + let req = mk_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -203,7 +203,7 @@ async fn axfr_not_allowed_over_udp() { "../../../../../test-data/zonefiles/nsd-example.txt" )); - let req = mk_udp_axfr_request(zone.apex_name(), ()); + let req = mk_udp_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone, &req).await.unwrap(); @@ -245,7 +245,8 @@ JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2 let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), vec![]); // The following IXFR query - let req = mk_udp_ixfr_request(zone.apex_name(), Serial(1), ()); + let req = + mk_udp_ixfr_request(zone.apex_name(), Serial(1), Default::default()); let res = do_preprocess(zone_with_diffs, &req).await.unwrap(); @@ -383,7 +384,8 @@ JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2 let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), diffs); // The following IXFR query - let req = mk_ixfr_request(zone.apex_name(), Serial(1), ()); + let req = + mk_ixfr_request(zone.apex_name(), Serial(1), Default::default()); let res = do_preprocess(zone_with_diffs, &req).await.unwrap(); @@ -480,7 +482,8 @@ async fn ixfr_rfc1995_section7_udp_packet_overflow() { "../../../../../test-data/zonefiles/big.example.com.txt" )); - let req = mk_udp_ixfr_request(zone.apex_name(), Serial(0), ()); + let req = + mk_udp_ixfr_request(zone.apex_name(), Serial(0), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -506,6 +509,7 @@ async fn axfr_with_tsig_key() { // type over which the Request produced by TsigMiddlewareSvc is generic. // When the XfrMiddlewareSvc receives a Request it // passes it to the XfrDataProvider which in turn can inspect it. + #[derive(Clone)] struct KeyReceivingXfrDataProvider { key: Arc, checked: Arc, @@ -685,23 +689,24 @@ fn mk_ixfr_request_for_transport( Request::new(client_addr, received_at, msg, transport_specific, metadata) } -async fn do_preprocess>( +async fn do_preprocess( zone: XDP, - req: &Request, RequestMeta>, + req: &Request, Option>>, ) -> Result< ControlFlow< XfrMiddlewareStream< - ::Future, - ::Stream, - <::Stream as Stream>::Item, + , Option>>>::Future, + , Option>>>::Stream, + <, Option>>>::Stream as Stream>::Item, >, >, OptRcode, > where + XDP: XfrDataProvider>> + Clone + Sync + Send + 'static, XDP::Diff: Debug + 'static, { - XfrMiddlewareSvc::, TestNextSvc, RequestMeta, XDP>::preprocess( + XfrMiddlewareSvc::, TestNextSvc, Option>, XDP>::preprocess( Arc::new(Semaphore::new(1)), Arc::new(Semaphore::new(1)), req, @@ -771,16 +776,20 @@ async fn assert_stream_eq< #[derive(Clone)] struct TestNextSvc; -impl Service, ()> for TestNextSvc { +impl Service, Option>> for TestNextSvc { type Target = Vec; type Stream = Once>>; type Future = Ready; - fn call(&self, _request: Request, ()>) -> Self::Future { + fn call( + &self, + _request: Request, Option>>, + ) -> Self::Future { todo!() } } +#[derive(Clone)] struct ZoneWithDiffs { zone: Zone, diffs: Vec>, @@ -806,11 +815,11 @@ impl ZoneWithDiffs { } } -impl XfrDataProvider for ZoneWithDiffs { +impl XfrDataProvider for ZoneWithDiffs { type Diff = Arc; fn request( &self, - req: &Request, + req: &Request, diff_from: Option, ) -> Pin< Box< diff --git a/src/net/server/qname_router.rs b/src/net/server/qname_router.rs index 749b8f7a5..46c2660e2 100644 --- a/src/net/server/qname_router.rs +++ b/src/net/server/qname_router.rs @@ -22,21 +22,24 @@ use tracing::trace; /// A service that routes requests to other services based on the Qname in the /// request. -pub struct QnameRouter { +pub struct QnameRouter { /// List of names and services for routing requests. - list: Vec>, + list: Vec>, } /// Element in the name space for the Qname router. -struct Element { +struct Element { /// Name to match for this element. name: Name, /// Service to call for this element. - service: Box + Send + Sync>, + service: + Box + Send + Sync>, } -impl QnameRouter { +impl + QnameRouter +{ /// Create a new empty router. pub fn new() -> Self { Self { list: Vec::new() } @@ -50,7 +53,10 @@ impl QnameRouter { EmptyBuilder + OctetsBuilder, TN: ToName, RequestOcts: Send + Sync, - SVC: SingleService + Send + Sync + 'static, + SVC: SingleService + + Send + + Sync + + 'static, { let el = Element { name: name.to_name(), @@ -60,22 +66,26 @@ impl QnameRouter { } } -impl Default for QnameRouter { +impl Default + for QnameRouter +{ fn default() -> Self { Self::new() } } -impl SingleService - for QnameRouter +impl + SingleService + for QnameRouter where Octs: AsRef<[u8]>, + RequestMeta: Clone, RequestOcts: Send + Sync, CR: ComposeReply + Send + Sync + 'static, { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]> + Octets, diff --git a/src/net/server/service.rs b/src/net/server/service.rs index 13e177bd9..455cd05b1 100644 --- a/src/net/server/service.rs +++ b/src/net/server/service.rs @@ -7,11 +7,10 @@ use core::fmt::Display; use core::ops::Deref; use std::time::Duration; -use std::vec::Vec; use crate::base::iana::Rcode; use crate::base::message_builder::{AdditionalBuilder, PushError}; -use crate::base::wire::ParseError; +use crate::base::wire::{Composer, ParseError}; use crate::base::StreamTarget; use super::message::Request; @@ -167,19 +166,20 @@ pub type ServiceResult = Result, ServiceError>; /// [`call`]: Self::call() /// [`service_fn`]: crate::net::server::util::service_fn() pub trait Service< - RequestOctets: AsRef<[u8]> + Send + Sync = Vec, - RequestMeta: Clone + Default = (), -> + RequestOctets: AsRef<[u8]> + Send + Sync, + RequestMeta: Clone + Default, +>: Send + Sync + 'static { /// The underlying byte storage type used to hold generated responses. - type Target; + type Target: Composer + Default + Send + Sync; /// The type of stream that the service produces. type Stream: futures_util::stream::Stream> - + Unpin; + + Unpin + + Send; /// The type of future that will yield the service result stream. - type Future: core::future::Future; + type Future: core::future::Future + Send; /// Generate a response to a fully pre-processed request. fn call( @@ -195,8 +195,8 @@ impl Service for U where RequestOctets: Unpin + Send + Sync + AsRef<[u8]>, - T: ?Sized + Service, - U: Deref + Clone, + T: Service, + U: Deref + Clone + Send + Sync + 'static, RequestMeta: Clone + Default, { type Target = T::Target; diff --git a/src/net/server/single_service.rs b/src/net/server/single_service.rs index 28c6d19fe..73035635c 100644 --- a/src/net/server/single_service.rs +++ b/src/net/server/single_service.rs @@ -22,13 +22,13 @@ use std::pin::Pin; use std::vec::Vec; /// Trait for a service that results in a single response. -pub trait SingleService { +pub trait SingleService { /// Call the service with a request message. /// /// The service returns a boxed future. fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]> + Octets; diff --git a/src/net/server/stream.rs b/src/net/server/stream.rs index c22b39d36..617086c85 100644 --- a/src/net/server/stream.rs +++ b/src/net/server/stream.rs @@ -39,7 +39,6 @@ use crate::utils::config::DefMinMax; use super::buf::VecBufSource; use super::connection::{self, Connection}; use super::ServerCommand; -use crate::base::wire::Composer; use tokio::io::{AsyncRead, AsyncWrite}; // TODO: Should this crate also provide a TLS listener implementation? @@ -270,8 +269,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, // + 'static, + Svc: Service + Clone, { /// The configuration of the server. config: Arc>, @@ -315,8 +313,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Creates a new [`StreamServer`] instance. /// @@ -395,8 +392,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Debug + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Get a reference to the source for this server. #[must_use] @@ -418,8 +414,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Start the server. /// @@ -435,10 +430,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { if let Err(err) = self.run_until_error().await { error!("Server stopped due to error: {err}"); @@ -513,8 +504,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Accept stream connections until shutdown or fatal error. async fn run_until_error(&self) -> Result<(), String> @@ -524,10 +514,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { let mut command_rx = self.command_rx.clone(); @@ -646,10 +632,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Composer + Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { // Work around the compiler wanting to move self to the async block by // preparing only those pieces of information from self for the new @@ -713,8 +695,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { fn drop(&mut self) { // Shutdown the StreamServer. Don't handle the failure case here as diff --git a/src/net/server/tests/integration.rs b/src/net/server/tests/integration.rs index bd02f52bc..178517a70 100644 --- a/src/net/server/tests/integration.rs +++ b/src/net/server/tests/integration.rs @@ -22,7 +22,6 @@ use tracing::{trace, warn}; use crate::base::iana::{Class, Rcode}; use crate::base::name::ToName; use crate::base::net::IpAddr; -use crate::base::wire::Composer; use crate::base::Name; use crate::base::Rtype; use crate::net::client::request::{RequestMessage, RequestMessageMulti}; @@ -233,17 +232,14 @@ fn mk_servers( Arc>, ) where - Svc: Clone + Service + Send + Sync, - ::Future: Send, - ::Target: Composer + Default + Send + Sync, - ::Stream: Send, + Svc: Service, ()> + Clone, { // Prepare middleware to be used by the DNS servers to pre-process // received requests and post-process created responses. let (dgram_config, stream_config) = mk_server_configs(server_config); // Create a dgram server for handling UDP requests. - let dgram_server = DgramServer::<_, _, Svc>::with_config( + let dgram_server = DgramServer::with_config( dgram_server_conn.clone(), VecBufSource, service.clone(), diff --git a/src/net/server/tests/unit.rs b/src/net/server/tests/unit.rs index 834799611..89139439b 100644 --- a/src/net/server/tests/unit.rs +++ b/src/net/server/tests/unit.rs @@ -329,6 +329,7 @@ impl futures_util::stream::Stream for MySingle { /// A mock service that returns MySingle whenever it receives a message. /// Just to show MySingle in action. +#[derive(Clone)] struct MyService; impl MyService { @@ -337,12 +338,15 @@ impl MyService { } } -impl Service> for MyService { +impl Service, ()> for MyService +where + Self: Clone + Send + Sync + 'static, +{ type Target = Vec; type Stream = MySingle; type Future = Ready; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { trace!("Processing request id {}", request.message().header().id()); ready(MySingle::new()) } diff --git a/src/net/server/util.rs b/src/net/server/util.rs index 220b6171f..2b30d5872 100644 --- a/src/net/server/util.rs +++ b/src/net/server/util.rs @@ -134,12 +134,13 @@ where RequestOctets: AsRef<[u8]> + Send + Sync + Unpin, RequestMeta: Default + Clone, Metadata: Clone, - Target: Composer + Default, + Target: Composer + Default + Send + Sync, T: Fn( Request, Metadata, ) -> ServiceResult + Clone, + Self: Clone + Send + Sync + 'static, { type Target = Target; type Stream = Once>>; From 904dc9a77260993d1a8b7a97e22c72958649a921 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:38:04 +0100 Subject: [PATCH 402/569] Fix most but not all doc test failures. --- src/net/server/dgram.rs | 2 +- src/net/server/service.rs | 18 +++++++++--------- src/net/server/stream.rs | 2 +- src/net/server/util.rs | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index f9d896700..6ff795e42 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -212,7 +212,7 @@ type CommandReceiver = watch::Receiver; /// use domain::net::server::stream::StreamServer; /// use domain::net::server::util::service_fn; /// -/// fn my_service(msg: Request>, _meta: ()) -> ServiceResult> +/// fn my_service(msg: Request, ()>, _meta: ()) -> ServiceResult> /// { /// todo!() /// } diff --git a/src/net/server/service.rs b/src/net/server/service.rs index 455cd05b1..f7577b171 100644 --- a/src/net/server/service.rs +++ b/src/net/server/service.rs @@ -58,7 +58,7 @@ pub type ServiceResult = Result, ServiceError>; /// use domain::rdata::A; /// /// fn mk_answer( -/// msg: &Request>, +/// msg: &Request, ()>, /// builder: MessageBuilder>>, /// ) -> AdditionalBuilder>> { /// let mut answer = builder @@ -73,7 +73,7 @@ pub type ServiceResult = Result, ServiceError>; /// answer.additional() /// } /// -/// fn mk_response_stream(msg: &Request>) +/// fn mk_response_stream(msg: &Request, ()>) /// -> Once>>> /// { /// let builder = mk_builder_for_target(); @@ -85,14 +85,14 @@ pub type ServiceResult = Result, ServiceError>; /// //------------ A synchronous service example ------------------------------ /// struct MySyncService; /// -/// impl Service> for MySyncService { +/// impl Service, ()> for MySyncService { /// type Target = Vec; /// type Stream = Once>>; /// type Future = Ready; /// /// fn call( /// &self, -/// msg: Request>, +/// msg: Request, ()>, /// ) -> Self::Future { /// ready(mk_response_stream(&msg)) /// } @@ -101,21 +101,21 @@ pub type ServiceResult = Result, ServiceError>; /// //------------ An anonymous async block service example ------------------- /// struct MyAsyncBlockService; /// -/// impl Service> for MyAsyncBlockService { +/// impl Service, ()> for MyAsyncBlockService { /// type Target = Vec; /// type Stream = Once>>; /// type Future = Pin>>; /// /// fn call( /// &self, -/// msg: Request>, +/// msg: Request, ()>, /// ) -> Self::Future { /// Box::pin(async move { mk_response_stream(&msg) }) /// } /// } /// /// //------------ A named Future service example ----------------------------- -/// struct MyFut(Request>); +/// struct MyFut(Request, ()>); /// /// impl std::future::Future for MyFut { /// type Output = Once>>>; @@ -127,12 +127,12 @@ pub type ServiceResult = Result, ServiceError>; /// /// struct MyNamedFutureService; /// -/// impl Service> for MyNamedFutureService { +/// impl Service, ()> for MyNamedFutureService { /// type Target = Vec; /// type Stream = Once>>; /// type Future = MyFut; /// -/// fn call(&self, msg: Request>) -> Self::Future { MyFut(msg) } +/// fn call(&self, msg: Request, ()>) -> Self::Future { MyFut(msg) } /// } /// ``` /// diff --git a/src/net/server/stream.rs b/src/net/server/stream.rs index 617086c85..960d77002 100644 --- a/src/net/server/stream.rs +++ b/src/net/server/stream.rs @@ -228,7 +228,7 @@ type CommandReceiver = watch::Receiver; /// use domain::net::server::stream::StreamServer; /// use domain::net::server::util::service_fn; /// -/// fn my_service(msg: Request>, _meta: ()) -> ServiceResult> +/// fn my_service(msg: Request, ()>, _meta: ()) -> ServiceResult> /// { /// todo!() /// } diff --git a/src/net/server/util.rs b/src/net/server/util.rs index 2b30d5872..a9c839723 100644 --- a/src/net/server/util.rs +++ b/src/net/server/util.rs @@ -77,7 +77,7 @@ where /// // provide, and returns one or more DNS responses. /// // /// // Note that using `service_fn()` does not permit you to use async code! -/// fn my_service(req: Request>, _meta: MyMeta) +/// fn my_service(req: Request, ()>, _meta: MyMeta) /// -> ServiceResult> /// { /// let builder = mk_builder_for_target(); From c5a0d5bfe6941866145e2159957575e3bc556827 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 29 Jan 2025 21:18:16 +0100 Subject: [PATCH 403/569] ZoneUpdater currently only supports ParsedRecord which is quite hard to construct, as that is what is output by the XfrResponseInterpreter. This commit generalizes the support to any Record type, not just ParsedRecord. --- src/zonetree/update.rs | 85 ++++++++++++++++++++++++++++-------------- 1 file changed, 56 insertions(+), 29 deletions(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 6a20140b3..bc77deca1 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -4,17 +4,17 @@ //! content of zones without requiring knowledge of the low-level details of //! how the [`WritableZone`] trait implemented by [`Zone`] works. use core::future::Future; +use core::marker::PhantomData; use core::pin::Pin; -use std::borrow::ToOwned; use std::boxed::Box; use bytes::Bytes; use tracing::trace; use crate::base::name::{FlattenInto, Label}; -use crate::base::{ParsedName, Record, Rtype}; -use crate::net::xfr::protocol::ParsedRecord; +use crate::base::scan::ScannerError; +use crate::base::{Name, Record, Rtype, ToName}; use crate::rdata::ZoneRecordData; use crate::zonetree::{Rrset, SharedRrset}; @@ -214,7 +214,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// ``` /// /// [`apply()`]: ZoneUpdater::apply() -pub struct ZoneUpdater { +pub struct ZoneUpdater { /// The zone to be updated. zone: Zone, @@ -226,9 +226,15 @@ pub struct ZoneUpdater { /// The current state of the updater. state: ZoneUpdaterState, + + _phantom: PhantomData, } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Creates a new [`ZoneUpdater`] that will update the given [`Zone`] /// content. /// @@ -246,12 +252,17 @@ impl ZoneUpdater { zone, write, state: Default::default(), + _phantom: PhantomData, }) }) } } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Apply the given [`ZoneUpdate`] to the [`Zone`] being updated. /// /// Returns `Ok` on success, `Err` otherwise. On success, if changes were @@ -266,7 +277,7 @@ impl ZoneUpdater { /// progress and re-open the zone for editing again. pub async fn apply( &mut self, - update: ZoneUpdate, + update: ZoneUpdate>>, ) -> Result, Error> { trace!("Update: {update}"); @@ -344,7 +355,11 @@ impl ZoneUpdater { } } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Given a zone record, obtain a [`WritableZoneNode`] for the owner. /// /// A [`Zone`] is a tree structure which can be modified by descending the @@ -364,7 +379,7 @@ impl ZoneUpdater { /// the record owner name. async fn get_writable_child_node_for_owner( &mut self, - rec: &ParsedRecord, + rec: &Record>, ) -> Result>, Error> { let mut it = rel_name_rev_iter(self.zone.apex_name(), rec.owner())?; @@ -386,17 +401,19 @@ impl ZoneUpdater { /// Create or update the SOA RRset using the given SOA record. async fn update_soa( &mut self, - new_soa: Record< - ParsedName, - ZoneRecordData>, - >, + new_soa: Record>, ) -> Result<(), Error> { if new_soa.rtype() != Rtype::SOA { return Err(Error::NotSoaRecord); } let mut rrset = Rrset::new(Rtype::SOA, new_soa.ttl()); - rrset.push_data(new_soa.data().to_owned().flatten_into()); + let Ok(flattened) = new_soa.data().clone().try_flatten_into() else { + return Err(Error::IoError(std::io::Error::custom( + "Unable to flatten bytes", + ))); + }; + rrset.push_data(flattened); self.write .update_root_rrset(SharedRrset::new(rrset)) .await?; @@ -407,10 +424,7 @@ impl ZoneUpdater { /// Find and delete a resource record in the zone by exact match. async fn delete_record_from_rrset( &mut self, - rec: Record< - ParsedName, - ZoneRecordData>, - >, + rec: Record>, ) -> Result<(), Error> { // Find or create the point to edit in the node tree. let tree_node = self.get_writable_child_node_for_owner(&rec).await?; @@ -443,11 +457,12 @@ impl ZoneUpdater { /// Add a resource record to a new or existing RRset. async fn add_record_to_rrset( &mut self, - rec: Record< - ParsedName, - ZoneRecordData>, - >, - ) -> Result<(), Error> { + rec: Record>, + ) -> Result<(), Error> + where + ZoneRecordData: + FlattenInto>>, + { // Find or create the point to edit in the node tree. let tree_node = self.get_writable_child_node_for_owner(&rec).await?; let tree_node = tree_node.as_ref().unwrap_or(self.write.root()); @@ -456,7 +471,11 @@ impl ZoneUpdater { // RRset in the tree plus the one to add. let mut rrset = Rrset::new(rec.rtype(), rec.ttl()); let rtype = rec.rtype(); - let data = rec.into_data().flatten_into(); + let Ok(data) = rec.into_data().try_flatten_into() else { + return Err(Error::IoError(std::io::Error::custom( + "Unable to flatten bytes", + ))); + }; rrset.push_data(data); @@ -904,7 +923,7 @@ mod tests { // IN NS NS.JAIN.AD.JP. let ns_1 = Record::new( - ParsedName::from(Name::from_str("JAIN.AD.JP.").unwrap()), + ParsedName::from(Name::::from_str("JAIN.AD.JP.").unwrap()), Class::IN, Ttl::from_secs(0), Ns::new(ParsedName::from( @@ -919,7 +938,9 @@ mod tests { // NS.JAIN.AD.JP. IN A 133.69.136.1 let a_1 = Record::new( - ParsedName::from(Name::from_str("NS.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NS.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 1)).into(), @@ -931,7 +952,9 @@ mod tests { // NEZU.JAIN.AD.JP. IN A 133.69.136.5 let nezu = Record::new( - ParsedName::from(Name::from_str("NEZU.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NEZU.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 5)).into(), @@ -956,7 +979,9 @@ mod tests { .await .unwrap(); let a_2 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 4)).into(), @@ -991,7 +1016,9 @@ mod tests { .await .unwrap(); let a_4 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 3)).into(), From 13f8e5176692afed139cba078ac2ac13d840d1b7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 30 Jan 2025 15:42:58 +0100 Subject: [PATCH 404/569] Take validity time for a signature as input to signing, not from a key, as the validity period required can change over the lifetime of a key and may be affected by other factors such as current signature expiration time and jitter. --- src/sign/config.rs | 23 ++- src/sign/keys/keymeta.rs | 28 +++- src/sign/keys/signingkey.rs | 27 ---- src/sign/mod.rs | 45 ++++-- src/sign/signatures/rrsigs.rs | 256 ++++++++++++++++++++++---------- src/sign/signatures/strategy.rs | 55 +++++++ src/sign/traits.rs | 54 ++++--- 7 files changed, 336 insertions(+), 152 deletions(-) diff --git a/src/sign/config.rs b/src/sign/config.rs index ea49553bd..df73e4c04 100644 --- a/src/sign/config.rs +++ b/src/sign/config.rs @@ -3,13 +3,14 @@ use core::marker::PhantomData; use octseq::{EmptyBuilder, FromBuilder}; -use super::signatures::strategy::DefaultSigningKeyUsageStrategy; use crate::base::{Name, ToName}; use crate::sign::denial::config::DenialConfig; use crate::sign::denial::nsec3::{ Nsec3HashProvider, OnDemandNsec3HashProvider, }; use crate::sign::records::{DefaultSorter, Sorter}; +use crate::sign::signatures::strategy::DefaultSigningKeyUsageStrategy; +use crate::sign::signatures::strategy::RrsigValidityPeriodStrategy; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::SignRaw; @@ -21,6 +22,7 @@ pub struct SigningConfig< Octs, Inner, KeyStrat, + ValidityStrat, Sort, HP = OnDemandNsec3HashProvider, > where @@ -28,6 +30,7 @@ pub struct SigningConfig< Octs: AsRef<[u8]> + From<&'static [u8]>, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, Sort: Sorter, { /// Authenticated denial of existing mechanism configuration. @@ -36,36 +39,42 @@ pub struct SigningConfig< /// Should keys used to sign the zone be added as DNSKEY RRs? pub add_used_dnskeys: bool, + pub rrsig_validity_period_strategy: ValidityStrat, + _phantom: PhantomData<(Inner, KeyStrat, Sort)>, } -impl - SigningConfig +impl + SigningConfig where HP: Nsec3HashProvider, Octs: AsRef<[u8]> + From<&'static [u8]>, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, Sort: Sorter, { pub fn new( denial: DenialConfig, add_used_dnskeys: bool, + rrsig_validity_period_strategy: ValidityStrat, ) -> Self { Self { denial, add_used_dnskeys, + rrsig_validity_period_strategy, _phantom: PhantomData, } } } -impl Default - for SigningConfig< +impl + SigningConfig< N, Octs, Inner, DefaultSigningKeyUsageStrategy, + ValidityStrat, DefaultSorter, OnDemandNsec3HashProvider, > @@ -74,11 +83,13 @@ where Octs: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, Inner: SignRaw, + ValidityStrat: RrsigValidityPeriodStrategy, { - fn default() -> Self { + pub fn default(rrsig_validity_period_strategy: ValidityStrat) -> Self { Self { denial: Default::default(), add_used_dnskeys: true, + rrsig_validity_period_strategy, _phantom: Default::default(), } } diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs index 9df2bf0b4..c2d0e285b 100644 --- a/src/sign/keys/keymeta.rs +++ b/src/sign/keys/keymeta.rs @@ -7,8 +7,7 @@ use crate::sign::SignRaw; //------------ DesignatedSigningKey ------------------------------------------ -pub trait DesignatedSigningKey: - Deref> +pub trait DesignatedSigningKey where Octs: AsRef<[u8]>, Inner: SignRaw, @@ -20,6 +19,27 @@ where /// Should this key be used to "sign a zone" (RFC 4033 section 2 "Zone /// Signing Key (ZSK)"). fn signs_zone_data(&self) -> bool; + + fn signing_key(&self) -> &SigningKey; +} + +impl DesignatedSigningKey for &T +where + Octs: AsRef<[u8]>, + Inner: SignRaw, + T: DesignatedSigningKey, +{ + fn signs_keys(&self) -> bool { + (**self).signs_keys() + } + + fn signs_zone_data(&self) -> bool { + (**self).signs_zone_data() + } + + fn signing_key(&self) -> &SigningKey { + (**self).signing_key() + } } //------------ IntendedKeyPurpose -------------------------------------------- @@ -202,4 +222,8 @@ where IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK ) } + + fn signing_key(&self) -> &SigningKey { + &self.key + } } diff --git a/src/sign/keys/signingkey.rs b/src/sign/keys/signingkey.rs index 835f90d86..67ec92675 100644 --- a/src/sign/keys/signingkey.rs +++ b/src/sign/keys/signingkey.rs @@ -1,8 +1,5 @@ -use core::ops::RangeInclusive; - use crate::base::iana::SecAlg; use crate::base::Name; -use crate::rdata::dnssec::Timestamp; use crate::sign::{PublicKeyBytes, SignRaw}; use crate::validate::Key; @@ -22,13 +19,6 @@ pub struct SigningKey { /// The raw private key. inner: Inner, - - /// The validity period to assign to any DNSSEC signatures created using - /// this key. - /// - /// The range spans from the inception timestamp up to and including the - /// expiration timestamp. - signature_validity_period: Option>, } //--- Construction @@ -40,25 +30,8 @@ impl SigningKey { owner, flags, inner, - signature_validity_period: None, } } - - pub fn with_validity( - mut self, - inception: Timestamp, - expiration: Timestamp, - ) -> Self { - self.signature_validity_period = - Some(RangeInclusive::new(inception, expiration)); - self - } - - pub fn signature_validity_period( - &self, - ) -> Option> { - self.signature_validity_period.clone() - } } //--- Inspection diff --git a/src/sign/mod.rs b/src/sign/mod.rs index 30ebb3d37..acb5bdcdf 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -143,7 +143,9 @@ use records::{RecordsIter, Sorter}; use signatures::rrsigs::{ generate_rrsigs, GenerateRrsigConfig, RrsigRecords, }; -use signatures::strategy::SigningKeyUsageStrategy; +use signatures::strategy::{ + RrsigValidityPeriodStrategy, SigningKeyUsageStrategy, +}; use traits::{SignRaw, SignableZone, SortedExtend}; //------------ SignableZoneInOut --------------------------------------------- @@ -349,9 +351,28 @@ where /// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace /// [`SortedRecords`]: crate::sign::records::SortedRecords /// [`Zone`]: crate::zonetree::Zone -pub fn sign_zone( +pub fn sign_zone< + N, + Octs, + S, + DSK, + Inner, + KeyStrat, + ValidityStrat, + Sort, + HP, + T, +>( mut in_out: SignableZoneInOut, - signing_config: &mut SigningConfig, + signing_config: &mut SigningConfig< + N, + Octs, + Inner, + KeyStrat, + ValidityStrat, + Sort, + HP, + >, signing_keys: &[DSK], ) -> Result<(), SigningError> where @@ -370,6 +391,7 @@ where Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy + Clone, S: SignableZone, Sort: Sorter, T: SortedExtend + ?Sized, @@ -439,7 +461,10 @@ where } if !signing_keys.is_empty() { - let mut rrsig_config = GenerateRrsigConfig::new(); + let mut rrsig_config = + GenerateRrsigConfig::::new( + signing_config.rrsig_validity_period_strategy.clone(), + ); rrsig_config.add_used_dnskeys = signing_config.add_used_dnskeys; rrsig_config.zone_apex = Some(&apex_owner); @@ -447,11 +472,7 @@ where let owner_rrs = RecordsIter::new(in_out.as_out_slice()); let RrsigRecords { rrsigs, dnskeys } = - generate_rrsigs::( - owner_rrs, - signing_keys, - &rrsig_config, - )?; + generate_rrsigs(owner_rrs, signing_keys, &rrsig_config)?; // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. @@ -466,11 +487,7 @@ where let owner_rrs = RecordsIter::new(in_out.as_slice()); let RrsigRecords { rrsigs, dnskeys } = - generate_rrsigs::( - owner_rrs, - signing_keys, - &rrsig_config, - )?; + generate_rrsigs(owner_rrs, signing_keys, &rrsig_config)?; // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index e48a92339..f80913601 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -10,16 +10,16 @@ use std::vec::Vec; use log::Level; use octseq::builder::FromBuilder; use octseq::{OctetsFrom, OctetsInto}; +use smallvec::SmallVec; use tracing::{debug, trace}; -use super::strategy::DefaultSigningKeyUsageStrategy; use crate::base::cmp::CanonicalOrd; use crate::base::iana::Rtype; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; use crate::base::Name; -use crate::rdata::dnssec::ProtoRrsig; +use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; use crate::rdata::{Dnskey, Rrsig, ZoneRecordData}; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; @@ -28,27 +28,34 @@ use crate::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, }; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; +use crate::sign::signatures::strategy::{ + DefaultSigningKeyUsageStrategy, RrsigValidityPeriodStrategy, +}; use crate::sign::traits::SignRaw; -use smallvec::SmallVec; -//----------- GenerateRrsigConfig -------------------------------------------- +//------------ GenerateRrsigConfig ------------------------------------------- -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct GenerateRrsigConfig<'a, N, KeyStrat, Sort> { +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct GenerateRrsigConfig<'a, N, KeyStrat, ValidityStrat, Sort> { pub add_used_dnskeys: bool, pub zone_apex: Option<&'a N>, + pub rrsig_validity_period_strategy: ValidityStrat, + _phantom: PhantomData<(KeyStrat, Sort)>, } -impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { +impl<'a, N, KeyStrat, ValidityStrat, Sort> + GenerateRrsigConfig<'a, N, KeyStrat, ValidityStrat, Sort> +{ /// Like [`Self::default()`] but gives control over the SigningKeyStrategy /// and Sorter used. - pub fn new() -> Self { + pub fn new(rrsig_validity_period_strategy: ValidityStrat) -> Self { Self { add_used_dnskeys: true, zone_apex: None, + rrsig_validity_period_strategy, _phantom: Default::default(), } } @@ -64,20 +71,19 @@ impl<'a, N, KeyStrat, Sort> GenerateRrsigConfig<'a, N, KeyStrat, Sort> { } } -impl Default - for GenerateRrsigConfig< +impl + GenerateRrsigConfig< '_, N, DefaultSigningKeyUsageStrategy, + ValidityStrat, DefaultSorter, > +where + ValidityStrat: RrsigValidityPeriodStrategy, { - fn default() -> Self { - Self { - add_used_dnskeys: true, - zone_apex: None, - _phantom: Default::default(), - } + pub fn default(rrsig_validity_period_strategy: ValidityStrat) -> Self { + Self::new(rrsig_validity_period_strategy) } } @@ -140,15 +146,16 @@ where /// subject to change. // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -pub fn generate_rrsigs( +pub fn generate_rrsigs( records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], - config: &GenerateRrsigConfig<'_, N, KeyStrat, Sort>, + config: &GenerateRrsigConfig<'_, N, KeyStrat, ValidityStrat, Sort>, ) -> Result, SigningError> where DSK: DesignatedSigningKey, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, N: ToName + PartialEq + Clone @@ -316,10 +323,15 @@ where for key in non_dnskey_signing_key_idxs.iter().map(|&idx| &keys[idx]) { + let (inception, expiration) = config + .rrsig_validity_period_strategy + .validity_period_for_rrset(&rrset); let rrsig_rr = sign_rrset_in( - key, + key.signing_key(), &rrset, zone_apex, + inception, + expiration, &mut reusable_scratch, )?; out.rrsigs.push(rrsig_rr); @@ -327,7 +339,7 @@ where "Signed {} RRSET at {} with keytag {}", rrset.rtype(), rrset.owner(), - key.public_key().key_tag() + key.signing_key().public_key().key_tag() ); } } @@ -391,14 +403,14 @@ fn log_keys_in_use( } else { "Unused" }; - debug_key(&format!("Key[{idx}]: {usage}"), key); + debug_key(&format!("Key[{idx}]: {usage}"), key.signing_key()); } } #[allow(clippy::too_many_arguments)] -fn generate_apex_rrsigs( +fn generate_apex_rrsigs( keys: &[DSK], - config: &GenerateRrsigConfig<'_, N, KeyStrat, Sort>, + config: &GenerateRrsigConfig<'_, N, KeyStrat, ValidityStrat, Sort>, records: &mut core::iter::Peekable< RecordsIter<'_, N, ZoneRecordData>, >, @@ -414,6 +426,7 @@ where DSK: DesignatedSigningKey, Inner: SignRaw, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, N: ToName + PartialEq + Clone @@ -501,8 +514,9 @@ where // into the correct output octets form, and if any keys we are going to // sign the zone with do not exist we add them. - for public_key in - keys_in_use_idxs.iter().map(|&idx| keys[idx].public_key()) + for public_key in keys_in_use_idxs + .iter() + .map(|&idx| keys[idx].signing_key().public_key()) { let dnskey = public_key.to_dnskey(); @@ -546,14 +560,23 @@ where }; for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { - let rrsig_rr = - sign_rrset_in(key, &rrset, zone_apex, reusable_scratch)?; + let (inception, expiration) = config + .rrsig_validity_period_strategy + .validity_period_for_rrset(&rrset); + let rrsig_rr = sign_rrset_in( + key.signing_key(), + &rrset, + zone_apex, + inception, + expiration, + reusable_scratch, + )?; out.rrsigs.push(rrsig_rr); trace!( "Signed {} RRs in RRSET {} at the zone apex with keytag {}", rrset.iter().len(), rrset.rtype(), - key.public_key().key_tag() + key.signing_key().public_key().key_tag() ); } } @@ -575,6 +598,8 @@ pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, apex_owner: &N, + inception: Timestamp, + expiration: Timestamp, ) -> Result>, SigningError> where N: ToName + Clone + Send, @@ -586,7 +611,7 @@ where Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { - sign_rrset_in(key, rrset, apex_owner, &mut vec![]) + sign_rrset_in(key, rrset, apex_owner, inception, expiration, &mut vec![]) } /// Generate `RRSIG` records for a given RRset. @@ -613,6 +638,8 @@ pub fn sign_rrset_in( key: &SigningKey, rrset: &Rrset<'_, N, D>, apex_owner: &N, + inception: Timestamp, + expiration: Timestamp, scratch: &mut Vec, ) -> Result>, SigningError> where @@ -633,11 +660,6 @@ where return Err(SigningError::RrsigRrsMustNotBeSigned); } - let (inception, expiration) = key - .signature_validity_period() - .ok_or(SigningError::NoSignatureValidityPeriodProvided)? - .into_inner(); - if expiration < inception { return Err(SigningError::InvalidSignatureValidityPeriod( inception, expiration, @@ -722,6 +744,7 @@ mod tests { use crate::zonetree::StoredName; use super::*; + use crate::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; use crate::zonetree::types::StoredRecordData; use rand::Rng; @@ -732,7 +755,8 @@ mod tests { fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); // RFC 4034 // 3.1.3. The Labels Field @@ -744,7 +768,9 @@ mod tests { records.insert(mk_a_rr("www.example.com.")).unwrap(); let rrset = Rrset::new(&records); - let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); + let rrsig_rr = + sign_rrset(&key, &rrset, &apex_owner, inception, expiration) + .unwrap(); let rrsig = rrsig_rr.data(); // RFC 4035 @@ -792,7 +818,8 @@ mod tests { fn sign_rrset_with_wildcard() { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); // RFC 4034 // 3.1.3. The Labels Field @@ -803,7 +830,9 @@ mod tests { records.insert(mk_a_rr("*.example.com.")).unwrap(); let rrset = Rrset::new(&records); - let rrsig_rr = sign_rrset(&key, &rrset, &apex_owner).unwrap(); + let rrsig_rr = + sign_rrset(&key, &rrset, &apex_owner, inception, expiration) + .unwrap(); let rrsig = rrsig_rr.data(); assert_eq!(rrsig.labels(), 2); @@ -818,7 +847,8 @@ mod tests { let apex_owner = Name::root(); let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let key = key.with_validity(Timestamp::from(0), Timestamp::from(0)); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); let dnskey = key.public_key().to_dnskey().convert(); let mut records = @@ -828,7 +858,8 @@ mod tests { .unwrap(); let rrset = Rrset::new(&records); - let res = sign_rrset(&key, &rrset, &apex_owner); + let res = + sign_rrset(&key, &rrset, &apex_owner, inception, expiration); assert!(matches!(res, Err(SigningError::RrsigRrsMustNotBeSigned))); } @@ -870,18 +901,16 @@ mod tests { // Good: Expiration > Inception. let (inception, expiration) = calc_timestamps(5, 5); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Good: Expiration == Inception. let (inception, expiration) = calc_timestamps(10, 0); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Bad: Expiration < Inception. let (expiration, inception) = calc_timestamps(5, 10); - let key = key.with_validity(inception, expiration); - let res = sign_rrset(&key, &rrset, &apex_owner); + let res = + sign_rrset(&key, &rrset, &apex_owner, inception, expiration); assert!(matches!( res, Err(SigningError::InvalidSignatureValidityPeriod(_, _)) @@ -890,26 +919,22 @@ mod tests { // Good: Expiration > Inception with Expiration near wrap around // point. let (inception, expiration) = calc_timestamps(u32::MAX - 10, 10); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Good: Expiration > Inception with Inception near wrap around point. let (inception, expiration) = calc_timestamps(0, 10); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Good: Expiration > Inception with Exception crossing the wrap // around point. let (inception, expiration) = calc_timestamps(u32::MAX - 10, 20); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Good: Expiration - Inception == 68 years. let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; let (inception, expiration) = calc_timestamps(0, sixty_eight_years_in_secs); - let key = key.with_validity(inception, expiration); - sign_rrset(&key, &rrset, &apex_owner).unwrap(); + sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); // Bad: Expiration - Inception > 68 years. // @@ -948,8 +973,8 @@ mod tests { Timestamp::from(0), Timestamp::from(sixty_eight_years_in_secs + one_year_in_secs), ); - let key = key.with_validity(inception, expiration); - let res = sign_rrset(&key, &rrset, &apex_owner); + let res = + sign_rrset(&key, &rrset, &apex_owner, inception, expiration); assert!(matches!( res, Err(SigningError::InvalidSignatureValidityPeriod(_, _)) @@ -958,6 +983,11 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let records = SortedRecords::::default(); let no_keys: [DnssecSigningKey; 0] = []; @@ -965,13 +995,18 @@ mod tests { generate_rrsigs( RecordsIter::new(&records), &no_keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ) .unwrap(); } #[test] fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let mut records = SortedRecords::default(); records.insert(mk_a_rr("example.")).unwrap(); let no_keys: [DnssecSigningKey; 0] = []; @@ -979,7 +1014,7 @@ mod tests { let res = generate_rrsigs( RecordsIter::new(&records), &no_keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ); assert!(matches!(res, Err(SigningError::NoKeysProvided))); @@ -988,27 +1023,32 @@ mod tests { #[test] fn generate_rrsigs_without_suitable_keys_should_fail_for_non_empty_zone() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let mut records = SortedRecords::default(); records.insert(mk_a_rr("example.")).unwrap(); let res = generate_rrsigs( RecordsIter::new(&records), &[mk_dnssec_signing_key(IntendedKeyPurpose::KSK)], - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ); assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); let res = generate_rrsigs( RecordsIter::new(&records), &[mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)], - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ); assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); let res = generate_rrsigs( RecordsIter::new(&records), &[mk_dnssec_signing_key(IntendedKeyPurpose::Inactive)], - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ); assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); } @@ -1024,6 +1064,12 @@ mod tests { } fn generate_rrsigs_for_partial_zone(zone_apex: &str, record_owner: &str) { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); + // This is an example of generating RRSIGs for something other than a // full zone, in this case just for an A record. This test // deliberately does not include a SOA record as the zone is partial. @@ -1042,7 +1088,7 @@ mod tests { let generated_records = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default() + &GenerateRrsigConfig::default(rrsig_validity_period_strategy) .with_zone_apex(&mk_name(zone_apex)), ) .unwrap(); @@ -1065,6 +1111,11 @@ mod tests { #[test] fn generate_rrsigs_ignores_records_outside_the_zone() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let mut records = SortedRecords::default(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), @@ -1079,7 +1130,7 @@ mod tests { let generated_records = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ) .unwrap(); @@ -1116,7 +1167,7 @@ mod tests { let generated_records = generate_rrsigs( RecordsIter::new(&records[2..]), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ) .unwrap(); @@ -1138,6 +1189,11 @@ mod tests { #[test] fn generate_rrsigs_fails_with_multiple_soas_at_apex() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let mut records = SortedRecords::default(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), @@ -1151,7 +1207,7 @@ mod tests { let res = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ); assert!(matches!( @@ -1162,34 +1218,58 @@ mod tests { #[test] fn generate_rrsigs_for_complete_zone_with_ksk_and_zsk() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [ mk_dnssec_signing_key(IntendedKeyPurpose::KSK), mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), ]; - let cfg = GenerateRrsigConfig::default(); + let cfg = + GenerateRrsigConfig::default(rrsig_validity_period_strategy); generate_rrsigs_for_complete_zone(&keys, 0, 1, &cfg).unwrap(); } #[test] fn generate_rrsigs_for_complete_zone_with_csk() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - let cfg = GenerateRrsigConfig::default(); + let cfg = + GenerateRrsigConfig::default(rrsig_validity_period_strategy); generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); } #[test] fn generate_rrsigs_for_complete_zone_with_csk_without_adding_dnskeys() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; let cfg = - GenerateRrsigConfig::default().without_adding_used_dns_keys(); + GenerateRrsigConfig::default(rrsig_validity_period_strategy) + .without_adding_used_dns_keys(); generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); } #[test] fn generate_rrsigs_for_complete_zone_with_only_zsk_should_fail_by_default( ) { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; - let cfg = GenerateRrsigConfig::default(); + let cfg = + GenerateRrsigConfig::default(rrsig_validity_period_strategy); // This should fail as the DefaultSigningKeyUsageStrategy requires // both ZSK and KSK, or a CSK. @@ -1200,6 +1280,11 @@ mod tests { #[test] fn generate_rrsigs_for_complete_zone_with_only_zsk_and_fallback_strategy() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; // Implement a strategy that falls back to the ZSK for signing zone @@ -1223,19 +1308,27 @@ mod tests { } } - let fallback_cfg = GenerateRrsigConfig::<_, FallbackStrat, _>::new(); + let fallback_cfg = GenerateRrsigConfig::<_, FallbackStrat, _, _>::new( + rrsig_validity_period_strategy, + ); generate_rrsigs_for_complete_zone(&keys, 0, 0, &fallback_cfg) .unwrap(); } - fn generate_rrsigs_for_complete_zone( + fn generate_rrsigs_for_complete_zone( keys: &[DnssecSigningKey], ksk_idx: usize, zsk_idx: usize, - cfg: &GenerateRrsigConfig, + cfg: &GenerateRrsigConfig< + StoredName, + KeyStrat, + ValidityStrat, + DefaultSorter, + >, ) -> Result<(), SigningError> where KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, { // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A let zonefile = include_bytes!( @@ -1469,6 +1562,11 @@ mod tests { #[test] fn generate_rrsigs_for_complete_zone_with_multiple_ksks_and_zsks() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let apex = "example."; let mut records = SortedRecords::default(); @@ -1493,7 +1591,7 @@ mod tests { let generated_records = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ) .unwrap(); @@ -1559,6 +1657,11 @@ mod tests { #[test] fn generate_rrsigs_for_already_signed_zone() { + let rrsig_validity_period_strategy = + FixedRrsigValidityPeriodStrategy::from(( + TEST_INCEPTION, + TEST_EXPIRATION, + )); let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; let dnskey = keys[0].public_key().to_dnskey().convert(); @@ -1584,7 +1687,7 @@ mod tests { let generated_records = generate_rrsigs( RecordsIter::new(&records), &keys, - &GenerateRrsigConfig::default(), + &GenerateRrsigConfig::default(rrsig_validity_period_strategy), ) .unwrap(); @@ -1667,11 +1770,6 @@ mod tests { TestKey::default(), ); - let key = key.with_validity( - Timestamp::from(TEST_INCEPTION), - Timestamp::from(TEST_EXPIRATION), - ); - DnssecSigningKey::new(key, purpose) } diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs index ba2d0abe4..85cac39eb 100644 --- a/src/sign/signatures/strategy.rs +++ b/src/sign/signatures/strategy.rs @@ -1,7 +1,9 @@ use smallvec::SmallVec; use crate::base::Rtype; +use crate::rdata::dnssec::Timestamp; use crate::sign::keys::keymeta::DesignatedSigningKey; +use crate::sign::records::Rrset; use crate::sign::SignRaw; //------------ SigningKeyUsageStrategy --------------------------------------- @@ -54,3 +56,56 @@ where { const NAME: &'static str = "Default key usage strategy"; } + +//------------ RrsigValidityPeriodStrategy ----------------------------------- + +/// The strategy for determining the validity period for an RRSIG for an +/// RRSET. +/// +/// Determining the right inception time and expiration time to use may depend +/// for example on the RTYPE of the RRSET being signed or on whether jitter +/// should be applied. +/// +/// See https://datatracker.ietf.org/doc/html/rfc6781#section-4.4.2. +pub trait RrsigValidityPeriodStrategy { + fn validity_period_for_rrset( + &self, + rrset: &Rrset<'_, N, D>, + ) -> (Timestamp, Timestamp); +} + +//------------ FixedRrsigValidityPeriodStrategy ------------------------------ + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct FixedRrsigValidityPeriodStrategy { + inception: Timestamp, + expiration: Timestamp, +} + +impl FixedRrsigValidityPeriodStrategy { + pub fn new(inception: Timestamp, expiration: Timestamp) -> Self { + Self { + inception, + expiration, + } + } +} + +//--- impl From<(u32, u32)> + +impl From<(u32, u32)> for FixedRrsigValidityPeriodStrategy { + fn from((inception, expiration): (u32, u32)) -> Self { + Self::new(Timestamp::from(inception), Timestamp::from(expiration)) + } +} + +//--- impl RrsigValidityPeriodStrategy + +impl RrsigValidityPeriodStrategy for FixedRrsigValidityPeriodStrategy { + fn validity_period_for_rrset( + &self, + _rrset: &Rrset<'_, N, D>, + ) -> (Timestamp, Timestamp) { + (self.inception, self.expiration) + } +} diff --git a/src/sign/traits.rs b/src/sign/traits.rs index 5ee34e98d..e633823e1 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -31,6 +31,7 @@ use crate::sign::sign_zone; use crate::sign::signatures::rrsigs::generate_rrsigs; use crate::sign::signatures::rrsigs::GenerateRrsigConfig; use crate::sign::signatures::rrsigs::RrsigRecords; +use crate::sign::signatures::strategy::RrsigValidityPeriodStrategy; use crate::sign::signatures::strategy::SigningKeyUsageStrategy; use crate::sign::SigningConfig; use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; @@ -163,6 +164,7 @@ where /// use domain::rdata::dnssec::Timestamp; /// use domain::sign::keys::DnssecSigningKey; /// use domain::sign::records::SortedRecords; +/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; /// use domain::sign::traits::SignableZone; /// use domain::sign::SigningConfig; /// @@ -189,11 +191,11 @@ where /// // Generate or import signing keys (see above). /// /// // Assign signature validity period and operator intent to the keys. -/// let key = key.with_validity(Timestamp::now(), Timestamp::now()); +/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); /// let keys = [DnssecSigningKey::new_csk(key)]; /// /// // Create a signing configuration. -/// let mut signing_config = SigningConfig::default(); +/// let mut signing_config = SigningConfig::default(validity); /// /// // Then generate the records which when added to the zone make it signed. /// let mut signer_generated_records = SortedRecords::default(); @@ -227,13 +229,14 @@ where /// This function is a convenience wrapper around calling /// [`crate::sign::sign_zone()`] function with enum variant /// [`SignableZoneInOut::SignInto`]. - fn sign_zone( + fn sign_zone( &self, signing_config: &mut SigningConfig< N, Octs, Inner, KeyStrat, + ValidityStrat, Sort, HP, >, @@ -248,17 +251,14 @@ where ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy + Clone, T: Deref>]> + SortedExtend + ?Sized, Self: Sized, { let in_out = SignableZoneInOut::new_into(self, out); - sign_zone::( - in_out, - signing_config, - signing_keys, - ) + sign_zone(in_out, signing_config, signing_keys) } } @@ -316,6 +316,7 @@ where /// use domain::rdata::dnssec::Timestamp; /// use domain::sign::keys::DnssecSigningKey; /// use domain::sign::records::SortedRecords; +/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; /// use domain::sign::traits::SignableZoneInPlace; /// use domain::sign::SigningConfig; /// @@ -342,11 +343,11 @@ where /// // Generate or import signing keys (see above). /// /// // Assign signature validity period and operator intent to the keys. -/// let key = key.with_validity(Timestamp::now(), Timestamp::now()); +/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); /// let keys = [DnssecSigningKey::new_csk(key)]; /// /// // Create a signing configuration. -/// let mut signing_config = SigningConfig::default(); +/// let mut signing_config = SigningConfig::default(validity); /// /// // Then sign the zone in-place. /// records.sign_zone(&mut signing_config, &keys).unwrap(); @@ -374,13 +375,14 @@ where /// This function is a convenience wrapper around calling /// [`crate::sign::sign_zone()`] function with enum variant /// [`SignableZoneInOut::SignInPlace`]. - fn sign_zone( + fn sign_zone( &mut self, signing_config: &mut SigningConfig< N, Octs, Inner, KeyStrat, + ValidityStrat, Sort, HP, >, @@ -394,13 +396,11 @@ where ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy + Clone, { - let in_out = SignableZoneInOut::new_in_place(self); - sign_zone::( - in_out, - signing_config, - signing_keys, - ) + let in_out = + SignableZoneInOut::<_, _, Self, _, _>::new_in_place(self); + sign_zone(in_out, signing_config, signing_keys) } } @@ -467,9 +467,11 @@ where /// # let mut records = SortedRecords::default(); /// use domain::sign::traits::Signable; /// use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; +/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; /// let apex = Name::>::root(); /// let rrset = Rrset::new(&records); -/// let generated_records = rrset.sign::(&apex, &keys).unwrap(); +/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); +/// let generated_records = rrset.sign::(&apex, &keys, validity).unwrap(); /// ``` pub trait Signable where @@ -496,20 +498,24 @@ where /// /// This function is a thin wrapper around [`generate_rrsigs()`]. #[allow(clippy::type_complexity)] - fn sign( + fn sign( &self, expected_apex: &N, keys: &[DSK], + rrsig_validity_period_strategy: ValidityStrat, ) -> Result, SigningError> where DSK: DesignatedSigningKey, KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy, { - generate_rrsigs::( - self.owner_rrs(), - keys, - &GenerateRrsigConfig::new().with_zone_apex(expected_apex), - ) + let rrsig_config = + GenerateRrsigConfig::::new( + rrsig_validity_period_strategy, + ) + .with_zone_apex(expected_apex); + + generate_rrsigs(self.owner_rrs(), keys, &rrsig_config) } } From 4910b9bedfd1bba2cbb8c5a3e3a48c4bf478cde6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:23:52 +0100 Subject: [PATCH 405/569] Impl Display for IntendedKeyPurpose. --- src/sign/keys/keymeta.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs index c2d0e285b..88707b362 100644 --- a/src/sign/keys/keymeta.rs +++ b/src/sign/keys/keymeta.rs @@ -2,6 +2,8 @@ use core::convert::From; use core::marker::PhantomData; use core::ops::Deref; +use std::fmt::Display; + use crate::sign::keys::signingkey::SigningKey; use crate::sign::SignRaw; @@ -83,6 +85,19 @@ pub enum IntendedKeyPurpose { Inactive, } +//--- impl Display + +impl Display for IntendedKeyPurpose { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + IntendedKeyPurpose::KSK => f.write_str("KSK"), + IntendedKeyPurpose::ZSK => f.write_str("ZSK"), + IntendedKeyPurpose::CSK => f.write_str("CSK"), + IntendedKeyPurpose::Inactive => f.write_str("Inactive"), + } + } +} + //------------ DnssecSigningKey ---------------------------------------------- /// A key that can be used for DNSSEC signing. @@ -161,6 +176,9 @@ where self.purpose } + pub fn set_purpose(&mut self, purpose: IntendedKeyPurpose) { + self.purpose = purpose; + } pub fn into_inner(self) -> SigningKey { self.key } From f785c51d37b1af1242199875bc99457b50eabbef Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 30 Jan 2025 23:59:54 +0100 Subject: [PATCH 406/569] Fix failing doc tests. --- src/net/server/service.rs | 16 ---------------- src/zonetree/update.rs | 12 ++++++------ 2 files changed, 6 insertions(+), 22 deletions(-) diff --git a/src/net/server/service.rs b/src/net/server/service.rs index f7577b171..ba5720956 100644 --- a/src/net/server/service.rs +++ b/src/net/server/service.rs @@ -98,22 +98,6 @@ pub type ServiceResult = Result, ServiceError>; /// } /// } /// -/// //------------ An anonymous async block service example ------------------- -/// struct MyAsyncBlockService; -/// -/// impl Service, ()> for MyAsyncBlockService { -/// type Target = Vec; -/// type Stream = Once>>; -/// type Future = Pin>>; -/// -/// fn call( -/// &self, -/// msg: Request, ()>, -/// ) -> Self::Future { -/// Box::pin(async move { mk_response_stream(&msg) }) -/// } -/// } -/// /// //------------ A named Future service example ----------------------------- /// struct MyFut(Request, ()>); /// diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index bc77deca1..2e066b467 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -51,7 +51,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// /// ``` /// # use std::str::FromStr; -/// # +/// # use bytes::Bytes; /// # use domain::base::iana::Class; /// # use domain::base::MessageBuilder; /// # use domain::base::Name; @@ -76,8 +76,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), @@ -106,7 +106,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// /// ```rust /// # use std::str::FromStr; -/// # +/// # use bytes::Bytes; /// # use domain::base::iana::Class; /// # use domain::base::MessageBuilder; /// # use domain::base::Name; @@ -133,8 +133,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), From aab0d9cc3040bb22b36fe25032d7da85f82a61d4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Jan 2025 00:01:23 +0100 Subject: [PATCH 407/569] Cargo fmt. --- src/zonetree/update.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 2e066b467..460f64095 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -51,7 +51,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// /// ``` /// # use std::str::FromStr; -/// # use bytes::Bytes; +/// # use bytes::Bytes; /// # use domain::base::iana::Class; /// # use domain::base::MessageBuilder; /// # use domain::base::Name; From 6b6588c9e5d38afbf252376f381b3e9317f3bffe Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Jan 2025 17:10:33 +0100 Subject: [PATCH 408/569] Review feedback: Remove From as it is not needed. --- src/sign/signatures/rrsigs.rs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index f80913601..5b3e74631 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -603,11 +603,7 @@ pub fn sign_rrset( ) -> Result>, SigningError> where N: ToName + Clone + Send, - D: RecordData - + ComposeRecordData - + From> - + CanonicalOrd - + Send, + D: RecordData + ComposeRecordData + CanonicalOrd + Send, Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { @@ -644,11 +640,7 @@ pub fn sign_rrset_in( ) -> Result>, SigningError> where N: ToName + Clone + Send, - D: RecordData - + ComposeRecordData - + From> - + CanonicalOrd - + Send, + D: RecordData + ComposeRecordData + CanonicalOrd + Send, Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { From 1d950ae50c252d4724996fe5a6ed0542d351abd7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Jan 2025 17:12:10 +0100 Subject: [PATCH 409/569] Remove unnecessary Send bounds. --- src/sign/signatures/rrsigs.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 5b3e74631..d6815d143 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -602,8 +602,8 @@ pub fn sign_rrset( expiration: Timestamp, ) -> Result>, SigningError> where - N: ToName + Clone + Send, - D: RecordData + ComposeRecordData + CanonicalOrd + Send, + N: ToName + Clone, + D: RecordData + ComposeRecordData + CanonicalOrd, Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { @@ -639,8 +639,8 @@ pub fn sign_rrset_in( scratch: &mut Vec, ) -> Result>, SigningError> where - N: ToName + Clone + Send, - D: RecordData + ComposeRecordData + CanonicalOrd + Send, + N: ToName + Clone, + D: RecordData + ComposeRecordData + CanonicalOrd, Inner: SignRaw, Octs: AsRef<[u8]> + OctetsFrom>, { From 01f542b51b534154dbb90cd77bfd03329ea1657e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Jan 2025 22:30:33 +0100 Subject: [PATCH 410/569] Add setter for RRSIg validity period to SigningConfig. --- src/sign/config.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sign/config.rs b/src/sign/config.rs index df73e4c04..43e23b905 100644 --- a/src/sign/config.rs +++ b/src/sign/config.rs @@ -66,6 +66,13 @@ where _phantom: PhantomData, } } + + pub fn set_rrsig_validity_period_strategy( + &mut self, + rrsig_validity_period_strategy: ValidityStrat, + ) { + self.rrsig_validity_period_strategy = rrsig_validity_period_strategy; + } } impl From 13d94a16acf76f9aa1b98146527a890e613ec618 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:39:12 +0100 Subject: [PATCH 411/569] Simplify NSEC unit test code. --- src/sign/denial/nsec.rs | 49 ++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index f7479ffa8..02894dce4 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -215,13 +215,13 @@ mod tests { use super::*; + type StoredSortedRecords = SortedRecords; + #[test] fn soa_is_required() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([mk_a_rr("some_a.a.")]); let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, @@ -233,10 +233,10 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_soa_rr("a.", "d.", "e."), + ]); let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, @@ -248,13 +248,12 @@ mod tests { fn records_outside_zone_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); - records.insert(mk_a_rr("some_a.b.")).unwrap(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("b.", "d.", "e."), + mk_a_rr("some_a.b."), + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); // First generate NSECs for the total record collection. As the // collection is sorted in canonical order the a zone preceeds the b @@ -290,14 +289,11 @@ mod tests { fn occluded_records_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records - .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) - .unwrap(); - records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("some_ns.a.", "some_a.other.b."), + mk_a_rr("some_a.some_ns.a."), + ]); let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); @@ -318,11 +314,10 @@ mod tests { fn expect_dnskeys_at_the_apex() { let cfg = GenerateNsecConfig::default(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); From 37f71b27e5c7eef7d4ffc43d9c5193fdcd46ee5c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:40:45 +0100 Subject: [PATCH 412/569] Simplify NSEC3 unit test code and fix up the occluded test copied from the NSEC unit test suite with the changes required for the NSEC3 case. --- src/sign/denial/nsec3.rs | 117 ++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 24bb39bc3..8faeb9703 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -804,8 +804,6 @@ mod tests { use crate::sign::records::SortedRecords; use crate::sign::test_util::*; - use crate::zonetree::types::StoredRecordData; - use crate::zonetree::StoredName; use super::*; @@ -813,9 +811,8 @@ mod tests { fn soa_is_required() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = + SortedRecords::<_, _>::from_iter([mk_a_rr("some_a.a.")]); let res = generate_nsec3s(records.owner_rrs(), &mut cfg); assert!(matches!( res, @@ -827,10 +824,10 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_soa_rr("a.", "d.", "e."), + ]); let res = generate_nsec3s(records.owner_rrs(), &mut cfg); assert!(matches!( res, @@ -842,25 +839,23 @@ mod tests { fn records_outside_zone_are_ignored() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); - records.insert(mk_a_rr("some_a.b.")).unwrap(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("b.", "d.", "e."), + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + mk_a_rr("some_a.b."), + ]); - // First generate NSECs for the total record collection. As the + // First generate NSEC3s for the total record collection. As the // collection is sorted in canonical order the a zone preceeds the b - // zone and NSECs should only be generated for the first zone in the + // zone and NSEC3s should only be generated for the first zone in the // collection. let a_and_b_records = records.owner_rrs(); let generated_records = generate_nsec3s(a_and_b_records, &mut cfg).unwrap(); - let mut expected_records = SortedRecords::default(); - expected_records.extend([ + let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( "a.", "a.", @@ -873,16 +868,15 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); - // Now skip the a zone in the collection and generate NSECs for the - // remaining records which should only generate NSECs for the b zone. + // Now skip the a zone in the collection and generate NSEC3s for the + // remaining records which should only generate NSEC3s for the b zone. let mut b_records_only = records.owner_rrs(); b_records_only.skip_before(&mk_name("b.")); let generated_records = generate_nsec3s(b_records_only, &mut cfg).unwrap(); - let mut expected_records = SortedRecords::default(); - expected_records.extend([ + let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( "b.", "b.", @@ -896,48 +890,47 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); } - // #[test] - // fn occluded_records_are_ignored() { - // let mut cfg = GenerateNsec3Config::default() - // .without_assuming_dnskeys_will_be_added(); - // let mut records = SortedRecords::default(); + #[test] + fn occluded_records_are_ignored() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("some_ns.a.", "some_a.other.b."), + mk_a_rr("some_a.some_ns.a."), + ]); - // records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - // records - // .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) - // .unwrap(); - // records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - // let generated_records = - // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "a.", + "a.", + "some_ns.a.", + "SOA RRSIG NSEC3PARAM", + &cfg, + ), + // Unlike with NSEC the type bitmap for the NSEC3 for some_ns.a + // does NOT include RRSIG. This is because with NSEC "Each owner + // name in the zone that has authoritative data or a delegation + // point NS RRset MUST have an NSEC resource record" (RFC 4035 + // section 2.3), and while the zone is not authoritative for the + // NS record, "NSEC RRsets are authoritative data and are + // therefore signed" (RFC 4035 section 2.3). With NSEC3 however + // as the NSEC3 record for the unsigned delegation is generated + // (because we are not using opt out) but not stored at some_ns.a + // (but instead at .a.) then the only record at some_ns.a + // is the NS record itself which is not authoritative and so + // doesn't get an RRSIG. + mk_nsec3_rr("a.", "some_ns.a.", "a.", "NS", &cfg), + ]); - // // Implicit negative test. - // assert_eq!( - // generated_records.nsec3s, - // [ - // mk_nsec3_rr( - // "a.", - // "a.", - // "some_ns.a.", - // "SOA RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "a.", - // "some_ns.a.", - // "a.", - // "NS RRSIG NSEC", - // &mut cfg - // ), - // ] - // ); + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } - // // Explicit negative test. - // assert!(!contains_owner( - // &generated_records.nsec3s, - // "some_a.some_ns.a.example." - // )); - // } + // TODO: Repeat above test but with opt out enabled. Or add a separate opt + // test. // #[test] // fn expect_dnskeys_at_the_apex() { From d3113715d92a634bf21f76f4225b322f37b09a70 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:39:12 +0100 Subject: [PATCH 413/569] Simplify NSEC unit test code. --- src/sign/denial/nsec.rs | 49 ++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index f7479ffa8..02894dce4 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -215,13 +215,13 @@ mod tests { use super::*; + type StoredSortedRecords = SortedRecords; + #[test] fn soa_is_required() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([mk_a_rr("some_a.a.")]); let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, @@ -233,10 +233,10 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_soa_rr("a.", "d.", "e."), + ]); let res = generate_nsecs(records.owner_rrs(), &cfg); assert!(matches!( res, @@ -248,13 +248,12 @@ mod tests { fn records_outside_zone_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); - records.insert(mk_a_rr("some_a.b.")).unwrap(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("b.", "d.", "e."), + mk_a_rr("some_a.b."), + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); // First generate NSECs for the total record collection. As the // collection is sorted in canonical order the a zone preceeds the b @@ -290,14 +289,11 @@ mod tests { fn occluded_records_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records - .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) - .unwrap(); - records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("some_ns.a.", "some_a.other.b."), + mk_a_rr("some_a.some_ns.a."), + ]); let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); @@ -318,11 +314,10 @@ mod tests { fn expect_dnskeys_at_the_apex() { let cfg = GenerateNsecConfig::default(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); From 2d3387741c086c0b06d0fb7441d89229e1960fc0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sun, 2 Feb 2025 15:40:45 +0100 Subject: [PATCH 414/569] Simplify NSEC3 unit test code and fix up the occluded test copied from the NSEC unit test suite with the changes required for the NSEC3 case. --- src/sign/denial/nsec3.rs | 117 ++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 62 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 24bb39bc3..8faeb9703 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -804,8 +804,6 @@ mod tests { use crate::sign::records::SortedRecords; use crate::sign::test_util::*; - use crate::zonetree::types::StoredRecordData; - use crate::zonetree::StoredName; use super::*; @@ -813,9 +811,8 @@ mod tests { fn soa_is_required() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = + SortedRecords::<_, _>::from_iter([mk_a_rr("some_a.a.")]); let res = generate_nsec3s(records.owner_rrs(), &mut cfg); assert!(matches!( res, @@ -827,10 +824,10 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_soa_rr("a.", "d.", "e.")).unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_soa_rr("a.", "d.", "e."), + ]); let res = generate_nsec3s(records.owner_rrs(), &mut cfg); assert!(matches!( res, @@ -842,25 +839,23 @@ mod tests { fn records_outside_zone_are_ignored() { let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); - let mut records = - SortedRecords::::default(); - - records.insert(mk_soa_rr("b.", "d.", "e.")).unwrap(); - records.insert(mk_a_rr("some_a.b.")).unwrap(); - records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - records.insert(mk_a_rr("some_a.a.")).unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("b.", "d.", "e."), + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + mk_a_rr("some_a.b."), + ]); - // First generate NSECs for the total record collection. As the + // First generate NSEC3s for the total record collection. As the // collection is sorted in canonical order the a zone preceeds the b - // zone and NSECs should only be generated for the first zone in the + // zone and NSEC3s should only be generated for the first zone in the // collection. let a_and_b_records = records.owner_rrs(); let generated_records = generate_nsec3s(a_and_b_records, &mut cfg).unwrap(); - let mut expected_records = SortedRecords::default(); - expected_records.extend([ + let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( "a.", "a.", @@ -873,16 +868,15 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); - // Now skip the a zone in the collection and generate NSECs for the - // remaining records which should only generate NSECs for the b zone. + // Now skip the a zone in the collection and generate NSEC3s for the + // remaining records which should only generate NSEC3s for the b zone. let mut b_records_only = records.owner_rrs(); b_records_only.skip_before(&mk_name("b.")); let generated_records = generate_nsec3s(b_records_only, &mut cfg).unwrap(); - let mut expected_records = SortedRecords::default(); - expected_records.extend([ + let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( "b.", "b.", @@ -896,48 +890,47 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); } - // #[test] - // fn occluded_records_are_ignored() { - // let mut cfg = GenerateNsec3Config::default() - // .without_assuming_dnskeys_will_be_added(); - // let mut records = SortedRecords::default(); + #[test] + fn occluded_records_are_ignored() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("some_ns.a.", "some_a.other.b."), + mk_a_rr("some_a.some_ns.a."), + ]); - // records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - // records - // .insert(mk_ns_rr("some_ns.a.", "some_a.other.b.")) - // .unwrap(); - // records.insert(mk_a_rr("some_a.some_ns.a.")).unwrap(); + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - // let generated_records = - // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "a.", + "a.", + "some_ns.a.", + "SOA RRSIG NSEC3PARAM", + &cfg, + ), + // Unlike with NSEC the type bitmap for the NSEC3 for some_ns.a + // does NOT include RRSIG. This is because with NSEC "Each owner + // name in the zone that has authoritative data or a delegation + // point NS RRset MUST have an NSEC resource record" (RFC 4035 + // section 2.3), and while the zone is not authoritative for the + // NS record, "NSEC RRsets are authoritative data and are + // therefore signed" (RFC 4035 section 2.3). With NSEC3 however + // as the NSEC3 record for the unsigned delegation is generated + // (because we are not using opt out) but not stored at some_ns.a + // (but instead at .a.) then the only record at some_ns.a + // is the NS record itself which is not authoritative and so + // doesn't get an RRSIG. + mk_nsec3_rr("a.", "some_ns.a.", "a.", "NS", &cfg), + ]); - // // Implicit negative test. - // assert_eq!( - // generated_records.nsec3s, - // [ - // mk_nsec3_rr( - // "a.", - // "a.", - // "some_ns.a.", - // "SOA RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "a.", - // "some_ns.a.", - // "a.", - // "NS RRSIG NSEC", - // &mut cfg - // ), - // ] - // ); + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } - // // Explicit negative test. - // assert!(!contains_owner( - // &generated_records.nsec3s, - // "some_a.some_ns.a.example." - // )); - // } + // TODO: Repeat above test but with opt out enabled. Or add a separate opt + // test. // #[test] // fn expect_dnskeys_at_the_apex() { From 3086e8578fa08b1ce765cf666f74854d0c395ed3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 3 Feb 2025 15:06:17 +0100 Subject: [PATCH 415/569] - Added a unit test verifying that existing NSEC RRs are ignored by generate_nsecs(). - Added a unit test verifying compliance with the RFC 5155 Appendix A signed zone example. - Imroved and added comments. - Rewrote cryptic slice based NSEC3 next owner hashing loop with initial (untested!) type bit map merging and collision detection support, and removed no longer needed (but added in this branch) SortedRecords::as_mut_slice(). - Removed confusing duplicate storage of NSEC3 opt-out info in GenerateNsec3Config. - Added missing (though not breaking) trailing periods in test data. --- src/rdata/nsec3.rs | 64 ++ src/sign/denial/nsec.rs | 40 +- src/sign/denial/nsec3.rs | 664 ++++++++++++-------- src/sign/mod.rs | 63 +- src/sign/records.rs | 4 - src/sign/test_util/mod.rs | 46 +- test-data/zonefiles/rfc4035-appendix-A.zone | 6 +- test-data/zonefiles/rfc5155-appendix-A.zone | 42 ++ 8 files changed, 612 insertions(+), 317 deletions(-) create mode 100644 test-data/zonefiles/rfc5155-appendix-A.zone diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index 1e4fb006b..3e551b43f 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -110,6 +110,10 @@ impl Nsec3 { &self.types } + pub fn set_types(&mut self, types: RtypeBitmap) { + self.types = types; + } + pub(super) fn convert_octets( self, ) -> Result, Target::Error> @@ -403,6 +407,12 @@ where //------------ Nsec3Param ---------------------------------------------------- +// https://datatracker.ietf.org/doc/html/rfc5155#section-3.2 +// 3.2. NSEC3 RDATA Wire Format +// "Flags field is a single octet, the Opt-Out flag is the least significant +// bit" +const NSEC3_OPT_OUT_FLAG_MASK: u8 = 0b0000_0001; + #[derive(Clone)] #[cfg_attr( feature = "serde", @@ -418,9 +428,55 @@ where )) )] pub struct Nsec3param { + /// https://www.rfc-editor.org/rfc/rfc5155.html#section-3.1.1 + /// 3.1.1. Hash Algorithm + /// "The Hash Algorithm field identifies the cryptographic hash + /// algorithm used to construct the hash-value." hash_algorithm: Nsec3HashAlg, + + /// https://www.rfc-editor.org/rfc/rfc5155.html#section-3.1.2 + /// 3.1.2. Flags + /// "The Flags field contains 8 one-bit flags that can be used to + /// indicate different processing. All undefined flags must be zero. + /// The only flag defined by this specification is the Opt-Out flag." + /// + /// 3.1.2.1. Opt-Out Flag + /// "If the Opt-Out flag is set, the NSEC3 record covers zero or more + /// unsigned delegations. + /// + /// If the Opt-Out flag is clear, the NSEC3 record covers zero unsigned + /// delegations. + /// + /// The Opt-Out Flag indicates whether this NSEC3 RR may cover unsigned + /// delegations. It is the least significant bit in the Flags field. + /// See Section 6 for details about the use of this flag." flags: u8, + + /// https://www.rfc-editor.org/rfc/rfc5155.html#section-3.1.3 + /// 3.1.3. Iterations + /// "The Iterations field defines the number of additional times the + /// hash function has been performed. More iterations result in + /// greater resiliency of the hash value against dictionary attacks, + /// but at a higher computational cost for both the server and + /// resolver. See Section 5 for details of the use of this field, and + /// Section 10.3 for limitations on the value." + /// + /// https://www.rfc-editor.org/rfc/rfc9276.html#section-3.1 + /// 3.1. Best Practice for Zone Publishers + /// "If NSEC3 must be used, then an iterations count of 0 MUST be used + /// to alleviate computational burdens." iterations: u16, + + /// https://datatracker.ietf.org/doc/html/rfc5155#section-3.1.5 + /// 3.1.5. Salt + /// "The Salt field is appended to the original owner name before + /// hashing in order to defend against pre-calculated dictionary + /// attacks." + /// + /// https://www.rfc-editor.org/rfc/rfc9276.html#section-3.1 + /// 3.1. Best Practice for Zone Publishers + /// "Operators SHOULD NOT use a salt by indicating a zero-length salt + /// value instead (represented as a "-" in the presentation format)." salt: Nsec3Salt, } @@ -452,6 +508,14 @@ impl Nsec3param { self.flags } + pub fn set_opt_out_flag(&mut self) { + self.flags |= NSEC3_OPT_OUT_FLAG_MASK; + } + + pub fn opt_out_flag(&self) -> bool { + self.flags & NSEC3_OPT_OUT_FLAG_MASK == NSEC3_OPT_OUT_FLAG_MASK + } + pub fn iterations(&self) -> u16 { self.iterations } diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index 02894dce4..a3370244d 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -348,15 +348,15 @@ mod tests { assert_eq!( nsecs, [ - mk_nsec_rr("example.", "a.example", "NS SOA MX RRSIG NSEC"), - mk_nsec_rr("a.example.", "ai.example", "NS DS RRSIG NSEC"), + mk_nsec_rr("example.", "a.example.", "NS SOA MX RRSIG NSEC"), + mk_nsec_rr("a.example.", "ai.example.", "NS DS RRSIG NSEC"), mk_nsec_rr( "ai.example.", "b.example", "A HINFO AAAA RRSIG NSEC" ), - mk_nsec_rr("b.example.", "ns1.example", "NS RRSIG NSEC"), - mk_nsec_rr("ns1.example.", "ns2.example", "A RRSIG NSEC"), + mk_nsec_rr("b.example.", "ns1.example.", "NS RRSIG NSEC"), + mk_nsec_rr("ns1.example.", "ns2.example.", "A RRSIG NSEC"), // The next record also validates that we comply with // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when @@ -368,13 +368,13 @@ mod tests { // Next Domain Name field without any wildcard expansion. // [RFC4035] describes the impact of wildcards on // authenticated denial of existence." - mk_nsec_rr("ns2.example.", "*.w.example", "A RRSIG NSEC"), - mk_nsec_rr("*.w.example.", "x.w.example", "MX RRSIG NSEC"), - mk_nsec_rr("x.w.example.", "x.y.w.example", "MX RRSIG NSEC"), - mk_nsec_rr("x.y.w.example.", "xx.example", "MX RRSIG NSEC"), + mk_nsec_rr("ns2.example.", "*.w.example.", "A RRSIG NSEC"), + mk_nsec_rr("*.w.example.", "x.w.example.", "MX RRSIG NSEC"), + mk_nsec_rr("x.w.example.", "x.y.w.example.", "MX RRSIG NSEC"), + mk_nsec_rr("x.y.w.example.", "xx.example.", "MX RRSIG NSEC"), mk_nsec_rr( "xx.example.", - "example", + "example.", "A HINFO AAAA RRSIG NSEC" ) ], @@ -448,4 +448,26 @@ mod tests { assert!(nsec.data().types().contains(Rtype::RRSIG)); assert!(!nsec.data().types().contains(Rtype::A)); } + + #[test] + fn existing_nsec_records_are_ignored() { + let cfg = GenerateNsecConfig::default(); + + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + mk_nsec_rr("a.", "some_a.a.", "SOA NSEC"), + mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), + ]); + + let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec_rr("a.", "some_a.a.", "SOA DNSKEY RRSIG NSEC"), + mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), + ] + ); + } } diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 8faeb9703..7c0075d19 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -13,16 +13,15 @@ use tracing::{debug, trace}; use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; use crate::base::name::{ToLabelIter, ToName}; -use crate::base::{Name, NameBuilder, Record, Ttl}; +use crate::base::{CanonicalOrd, Name, NameBuilder, Record, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; use crate::sign::error::SigningError; -use crate::sign::records::{ - DefaultSorter, RecordsIter, SortedRecords, Sorter, -}; +use crate::sign::records::{DefaultSorter, RecordsIter, Sorter}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; +use std::collections::HashSet; //----------- GenerateNsec3Config -------------------------------------------- @@ -32,11 +31,55 @@ where HashProvider: Nsec3HashProvider, Octs: AsRef<[u8]> + From<&'static [u8]>, { + /// Whether to assume that the final zone will one or more DNSKEY RRs at + /// the apex. + /// + /// If true, an NSEC3 RR created for the zone apex according to these + /// config settings should have the DNSKEY bit _*SET*_ in the NSEC3 type + /// bitmap. + /// + /// If false, an NSEC3 RR created for the zone apex according to these + /// config settings should have the DNSKEY bit _*UNSET*_ in the NSEC3 type + /// bitmap. pub assume_dnskeys_will_be_added: bool, + + /// NSEC3 and NSEC3PARAM settings. + /// + /// Hash algorithm, flags, iterations and salt. pub params: Nsec3param, - pub opt_out: Nsec3OptOut, + + /// Whether to exclude owner names of unsigned delegations when Opt-Out + /// is being used. + /// + /// Some zone signing tools (e.g. ldns-signzone) set the NSEC3 Opt-Out + /// flag but still include insecure delegations in the NSEC3 chain. + /// + /// This is possible because RFC 5155 section 7.1 says: + /// + /// https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 + /// 7.1. Zone Signing + /// ... + /// "If Opt-Out is being used, owner names of unsigned delegations MAY + /// be excluded." + /// + /// I.e. owner names of unsigned delegations MAY also NOT be excluded. + pub opt_out_exclude_owner_names_of_unsigned_delegations: bool, + + /// Which TTL value should be used for the NSEC3PARAM RR. pub nsec3param_ttl_mode: Nsec3ParamTtlMode, + + /// Which [`Nsec3HashProvider`] impl should be used to generate NSEC3 + /// hashes. + /// + /// By default the [`OnDemandNsec3HashProvider`] impl is used. + /// + /// Users may override this with their own impl. The primary use case + /// evisioned for this is to track the relationship between the original + /// owner names and the hashes generated for them in order to be able to + /// output diagnostic information about generated NSEC3 RRs for diagnostic + /// purposes. pub hash_provider: HashProvider, + _phantom: PhantomData<(N, Sort)>, } @@ -48,15 +91,14 @@ where { pub fn new( params: Nsec3param, - opt_out: Nsec3OptOut, hash_provider: HashProvider, ) -> Self { Self { assume_dnskeys_will_be_added: true, params, - opt_out, hash_provider, nsec3param_ttl_mode: Default::default(), + opt_out_exclude_owner_names_of_unsigned_delegations: true, _phantom: Default::default(), } } @@ -66,6 +108,18 @@ where self } + pub fn with_opt_out(mut self) -> Self { + self.params.set_opt_out_flag(); + self + } + + pub fn without_opt_out_excluding_owner_names_of_unsigned_delegations( + mut self, + ) -> Self { + self.opt_out_exclude_owner_names_of_unsigned_delegations = true; + self + } + pub fn without_assuming_dnskeys_will_be_added(mut self) -> Self { self.assume_dnskeys_will_be_added = false; self @@ -94,8 +148,9 @@ where Self { assume_dnskeys_will_be_added: true, params, - opt_out: Default::default(), nsec3param_ttl_mode: Default::default(), + opt_out_exclude_owner_names_of_unsigned_delegations: + Default::default(), hash_provider, _phantom: Default::default(), } @@ -137,31 +192,19 @@ where { // TODO: // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) - // - RFC 5155 section 2 Backwards compatibility: Reject old algorithms? - // if not, map 3 to 6 and 5 to 7, or reject use of 3 and 5? // RFC 5155 7.1 step 2: // "If Opt-Out is being used, set the Opt-Out bit to one." - let mut nsec3_flags = config.params.flags(); - if matches!( - config.opt_out, - Nsec3OptOut::OptOut | Nsec3OptOut::OptOutFlagsOnly - ) { - // Set the Opt-Out flag. - nsec3_flags |= 0b0000_0001; - } + let exclude_owner_names_of_unsigned_delegations = + config.params.opt_out_flag() + && config.opt_out_exclude_owner_names_of_unsigned_delegations; - // RFC 5155 7.1 step 5: - // "Sort the set of NSEC3 RRs into hash order." We store the NSEC3s as - // we create them and sort them afterwards. let mut nsec3s = Vec::>>::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; - // We also need the apex for the last NSEC3. let first_rr = records.first(); let apex_owner = first_rr.owner().clone(); let apex_label_count = apex_owner.iter_labels().count(); @@ -170,6 +213,9 @@ where let mut ttl = None; let mut nsec3param_ttl = None; + // RFC 5155 7.1 step 2 + // For each unique original owner name in the zone add an NSEC3 RR. + for owner_rrs in records { trace!("Owner: {}", owner_rrs.owner()); @@ -223,7 +269,10 @@ where // even when Opt-Out is not being used because we also need to know // there at a later step. let has_ds = owner_rrs.records().any(|rec| rec.rtype() == Rtype::DS); - if config.opt_out == Nsec3OptOut::OptOut && cut.is_some() && !has_ds { + if exclude_owner_names_of_unsigned_delegations + && cut.is_some() + && !has_ds + { debug!("Excluding owner {} as it is an insecure delegation (lacks a DS RR) and opt-out is enabled",owner_rrs.owner()); continue; } @@ -437,7 +486,7 @@ where &name, &mut config.hash_provider, config.params.hash_algorithm(), - nsec3_flags, + config.params.flags(), config.params.iterations(), config.params.salt(), &apex_owner, @@ -465,7 +514,7 @@ where &name, &mut config.hash_provider, config.params.hash_algorithm(), - nsec3_flags, + config.params.flags(), config.params.iterations(), config.params.salt(), &apex_owner, @@ -478,32 +527,131 @@ where nsec3s.push(rec); } + // RFC 5155 7.1 step 5: + // "Sort the set of NSEC3 RRs into hash order." + // RFC 5155 7.1 step 7: // "In each NSEC3 RR, insert the next hashed owner name by using the // value of the next NSEC3 RR in hash order. The next hashed owner // name of the last NSEC3 RR in the zone contains the value of the // hashed owner name of the first NSEC3 RR in the hash order." trace!("Sorting NSEC3 RRs"); - let mut nsec3s = SortedRecords::, Sort>::from(nsec3s); - let num_nsec3s = nsec3s.len(); - for i in 1..=num_nsec3s { - // TODO: Detect duplicate hashes. - let next_i = if i == num_nsec3s { 0 } else { i }; - let cur_owner = nsec3s.as_ref()[next_i].owner(); - let name: Name = cur_owner.try_to_name().unwrap(); - let label = name.iter_labels().next().unwrap(); - let owner_hash = if let Ok(hash_octets) = - base32::decode_hex(&format!("{label}")) + nsec3s.sort_by(CanonicalOrd::canonical_cmp); + nsec3s.dedup(); + + let mut iter = nsec3s.iter_mut().peekable(); + + while let Some(nsec3) = iter.next() { + // Replace the owner name of this NSEC3 RR with the NSEC3 hashed name + // of the next NSEC3 RR. + + // Save a mutable reference to the NSEC3 we currently iterated to as + // we will move the iterator ahead if subsequent NSEC3s have the same + // hashed owner name as this one. + let this_nsec3 = nsec3; + + // RFC 5155 7.2 step 6: + // "Combine NSEC3 RRs with identical hashed owner names by replacing + // them with a single NSEC3 RR with the Type Bit Maps field + // consisting of the union of the types represented by the set of + // NSEC3 RRs. If the original owner name was tracked, then + // collisions may be detected when combining, as all of the + // matching NSEC3 RRs should have the same original owner name. + // Discard any possible temporary NSEC3 RRs." + // + // Note: In mk_nsec3() the original owner name was stored as the + // placeholder next owner name in the generated NSEC3 record. + + let next = if iter.peek().is_some() { + let mut merged_rtypes = None; + + while let Some(next_nsec3) = iter.peek() { + if next_nsec3.owner() == this_nsec3.owner() { + // NSEC3 RR found with identical hashed owner name. + // Is the saved original owner name different to ours? If + // so that means that a hash collision occurred. + if this_nsec3.data().next_owner() + != next_nsec3.data().next_owner() + { + // Collision! + // RFC 5155 7.2: + // "If a hash collision is detected, then a new salt + // has to be chosen, and the signing process + // restarted." + todo!() + } + + if merged_rtypes.is_none() { + merged_rtypes = Some( + this_nsec3 + .data() + .types() + .iter() + .collect::>(), + ); + } + + // Combine its Type Bit Maps field into ours. + merged_rtypes + .as_mut() + .unwrap() + .extend(next_nsec3.data().types().iter()); + iter.next(); + } else { + break; + } + } + + if let Some(merged_rtypes) = merged_rtypes { + let mut types = RtypeBitmap::::builder(); + for rtype in merged_rtypes { + types + .add(rtype) + .map_err(|_| Nsec3HashError::AppendError)?; + } + this_nsec3.data_mut().set_types(types.finalize()); + } + + if let Some(next) = iter.peek() { + next + } else { + break; + } + } else { + break; + }; + + // Handle the this -> next case. + let next_name: Name = next.owner().try_to_name().unwrap(); + let next_first_label = next_name.iter_labels().next().unwrap(); + let next_owner_hash = if let Ok(hash_octets) = + base32::decode_hex(&format!("{next_first_label}")) { OwnerHash::::from_octets(hash_octets).unwrap() } else { - OwnerHash::::from_octets(name.as_octets().clone()).unwrap() + OwnerHash::::from_octets(next_name.as_octets().clone()) + .unwrap() }; - let last_rec = &mut nsec3s.as_mut_slice()[i - 1]; - let last_nsec3: &mut Nsec3 = last_rec.data_mut(); - last_nsec3.set_next_owner(owner_hash.clone()); + this_nsec3.data_mut().set_next_owner(next_owner_hash); } + // Handle the last -> first case. + let next_name: Name = nsec3s[0].owner().try_to_name().unwrap(); + let next_first_label = next_name.iter_labels().next().unwrap(); + let next_owner_hash = if let Ok(hash_octets) = + base32::decode_hex(&format!("{next_first_label}")) + { + OwnerHash::::from_octets(hash_octets).unwrap() + } else { + OwnerHash::::from_octets(next_name.as_octets().clone()).unwrap() + }; + nsec3s + .iter_mut() + .last() + .unwrap() + .data_mut() + .set_next_owner(next_owner_hash); + let Some(nsec3param_ttl) = nsec3param_ttl else { return Err(SigningError::SoaRecordCouldNotBeDetermined); }; @@ -528,7 +676,7 @@ where // // Handled above. - Ok(Nsec3Records::new(nsec3s.into_inner(), nsec3param)) + Ok(Nsec3Records::new(nsec3s, nsec3param)) } // unhashed_owner_name_is_ent is used to signal that the unhashed owner name @@ -565,9 +713,16 @@ where // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." - // Create a placeholder next owner, we'll fix it later. - let placeholder_next_owner = - OwnerHash::::from_octets(Octs::default()).unwrap(); + // Create a placeholder next owner, we'll fix it later. To enable + // detection of collisions we use the original ower name as the + // placeholder value. + let placeholder_next_owner = OwnerHash::::from_octets( + name.try_to_name::() + .map_err(|_| Nsec3HashError::AppendError)? + .as_octets() + .clone(), + ) + .unwrap(); // Create an NSEC3 record. let nsec3 = Nsec3::new( @@ -628,25 +783,6 @@ where Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) } -//------------ Nsec3OptOut --------------------------------------------------- - -/// The different types of NSEC3 opt-out. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub enum Nsec3OptOut { - /// No opt-out. The opt-out flag of NSEC3 RRs will NOT be set and insecure - /// delegations will be included in the NSEC3 chain. - #[default] - NoOptOut, - - /// Opt-out. The opt-out flag of NSEC3 RRs will be set and insecure - /// delegations will NOT be included in the NSEC3 chain. - OptOut, - - /// Opt-out (flags only). The opt-out flag of NSEC3 RRs will be set and - /// insecure delegations will be included in the NSEC3 chain. - OptOutFlagsOnly, -} - //------------ Nsec3HashProvider --------------------------------------------- pub trait Nsec3HashProvider { @@ -800,12 +936,41 @@ impl Nsec3Records { #[cfg(test)] mod tests { + // Note: These tests are similar to the nsec.rs unit tests but with two + // key differences: + // + // 1. Unlike NSEC which set the bit for the NSEC RTYPE in the NSEC type + // bitmap, with NSEC3 the NSEC bit is never set and so NSEC in type + // bitmap tests is not replaced by NSEC3 in these tests but is + // instead removed from the expected type bitmap. + // + // 2. With NSEC the NSEC RRs are added at the same owner name as the + // covered records and so it is easy to write out by hand the + // expected RRs in DNSSEC canonical order. With NSEC3 however the + // NSEC3 RRs are added at an owner name that is based on a hash of + // the original owner name, and rather than include these long + // unreadable hashes in the expected RR names we instead decide that + // it is not the responsibility of these tests to verify that NSEC3 + // hash generation is correct (that belongs with the code that + // generates NSEC3 hashes which is not done in this module at the + // time of writing), and so instead we generate the hashes during + // test execution and only refer to the unhashed names when defining + // the expected test records. As it is also not part of this module + // to correctly order NSEC3 recods by DNSSEC canonical order, we also + // assume that that ordering is applied correctly and so choose to + // define the correct order of expected NSEC3 records by letting + // SortedRecords sort them by hashed owner name in DNSSEC canonical + // order for us. + + use bytes::Bytes; use pretty_assertions::assert_eq; use crate::sign::records::SortedRecords; use crate::sign::test_util::*; + use crate::zonetree::StoredName; use super::*; + use core::str::FromStr; #[test] fn soa_is_required() { @@ -929,216 +1094,165 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); } - // TODO: Repeat above test but with opt out enabled. Or add a separate opt - // test. - - // #[test] - // fn expect_dnskeys_at_the_apex() { - // let mut cfg = GenerateNsec3Config::default(); - - // let mut records = - // SortedRecords::::default(); - - // records.insert(mk_soa_rr("a.", "b.", "c.")).unwrap(); - // records.insert(mk_a_rr("some_a.a.")).unwrap(); - - // let generated_records = - // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - - // assert_eq!( - // generated_records.nsec3s, - // [ - // mk_nsec3_rr( - // "a.", - // "a.", - // "some_a.a.", - // "SOA DNSKEY RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "a.", - // "some_a.a.", - // "a.", - // "A RRSIG NSEC", - // &mut cfg - // ), - // ] - // ); - // } - - // #[test] - // fn rfc_4034_and_9077_compliant() { - // let mut cfg = GenerateNsec3Config::default() - // .without_assuming_dnskeys_will_be_added(); - - // // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A - // let zonefile = include_bytes!( - // "../../../test-data/zonefiles/rfc4035-appendix-A.zone" - // ); - - // let records = bytes_to_records(&zonefile[..]); - // let generated_records = - // generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - - // assert_eq!(generated_records.nsec3s.len(), 10); - - // assert_eq!( - // generated_records.nsec3s, - // [ - // mk_nsec3_rr( - // "example.", - // "example.", - // "a.example", - // "NS SOA MX RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "a.example.", - // "ai.example", - // "NS DS RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "ai.example.", - // "b.example", - // "A HINFO AAAA RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "b.example.", - // "ns1.example", - // "NS RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "ns1.example.", - // "ns2.example", - // "A RRSIG NSEC", - // &mut cfg - // ), - // // The next record also validates that we comply with - // // https://datatracker.ietf.org/doc/html/rfc4034#section-6.2 - // // 4.1.3. "Inclusion of Wildcard Names in NSEC RDATA" when - // // it says: - // // "If a wildcard owner name appears in a zone, the wildcard - // // label ("*") is treated as a literal symbol and is treated - // // the same as any other owner name for the purposes of - // // generating NSEC RRs. Wildcard owner names appear in the - // // Next Domain Name field without any wildcard expansion. - // // [RFC4035] describes the impact of wildcards on - // // authenticated denial of existence." - // mk_nsec3_rr( - // "example.", - // "ns2.example.", - // "*.w.example", - // "A RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "*.w.example.", - // "x.w.example", - // "MX RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "x.w.example.", - // "x.y.w.example", - // "MX RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "x.y.w.example.", - // "xx.example", - // "MX RRSIG NSEC", - // &mut cfg - // ), - // mk_nsec3_rr( - // "example.", - // "xx.example.", - // "example", - // "A HINFO AAAA RRSIG NSEC", - // &mut cfg - // ) - // ], - // ); - - // // TTLs are not compared by the eq check above so check them - // // explicitly now. - // // - // // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // // the MINIMUM field of the SOA record and the TTL of the SOA itself". - // // - // // So in our case that is min(1800, 3600) = 1800. - // for nsec3 in &generated_records.nsec3s { - // assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); - // } - - // // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 - // // 2.3. Including NSEC RRs in a Zone - // // ... - // // "The type bitmap of every NSEC resource record in a signed zone - // // MUST indicate the presence of both the NSEC record itself and its - // // corresponding RRSIG record." - // for nsec3 in &generated_records.nsec3s { - // assert!(nsec3.data().types().contains(Rtype::NSEC)); - // assert!(nsec3.data().types().contains(Rtype::RRSIG)); - // } - - // // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 - // // 4.1.2. The Type Bit Maps Field - // // "Bits representing pseudo-types MUST be clear, as they do not - // // appear in zone data." - // // - // // There is nothing to test for this as it is excluded at the Rust - // // type system level by the generate_nsecs() function taking - // // ZoneRecordData (which excludes pseudo record types) as input rather - // // than AllRecordData (which includes pseudo record types). - - // // https://rfc-annotations.research.icann.org/rfc4034.html#section-4.1.1 - // // 4.1.2. The Type Bit Maps Field - // // ... - // // "A zone MUST NOT include an NSEC RR for any domain name that only - // // holds glue records." - // // - // // The "rfc4035-appendix-A.zone" file that we load contains glue A - // // records for ns1.example, ns1.a.example, ns1.b.example, ns2.example - // // and ns2.a.example all with no other record types at that name. We - // // can verify that an NSEC RR was NOT created for those that are not - // // within the example zone as we are not authoritative for thos. - // assert!(contains_owner(&generated_records.nsec3s, "ns1.example.")); - // assert!(!contains_owner(&generated_records.nsec3s, "ns1.a.example.")); - // assert!(!contains_owner(&generated_records.nsec3s, "ns1.b.example.")); - // assert!(contains_owner(&generated_records.nsec3s, "ns2.example.")); - // assert!(!contains_owner(&generated_records.nsec3s, "ns2.a.example.")); - - // // https://rfc-annotations.research.icann.org/rfc4035.html#section-2.3 - // // 2.3. Including NSEC RRs in a Zone - // // ... - // // "The bitmap for the NSEC RR at a delegation point requires special - // // attention. Bits corresponding to the delegation NS RRset and any - // // RRsets for which the parent zone has authoritative data MUST be - // // set; bits corresponding to any non-NS RRset for which the parent - // // is not authoritative MUST be clear." - // // - // // The "rfc4035-appendix-A.zone" file that we load has been modified - // // compared to the original to include a glue A record at b.example. - // // We can verify that an NSEC RR was NOT created for that name. - // let name = mk_name("b.example."); - // let nsec3 = generated_records - // .nsec3s - // .iter() - // .find(|rr| rr.owner() == &name) - // .unwrap(); - // assert!(nsec3.data().types().contains(Rtype::NSEC)); - // assert!(nsec3.data().types().contains(Rtype::RRSIG)); - // assert!(!nsec3.data().types().contains(Rtype::A)); - // } + // TODO: Test Opt-Out. + + #[test] + fn expect_dnskeys_at_the_apex() { + let mut cfg = GenerateNsec3Config::default(); + + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); + + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "a.", + "a.", + "some_a.a.", + "SOA DNSKEY RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr("a.", "some_a.a.", "a.", "A RRSIG", &cfg), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } + + #[test] + fn rfc_5155_and_9077_compliant() { + let nsec3params = Nsec3param::new( + Nsec3HashAlg::SHA1, + 1, + 12, + Nsec3Salt::from_str("aabbccdd").unwrap(), + ); + let mut cfg = GenerateNsec3Config::< + StoredName, + Bytes, + OnDemandNsec3HashProvider, + DefaultSorter, + >::new( + nsec3params.clone(), + OnDemandNsec3HashProvider::new( + nsec3params.hash_algorithm(), + nsec3params.iterations(), + nsec3params.salt().clone(), + ), + ) + .without_assuming_dnskeys_will_be_added(); + + // See https://datatracker.ietf.org/doc/html/rfc5155#appendix-A + let zonefile = include_bytes!( + "../../../test-data/zonefiles/rfc5155-appendix-A.zone" + ); + + let records = bytes_to_records(&zonefile[..]); + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // Generate the expected NSEC3 RRs, with placeholder next owner hashes + // as the next owner hashes have to be in DNSSEC canonical order which + // we can't know before generating the hashes (which is why we + // pre-generate the hashes above). + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_precalculated_nsec3_rr( + "t644ebqk9bibcna874givr6joj62mlhv.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", + "A HINFO AAAA RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", + "t644ebqk9bibcna874givr6joj62mlhv", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", + "r53bq7cc2uvmubfu5ocmm6pers9tk9en", + "A RRSIG", + &cfg, + ), + // Unlike NSEC, with NSEC3 empty non-terminals must also have + // NSEC3 RRs: + // + // https://www.rfc-editor.org/rfc/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "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." + // + // ENT NSEC3 RRs have an empty Type Bit Map. + mk_precalculated_nsec3_rr( + "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", + "q04jkcevqvmu85r014c7dkba38o0ji5r", + "", + &cfg, + ), + mk_precalculated_nsec3_rr( + "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", + "k8udemvp1j2f7eg6jebps17vp3n8i58h", + "", + &cfg, + ), + mk_precalculated_nsec3_rr( + "gjeqe526plbf1g8mklp59enfd789njgi.example.", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", + "A HINFO AAAA RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "b4um86eghhds6nea196smvmlo4ors995.example.", + "gjeqe526plbf1g8mklp59enfd789njgi", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", + "b4um86eghhds6nea196smvmlo4ors995", + "NS DS RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "2vptu5timamqttgl4luu9kg21e0aor3s.example.", + "35mthgpgcu1qg68fab165klnsnk3dpvl", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", + "2vptu5timamqttgl4luu9kg21e0aor3s", + "A RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "2t7b4g4vsa5smi47k61mv5bv1a22bojr", + "NS SOA MX RRSIG NSEC3PARAM", + &cfg, + ), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + + let expected_nsec3param = mk_nsec3param_rr("example.", &cfg); + assert_eq!(generated_records.nsec3param, expected_nsec3param); + + // TTLs are not compared by the eq check above so check them + // explicitly now. + // + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + // + // So in our case that is min(1800, 3600) = 1800. + for nsec3 in &generated_records.nsec3s { + assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); + } + } } diff --git a/src/sign/mod.rs b/src/sign/mod.rs index acb5bdcdf..eaf0675cf 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -60,9 +60,9 @@ //! [`generate_rrsigs()`] and [`sign_rrset()`] functions. //! - To generate signatures for arbitrary data see the [`SignRaw`] trait. //! -//! # Known limitations +//! # Limitations //! -//! This module does not yet support : +//! This module does not yet support: //! - `async` signing (useful for interacting with cryptographic hardware like //! Hardware Security Modules (HSMs)). //! - Re-signing an already signed zone, only unsigned zones can be signed. @@ -72,6 +72,9 @@ //! [`Record`]s, only signing of slices is supported. //! - Signing with both `NSEC` and `NSEC3` or multiple `NSEC3` configurations //! at once. +//! - Rewriting the DNSKEY RR algorithm identifier when using NSEC3 with the +//! older DSA or RSASHA1 algorithms (which is anyway only possible at +//! present if you bring your own cryptography). //! //! [`common`]: crate::sign::crypto::common //! [`keyset`]: crate::sign::keys::keyset @@ -300,46 +303,54 @@ where /// as they handle the construction of the [`SignableZoneInOut`] type and /// calling of this function for you. /// +/// # Requirements +/// /// The record collection to be signed is required to implement the /// [`SignableZone`] trait. The collection to extend with generated records is /// required to implement the [`SortedExtend`] trait, implementations of which /// are provided for the [`SortedRecords`] and [`Vec`] types. /// -///
-/// /// The record collection to be signed must meet the following requirements. -/// /// Failure to meet these requirements will likely lead to incorrect signing /// output. /// /// 1. The record collection to be signed **MUST** be ordered according to -/// [`CanonicalOrd`]. +/// [`CanonicalOrd`]. This is always true for [`SortedRecords`]. /// 2. The record collection to be signed **MUST** be unsigned, i.e. must not /// contain `DNSKEY`, `NSEC`, `NSEC3`, `NSEC3PARAM`, or `RRSIG` records. /// -/// [`SortedRecords`] will be sorted at all times and thus is safe to use with -/// this function. [`Vec`] however is safe to use **ONLY IF** the content has -/// been sorted prior to calling this function. +///
+/// +/// When using a type other than [`SortedRecords`] as input to this function +/// you **MUST** be sure that its content is already sorted according to +/// [`CanonicalOrd`] prior to calling this function. /// -/// This function does **NOT** yet support re-signing, i.e. re-generating -/// expired `RRSIG` signatures, updating the NSEC(3) chain to match added or -/// removed records or adding signatures for another key to an already signed -/// zone e.g. to support key rollover. For the latter case it does however -/// support providing multiple sets of key to sign with the -/// [`SigningKeyUsageStrategy`] implementation being used to determine which -/// keys to use to sign which records. +///
/// -/// This function does **NOT** yet support signing with multiple NSEC(3) -/// configurations at once, e.g. to migrate from NSEC <-> NSEC3 or between -/// NSEC3 configurations. +/// # Limitations /// -/// This function does **NOT** yet support signing of record collections -/// stored in the [`Zone`] type as it currently only support signing of record -/// slices whereas the records in a [`Zone`] currently only supports a visitor -/// style read interface via [`ReadableZone`] whereby a callback function is -/// invoked for each node that is "walked". +/// This function does not yet support: /// -///
+/// - Enforcement of [RFC 5155 section 2 Backwards Compatibility] regarding +/// use of NSEC3 algorithm aliases in DNSKEY RRs. +/// +/// - Re-signing, i.e. re-generating expired `RRSIG` signatures, updating the +/// NSEC(3) chain to match added or removed records or adding signatures for +/// another key to an already signed zone e.g. to support key rollover. For +/// the latter case it does however support providing multiple sets of key +/// to sign with the [`SigningKeyUsageStrategy`] implementation being used +/// to determine which keys to use to sign which records. +/// +/// - Signing with multiple NSEC(3) configurations at once, e.g. to migrate +/// from NSEC <-> NSEC3 or between NSEC3 configurations. +/// +/// - Signing of record collections stored in the [`Zone`] type as it +/// currently only support signing of record slices whereas the records in a +/// [`Zone`] currently only supports a visitor style read interface via +/// [`ReadableZone`] whereby a callback function is invoked for each node +/// that is "walked". +/// +/// # Configuration /// /// Various aspects of the signing process are configurable, see /// [`SigningConfig`] for more information. @@ -348,6 +359,8 @@ where /// [RFC 4035 section 2 Zone Signing]: /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2 /// [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 +/// [RFC 5155 section 2 Backwards Compatibility]: +/// https://www.rfc-editor.org/rfc/rfc5155.html#section-2 /// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace /// [`SortedRecords`]: crate::sign::records::SortedRecords /// [`Zone`]: crate::zonetree::Zone diff --git a/src/sign/records.rs b/src/sign/records.rs index ed70ad64c..0e942ee89 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -233,10 +233,6 @@ where self.records.iter() } - pub(super) fn as_mut_slice(&mut self) -> &mut [Record] { - self.records.as_mut_slice() - } - pub fn into_inner(self) -> Vec> { self.records } diff --git a/src/sign/test_util/mod.rs b/src/sign/test_util/mod.rs index 38e555699..1dbb2f923 100644 --- a/src/sign/test_util/mod.rs +++ b/src/sign/test_util/mod.rs @@ -12,7 +12,7 @@ use crate::base::name::FlattenInto; use crate::base::{Name, Record, Rtype, Serial, ToName, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; use crate::rdata::nsec3::OwnerHash; -use crate::rdata::{Dnskey, Ns, Nsec, Nsec3, Rrsig, Soa, A}; +use crate::rdata::{Dnskey, Ns, Nsec, Nsec3, Nsec3param, Rrsig, Soa, A}; use crate::sign::denial::nsec3::mk_hashed_nsec3_owner_name; use crate::utils::base32; use crate::validate::nsec3_hash; @@ -101,6 +101,18 @@ where mk_record(owner, Nsec::new(next_name, types).into()) } +pub(crate) fn mk_nsec3param_rr( + owner: &str, + cfg: &GenerateNsec3Config, +) -> Record +where + HP: Nsec3HashProvider, + N: FromStr + ToName + From>, + R: From>, +{ + mk_record(owner, cfg.params.clone().into()) +} + pub(crate) fn mk_nsec3_rr( apex_owner: &str, owner: &str, @@ -156,6 +168,38 @@ where ) } +pub(crate) fn mk_precalculated_nsec3_rr( + owner: &str, + next_owner: &str, + types: &str, + cfg: &GenerateNsec3Config, +) -> Record +where + HP: Nsec3HashProvider, + N: FromStr + ToName + From>, + ::Err: Debug, + R: From>, +{ + let mut builder = RtypeBitmap::::builder(); + for rtype in types.split_whitespace() { + builder.add(Rtype::from_str(rtype).unwrap()).unwrap(); + } + let types = builder.finalize(); + + mk_record( + owner, + Nsec3::new( + cfg.params.hash_algorithm(), + cfg.params.flags(), + cfg.params.iterations(), + cfg.params.salt().clone(), + OwnerHash::from_str(next_owner).unwrap(), + types, + ) + .into(), + ) +} + #[allow(clippy::too_many_arguments)] pub(crate) fn mk_rrsig_rr( owner: &str, diff --git a/test-data/zonefiles/rfc4035-appendix-A.zone b/test-data/zonefiles/rfc4035-appendix-A.zone index adbb4f7ab..f19de18aa 100644 --- a/test-data/zonefiles/rfc4035-appendix-A.zone +++ b/test-data/zonefiles/rfc4035-appendix-A.zone @@ -1,7 +1,7 @@ ; Extracted using ldns-readzone -s from the signed zone defined at -; https://datatracker.ietf.org/doc/html/rfc4035#appendix-A -; Keys have been replaced by newer algorithm 8 instead of older algorithm 5 -; which we do not support, and to match key pairs stored alongside this file. +; https://datatracker.ietf.org/doc/html/rfc4035#appendix-A +; DNSSEC RRs have been removed, e.g. DNSKEY, NSEC and RRSIG. +; The SOA MINIMUM has been changed from 3600 to 1800 for RFC 9077 testing. ; Contains one extra record compared to that defined in Appendix A of RFC ; 4035, b.example A, for additional testing. example. 3600 IN SOA ns1.example. bugs.x.w.example. 1081539377 3600 300 3600000 1800 diff --git a/test-data/zonefiles/rfc5155-appendix-A.zone b/test-data/zonefiles/rfc5155-appendix-A.zone new file mode 100644 index 000000000..3d37f03a6 --- /dev/null +++ b/test-data/zonefiles/rfc5155-appendix-A.zone @@ -0,0 +1,42 @@ +; Extracted using ldns-readzone -s from the signed zone defined at +; https://datatracker.ietf.org/doc/html/rfc5155#appendix-A +; Specifically from the errata concerning that example zone, available at: +; https://www.rfc-editor.org/errata/eid4993 +; And with the NSEC3 salt corrected from 'aabbccdd:1' to 'aabbccdd'. +; DNSSEC RRs have been removed, e.g. DNSKEY, NSEC3, NSE3PARAM and RRSIG. +; The SOA MINIMUM has been changed from 3600 to 1800 for RFC 9077 testing. +; H(example) = 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom +; H(a.example) = 35mthgpgcu1qg68fab165klnsnk3dpvl +; H(ai.example) = gjeqe526plbf1g8mklp59enfd789njgi +; H(ns1.example) = 2t7b4g4vsa5smi47k61mv5bv1a22bojr +; H(ns2.example) = q04jkcevqvmu85r014c7dkba38o0ji5r +; H(w.example) = k8udemvp1j2f7eg6jebps17vp3n8i58h +; H(*.w.example) = r53bq7cc2uvmubfu5ocmm6pers9tk9en +; H(x.w.example) = b4um86eghhds6nea196smvmlo4ors995 +; H(y.w.example) = ji6neoaepv8b5o6k4ev33abha8ht9fgc +; H(x.y.w.example) = 2vptu5timamqttgl4luu9kg21e0aor3s +; H(xx.example) = t644ebqk9bibcna874givr6joj62mlhv +example. 3600 IN SOA ns1.example. bugs.x.w.example. 1 3600 300 3600000 1800 +example. 3600 IN NS ns1.example. +example. 3600 IN NS ns2.example. +example. 3600 IN MX 1 xx.example. +a.example. 3600 IN NS ns1.a.example. +a.example. 3600 IN NS ns2.a.example. +a.example. 3600 IN DS 58470 5 1 3079f1593ebad6dc121e202a8b766a6a4837206c +ns1.a.example. 3600 IN A 192.0.2.5 +ns2.a.example. 3600 IN A 192.0.2.6 +ai.example. 3600 IN A 192.0.2.9 +ai.example. 3600 IN HINFO "KLH-10" "ITS" +ai.example. 3600 IN AAAA 2001:db8::f00:baa9 +c.example. 3600 IN NS ns1.c.example. +c.example. 3600 IN NS ns2.c.example. +ns1.c.example. 3600 IN A 192.0.2.7 +ns2.c.example. 3600 IN A 192.0.2.8 +ns1.example. 3600 IN A 192.0.2.1 +ns2.example. 3600 IN A 192.0.2.2 +*.w.example. 3600 IN MX 1 ai.example. +x.w.example. 3600 IN MX 1 xx.example. +x.y.w.example. 3600 IN MX 1 xx.example. +xx.example. 3600 IN A 192.0.2.10 +xx.example. 3600 IN HINFO "KLH-10" "TOPS-20" +xx.example. 3600 IN AAAA 2001:db8::f00:baaa From ae3605387921ee47ec0d120f3b1e7c58bc0af485 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:00:09 +0100 Subject: [PATCH 416/569] FIX: Inverted flag. --- src/sign/denial/nsec3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 7c0075d19..6d187e379 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -116,7 +116,7 @@ where pub fn without_opt_out_excluding_owner_names_of_unsigned_delegations( mut self, ) -> Self { - self.opt_out_exclude_owner_names_of_unsigned_delegations = true; + self.opt_out_exclude_owner_names_of_unsigned_delegations = false; self } From 1904364b4f1853b98420034ae16ba6a185b1816b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:00:26 +0100 Subject: [PATCH 417/569] Review feedback: Wrong comment. --- src/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d6815d143..c0ffc1f08 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -94,7 +94,7 @@ pub struct RrsigRecords where Octs: AsRef<[u8]>, { - /// The NSEC3 records. + /// The RRSIG records. pub rrsigs: Vec>>, /// The DNSKEY records. From 3c0746a227426d19b0320ae28974d81dcc3454b8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:04:20 +0100 Subject: [PATCH 418/569] FIX: Use the supplied sorter. --- src/sign/denial/nsec3.rs | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 6d187e379..1ffa513e0 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -530,26 +530,10 @@ where // RFC 5155 7.1 step 5: // "Sort the set of NSEC3 RRs into hash order." - // RFC 5155 7.1 step 7: - // "In each NSEC3 RR, insert the next hashed owner name by using the - // value of the next NSEC3 RR in hash order. The next hashed owner - // name of the last NSEC3 RR in the zone contains the value of the - // hashed owner name of the first NSEC3 RR in the hash order." trace!("Sorting NSEC3 RRs"); - nsec3s.sort_by(CanonicalOrd::canonical_cmp); + Sort::sort_by(&mut nsec3s, CanonicalOrd::canonical_cmp); nsec3s.dedup(); - let mut iter = nsec3s.iter_mut().peekable(); - - while let Some(nsec3) = iter.next() { - // Replace the owner name of this NSEC3 RR with the NSEC3 hashed name - // of the next NSEC3 RR. - - // Save a mutable reference to the NSEC3 we currently iterated to as - // we will move the iterator ahead if subsequent NSEC3s have the same - // hashed owner name as this one. - let this_nsec3 = nsec3; - // RFC 5155 7.2 step 6: // "Combine NSEC3 RRs with identical hashed owner names by replacing // them with a single NSEC3 RR with the Type Bit Maps field From 72b7785b766df3655a1b2e52660f0f01d31f8bb2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:06:36 +0100 Subject: [PATCH 419/569] - Remove NSEC3 type bit map merging as it is not necessary due to the requirement that the input be sorted. - Return an error on collision. - Add a collision test. - More unwrap -> Err. - More comments. --- src/sign/denial/nsec.rs | 2 +- src/sign/denial/nsec3.rs | 490 ++++++++++++++++++++++++++------------ src/sign/test_util/mod.rs | 11 +- 3 files changed, 349 insertions(+), 154 deletions(-) diff --git a/src/sign/denial/nsec.rs b/src/sign/denial/nsec.rs index a3370244d..cc5064537 100644 --- a/src/sign/denial/nsec.rs +++ b/src/sign/denial/nsec.rs @@ -331,7 +331,7 @@ mod tests { } #[test] - fn rfc_4034_and_9077_compliant() { + fn rfc_4034_appendix_a_and_rfc_9077_compliant() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 1ffa513e0..e645da32b 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -2,6 +2,7 @@ use core::cmp::min; use core::convert::From; use core::fmt::{Debug, Display}; use core::marker::{PhantomData, Send}; +use core::ops::Deref; use std::hash::Hash; use std::string::String; @@ -21,7 +22,6 @@ use crate::sign::error::SigningError; use crate::sign::records::{DefaultSorter, RecordsIter, Sorter}; use crate::utils::base32; use crate::validate::{nsec3_hash, Nsec3HashError}; -use std::collections::HashSet; //----------- GenerateNsec3Config -------------------------------------------- @@ -149,8 +149,7 @@ where assume_dnskeys_will_be_added: true, params, nsec3param_ttl_mode: Default::default(), - opt_out_exclude_owner_names_of_unsigned_delegations: - Default::default(), + opt_out_exclude_owner_names_of_unsigned_delegations: true, hash_provider, _phantom: Default::default(), } @@ -168,6 +167,11 @@ where /// - The `params` should be set to _"SHA-1, no extra iterations, empty salt"_ /// and zero flags. See [`Nsec3param::default()`]. /// +/// # Panics +/// +/// This function may panic if the input records are not sorted in DNSSEC +/// canonical order (see [`CanonicalOrd`]). +/// /// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html @@ -190,9 +194,6 @@ where HashProvider: Nsec3HashProvider, Sort: Sorter, { - // TODO: - // - Handle name collisions? (see RFC 5155 7.1 Zone Signing) - // RFC 5155 7.1 step 2: // "If Opt-Out is being used, set the Opt-Out bit to one." let exclude_owner_names_of_unsigned_delegations = @@ -534,108 +535,102 @@ where Sort::sort_by(&mut nsec3s, CanonicalOrd::canonical_cmp); nsec3s.dedup(); - // RFC 5155 7.2 step 6: - // "Combine NSEC3 RRs with identical hashed owner names by replacing - // them with a single NSEC3 RR with the Type Bit Maps field - // consisting of the union of the types represented by the set of - // NSEC3 RRs. If the original owner name was tracked, then - // collisions may be detected when combining, as all of the - // matching NSEC3 RRs should have the same original owner name. - // Discard any possible temporary NSEC3 RRs." - // - // Note: In mk_nsec3() the original owner name was stored as the - // placeholder next owner name in the generated NSEC3 record. - - let next = if iter.peek().is_some() { - let mut merged_rtypes = None; - - while let Some(next_nsec3) = iter.peek() { - if next_nsec3.owner() == this_nsec3.owner() { - // NSEC3 RR found with identical hashed owner name. - // Is the saved original owner name different to ours? If - // so that means that a hash collision occurred. - if this_nsec3.data().next_owner() - != next_nsec3.data().next_owner() - { - // Collision! - // RFC 5155 7.2: - // "If a hash collision is detected, then a new salt - // has to be chosen, and the signing process - // restarted." - todo!() - } - - if merged_rtypes.is_none() { - merged_rtypes = Some( - this_nsec3 - .data() - .types() - .iter() - .collect::>(), - ); - } - - // Combine its Type Bit Maps field into ours. - merged_rtypes - .as_mut() - .unwrap() - .extend(next_nsec3.data().types().iter()); - iter.next(); - } else { - break; - } - } - - if let Some(merged_rtypes) = merged_rtypes { - let mut types = RtypeBitmap::::builder(); - for rtype in merged_rtypes { - types - .add(rtype) - .map_err(|_| Nsec3HashError::AppendError)?; - } - this_nsec3.data_mut().set_types(types.finalize()); - } + // RFC 5155 7.2 step 6: + // "Combine NSEC3 RRs with identical hashed owner names by replacing + // them with a single NSEC3 RR with the Type Bit Maps field consisting + // of the union of the types represented by the set of NSEC3 RRs. If + // the original owner name was tracked, then collisions may be detected + // when combining, as all of the matching NSEC3 RRs should have the + // same original owner name. Discard any possible temporary NSEC3 RRs." + // + // ^^^ Combining isn't necessary in our implementation as the input zone + // is assumed to be sorted in DNSSEC canonical order and we created NSEC3 + // one owner name at a time already with all RTYPEs reflected in the type + // bit map. We do track the original owner name in order to detect + // collisions. We did not create temporary wildcard NSEC3s so have none to + // discard. + // + // TODO: Create temporary wildcard NSEC3s. See RFC 5155 section 7.1 step + // 4. + // + // Note: In mk_nsec3() the original owner name was stored as the + // placeholder next owner name in the generated NSEC3 record in order to + // detect hash collisions. + + // RFC 5155 7.1 step 7: + // "In each NSEC3 RR, insert the next hashed owner name by using the + // value of the next NSEC3 RR in hash order. The next hashed owner + // name of the last NSEC3 RR in the zone contains the value of the + // hashed owner name of the first NSEC3 RR in the hash order." + + // We don't walk over windows of size two (as that would require nightly + // Rust support) or keep a mutable reference to the previous NSEC3 (as + // simultaneous mutable references would anger the borrow checker). + // Instead we peek at the next and update the current, handling the final + // last -> first case separately. + + let only_one_nsec3 = nsec3s.len() == 1; + let first = nsec3s.iter().next().unwrap().clone(); + let mut iter = nsec3s.iter_mut().peekable(); + + while let Some(nsec3) = iter.next() { + // If we are at the end of the NSEC3 chain the next NSEC3 is the first + // NSEC3. + let next_nsec3 = if let Some(next) = iter.peek() { + next.deref() + } else { + &first + }; - if let Some(next) = iter.peek() { - next + // Each NSEC3 should have a unique owner name, as we already combined + // all RTYPEs into a single NSEC3 for a given owner name above. As the + // NSEC3s are sorted, if another NSEC3 has the same owner name but + // different RDATA it will be the next NSEC3 in the iterator. (a) this + // shouldn't happen, and (b) if it does it should only be because the + // original owner name of the two NSEC3s are different but hash to the + // same hashed owner name, i.e. there was a hash collision. If the + // next NSEC3 has a different hashed owner name it must have a + // different original owner name, the same owner name can't hash to two + // different values. If there is only one NSEC3 then it will point to + // itself and clearly the current and next will be the same so exclude + // that special case. + if !only_one_nsec3 && nsec3.owner() == next_nsec3.owner() { + if nsec3.data().next_owner() != next_nsec3.data().next_owner() { + return Err(Nsec3HashError::CollisionDetected)?; } else { - break; + // This shouldn't happen. Could it maybe happen if the input + // data were unsorted? + unreachable!("All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?"); } - } else { - break; - }; + } - // Handle the this -> next case. - let next_name: Name = next.owner().try_to_name().unwrap(); - let next_first_label = next_name.iter_labels().next().unwrap(); - let next_owner_hash = if let Ok(hash_octets) = - base32::decode_hex(&format!("{next_first_label}")) + // Replace the Next Hashed Owner Name of the current NSEC3 RR with the + // first label of the next NSEC3 RR owner name (which is itself an + // NSEC3 hash). + let next_owner_name: Name = next_nsec3 + .owner() + .try_to_name() + .map_err(|_| Nsec3HashError::AppendError)?; + + // SAFETY: We created the owner name by appending the zone apex owner + // name to an NSEC3 hash so by definition there must be two labels and + // it is safe to unwrap the first. + let first_label_of_next_owner_name = + next_owner_name.iter_labels().next().unwrap(); + let next_hashed_owner_name = if let Ok(hash_octets) = + base32::decode_hex(&format!("{first_label_of_next_owner_name}")) { OwnerHash::::from_octets(hash_octets).unwrap() } else { - OwnerHash::::from_octets(next_name.as_octets().clone()) - .unwrap() + // TODO: Why would an NSEC3 RR have an unhashed owner name? + OwnerHash::::from_octets( + next_owner_name.as_octets().clone(), + ) + .unwrap() }; - this_nsec3.data_mut().set_next_owner(next_owner_hash); + nsec3.data_mut().set_next_owner(next_hashed_owner_name); } - // Handle the last -> first case. - let next_name: Name = nsec3s[0].owner().try_to_name().unwrap(); - let next_first_label = next_name.iter_labels().next().unwrap(); - let next_owner_hash = if let Ok(hash_octets) = - base32::decode_hex(&format!("{next_first_label}")) - { - OwnerHash::::from_octets(hash_octets).unwrap() - } else { - OwnerHash::::from_octets(next_name.as_octets().clone()).unwrap() - }; - nsec3s - .iter_mut() - .last() - .unwrap() - .data_mut() - .set_next_owner(next_owner_hash); - let Some(nsec3param_ttl) = nsec3param_ttl else { return Err(SigningError::SoaRecordCouldNotBeDetermined); }; @@ -698,7 +693,7 @@ where // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." // Create a placeholder next owner, we'll fix it later. To enable - // detection of collisions we use the original ower name as the + // detection of collisions we use the original owner name as the // placeholder value. let placeholder_next_owner = OwnerHash::::from_octets( name.try_to_name::() @@ -706,7 +701,7 @@ where .as_octets() .clone(), ) - .unwrap(); + .map_err(|_| Nsec3HashError::OwnerHashError)?; // Create an NSEC3 record. let nsec3 = Nsec3::new( @@ -945,6 +940,7 @@ mod tests { // define the correct order of expected NSEC3 records by letting // SortedRecords sort them by hashed owner name in DNSSEC canonical // order for us. + use core::str::FromStr; use bytes::Bytes; use pretty_assertions::assert_eq; @@ -954,7 +950,6 @@ mod tests { use crate::zonetree::StoredName; use super::*; - use core::str::FromStr; #[test] fn soa_is_required() { @@ -1037,6 +1032,7 @@ mod tests { ]); assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + assert!(!generated_records.nsec3param.data().opt_out_flag()); } #[test] @@ -1076,10 +1072,9 @@ mod tests { ]); assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + assert!(!generated_records.nsec3param.data().opt_out_flag()); } - // TODO: Test Opt-Out. - #[test] fn expect_dnskeys_at_the_apex() { let mut cfg = GenerateNsec3Config::default(); @@ -1104,22 +1099,20 @@ mod tests { ]); assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + assert!(!generated_records.nsec3param.data().opt_out_flag()); } #[test] - fn rfc_5155_and_9077_compliant() { + fn rfc_5155_appendix_a_and_rfc_9077_compliant_plus_ents() { + // These NSEC3 settings match those of the NSEC3PARAM record shown in + // https://datatracker.ietf.org/doc/html/rfc5155#appendix-A. let nsec3params = Nsec3param::new( Nsec3HashAlg::SHA1, - 1, + 1, // opt-out 12, Nsec3Salt::from_str("aabbccdd").unwrap(), ); - let mut cfg = GenerateNsec3Config::< - StoredName, - Bytes, - OnDemandNsec3HashProvider, - DefaultSorter, - >::new( + let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( nsec3params.clone(), OnDemandNsec3HashProvider::new( nsec3params.hash_algorithm(), @@ -1138,27 +1131,91 @@ mod tests { let generated_records = generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - // Generate the expected NSEC3 RRs, with placeholder next owner hashes - // as the next owner hashes have to be in DNSSEC canonical order which - // we can't know before generating the hashes (which is why we - // pre-generate the hashes above). + // Generate the expected NSEC3 RRs. The hashes used match those listed + // in https://datatracker.ietf.org/doc/html/rfc5155#appendix-A and can + // be replicated by e.g. using a command such as: + // ldns-nsec3-hash -t 12 -s 'aabbccdd' xx.example. + // The records are listed in hash chain order, e.g. + // 0p9.. -> 2t7.. + // 2t7.. -> 2vp.. + // 2vp.. -> 35m.. + // + // https://datatracker.ietf.org/doc/html/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "The owner name of the NSEC3 RR is the hash of the original owner + // name, prepended as a single label to the zone name." + // + // E.g. the hash of example. computed with: + // ldns-nsec3-hash -t 12 -s 'aabbccdd' example. + // is: + // 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom. + // + // So the owner name of the NSEC3 RR for original owner example. is + // the hash value we just calculated "pre-pended as a single label to + // the zone name" with the zone name in this case being "example.", + // i.e.: + // 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example. + // + // Next we calculate the "next hashed owner name" like so: + // + // https://datatracker.ietf.org/doc/html/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "7. In each NSEC3 RR, insert the next hashed owner name by using + // the value of the next NSEC3 RR in hash order. The next hashed + // owner name of the last NSEC3 RR in the zone contains the value + // of the hashed owner name of the first NSEC3 RR in the hash + // order." + // + // The generated NSEC3s should be in hash order because we have to sort + // them that way anyway for the RFC 5155 algorithm: + // + // https://datatracker.ietf.org/doc/html/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // " 5. Sort the set of NSEC3 RRs into hash order." let expected_records = SortedRecords::<_, _>::from_iter([ mk_precalculated_nsec3_rr( - "t644ebqk9bibcna874givr6joj62mlhv.example.", - "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", - "A HINFO AAAA RRSIG", + // from: example. to: ns1.example. + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "2t7b4g4vsa5smi47k61mv5bv1a22bojr", + "NS SOA MX RRSIG NSEC3PARAM", &cfg, ), mk_precalculated_nsec3_rr( - "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", - "t644ebqk9bibcna874givr6joj62mlhv", + // from: ns1.example. to: x.y.w.example. + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", + "2vptu5timamqttgl4luu9kg21e0aor3s", + "A RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // from: x.y.w.example. to: a.example. + "2vptu5timamqttgl4luu9kg21e0aor3s.example.", + "35mthgpgcu1qg68fab165klnsnk3dpvl", "MX RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", - "r53bq7cc2uvmubfu5ocmm6pers9tk9en", - "A RRSIG", + // from: a.example to: x.w.example. + "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", + "b4um86eghhds6nea196smvmlo4ors995", + "NS DS RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // from: x.w.example. to: ai.example. + "b4um86eghhds6nea196smvmlo4ors995.example.", + "gjeqe526plbf1g8mklp59enfd789njgi", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // from: ai.example. to: y.w.example. + "gjeqe526plbf1g8mklp59enfd789njgi.example.", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", + "A HINFO AAAA RRSIG", &cfg, ), // Unlike NSEC, with NSEC3 empty non-terminals must also have @@ -1173,59 +1230,55 @@ mod tests { // // ENT NSEC3 RRs have an empty Type Bit Map. mk_precalculated_nsec3_rr( - "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", - "q04jkcevqvmu85r014c7dkba38o0ji5r", - "", - &cfg, - ), - mk_precalculated_nsec3_rr( + // from: y.w.example. to: w.example. "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", "k8udemvp1j2f7eg6jebps17vp3n8i58h", "", &cfg, ), mk_precalculated_nsec3_rr( - "gjeqe526plbf1g8mklp59enfd789njgi.example.", - "ji6neoaepv8b5o6k4ev33abha8ht9fgc", - "A HINFO AAAA RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - "b4um86eghhds6nea196smvmlo4ors995.example.", - "gjeqe526plbf1g8mklp59enfd789njgi", - "MX RRSIG", + // from: w.example. to: ns2.example. + "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", + "q04jkcevqvmu85r014c7dkba38o0ji5r", + "", &cfg, ), mk_precalculated_nsec3_rr( - "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", - "b4um86eghhds6nea196smvmlo4ors995", - "NS DS RRSIG", + // from: ns2.example. to: *.w.example. + "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", + "r53bq7cc2uvmubfu5ocmm6pers9tk9en", + "A RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "2vptu5timamqttgl4luu9kg21e0aor3s.example.", - "35mthgpgcu1qg68fab165klnsnk3dpvl", + // from: *.w.example. to: xx.example. + "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", + "t644ebqk9bibcna874givr6joj62mlhv", "MX RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", - "2vptu5timamqttgl4luu9kg21e0aor3s", - "A RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", - "2t7b4g4vsa5smi47k61mv5bv1a22bojr", - "NS SOA MX RRSIG NSEC3PARAM", + // from: xx.example. to: example. + "t644ebqk9bibcna874givr6joj62mlhv.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", + "A HINFO AAAA RRSIG", &cfg, ), ]); assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + // https://www.rfc-editor.org/rfc/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "8. Finally, add an NSEC3PARAM RR with the same Hash Algorithm, + // Iterations, and Salt fields to the zone apex." + // + // We don't actually add the NSEC3PARAM RR to the zone, instead we + // generate it so that the caller can do that. let expected_nsec3param = mk_nsec3param_rr("example.", &cfg); assert_eq!(generated_records.nsec3param, expected_nsec3param); + assert!(generated_records.nsec3param.data().opt_out_flag()); // TTLs are not compared by the eq check above so check them // explicitly now. @@ -1239,4 +1292,137 @@ mod tests { assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); } } + + #[test] + fn opt_out_with_exclusion() { + // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 + // 7.1. Zone Signing + // .. + // "Owner names that correspond to unsigned delegations MAY have a + // corresponding NSEC3 RR. However, if there is not a corresponding + // NSEC3 RR, there MUST be an Opt-Out NSEC3 RR that covers the + // "next closer" name to the delegation." + // + // This test tests opt-out with exclusion, i.e. opt-out that excludes + // an unsigned delegation and thus there "MUST be an Opt-Out NSEC3 + // RR...". + let mut cfg = GenerateNsec3Config::default() + .with_opt_out() + .without_assuming_dnskeys_will_be_added(); + + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), + ]); + + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + let expected_records = + SortedRecords::<_, _>::from_iter([mk_nsec3_rr( + "a.", + "a.", + "a.", + "SOA RRSIG NSEC3PARAM", + &cfg, + )]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + assert!(generated_records.nsec3param.data().opt_out_flag()); + } + + #[test] + fn opt_out_without_exclusion() { + // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 + // 7.1. Zone Signing + // .. + // "Owner names that correspond to unsigned delegations MAY have a + // corresponding NSEC3 RR. However, if there is not a corresponding + // NSEC3 RR, there MUST be an Opt-Out NSEC3 RR that covers the + // "next closer" name to the delegation." + // + // This test tests opt-out with_out_ exclusion, i.e. opt-out that + // creates an NSEC RR for an unsigned delegation. + let mut cfg = GenerateNsec3Config::default() + .with_opt_out() + .without_opt_out_excluding_owner_names_of_unsigned_delegations() + .without_assuming_dnskeys_will_be_added(); + + // This also tests the case of handling a single NSEC3 as only the SOA + // RR gets an NSEC3, the NS RR does not. + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), + ]); + + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "a.", + "a.", + "unsigned_delegation.a.", + "SOA RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr("a.", "unsigned_delegation.a.", "a.", "NS", &cfg), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + assert!(generated_records.nsec3param.data().opt_out_flag()); + } + + #[test] + #[should_panic( + expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?" + )] + fn generating_nsec3s_for_unordered_input_should_panic() { + let mut cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + + let records = vec![ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + mk_a_rr("some_b.a."), + mk_aaaa_rr("some_a.a."), + ]; + + let _res = generate_nsec3s(RecordsIter::new(&records), &mut cfg); + } + + #[test] + fn test_nsec3_hash_collision_handling() { + let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( + Nsec3param::default(), + CollidingHashProvider, + ); + + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); + + assert!(matches!( + generate_nsec3s(records.owner_rrs(), &mut cfg), + Err(SigningError::Nsec3HashingError( + Nsec3HashError::CollisionDetected + )) + )); + } + + //------------ Test helpers ---------------------------------------------- + + struct CollidingHashProvider; + + impl Nsec3HashProvider for CollidingHashProvider { + fn get_or_create( + &mut self, + _apex_owner: &StoredName, + _unhashed_owner_name: &StoredName, + _unhashed_owner_name_is_ent: bool, + ) -> Result { + Ok(StoredName::root()) + } + } } diff --git a/src/sign/test_util/mod.rs b/src/sign/test_util/mod.rs index 1dbb2f923..f57c45999 100644 --- a/src/sign/test_util/mod.rs +++ b/src/sign/test_util/mod.rs @@ -12,7 +12,9 @@ use crate::base::name::FlattenInto; use crate::base::{Name, Record, Rtype, Serial, ToName, Ttl}; use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; use crate::rdata::nsec3::OwnerHash; -use crate::rdata::{Dnskey, Ns, Nsec, Nsec3, Nsec3param, Rrsig, Soa, A}; +use crate::rdata::{ + Aaaa, Dnskey, Ns, Nsec, Nsec3, Nsec3param, Rrsig, Soa, A, +}; use crate::sign::denial::nsec3::mk_hashed_nsec3_owner_name; use crate::utils::base32; use crate::validate::nsec3_hash; @@ -54,6 +56,13 @@ where mk_record(owner, A::from_str("1.2.3.4").unwrap().into()) } +pub(crate) fn mk_aaaa_rr(owner: &str) -> Record +where + R: From, +{ + mk_record(owner, Aaaa::from_str("2001:db8::0").unwrap().into()) +} + pub(crate) fn mk_dnskey_rr( owner: &str, flags: u16, From daa61594d2951b1ffc41c09e377ba651dfd4ebc0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:13:20 +0100 Subject: [PATCH 420/569] Clippy. --- src/sign/denial/nsec3.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index e645da32b..40c62026a 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -570,7 +570,7 @@ where // last -> first case separately. let only_one_nsec3 = nsec3s.len() == 1; - let first = nsec3s.iter().next().unwrap().clone(); + let first = nsec3s.first().unwrap().clone(); let mut iter = nsec3s.iter_mut().peekable(); while let Some(nsec3) = iter.next() { @@ -596,7 +596,7 @@ where // that special case. if !only_one_nsec3 && nsec3.owner() == next_nsec3.owner() { if nsec3.data().next_owner() != next_nsec3.data().next_owner() { - return Err(Nsec3HashError::CollisionDetected)?; + Err(Nsec3HashError::CollisionDetected)?; } else { // This shouldn't happen. Could it maybe happen if the input // data were unsorted? @@ -953,6 +953,7 @@ mod tests { #[test] fn soa_is_required() { + init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = @@ -966,6 +967,7 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { + init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -981,6 +983,7 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { + init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -1037,6 +1040,7 @@ mod tests { #[test] fn occluded_records_are_ignored() { + init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -1077,6 +1081,7 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { + init_logging(); let mut cfg = GenerateNsec3Config::default(); let records = SortedRecords::<_, _>::from_iter([ @@ -1104,6 +1109,7 @@ mod tests { #[test] fn rfc_5155_appendix_a_and_rfc_9077_compliant_plus_ents() { + init_logging(); // These NSEC3 settings match those of the NSEC3PARAM record shown in // https://datatracker.ietf.org/doc/html/rfc5155#appendix-A. let nsec3params = Nsec3param::new( @@ -1295,6 +1301,7 @@ mod tests { #[test] fn opt_out_with_exclusion() { + init_logging(); // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 // 7.1. Zone Signing // .. @@ -1333,6 +1340,7 @@ mod tests { #[test] fn opt_out_without_exclusion() { + init_logging(); // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 // 7.1. Zone Signing // .. @@ -1378,6 +1386,7 @@ mod tests { expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?" )] fn generating_nsec3s_for_unordered_input_should_panic() { + init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); @@ -1393,6 +1402,7 @@ mod tests { #[test] fn test_nsec3_hash_collision_handling() { + init_logging(); let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( Nsec3param::default(), CollidingHashProvider, @@ -1425,4 +1435,13 @@ mod tests { Ok(StoredName::root()) } } + + fn init_logging() { + tracing_subscriber::fmt() + .with_max_level(tracing::level_filters::LevelFilter::TRACE) + .with_thread_ids(true) + .without_time() + .try_init() + .ok(); + } } From f372c9129d795de10f3e534ad8dace71610f04fd Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:14:00 +0100 Subject: [PATCH 421/569] Remove temporary init_logging() helper fn. --- src/sign/denial/nsec3.rs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 40c62026a..614d02d7e 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -953,7 +953,6 @@ mod tests { #[test] fn soa_is_required() { - init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = @@ -967,7 +966,6 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { - init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -983,7 +981,6 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { - init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -1040,7 +1037,6 @@ mod tests { #[test] fn occluded_records_are_ignored() { - init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); let records = SortedRecords::<_, _>::from_iter([ @@ -1081,7 +1077,6 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { - init_logging(); let mut cfg = GenerateNsec3Config::default(); let records = SortedRecords::<_, _>::from_iter([ @@ -1109,7 +1104,6 @@ mod tests { #[test] fn rfc_5155_appendix_a_and_rfc_9077_compliant_plus_ents() { - init_logging(); // These NSEC3 settings match those of the NSEC3PARAM record shown in // https://datatracker.ietf.org/doc/html/rfc5155#appendix-A. let nsec3params = Nsec3param::new( @@ -1301,7 +1295,6 @@ mod tests { #[test] fn opt_out_with_exclusion() { - init_logging(); // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 // 7.1. Zone Signing // .. @@ -1340,7 +1333,6 @@ mod tests { #[test] fn opt_out_without_exclusion() { - init_logging(); // https://www.rfc-editor.org/rfc/rfc5155.html#section-7.1 // 7.1. Zone Signing // .. @@ -1386,7 +1378,6 @@ mod tests { expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?" )] fn generating_nsec3s_for_unordered_input_should_panic() { - init_logging(); let mut cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); @@ -1402,7 +1393,6 @@ mod tests { #[test] fn test_nsec3_hash_collision_handling() { - init_logging(); let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( Nsec3param::default(), CollidingHashProvider, @@ -1435,13 +1425,4 @@ mod tests { Ok(StoredName::root()) } } - - fn init_logging() { - tracing_subscriber::fmt() - .with_max_level(tracing::level_filters::LevelFilter::TRACE) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); - } } From fcc94d299cb0cfadb88c388aec7aabaa2da12d5b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 4 Feb 2025 11:30:26 +0100 Subject: [PATCH 422/569] Add a test for hashing not producing the expected result. --- src/sign/denial/nsec3.rs | 42 ++++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 614d02d7e..61b04e452 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -620,13 +620,10 @@ where let next_hashed_owner_name = if let Ok(hash_octets) = base32::decode_hex(&format!("{first_label_of_next_owner_name}")) { - OwnerHash::::from_octets(hash_octets).unwrap() + OwnerHash::::from_octets(hash_octets) + .map_err(|_| Nsec3HashError::OwnerHashError)? } else { - // TODO: Why would an NSEC3 RR have an unhashed owner name? - OwnerHash::::from_octets( - next_owner_name.as_octets().clone(), - ) - .unwrap() + return Err(Nsec3HashError::OwnerHashError)?; }; nsec3.data_mut().set_next_owner(next_hashed_owner_name); } @@ -1411,6 +1408,26 @@ mod tests { )); } + #[test] + fn test_nsec3_hashing_failure() { + let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( + Nsec3param::default(), + NonHashingHashProvider, + ); + + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("a.", "b.", "c."), + mk_a_rr("some_a.a."), + ]); + + assert!(matches!( + generate_nsec3s(records.owner_rrs(), &mut cfg), + Err(SigningError::Nsec3HashingError( + Nsec3HashError::OwnerHashError + )) + )); + } + //------------ Test helpers ---------------------------------------------- struct CollidingHashProvider; @@ -1425,4 +1442,17 @@ mod tests { Ok(StoredName::root()) } } + + struct NonHashingHashProvider; + + impl Nsec3HashProvider for NonHashingHashProvider { + fn get_or_create( + &mut self, + _apex_owner: &StoredName, + unhashed_owner_name: &StoredName, + _unhashed_owner_name_is_ent: bool, + ) -> Result { + Ok(unhashed_owner_name.clone()) + } + } } From 0d4cc741551de3a0e7382021f920e3480abc1320 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:30:07 +0100 Subject: [PATCH 423/569] Make GenerateRrsigConfig take the zone apex name by value instead of by reference. --- src/sign/mod.rs | 2 +- src/sign/signatures/rrsigs.rs | 19 +++++++++---------- src/sign/traits.rs | 2 +- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/sign/mod.rs b/src/sign/mod.rs index eaf0675cf..89d12f9a1 100644 --- a/src/sign/mod.rs +++ b/src/sign/mod.rs @@ -479,7 +479,7 @@ where signing_config.rrsig_validity_period_strategy.clone(), ); rrsig_config.add_used_dnskeys = signing_config.add_used_dnskeys; - rrsig_config.zone_apex = Some(&apex_owner); + rrsig_config.zone_apex = Some(apex_owner); // Sign the NSEC(3)s. let owner_rrs = RecordsIter::new(in_out.as_out_slice()); diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index d6815d143..41c862c5a 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -36,18 +36,18 @@ use crate::sign::traits::SignRaw; //------------ GenerateRrsigConfig ------------------------------------------- #[derive(Copy, Clone, Debug, PartialEq)] -pub struct GenerateRrsigConfig<'a, N, KeyStrat, ValidityStrat, Sort> { +pub struct GenerateRrsigConfig { pub add_used_dnskeys: bool, - pub zone_apex: Option<&'a N>, + pub zone_apex: Option, pub rrsig_validity_period_strategy: ValidityStrat, _phantom: PhantomData<(KeyStrat, Sort)>, } -impl<'a, N, KeyStrat, ValidityStrat, Sort> - GenerateRrsigConfig<'a, N, KeyStrat, ValidityStrat, Sort> +impl + GenerateRrsigConfig { /// Like [`Self::default()`] but gives control over the SigningKeyStrategy /// and Sorter used. @@ -65,7 +65,7 @@ impl<'a, N, KeyStrat, ValidityStrat, Sort> self } - pub fn with_zone_apex(mut self, zone_apex: &'a N) -> Self { + pub fn with_zone_apex(mut self, zone_apex: N) -> Self { self.zone_apex = Some(zone_apex); self } @@ -73,7 +73,6 @@ impl<'a, N, KeyStrat, ValidityStrat, Sort> impl GenerateRrsigConfig< - '_, N, DefaultSigningKeyUsageStrategy, ValidityStrat, @@ -149,7 +148,7 @@ where pub fn generate_rrsigs( records: RecordsIter<'_, N, ZoneRecordData>, keys: &[DSK], - config: &GenerateRrsigConfig<'_, N, KeyStrat, ValidityStrat, Sort>, + config: &GenerateRrsigConfig, ) -> Result, SigningError> where DSK: DesignatedSigningKey, @@ -199,7 +198,7 @@ where // canonically ordered that the first record is part of the apex RRSET. // Otherwise, check if the first record matches the given apex, if not // that means that the input starts beneath the apex. - let (zone_apex, at_apex) = match config.zone_apex { + let (zone_apex, at_apex) = match &config.zone_apex { Some(zone_apex) => (zone_apex, first_rrs.owner() == zone_apex), None => (&first_owner, true), }; @@ -410,7 +409,7 @@ fn log_keys_in_use( #[allow(clippy::too_many_arguments)] fn generate_apex_rrsigs( keys: &[DSK], - config: &GenerateRrsigConfig<'_, N, KeyStrat, ValidityStrat, Sort>, + config: &GenerateRrsigConfig, records: &mut core::iter::Peekable< RecordsIter<'_, N, ZoneRecordData>, >, @@ -1081,7 +1080,7 @@ mod tests { RecordsIter::new(&records), &keys, &GenerateRrsigConfig::default(rrsig_validity_period_strategy) - .with_zone_apex(&mk_name(zone_apex)), + .with_zone_apex(mk_name(zone_apex)), ) .unwrap(); diff --git a/src/sign/traits.rs b/src/sign/traits.rs index e633823e1..c09ede84d 100644 --- a/src/sign/traits.rs +++ b/src/sign/traits.rs @@ -513,7 +513,7 @@ where GenerateRrsigConfig::::new( rrsig_validity_period_strategy, ) - .with_zone_apex(expected_apex); + .with_zone_apex(expected_apex.clone()); generate_rrsigs(self.owner_rrs(), keys, &rrsig_config) } From f37b4d9ec0b735a2f1269c304fe478218bf095af Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:30:19 +0100 Subject: [PATCH 424/569] Add OwnerRrs::into_inner(). --- src/sign/records.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sign/records.rs b/src/sign/records.rs index 0e942ee89..fc34e3b19 100644 --- a/src/sign/records.rs +++ b/src/sign/records.rs @@ -374,6 +374,10 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { OwnerRrs { slice } } + pub fn into_inner(self) -> &'a [Record] { + self.slice + } + pub fn owner(&self) -> &N { self.slice[0].owner() } From 9eedd889ac79de677ced8d7c4c4bdd1d0d0fbb76 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 17 Mar 2025 14:31:02 +0100 Subject: [PATCH 425/569] Make diff creation by ZoneUpdater configurable (as a diff due to applying an XFR is useful but a diff due to signing may not be). --- src/zonetree/update.rs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 460f64095..03d32db1f 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -244,9 +244,11 @@ where /// Use [`apply`][Self::apply] to apply changes to the zone. pub fn new( zone: Zone, + create_diff: bool, ) -> Pin> + Send>> { Box::pin(async move { - let write = ReopenableZoneWriter::new(zone.clone()).await?; + let write = + ReopenableZoneWriter::new(zone.clone(), create_diff).await?; Ok(Self { zone, @@ -528,15 +530,22 @@ struct ReopenableZoneWriter { /// A write interface to the root node of a zone for a particular zone /// version. writable: Option>, + + /// Whether or not to create diffs on write. + create_diff: bool, } impl ReopenableZoneWriter { /// Creates a writer for the given [`Zone`]. - async fn new(zone: Zone) -> std::io::Result { + async fn new(zone: Zone, create_diff: bool) -> std::io::Result { let write = zone.write().await; - let writable = Some(write.open(true).await?); + let writable = Some(write.open(create_diff).await?); let write = Some(write); - Ok(Self { write, writable }) + Ok(Self { + write, + writable, + create_diff, + }) } /// Commits any pending changes to the [`Zone`] being written to. @@ -570,7 +579,7 @@ impl ReopenableZoneWriter { self.write .as_mut() .ok_or(Error::Finished)? - .open(true) + .open(self.create_diff) .await?, ); Ok(()) @@ -653,7 +662,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); let qname = Name::from_str("example.com").unwrap(); @@ -711,7 +720,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); let qname = Name::from_str("example.com").unwrap(); @@ -752,7 +761,7 @@ mod tests { let res = updater.apply(ZoneUpdate::AddRecord(soa_rec.clone())).await; assert!(matches!(res, Err(crate::zonetree::update::Error::Finished))); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); updater .apply(ZoneUpdate::AddRecord(soa_rec.clone())) @@ -801,7 +810,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // Create an AXFR request to reply to. let req = mk_request("example.com", Rtype::AXFR).into_message(); @@ -912,7 +921,7 @@ mod tests { // serial number of 3," let zone = mk_empty_zone("JAIN.AD.JP."); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // JAIN.AD.JP. IN SOA NS.JAIN.AD.JP. mohta.jain.ad.jp. ( // 1 600 600 3600000 604800) let soa_1 = mk_rfc_1995_ixfr_example_soa(1); @@ -1215,7 +1224,7 @@ mod tests { let zone = mk_empty_zone("example.com"); - let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); + let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); // Create an AXFR request to reply to. let req = mk_request("example.com", Rtype::AXFR).into_message(); From a14d7dac0ff22aea33a662a40516cce5790c2449 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:10:39 +0100 Subject: [PATCH 426/569] Add From<&SigningConfig> for GenerateRrsigConfig. --- src/sign/signatures/rrsigs.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs index 41c862c5a..c27ecc7ce 100644 --- a/src/sign/signatures/rrsigs.rs +++ b/src/sign/signatures/rrsigs.rs @@ -21,6 +21,7 @@ use crate::base::record::Record; use crate::base::Name; use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; use crate::rdata::{Dnskey, Rrsig, ZoneRecordData}; +use crate::sign::denial::nsec3::Nsec3HashProvider; use crate::sign::error::SigningError; use crate::sign::keys::keymeta::DesignatedSigningKey; use crate::sign::keys::signingkey::SigningKey; @@ -32,6 +33,7 @@ use crate::sign::signatures::strategy::{ DefaultSigningKeyUsageStrategy, RrsigValidityPeriodStrategy, }; use crate::sign::traits::SignRaw; +use crate::sign::SigningConfig; //------------ GenerateRrsigConfig ------------------------------------------- @@ -86,6 +88,37 @@ where } } +impl + From<&SigningConfig> + for GenerateRrsigConfig +where + HP: Nsec3HashProvider, + Octs: AsRef<[u8]> + From<&'static [u8]>, + Inner: SignRaw, + KeyStrat: SigningKeyUsageStrategy, + ValidityStrat: RrsigValidityPeriodStrategy + Clone, + Sort: Sorter, +{ + fn from( + signing_cfg: &SigningConfig< + N, + Octs, + Inner, + KeyStrat, + ValidityStrat, + Sort, + HP, + >, + ) -> Self { + let mut rrsig_cfg = + GenerateRrsigConfig::::new( + signing_cfg.rrsig_validity_period_strategy.clone(), + ); + rrsig_cfg.add_used_dnskeys = signing_cfg.add_used_dnskeys; + rrsig_cfg + } +} + //------------ RrsigRecords -------------------------------------------------- #[derive(Clone, Debug)] From 2a37a914e29c017406faa3aaf3332b11d0658519 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:08:56 +0100 Subject: [PATCH 427/569] Fix wrongly changed default value, appears to be correct in the original branch. --- src/sign/denial/nsec3.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 7c0075d19..6d187e379 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -116,7 +116,7 @@ where pub fn without_opt_out_excluding_owner_names_of_unsigned_delegations( mut self, ) -> Self { - self.opt_out_exclude_owner_names_of_unsigned_delegations = true; + self.opt_out_exclude_owner_names_of_unsigned_delegations = false; self } From 875afa8a59fc38ac2ab1f6e2f4837f9a9bdfd0c6 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 19 Mar 2025 23:36:15 +0100 Subject: [PATCH 428/569] Add a test for hashing not producing the expected result. --- src/sign/denial/nsec3.rs | 393 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 365 insertions(+), 28 deletions(-) diff --git a/src/sign/denial/nsec3.rs b/src/sign/denial/nsec3.rs index 6d187e379..5af21bba1 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/sign/denial/nsec3.rs @@ -149,8 +149,7 @@ where assume_dnskeys_will_be_added: true, params, nsec3param_ttl_mode: Default::default(), - opt_out_exclude_owner_names_of_unsigned_delegations: - Default::default(), + opt_out_exclude_owner_names_of_unsigned_delegations: true, hash_provider, _phantom: Default::default(), } @@ -1123,11 +1122,11 @@ mod tests { } #[test] - fn rfc_5155_and_9077_compliant() { + fn rfc_5155_and_9077_compliant_opt_out_enabled() { let nsec3params = Nsec3param::new( Nsec3HashAlg::SHA1, - 1, - 12, + 1, // enable opt-out + 12, // do 12 extra hashing iterations Nsec3Salt::from_str("aabbccdd").unwrap(), ); let mut cfg = GenerateNsec3Config::< @@ -1154,29 +1153,234 @@ mod tests { let generated_records = generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - // Generate the expected NSEC3 RRs, with placeholder next owner hashes - // as the next owner hashes have to be in DNSSEC canonical order which - // we can't know before generating the hashes (which is why we - // pre-generate the hashes above). + // Generate the expected NSEC3 RRs. c.example. is present in the + // unsigned input zone but is not included in the generated NSEC3 + // chain because (a) it is an insecure delegation, and (b) we have + // NSEC3 opt-out behaviour enabled, thus per RFC 5155: + // + // https://www.rfc-editor.org/rfc/rfc5155#section-6 + // 6. Opt-Out + // "In this specification, as in [RFC4033], [RFC4034] and [RFC4035], + // NS RRSets at delegation points are not signed and may be + // accompanied by a DS RRSet. With the Opt-Out bit clear, the + // security status of the child zone is determined by the presence + // or absence of this DS RRSet, cryptographically proven by the + // signed NSEC3 RR at the hashed owner name of the delegation. + // Setting the Opt-Out flag modifies this by allowing insecure + // delegations to exist within the signed zone without a + // corresponding NSEC3 RR at the hashed owner name of the + // delegation." let expected_records = SortedRecords::<_, _>::from_iter([ mk_precalculated_nsec3_rr( - "t644ebqk9bibcna874givr6joj62mlhv.example.", - "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", + // example. -> ns1.example. + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "2t7b4g4vsa5smi47k61mv5bv1a22bojr", + "NS SOA MX RRSIG NSEC3PARAM", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ns1.example. -> x.y.w.example. + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", + "2vptu5timamqttgl4luu9kg21e0aor3s", + "A RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // x.y.w.example. -> a.example. + "2vptu5timamqttgl4luu9kg21e0aor3s.example.", + "35mthgpgcu1qg68fab165klnsnk3dpvl", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // a.example. -> x.w.example. + // This is an Opt-Out NSEC3 which covers the c.example. + // unsigned delegation thereby allowing the signed zone to + // omit an NSEC3 RR for the c.example. RR. This RR is the + // covering RR because it hash hash 36mt... which orders just + // before the c.example. NSEC3 hash (which would be + // 4g6p9u5gvfshp30pqecj98b3maqbn1ck). + "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", + "b4um86eghhds6nea196smvmlo4ors995", + "NS DS RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // x.w.example. -> ai.example. + "b4um86eghhds6nea196smvmlo4ors995.example.", + "gjeqe526plbf1g8mklp59enfd789njgi", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ai.example. -> y.w.example. + "gjeqe526plbf1g8mklp59enfd789njgi.example.", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", "A HINFO AAAA RRSIG", &cfg, ), mk_precalculated_nsec3_rr( + // y.w.example -> w.example. + "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", + "k8udemvp1j2f7eg6jebps17vp3n8i58h", + "", + &cfg, + ), + // Unlike NSEC, with NSEC3 empty non-terminals must also have + // NSEC3 RRs: + // + // https://www.rfc-editor.org/rfc/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "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." + // + // ENT NSEC3 RRs have an empty Type Bit Map. + mk_precalculated_nsec3_rr( + // w.example. -> ns2.example. + "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", + "q04jkcevqvmu85r014c7dkba38o0ji5r", + "", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ns2.example. -> *.w.example. + "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", + "r53bq7cc2uvmubfu5ocmm6pers9tk9en", + "A RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // *.w.example. -> xx.example. "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", "t644ebqk9bibcna874givr6joj62mlhv", "MX RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", - "r53bq7cc2uvmubfu5ocmm6pers9tk9en", + // xx.example. -> example. + "t644ebqk9bibcna874givr6joj62mlhv.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", + "A HINFO AAAA RRSIG", + &cfg, + ), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + + let expected_nsec3param = mk_nsec3param_rr("example.", &cfg); + assert_eq!(generated_records.nsec3param, expected_nsec3param); + + // TTLs are not compared by the eq check above so check them + // explicitly now. + // + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + // + // So in our case that is min(1800, 3600) = 1800. + for nsec3 in &generated_records.nsec3s { + assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); + } + } + + #[test] + fn rfc_5155_and_9077_compliant_opt_out_enabled_flags_only() { + let nsec3params = Nsec3param::new( + Nsec3HashAlg::SHA1, + 1, // enable opt-out + 12, // do 12 extra hashing iterations + Nsec3Salt::from_str("aabbccdd").unwrap(), + ); + + let mut cfg = GenerateNsec3Config::< + StoredName, + Bytes, + OnDemandNsec3HashProvider, + DefaultSorter, + >::new( + nsec3params.clone(), + OnDemandNsec3HashProvider::new( + nsec3params.hash_algorithm(), + nsec3params.iterations(), + nsec3params.salt().clone(), + ), + ) + .without_assuming_dnskeys_will_be_added() + .without_opt_out_excluding_owner_names_of_unsigned_delegations(); + + // See https://datatracker.ietf.org/doc/html/rfc5155#appendix-A + let zonefile = include_bytes!( + "../../../test-data/zonefiles/rfc5155-appendix-A.zone" + ); + + let records = bytes_to_records(&zonefile[..]); + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // Generate the expected NSEC3 RRs. + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_precalculated_nsec3_rr( + // example. -> ns1.example. + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "2t7b4g4vsa5smi47k61mv5bv1a22bojr", + "NS SOA MX RRSIG NSEC3PARAM", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ns1.example. -> x.y.w.example. + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", + "2vptu5timamqttgl4luu9kg21e0aor3s", "A RRSIG", &cfg, ), + mk_precalculated_nsec3_rr( + // x.y.w.example. -> a.example. + "2vptu5timamqttgl4luu9kg21e0aor3s.example.", + "35mthgpgcu1qg68fab165klnsnk3dpvl", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // a.example. -> c.example. + "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", + "4g6p9u5gvfshp30pqecj98b3maqbn1ck", + "NS DS RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // c.example. -> x.w.example. + // Note: as this is an insecure delegation and NSEC3 opt-out + // is disabled the c.example. RRSET has an NSEC3 RR but its + // type bitmap lacks the RRSIG RTYPE as insecure delegations + // are not signed. + "4g6p9u5gvfshp30pqecj98b3maqbn1ck.example.", + "b4um86eghhds6nea196smvmlo4ors995", + "NS", + &cfg, + ), + mk_precalculated_nsec3_rr( + // x.w.example. -> ai.example. + "b4um86eghhds6nea196smvmlo4ors995.example.", + "gjeqe526plbf1g8mklp59enfd789njgi", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ai.example. -> y.w.example. + "gjeqe526plbf1g8mklp59enfd789njgi.example.", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", + "A HINFO AAAA RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // y.w.example -> w.example. + "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", + "k8udemvp1j2f7eg6jebps17vp3n8i58h", + "", + &cfg, + ), // Unlike NSEC, with NSEC3 empty non-terminals must also have // NSEC3 RRs: // @@ -1189,51 +1393,184 @@ mod tests { // // ENT NSEC3 RRs have an empty Type Bit Map. mk_precalculated_nsec3_rr( + // w.example. -> ns2.example. "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", "q04jkcevqvmu85r014c7dkba38o0ji5r", "", &cfg, ), mk_precalculated_nsec3_rr( - "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", - "k8udemvp1j2f7eg6jebps17vp3n8i58h", - "", + // ns2.example. -> *.w.example. + "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", + "r53bq7cc2uvmubfu5ocmm6pers9tk9en", + "A RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "gjeqe526plbf1g8mklp59enfd789njgi.example.", - "ji6neoaepv8b5o6k4ev33abha8ht9fgc", + // *.w.example. -> xx.example. + "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", + "t644ebqk9bibcna874givr6joj62mlhv", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // xx.example. -> example. + "t644ebqk9bibcna874givr6joj62mlhv.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", "A HINFO AAAA RRSIG", &cfg, ), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + + let expected_nsec3param = mk_nsec3param_rr("example.", &cfg); + assert_eq!(generated_records.nsec3param, expected_nsec3param); + + // TTLs are not compared by the eq check above so check them + // explicitly now. + // + // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that + // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of + // the MINIMUM field of the SOA record and the TTL of the SOA itself". + // + // So in our case that is min(1800, 3600) = 1800. + for nsec3 in &generated_records.nsec3s { + assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); + } + } + + #[test] + fn rfc_5155_and_9077_compliant_opt_out_disabled() { + let nsec3params = Nsec3param::new( + Nsec3HashAlg::SHA1, + 0, // disable opt-out + 12, // do 12 extra hashing iterations + Nsec3Salt::from_str("aabbccdd").unwrap(), + ); + let mut cfg = GenerateNsec3Config::< + StoredName, + Bytes, + OnDemandNsec3HashProvider, + DefaultSorter, + >::new( + nsec3params.clone(), + OnDemandNsec3HashProvider::new( + nsec3params.hash_algorithm(), + nsec3params.iterations(), + nsec3params.salt().clone(), + ), + ) + .without_assuming_dnskeys_will_be_added(); + + // See https://datatracker.ietf.org/doc/html/rfc5155#appendix-A + let zonefile = include_bytes!( + "../../../test-data/zonefiles/rfc5155-appendix-A.zone" + ); + + let records = bytes_to_records(&zonefile[..]); + let generated_records = + generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + + // Generate the expected NSEC3 RRs. + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_precalculated_nsec3_rr( + // example. -> ns1.example. + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", + "2t7b4g4vsa5smi47k61mv5bv1a22bojr", + "NS SOA MX RRSIG NSEC3PARAM", + &cfg, + ), mk_precalculated_nsec3_rr( - "b4um86eghhds6nea196smvmlo4ors995.example.", - "gjeqe526plbf1g8mklp59enfd789njgi", + // ns1.example. -> x.y.w.example. + "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", + "2vptu5timamqttgl4luu9kg21e0aor3s", + "A RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // x.y.w.example. -> a.example. + "2vptu5timamqttgl4luu9kg21e0aor3s.example.", + "35mthgpgcu1qg68fab165klnsnk3dpvl", "MX RRSIG", &cfg, ), mk_precalculated_nsec3_rr( + // a.example. -> c.example. "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", - "b4um86eghhds6nea196smvmlo4ors995", + "4g6p9u5gvfshp30pqecj98b3maqbn1ck", "NS DS RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "2vptu5timamqttgl4luu9kg21e0aor3s.example.", - "35mthgpgcu1qg68fab165klnsnk3dpvl", + // c.example. -> x.w.example. + // Note: as this is an insecure delegation and NSEC3 opt-out + // is disabled the c.example. RRSET has an NSEC3 RR but its + // type bitmap lacks the RRSIG RTYPE as insecure delegations + // are not signed. + "4g6p9u5gvfshp30pqecj98b3maqbn1ck.example.", + "b4um86eghhds6nea196smvmlo4ors995", + "NS", + &cfg, + ), + mk_precalculated_nsec3_rr( + // x.w.example. -> ai.example. + "b4um86eghhds6nea196smvmlo4ors995.example.", + "gjeqe526plbf1g8mklp59enfd789njgi", "MX RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", - "2vptu5timamqttgl4luu9kg21e0aor3s", + // ai.example. -> y.w.example. + "gjeqe526plbf1g8mklp59enfd789njgi.example.", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", + "A HINFO AAAA RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // y.w.example -> w.example. + "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", + "k8udemvp1j2f7eg6jebps17vp3n8i58h", + "", + &cfg, + ), + // Unlike NSEC, with NSEC3 empty non-terminals must also have + // NSEC3 RRs: + // + // https://www.rfc-editor.org/rfc/rfc5155#section-7.1 + // 7.1. Zone Signing + // .. + // "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." + // + // ENT NSEC3 RRs have an empty Type Bit Map. + mk_precalculated_nsec3_rr( + // w.example. -> ns2.example. + "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", + "q04jkcevqvmu85r014c7dkba38o0ji5r", + "", + &cfg, + ), + mk_precalculated_nsec3_rr( + // ns2.example. -> *.w.example. + "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", + "r53bq7cc2uvmubfu5ocmm6pers9tk9en", "A RRSIG", &cfg, ), mk_precalculated_nsec3_rr( - "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.", - "2t7b4g4vsa5smi47k61mv5bv1a22bojr", - "NS SOA MX RRSIG NSEC3PARAM", + // *.w.example. -> xx.example. + "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", + "t644ebqk9bibcna874givr6joj62mlhv", + "MX RRSIG", + &cfg, + ), + mk_precalculated_nsec3_rr( + // xx.example. -> example. + "t644ebqk9bibcna874givr6joj62mlhv.example.", + "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", + "A HINFO AAAA RRSIG", &cfg, ), ]); From b7a47317d9bab88f836c652a9ce0dc6e14182aac Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 19 Mar 2025 23:37:31 +0100 Subject: [PATCH 429/569] Add fix from PR #499. --- src/zonetree/in_memory/read.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/zonetree/in_memory/read.rs b/src/zonetree/in_memory/read.rs index 5a6afe1f4..080f2f173 100644 --- a/src/zonetree/in_memory/read.rs +++ b/src/zonetree/in_memory/read.rs @@ -110,7 +110,18 @@ impl ReadZone { ) } } - Some(Special::NxDomain) => NodeAnswer::nx_domain(), + Some(Special::NxDomain) => { + if walk.enabled() { + self.query_children( + node.children(), + label, + qname, + qtype, + walk, + ); + } + NodeAnswer::nx_domain() + } Some(Special::Cname(cname)) => { if walk.enabled() { let mut rrset = Rrset::new(Rtype::CNAME, cname.ttl()); From ac5a59bfd7da22e384946e82b789c118e7dc30e9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 19 Mar 2025 23:37:51 +0100 Subject: [PATCH 430/569] RustDoc code fixes. --- src/zonetree/update.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 03d32db1f..e5a1c4865 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -94,7 +94,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # ZoneRecordData::A(A::new(Ipv4Addr::LOCALHOST)), /// # ); /// # -/// let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// updater.apply(ZoneUpdate::DeleteAllRecords); /// updater.apply(ZoneUpdate::AddRecord(a_rec)); /// updater.apply(ZoneUpdate::Finished(new_soa_rec)); @@ -159,7 +159,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # ZoneRecordData::A(A::new(Ipv4Addr::LOCALHOST)), /// # ); /// # -/// let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// updater.apply(ZoneUpdate::DeleteRecord(old_a_rec)); /// updater.apply(ZoneUpdate::AddRecord(new_aaaa_rec)); /// updater.apply(ZoneUpdate::Finished(new_soa_rec)); @@ -188,7 +188,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// let zone = builder.build(); /// /// // And a ZoneUpdater -/// let mut updater = ZoneUpdater::new(zone.clone()).await.unwrap(); +/// let mut updater = ZoneUpdater::new(zone.clone(), true).await.unwrap(); /// /// // And an XFR response interpreter /// let mut interpreter = XfrResponseInterpreter::new(); From afcd7d868936d1196c90af85a31b66316999b8df Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sat, 29 Mar 2025 22:47:19 +0100 Subject: [PATCH 431/569] FIX: Handle the AXFR -> IXFR fallback case properly. --- src/net/xfr/protocol/iterator.rs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/src/net/xfr/protocol/iterator.rs b/src/net/xfr/protocol/iterator.rs index 055e82e9a..a0beec3d8 100644 --- a/src/net/xfr/protocol/iterator.rs +++ b/src/net/xfr/protocol/iterator.rs @@ -4,7 +4,7 @@ use bytes::Bytes; use tracing::trace; use crate::base::message::RecordIter; -use crate::base::{Message, ParsedName}; +use crate::base::{Message, ParsedName, Record}; use crate::rdata::ZoneRecordData; use crate::zonetree::types::ZoneUpdate; @@ -22,6 +22,9 @@ pub struct XfrZoneUpdateIterator<'a, 'b> { /// An iterator over the records in the current response. iter: RecordIter<'b, Bytes, ZoneRecordData>>, + + /// TODO + saved_update: Option, ZoneRecordData>>>>, } impl<'a, 'b> XfrZoneUpdateIterator<'a, 'b> { @@ -51,12 +54,14 @@ impl<'a, 'b> XfrZoneUpdateIterator<'a, 'b> { let mut iter = answer.limit_to(); if state.rr_count == 0 { + // Skip the opening SOA record, it was already processed and + // stored by the given RecordProcessor. let Some(Ok(_)) = iter.next() else { return Err(Error::Malformed); }; } - Ok(Self { state, iter }) + Ok(Self { state, iter, saved_update: None }) } } @@ -64,13 +69,15 @@ impl Iterator for XfrZoneUpdateIterator<'_, '_> { type Item = Result, IterationError>; fn next(&mut self) -> Option { - if self.state.rr_count == 0 { + let is_first_rr = self.state.rr_count == 0; + + if is_first_rr { // We already skipped the first record in new() above by calling // iter.next(). We didn't reflect that yet in rr_count because we // wanted to still be able to detect the first call to next() and // handle it specially for AXFR. self.state.rr_count += 1; - + if self.state.actual_xfr_type == XfrType::Axfr { // For AXFR we're not making incremental changes to a zone, // we're replacing its entire contents, so before returning @@ -80,10 +87,26 @@ impl Iterator for XfrZoneUpdateIterator<'_, '_> { } } + if let Some(update) = self.saved_update.take() { + return Some(Ok(update)); + } + match self.iter.next()? { Ok(record) => { trace!("XFR record {}: {record:?}", self.state.rr_count); let update = self.state.process_record(record); + + if is_first_rr && self.state.actual_xfr_type == XfrType::Axfr { + // We didn't return DeleteAllRecords above because the + // transfer was thought to be IXFR rather than AXFR, but + // now that the next record has been processed we have had + // the chance to detect fallback from IXFR to AXFR for + // which we should also delete all records first. Save this + // update so that we can first return DeleteAllRecords. + self.saved_update = Some(update); + return Some(Ok(ZoneUpdate::DeleteAllRecords)); + } + Some(Ok(update)) } From 74e098dde06ad5247e59bbda19b4a4877245fb36 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 17 Apr 2025 09:47:03 +0200 Subject: [PATCH 432/569] WIP: Initial skeleton for KMIP based crypto operations. --- Cargo.lock | 255 ++++++++++++++++++++++++++++++++++++---- Cargo.toml | 3 + src/crypto/kmip/key.rs | 47 ++++++++ src/crypto/kmip/mod.rs | 4 + src/crypto/kmip/pool.rs | 141 ++++++++++++++++++++++ src/crypto/mod.rs | 1 + 6 files changed, 429 insertions(+), 22 deletions(-) create mode 100644 src/crypto/kmip/key.rs create mode 100644 src/crypto/kmip/mod.rs create mode 100644 src/crypto/kmip/pool.rs diff --git a/Cargo.lock b/Cargo.lock index 2164b039b..67134df8c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,7 +92,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -116,6 +116,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bitflags" version = "2.8.0" @@ -223,7 +229,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -244,6 +250,7 @@ dependencies = [ "hashbrown 0.14.5", "heapless", "itertools", + "kmip-protocol", "lazy_static", "libc", "log", @@ -254,10 +261,11 @@ dependencies = [ "parking_lot", "pretty_assertions", "proc-macro2", + "r2d2", "rand", - "ring", + "ring 0.17.12", "rstest", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustversion", "secrecy", "serde", @@ -284,6 +292,28 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7914353092ddf589ad78f25c5c1c21b7f80b0ff8621e7c814c3485b5306da9d" +[[package]] +name = "enum-display-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f16ef37b2a9b242295d61a154ee91ae884afff6b8b933b486b12481cc58310ca" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "enum-flags" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3682d2328e61f5529088a02cd20bb0a9aeaeeeb2f26597436dd7d75d1340f8f5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -382,7 +412,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -503,6 +533,12 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "iana-time-zone" version = "0.1.61" @@ -561,6 +597,40 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kmip-protocol" +version = "0.5.0" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#d8b74c8a7de2f93a200f9f555155006e6c639139" +dependencies = [ + "cfg-if", + "enum-display-derive", + "enum-flags", + "kmip-ttlv", + "log", + "maybe-async", + "rustc_version", + "rustls 0.19.1", + "rustls-pemfile 0.2.1", + "serde", + "serde_bytes", + "serde_derive", + "trait-set", + "webpki", +] + +[[package]] +name = "kmip-ttlv" +version = "0.4.0" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#dad2803cbcb35556e35ab2fa1341e860fcecec60" +dependencies = [ + "cfg-if", + "hex", + "maybe-async", + "rustc_version", + "serde", + "trait-set", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -611,6 +681,17 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "maybe-async" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "memchr" version = "2.7.4" @@ -740,7 +821,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -807,7 +888,7 @@ checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -886,6 +967,17 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r2d2" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93" +dependencies = [ + "log", + "parking_lot", + "scheduled-thread-pool", +] + [[package]] name = "rand" version = "0.8.5" @@ -975,6 +1067,21 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[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 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.12" @@ -985,7 +1092,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.15", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1015,7 +1122,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn", + "syn 2.0.98", "unicode-ident", ] @@ -1034,6 +1141,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring 0.16.20", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.23.23" @@ -1042,13 +1162,22 @@ checksum = "47796c98c480fce5406ef69d1c76378375492c3b0a0de587be0c1d9feb12f395" dependencies = [ "log", "once_cell", - "ring", + "ring 0.17.12", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1070,9 +1199,9 @@ version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ - "ring", + "ring 0.17.12", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1087,6 +1216,15 @@ version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd" +[[package]] +name = "scheduled-thread-pool" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cbc66816425a074528352f5789333ecff06ca41b36b0b0efdfbb29edc391a19" +dependencies = [ + "parking_lot", +] + [[package]] name = "scoped-tls" version = "1.0.1" @@ -1099,6 +1237,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "secrecy" version = "0.10.3" @@ -1123,6 +1271,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.218" @@ -1131,7 +1288,7 @@ checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1214,6 +1371,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1226,6 +1389,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.98" @@ -1260,7 +1434,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1328,7 +1502,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1337,7 +1511,7 @@ version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" dependencies = [ - "rustls", + "rustls 0.23.23", "tokio", ] @@ -1419,7 +1593,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1461,6 +1635,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-set" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875c4c873cc824e362fa9a9419ffa59807244824275a44ad06fec9684fff08f2" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "unicode-ident" version = "1.0.17" @@ -1473,6 +1658,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -1537,7 +1728,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn", + "syn 2.0.98", "wasm-bindgen-shared", ] @@ -1559,7 +1750,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1573,6 +1764,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "webpki-roots" version = "0.26.8" @@ -1644,7 +1855,7 @@ checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1655,7 +1866,7 @@ checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] @@ -1864,7 +2075,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.98", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 61ccf1a5a..609555202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,12 +30,14 @@ chrono = { version = "0.4.35", optional = true, default-features = false futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.14.2", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing heapless = { version = "0.8", optional = true } +kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-rustls"] } # TODO: use the async feature instead libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } openssl = { version = "0.10.57", optional = true } # 0.10.70 upgrades to 'bitflags' 2.x proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build +r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } @@ -62,6 +64,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] +kmip = ["dep:kmip", "dep:r2d2"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/crypto/kmip/key.rs b/src/crypto/kmip/key.rs new file mode 100644 index 000000000..981bc62dd --- /dev/null +++ b/src/crypto/kmip/key.rs @@ -0,0 +1,47 @@ +use std::string::String; + +use crate::base::iana::SecurityAlgorithm; + +use super::pool::KmipConnPool; + +struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + private_key_id: String, + + conn_pool: KmipConnPool, +} + +#[cfg(feature = "unstable-crypto-sign")] +pub mod sign { + use std::boxed::Box; + use std::vec::Vec; + + use crate::crypto::sign::{SignError, SignRaw, Signature}; + use crate::rdata::Dnskey; + use crate::base::iana::SecurityAlgorithm; + + impl SignRaw for super::KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn dnskey(&self) -> Dnskey> { + todo!() + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let client = self.conn_pool.get().map_err(|_| SignError)?; + + let signed = client.sign(&self.private_key_id, data).unwrap(); + + let signature: [u8; 64] = + signed.signature_data.try_into().unwrap(); + + let sig = Signature::EcdsaP256Sha256(Box::new(signature)); + + Ok(sig) + } + } +} diff --git a/src/crypto/kmip/mod.rs b/src/crypto/kmip/mod.rs new file mode 100644 index 000000000..d18344917 --- /dev/null +++ b/src/crypto/kmip/mod.rs @@ -0,0 +1,4 @@ +#![cfg(feature = "kmip")] +#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] +pub mod pool; +pub mod key; diff --git a/src/crypto/kmip/pool.rs b/src/crypto/kmip/pool.rs new file mode 100644 index 000000000..5dca1d25b --- /dev/null +++ b/src/crypto/kmip/pool.rs @@ -0,0 +1,141 @@ +//! KMIP TLS connection pool +//! +//! Used to: +//! - Avoid repeated TCP connection setup and TLS session establishment +//! for mutiple KMIP requests made close together in time. +//! - Handle loss of connectivity by re-creating the connection when an +//! existing connection is considered to be "broken" at the network +//! level. +use std::net::TcpStream; +use std::{sync::Arc, time::Duration}; + +use kmip::client::{Client, ConnectionSettings}; + +pub type KmipTlsClient = Client>; + +pub type KmipConnPool = r2d2::Pool; + +/// Manages KMIP TCP + TLS connection creation. +/// +/// Uses the [r2d2] crate to manage a pool of connections. +/// +/// [r2d2]: https://crates.io/crates/r2d2/ +#[derive(Debug)] +pub struct ConnectionManager { + conn_settings: Arc, +} + +impl ConnectionManager { + /// Create a pool of up-to N TCP + TLS connections to the KMIP server. + #[rustfmt::skip] + pub fn create_connection_pool( + conn_settings: Arc, + max_response_bytes: u32, + max_life_time: Duration, + max_idle_time: Duration, + ) -> Result { + let max_life_time = Some(max_life_time); + let max_idle_time = Some(max_idle_time); + + let pool = r2d2::Pool::builder() + // Don't pre-create idle connections to the KMIP server + .min_idle(Some(0)) + + // Create at most this many concurrent connections to the KMIP server + .max_size(max_response_bytes) + + // Don't verify that a connection is usable when fetching it from the pool (as doing so requires sending a + // request to the server and we might as well just try the actual request that we want the connection for) + .test_on_check_out(false) + + // Don't use the default logging behaviour as `[ERROR] [r2d2] Server error: ...` is a bit confusing for end + // users who shouldn't know or care that we use the r2d2 crate. + // .error_handler(Box::new(ErrorLoggingHandler)) + + // Don't keep using the same connection for longer than around N minutes (unless in use in which case it + // will wait until the connection is returned to the pool before closing it) - maybe long held connections + // would run into problems with some firewalls. + .max_lifetime(max_life_time) + + // Don't keep connections open that were not used in the last N minutes. + .idle_timeout(max_idle_time) + + // Don't wait longer than N seconds for a new connection to be established, instead try again to connect. + .connection_timeout(conn_settings.connect_timeout.unwrap_or(Duration::from_secs(30))) + + // Use our connection manager to create connections in the pool and to verify their health + .build(ConnectionManager { conn_settings }).unwrap(); + + Ok(pool) + } + + /// Connect using the given connection settings to a KMIP server. + /// + /// This function creates a new connection to the server. The connection + /// is NOT taken from the connection pool. + pub fn connect_one_off( + settings: &ConnectionSettings, + ) -> kmip::client::Result { + kmip::client::tls::rustls::connect(settings) + } +} + +impl r2d2::ManageConnection for ConnectionManager { + type Connection = KmipTlsClient; + + type Error = kmip::client::Error; + + /// Establishes a KMIP server connection which will be added to the + /// connection pool. + fn connect(&self) -> Result { + Self::connect_one_off(&self.conn_settings) + } + + /// This function is never used because the [r2d2] `test_on_check_out` + /// flag is set to false when the connection pool is created. + /// + /// [r2d2]: https://crates.io/crates/r2d2/ + fn is_valid(&self, _conn: &mut Self::Connection) -> Result<(), Self::Error> { + unreachable!() + } + + /// Quickly verify if an existing connection is broken. + /// + /// Used to discard and re-create connections that encounter multiple + /// connection related errors. + fn has_broken(&self, conn: &mut Self::Connection) -> bool { + conn.connection_error_count() > 1 + } +} + +// /// A Krill specific [r2d2] error logging handler. +// /// +// /// Logs connection pool related connection error messages using the format +// /// `"[] Pool error: ..."` instead of +// /// the default [r2d2] `"[ERROR] [r2d2] Server error: ..."` format. Assumes +// /// that the logging framework will include the logging module context in the +// /// logged message, i.e. `xxx::kmip::xxx` and thus we don't need to mention +// /// KMIP in the logged message content. +// /// +// /// Rationale: +// /// - The use of the [r2d2] crate is an internal detail which of no use to +// /// end users consulting the logs and which we may change at any time. +// /// - Krill should be the one to determine the appropriate level to log a +// /// connection issue at, not [r2d2]. +// #[derive(Debug)] +// struct ErrorLoggingHandler; + +// impl r2d2::HandleError for ErrorLoggingHandler +// where +// E: std::fmt::Display, +// { +// fn handle_error(&self, err: E) { +// warn!("Pool error: {}", err) +// } +// } + +// impl From for SignerError { +// fn from(err: r2d2::Error) -> Self { +// SignerError::KmipError(format!("{}", err)) +// } +// } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index a35077d74..836192b87 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -128,6 +128,7 @@ #![warn(clippy::missing_docs_in_private_items)] pub mod common; +pub mod kmip; pub mod openssl; pub mod ring; pub mod sign; From e659f9b22a03e7c3d338bf12a41155ce0063cec9 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 17 Apr 2025 15:55:45 +0200 Subject: [PATCH 433/569] Support generating RSASHA512 keys. --- src/crypto/openssl.rs | 3 ++- src/crypto/sign.rs | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 978f612a7..e214c4dec 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -780,7 +780,8 @@ pub mod sign { ) -> Result { let algorithm = params.algorithm(); let pkey = match params { - GenerateParams::RsaSha256 { bits } => { + GenerateParams::RsaSha256 { bits } + | GenerateParams::RsaSha512 { bits } => { Rsa::generate(bits).and_then(PKey::from_rsa)? } GenerateParams::EcdsaP256Sha256 => { diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index b6d33a244..8a9068968 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -115,6 +115,12 @@ pub enum GenerateParams { bits: u32, }, + /// Generate an RSA/SHA-512 keypair. + RsaSha512 { + /// The number of bits in the public modulus. + bits: u32, + }, + /// Generate an ECDSA P-256/SHA-256 keypair. EcdsaP256Sha256, @@ -135,6 +141,7 @@ impl GenerateParams { pub fn algorithm(&self) -> SecurityAlgorithm { match self { Self::RsaSha256 { .. } => SecurityAlgorithm::RSASHA256, + Self::RsaSha512 { .. } => SecurityAlgorithm::RSASHA512, Self::EcdsaP256Sha256 => SecurityAlgorithm::ECDSAP256SHA256, Self::EcdsaP384Sha384 => SecurityAlgorithm::ECDSAP384SHA384, Self::Ed25519 => SecurityAlgorithm::ED25519, From 2c0438fa10faf0de015c399a65290111811a4003 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 17 Apr 2025 15:56:37 +0200 Subject: [PATCH 434/569] Remove unneeded mut in actions. Remove debug output. Add Clone, Eq, and Hash to Action. --- examples/keyset.rs | 2 +- src/dnssec/sign/keys/keyset.rs | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index a72f3a599..b437b1261 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -314,7 +314,7 @@ fn do_actions(filename: &str, args: &[String]) { let rolltype = str_to_rolltype(rolltype); - let mut ks = load_keyset(filename); + let ks = load_keyset(filename); let actions = ks.actions(rolltype); report_actions(Ok(actions), &ks); diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 8a12ff63d..2b17835c4 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -333,7 +333,7 @@ impl KeySet { /// Return the actions that need to be performed for the current /// roll state. - pub fn actions(&mut self, rolltype: RollType) -> Vec { + pub fn actions(&self, rolltype: RollType) -> Vec { if let Some(rollstate) = self.rollstates.get(&rolltype) { rolltype.roll_actions_fn()(rollstate.clone()) } else { @@ -874,7 +874,7 @@ enum Mode { /// Note that if a list contains multiple report actions then the user /// has to wait until all action have completed and has to report the /// highest TTL value among the values to report. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, Eq, Hash, PartialEq)] pub enum Action { /// Generate a new version of the zone with an updated DNSKEY RRset. UpdateDnskeyRrset, @@ -1169,10 +1169,8 @@ fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { // Check if we can move the states of the keys ks.update_zsk(Mode::DryRun, old, new)?; // Move the states of the keys - println!("line {} = {:?}", line!(), ks.keys().get("second ZSK")); ks.update_zsk(Mode::ForReal, old, new) .expect("Should have been checked with DryRun"); - println!("line {} = {:?}", line!(), ks.keys().get("second ZSK")); } RollOp::Propagation1 => { // Set the visiable time of new ZSKs to the current time. From 6844f2d7d224a72d2b3b2c2a3614cf470063e8b7 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 22 May 2025 16:38:59 +0200 Subject: [PATCH 435/569] Add more features to UnixTime. --- src/dnssec/sign/keys/keyset.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 2b17835c4..bd2198850 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -55,13 +55,16 @@ // - add support for undo/abort. use crate::base::Name; +use crate::rdata::dnssec::Timestamp; use serde::{Deserialize, Serialize}; use std::collections::{hash_map, HashMap}; use std::fmt; use std::fmt::{Debug, Display, Formatter}; +use std::ops::Add; use std::str::FromStr; use std::string::{String, ToString}; use std::time::Duration; +use std::time::SystemTimeError; use std::vec::Vec; use time::format_description; use time::OffsetDateTime; @@ -807,6 +810,32 @@ impl UnixTime { } } +impl TryFrom for UnixTime { + type Error = SystemTimeError; + fn try_from(t: SystemTime) -> Result { + Ok(Self(t.duration_since(UNIX_EPOCH)?)) + } +} + +impl From for UnixTime { + fn from(t: Timestamp) -> Self { + Self(Duration::from_secs(t.into_int() as u64)) + } +} + +impl From for Duration { + fn from(t: UnixTime) -> Self { + t.0 + } +} + +impl Add for UnixTime { + type Output = UnixTime; + fn add(self, d: Duration) -> Self { + Self(self.0 + d) + } +} + impl Display for UnixTime { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> { let nanos = self.0.as_nanos(); From 35a17a19909aaba8f1a98e331783d7989a9c6220 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sat, 31 May 2025 00:35:57 +0200 Subject: [PATCH 436/569] - Remove defaults from trait generic type parameters (i.e. TraitName) to prevent intermediate types that impl the trait from not themselves allowing the default type parameter to be overridden. - Move common bounds up from implementers of the Service trait to the Service trait itself. This both reduces boilerplate and reduces the chances of a type having stricter bounds on the struct and fns (such as fn new()) than its Service trait impl, making it hard to find the cause of a bounds mismatch. - Align bounds on all middleware struct blocks to match the bounds on the Service trait impls to catch mismatched bounds earlier. - Remove unnecessary ?Sized bound on impl Service for U where U: Deref. --- Cargo.lock | 10 +++ examples/query-routing.rs | 6 +- examples/serve-zone.rs | 19 +++--- examples/server-transports.rs | 65 +++++++++--------- src/net/server/adapter.rs | 43 +++++++++--- src/net/server/connection.rs | 34 +++++----- src/net/server/dgram.rs | 45 ++---------- src/net/server/message.rs | 10 +-- src/net/server/middleware/cookies.rs | 28 +++++--- src/net/server/middleware/edns.rs | 30 +++++--- src/net/server/middleware/mandatory.rs | 30 +++++--- src/net/server/middleware/notify.rs | 43 +++++++----- src/net/server/middleware/tsig.rs | 68 ++++++++++++------- .../server/middleware/xfr/data_provider.rs | 2 +- src/net/server/middleware/xfr/service.rs | 52 +++++++++----- src/net/server/middleware/xfr/tests.rs | 41 ++++++----- src/net/server/qname_router.rs | 30 +++++--- src/net/server/service.rs | 40 +++++------ src/net/server/single_service.rs | 4 +- src/net/server/stream.rs | 41 ++++------- src/net/server/tests/integration.rs | 8 +-- src/net/server/tests/unit.rs | 8 ++- src/net/server/util.rs | 5 +- 23 files changed, 375 insertions(+), 287 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fcd239ac9..3bbe59c1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -754,6 +754,15 @@ dependencies = [ "syn", ] +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.107" @@ -762,6 +771,7 @@ checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/examples/query-routing.rs b/examples/query-routing.rs index a0b829133..fc1b2b2da 100644 --- a/examples/query-routing.rs +++ b/examples/query-routing.rs @@ -39,7 +39,7 @@ async fn main() { .ok(); // Start building the query router plus upstreams. - let mut qr: QnameRouter, Vec, ReplyMessage> = + let mut qr: QnameRouter, Vec, (), ReplyMessage> = QnameRouter::new(); // Queries to the root go to 2606:4700:4700::1111 and 1.1.1.1. @@ -57,8 +57,8 @@ async fn main() { let conn_service = ClientTransportToSingleService::new(redun); qr.add(Name::>::from_str("nl").unwrap(), conn_service); - let srv = SingleServiceToService::new(qr); - let srv = MandatoryMiddlewareSvc::, _, _>::new(srv); + let srv = SingleServiceToService::<_, _, _, _>::new(qr); + let srv = MandatoryMiddlewareSvc::new(srv); let my_svc = Arc::new(srv); let udpsocket = UdpSocket::bind("[::1]:8053").await.unwrap(); diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index 60c9c2c80..109676e24 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -118,16 +118,16 @@ async fn main() { let svc = service_fn(my_service, zones.clone()); #[cfg(feature = "siphasher")] - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); let svc = XfrMiddlewareSvc::, _, _, _>::new( svc, zones_and_diffs.clone(), 1, ); let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); + let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); + let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::, _, _>::new(svc); let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); - let svc = TsigMiddlewareSvc::new(svc, key_store); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); @@ -135,15 +135,18 @@ async fn main() { let mut udp_metrics = vec![]; let num_cores = std::thread::available_parallelism().unwrap().get(); for _i in 0..num_cores { - let udp_srv = - DgramServer::new(sock.clone(), VecBufSource, svc.clone()); + let udp_srv = DgramServer::<_, _, _>::new( + sock.clone(), + VecBufSource, + svc.clone(), + ); let metrics = udp_srv.metrics(); udp_metrics.push(metrics); tokio::spawn(async move { udp_srv.run().await }); } let sock = TcpListener::bind(addr).await.unwrap(); - let tcp_srv = StreamServer::new(sock, VecBufSource, svc); + let tcp_srv = StreamServer::<_, _, _>::new(sock, VecBufSource, svc); let tcp_metrics = tcp_srv.metrics(); tokio::spawn(async move { tcp_srv.run().await }); @@ -240,8 +243,8 @@ async fn main() { } #[allow(clippy::type_complexity)] -fn my_service( - request: Request>, +fn my_service( + request: Request, RequestMeta>, zones: Arc, ) -> ServiceResult> { let question = request.message().sole_question().unwrap(); diff --git a/examples/server-transports.rs b/examples/server-transports.rs index 16bcff007..159d6a691 100644 --- a/examples/server-transports.rs +++ b/examples/server-transports.rs @@ -7,6 +7,7 @@ use core::time::Duration; use std::fs::File; use std::io; use std::io::BufReader; +use std::marker::Unpin; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; @@ -50,7 +51,7 @@ use domain::rdata::{Soa, A}; // Helper fn to create a dummy response to send back to the client fn mk_answer( - msg: &Request>, + msg: &Request, ()>, builder: MessageBuilder>, ) -> Result>, PushError> where @@ -69,7 +70,7 @@ where } fn mk_soa_answer( - msg: &Request>, + msg: &Request, ()>, builder: MessageBuilder>, ) -> Result>, PushError> where @@ -100,6 +101,7 @@ where //--- MySingleResultService +#[derive(Clone)] struct MySingleResultService; /// This example shows how to implement the [`Service`] trait directly. @@ -116,12 +118,12 @@ struct MySingleResultService; /// /// See [`query`] and [`name_to_ip`] for ways of implementing the [`Service`] /// trait for a function instead of a struct. -impl Service> for MySingleResultService { +impl Service, ()> for MySingleResultService { type Target = Vec; type Stream = Once>>; type Future = Ready; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { let builder = mk_builder_for_target(); let additional = mk_answer(&request, builder).unwrap(); let item = Ok(CallResult::new(additional)); @@ -131,6 +133,7 @@ impl Service> for MySingleResultService { //--- MyAsyncStreamingService +#[derive(Clone)] struct MyAsyncStreamingService; /// This example also shows how to implement the [`Service`] trait directly. @@ -147,13 +150,13 @@ struct MyAsyncStreamingService; /// and/or Stream implementations that actually wait and/or stream, e.g. /// making the Stream type be UnboundedReceiver instead of Pin>. -impl Service> for MyAsyncStreamingService { +impl Service, ()> for MyAsyncStreamingService { type Target = Vec; type Stream = Pin> + Send>>; type Future = Pin + Send>>; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { Box::pin(async move { if !matches!( request @@ -209,7 +212,10 @@ impl Service> for MyAsyncStreamingService { /// The function signature is slightly more complex than when using /// [`service_fn`] (see the [`query`] example below). #[allow(clippy::type_complexity)] -fn name_to_ip(request: Request>, _: ()) -> ServiceResult> { +fn name_to_ip( + request: Request, ()>, + _: (), +) -> ServiceResult> { let mut out_answer = None; if let Ok(question) = request.message().sole_question() { let qname = question.qname(); @@ -257,7 +263,7 @@ fn name_to_ip(request: Request>, _: ()) -> ServiceResult> { /// [`service_fn`] and supports passing in meta data without any extra /// boilerplate. fn query( - request: Request>, + request: Request, ()>, count: Arc, ) -> ServiceResult> { let cnt = count @@ -455,6 +461,7 @@ impl std::fmt::Display for Stats { } } +#[derive(Clone)] pub struct StatsMiddlewareSvc { svc: Svc, stats: Arc>, @@ -467,7 +474,7 @@ impl StatsMiddlewareSvc { Self { svc, stats } } - fn preprocess(&self, request: &Request) + fn preprocess(&self, request: &Request) where RequestOctets: Octets + Send + Sync + Unpin, { @@ -488,12 +495,12 @@ impl StatsMiddlewareSvc { } fn postprocess( - request: &Request, + request: &Request, response: &AdditionalBuilder>, stats: &RwLock, ) where RequestOctets: Octets + Send + Sync + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, { let duration = Instant::now().duration_since(request.received_at()); @@ -510,13 +517,13 @@ impl StatsMiddlewareSvc { } fn map_stream_item( - request: Request, + request: Request, stream_item: ServiceResult, stats: &mut Arc>, ) -> ServiceResult where RequestOctets: Octets + Send + Sync + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, { if let Ok(cr) = &stream_item { @@ -528,10 +535,11 @@ impl StatsMiddlewareSvc { } } -impl Service for StatsMiddlewareSvc +impl Service + for StatsMiddlewareSvc where RequestOctets: Octets + Send + Sync + 'static + Unpin, - Svc: Service, + Svc: Service, Svc::Target: AsRef<[u8]>, Svc::Future: Unpin, { @@ -551,7 +559,7 @@ where >; type Future = Ready; - fn call(&self, request: Request) -> Self::Future { + fn call(&self, request: Request) -> Self::Future { self.preprocess(&request); let svc_call_fut = self.svc.call(request.clone()); let map = PostprocessingStream::new( @@ -567,24 +575,19 @@ where //------------ build_middleware_chain() -------------------------------------- #[allow(clippy::type_complexity)] -fn build_middleware_chain( +fn build_middleware_chain( svc: Svc, stats: Arc>, -) -> StatsMiddlewareSvc< - MandatoryMiddlewareSvc< - Vec, - EdnsMiddlewareSvc< - Vec, - CookiesMiddlewareSvc, Svc, ()>, - (), - >, - (), - >, -> { +) -> impl Service +where + Octs: Octets + Send + Sync + Clone + Unpin + 'static, + Svc: Service, + >::Future: Unpin, +{ #[cfg(feature = "siphasher")] - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); - let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); + let svc = CookiesMiddlewareSvc::::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::new(svc); + let svc = MandatoryMiddlewareSvc::new(svc); StatsMiddlewareSvc::new(svc, stats.clone()) } diff --git a/src/net/server/adapter.rs b/src/net/server/adapter.rs index a28c1d80d..5a9c785d6 100644 --- a/src/net/server/adapter.rs +++ b/src/net/server/adapter.rs @@ -31,15 +31,30 @@ use std::string::ToString; use std::vec::Vec; /// Provide a [Service] trait for an object that implements [SingleService]. -pub struct SingleServiceToService { +pub struct SingleServiceToService +where + RequestMeta: Clone + Default, + RequestOcts: Octets + Send + Sync, + SVC: SingleService, + CR: ComposeReply + 'static, + Self: Send + Sync + 'static, +{ /// Service that is wrapped by this object. service: SVC, /// Phantom field for RequestOcts and CR. - _phantom: PhantomData<(RequestOcts, CR)>, + _phantom: PhantomData<(RequestOcts, CR, RequestMeta)>, } -impl SingleServiceToService { +impl + SingleServiceToService +where + RequestMeta: Clone + Default, + RequestOcts: Octets + Send + Sync, + SVC: SingleService, + CR: ComposeReply + 'static, + Self: Send + Sync + 'static, +{ /// Create a new [SingleServiceToService] object. pub fn new(service: SVC) -> Self { Self { @@ -49,18 +64,23 @@ impl SingleServiceToService { } } -impl Service - for SingleServiceToService +impl Service + for SingleServiceToService where + RequestMeta: Clone + Default, RequestOcts: Octets + Send + Sync, - SVC: SingleService, + SVC: SingleService, CR: ComposeReply + 'static, + Self: Send + Sync + 'static, { type Target = Vec; type Stream = Once>>; type Future = Pin + Send>>; - fn call(&self, request: Request) -> Self::Future { + fn call( + &self, + request: Request, + ) -> Self::Future { let fut = self.service.call(request); let fut = async move { let reply = match fut.await { @@ -114,7 +134,8 @@ where } } -impl SingleService +impl + SingleService for ClientTransportToSingleService where RequestOcts: AsRef<[u8]> + Clone + Debug + Octets + Send + Sync, @@ -123,7 +144,7 @@ where { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]>, @@ -194,7 +215,7 @@ where } } -impl SingleService +impl SingleService for BoxClientTransportToSingleService where RequestOcts: AsRef<[u8]> + Clone + Debug + Octets + Send + Sync, @@ -202,7 +223,7 @@ where { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]>, diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index 7e29ba7e7..772fa183d 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -21,7 +21,6 @@ use tokio::time::{sleep_until, timeout}; use tracing::{debug, error, trace, warn}; use crate::base::message_builder::AdditionalBuilder; -use crate::base::wire::Composer; use crate::base::{Message, StreamTarget}; use crate::net::server::buf::BufSource; use crate::net::server::message::Request; @@ -219,11 +218,12 @@ impl Clone for Config { //------------ Connection ---------------------------------------------------- /// A handler for a single stream connection between client and server. -pub struct Connection +pub struct Connection where + RequestMeta: Default + Clone + Send + 'static, Buf: BufSource, Buf::Output: Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { /// Flag used by the Drop impl to track if the metric count has to be /// decreased or not. @@ -270,12 +270,13 @@ where /// Creation /// -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite, Buf: BufSource, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { /// Creates a new handler for an accepted stream connection. #[must_use] @@ -340,14 +341,13 @@ where /// Control /// -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite + Send + Sync + 'static, Buf: BufSource + Send + Sync + Clone + 'static, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone + Send + Sync + 'static, - Svc::Target: Composer + Send, - Svc::Stream: Send, + Svc: Service + Clone, { /// Start reading requests and writing responses to the stream. /// @@ -377,15 +377,13 @@ where //--- Internal details -impl Connection +impl Connection where + RequestMeta: Default + Clone + Send + 'static, Stream: AsyncRead + AsyncWrite + Send + Sync + 'static, Buf: BufSource + Send + Sync + Clone + 'static, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Clone + Send + Sync + 'static, - Svc::Target: Composer + Send, - Svc::Future: Send, - Svc::Stream: Send, + Svc: Service + Clone, { /// Connection handler main loop. async fn run_until_error( @@ -685,7 +683,7 @@ where received_at, msg, ctx, - (), + Default::default(), ); let svc = self.service.clone(); @@ -799,11 +797,13 @@ where //--- Drop -impl Drop for Connection +impl Drop + for Connection where + RequestMeta: Default + Clone + Send + 'static, Buf: BufSource, Buf::Output: Send + Sync + Unpin, - Svc: Service + Clone, + Svc: Service + Clone, { fn drop(&mut self) { if self.active { diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 1b63d6ffd..bab33cda1 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -33,7 +33,6 @@ use tokio::time::Instant; use tokio::time::MissedTickBehavior; use tracing::{error, trace, warn}; -use crate::base::wire::Composer; use crate::base::Message; use crate::net::server::buf::BufSource; use crate::net::server::error::Error; @@ -213,7 +212,7 @@ type CommandReceiver = watch::Receiver; /// use domain::net::server::stream::StreamServer; /// use domain::net::server::util::service_fn; /// -/// fn my_service(msg: Request>, _meta: ()) -> ServiceResult> +/// fn my_service(msg: Request, ()>, _meta: ()) -> ServiceResult> /// { /// todo!() /// } @@ -251,14 +250,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin + 'static, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// The configuration of the server. config: Arc>, @@ -295,10 +287,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Constructs a new [`DgramServer`] with default configuration. /// @@ -352,10 +341,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Get a reference to the network source being used to receive messages. #[must_use] @@ -377,14 +363,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + 'static + Unpin, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Start the server. /// @@ -465,10 +444,7 @@ where Sock: AsyncDgramSock + Send + Sync, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin, - Svc: Clone + Service<::Output, ()> + Send + Sync, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { /// Receive incoming messages until shutdown or fatal error. async fn run_until_error(&self) -> Result<(), String> { @@ -678,14 +654,7 @@ where Sock: AsyncDgramSock + Send + Sync + 'static, Buf: BufSource + Send + Sync, ::Output: Octets + Send + Sync + Unpin + 'static, - Svc: Clone - + Service<::Output, ()> - + Send - + Sync - + 'static, - ::Output, ()>>::Future: Send, - ::Output, ()>>::Stream: Send, - ::Output, ()>>::Target: Composer + Send, + Svc: Service<::Output, ()> + Clone, { fn drop(&mut self) { // Shutdown the DgramServer. Don't handle the failure case here as diff --git a/src/net/server/message.rs b/src/net/server/message.rs index 197ab0718..6e7687daf 100644 --- a/src/net/server/message.rs +++ b/src/net/server/message.rs @@ -166,7 +166,7 @@ impl From for TransportSpecificContext { /// message itself but also on the circumstances surrounding its creation and /// delivery. #[derive(Debug)] -pub struct Request +pub struct Request where Octs: AsRef<[u8]> + Send + Sync, { @@ -191,7 +191,7 @@ where /// still possible to generate responses that ignore this value. num_reserved_bytes: u16, - /// user defined metadata to associate with the request. + /// User defined metadata to associate with the request. /// /// For example this could be used to pass data from one [middleware] /// [`Service`] impl to another. @@ -298,12 +298,12 @@ where //--- TryFrom> for RequestMessage> -impl TryFrom> - for RequestMessage +impl + TryFrom> for RequestMessage { type Error = request::Error; - fn try_from(req: Request) -> Result { + fn try_from(req: Request) -> Result { // Copy the ECS option from the message. This is just an example, // there should be a separate plugin that deals with ECS. diff --git a/src/net/server/middleware/cookies.rs b/src/net/server/middleware/cookies.rs index fd6b089be..4344cdc64 100644 --- a/src/net/server/middleware/cookies.rs +++ b/src/net/server/middleware/cookies.rs @@ -14,7 +14,7 @@ use crate::base::iana::{OptRcode, Rcode}; use crate::base::message_builder::AdditionalBuilder; use crate::base::net::IpAddr; use crate::base::opt; -use crate::base::wire::{Composer, ParseError}; +use crate::base::wire::ParseError; use crate::base::{Serial, StreamTarget}; use crate::net::server::message::Request; use crate::net::server::middleware::stream::MiddlewareStream; @@ -46,7 +46,13 @@ const ONE_HOUR_AS_SECS: u32 = 60 * 60; /// [7873]: https://datatracker.ietf.org/doc/html/rfc7873 /// [9018]: https://datatracker.ietf.org/doc/html/rfc7873 #[derive(Clone, Debug)] -pub struct CookiesMiddlewareSvc { +pub struct CookiesMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -70,6 +76,11 @@ pub struct CookiesMiddlewareSvc { impl CookiesMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { /// Creates an instance of this middleware service. #[must_use] @@ -108,10 +119,10 @@ impl impl CookiesMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, - RequestMeta: Clone + Default, NextSvc: Service, - NextSvc::Target: Composer + Default, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { /// Get the DNS COOKIE, if any, for the given message. /// @@ -457,11 +468,10 @@ where impl Service for CookiesMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, - RequestMeta: Clone + Default, NextSvc: Service, - NextSvc::Target: Composer + Default, NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Send + Sync + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -534,7 +544,7 @@ mod tests { ); fn my_service( - _req: Request>, + _req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/edns.rs b/src/net/server/middleware/edns.rs index 880a12b8c..cef77a066 100644 --- a/src/net/server/middleware/edns.rs +++ b/src/net/server/middleware/edns.rs @@ -12,7 +12,6 @@ use crate::base::iana::OptRcode; use crate::base::message_builder::AdditionalBuilder; use crate::base::opt::keepalive::IdleTimeout; use crate::base::opt::{ComposeOptData, Opt, OptRecord, TcpKeepalive}; -use crate::base::wire::Composer; use crate::base::{Message, Name, StreamTarget}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::middleware::stream::MiddlewareStream; @@ -47,7 +46,13 @@ const EDNS_VERSION_ZERO: u8 = 0; /// [7828]: https://datatracker.ietf.org/doc/html/rfc7828 /// [9210]: https://datatracker.ietf.org/doc/html/rfc9210 #[derive(Clone, Debug, Default)] -pub struct EdnsMiddlewareSvc { +pub struct EdnsMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -63,6 +68,11 @@ pub struct EdnsMiddlewareSvc { impl EdnsMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { /// Creates an instance of this middleware service. #[must_use] @@ -83,10 +93,10 @@ impl impl EdnsMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default, + NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { fn preprocess( &self, @@ -411,11 +421,10 @@ where impl Service for EdnsMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, - RequestMeta: Clone + Default + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, NextSvc::Future: Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, + RequestMeta: Clone + Default + Unpin + Send + Sync + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -431,8 +440,7 @@ where Once::Item>>, ::Item, >; - type Future = core::future::Ready; - + type Future = Ready; fn call( &self, mut request: Request, @@ -568,7 +576,7 @@ mod tests { ); fn my_service( - req: Request>, + req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/mandatory.rs b/src/net/server/middleware/mandatory.rs index 933b21afb..5174090b9 100644 --- a/src/net/server/middleware/mandatory.rs +++ b/src/net/server/middleware/mandatory.rs @@ -11,7 +11,7 @@ use tracing::{debug, error, trace, warn}; use crate::base::iana::{Opcode, OptRcode}; use crate::base::message_builder::{AdditionalBuilder, PushError}; -use crate::base::wire::{Composer, ParseError}; +use crate::base::wire::ParseError; use crate::base::{Message, StreamTarget}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::service::{CallResult, Service, ServiceResult}; @@ -41,7 +41,13 @@ pub const MINIMUM_RESPONSE_BYTE_LEN: u16 = 512; /// [2181]: https://datatracker.ietf.org/doc/html/rfc2181 /// [9619]: https://datatracker.ietf.org/doc/html/rfc9619 #[derive(Clone, Debug)] -pub struct MandatoryMiddlewareSvc { +pub struct MandatoryMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -55,6 +61,11 @@ pub struct MandatoryMiddlewareSvc { impl MandatoryMiddlewareSvc +where + NextSvc: Service, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { /// Creates an instance of this middleware service. /// @@ -84,10 +95,10 @@ impl impl MandatoryMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin, NextSvc: Service, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default, + NextSvc::Future: Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { /// Truncate the given response message if it is too large. /// @@ -303,11 +314,10 @@ where impl Service for MandatoryMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, NextSvc: Service, NextSvc::Future: Unpin, - NextSvc::Target: Composer + Default, - RequestMeta: Clone + Default + Unpin, + RequestMeta: Clone + Default + 'static + Send + Sync + Unpin, + RequestOctets: Octets + Send + Sync + 'static + Unpin + Clone, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -331,6 +341,7 @@ where ) -> Self::Future { match self.preprocess(request.message()) { ControlFlow::Continue(()) => { + let request = request.with_new_metadata(Default::default()); let svc_call_fut = self.next_svc.call(request.clone()); let map = PostprocessingStream::new( svc_call_fut, @@ -341,6 +352,7 @@ where ready(MiddlewareStream::Map(map)) } ControlFlow::Break(mut response) => { + let request = request.with_new_metadata(Default::default()); Self::postprocess(&request, &mut response, self.strict); ready(MiddlewareStream::Result(once(ready(Ok( CallResult::new(response), @@ -457,7 +469,7 @@ mod tests { ); fn my_service( - req: Request>, + req: Request, ()>, _meta: (), ) -> ServiceResult> { // For each request create a single response: diff --git a/src/net/server/middleware/notify.rs b/src/net/server/middleware/notify.rs index 2eef038ef..fa0e465f9 100644 --- a/src/net/server/middleware/notify.rs +++ b/src/net/server/middleware/notify.rs @@ -61,7 +61,6 @@ use crate::base::message::CopyRecordsError; use crate::base::message_builder::AdditionalBuilder; use crate::base::name::Name; use crate::base::net::IpAddr; -use crate::base::wire::Composer; use crate::base::{ Message, ParsedName, Question, Rtype, StreamTarget, ToName, }; @@ -80,7 +79,15 @@ use crate::rdata::AllRecordData; /// /// [RFC 1996]: https://www.rfc-editor.org/info/rfc1996 #[derive(Clone, Debug)] -pub struct NotifyMiddlewareSvc { +pub struct NotifyMiddlewareSvc +where + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -93,6 +100,13 @@ pub struct NotifyMiddlewareSvc { impl NotifyMiddlewareSvc +where + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, { /// Creates an instance of this middleware service. /// @@ -111,11 +125,12 @@ impl impl NotifyMiddlewareSvc where - RequestOctets: Octets + Send + Sync, - RequestMeta: Clone + Default, - NextSvc: Service, - NextSvc::Target: Composer + Default, - N: Clone + Notifiable + Sync + Send, + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, + RequestMeta: Clone + Default + Sync + Send + 'static, + for<'a> ::Range<'a>: Send + Sync, { /// Pre-process received DNS NOTIFY queries. /// @@ -326,18 +341,12 @@ impl Service for NotifyMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static, + NextSvc: Service + Unpin + Clone, + NextSvc::Future: Sync + Unpin, + N: Notifiable + Clone + Sync + Send + 'static, + RequestOctets: Octets + Send + Sync + 'static + Clone, RequestMeta: Clone + Default + Sync + Send + 'static, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service - + Clone - + 'static - + Send - + Sync - + Unpin, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - N: Notifiable + Clone + Sync + Send + 'static, { type Target = NextSvc::Target; type Stream = MiddlewareStream< diff --git a/src/net/server/middleware/tsig.rs b/src/net/server/middleware/tsig.rs index 195fe3484..4edb9add3 100644 --- a/src/net/server/middleware/tsig.rs +++ b/src/net/server/middleware/tsig.rs @@ -66,20 +66,33 @@ use futures_util::Stream; /// Upstream services can detect whether a request is signed and with which /// key by consuming the `Option` metadata output by this service. #[derive(Clone, Debug)] -pub struct TsigMiddlewareSvc +pub struct TsigMiddlewareSvc where + Infallible: From<>>::Error>, KS: Clone + KeyStore, + KS::Key: Clone, + NextSvc: Service>, + NextSvc::Target: Composer + Default, + RequestOctets: Octets + OctetsFrom> + Send + Sync + Unpin, { next_svc: NextSvc, key_store: KS, - _phantom: PhantomData, + _phantom: PhantomData<(RequestOctets, IgnoredRequestMeta)>, } -impl TsigMiddlewareSvc +impl + TsigMiddlewareSvc where - KS: Clone + KeyStore, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, + Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, + NextSvc: Service>, + NextSvc::Future: Unpin, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { /// Creates an instance of this middleware service. /// @@ -95,18 +108,21 @@ where } } -impl TsigMiddlewareSvc +impl + TsigMiddlewareSvc where - RequestOctets: Octets + OctetsFrom> + Send + Sync + Unpin, - NextSvc: Service>, - NextSvc::Target: Composer + Default, - KS: Clone + KeyStore, - KS::Key: Clone, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, + NextSvc: Service>, + NextSvc::Future: Unpin, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { #[allow(clippy::type_complexity)] fn preprocess( - req: &Request, + req: &Request, key_store: &KS, ) -> Result< ControlFlow< @@ -188,7 +204,7 @@ where /// Sign the given response, or if necessary construct and return an /// alternate response. fn postprocess( - request: &Request, + request: &Request, response: &mut AdditionalBuilder>, state: &mut PostprocessingState, ) -> Result< @@ -272,7 +288,7 @@ where } fn mk_signed_truncated_response( - request: &Request, + request: &Request, truncation_ctx: TruncationContext, ) -> Result>, ServiceError> { @@ -334,7 +350,7 @@ where } fn map_stream_item( - request: Request, + request: Request, stream_item: ServiceResult, pp_config: &mut PostprocessingState, ) -> ServiceResult { @@ -396,17 +412,18 @@ where /// and (b) because this service does not propagate the metadata it receives /// from downstream but instead outputs [`Option`] metadata to /// upstream services. -impl Service - for TsigMiddlewareSvc +impl + Service + for TsigMiddlewareSvc where - RequestOctets: - Octets + OctetsFrom> + Send + Sync + 'static + Unpin, + IgnoredRequestMeta: Default + Clone + Send + Sync + Unpin + 'static, + Infallible: From<>>::Error>, + KS: Clone + KeyStore + Unpin + Send + Sync + 'static, + KS::Key: Clone + Unpin + Send + Sync, NextSvc: Service>, NextSvc::Future: Unpin, - NextSvc::Target: Composer + Default, - KS: Clone + KeyStore + Unpin, - KS::Key: Clone + Unpin, - Infallible: From<>>::Error>, + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin + Clone, { type Target = NextSvc::Target; type Stream = MiddlewareStream< @@ -416,7 +433,7 @@ where RequestOctets, NextSvc::Future, NextSvc::Stream, - (), + IgnoredRequestMeta, PostprocessingState, >, Once>>, @@ -424,7 +441,10 @@ where >; type Future = Ready; - fn call(&self, request: Request) -> Self::Future { + fn call( + &self, + request: Request, + ) -> Self::Future { match Self::preprocess(&request, &self.key_store) { Ok(ControlFlow::Continue(Some((modified_req, signer)))) => { let pp_config = PostprocessingState::new(signer); diff --git a/src/net/server/middleware/xfr/data_provider.rs b/src/net/server/middleware/xfr/data_provider.rs index 65aae2619..a274da5d8 100644 --- a/src/net/server/middleware/xfr/data_provider.rs +++ b/src/net/server/middleware/xfr/data_provider.rs @@ -85,7 +85,7 @@ impl XfrData { //------------ XfrDataProvider ------------------------------------------------ /// A provider of data needed for responding to XFR requests. -pub trait XfrDataProvider { +pub trait XfrDataProvider { type Diff: ZoneDiff + Send + Sync; /// Request data needed to respond to an XFR request. diff --git a/src/net/server/middleware/xfr/service.rs b/src/net/server/middleware/xfr/service.rs index d0862f5e7..8f330eaba 100644 --- a/src/net/server/middleware/xfr/service.rs +++ b/src/net/server/middleware/xfr/service.rs @@ -16,7 +16,6 @@ use tokio_stream::wrappers::UnboundedReceiverStream; use tracing::{debug, error, info, trace, warn}; use crate::base::iana::{Opcode, OptRcode}; -use crate::base::wire::Composer; use crate::base::{Message, ParsedName, Question, Rtype, Serial, ToName}; use crate::net::server::message::{Request, TransportSpecificContext}; use crate::net::server::middleware::stream::MiddlewareStream; @@ -55,7 +54,18 @@ const MAX_TCP_MSG_BYTE_LEN: u16 = u16::MAX; /// /// [module documentation]: crate::net::server::middleware::xfr #[derive(Clone, Debug)] -pub struct XfrMiddlewareSvc { +pub struct XfrMiddlewareSvc +where + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, + for<'a> ::Range<'a>: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, +{ /// The upstream [`Service`] to pass requests to and receive responses /// from. next_svc: NextSvc, @@ -78,7 +88,15 @@ pub struct XfrMiddlewareSvc { impl XfrMiddlewareSvc where - XDP: XfrDataProvider, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, + for<'a> ::Range<'a>: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, { /// Creates a new instance of this middleware. /// @@ -110,14 +128,15 @@ where impl XfrMiddlewareSvc where - RequestOctets: Octets + Send + Sync + 'static + Unpin, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service + Clone + Send + Sync + 'static, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - NextSvc::Stream: Send + Sync, - XDP: XfrDataProvider, - XDP::Diff: Debug + 'static, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, + XDP: XfrDataProvider + Clone + Sync + Send + 'static, + XDP::Diff: Debug + Sync, + RequestMeta: Clone + Default + Sync + Send + 'static, { /// Pre-process received DNS XFR queries. /// @@ -684,12 +703,12 @@ impl Service for XfrMiddlewareSvc where - RequestOctets: Octets + Send + Sync + Unpin + 'static, + RequestOctets: Octets + Send + Sync + Unpin + 'static + Clone, for<'a> ::Range<'a>: Send + Sync, - NextSvc: Service + Clone + Send + Sync + 'static, - NextSvc::Future: Send + Sync + Unpin, - NextSvc::Target: Composer + Default + Send + Sync, - NextSvc::Stream: Send + Sync, + NextSvc: + Service + Clone + Send + Sync + 'static, + NextSvc::Future: Sync + Unpin, + NextSvc::Stream: Sync, XDP: XfrDataProvider + Clone + Sync + Send + 'static, XDP::Diff: Debug + Sync, RequestMeta: Clone + Default + Sync + Send + 'static, @@ -721,7 +740,8 @@ where .await { Ok(ControlFlow::Continue(())) => { - let request = request.with_new_metadata(()); + let request = + request.with_new_metadata(Default::default()); let stream = next_svc.call(request).await; MiddlewareStream::IdentityStream(stream) } diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index 41e6f7539..79beff5d6 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -59,7 +59,7 @@ async fn axfr_with_example_zone() { "../../../../../test-data/zonefiles/nsd-example.txt" )); - let req = mk_axfr_request(zone.apex_name(), ()); + let req = mk_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -127,7 +127,7 @@ async fn axfr_multi_response() { "../../../../../test-data/zonefiles/big.example.com.txt" )); - let req = mk_axfr_request(zone.apex_name(), ()); + let req = mk_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -205,7 +205,7 @@ async fn axfr_not_allowed_over_udp() { "../../../../../test-data/zonefiles/nsd-example.txt" )); - let req = mk_udp_axfr_request(zone.apex_name(), ()); + let req = mk_udp_axfr_request(zone.apex_name(), Default::default()); let res = do_preprocess(zone, &req).await.unwrap(); @@ -247,7 +247,8 @@ JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2 let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), vec![]); // The following IXFR query - let req = mk_udp_ixfr_request(zone.apex_name(), Serial(1), ()); + let req = + mk_udp_ixfr_request(zone.apex_name(), Serial(1), Default::default()); let res = do_preprocess(zone_with_diffs, &req).await.unwrap(); @@ -385,7 +386,8 @@ JAIN-BB.JAIN.AD.JP. IN A 192.41.197.2 let zone_with_diffs = ZoneWithDiffs::new(zone.clone(), diffs); // The following IXFR query - let req = mk_ixfr_request(zone.apex_name(), Serial(1), ()); + let req = + mk_ixfr_request(zone.apex_name(), Serial(1), Default::default()); let res = do_preprocess(zone_with_diffs, &req).await.unwrap(); @@ -482,7 +484,8 @@ async fn ixfr_rfc1995_section7_udp_packet_overflow() { "../../../../../test-data/zonefiles/big.example.com.txt" )); - let req = mk_udp_ixfr_request(zone.apex_name(), Serial(0), ()); + let req = + mk_udp_ixfr_request(zone.apex_name(), Serial(0), Default::default()); let res = do_preprocess(zone.clone(), &req).await.unwrap(); @@ -508,6 +511,7 @@ async fn axfr_with_tsig_key() { // type over which the Request produced by TsigMiddlewareSvc is generic. // When the XfrMiddlewareSvc receives a Request it // passes it to the XfrDataProvider which in turn can inspect it. + #[derive(Clone)] struct KeyReceivingXfrDataProvider { key: Arc, checked: Arc, @@ -687,23 +691,24 @@ fn mk_ixfr_request_for_transport( Request::new(client_addr, received_at, msg, transport_specific, metadata) } -async fn do_preprocess>( +async fn do_preprocess( zone: XDP, - req: &Request, RequestMeta>, + req: &Request, Option>>, ) -> Result< ControlFlow< XfrMiddlewareStream< - ::Future, - ::Stream, - <::Stream as Stream>::Item, + , Option>>>::Future, + , Option>>>::Stream, + <, Option>>>::Stream as Stream>::Item, >, >, OptRcode, > where + XDP: XfrDataProvider>> + Clone + Sync + Send + 'static, XDP::Diff: Debug + 'static, { - XfrMiddlewareSvc::, TestNextSvc, RequestMeta, XDP>::preprocess( + XfrMiddlewareSvc::, TestNextSvc, Option>, XDP>::preprocess( Arc::new(Semaphore::new(1)), Arc::new(Semaphore::new(1)), req, @@ -773,16 +778,20 @@ async fn assert_stream_eq< #[derive(Clone)] struct TestNextSvc; -impl Service, ()> for TestNextSvc { +impl Service, Option>> for TestNextSvc { type Target = Vec; type Stream = Once>>; type Future = Ready; - fn call(&self, _request: Request, ()>) -> Self::Future { + fn call( + &self, + _request: Request, Option>>, + ) -> Self::Future { todo!() } } +#[derive(Clone)] struct ZoneWithDiffs { zone: Zone, diffs: Vec>, @@ -808,11 +817,11 @@ impl ZoneWithDiffs { } } -impl XfrDataProvider for ZoneWithDiffs { +impl XfrDataProvider for ZoneWithDiffs { type Diff = Arc; fn request( &self, - req: &Request, + req: &Request, diff_from: Option, ) -> Pin< Box< diff --git a/src/net/server/qname_router.rs b/src/net/server/qname_router.rs index 749b8f7a5..46c2660e2 100644 --- a/src/net/server/qname_router.rs +++ b/src/net/server/qname_router.rs @@ -22,21 +22,24 @@ use tracing::trace; /// A service that routes requests to other services based on the Qname in the /// request. -pub struct QnameRouter { +pub struct QnameRouter { /// List of names and services for routing requests. - list: Vec>, + list: Vec>, } /// Element in the name space for the Qname router. -struct Element { +struct Element { /// Name to match for this element. name: Name, /// Service to call for this element. - service: Box + Send + Sync>, + service: + Box + Send + Sync>, } -impl QnameRouter { +impl + QnameRouter +{ /// Create a new empty router. pub fn new() -> Self { Self { list: Vec::new() } @@ -50,7 +53,10 @@ impl QnameRouter { EmptyBuilder + OctetsBuilder, TN: ToName, RequestOcts: Send + Sync, - SVC: SingleService + Send + Sync + 'static, + SVC: SingleService + + Send + + Sync + + 'static, { let el = Element { name: name.to_name(), @@ -60,22 +66,26 @@ impl QnameRouter { } } -impl Default for QnameRouter { +impl Default + for QnameRouter +{ fn default() -> Self { Self::new() } } -impl SingleService - for QnameRouter +impl + SingleService + for QnameRouter where Octs: AsRef<[u8]>, + RequestMeta: Clone, RequestOcts: Send + Sync, CR: ComposeReply + Send + Sync + 'static, { fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]> + Octets, diff --git a/src/net/server/service.rs b/src/net/server/service.rs index 13e177bd9..ee92e4170 100644 --- a/src/net/server/service.rs +++ b/src/net/server/service.rs @@ -7,11 +7,10 @@ use core::fmt::Display; use core::ops::Deref; use std::time::Duration; -use std::vec::Vec; use crate::base::iana::Rcode; use crate::base::message_builder::{AdditionalBuilder, PushError}; -use crate::base::wire::ParseError; +use crate::base::wire::{Composer, ParseError}; use crate::base::StreamTarget; use super::message::Request; @@ -59,7 +58,7 @@ pub type ServiceResult = Result, ServiceError>; /// use domain::rdata::A; /// /// fn mk_answer( -/// msg: &Request>, +/// msg: &Request, ()>, /// builder: MessageBuilder>>, /// ) -> AdditionalBuilder>> { /// let mut answer = builder @@ -74,7 +73,7 @@ pub type ServiceResult = Result, ServiceError>; /// answer.additional() /// } /// -/// fn mk_response_stream(msg: &Request>) +/// fn mk_response_stream(msg: &Request, ()>) /// -> Once>>> /// { /// let builder = mk_builder_for_target(); @@ -86,14 +85,14 @@ pub type ServiceResult = Result, ServiceError>; /// //------------ A synchronous service example ------------------------------ /// struct MySyncService; /// -/// impl Service> for MySyncService { +/// impl Service, ()> for MySyncService { /// type Target = Vec; /// type Stream = Once>>; /// type Future = Ready; /// /// fn call( /// &self, -/// msg: Request>, +/// msg: Request, ()>, /// ) -> Self::Future { /// ready(mk_response_stream(&msg)) /// } @@ -102,21 +101,21 @@ pub type ServiceResult = Result, ServiceError>; /// //------------ An anonymous async block service example ------------------- /// struct MyAsyncBlockService; /// -/// impl Service> for MyAsyncBlockService { +/// impl Service, ()> for MyAsyncBlockService { /// type Target = Vec; /// type Stream = Once>>; -/// type Future = Pin>>; +/// type Future = Pin + Send>>; /// /// fn call( /// &self, -/// msg: Request>, +/// msg: Request, ()>, /// ) -> Self::Future { /// Box::pin(async move { mk_response_stream(&msg) }) /// } /// } /// /// //------------ A named Future service example ----------------------------- -/// struct MyFut(Request>); +/// struct MyFut(Request, ()>); /// /// impl std::future::Future for MyFut { /// type Output = Once>>>; @@ -128,12 +127,12 @@ pub type ServiceResult = Result, ServiceError>; /// /// struct MyNamedFutureService; /// -/// impl Service> for MyNamedFutureService { +/// impl Service, ()> for MyNamedFutureService { /// type Target = Vec; /// type Stream = Once>>; /// type Future = MyFut; /// -/// fn call(&self, msg: Request>) -> Self::Future { MyFut(msg) } +/// fn call(&self, msg: Request, ()>) -> Self::Future { MyFut(msg) } /// } /// ``` /// @@ -167,19 +166,20 @@ pub type ServiceResult = Result, ServiceError>; /// [`call`]: Self::call() /// [`service_fn`]: crate::net::server::util::service_fn() pub trait Service< - RequestOctets: AsRef<[u8]> + Send + Sync = Vec, - RequestMeta: Clone + Default = (), -> + RequestOctets: AsRef<[u8]> + Send + Sync, + RequestMeta: Clone + Default, +>: Send + Sync + 'static { /// The underlying byte storage type used to hold generated responses. - type Target; + type Target: Composer + Default + Send + Sync; /// The type of stream that the service produces. type Stream: futures_util::stream::Stream> - + Unpin; + + Unpin + + Send; /// The type of future that will yield the service result stream. - type Future: core::future::Future; + type Future: core::future::Future + Send; /// Generate a response to a fully pre-processed request. fn call( @@ -195,8 +195,8 @@ impl Service for U where RequestOctets: Unpin + Send + Sync + AsRef<[u8]>, - T: ?Sized + Service, - U: Deref + Clone, + T: Service, + U: Deref + Clone + Send + Sync + 'static, RequestMeta: Clone + Default, { type Target = T::Target; diff --git a/src/net/server/single_service.rs b/src/net/server/single_service.rs index 28c6d19fe..73035635c 100644 --- a/src/net/server/single_service.rs +++ b/src/net/server/single_service.rs @@ -22,13 +22,13 @@ use std::pin::Pin; use std::vec::Vec; /// Trait for a service that results in a single response. -pub trait SingleService { +pub trait SingleService { /// Call the service with a request message. /// /// The service returns a boxed future. fn call( &self, - request: Request, + request: Request, ) -> Pin> + Send + Sync>> where RequestOcts: AsRef<[u8]> + Octets; diff --git a/src/net/server/stream.rs b/src/net/server/stream.rs index c22b39d36..8749bf1d5 100644 --- a/src/net/server/stream.rs +++ b/src/net/server/stream.rs @@ -13,17 +13,20 @@ //! > the Internet._ //! //! [stream]: https://en.wikipedia.org/wiki/Reliable_byte_streamuse -use arc_swap::ArcSwap; use core::future::poll_fn; use core::ops::Deref; use core::sync::atomic::{AtomicUsize, Ordering}; use core::time::Duration; -use octseq::Octets; + use std::fmt::Debug; use std::io; use std::net::SocketAddr; use std::string::{String, ToString}; use std::sync::{Arc, Mutex}; + +use arc_swap::ArcSwap; +use octseq::Octets; +use tokio::io::{AsyncRead, AsyncWrite}; use tokio::net::TcpListener; use tokio::sync::watch; use tokio::time::{interval, timeout, MissedTickBehavior}; @@ -39,8 +42,6 @@ use crate::utils::config::DefMinMax; use super::buf::VecBufSource; use super::connection::{self, Connection}; use super::ServerCommand; -use crate::base::wire::Composer; -use tokio::io::{AsyncRead, AsyncWrite}; // TODO: Should this crate also provide a TLS listener implementation? @@ -229,7 +230,7 @@ type CommandReceiver = watch::Receiver; /// use domain::net::server::stream::StreamServer; /// use domain::net::server::util::service_fn; /// -/// fn my_service(msg: Request>, _meta: ()) -> ServiceResult> +/// fn my_service(msg: Request, ()>, _meta: ()) -> ServiceResult> /// { /// todo!() /// } @@ -270,8 +271,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, // + 'static, + Svc: Service + Clone, { /// The configuration of the server. config: Arc>, @@ -315,8 +315,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Creates a new [`StreamServer`] instance. /// @@ -395,8 +394,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Debug + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Get a reference to the source for this server. #[must_use] @@ -418,8 +416,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Start the server. /// @@ -435,10 +432,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { if let Err(err) = self.run_until_error().await { error!("Server stopped due to error: {err}"); @@ -513,8 +506,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { /// Accept stream connections until shutdown or fatal error. async fn run_until_error(&self) -> Result<(), String> @@ -524,10 +516,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { let mut command_rx = self.command_rx.clone(); @@ -646,10 +634,6 @@ where Listener::Error: Send, Listener::Future: Send + 'static, Listener::StreamType: AsyncRead + AsyncWrite + Send + Sync + 'static, - Svc: 'static, - Svc::Target: Composer + Send + Sync, - Svc::Stream: Send, - Svc::Future: Send, { // Work around the compiler wanting to move self to the async block by // preparing only those pieces of information from self for the new @@ -713,8 +697,7 @@ where Listener: AsyncAccept + Send + Sync, Buf: BufSource + Send + Sync + Clone, Buf::Output: Octets + Send + Sync + Unpin, - Svc: Service + Send + Sync + Clone, - Svc::Target: Composer + Default, + Svc: Service + Clone, { fn drop(&mut self) { // Shutdown the StreamServer. Don't handle the failure case here as diff --git a/src/net/server/tests/integration.rs b/src/net/server/tests/integration.rs index 26bc906f4..5daf9cf07 100644 --- a/src/net/server/tests/integration.rs +++ b/src/net/server/tests/integration.rs @@ -22,7 +22,6 @@ use tracing::{trace, warn}; use crate::base::iana::{Class, Rcode}; use crate::base::name::ToName; use crate::base::net::IpAddr; -use crate::base::wire::Composer; use crate::base::Name; use crate::base::Rtype; use crate::logging::init_logging; @@ -226,17 +225,14 @@ fn mk_servers( Arc>, ) where - Svc: Clone + Service + Send + Sync, - ::Future: Send, - ::Target: Composer + Default + Send + Sync, - ::Stream: Send, + Svc: Service, ()> + Clone, { // Prepare middleware to be used by the DNS servers to pre-process // received requests and post-process created responses. let (dgram_config, stream_config) = mk_server_configs(server_config); // Create a dgram server for handling UDP requests. - let dgram_server = DgramServer::<_, _, Svc>::with_config( + let dgram_server = DgramServer::with_config( dgram_server_conn.clone(), VecBufSource, service.clone(), diff --git a/src/net/server/tests/unit.rs b/src/net/server/tests/unit.rs index c14375c77..7a5c9f081 100644 --- a/src/net/server/tests/unit.rs +++ b/src/net/server/tests/unit.rs @@ -329,6 +329,7 @@ impl futures_util::stream::Stream for MySingle { /// A mock service that returns MySingle whenever it receives a message. /// Just to show MySingle in action. +#[derive(Clone)] struct MyService; impl MyService { @@ -337,12 +338,15 @@ impl MyService { } } -impl Service> for MyService { +impl Service, ()> for MyService +where + Self: Clone + Send + Sync + 'static, +{ type Target = Vec; type Stream = MySingle; type Future = Ready; - fn call(&self, request: Request>) -> Self::Future { + fn call(&self, request: Request, ()>) -> Self::Future { trace!("Processing request id {}", request.message().header().id()); ready(MySingle::new()) } diff --git a/src/net/server/util.rs b/src/net/server/util.rs index 220b6171f..a9c839723 100644 --- a/src/net/server/util.rs +++ b/src/net/server/util.rs @@ -77,7 +77,7 @@ where /// // provide, and returns one or more DNS responses. /// // /// // Note that using `service_fn()` does not permit you to use async code! -/// fn my_service(req: Request>, _meta: MyMeta) +/// fn my_service(req: Request, ()>, _meta: MyMeta) /// -> ServiceResult> /// { /// let builder = mk_builder_for_target(); @@ -134,12 +134,13 @@ where RequestOctets: AsRef<[u8]> + Send + Sync + Unpin, RequestMeta: Default + Clone, Metadata: Clone, - Target: Composer + Default, + Target: Composer + Default + Send + Sync, T: Fn( Request, Metadata, ) -> ServiceResult + Clone, + Self: Clone + Send + Sync + 'static, { type Target = Target; type Stream = Once>>; From e4e8d8b4868e665ee4a6fd995a334287ecef1e51 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:25:28 +0200 Subject: [PATCH 437/569] Generalize `ZoneUpdater` to support any `Record` type, not just `ParsedRecord`. --- src/zonetree/update.rs | 110 +++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index b50d0c1e8..460f64095 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -4,17 +4,17 @@ //! content of zones without requiring knowledge of the low-level details of //! how the [`WritableZone`] trait implemented by [`Zone`] works. use core::future::Future; +use core::marker::PhantomData; use core::pin::Pin; -use std::borrow::ToOwned; use std::boxed::Box; use bytes::Bytes; use tracing::trace; use crate::base::name::{FlattenInto, Label}; -use crate::base::{ParsedName, Record, Rtype}; -use crate::net::xfr::protocol::ParsedRecord; +use crate::base::scan::ScannerError; +use crate::base::{Name, Record, Rtype, ToName}; use crate::rdata::ZoneRecordData; use crate::zonetree::{Rrset, SharedRrset}; @@ -51,7 +51,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// /// ``` /// # use std::str::FromStr; -/// # +/// # use bytes::Bytes; /// # use domain::base::iana::Class; /// # use domain::base::MessageBuilder; /// # use domain::base::Name; @@ -76,8 +76,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), @@ -106,7 +106,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// /// ```rust /// # use std::str::FromStr; -/// # +/// # use bytes::Bytes; /// # use domain::base::iana::Class; /// # use domain::base::MessageBuilder; /// # use domain::base::Name; @@ -133,8 +133,8 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// # /// # // Prepare some records to pass to ZoneUpdater /// # let serial = Serial::now(); -/// # let mname = ParsedName::from(Name::from_str("mname").unwrap()); -/// # let rname = ParsedName::from(Name::from_str("rname").unwrap()); +/// # let mname = ParsedName::from(Name::::from_str("mname").unwrap()); +/// # let rname = ParsedName::from(Name::::from_str("rname").unwrap()); /// # let ttl = Ttl::from_secs(0); /// # let new_soa_rec = Record::new( /// # ParsedName::from(Name::from_str("example.com").unwrap()), @@ -214,7 +214,7 @@ use super::{InMemoryZoneDiff, WritableZone, WritableZoneNode, Zone}; /// ``` /// /// [`apply()`]: ZoneUpdater::apply() -pub struct ZoneUpdater { +pub struct ZoneUpdater { /// The zone to be updated. zone: Zone, @@ -226,9 +226,15 @@ pub struct ZoneUpdater { /// The current state of the updater. state: ZoneUpdaterState, + + _phantom: PhantomData, } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Creates a new [`ZoneUpdater`] that will update the given [`Zone`] /// content. /// @@ -246,12 +252,17 @@ impl ZoneUpdater { zone, write, state: Default::default(), + _phantom: PhantomData, }) }) } } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Apply the given [`ZoneUpdate`] to the [`Zone`] being updated. /// /// Returns `Ok` on success, `Err` otherwise. On success, if changes were @@ -266,7 +277,7 @@ impl ZoneUpdater { /// progress and re-open the zone for editing again. pub async fn apply( &mut self, - update: ZoneUpdate, + update: ZoneUpdate>>, ) -> Result, Error> { trace!("Update: {update}"); @@ -344,7 +355,11 @@ impl ZoneUpdater { } } -impl ZoneUpdater { +impl ZoneUpdater +where + N: ToName + Clone, + ZoneRecordData: FlattenInto>>, +{ /// Given a zone record, obtain a [`WritableZoneNode`] for the owner. /// /// A [`Zone`] is a tree structure which can be modified by descending the @@ -364,7 +379,7 @@ impl ZoneUpdater { /// the record owner name. async fn get_writable_child_node_for_owner( &mut self, - rec: &ParsedRecord, + rec: &Record>, ) -> Result>, Error> { let mut it = rel_name_rev_iter(self.zone.apex_name(), rec.owner())?; @@ -386,17 +401,19 @@ impl ZoneUpdater { /// Create or update the SOA RRset using the given SOA record. async fn update_soa( &mut self, - new_soa: Record< - ParsedName, - ZoneRecordData>, - >, + new_soa: Record>, ) -> Result<(), Error> { if new_soa.rtype() != Rtype::SOA { return Err(Error::NotSoaRecord); } let mut rrset = Rrset::new(Rtype::SOA, new_soa.ttl()); - rrset.push_data(new_soa.data().to_owned().flatten_into()); + let Ok(flattened) = new_soa.data().clone().try_flatten_into() else { + return Err(Error::IoError(std::io::Error::custom( + "Unable to flatten bytes", + ))); + }; + rrset.push_data(flattened); self.write .update_root_rrset(SharedRrset::new(rrset)) .await?; @@ -407,10 +424,7 @@ impl ZoneUpdater { /// Find and delete a resource record in the zone by exact match. async fn delete_record_from_rrset( &mut self, - rec: Record< - ParsedName, - ZoneRecordData>, - >, + rec: Record>, ) -> Result<(), Error> { // Find or create the point to edit in the node tree. let tree_node = self.get_writable_child_node_for_owner(&rec).await?; @@ -443,11 +457,12 @@ impl ZoneUpdater { /// Add a resource record to a new or existing RRset. async fn add_record_to_rrset( &mut self, - rec: Record< - ParsedName, - ZoneRecordData>, - >, - ) -> Result<(), Error> { + rec: Record>, + ) -> Result<(), Error> + where + ZoneRecordData: + FlattenInto>>, + { // Find or create the point to edit in the node tree. let tree_node = self.get_writable_child_node_for_owner(&rec).await?; let tree_node = tree_node.as_ref().unwrap_or(self.write.root()); @@ -456,7 +471,11 @@ impl ZoneUpdater { // RRset in the tree plus the one to add. let mut rrset = Rrset::new(rec.rtype(), rec.ttl()); let rtype = rec.rtype(); - let data = rec.into_data().flatten_into(); + let Ok(data) = rec.into_data().try_flatten_into() else { + return Err(Error::IoError(std::io::Error::custom( + "Unable to flatten bytes", + ))); + }; rrset.push_data(data); @@ -622,7 +641,6 @@ mod tests { use crate::base::{ Message, MessageBuilder, Name, ParsedName, Record, Serial, Ttl, }; - use crate::logging::init_logging; use crate::net::xfr::protocol::XfrResponseInterpreter; use crate::rdata::{Ns, Soa, A}; use crate::zonetree::ZoneBuilder; @@ -905,7 +923,7 @@ mod tests { // IN NS NS.JAIN.AD.JP. let ns_1 = Record::new( - ParsedName::from(Name::from_str("JAIN.AD.JP.").unwrap()), + ParsedName::from(Name::::from_str("JAIN.AD.JP.").unwrap()), Class::IN, Ttl::from_secs(0), Ns::new(ParsedName::from( @@ -920,7 +938,9 @@ mod tests { // NS.JAIN.AD.JP. IN A 133.69.136.1 let a_1 = Record::new( - ParsedName::from(Name::from_str("NS.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NS.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 1)).into(), @@ -932,7 +952,9 @@ mod tests { // NEZU.JAIN.AD.JP. IN A 133.69.136.5 let nezu = Record::new( - ParsedName::from(Name::from_str("NEZU.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("NEZU.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 5)).into(), @@ -957,7 +979,9 @@ mod tests { .await .unwrap(); let a_2 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 4)).into(), @@ -992,7 +1016,9 @@ mod tests { .await .unwrap(); let a_4 = Record::new( - ParsedName::from(Name::from_str("JAIN-BB.JAIN.AD.JP.").unwrap()), + ParsedName::from( + Name::::from_str("JAIN-BB.JAIN.AD.JP.").unwrap(), + ), Class::IN, Ttl::from_secs(0), A::new(Ipv4Addr::new(133, 69, 136, 3)).into(), @@ -1240,6 +1266,18 @@ mod tests { //------------ Helper functions ------------------------------------------- + fn init_logging() { + // Initialize tracing based logging. Override with env var RUST_LOG, e.g. + // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step + // numbers and types as they are being executed. + tracing_subscriber::fmt() + .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_thread_ids(true) + .without_time() + .try_init() + .ok(); + } + fn mk_empty_zone(apex_name: &str) -> Zone { ZoneBuilder::new(Name::from_str(apex_name).unwrap(), Class::IN) .build() From b4202772b3b3366f4c70ebbf9a4f5ba1aeb8c3ac Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:33:34 +0200 Subject: [PATCH 438/569] Remove duplicate fn. --- src/zonetree/update.rs | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index 460f64095..2d49e5263 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -641,6 +641,7 @@ mod tests { use crate::base::{ Message, MessageBuilder, Name, ParsedName, Record, Serial, Ttl, }; + use crate::logging::init_logging; use crate::net::xfr::protocol::XfrResponseInterpreter; use crate::rdata::{Ns, Soa, A}; use crate::zonetree::ZoneBuilder; @@ -1266,18 +1267,6 @@ mod tests { //------------ Helper functions ------------------------------------------- - fn init_logging() { - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step - // numbers and types as they are being executed. - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); - } - fn mk_empty_zone(apex_name: &str) -> Zone { ZoneBuilder::new(Name::from_str(apex_name).unwrap(), Class::IN) .build() From cc3f8ae4fd0ca30595e6e6126cac23b396ebf186 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Jun 2025 10:58:59 +0200 Subject: [PATCH 439/569] Merge branch 'main' into patches-for-nameshed-prototype --- Cargo.lock | 474 +++-- Cargo.toml | 41 +- Changelog.md | 61 +- examples/client-cache-transport.rs | 85 + examples/client-transports.rs | 41 +- examples/keyset.rs | 2 +- macros/Cargo.toml | 28 + macros/src/data.rs | 159 ++ macros/src/impls.rs | 274 +++ macros/src/lib.rs | 632 ++++++ macros/src/repr.rs | 91 + src/base/iana/digestalg.rs | 14 +- src/base/iana/mod.rs | 8 +- src/base/iana/nsec3.rs | 8 +- src/base/iana/secalg.rs | 8 +- src/base/iana/zonemd.rs | 8 +- src/base/opt/algsig.rs | 44 +- src/base/record.rs | 6 +- src/base/scan.rs | 8 +- src/base/zonefile_fmt.rs | 14 +- src/crypto/common.rs | 290 +++ src/crypto/mod.rs | 133 ++ src/crypto/openssl.rs | 981 +++++++++ src/crypto/ring.rs | 765 +++++++ src/crypto/sign.rs | 1098 ++++++++++ src/dnssec/common.rs | 362 ++++ src/dnssec/mod.rs | 46 + src/dnssec/sign/config.rs | 43 + src/dnssec/sign/denial/config.rs | 62 + src/{ => dnssec}/sign/denial/mod.rs | 0 src/{ => dnssec}/sign/denial/nsec.rs | 138 +- src/{ => dnssec}/sign/denial/nsec3.rs | 561 ++---- src/dnssec/sign/error.rs | 81 + src/{ => dnssec}/sign/keys/keyset.rs | 4 +- src/{ => dnssec}/sign/keys/mod.rs | 14 - src/{ => dnssec}/sign/keys/signingkey.rs | 41 +- src/{ => dnssec}/sign/mod.rs | 230 +-- src/{ => dnssec}/sign/records.rs | 62 +- src/{ => dnssec}/sign/signatures/mod.rs | 1 - src/dnssec/sign/signatures/rrsigs.rs | 1237 ++++++++++++ src/{ => dnssec}/sign/test_util/mod.rs | 62 +- src/{ => dnssec}/sign/traits.rs | 272 ++- src/{ => dnssec}/validator/anchor.rs | 3 + src/dnssec/validator/base.rs | 751 +++++++ src/{ => dnssec}/validator/context.rs | 5 +- src/{ => dnssec}/validator/group.rs | 8 +- src/{ => dnssec}/validator/mod.rs | 20 +- src/{ => dnssec}/validator/nsec.rs | 12 +- src/{ => dnssec}/validator/utilities.rs | 0 src/lib.rs | 85 +- src/logging.rs | 21 + src/net/client/dgram.rs | 23 +- src/net/client/mod.rs | 32 +- src/net/client/multi_stream.rs | 2 +- src/net/client/request.rs | 26 +- src/net/client/stream.rs | 2 +- src/net/client/validator.rs | 22 +- src/net/client/validator_test.rs | 6 +- src/net/server/connection.rs | 1 - src/net/server/dgram.rs | 2 +- src/net/server/middleware/xfr/tests.rs | 8 +- src/net/server/tests/integration.rs | 71 +- src/net/server/tests/unit.rs | 20 +- src/net/xfr/protocol/iterator.rs | 20 +- src/net/xfr/protocol/tests.rs | 13 +- src/new/base/build/message.rs | 411 ++++ src/new/base/build/mod.rs | 210 ++ src/new/base/charstr.rs | 467 +++++ src/new/base/message.rs | 674 +++++++ src/new/base/mod.rs | 362 ++++ src/new/base/name/absolute.rs | 694 +++++++ src/new/base/name/compressor.rs | 699 +++++++ src/new/base/name/label.rs | 596 ++++++ src/new/base/name/mod.rs | 201 ++ src/new/base/name/reversed.rs | 604 ++++++ src/new/base/name/unparsed.rs | 223 +++ src/new/base/parse/message.rs | 140 ++ src/new/base/parse/mod.rs | 422 ++++ src/new/base/question.rs | 294 +++ src/new/base/record.rs | 662 ++++++ src/new/base/serial.rs | 125 ++ src/new/base/wire/build.rs | 291 +++ src/new/base/wire/ints.rs | 307 +++ src/new/base/wire/mod.rs | 105 + src/new/base/wire/parse.rs | 590 ++++++ src/new/base/wire/size_prefixed.rs | 382 ++++ src/new/edns/cookie.rs | 244 +++ src/new/edns/ext_err.rs | 224 +++ src/new/edns/mod.rs | 572 ++++++ src/new/mod.rs | 66 + src/new/rdata/basic/a.rs | 215 ++ src/new/rdata/basic/cname.rs | 199 ++ src/new/rdata/basic/hinfo.rs | 229 +++ src/new/rdata/basic/mod.rs | 27 + src/new/rdata/basic/mx.rs | 218 ++ src/new/rdata/basic/ns.rs | 204 ++ src/new/rdata/basic/ptr.rs | 193 ++ src/new/rdata/basic/soa.rs | 359 ++++ src/new/rdata/basic/txt.rs | 238 +++ src/new/rdata/dnssec/dnskey.rs | 139 ++ src/new/rdata/dnssec/ds.rs | 104 + src/new/rdata/dnssec/mod.rs | 69 + src/new/rdata/dnssec/nsec.rs | 179 ++ src/new/rdata/dnssec/nsec3.rs | 262 +++ src/new/rdata/dnssec/rrsig.rs | 105 + src/new/rdata/edns.rs | 368 ++++ src/new/rdata/ipv6.rs | 227 +++ src/new/rdata/mod.rs | 746 +++++++ src/rdata/cds.rs | 54 +- src/rdata/dnssec.rs | 86 +- src/rdata/nsec3.rs | 63 +- src/rdata/zonemd.rs | 14 +- src/resolv/stub/mod.rs | 17 +- src/sign/config.rs | 103 - src/sign/crypto/common.rs | 213 -- src/sign/crypto/mod.rs | 103 - src/sign/crypto/openssl.rs | 567 ------ src/sign/crypto/ring.rs | 442 ---- src/sign/denial/config.rs | 145 -- src/sign/error.rs | 270 --- src/sign/keys/bytes.rs | 509 ----- src/sign/keys/keymeta.rs | 247 --- src/sign/signatures/rrsigs.rs | 1864 ----------------- src/sign/signatures/strategy.rs | 111 -- src/stelline/README.md | 114 ++ src/stelline/client.rs | 251 +-- src/stelline/matches.rs | 866 ++++---- src/stelline/mod.rs | 1 + src/stelline/parse_stelline.rs | 459 ++--- src/stelline/server.rs | 20 +- src/utils/dst.rs | 447 +++++ src/utils/mod.rs | 2 + src/validate.rs | 1881 ------------------ src/zonefile/inplace.rs | 226 ++- src/zonetree/error.rs | 4 +- src/zonetree/in_memory/read.rs | 78 + src/zonetree/in_memory/write.rs | 28 +- src/zonetree/parsed.rs | 1 + src/zonetree/update.rs | 13 +- test-data/zonefiles/defaultclass.yaml | 23 + test-data/zonefiles/mixedclass.yaml | 16 + test-data/zonefiles/unknown-zero-length.yaml | 10 + tests/interop.rs | 2 +- 143 files changed, 23764 insertions(+), 8774 deletions(-) create mode 100644 examples/client-cache-transport.rs create mode 100644 macros/Cargo.toml create mode 100644 macros/src/data.rs create mode 100644 macros/src/impls.rs create mode 100644 macros/src/lib.rs create mode 100644 macros/src/repr.rs create mode 100644 src/crypto/common.rs create mode 100644 src/crypto/mod.rs create mode 100644 src/crypto/openssl.rs create mode 100644 src/crypto/ring.rs create mode 100644 src/crypto/sign.rs create mode 100644 src/dnssec/common.rs create mode 100644 src/dnssec/mod.rs create mode 100644 src/dnssec/sign/config.rs create mode 100644 src/dnssec/sign/denial/config.rs rename src/{ => dnssec}/sign/denial/mod.rs (100%) rename src/{ => dnssec}/sign/denial/nsec.rs (79%) rename src/{ => dnssec}/sign/denial/nsec3.rs (77%) create mode 100644 src/dnssec/sign/error.rs rename src/{ => dnssec}/sign/keys/keyset.rs (99%) rename src/{ => dnssec}/sign/keys/mod.rs (58%) rename src/{ => dnssec}/sign/keys/signingkey.rs (83%) rename src/{ => dnssec}/sign/mod.rs (71%) rename src/{ => dnssec}/sign/records.rs (92%) rename src/{ => dnssec}/sign/signatures/mod.rs (70%) create mode 100644 src/dnssec/sign/signatures/rrsigs.rs rename src/{ => dnssec}/sign/test_util/mod.rs (80%) rename src/{ => dnssec}/sign/traits.rs (62%) rename src/{ => dnssec}/validator/anchor.rs (97%) create mode 100644 src/dnssec/validator/base.rs rename src/{ => dnssec}/validator/context.rs (99%) rename src/{ => dnssec}/validator/group.rs (99%) rename src/{ => dnssec}/validator/mod.rs (90%) rename src/{ => dnssec}/validator/nsec.rs (99%) rename src/{ => dnssec}/validator/utilities.rs (100%) create mode 100644 src/logging.rs create mode 100644 src/new/base/build/message.rs create mode 100644 src/new/base/build/mod.rs create mode 100644 src/new/base/charstr.rs create mode 100644 src/new/base/message.rs create mode 100644 src/new/base/mod.rs create mode 100644 src/new/base/name/absolute.rs create mode 100644 src/new/base/name/compressor.rs create mode 100644 src/new/base/name/label.rs create mode 100644 src/new/base/name/mod.rs create mode 100644 src/new/base/name/reversed.rs create mode 100644 src/new/base/name/unparsed.rs create mode 100644 src/new/base/parse/message.rs create mode 100644 src/new/base/parse/mod.rs create mode 100644 src/new/base/question.rs create mode 100644 src/new/base/record.rs create mode 100644 src/new/base/serial.rs create mode 100644 src/new/base/wire/build.rs create mode 100644 src/new/base/wire/ints.rs create mode 100644 src/new/base/wire/mod.rs create mode 100644 src/new/base/wire/parse.rs create mode 100644 src/new/base/wire/size_prefixed.rs create mode 100644 src/new/edns/cookie.rs create mode 100644 src/new/edns/ext_err.rs create mode 100644 src/new/edns/mod.rs create mode 100644 src/new/mod.rs create mode 100644 src/new/rdata/basic/a.rs create mode 100644 src/new/rdata/basic/cname.rs create mode 100644 src/new/rdata/basic/hinfo.rs create mode 100644 src/new/rdata/basic/mod.rs create mode 100644 src/new/rdata/basic/mx.rs create mode 100644 src/new/rdata/basic/ns.rs create mode 100644 src/new/rdata/basic/ptr.rs create mode 100644 src/new/rdata/basic/soa.rs create mode 100644 src/new/rdata/basic/txt.rs create mode 100644 src/new/rdata/dnssec/dnskey.rs create mode 100644 src/new/rdata/dnssec/ds.rs create mode 100644 src/new/rdata/dnssec/mod.rs create mode 100644 src/new/rdata/dnssec/nsec.rs create mode 100644 src/new/rdata/dnssec/nsec3.rs create mode 100644 src/new/rdata/dnssec/rrsig.rs create mode 100644 src/new/rdata/edns.rs create mode 100644 src/new/rdata/ipv6.rs create mode 100644 src/new/rdata/mod.rs delete mode 100644 src/sign/config.rs delete mode 100644 src/sign/crypto/common.rs delete mode 100644 src/sign/crypto/mod.rs delete mode 100644 src/sign/crypto/openssl.rs delete mode 100644 src/sign/crypto/ring.rs delete mode 100644 src/sign/denial/config.rs delete mode 100644 src/sign/error.rs delete mode 100644 src/sign/keys/bytes.rs delete mode 100644 src/sign/keys/keymeta.rs delete mode 100644 src/sign/signatures/rrsigs.rs delete mode 100644 src/sign/signatures/strategy.rs create mode 100644 src/utils/dst.rs delete mode 100644 src/validate.rs create mode 100644 test-data/zonefiles/defaultclass.yaml create mode 100644 test-data/zonefiles/mixedclass.yaml create mode 100644 test-data/zonefiles/unknown-zero-length.yaml diff --git a/Cargo.lock b/Cargo.lock index 0b2a9140f..17b943904 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -118,15 +118,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.8.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" -version = "3.16.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "byteorder" @@ -136,15 +136,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.9.0" +version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.10" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "shlex", ] @@ -157,14 +157,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.39" +version = "0.4.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -184,9 +184,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "crossbeam-channel" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -208,9 +208,9 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -234,12 +234,14 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "domain" -version = "0.10.3" +version = "0.11.1-dev" dependencies = [ "arbitrary", "arc-swap", + "bumpalo", "bytes", "chrono", + "domain-macros", "futures-util", "hashbrown 0.14.5", "heapless", @@ -275,20 +277,29 @@ dependencies = [ "tokio-tfo", "tracing", "tracing-subscriber", - "webpki-roots", + "webpki-roots 0.26.11", +] + +[[package]] +name = "domain-macros" +version = "0.11.1-dev" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "either" -version = "1.13.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" [[package]] name = "equivalent" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "event-listener" @@ -303,9 +314,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", @@ -423,10 +434,11 @@ dependencies = [ [[package]] name = "generator" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bd114ceda131d3b1d665eba35788690ad37f5916457286b32ab6fd3c438dd" +checksum = "d18470a76cb7f8ff746cf1f7470914f900252ec36bbc40b569d74b1258446827" dependencies = [ + "cc", "cfg-if", "libc", "log", @@ -436,13 +448,25 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", ] [[package]] @@ -477,9 +501,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heapless" @@ -493,16 +517,17 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.61" +version = "0.1.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" dependencies = [ "android_system_properties", "core-foundation-sys", "iana-time-zone-haiku", "js-sys", + "log", "wasm-bindgen", - "windows-core 0.52.0", + "windows-core", ] [[package]] @@ -516,12 +541,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.2", + "hashbrown 0.15.3", ] [[package]] @@ -535,9 +560,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" @@ -557,15 +582,15 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.169" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -573,9 +598,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.25" +version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" [[package]] name = "loom" @@ -607,29 +632,29 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" -version = "0.8.3" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8402cab7aefae129c6977bb0ff1b8fd9a04eb5b51efc50a70bea51cda0c7924" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", ] [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi", - "windows-sys 0.52.0", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys 0.59.0", ] [[package]] name = "mock_instant" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15dce1a197fd6e12d773d7e4e7e1681fa5de6eb689ecaae824dc3cd1f643e7dc" +checksum = "4e1d4c44418358edcac6e1d9ce59cea7fb38052429c7704033f1196f0c179e6a" [[package]] name = "moka" @@ -701,15 +726,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.20.2" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "openssl" -version = "0.10.68" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ "bitflags", "cfg-if", @@ -731,14 +756,24 @@ dependencies = [ "syn", ] +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" -version = "0.9.104" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -757,9 +792,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -767,9 +802,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -780,18 +815,18 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.8" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", @@ -812,15 +847,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "portable-atomic" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" +checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" [[package]] name = "powerfmt" @@ -830,9 +865,9 @@ checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ "zerocopy", ] @@ -847,24 +882,39 @@ dependencies = [ "yansi", ] +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" -version = "1.0.93" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.38" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" + [[package]] name = "rand" version = "0.8.5" @@ -892,14 +942,14 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] name = "redox_syscall" -version = "0.5.8" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ "bitflags", ] @@ -956,24 +1006,23 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "ring" -version = "0.17.8" +version = "0.17.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom", + "getrandom 0.2.16", "libc", - "spin", "untrusted", "windows-sys 0.52.0", ] [[package]] name = "rstest" -version = "0.19.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5316d2a1479eeef1ea21e7f9ddc67c191d497abc8fc3ba2467857abbb68330" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" dependencies = [ "futures", "futures-timer", @@ -983,12 +1032,13 @@ dependencies = [ [[package]] name = "rstest_macros" -version = "0.19.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04a9df72cc1f67020b0d63ad9bfe4a323e459ea7eb68e03bd9824db49f9a4c25" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" dependencies = [ "cfg-if", "glob", + "proc-macro-crate", "proc-macro2", "quote", "regex", @@ -1015,9 +1065,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.21" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "log", "once_cell", @@ -1039,15 +1089,18 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.10.1" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] [[package]] name = "rustls-webpki" -version = "0.102.8" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -1056,15 +1109,15 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.19" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "scoped-tls" @@ -1089,24 +1142,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79dfe2d285b0488816f30e700a7438c5a73d816b5b7d3ac72fbc48b0d185e03" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" [[package]] name = "serde" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.217" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", @@ -1115,9 +1168,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" +checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" dependencies = [ "itoa", "memchr", @@ -1179,26 +1232,20 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.2" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.8" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1213,9 +1260,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.96" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -1260,9 +1307,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.37" +version = "0.3.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" dependencies = [ "deranged", "itoa", @@ -1275,15 +1322,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.19" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" dependencies = [ "num-conv", "time-core", @@ -1291,9 +1338,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.43.0" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -1318,9 +1365,9 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ "rustls", "tokio", @@ -1367,6 +1414,23 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "toml_datetime" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" + +[[package]] +name = "toml_edit" +version = "0.22.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + [[package]] name = "tracing" version = "0.1.41" @@ -1431,9 +1495,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.14" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unsafe-libyaml" @@ -1449,11 +1513,13 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.12.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" dependencies = [ - "getrandom", + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", ] [[package]] @@ -1474,6 +1540,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -1534,9 +1609,18 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.26.7" +version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d642ff16b7e79272ae451b7322067cdc17cadf68c23264be9d94a32319efe7e" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.0", +] + +[[package]] +name = "webpki-roots" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2853738d1cc4f2da3a225c18ec6c3721abb31961096e9dbf5ab35fa88b19cfdb" dependencies = [ "rustls-pki-types", ] @@ -1565,41 +1649,55 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.58.0" +version = "0.61.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections", + "windows-core", + "windows-future", + "windows-link", + "windows-numerics", ] [[package]] -name = "windows-core" -version = "0.52.0" +name = "windows-collections" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" dependencies = [ - "windows-targets 0.52.6", + "windows-core", ] [[package]] name = "windows-core" -version = "0.58.0" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.52.6", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", ] [[package]] name = "windows-implement" -version = "0.58.0" +version = "0.60.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", @@ -1608,9 +1706,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.58.0" +version = "0.59.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", @@ -1618,22 +1716,37 @@ dependencies = [ ] [[package]] -name = "windows-result" +name = "windows-link" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" + +[[package]] +name = "windows-numerics" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ - "windows-targets 0.52.6", + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", ] [[package]] name = "windows-strings" -version = "0.1.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-result", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -1654,6 +1767,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1685,6 +1807,15 @@ dependencies = [ "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -1775,6 +1906,24 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags", +] + [[package]] name = "yansi" version = "1.0.1" @@ -1783,19 +1932,18 @@ checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" [[package]] name = "zerocopy" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +checksum = "a1702d9583232ddb9174e01bb7c15a2ab8fb1bc6f227aa1233858c351a3ba0cb" dependencies = [ - "byteorder", "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.35" +version = "0.8.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +checksum = "28a6e20d751156648aa063f3800b706ee209a32c0b4d9f24be3d980b01be55ef" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index c8bf07c0c..ee5ef6d5b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,10 @@ +[workspace] +resolver = "2" +members = [".", "./macros"] + [package] name = "domain" -version = "0.10.3" +version = "0.11.1-dev" # The MSRV is at least 4 versions behind stable (about half a year). rust-version = "1.79.0" @@ -9,18 +13,17 @@ edition = "2021" authors = ["NLnet Labs "] description = "A DNS library for Rust." documentation = "https://docs.rs/domain" -homepage = "https://github.com/nlnetlabs/domain/" +homepage = "https://nlnetlabs.nl/projects/domain/" repository = "https://github.com/nlnetlabs/domain/" readme = "README.md" keywords = ["DNS", "domain"] license = "BSD-3-Clause" -[lib] -name = "domain" -path = "src/lib.rs" - [dependencies] +domain-macros = { path = "./macros", version = "=0.11.1-dev" } + arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } +bumpalo = { version = "3.12", optional = true } octseq = { version = "0.5.2", default-features = false } time = { version = "0.3.1", default-features = false } rand = { version = "0.8", optional = true } @@ -34,9 +37,9 @@ libc = { version = "0.2.153", default-features = false, optional = tru log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.57", optional = true } # 0.10.57 upgrades to 'bitflags' 2.x +openssl = { version = "0.10.72", optional = true } # 0.10.70 upgrades to 'bitflags' 2.x 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 } +ring = { version = "0.17.2", optional = true } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } @@ -52,16 +55,19 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] # Support for libraries +alloc = [] +bumpalo = ["dep:bumpalo", "std"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] serde = ["dep:serde", "octseq/serde"] smallvec = ["dep:smallvec", "octseq/smallvec"] -std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] +std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "time/std"] tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] +static-openssl = ["openssl/vendored"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] @@ -71,12 +77,15 @@ tsig = ["bytes", "ring", "smallvec"] zonefile = ["bytes", "serde", "std"] # Unstable features +unstable-new = [] +unstable-client-cache = ["unstable-client-transport", "moka"] unstable-client-transport = ["moka", "net", "tracing"] +unstable-crypto = ["bytes"] +unstable-crypto-sign = ["dep:secrecy", "unstable-crypto"] unstable-server-transport = ["arc-swap", "chrono/clock", "libc", "net", "siphasher", "tracing"] -unstable-sign = ["std", "dep:secrecy", "dep:smallvec", "dep:serde", "time/formatting", "tracing", "unstable-validate"] +unstable-sign = ["std", "dep:smallvec", "dep:serde", "time/formatting", "tracing", "unstable-crypto-sign"] 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"] +unstable-validator = ["zonefile", "unstable-client-transport", "unstable-crypto", "moka"] unstable-xfr = ["net"] unstable-zonetree = ["futures-util", "parking_lot", "rustversion", "serde", "std", "tokio", "tracing", "unstable-xfr", "zonefile"] @@ -129,6 +138,10 @@ required-features = ["std", "rand"] name = "client-transports" required-features = ["net", "tokio-rustls", "unstable-client-transport"] +[[example]] +name = "client-cache-transport" +required-features = ["net", "unstable-client-cache"] + [[example]] name = "server-transports" required-features = ["net", "tokio-stream", "tracing-subscriber", "unstable-client-transport", "unstable-server-transport"] @@ -155,7 +168,9 @@ required-features = ["net", "unstable-client-transport", "unstable-server-transp [[example]] name = "keyset" -required-features = ["serde", "unstable-sign"] +# Note that we have to pick a crypto backend eventhough the example doesn't +# need one. +required-features = ["serde", "unstable-sign", "ring"] # This example is commented out because it is difficult, if not impossible, # when including the sqlx dependency, to make the dependency tree compatible diff --git a/Changelog.md b/Changelog.md index fb5419189..57259bdae 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,10 +1,27 @@ # Change Log + ## Unreleased next version Breaking changes +New + +Bug fixes + +Other changes + +## 0.11.0 + +Released 2025-05-21. + +Breaking changes + * FIX: Use base 16 per RFC 4034 for the DS digest, not base 64. ([#423]) +* FIX: NSEC3 salt strings should only be accepted if within the salt size limit. (#431) +* Stricter RFC 1035 compliance by default in the `Zonefile` parser. ([#477]) +* Rename {DigestAlg, Nsec3HashAlg, SecAlg, ZonemdAlg} to + {DigestAlgorithm, Nsec3HashAlgorithm, SecurityAlgorithm, ZonemdAlgorithm} New @@ -19,7 +36,7 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], +* Added `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], [#463]) Bug fixes @@ -27,9 +44,23 @@ Bug fixes * NSEC records should include themselves in the generated bitmap. ([#417]) * Trailing double quote wrongly preserved when parsing record data. ([#470], [#472]) +* Don't error with unexpected end of entry for RFC 3597 RDATA of length zero. ([475]) Unstable features +* New unstable feature `unstable-crypto` that enable cryptography support + for features that do not rely on secret keys. This feature needs either + or both of the features `ring` and `openssl` ([#416]) +* New unstable feature `unstable-crypto-sign` that enable cryptography support + including features that rely on secret keys. This feature needs either + or both of the features `ring` and `openssl` ([#416]) +* New unstable feature `unstable-client-cache` that enable the client transport + cache. The reason is that the client cache uses the `moka` crate. + +* New unstable feature `unstable-new` that introduces a new API for all of + domain (currently only with `base`, `rdata`, and `edns` modules). Also see + the [associated blog post][new-base-post]. + * `unstable-server-transport` * The trait `SingleService` which is a simplified service trait for requests that should generate a single response ([#353]). @@ -48,21 +79,30 @@ Unstable features * restructure configuration for multi_stream and redundant ([#424]). * introduce a load balancer client transport. This transport tries to distribute requests equally over upstream transports ([#425]). + * the client cache now has it's own feature `unstable-client-cache`. * `unstable-sign` * add key lifecycle management ([#459]). + * add support for adding NSEC3 records when signing. + * add support for ZONEMD. + +* `unstable-validator` + * The `validate` crate is moved to `dnssec::validator::base`. + * The `validator` crate is moved to `dnssec::validator`. Other changes [#353]: https://github.com/NLnetLabs/domain/pull/353 [#379]: https://github.com/NLnetLabs/domain/pull/379 [#396]: https://github.com/NLnetLabs/domain/pull/396 +[#416]: https://github.com/NLnetLabs/domain/pull/416 [#417]: https://github.com/NLnetLabs/domain/pull/417 [#421]: https://github.com/NLnetLabs/domain/pull/421 [#423]: https://github.com/NLnetLabs/domain/pull/423 [#424]: https://github.com/NLnetLabs/domain/pull/424 [#425]: https://github.com/NLnetLabs/domain/pull/425 [#427]: https://github.com/NLnetLabs/domain/pull/427 +[#431]: https://github.com/NLnetLabs/domain/pull/431 [#440]: https://github.com/NLnetLabs/domain/pull/440 [#441]: https://github.com/NLnetLabs/domain/pull/441 [#446]: https://github.com/NLnetLabs/domain/pull/446 @@ -70,7 +110,26 @@ Other changes [#463]: https://github.com/NLnetLabs/domain/pull/463 [#470]: https://github.com/NLnetLabs/domain/pull/470 [#472]: https://github.com/NLnetLabs/domain/pull/472 +[#474]: https://github.com/NLnetLabs/domain/pull/474 +[#475]: https://github.com/NLnetLabs/domain/pull/475 +[#4775]: https://github.com/NLnetLabs/domain/pull/477 [@weilence]: https://github.com/weilence +[new-base-post]: https://blog.nlnetlabs.nl/overhauling-domain/ + + +## 0.10.4 + +Released 2025-03-31. + +Other changes + +* Fix a build issue with [*time*](https://time-rs.github.io/) 0.3.41. + ([#505], backported from [#503] by [@PSeitz]) + +[#503]: https://github.com/NLnetLabs/domain/pull/503 +[#505]: https://github.com/NLnetLabs/domain/pull/505 +[@PSeitz]: https://github.com/PSeitz + ## 0.10.3 diff --git a/examples/client-cache-transport.rs b/examples/client-cache-transport.rs new file mode 100644 index 000000000..50d57b29a --- /dev/null +++ b/examples/client-cache-transport.rs @@ -0,0 +1,85 @@ +//! Example of using the `domain::net::client::cache` module. +use domain::base::{MessageBuilder, Name, Rtype}; +use domain::net::client::cache; +use domain::net::client::protocol::{TcpConnect, UdpConnect}; +use domain::net::client::request::{RequestMessage, SendRequest}; +use domain::net::client::{dgram, dgram_stream, multi_stream, stream}; +use std::net::{IpAddr, SocketAddr}; +use std::str::FromStr; +use std::time::Duration; + +#[tokio::main] +async fn main() { + // Create DNS request message. + // + // Transports currently take a `RequestMessage` as their input to be able + // to add options along the way. + // + // In the future, it will also be possible to pass in a message or message + // builder directly as input but for now it needs to be converted into a + // `RequestMessage` manually. + let mut msg = MessageBuilder::new_vec(); + msg.header_mut().set_rd(true); + msg.header_mut().set_ad(true); + let mut msg = msg.question(); + msg.push((Name::vec_from_str("example.com").unwrap(), Rtype::AAAA)) + .unwrap(); + let req = RequestMessage::new(msg).unwrap(); + + // Destination for UDP and TCP + let server_addr = SocketAddr::new(IpAddr::from_str("::1").unwrap(), 53); + + let mut stream_config = stream::Config::new(); + stream_config.set_response_timeout(Duration::from_millis(100)); + let multi_stream_config = + multi_stream::Config::from(stream_config.clone()); + + // Create a new UDP+TCP transport connection. Pass the destination address + // and port as parameter. + let mut dgram_config = dgram::Config::new(); + dgram_config.set_max_parallel(1); + dgram_config.set_read_timeout(Duration::from_millis(1000)); + dgram_config.set_max_retries(1); + dgram_config.set_udp_payload_size(Some(1400)); + let dgram_stream_config = dgram_stream::Config::from_parts( + dgram_config.clone(), + multi_stream_config.clone(), + ); + let udp_connect = UdpConnect::new(server_addr); + let tcp_connect = TcpConnect::new(server_addr); + let (udptcp_conn, transport) = dgram_stream::Connection::with_config( + udp_connect, + tcp_connect, + dgram_stream_config.clone(), + ); + + // Start the run function in a separate task. The run function will + // terminate when all references to the connection have been dropped. + // Make sure that the task does not accidentally get a reference to the + // connection. + tokio::spawn(async move { + transport.run().await; + println!("UDP+TCP run exited"); + }); + + // Create a cached transport. + let mut cache_config = cache::Config::new(); + cache_config.set_max_cache_entries(100); // Just an example. + let cache = cache::Connection::with_config(udptcp_conn, cache_config); + + // Send a request message. + let mut request = cache.send_request(req.clone()); + + // Get the reply + println!("Waiting for cache reply"); + let reply = request.get_response().await; + println!("Cache reply: {reply:?}"); + + // Send the request message again. + let mut request = cache.send_request(req.clone()); + + // Get the reply + println!("Waiting for cached reply"); + let reply = request.get_response().await; + println!("Cached reply: {reply:?}"); +} diff --git a/examples/client-transports.rs b/examples/client-transports.rs index 5b6832a0d..b4bf020d8 100644 --- a/examples/client-transports.rs +++ b/examples/client-transports.rs @@ -5,8 +5,7 @@ use domain::net::client::request::{ RequestMessage, RequestMessageMulti, SendRequest, }; use domain::net::client::{ - cache, dgram, dgram_stream, load_balancer, multi_stream, redundant, - stream, + dgram, dgram_stream, load_balancer, multi_stream, redundant, stream, }; use std::net::{IpAddr, SocketAddr}; use std::str::FromStr; @@ -92,28 +91,6 @@ async fn main() { // when it is no longer needed. drop(request); - // Create a cached transport. - let mut cache_config = cache::Config::new(); - cache_config.set_max_cache_entries(100); // Just an example. - let cache = - cache::Connection::with_config(udptcp_conn.clone(), cache_config); - - // Send a request message. - let mut request = cache.send_request(req.clone()); - - // Get the reply - println!("Wating for cache reply"); - let reply = request.get_response().await; - println!("Cache reply: {reply:?}"); - - // Send the request message again. - let mut request = cache.send_request(req.clone()); - - // Get the reply - println!("Wating for cached reply"); - let reply = request.get_response().await; - println!("Cached reply: {reply:?}"); - #[cfg(feature = "unstable-validator")] do_validator(udptcp_conn.clone(), req.clone()).await; @@ -331,13 +308,15 @@ where { // Create a validating transport let anchor_file = std::fs::File::open("examples/root.key").unwrap(); - let ta = - domain::validator::anchor::TrustAnchors::from_reader(anchor_file) - .unwrap(); - let vc = Arc::new(domain::validator::context::ValidationContext::new( - ta, - conn.clone(), - )); + let ta = domain::dnssec::validator::anchor::TrustAnchors::from_reader( + anchor_file, + ) + .unwrap(); + let vc = + Arc::new(domain::dnssec::validator::context::ValidationContext::new( + ta, + conn.clone(), + )); let val_conn = domain::net::client::validator::Connection::new(conn, vc); // Send a query message. diff --git a/examples/keyset.rs b/examples/keyset.rs index 808cd461a..a72f3a599 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -1,6 +1,6 @@ //! Demonstrate the use of key sets. use domain::base::Name; -use domain::sign::keys::keyset::{ +use domain::dnssec::sign::keys::keyset::{ Action, Error, KeySet, KeyType, RollType, UnixTime, }; use itertools::{Either, Itertools}; diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 000000000..a79d619a8 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "domain-macros" + +# Copied from 'domain'. +version = "0.11.1-dev" +rust-version = "1.68.2" +edition = "2021" + +authors = ["NLnet Labs "] +description = "Procedural macros for the `domain` crate." +documentation = "https://docs.rs/domain-macros" +homepage = "https://github.com/nlnetlabs/domain/" +repository = "https://github.com/nlnetlabs/domain/" +keywords = ["DNS", "domain"] +license = "BSD-3-Clause" + +[lib] +proc-macro = true + +[dependencies.proc-macro2] +version = "1.0" + +[dependencies.syn] +version = "2.0" +features = ["full", "visit"] + +[dependencies.quote] +version = "1.0" diff --git a/macros/src/data.rs b/macros/src/data.rs new file mode 100644 index 000000000..ee0c52baf --- /dev/null +++ b/macros/src/data.rs @@ -0,0 +1,159 @@ +//! Working with structs, enums, and unions. + +use std::ops::Deref; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{spanned::Spanned, Field, Fields, Ident, Index, Member, Token}; + +//----------- Struct --------------------------------------------------------- + +/// A defined 'struct'. +pub struct Struct { + /// The identifier for this 'struct'. + ident: Ident, + + /// The fields in this 'struct'. + fields: Fields, +} + +impl Struct { + /// Construct a [`Struct`] for a 'Self'. + pub fn new_as_self(fields: &Fields) -> Self { + Self { + ident: ::default().into(), + fields: fields.clone(), + } + } + + /// Whether this 'struct' has no fields. + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + /// The number of fields in this 'struct'. + pub fn num_fields(&self) -> usize { + self.fields.len() + } + + /// The fields of this 'struct'. + pub fn fields(&self) -> impl Iterator + '_ { + self.fields.iter() + } + + /// The sized fields of this 'struct'. + pub fn sized_fields(&self) -> impl Iterator + '_ { + self.fields().take(self.num_fields() - 1) + } + + /// The unsized field of this 'struct'. + pub fn unsized_field(&self) -> Option<&Field> { + self.fields.iter().next_back() + } + + /// The names of the fields of this 'struct'. + pub fn members(&self) -> impl Iterator + '_ { + self.fields + .iter() + .enumerate() + .map(|(i, f)| make_member(i, f)) + } + + /// The names of the sized fields of this 'struct'. + pub fn sized_members(&self) -> impl Iterator + '_ { + self.members().take(self.num_fields() - 1) + } + + /// The name of the last field of this 'struct'. + pub fn unsized_member(&self) -> Option { + self.fields + .iter() + .next_back() + .map(|f| make_member(self.num_fields() - 1, f)) + } + + /// Construct a builder for this 'struct'. + pub fn builder Ident>( + &self, + f: F, + ) -> StructBuilder<'_, F> { + StructBuilder { + target: self, + var_fn: f, + } + } +} + +/// Construct a [`Member`] from a field and index. +fn make_member(index: usize, field: &Field) -> Member { + match &field.ident { + Some(ident) => Member::Named(ident.clone()), + None => Member::Unnamed(Index { + index: index as u32, + span: field.ty.span(), + }), + } +} + +//----------- StructBuilder -------------------------------------------------- + +/// A means of constructing a 'struct'. +pub struct StructBuilder<'a, F: Fn(Member) -> Ident> { + /// The 'struct' being constructed. + target: &'a Struct, + + /// A map from field names to constructing variables. + var_fn: F, +} + +impl Ident> StructBuilder<'_, F> { + /// The initializing variables for this 'struct'. + pub fn init_vars(&self) -> impl Iterator + '_ { + self.members().map(&self.var_fn) + } + + /// The names of the sized fields of this 'struct'. + pub fn sized_init_vars(&self) -> impl Iterator + '_ { + self.sized_members().map(&self.var_fn) + } + + /// The name of the last field of this 'struct'. + pub fn unsized_init_var(&self) -> Option { + self.unsized_member().map(&self.var_fn) + } +} + +impl Ident> Deref for StructBuilder<'_, F> { + type Target = Struct; + + fn deref(&self) -> &Self::Target { + self.target + } +} + +impl Ident> ToTokens for StructBuilder<'_, F> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.ident; + match self.fields { + Fields::Named(_) => { + let members = self.members(); + let init_vars = self.init_vars(); + quote! { + #ident { #(#members: #init_vars),* } + } + } + + Fields::Unnamed(_) => { + let init_vars = self.init_vars(); + quote! { + #ident ( #(#init_vars),* ) + } + } + + Fields::Unit => { + quote! { #ident } + } + } + .to_tokens(tokens); + } +} diff --git a/macros/src/impls.rs b/macros/src/impls.rs new file mode 100644 index 000000000..4c0971998 --- /dev/null +++ b/macros/src/impls.rs @@ -0,0 +1,274 @@ +//! Helpers for generating `impl` blocks. + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + punctuated::Punctuated, visit::Visit, ConstParam, GenericArgument, + GenericParam, Ident, Lifetime, LifetimeParam, Token, TypeParam, + TypeParamBound, WhereClause, WherePredicate, +}; + +//----------- ImplSkeleton --------------------------------------------------- + +/// The skeleton of an `impl` block. +pub struct ImplSkeleton { + /// Lifetime parameters for the `impl` block. + pub lifetimes: Vec, + + /// Type parameters for the `impl` block. + pub types: Vec, + + /// Const generic parameters for the `impl` block. + pub consts: Vec, + + /// Whether the `impl` is unsafe. + pub unsafety: Option, + + /// The trait being implemented. + pub bound: Option, + + /// The type being implemented on. + pub subject: syn::Path, + + /// The where clause of the `impl` block. + pub where_clause: WhereClause, + + /// The contents of the `impl`. + pub contents: syn::Block, + + /// A `const` block for asserting requirements. + pub requirements: syn::Block, +} + +impl ImplSkeleton { + /// Construct an [`ImplSkeleton`] for a [`DeriveInput`]. + pub fn new(input: &syn::DeriveInput, unsafety: bool) -> Self { + let mut lifetimes = Vec::new(); + let mut types = Vec::new(); + let mut consts = Vec::new(); + let mut subject_args = Punctuated::new(); + + for param in &input.generics.params { + match param { + GenericParam::Lifetime(value) => { + lifetimes.push(value.clone()); + let id = value.lifetime.clone(); + subject_args.push(GenericArgument::Lifetime(id)); + } + + GenericParam::Type(value) => { + types.push(value.clone()); + let id = value.ident.clone(); + let id = syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: id, + arguments: syn::PathArguments::None, + }] + .into_iter() + .collect(), + }, + }; + subject_args.push(GenericArgument::Type(id.into())); + } + + GenericParam::Const(value) => { + consts.push(value.clone()); + let id = value.ident.clone(); + let id = syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: id, + arguments: syn::PathArguments::None, + }] + .into_iter() + .collect(), + }, + }; + subject_args.push(GenericArgument::Type(id.into())); + } + } + } + + let unsafety = unsafety.then_some(::default()); + + let subject = syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: input.ident.clone(), + arguments: syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: subject_args, + gt_token: Default::default(), + }, + ), + }] + .into_iter() + .collect(), + }; + + let where_clause = + input.generics.where_clause.clone().unwrap_or(WhereClause { + where_token: Default::default(), + predicates: Punctuated::new(), + }); + + let contents = syn::Block { + brace_token: Default::default(), + stmts: Vec::new(), + }; + + let requirements = syn::Block { + brace_token: Default::default(), + stmts: Vec::new(), + }; + + Self { + lifetimes, + types, + consts, + unsafety, + bound: None, + subject, + where_clause, + contents, + requirements, + } + } + + /// Require a bound for a type. + /// + /// If the type is concrete, a verifying statement is added for it. + /// Otherwise, it is added to the where clause. + pub fn require_bound( + &mut self, + target: syn::Type, + bound: TypeParamBound, + ) { + let mut visitor = ConcretenessVisitor { + skeleton: self, + is_concrete: true, + }; + + // Concreteness applies to both the type and the bound. + visitor.visit_type(&target); + visitor.visit_type_param_bound(&bound); + + if visitor.is_concrete { + // Add a concrete requirement for this bound. + self.requirements.stmts.push(syn::parse_quote! { + const _: fn() = || { + fn assert_impl() {} + assert_impl::<#target>(); + }; + }); + } else { + // Add this bound to the `where` clause. + let mut bounds = Punctuated::new(); + bounds.push(bound); + let pred = WherePredicate::Type(syn::PredicateType { + lifetimes: None, + bounded_ty: target, + colon_token: Default::default(), + bounds, + }); + self.where_clause.predicates.push(pred); + } + } + + /// Generate a unique lifetime with the given prefix. + pub fn new_lifetime(&self, prefix: &str) -> Lifetime { + [format_ident!("{}", prefix)] + .into_iter() + .chain((0u32..).map(|i| format_ident!("{}_{}", prefix, i))) + .find(|id| self.lifetimes.iter().all(|l| l.lifetime.ident != *id)) + .map(|ident| Lifetime { + apostrophe: Span::call_site(), + ident, + }) + .unwrap() + } + + /// Generate a unique lifetime parameter with the given prefix and bounds. + pub fn new_lifetime_param( + &self, + prefix: &str, + bounds: impl IntoIterator, + ) -> (Lifetime, LifetimeParam) { + let lifetime = self.new_lifetime(prefix); + let mut bounds = bounds.into_iter().peekable(); + let param = if bounds.peek().is_some() { + syn::parse_quote! { #lifetime: #(#bounds)+* } + } else { + syn::parse_quote! { #lifetime } + }; + (lifetime, param) + } +} + +impl ToTokens for ImplSkeleton { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { + lifetimes, + types, + consts, + unsafety, + bound, + subject, + where_clause, + contents, + requirements, + } = self; + + let target = match bound { + Some(bound) => quote!(#bound for #subject), + None => quote!(#subject), + }; + + quote! { + #unsafety + impl<#(#lifetimes,)* #(#types,)* #(#consts,)*> + #target + #where_clause + #contents + } + .to_tokens(tokens); + + if !requirements.stmts.is_empty() { + quote! { + const _: () = #requirements; + } + .to_tokens(tokens); + } + } +} + +//----------- ConcretenessVisitor -------------------------------------------- + +struct ConcretenessVisitor<'a> { + /// The `impl` skeleton being added to. + skeleton: &'a ImplSkeleton, + + /// Whether the visited type is concrete. + is_concrete: bool, +} + +impl<'ast> Visit<'ast> for ConcretenessVisitor<'_> { + fn visit_lifetime(&mut self, i: &'ast Lifetime) { + self.is_concrete = self.is_concrete + && self.skeleton.lifetimes.iter().all(|l| l.lifetime != *i); + } + + fn visit_ident(&mut self, i: &'ast Ident) { + self.is_concrete = self.is_concrete + && self.skeleton.types.iter().all(|t| t.ident != *i); + self.is_concrete = self.is_concrete + && self.skeleton.consts.iter().all(|c| c.ident != *i); + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 000000000..e74068c97 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,632 @@ +//! Procedural macros for [`domain`]. +//! +//! [`domain`]: https://docs.rs/domain + +use proc_macro as pm; +use proc_macro2::TokenStream; +use quote::{format_ident, ToTokens}; +use syn::{Error, Ident, Result}; + +mod impls; +use impls::ImplSkeleton; + +mod data; +use data::Struct; + +mod repr; +use repr::Repr; + +//----------- SplitBytes ----------------------------------------------------- + +#[proc_macro_derive(SplitBytes)] +pub fn derive_split_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'SplitBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'SplitBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + + // Add the parsing lifetime to the 'impl'. + let (lifetime, param) = skeleton.new_lifetime_param( + "bytes", + skeleton.lifetimes.iter().map(|l| l.lifetime.clone()), + ); + skeleton.lifetimes.push(param); + skeleton.bound = Some( + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + let builder = data.builder(field_prefixed); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + } + + // Define 'parse_bytes()'. + let init_vars = builder.init_vars(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (Self, & #lifetime [::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + #(let (#init_vars, bytes) = + <#tys as ::domain::new::base::wire::SplitBytes<#lifetime>> + ::split_bytes(bytes)?;)* + Ok((#builder, bytes)) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- ParseBytes ----------------------------------------------------- + +#[proc_macro_derive(ParseBytes)] +pub fn derive_parse_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'ParseBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'ParseBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + + // Add the parsing lifetime to the 'impl'. + let (lifetime, param) = skeleton.new_lifetime_param( + "bytes", + skeleton.lifetimes.iter().map(|l| l.lifetime.clone()), + ); + skeleton.lifetimes.push(param); + skeleton.bound = Some( + syn::parse_quote!(::domain::new::base::wire::ParseBytes<#lifetime>), + ); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + let builder = data.builder(field_prefixed); + + // Establish bounds on the fields. + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + } + if let Some(field) = data.unsized_field() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::ParseBytes<#lifetime>), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + Self, + ::domain::new::base::wire::ParseError, + > { + if bytes.is_empty() { + Ok(#builder) + } else { + Err(::domain::new::base::wire::ParseError) + } + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'parse_bytes()'. + let init_vars = builder.sized_init_vars(); + let tys = builder.sized_fields().map(|f| &f.ty); + let unsized_ty = &builder.unsized_field().unwrap().ty; + let unsized_init_var = builder.unsized_init_var().unwrap(); + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + Self, + ::domain::new::base::wire::ParseError, + > { + #(let (#init_vars, bytes) = + <#tys as ::domain::new::base::wire::SplitBytes<#lifetime>> + ::split_bytes(bytes)?;)* + let #unsized_init_var = + <#unsized_ty as ::domain::new::base::wire::ParseBytes<#lifetime>> + ::parse_bytes(bytes)?; + Ok(#builder) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- SplitBytesZC --------------------------------------------------- + +#[proc_macro_derive(SplitBytesZC)] +pub fn derive_split_bytes_zc(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'SplitBytesZC' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'SplitBytesZC' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "SplitBytesZC")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::SplitBytesZC)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytesZC), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (&Self, &[::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + Ok(( + // SAFETY: 'Self' is a 'struct' with no fields, + // and so has size 0 and alignment 1. It can be + // constructed at any address. + unsafe { &*bytes.as_ptr().cast::() }, + bytes, + )) + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'split_bytes_by_ref()'. + let tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (&Self, &[::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + let start = bytes.as_ptr(); + #(let (_, bytes) = + <#tys as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?;)* + let (last, rest) = + <#unsized_ty as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?; + let ptr = + <#unsized_ty as ::domain::utils::dst::UnsizedCopy> + ::ptr_with_addr(last, start as *const ()); + + // SAFETY: + // - The original 'bytes' contained a valid instance of every + // field in 'Self', in succession. + // - Every field implements 'ParseBytesZC' and so has no + // alignment restriction. + // - 'Self' is unaligned, since every field is unaligned, and + // any explicit alignment modifiers only make it unaligned. + // - 'start' is thus the start of a valid instance of 'Self'. + // - 'ptr' has the same address as 'start' but can be cast to + // 'Self', since it has the right pointer metadata. + Ok((unsafe { &*(ptr as *const Self) }, rest)) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- ParseBytesZC --------------------------------------------------- + +#[proc_macro_derive(ParseBytesZC)] +pub fn derive_parse_bytes_zc(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'ParseBytesZC' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'ParseBytesZC' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "ParseBytesZC")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::ParseBytesZC)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Establish bounds on the fields. + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytesZC), + ); + } + if let Some(field) = data.unsized_field() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::ParseBytesZC), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + &Self, + ::domain::new::base::wire::ParseError, + > { + if bytes.is_empty() { + // SAFETY: 'Self' is a 'struct' with no fields, + // and so has size 0 and alignment 1. It can be + // constructed at any address. + Ok(unsafe { &*bytes.as_ptr().cast::() }) + } else { + Err(::domain::new::base::wire::ParseError) + } + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'parse_bytes_by_ref()'. + let tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + &Self, + ::domain::new::base::wire::ParseError, + > { + let start = bytes.as_ptr(); + #(let (_, bytes) = + <#tys as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?;)* + let last = + <#unsized_ty as ::domain::new::base::wire::ParseBytesZC> + ::parse_bytes_by_ref(bytes)?; + let ptr = + <#unsized_ty as ::domain::utils::dst::UnsizedCopy> + ::ptr_with_addr(last, start as *const ()); + + // SAFETY: + // - The original 'bytes' contained a valid instance of every + // field in 'Self', in succession. + // - Every field implements 'ParseBytesZC' and so has no + // alignment restriction. + // - 'Self' is unaligned, since every field is unaligned, and + // any explicit alignment modifiers only make it unaligned. + // - 'start' is thus the start of a valid instance of 'Self'. + // - 'ptr' has the same address as 'start' but can be cast to + // 'Self', since it has the right pointer metadata. + Ok(unsafe { &*(ptr as *const Self) }) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- BuildBytes ----------------------------------------------------- + +#[proc_macro_derive(BuildBytes)] +pub fn derive_build_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'BuildBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'BuildBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::BuildBytes)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Get a lifetime for the input buffer. + let lifetime = skeleton.new_lifetime("bytes"); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::BuildBytes), + ); + } + + // Define 'build_bytes()'. + let members = data.members(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn build_bytes<#lifetime>( + &self, + mut bytes: & #lifetime mut [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + & #lifetime mut [::domain::__core::primitive::u8], + ::domain::new::base::wire::TruncationError, + > { + #(bytes = <#tys as ::domain::new::base::wire::BuildBytes> + ::build_bytes(&self.#members, bytes)?;)* + Ok(bytes) + } + }); + + // Define 'built_bytes_size()'. + let members = data.members(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn built_bytes_size(&self) -> ::domain::__core::primitive::usize { + 0 #(+ <#tys as ::domain::new::base::wire::BuildBytes> + ::built_bytes_size(&self.#members))* + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- AsBytes -------------------------------------------------------- + +#[proc_macro_derive(AsBytes)] +pub fn derive_as_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'AsBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'AsBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "AsBytes")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::AsBytes)); + + // Establish bounds on the fields. + for field in data.fields.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::AsBytes), + ); + } + + // The default implementation of 'as_bytes()' works perfectly. + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- UnsizedCopy ---------------------------------------------------- + +#[proc_macro_derive(UnsizedCopy)] +pub fn derive_unsized_copy(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::utils::dst::UnsizedCopy)); + + let struct_data = match &input.data { + syn::Data::Struct(data) if !data.fields.is_empty() => { + let data = Struct::new_as_self(&data.fields); + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + + skeleton.require_bound( + data.unsized_field().unwrap().ty.clone(), + syn::parse_quote!(::domain::utils::dst::UnsizedCopy), + ); + + Some(data) + } + + syn::Data::Struct(_) => None, + + syn::Data::Enum(data) => { + for variant in data.variants.iter() { + for field in variant.fields.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + } + + None + } + + syn::Data::Union(data) => { + for field in data.fields.named.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + + None + } + }; + + if let Some(data) = struct_data { + let sized_tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + let unsized_member = data.unsized_member().unwrap(); + + skeleton.contents.stmts.push(syn::parse_quote! { + type Alignment = (#(#sized_tys,)* <#unsized_ty as ::domain::utils::dst::UnsizedCopy>::Alignment,); + }); + + skeleton.contents.stmts.push(syn::parse_quote! { + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + ::domain::utils::dst::UnsizedCopy::ptr_with_addr( + &self.#unsized_member, + addr, + ) as *const Self + } + }); + } else { + skeleton.contents.stmts.push(syn::parse_quote! { + type Alignment = Self; + }); + + skeleton.contents.stmts.push(syn::parse_quote! { + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr as *const Self + } + }); + } + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- Utility Functions ---------------------------------------------- + +/// Add a `field_` prefix to member names. +fn field_prefixed(member: syn::Member) -> Ident { + format_ident!("field_{}", member) +} diff --git a/macros/src/repr.rs b/macros/src/repr.rs new file mode 100644 index 000000000..b699b571b --- /dev/null +++ b/macros/src/repr.rs @@ -0,0 +1,91 @@ +//! Determining the memory layout of a type. + +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, spanned::Spanned, Attribute, Error, LitInt, Meta, + Token, +}; + +//----------- Repr ----------------------------------------------------------- + +/// The memory representation of a type. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Repr { + /// Transparent to an underlying field. + Transparent, + + /// Compatible with C. + C, +} + +impl Repr { + /// Determine the representation for a type from its attributes. + /// + /// This will fail if a stable representation cannot be found. + pub fn determine( + attrs: &[Attribute], + bound: &str, + ) -> Result { + let mut repr = None; + for attr in attrs { + if !attr.path().is_ident("repr") { + continue; + } + + let nested = attr.parse_args_with( + Punctuated::::parse_terminated, + )?; + + // We don't check for consistency in the 'repr' attributes, since + // the compiler should be doing that for us anyway. This lets us + // ignore conflicting 'repr's entirely. + for meta in nested { + match meta { + Meta::Path(p) if p.is_ident("transparent") => { + repr = Some(Repr::Transparent); + } + + Meta::Path(p) if p.is_ident("C") => { + repr = Some(Repr::C); + } + + Meta::Path(p) if p.is_ident("Rust") => { + return Err(Error::new_spanned(p, + format!("repr(Rust) is not stable, cannot derive {bound} for it"))); + } + + Meta::Path(p) if p.is_ident("packed") => { + // The alignment can be set to 1 safely. + } + + Meta::List(meta) + if meta.path.is_ident("packed") + || meta.path.is_ident("aligned") => + { + let span = meta.span(); + let lit: LitInt = syn::parse2(meta.tokens)?; + let n: usize = lit.base10_parse()?; + if n != 1 { + return Err(Error::new(span, + format!("'Self' must be unaligned to derive {bound}"))); + } + } + + meta => { + // We still need to error out here, in case a future + // version of Rust introduces more memory layout data + return Err(Error::new_spanned( + meta, + "unrecognized repr attribute", + )); + } + } + } + } + + repr.ok_or_else(|| { + Error::new(Span::call_site(), + "repr(C) or repr(transparent) must be specified to derive this") + }) + } +} diff --git a/src/base/iana/digestalg.rs b/src/base/iana/digestalg.rs index 66c3c4bee..7498bde40 100644 --- a/src/base/iana/digestalg.rs +++ b/src/base/iana/digestalg.rs @@ -1,6 +1,6 @@ //! Delegation signer digest algorithm numbers. -//------------ DigestAlg ----------------------------------------------------- +//------------ DigestAlgorithm ----------------------------------------------- int_enum! { /// Delegation signer digest algorithm numbers. @@ -13,7 +13,7 @@ int_enum! { /// /// [IANA registration]: https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml#ds-rr-types-1 => - DigestAlg, u8; + DigestAlgorithm, u8; /// Specifies that the SHA-1 hash function is used. /// @@ -42,8 +42,8 @@ int_enum! { (SHA384 => 4, "SHA-384") } -int_enum_str_decimal!(DigestAlg, u8); -int_enum_zonefile_fmt_decimal!(DigestAlg, "digest type"); +int_enum_str_decimal!(DigestAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(DigestAlgorithm, "digest type"); //============ Tests ========================================================= @@ -52,10 +52,10 @@ mod test { #[cfg(feature = "serde")] #[test] fn ser_de() { - use super::DigestAlg; + use super::DigestAlgorithm; use serde_test::{assert_tokens, Token}; - assert_tokens(&DigestAlg::SHA384, &[Token::U8(4)]); - assert_tokens(&DigestAlg(100), &[Token::U8(100)]); + assert_tokens(&DigestAlgorithm::SHA384, &[Token::U8(4)]); + assert_tokens(&DigestAlgorithm(100), &[Token::U8(100)]); } } diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 9d86b2e94..1eee9dc8f 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -26,16 +26,16 @@ //! `FromStrError` without having to resort to devilishly long names. pub use self::class::Class; -pub use self::digestalg::DigestAlg; +pub use self::digestalg::DigestAlgorithm; pub use self::exterr::ExtendedErrorCode; -pub use self::nsec3::Nsec3HashAlg; +pub use self::nsec3::Nsec3HashAlgorithm; pub use self::opcode::Opcode; pub use self::opt::OptionCode; pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; -pub use self::secalg::SecAlg; +pub use self::secalg::SecurityAlgorithm; pub use self::svcb::SvcParamKey; -pub use self::zonemd::{ZonemdAlg, ZonemdScheme}; +pub use self::zonemd::{ZonemdAlgorithm, ZonemdScheme}; #[macro_use] mod macros; diff --git a/src/base/iana/nsec3.rs b/src/base/iana/nsec3.rs index 3714d6b42..650fbdb4e 100644 --- a/src/base/iana/nsec3.rs +++ b/src/base/iana/nsec3.rs @@ -1,6 +1,6 @@ //! NSEC3 hash algorithms. -//------------ Nsec3HashAlg -------------------------------------------------- +//------------ Nsec3HashAlgorithm -------------------------------------------- int_enum! { /// NSEC3 hash algorithm numbers. @@ -14,11 +14,11 @@ int_enum! { /// [NSEC3]: ../../../rdata/rfc5155/index.html /// [IANA registration]: https://www.iana.org/assignments/dnssec-nsec3-parameters/dnssec-nsec3-parameters.xhtml#dnssec-nsec3-parameters-3 => - Nsec3HashAlg, u8; + Nsec3HashAlgorithm, u8; /// Specifies that the SHA-1 hash function is used. (SHA1 => 1, "SHA-1") } -int_enum_str_decimal!(Nsec3HashAlg, u8); -int_enum_zonefile_fmt_decimal!(Nsec3HashAlg, "hash algorithm"); +int_enum_str_decimal!(Nsec3HashAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(Nsec3HashAlgorithm, "hash algorithm"); diff --git a/src/base/iana/secalg.rs b/src/base/iana/secalg.rs index 9e3e17f56..82d222395 100644 --- a/src/base/iana/secalg.rs +++ b/src/base/iana/secalg.rs @@ -1,6 +1,6 @@ //! DNSSEC Algorithm Numbers -//------------ SecAlg ------------------------------------------------------- +//------------ SecurityAlgorithm --------------------------------------------- int_enum! { /// Security Algorithm Numbers. @@ -11,7 +11,7 @@ int_enum! { /// /// [IANA registration]: http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml#dns-sec-alg-numbers-1]. => - SecAlg, u8; + SecurityAlgorithm, u8; /// Delete DS /// @@ -116,5 +116,5 @@ int_enum! { (PRIVATEOID => 254, "PRIVATEOID") } -int_enum_str_decimal!(SecAlg, u8); -int_enum_zonefile_fmt_decimal!(SecAlg, "algorithm"); +int_enum_str_decimal!(SecurityAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(SecurityAlgorithm, "algorithm"); diff --git a/src/base/iana/zonemd.rs b/src/base/iana/zonemd.rs index cd92a1012..49ce637e3 100644 --- a/src/base/iana/zonemd.rs +++ b/src/base/iana/zonemd.rs @@ -23,7 +23,7 @@ int_enum! { int_enum_str_decimal!(ZonemdScheme, u8); int_enum_zonefile_fmt_decimal!(ZonemdScheme, "scheme"); -//------------ ZonemdAlg ----------------------------------------------------- +//------------ ZonemdAlgorithm ----------------------------------------------- int_enum! { /// ZONEMD algorithms. @@ -37,7 +37,7 @@ int_enum! { /// [ZONEMD]: ../../../rdata/zonemd/index.html /// [IANA registration]: https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#zonemd-hash-algorithms => - ZonemdAlg, u8; + ZonemdAlgorithm, u8; /// Specifies that the SHA-384 algorithm is used. (SHA384 => 1, "SHA384") @@ -46,5 +46,5 @@ int_enum! { (SHA512 => 2, "SHA512") } -int_enum_str_decimal!(ZonemdAlg, u8); -int_enum_zonefile_fmt_decimal!(ZonemdAlg, "hash algorithm"); +int_enum_str_decimal!(ZonemdAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(ZonemdAlgorithm, "hash algorithm"); diff --git a/src/base/opt/algsig.rs b/src/base/opt/algsig.rs index d00159342..db613f651 100644 --- a/src/base/opt/algsig.rs +++ b/src/base/opt/algsig.rs @@ -14,7 +14,7 @@ //! //! [RFC 6975]: https://tools.ietf.org/html/rfc6975 -use super::super::iana::{OptionCode, SecAlg}; +use super::super::iana::{OptionCode, SecurityAlgorithm}; use super::super::message_builder::OptBuilder; use super::super::wire::{Compose, Composer, ParseError}; use super::{ @@ -34,14 +34,14 @@ use core::marker::PhantomData; /// This type provides the option data for the three options DAU, DHU, and /// N3U which allow a client to specify the cryptographic algorithms it /// supports for DNSSEC signatures, DS hashes, and NSEC3 hashes respectively. -/// Each of them contains a sequence of [`SecAlg`] values in wire format. +/// Each of them contains a sequence of [`SecurityAlgorithm`] values in wire format. /// /// Which exact option is to be used is specified via the `Variant` type /// argument. Three marker types `DauVariant`, `DhuVariant` and `N3uVariant` /// are defined with accompanying type aliases [`Dau`], [`Dhu`], and [`N3u`]. /// /// You can create a new value from anything that can be turned into an -/// iterator over [`SecAlg`] via the +/// iterator over [`SecurityAlgorithm`] via the /// [`from_sec_algs`][Understood::from_sec_algs] associated function. /// Once you have a value, you can iterate over the algorithms via the /// [`iter`][Understood::iter] method or use the [`IntoIterator`] implementation @@ -54,7 +54,7 @@ pub struct Understood { /// The octets with the data. /// - /// These octets contain a sequence of composed [`SecAlg`] values. + /// These octets contain a sequence of composed [`SecurityAlgorithm`] values. octets: Octs, } @@ -119,7 +119,7 @@ impl Understood { /// The operation will fail if the iterator returns more than 32,767 /// algorithms. pub fn from_sec_algs( - sec_algs: impl IntoIterator + sec_algs: impl IntoIterator ) -> Result where Octs: FromBuilder, @@ -211,11 +211,11 @@ impl Understood { } /// Returns an iterator over the algorithms in the data. - pub fn iter(&self) -> SecAlgsIter + pub fn iter(&self) -> SecurityAlgorithmIter where Octs: AsRef<[u8]>, { - SecAlgsIter::new(self.octets.as_ref()) + SecurityAlgorithmIter::new(self.octets.as_ref()) } } @@ -377,8 +377,8 @@ impl<'a, Variant, Octs> IntoIterator for &'a Understood where Octs: AsRef<[u8]> + ?Sized { - type Item = SecAlg; - type IntoIter = SecAlgsIter<'a>; + type Item = SecurityAlgorithm; + type IntoIter = SecurityAlgorithmIter<'a>; fn into_iter(self) -> Self::IntoIter { self.iter() @@ -480,12 +480,12 @@ impl OptBuilder<'_, Target> { /// The DAU option lists the DNSSEC signature algorithms the requester /// supports. pub fn dau( - &mut self, algs: &impl AsRef<[SecAlg]>, + &mut self, algs: &impl AsRef<[SecurityAlgorithm]>, ) -> Result<(), BuildDataError> { Ok(self.push_raw_option( OptionCode::DAU, u16::try_from( - algs.as_ref().len() * usize::from(SecAlg::COMPOSE_LEN) + algs.as_ref().len() * usize::from(SecurityAlgorithm::COMPOSE_LEN) ).map_err(|_| BuildDataError::LongOptData)?, |octs| { algs.as_ref().iter().try_for_each(|item| item.compose(octs)) @@ -497,12 +497,12 @@ impl OptBuilder<'_, Target> { /// /// The DHU option lists the DS hash algorithms the requester supports. pub fn dhu( - &mut self, algs: &impl AsRef<[SecAlg]>, + &mut self, algs: &impl AsRef<[SecurityAlgorithm]>, ) -> Result<(), BuildDataError> { Ok(self.push_raw_option( OptionCode::DHU, u16::try_from( - algs.as_ref().len() * usize::from(SecAlg::COMPOSE_LEN) + algs.as_ref().len() * usize::from(SecurityAlgorithm::COMPOSE_LEN) ).map_err(|_| BuildDataError::LongOptData)?, |octs| { algs.as_ref().iter().try_for_each(|item| item.compose(octs)) @@ -514,12 +514,12 @@ impl OptBuilder<'_, Target> { /// /// The N3U option lists the NSEC3 hash algorithms the requester supports. pub fn n3u( - &mut self, algs: &impl AsRef<[SecAlg]>, + &mut self, algs: &impl AsRef<[SecurityAlgorithm]>, ) -> Result<(), BuildDataError> { Ok(self.push_raw_option( OptionCode::N3U, u16::try_from( - algs.as_ref().len() * usize::from(SecAlg::COMPOSE_LEN) + algs.as_ref().len() * usize::from(SecurityAlgorithm::COMPOSE_LEN) ).map_err(|_| BuildDataError::LongOptData)?, |octs| { algs.as_ref().iter().try_for_each(|item| item.compose(octs)) @@ -528,21 +528,21 @@ impl OptBuilder<'_, Target> { } } -//------------ SecAlgsIter --------------------------------------------------- +//------------ SecurityAlgorithmsIter ---------------------------------------- -pub struct SecAlgsIter<'a>(slice::Iter<'a, u8>); +pub struct SecurityAlgorithmIter<'a>(slice::Iter<'a, u8>); -impl<'a> SecAlgsIter<'a> { +impl<'a> SecurityAlgorithmIter<'a> { fn new(slice: &'a [u8]) -> Self { - SecAlgsIter(slice.iter()) + SecurityAlgorithmIter(slice.iter()) } } -impl Iterator for SecAlgsIter<'_> { - type Item = SecAlg; +impl Iterator for SecurityAlgorithmIter<'_> { + type Item = SecurityAlgorithm; fn next(&mut self) -> Option { - self.0.next().map(|x| SecAlg::from_int(*x)) + self.0.next().map(|x| SecurityAlgorithm::from_int(*x)) } } diff --git a/src/base/record.rs b/src/base/record.rs index 010f0f20e..22b83d134 100644 --- a/src/base/record.rs +++ b/src/base/record.rs @@ -1655,7 +1655,7 @@ mod test { #[cfg(feature = "bytes")] fn ds_octets_into() { use super::*; - use crate::base::iana::{Class, DigestAlg, SecAlg}; + use crate::base::iana::{Class, DigestAlgorithm, SecurityAlgorithm}; use crate::base::name::Name; use crate::rdata::Ds; use bytes::Bytes; @@ -1667,8 +1667,8 @@ mod test { Ttl::from_secs(86400), Ds::new( 12, - SecAlg::RSASHA256, - DigestAlg::SHA256, + SecurityAlgorithm::RSASHA256, + DigestAlgorithm::SHA256, b"something".as_ref(), ) .unwrap(), diff --git a/src/base/scan.rs b/src/base/scan.rs index 85756f898..e49d540f8 100644 --- a/src/base/scan.rs +++ b/src/base/scan.rs @@ -283,7 +283,7 @@ declare_error_trait!(ScannerError: Sized + fmt::Debug + fmt::Display); #[cfg(feature = "std")] impl ScannerError for std::io::Error { fn custom(msg: &'static str) -> Self { - std::io::Error::new(std::io::ErrorKind::Other, msg) + std::io::Error::other(msg) } fn end_of_entry() -> Self { @@ -294,11 +294,11 @@ impl ScannerError for std::io::Error { } fn short_buf() -> Self { - std::io::Error::new(std::io::ErrorKind::Other, ShortBuf) + std::io::Error::other(ShortBuf) } fn trailing_tokens() -> Self { - std::io::Error::new(std::io::ErrorKind::Other, "trailing data") + std::io::Error::other("trailing data") } } @@ -1168,7 +1168,7 @@ impl std::error::Error for BadSymbol {} #[cfg(feature = "std")] impl From for std::io::Error { fn from(err: BadSymbol) -> Self { - std::io::Error::new(std::io::ErrorKind::Other, err) + std::io::Error::other(err) } } diff --git a/src/base/zonefile_fmt.rs b/src/base/zonefile_fmt.rs index 8902d7cf3..a6ef30e64 100644 --- a/src/base/zonefile_fmt.rs +++ b/src/base/zonefile_fmt.rs @@ -311,7 +311,7 @@ mod test { use std::string::ToString as _; use std::vec::Vec; - use crate::base::iana::{Class, DigestAlg, SecAlg}; + use crate::base::iana::{Class, DigestAlgorithm, SecurityAlgorithm}; use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt}; use crate::base::{Name, Record, Ttl}; use crate::rdata::{Cds, Cname, Ds, Mx, Txt, A}; @@ -346,8 +346,8 @@ mod test { let record = create_record( Ds::new( 5414, - SecAlg::ED25519, - DigestAlg::SHA256, + SecurityAlgorithm::ED25519, + DigestAlgorithm::SHA256, &[0xDE, 0xAD, 0xBE, 0xEF], ) .unwrap(), @@ -373,8 +373,8 @@ mod test { let record = create_record( Cds::new( 5414, - SecAlg::ED25519, - DigestAlg::SHA256, + SecurityAlgorithm::ED25519, + DigestAlgorithm::SHA256, &[0xDE, 0xAD, 0xBE, 0xEF], ) .unwrap(), @@ -452,8 +452,8 @@ mod test { let record = create_record( Cds::new( 5414, - SecAlg::ED25519, - DigestAlg::SHA256, + SecurityAlgorithm::ED25519, + DigestAlgorithm::SHA256, &[0xDE, 0xAD, 0xBE, 0xEF], ) .unwrap(), diff --git a/src/crypto/common.rs b/src/crypto/common.rs new file mode 100644 index 000000000..8b3cd4d27 --- /dev/null +++ b/src/crypto/common.rs @@ -0,0 +1,290 @@ +//! DNSSEC message digests and signature verification using built-in backends. +//! +//! This backend supports all the algorithms supported by Ring and OpenSSL, +//! depending on whether the respective crate features are enabled. See the +//! documentation for each backend for more information. + +#![cfg(any(feature = "ring", feature = "openssl"))] +#![cfg_attr(docsrs, doc(cfg(any(feature = "ring", feature = "openssl"))))] + +use core::fmt; +use std::error; +use std::vec::Vec; + +use crate::rdata::Dnskey; + +#[cfg(feature = "openssl")] +use super::openssl; + +#[cfg(feature = "ring")] +use super::ring; + +//----------- DigestType ----------------------------------------------------- + +/// Type of message digest to compute. +pub enum DigestType { + /// [FIPS Secure Hash Standard] Section 6.1. + /// + /// [FIPS Secure Hash Standard]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf + Sha1, + + /// [FIPS Secure Hash Standard] Section 6.2. + /// + /// [FIPS Secure Hash Standard]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf + Sha256, + + /// [FIPS Secure Hash Standard] Section 6.5. + /// + /// [FIPS Secure Hash Standard]: http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf + Sha384, +} + +//----------- DigestBuilder -------------------------------------------------- + +/// Builder for computing a message digest. +pub enum DigestBuilder { + /// Use ring to compute the message digest. + #[cfg(feature = "ring")] + Ring(ring::DigestBuilder), + /// Use openssl to compute the message digest. + #[cfg(feature = "openssl")] + Openssl(openssl::DigestBuilder), +} + +impl DigestBuilder { + /// Create a new context for a specified digest type. + #[allow(unreachable_code)] + pub fn new(digest_type: DigestType) -> Self { + #[cfg(feature = "ring")] + return Self::Ring(ring::DigestBuilder::new(digest_type)); + + #[cfg(feature = "openssl")] + return Self::Openssl(openssl::DigestBuilder::new(digest_type)); + } + + /// Add input to the digest computation. + pub fn update(&mut self, data: &[u8]) { + match self { + #[cfg(feature = "ring")] + DigestBuilder::Ring(digest_context) => { + digest_context.update(data) + } + #[cfg(feature = "openssl")] + DigestBuilder::Openssl(digest_context) => { + digest_context.update(data) + } + } + } + + /// Finish computing the digest. + pub fn finish(self) -> Digest { + match self { + #[cfg(feature = "ring")] + DigestBuilder::Ring(digest_context) => { + Digest::Ring(digest_context.finish()) + } + #[cfg(feature = "openssl")] + DigestBuilder::Openssl(digest_context) => { + Digest::Openssl(digest_context.finish()) + } + } + } +} + +//----------- Digest --------------------------------------------------------- + +/// A message digest. +pub enum Digest { + /// A message digest computed using ring. + #[cfg(feature = "ring")] + Ring(ring::Digest), + /// A message digest computed using openssl. + #[cfg(feature = "openssl")] + Openssl(openssl::Digest), +} + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + match self { + #[cfg(feature = "ring")] + Digest::Ring(digest) => digest.as_ref(), + #[cfg(feature = "openssl")] + Digest::Openssl(digest) => digest.as_ref(), + } + } +} + +//----------- PublicKey ------------------------------------------------------ + +/// A public key for verifying a signature. +pub enum PublicKey { + /// A public key implemented using ring. + #[cfg(feature = "ring")] + Ring(ring::PublicKey), + + /// A public key implemented using openssl. + #[cfg(feature = "openssl")] + Openssl(openssl::PublicKey), +} + +impl PublicKey { + /// Create a public key from a [`Dnskey`]. + #[allow(unreachable_code)] + pub fn from_dnskey( + dnskey: &Dnskey>, + ) -> Result { + #[cfg(feature = "ring")] + return Ok(Self::Ring(ring::PublicKey::from_dnskey(dnskey)?)); + + #[cfg(feature = "openssl")] + return Ok(Self::Openssl(openssl::PublicKey::from_dnskey(dnskey)?)); + + #[cfg(not(any(feature = "ring", feature = "openssl")))] + compile_error!("Either feature \"ring\" or \"openssl\" must be enabled for this crate."); + } + + /// Verify a signature. + pub fn verify( + &self, + signed_data: &[u8], + signature: &[u8], + ) -> Result<(), AlgorithmError> { + match self { + #[cfg(feature = "ring")] + PublicKey::Ring(public_key) => { + public_key.verify(signed_data, signature) + } + #[cfg(feature = "openssl")] + PublicKey::Openssl(public_key) => { + public_key.verify(signed_data, signature) + } + } + } +} + +/// Return the RSA exponent and modulus components from DNSKEY record data. +pub fn rsa_exponent_modulus( + dnskey: &Dnskey>, + min_len: usize, +) -> Result<(Vec, Vec), AlgorithmError> { + let public_key: &[u8] = dnskey.public_key().as_ref(); + + // Determine the exponent length. + let (exp_len, rest) = match *public_key { + [exp_len @ 1..=255, ref rest @ ..] => (exp_len as usize, rest), + + [0, hi @ 1..=255, lo, ref rest @ ..] => { + let exp_len = u16::from_be_bytes([hi, lo]); + (exp_len as usize, rest) + } + + _ => return Err(AlgorithmError::InvalidData), + }; + + // Split the exponent and the modulus. + if rest.len() < exp_len { + return Err(AlgorithmError::InvalidData); + } + let (exp, num) = rest.split_at(exp_len); + + // Validate the exponent and modulus. + for i in [exp, num] { + // [RFC 3110, section 2](https://www.rfc-editor.org/rfc/rfc3110#section-2): + // + // > For interoperability, the exponent and modulus are each limited + // > to 4096 bits in length. + // + // > Leading zero octets are prohibited in the exponent and modulus. + if !(1..=512).contains(&i.len()) || i[0] == 0 { + return Err(AlgorithmError::InvalidData); + } + } + + // Check that the modulus is big enough for the caller. + if num.len() < min_len { + return Err(AlgorithmError::Unsupported); + } + + Ok((exp.to_vec(), num.to_vec())) +} + +/// Encode the RSA exponent and modulus components in DNSKEY record data +/// format. +pub fn rsa_encode(e: &[u8], n: &[u8]) -> Vec { + let mut key = Vec::new(); + + // Encode the exponent length. + if let Ok(exp_len) = u8::try_from(e.len()) { + key.reserve_exact(1 + e.len() + n.len()); + key.push(exp_len); + } else if let Ok(exp_len) = u16::try_from(e.len()) { + key.reserve_exact(3 + e.len() + n.len()); + key.push(0u8); + key.extend(&exp_len.to_be_bytes()); + } else { + unreachable!("RSA exponents are (much) shorter than 64KiB") + } + + key.extend(e); + key.extend(n); + + key +} + +//------------ AlgorithmError ------------------------------------------------ + +/// An algorithm error during verification. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum AlgorithmError { + /// Unsupported algorithm. + Unsupported, + + /// Bad signature. + BadSig, + + /// Invalid data. + InvalidData, +} + +//--- Display, Error + +impl fmt::Display for AlgorithmError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + AlgorithmError::Unsupported => "unsupported algorithm", + AlgorithmError::BadSig => "bad signature", + AlgorithmError::InvalidData => "invalid data", + }) + } +} + +impl error::Error for AlgorithmError {} + +//----------- FromDnskeyError ------------------------------------------------ + +/// An error in reading a DNSKEY record. +#[derive(Clone, Debug)] +pub enum FromDnskeyError { + /// The key's algorithm is not supported. + UnsupportedAlgorithm, + + /// The key's protocol is not supported. + UnsupportedProtocol, + + /// The key is not valid. + InvalidKey, +} + +//--- Display, Error + +impl fmt::Display for FromDnskeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "unsupported algorithm", + Self::UnsupportedProtocol => "unsupported protocol", + Self::InvalidKey => "malformed key", + }) + } +} + +impl error::Error for FromDnskeyError {} diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 000000000..a35077d74 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,133 @@ +//! Cryptographic backends, key generation and import. +//! +//! This module is enabled by the `unstable-crypto` or `unstable-crypto-sign` +//! feature flags. The `unstable-crypto` enables all features except for +//! private key operations such as generation and signing. All features of +//! this module are enabled with the `unstable-crypto-sign` feature flag. +//! +//! 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 (and, for RSA keys, supports +//! weaker key sizes). A +#![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] +#![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] +//! backend is provided for users that wish +//! to use either or both backends at runtime. +//! +//! Each backend module ( +#![cfg_attr( + all(feature = "unstable-crypto-sign", feature = "openssl"), + doc = "[`openssl::sign`]" +)] +#![cfg_attr( + not(all(feature = "unstable-crypto-sign", feature = "openssl")), + doc = "`openssl::sign`" +)] +//! , +#![cfg_attr( + all(feature = "unstable-crypto-sign", feature = "ring"), + doc = "[`ring::sign`]" +)] +#![cfg_attr( + not(all(feature = "unstable-crypto-sign", feature = "ring")), + doc = "`ring::sign`" +)] +//! , and +#![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] +#![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] +//! ) +//! 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 the +#![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign::SignRaw`]")] +#![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign::SignRaw`")] +//! trait. +//! +//! While each cryptographic backend can support a limited number of signature +//! algorithms, even the types independent of a cryptographic backend (e.g. +#![cfg_attr( + feature = "unstable-crypto-sign", + doc = "[`sign::SecretKeyBytes`]" +)] +#![cfg_attr( + not(feature = "unstable-crypto-sign"), + doc = "`sign::SecretKeyBytes`" +)] +//! and +#![cfg_attr( + feature = "unstable-crypto-sign", + doc = "[`sign::GenerateParams`]" +)] +#![cfg_attr( + not(feature = "unstable-crypto-sign"), + doc = "`sign::GenerateParams`" +)] +//! ) support a limited +//! number of algorithms. Even with custom cryptographic backends, +//! this module can only +//! support these algorithms. +//! +//! In addition to private key operations, this module provides the +#![cfg_attr( + any(feature = "ring", feature = "openssl"), + doc = "[`common::PublicKey`]" +)] +#![cfg_attr( + not(any(feature = "ring", feature = "openssl")), + doc = "`common::PublicKey`" +)] +//! type for signature verification. +//! +//! The module also support computing message digests using the +#![cfg_attr( + any(feature = "ring", feature = "openssl"), + doc = "[`common::DigestBuilder`]" +)] +#![cfg_attr( + not(any(feature = "ring", feature = "openssl")), + doc = "`common::DigestBuilder`" +)] +//! type. +//! +//! # Message digests +//! +//! Given some data compute a message digest. +//! +//! ``` +//! use domain::crypto::common::{DigestBuilder, DigestType}; +//! +//! let input = "Hello World!"; +//! let mut ctx = DigestBuilder::new(DigestType::Sha256); +//! ctx.update(input.as_bytes()); +//! ctx.finish().as_ref(); +//! ``` +//! +//! # Signature verification +//! +//! Given some data, a signature, and a DNSKEY, the signature can be verified. +//! +//! ```no_run +//! use domain::rdata::Dnskey; +//! use domain::crypto::common::PublicKey; +//! use domain::base::iana::SecurityAlgorithm; +//! +//! let keyraw = [0u8; 16]; +//! let input = "Hello World!"; +//! let bad_sig = [0u8; 16]; +//! let dnskey = Dnskey::new(256, 3, SecurityAlgorithm::ED25519, keyraw).unwrap(); +//! let public_key = PublicKey::from_dnskey(&dnskey).unwrap(); +//! let res = public_key.verify(input.as_bytes(), &bad_sig); +//! println!("verify result: {res:?}"); +//! ``` + +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + +pub mod common; +pub mod openssl; +pub mod ring; +pub mod sign; diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs new file mode 100644 index 000000000..302044cf0 --- /dev/null +++ b/src/crypto/openssl.rs @@ -0,0 +1,981 @@ +//! DNSSEC signing using OpenSSL. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (512-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 +//! - Ed448 + +#![cfg(feature = "openssl")] +#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] + +use core::fmt; + +use std::vec::Vec; + +use openssl::bn::{BigNum, BigNumContext}; +use openssl::ec::{EcGroup, EcKey, EcPoint, PointConversionForm}; +use openssl::ecdsa::EcdsaSig; +use openssl::error::ErrorStack; +use openssl::hash::{DigestBytes, Hasher, MessageDigest}; +use openssl::nid::Nid; +use openssl::pkey::{Id, PKey, Public}; +use openssl::rsa::Rsa; +use openssl::sign::Verifier; + +use super::common::{ + rsa_encode, rsa_exponent_modulus, AlgorithmError, DigestType, +}; +use crate::base::iana::SecurityAlgorithm; +use crate::rdata::Dnskey; + +//============ Error Types =================================================== + +//----------- FromBytesError ----------------------------------------------- + +/// An error in importing a key pair from bytes into OpenSSL. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for FromBytesError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair with OpenSSL. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ErrorStack) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +//----------- DigestBuilder -------------------------------------------------- + +/// Builder for computing a message digest. +pub struct DigestBuilder(Hasher); + +impl DigestBuilder { + /// Create a new builder for a specified digest type. + pub fn new(digest_type: DigestType) -> Self { + Self( + match digest_type { + DigestType::Sha1 => Hasher::new(MessageDigest::sha1()), + DigestType::Sha256 => Hasher::new(MessageDigest::sha256()), + DigestType::Sha384 => Hasher::new(MessageDigest::sha384()), + } + .expect("assume that new cannot fail"), + ) + } + + /// Add input to the digest computation. + pub fn update(&mut self, data: &[u8]) { + self.0 + .update(data) + .expect("assume that update does not fail") + } + + /// Finish computing the digest. + pub fn finish(mut self) -> Digest { + Digest(self.0.finish().expect("assume that finish does not fail")) + } +} + +//----------- Digest --------------------------------------------------------- + +/// A message digest. +pub struct Digest(DigestBytes); + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +//----------- PublicKey ------------------------------------------------------ + +/// A public key for verifying a signature. +pub enum PublicKey { + /// Variant for RSA. + Rsa(MessageDigest, PKey, u16), + + /// Variant for Ed25519 and Ed448. + NoDigest(PKey, u16), + + /// Variant for EcDsa. + EcDsa(MessageDigest, EcKey, u16), +} + +impl PublicKey { + /// Create a public key from a [`Dnskey`]. + pub fn from_dnskey( + dnskey: &Dnskey>, + ) -> Result { + let sec_alg = dnskey.algorithm(); + match sec_alg { + SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 => { + let (digest_algorithm, min_bytes) = match sec_alg { + SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 => { + (MessageDigest::sha1(), 1024 / 8) + } + SecurityAlgorithm::RSASHA256 => { + (MessageDigest::sha256(), 1024 / 8) + } + SecurityAlgorithm::RSASHA512 => { + (MessageDigest::sha512(), 1024 / 8) + } + _ => unreachable!(), + }; + + // The key isn't available in either PEM or DER, so use the + // direct RSA builder. + let (e, n) = rsa_exponent_modulus(dnskey, min_bytes)?; + let e = BigNum::from_slice(&e) + .map_err(|_| AlgorithmError::InvalidData)?; + let n = BigNum::from_slice(&n) + .map_err(|_| AlgorithmError::InvalidData)?; + let public_key = Rsa::from_public_components(n, e) + .map_err(|_| AlgorithmError::InvalidData)?; + let public_key = PKey::from_rsa(public_key) + .map_err(|_| AlgorithmError::InvalidData)?; + Ok(PublicKey::Rsa( + digest_algorithm, + public_key, + dnskey.flags(), + )) + } + SecurityAlgorithm::ECDSAP256SHA256 + | SecurityAlgorithm::ECDSAP384SHA384 => { + let (digest_algorithm, group_id) = match sec_alg { + SecurityAlgorithm::ECDSAP256SHA256 => { + (MessageDigest::sha256(), Nid::X9_62_PRIME256V1) + } + SecurityAlgorithm::ECDSAP384SHA384 => { + (MessageDigest::sha384(), Nid::SECP384R1) + } + _ => unreachable!(), + }; + + let group = EcGroup::from_curve_name(group_id) + .expect("should not fail"); + let mut ctx = BigNumContext::new().expect("should not fail"); + + // Add 0x4 identifier to the ECDSA pubkey as expected by openssl. + let public_key = dnskey.public_key().as_ref(); + let mut key = Vec::with_capacity(public_key.len() + 1); + key.push(0x4); + key.extend_from_slice(public_key); + let point = EcPoint::from_bytes(&group, &key, &mut ctx) + .map_err(|_| AlgorithmError::InvalidData)?; + let public_key = EcKey::from_public_key(&group, &point) + .map_err(|_| AlgorithmError::InvalidData)?; + public_key + .check_key() + .map_err(|_| AlgorithmError::InvalidData)?; + + Ok(PublicKey::EcDsa( + digest_algorithm, + public_key, + dnskey.flags(), + )) + } + SecurityAlgorithm::ED25519 => { + let public_key = PKey::public_key_from_raw_bytes( + dnskey.public_key().as_ref(), + Id::ED25519, + ) + .map_err(|_| AlgorithmError::InvalidData)?; + Ok(PublicKey::NoDigest(public_key, dnskey.flags())) + } + SecurityAlgorithm::ED448 => { + let public_key = PKey::public_key_from_raw_bytes( + dnskey.public_key().as_ref(), + Id::ED448, + ) + .map_err(|_| AlgorithmError::InvalidData)?; + Ok(PublicKey::NoDigest(public_key, dnskey.flags())) + } + _ => Err(AlgorithmError::Unsupported), + } + } + + /// Verify a signature. + pub fn verify( + &self, + signed_data: &[u8], + signature: &[u8], + ) -> Result<(), AlgorithmError> { + let valid = match self { + PublicKey::Rsa(digest_algorithm, public_key, _) => { + let mut verifier = + Verifier::new(*digest_algorithm, public_key.as_ref()) + .map_err(|_| AlgorithmError::InvalidData)?; + verifier + .verify_oneshot(signature, signed_data) + .map_err(|_| AlgorithmError::InvalidData)? + } + PublicKey::NoDigest(public_key, _) => { + let mut verifier = + Verifier::new_without_digest(public_key.as_ref()) + .map_err(|_| AlgorithmError::InvalidData)?; + verifier + .verify_oneshot(signature, signed_data) + .map_err(|_| AlgorithmError::InvalidData)? + } + PublicKey::EcDsa(digest_algorithm, public_key, _) => { + let half_len = signature.len() / 2; + let mut hasher = Hasher::new(*digest_algorithm) + .map_err(|_| AlgorithmError::InvalidData)?; + hasher + .update(signed_data) + .map_err(|_| AlgorithmError::InvalidData)?; + let hash = hasher + .finish() + .map_err(|_| AlgorithmError::InvalidData)?; + let r = BigNum::from_slice(&signature[0..half_len]) + .map_err(|_| AlgorithmError::InvalidData)?; + let s = BigNum::from_slice(&signature[half_len..]) + .map_err(|_| AlgorithmError::InvalidData)?; + let ecdsa_sig = EcdsaSig::from_private_components(r, s) + .map_err(|_| AlgorithmError::InvalidData)?; + ecdsa_sig + .verify(hash.as_ref(), public_key) + .map_err(|_| AlgorithmError::InvalidData)? + } + }; + if valid { + Ok(()) + } else { + Err(AlgorithmError::BadSig) + } + } + + /// Convert to a [`Dnskey`]. + pub fn dnskey(&self) -> Dnskey> { + match self { + PublicKey::Rsa(message_digest, public_key, flags) => { + let alg = if *message_digest == MessageDigest::sha1() { + // We have problem. This case can either be RSASHA1 or + // RSASHA1_NSEC3_SHA1. We would need an extra flag to + // record which one. Both are almost deprecated. Return + // RSASHA1_NSEC3_SHA1 because it is newer. If it causes + // problems then we need to be explicit. + SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + } else if *message_digest == MessageDigest::sha256() { + SecurityAlgorithm::RSASHA256 + } else if *message_digest == MessageDigest::sha512() { + SecurityAlgorithm::RSASHA512 + } else { + unreachable!(); + }; + let rsa = public_key.rsa().expect("should not fail"); + let e = rsa.e().to_vec(); + let n = rsa.n().to_vec(); + + let key = rsa_encode(&e, &n); + + Dnskey::new(*flags, 3, alg, key).expect("should not fail") + } + PublicKey::NoDigest(public_key, flags) => { + let alg = match public_key.id() { + Id::ED25519 => SecurityAlgorithm::ED25519, + Id::ED448 => SecurityAlgorithm::ED448, + _ => unreachable!(), + }; + + let key = + public_key.raw_public_key().expect("should not fail"); + Dnskey::new(*flags, 3, alg, key).expect("should not fail") + } + PublicKey::EcDsa(message_digest, public_key, flags) => { + let alg = if *message_digest == MessageDigest::sha256() { + SecurityAlgorithm::ECDSAP256SHA256 + } else if *message_digest == MessageDigest::sha384() { + SecurityAlgorithm::ECDSAP384SHA384 + } else { + unreachable!(); + }; + + let key = public_key.public_key(); + let group = public_key.group(); + let mut ctx = BigNumContext::new().expect("should not fail"); + let key = key + .to_bytes( + group, + PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .expect("should not fail"); + + // Openssl has an extra byte with the value 4 in front. + let key = key[1..].to_vec(); + + Dnskey::new(*flags, 3, alg, key).expect("should not fail") + } + } + } +} + +#[cfg(feature = "unstable-crypto-sign")] +/// Submodule for private keys and signing. +pub mod sign { + use std::boxed::Box; + use std::vec::Vec; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::sign::{ + GenerateParams, RsaSecretKeyBytes, SecretKeyBytes, SignError, + SignRaw, Signature, + }; + use crate::rdata::Dnskey; + + use super::{FromBytesError, GenerateError, PublicKey}; + + use openssl::bn::BigNum; + use openssl::ec::{EcGroup, EcKey}; + use openssl::ecdsa::EcdsaSig; + use openssl::error::ErrorStack; + use openssl::hash::MessageDigest; + use openssl::nid::Nid; + use openssl::pkey::{self, Id, PKey, Private}; + use openssl::rsa::Rsa; + + use secrecy::ExposeSecret; + + //----------- KeyPair ---------------------------------------------------- + + /// A key pair backed by OpenSSL. + #[derive(Clone, Debug)] + pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + /// Flags from [`Dnskey`]. + flags: u16, + + /// The private key. + pkey: PKey, + } + + //--- Conversion to and from bytes + + impl KeyPair { + /// Import a key pair from bytes into OpenSSL. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &Dnskey, + ) -> Result + where + Octs: AsRef<[u8]>, + { + /// Create a [`BigNum`] from a slice. + fn num(slice: &[u8]) -> Result { + Ok(BigNum::from_slice(slice)?) + } + + /// Create a [`BigNum`] from a slice with secure storage. + fn secure_num(slice: &[u8]) -> Result { + let mut v = BigNum::new_secure()?; + v.copy_from_slice(slice)?; + Ok(v) + } + + let pkey = match secret { + SecretKeyBytes::RsaSha256(s) => { + let n = num(&s.n)?; + let e = num(&s.e)?; + + // Ensure that the public and private key match. + let rsa_public = Rsa::from_public_components( + n.to_owned().expect("should not fail"), + e.to_owned().expect("should not fail"), + ) + .expect("should not fail"); + let rsa_public = + PKey::from_rsa(rsa_public).expect("should not fail"); + let p = PublicKey::Rsa( + MessageDigest::sha256(), + rsa_public, + public.flags(), + ) + .dnskey(); + if p != *public { + return Err(FromBytesError::InvalidKey); + } + + 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 + // deprecated methods called here. + + let key = Rsa::from_private_components( + n, e, d, p, q, d_p, d_q, q_i, + )?; + + if !key.check_key()? { + return Err(FromBytesError::InvalidKey); + } + + PKey::from_rsa(key)? + } + + SecretKeyBytes::EcdsaP256Sha256(s) => { + use openssl::ec; + + let group = Nid::X9_62_PRIME256V1; + let group = ec::EcGroup::from_curve_name(group)?; + let n = secure_num(s.expose_secret().as_slice())?; + + let public_key = PublicKey::from_dnskey(public) + .map_err(|_| FromBytesError::InvalidKey)?; + let PublicKey::EcDsa(_, eckey, _) = public_key else { + return Err(FromBytesError::InvalidKey); + }; + let p = eckey.public_key(); + + let k = + ec::EcKey::from_private_components(&group, &n, p)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; + PKey::from_ec_key(k)? + } + + SecretKeyBytes::EcdsaP384Sha384(s) => { + use openssl::ec; + + let group = ec::EcGroup::from_curve_name(Nid::SECP384R1)?; + let n = secure_num(s.expose_secret().as_slice())?; + + let public_key = PublicKey::from_dnskey(public) + .map_err(|_| FromBytesError::InvalidKey)?; + let PublicKey::EcDsa(_, eckey, _) = public_key else { + return Err(FromBytesError::InvalidKey); + }; + let p = eckey.public_key(); + + let k = + ec::EcKey::from_private_components(&group, &n, p)?; + k.check_key().map_err(|_| FromBytesError::InvalidKey)?; + PKey::from_ec_key(k)? + } + + SecretKeyBytes::Ed25519(s) => { + use openssl::memcmp; + + let id = pkey::Id::ED25519; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; + + let pub1 = k.raw_public_key().expect("should not fail"); + let pub2 = public.public_key().as_ref(); + + // The OpenSSL memcmp::eq() fn requires that the given + // arguments be of equal length otherwise it will panic + // so test their length before invoking memcmp::eq(). + if pub1.len() != pub2.len() || !memcmp::eq(&pub1, pub2) { + return Err(FromBytesError::InvalidKey); + } else { + k + } + } + + SecretKeyBytes::Ed448(s) => { + use openssl::memcmp; + + let id = pkey::Id::ED448; + let s = s.expose_secret(); + let k = PKey::private_key_from_raw_bytes(s, id)?; + + let pub1 = k.raw_public_key().expect("should not fail"); + let pub2 = public.public_key().as_ref(); + + // The OpenSSL memcmp::eq() fn requires that the given + // arguments be of equal length otherwise it will panic + // so test their length before invoking memcmp::eq(). + if pub1.len() != pub2.len() || !memcmp::eq(&pub1, pub2) { + return Err(FromBytesError::InvalidKey); + } else { + k + } + } + }; + + Ok(Self { + algorithm: secret.algorithm(), + flags: public.flags(), + pkey, + }) + } + + /// Export the secret key into bytes. + /// + /// # Panics + /// + /// Panics if OpenSSL fails or if memory could not be allocated. + pub fn to_bytes(&self) -> SecretKeyBytes { + // TODO: Consider security implications of secret data in 'Vec's. + match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + let key = self.pkey.rsa().unwrap(); + SecretKeyBytes::RsaSha256(RsaSecretKeyBytes { + n: key.n().to_vec().into(), + e: key.e().to_vec().into(), + d: key.d().to_vec().into(), + p: key.p().unwrap().to_vec().into(), + q: key.q().unwrap().to_vec().into(), + d_p: key.dmp1().unwrap().to_vec().into(), + d_q: key.dmq1().unwrap().to_vec().into(), + q_i: key.iqmp().unwrap().to_vec().into(), + }) + } + SecurityAlgorithm::ECDSAP256SHA256 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec_padded(32).unwrap(); + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP256Sha256(key.into()) + } + SecurityAlgorithm::ECDSAP384SHA384 => { + let key = self.pkey.ec_key().unwrap(); + let key = key.private_key().to_vec_padded(48).unwrap(); + let key: Box<[u8; 48]> = key.try_into().unwrap(); + SecretKeyBytes::EcdsaP384Sha384(key.into()) + } + SecurityAlgorithm::ED25519 => { + let key = self.pkey.raw_private_key().unwrap(); + let key: Box<[u8; 32]> = key.try_into().unwrap(); + SecretKeyBytes::Ed25519(key.into()) + } + SecurityAlgorithm::ED448 => { + let key = self.pkey.raw_private_key().unwrap(); + let key: Box<[u8; 57]> = key.try_into().unwrap(); + SecretKeyBytes::Ed448(key.into()) + } + _ => unreachable!(), + } + } + } + + //--- Signing + + impl KeyPair { + /// Sign some data. + fn sign(&self, data: &[u8]) -> Result, ErrorStack> { + use openssl::hash::MessageDigest; + use openssl::sign::Signer; + + match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey)?; + s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; + s.sign_oneshot_to_vec(data) + } + + SecurityAlgorithm::ECDSAP256SHA256 => { + let mut s = + Signer::new(MessageDigest::sha256(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(32)?; + let mut s = signature.s().to_vec_padded(32)?; + r.append(&mut s); + Ok(r) + } + SecurityAlgorithm::ECDSAP384SHA384 => { + let mut s = + Signer::new(MessageDigest::sha384(), &self.pkey)?; + let signature = s.sign_oneshot_to_vec(data)?; + // Convert from DER to the fixed representation. + let signature = EcdsaSig::from_der(&signature)?; + let mut r = signature.r().to_vec_padded(48)?; + let mut s = signature.s().to_vec_padded(48)?; + r.append(&mut s); + Ok(r) + } + + SecurityAlgorithm::ED25519 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + SecurityAlgorithm::ED448 => { + let mut s = Signer::new_without_digest(&self.pkey)?; + s.sign_oneshot_to_vec(data) + } + + _ => unreachable!(), + } + } + } + + //--- SignRaw + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn dnskey(&self) -> Dnskey> { + match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + let key = self.pkey.rsa().expect("should not fail"); + let n = key.n().to_owned().expect("should not fail"); + let e = key.e().to_owned().expect("should not fail"); + let key = Rsa::from_public_components(n, e) + .expect("should not fail"); + let key = PKey::from_rsa(key).expect("should not fail"); + let public = PublicKey::Rsa( + MessageDigest::sha256(), + key, + self.flags, + ); + public.dnskey() + } + SecurityAlgorithm::ECDSAP256SHA256 + | SecurityAlgorithm::ECDSAP384SHA384 => { + let (digest_algorithm, group_id) = match self.algorithm { + SecurityAlgorithm::ECDSAP256SHA256 => { + (MessageDigest::sha256(), Nid::X9_62_PRIME256V1) + } + SecurityAlgorithm::ECDSAP384SHA384 => { + (MessageDigest::sha384(), Nid::SECP384R1) + } + _ => unreachable!(), + }; + + let key = self.pkey.ec_key().expect("should not fail"); + let key = key.public_key(); + let group = EcGroup::from_curve_name(group_id) + .expect("should not fail"); + let public_key = EcKey::from_public_key(&group, key) + .expect("should not fail"); + public_key.check_key().expect("should not fail"); + let public = PublicKey::EcDsa( + digest_algorithm, + public_key, + self.flags, + ); + public.dnskey() + } + SecurityAlgorithm::ED25519 | SecurityAlgorithm::ED448 => { + let id = match self.algorithm { + SecurityAlgorithm::ED25519 => Id::ED25519, + SecurityAlgorithm::ED448 => Id::ED448, + _ => unreachable!(), + }; + + let key = + self.pkey.raw_public_key().expect("should not fail"); + let key = PKey::public_key_from_raw_bytes(&key, id) + .expect("shoul not fail"); + let public = PublicKey::NoDigest(key, self.flags); + public.dnskey() + } + _ => unreachable!(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let signature = self + .sign(data) + .map(Vec::into_boxed_slice) + .map_err(|_| SignError)?; + + match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + Ok(Signature::RsaSha256(signature)) + } + + SecurityAlgorithm::ECDSAP256SHA256 => signature + .try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError), + SecurityAlgorithm::ECDSAP384SHA384 => signature + .try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError), + + SecurityAlgorithm::ED25519 => signature + .try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError), + SecurityAlgorithm::ED448 => signature + .try_into() + .map(Signature::Ed448) + .map_err(|_| SignError), + + _ => unreachable!(), + } + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new secret key for the given algorithm. + pub fn generate( + params: GenerateParams, + flags: u16, + ) -> Result { + let algorithm = params.algorithm(); + let pkey = match params { + GenerateParams::RsaSha256 { bits } => { + Rsa::generate(bits).and_then(PKey::from_rsa)? + } + GenerateParams::EcdsaP256Sha256 => { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? + } + GenerateParams::EcdsaP384Sha384 => { + let group = EcGroup::from_curve_name(Nid::SECP384R1)?; + PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? + } + GenerateParams::Ed25519 => PKey::generate_ed25519()?, + GenerateParams::Ed448 => PKey::generate_ed448()?, + }; + + Ok(KeyPair { + algorithm, + flags, + pkey, + }) + } + + //============ Tests ===================================================== + + #[cfg(test)] + mod tests { + + use std::string::ToString; + use std::vec::Vec; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::sign::{GenerateParams, SecretKeyBytes, SignRaw}; + use crate::dnssec::common::parse_from_bind; + + use super::KeyPair; + + const KEYS: &[(SecurityAlgorithm, u16)] = &[ + (SecurityAlgorithm::RSASHA256, 60616), + (SecurityAlgorithm::ECDSAP256SHA256, 42253), + (SecurityAlgorithm::ECDSAP384SHA384, 33566), + (SecurityAlgorithm::ED25519, 56037), + (SecurityAlgorithm::ED448, 7379), + ]; + + #[test] + fn generate() { + for &(algorithm, _) in KEYS { + let params = match algorithm { + SecurityAlgorithm::RSASHA256 => { + GenerateParams::RsaSha256 { bits: 3072 } + } + SecurityAlgorithm::ECDSAP256SHA256 => { + GenerateParams::EcdsaP256Sha256 + } + SecurityAlgorithm::ECDSAP384SHA384 => { + GenerateParams::EcdsaP384Sha384 + } + SecurityAlgorithm::ED25519 => GenerateParams::Ed25519, + SecurityAlgorithm::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let _ = crate::crypto::sign::generate(params, 0).unwrap(); + } + } + + #[test] + fn generated_roundtrip() { + for &(algorithm, _) in KEYS { + let params = match algorithm { + SecurityAlgorithm::RSASHA256 => { + GenerateParams::RsaSha256 { bits: 3072 } + } + SecurityAlgorithm::ECDSAP256SHA256 => { + GenerateParams::EcdsaP256Sha256 + } + SecurityAlgorithm::ECDSAP384SHA384 => { + GenerateParams::EcdsaP384Sha384 + } + SecurityAlgorithm::ED25519 => GenerateParams::Ed25519, + SecurityAlgorithm::ED448 => GenerateParams::Ed448, + _ => unreachable!(), + }; + + let key = super::generate(params, 256).unwrap(); + let gen_key = key.to_bytes(); + let pub_key = key.dnskey(); + let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); + assert!(key.pkey.public_eq(&equiv.pkey)); + } + } + + #[test] + fn imported_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); + let same = key.to_bytes().display_as_bind().to_string(); + + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); + assert_eq!(data, same); + } + } + + #[test] + fn public_key() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); + + assert_eq!(key.dnskey(), *pub_key.data()); + } + } + + #[test] + fn mismatched_public_key() { + for i in 1..KEYS.len() { + if KEYS[i - 1].0 == KEYS[i].0 { + continue; + } + + // Found a pair of keys whose algorithms differ. + let alg1 = KEYS[i - 1].0; + let alg2 = KEYS[i].0; + let key_tag1 = KEYS[i - 1].1; + let key_tag2 = KEYS[i].1; + + let name1 = + format!("test.+{:03}+{:05}", alg1.to_int(), key_tag1); + let path = + format!("test-data/dnssec-keys/K{}.private", name1); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let name2 = + format!("test.+{:03}+{:05}", alg2.to_int(), key_tag2); + let path = format!("test-data/dnssec-keys/K{}.key", name2); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + assert!( + KeyPair::from_bytes(&gen_key, pub_key.data()).is_err() + ); + } + } + + #[test] + fn sign() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); + + let _ = key.sign_raw(b"Hello, World!").unwrap(); + } + } + } +} diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs new file mode 100644 index 000000000..6ce4f3b18 --- /dev/null +++ b/src/crypto/ring.rs @@ -0,0 +1,765 @@ +//! DNSSEC signing using `ring`. +//! +//! This backend supports the following algorithms: +//! +//! - RSA/SHA-256 (2048-bit keys or larger) +//! - ECDSA P-256/SHA-256 +//! - ECDSA P-384/SHA-384 +//! - Ed25519 + +#![cfg(feature = "ring")] +#![cfg_attr(docsrs, doc(cfg(feature = "ring")))] + +use core::fmt; + +use std::ptr; +use std::vec::Vec; + +use ring::digest; +use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; +use ring::digest::{Context, Digest as RingDigest}; +use ring::rsa::PublicKeyComponents; +use ring::signature::{self, RsaParameters, UnparsedPublicKey}; + +use super::common::{ + rsa_encode, rsa_exponent_modulus, AlgorithmError, DigestType, +}; + +use crate::base::iana::SecurityAlgorithm; +use crate::rdata::Dnskey; + +//============ Error Types =================================================== + +/// An error in importing a key pair from bytes into Ring. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The provided keypair was invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair with Ring. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +impl From for GenerateError { + fn from(_: ring::error::Unspecified) -> Self { + Self::Implementation + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +//----------- DigestBuilder -------------------------------------------------- + +/// Builder for computing a message digest. +pub struct DigestBuilder(Context); + +impl DigestBuilder { + /// Create a new builder for a specified digest type. + pub fn new(digest_type: DigestType) -> Self { + Self(match digest_type { + DigestType::Sha1 => Context::new(&SHA1_FOR_LEGACY_USE_ONLY), + DigestType::Sha256 => Context::new(&digest::SHA256), + DigestType::Sha384 => Context::new(&digest::SHA384), + }) + } + + /// Add input to the digest computation. + pub fn update(&mut self, data: &[u8]) { + self.0.update(data) + } + + /// Finish computing the digest. + pub fn finish(self) -> Digest { + Digest(self.0.finish()) + } +} + +//----------- Digest --------------------------------------------------------- + +/// A message digest. +pub struct Digest(RingDigest); + +impl AsRef<[u8]> for Digest { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +//----------- PublicKey ------------------------------------------------------ + +/// A public key for verifying a signature. +pub enum PublicKey { + /// Variant for RSA public keys. + Rsa(&'static RsaParameters, PublicKeyComponents>), + + /// Variant for elliptic-curve public keys. + Unparsed(SecurityAlgorithm, UnparsedPublicKey>), +} + +impl PublicKey { + /// Create a public key from a [`Dnskey`]. + pub fn from_dnskey( + dnskey: &Dnskey>, + ) -> Result { + let sec_alg = dnskey.algorithm(); + match sec_alg { + SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 => { + let (algorithm, min_bytes) = match sec_alg { + SecurityAlgorithm::RSASHA1 | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 => ( + &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, + 1024 / 8, + ), + SecurityAlgorithm::RSASHA256 => ( + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, + 1024 / 8, + ), + SecurityAlgorithm::RSASHA512 => ( + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY, + 1024 / 8, + ), + _ => unreachable!(), + }; + + // The key isn't available in either PEM or DER, so use the + // direct RSA verifier. + let (e, n) = rsa_exponent_modulus(dnskey, min_bytes)?; + let public_key = signature::RsaPublicKeyComponents { n, e }; + Ok(PublicKey::Rsa(algorithm, public_key)) + } + SecurityAlgorithm::ECDSAP256SHA256 + | SecurityAlgorithm::ECDSAP384SHA384 => { + let algorithm = match sec_alg { + SecurityAlgorithm::ECDSAP256SHA256 => { + &signature::ECDSA_P256_SHA256_FIXED + } + SecurityAlgorithm::ECDSAP384SHA384 => { + &signature::ECDSA_P384_SHA384_FIXED + } + _ => unreachable!(), + }; + + // Add 0x4 identifier to the ECDSA pubkey as expected by ring. + let public_key = dnskey.public_key().as_ref(); + let mut key = Vec::with_capacity(public_key.len() + 1); + key.push(0x4); + key.extend_from_slice(public_key); + + Ok(PublicKey::Unparsed( + sec_alg, + signature::UnparsedPublicKey::new(algorithm, key), + )) + } + SecurityAlgorithm::ED25519 => { + let key = dnskey.public_key().as_ref().to_vec(); + let algorithm = &signature::ED25519; + Ok(PublicKey::Unparsed( + sec_alg, + signature::UnparsedPublicKey::new(algorithm, key), + )) + } + _ => Err(AlgorithmError::Unsupported), + } + } + + /// Verify a signature. + pub fn verify( + &self, + signed_data: &[u8], + signature: &[u8], + ) -> Result<(), AlgorithmError> { + match self { + PublicKey::Rsa(algorithm, public_key) => { + public_key.verify(algorithm, signed_data, signature) + } + PublicKey::Unparsed(_, public_key) => { + public_key.verify(signed_data, signature) + } + } + .map_err(|_| AlgorithmError::BadSig) + } + + /// Convert to a [`Dnskey`]. + pub fn dnskey(&self, flags: u16) -> Dnskey> { + match self { + PublicKey::Rsa(parameters, components) => { + let alg = if ptr::eq( + *parameters as *const _, + &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY + as *const _, + ) { + // This is a bit of a problem. It could also be RSASHA1. + // Assume that we do not generate new RSASHA1 keys. + // If we do, we need an extra field. + SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + } else if ptr::eq( + *parameters as *const _, + &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY + as *const _, + ) { + SecurityAlgorithm::RSASHA256 + } else if ptr::eq( + *parameters as *const _, + &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY + as *const _, + ) { + SecurityAlgorithm::RSASHA512 + } else { + unreachable!(); + }; + + let e = &components.e; + let n = &components.n; + + let key = rsa_encode(e, n); + + Dnskey::new(flags, 3, alg, key).expect("new should not fail") + } + PublicKey::Unparsed(algorithm, unparsed) => { + match *algorithm { + SecurityAlgorithm::ECDSAP256SHA256 + | SecurityAlgorithm::ECDSAP384SHA384 => { + // Ring has an extra byte with the value 4 in front. + let p = unparsed.as_ref(); + let p = p[1..].to_vec(); + Dnskey::new(flags, 3, *algorithm, p) + .expect("new should not fail") + } + SecurityAlgorithm::ED25519 => Dnskey::new( + flags, + 3, + *algorithm, + unparsed.as_ref().to_vec(), + ) + .expect("new should not fail"), + _ => unreachable!(), + } + } + } + } + + // key_size should only be called for RSA keys to see if the key is long + // enough to be supported by ring. + #[cfg(feature = "unstable-crypto-sign")] + /// Compute the key size. This is currently only implemented for RSA. + pub(super) fn key_size(&self) -> usize { + match self { + PublicKey::Rsa(_, components) => { + let n = &components.n; + n.len() * 8 + } + PublicKey::Unparsed(_, _) => unreachable!(), + } + } +} + +#[cfg(feature = "unstable-crypto-sign")] +/// Submodule for private keys and signing. +pub mod sign { + use std::boxed::Box; + use std::sync::Arc; + use std::vec::Vec; + + use secrecy::ExposeSecret; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::sign::{ + FromBytesError, GenerateParams, SecretKeyBytes, SignError, SignRaw, + Signature, + }; + use crate::rdata::Dnskey; + + use super::{GenerateError, PublicKey}; + + use ring::rand::SystemRandom; + use ring::signature::{ + self, EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, + }; + + //----------- KeyPair ---------------------------------------------------- + + /// A key pair backed by `ring`. + // Note: ring does not implement Clone for *KeyPair. + #[derive(Debug)] + pub enum KeyPair { + /// An RSA/SHA-256 keypair. + RsaSha256 { + /// They RSA key. + key: RsaKeyPair, + + /// Flags from [`Dnskey`]. + flags: u16, + + /// Random number generator. + rng: Arc, + }, + + /// An ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256 { + /// The ECDSA key. + key: EcdsaKeyPair, + + /// Flags from [`Dnskey`]. + flags: u16, + + /// Random number generator. + rng: Arc, + }, + + /// An ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384 { + /// The ECDSA key. + key: EcdsaKeyPair, + + /// Flags from [`Dnskey`]. + flags: u16, + + /// Random number generator. + rng: Arc, + }, + + /// An Ed25519 keypair. + Ed25519(Ed25519KeyPair, u16), + } + + //--- Conversion from bytes + + impl KeyPair { + /// Import a key pair from bytes into OpenSSL. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &Dnskey, + ) -> Result + where + Octs: AsRef<[u8]>, + { + let rng = Arc::new(SystemRandom::new()); + match secret { + SecretKeyBytes::RsaSha256(s) => { + let rsa_public = signature::RsaPublicKeyComponents { + n: s.n.to_vec(), + e: s.e.to_vec(), + }; + let p = PublicKey::Rsa(&signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, rsa_public).dnskey(public.flags()); + // Ensure that the public and private key match. + if p != *public { + return Err(FromBytesError::InvalidKey); + } + + // Ensure that the key is strong enough. + if s.n.len() < 2048 / 8 { + return Err(FromBytesError::WeakKey); + } + + let components = ring::rsa::KeyPairComponents { + public_key: ring::rsa::PublicKeyComponents { + n: s.n.as_ref(), + e: s.e.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) + .map(|key| Self::RsaSha256 { + key, + flags: public.flags(), + rng, + }) + } + + SecretKeyBytes::EcdsaP256Sha256(s) => { + let alg = + &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + + let public_key = PublicKey::from_dnskey(public) + .map_err(|_| FromBytesError::InvalidKey)?; + let PublicKey::Unparsed(_, unparsed) = public_key else { + return Err(FromBytesError::InvalidKey); + }; + + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.expose_secret(), + unparsed.as_ref(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP256Sha256 { + key, + flags: public.flags(), + rng, + }) + } + + SecretKeyBytes::EcdsaP384Sha384(s) => { + let alg = + &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + + let public_key = PublicKey::from_dnskey(public) + .map_err(|_| FromBytesError::InvalidKey)?; + let PublicKey::Unparsed(_, unparsed) = public_key else { + return Err(FromBytesError::InvalidKey); + }; + + EcdsaKeyPair::from_private_key_and_public_key( + alg, + s.expose_secret(), + unparsed.as_ref(), + &*rng, + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|key| Self::EcdsaP384Sha384 { + key, + flags: public.flags(), + rng, + }) + } + + SecretKeyBytes::Ed25519(s) => { + Ed25519KeyPair::from_seed_and_public_key( + s.expose_secret(), + public.public_key().as_ref(), + ) + .map_err(|_| FromBytesError::InvalidKey) + .map(|k| Self::Ed25519(k, public.flags())) + } + + SecretKeyBytes::Ed448(_) => { + Err(FromBytesError::UnsupportedAlgorithm) + } + } + } + } + + //--- SignRaw + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + match self { + Self::RsaSha256 { .. } => SecurityAlgorithm::RSASHA256, + Self::EcdsaP256Sha256 { .. } => { + SecurityAlgorithm::ECDSAP256SHA256 + } + Self::EcdsaP384Sha384 { .. } => { + SecurityAlgorithm::ECDSAP384SHA384 + } + Self::Ed25519(_, _) => SecurityAlgorithm::ED25519, + } + } + + fn dnskey(&self) -> Dnskey> { + match self { + Self::RsaSha256 { key, flags, rng: _ } => { + let components: ring::rsa::PublicKeyComponents> = + key.public().into(); + let n = components.n; + let e = components.e; + let public_key = + signature::RsaPublicKeyComponents { n, e }; + let public = PublicKey::Rsa(&signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, public_key); + public.dnskey(*flags) + } + + Self::EcdsaP256Sha256 { key, flags, rng: _ } + | Self::EcdsaP384Sha384 { key, flags, rng: _ } => { + let (algorithm, sec_alg) = match self { + Self::EcdsaP256Sha256 { + key: _, + flags: _, + rng: _, + } => ( + &signature::ECDSA_P256_SHA256_FIXED, + SecurityAlgorithm::ECDSAP256SHA256, + ), + Self::EcdsaP384Sha384 { + key: _, + flags: _, + rng: _, + } => ( + &signature::ECDSA_P384_SHA384_FIXED, + SecurityAlgorithm::ECDSAP384SHA384, + ), + _ => unreachable!(), + }; + let key = key.public_key().as_ref(); + let public = PublicKey::Unparsed( + sec_alg, + signature::UnparsedPublicKey::new( + algorithm, + key.to_vec(), + ), + ); + public.dnskey(*flags) + } + Self::Ed25519(key, flags) => { + let (algorithm, sec_alg) = match self { + Self::Ed25519(_, _) => { + (&signature::ED25519, SecurityAlgorithm::ED25519) + } + _ => unreachable!(), + }; + let key = key.public_key().as_ref(); + let public = PublicKey::Unparsed( + sec_alg, + signature::UnparsedPublicKey::new( + algorithm, + key.to_vec(), + ), + ); + public.dnskey(*flags) + } + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + Self::RsaSha256 { key, flags: _, rng } => { + let mut buf = vec![0u8; key.public().modulus_len()]; + let pad = &ring::signature::RSA_PKCS1_SHA256; + key.sign(pad, &**rng, data, &mut buf) + .map(|()| { + Signature::RsaSha256(buf.into_boxed_slice()) + }) + .map_err(|_| SignError) + } + + Self::EcdsaP256Sha256 { key, flags: _, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP256Sha256) + .map_err(|_| SignError) + }), + + Self::EcdsaP384Sha384 { key, flags: _, rng } => key + .sign(&**rng, data) + .map(|sig| Box::<[u8]>::from(sig.as_ref())) + .map_err(|_| SignError) + .and_then(|buf| { + buf.try_into() + .map(Signature::EcdsaP384Sha384) + .map_err(|_| SignError) + }), + + Self::Ed25519(key, _) => { + let sig = key.sign(data); + let buf: Box<[u8]> = sig.as_ref().into(); + buf.try_into() + .map(Signature::Ed25519) + .map_err(|_| SignError) + } + } + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new key pair for the given algorithm. + /// + /// While this uses Ring internally, the opaque nature of Ring means that it + /// is not possible to export a secret key from [`KeyPair`]. Thus, the bytes + /// of the secret key are returned directly. + pub fn generate( + params: GenerateParams, + flags: u16, + rng: &dyn ring::rand::SecureRandom, + ) -> Result<(SecretKeyBytes, Dnskey>), GenerateError> { + match params { + GenerateParams::EcdsaP256Sha256 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[36..68]); + 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 = doc.as_ref()[73..138].to_vec(); + let algorithm = &signature::ECDSA_P256_SHA256_FIXED; + let sec_alg = SecurityAlgorithm::ECDSAP256SHA256; + let pk = signature::UnparsedPublicKey::new(algorithm, pk); + let pk = PublicKey::Unparsed(sec_alg, pk); + + Ok((sk, pk.dnskey(flags))) + } + + GenerateParams::EcdsaP384Sha384 => { + // Generate a key and a PKCS#8 document out of Ring. + let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; + let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[35..83]); + 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 = doc.as_ref()[88..185].to_vec(); + let algorithm = &signature::ECDSA_P384_SHA384_FIXED; + let sec_alg = SecurityAlgorithm::ECDSAP384SHA384; + let pk = signature::UnparsedPublicKey::new(algorithm, pk); + let pk = PublicKey::Unparsed(sec_alg, pk); + Ok((sk, pk.dnskey(flags))) + } + + GenerateParams::Ed25519 => { + // Generate a key and a PKCS#8 document out of Ring. + let doc = Ed25519KeyPair::generate_pkcs8(rng)?; + + // Manually parse the PKCS#8 document for the private key. + let sk: Box<[u8]> = Box::from(&doc.as_ref()[16..48]); + 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 = doc.as_ref()[51..83].to_vec(); + let algorithm = &signature::ED25519; + let sec_alg = SecurityAlgorithm::ED25519; + let pk = signature::UnparsedPublicKey::new(algorithm, pk); + let pk = PublicKey::Unparsed(sec_alg, pk); + + Ok((sk, pk.dnskey(flags))) + } + + _ => Err(GenerateError::UnsupportedAlgorithm), + } + } + + //============ Tests ===================================================== + + #[cfg(test)] + mod test { + + use std::vec::Vec; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::ring::sign::KeyPair; + use crate::crypto::sign::{GenerateParams, SecretKeyBytes, SignRaw}; + use crate::dnssec::common::parse_from_bind; + + const GENERATE_PARAMS: &[GenerateParams] = &[ + GenerateParams::EcdsaP256Sha256, + GenerateParams::EcdsaP384Sha384, + GenerateParams::Ed25519, + ]; + + const KEYS: &[(SecurityAlgorithm, u16)] = &[ + (SecurityAlgorithm::RSASHA256, 60616), + (SecurityAlgorithm::ECDSAP256SHA256, 42253), + (SecurityAlgorithm::ECDSAP384SHA384, 33566), + (SecurityAlgorithm::ED25519, 56037), + ]; + + #[test] + fn generated_roundtrip() { + for params in GENERATE_PARAMS { + let (sk, pk) = + crate::crypto::sign::generate(params.clone(), 256) + .unwrap(); + let key = KeyPair::from_bytes(&sk, &pk).unwrap(); + assert_eq!(key.dnskey(), pk); + } + } + + #[test] + fn public_key() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); + + assert_eq!(key.dnskey(), *pub_key.data()); + } + } + + #[test] + fn sign() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + let key = + KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); + + let _ = key.sign_raw(b"Hello, World!").unwrap(); + } + } + } +} diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs new file mode 100644 index 000000000..ba5920304 --- /dev/null +++ b/src/crypto/sign.rs @@ -0,0 +1,1098 @@ +//! DNSSEC signing using built-in backends. +//! +//! This backend supports all the algorithms supported by Ring and OpenSSL, +//! depending on whether the respective crate features are enabled. See the +//! documentation for each backend for more information. +//! +//! The [`SecretKeyBytes`] type is a generic representation of a secret key as +//! a byte slice. While it does not offer any cryptographic functionality, it +//! is useful to transfer secret keys stored in memory, independent of any +//! cryptographic backend. +//! +//! [`SecretKeyBytes`] also supports importing and exporting keys from and to +//! the conventional private-key format popularized by BIND. This format is +//! used by a variety of tools for storing DNSSEC keys on disk. See the +//! type-level documentation for a specification of the format. +//! +//! # Importing keys +//! +//! Keys can be imported from files stored on disk in the conventional BIND +//! format. +//! +//! ``` +//! # use domain::base::iana::SecurityAlgorithm; +//! # use domain::crypto::sign::{KeyPair, self, SecretKeyBytes, SignRaw}; +//! # use domain::dnssec::common::parse_from_bind; +//! // Load an Ed25519 key named 'Ktest.+015+56037'. +//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; +//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); +//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); +//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); +//! let pub_key = parse_from_bind::>(&pub_text).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = KeyPair::from_bytes(&sec_bytes, pub_key.data()) +//! .unwrap(); +//! +//! // Check that the owner, algorithm, and key tag matched expectations. +//! assert_eq!(key_pair.algorithm(), SecurityAlgorithm::ED25519); +//! assert_eq!(key_pair.dnskey().key_tag(), 56037); +//! ``` +//! +//! # Generating keys +//! +//! Keys can also be generated. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::crypto::common; +//! # use domain::crypto::sign::{generate, GenerateParams, KeyPair}; +//! // Generate a new Ed25519 key. +//! let params = GenerateParams::Ed25519; +//! let (sec_bytes, pub_key) = generate(params, 257).unwrap(); +//! +//! // Parse the key into Ring or OpenSSL. +//! let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_key).unwrap(); +//! +//! // Access the public key (with metadata). +//! println!("{:?}", pub_key); +//! ``` +//! +//! # Signing data +//! +//! Given some data and a key, the data can be signed with the key. +//! +//! ``` +//! # use domain::base::Name; +//! # use domain::crypto::common; +//! # use domain::crypto::sign::{generate, GenerateParams, KeyPair, SignRaw}; +//! # let (sec_bytes, pub_bytes) = generate( +//! GenerateParams::Ed25519, +//! 256).unwrap(); +//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); +//! // Sign arbitrary byte sequences with the key. +//! let sig = key_pair.sign_raw(b"Hello, World!").unwrap(); +//! println!("{:?}", sig); +//! ``` +//! + +#![cfg(feature = "unstable-crypto-sign")] +#![cfg_attr(docsrs, doc(cfg(feature = "unstable-crypto-sign")))] + +use std::boxed::Box; +use std::fmt; +use std::vec::Vec; + +use secrecy::{ExposeSecret, SecretBox}; + +use crate::base::iana::SecurityAlgorithm; +use crate::rdata::Dnskey; +use crate::utils::base64; + +#[cfg(feature = "openssl")] +use super::openssl; + +#[cfg(feature = "ring")] +use super::ring; + +//----------- GenerateParams ------------------------------------------------- + +/// Parameters for generating a secret key. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum GenerateParams { + /// Generate an RSA/SHA-256 keypair. + RsaSha256 { + /// The number of bits in the public modulus. + /// + /// 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, + }, + + /// Generate an ECDSA P-256/SHA-256 keypair. + EcdsaP256Sha256, + + /// Generate an ECDSA P-384/SHA-384 keypair. + EcdsaP384Sha384, + + /// Generate an Ed25519 keypair. + Ed25519, + + /// An Ed448 keypair. + Ed448, +} + +//--- Inspection + +impl GenerateParams { + /// The algorithm of the generated key. + pub fn algorithm(&self) -> SecurityAlgorithm { + match self { + Self::RsaSha256 { .. } => SecurityAlgorithm::RSASHA256, + Self::EcdsaP256Sha256 => SecurityAlgorithm::ECDSAP256SHA256, + Self::EcdsaP384Sha384 => SecurityAlgorithm::ECDSAP384SHA384, + Self::Ed25519 => SecurityAlgorithm::ED25519, + Self::Ed448 => SecurityAlgorithm::ED448, + } + } +} + +//----------- SignRaw -------------------------------------------------------- + +/// Low-level signing functionality. +/// +/// Types that implement this trait own a private key and can sign arbitrary +/// information (in the form of slices of bytes). +/// +/// 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 { + /// The signature algorithm used. + /// + /// See [RFC 8624, section 3.1] for IETF implementation recommendations. + /// + /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 + fn algorithm(&self) -> SecurityAlgorithm; + + /// The public key. + /// + /// This can be used to verify produced signatures. It must use the same + /// algorithm as returned by [`algorithm()`]. + /// + /// [`algorithm()`]: Self::algorithm() + fn dnskey(&self) -> Dnskey>; + + /// Sign the given bytes. + /// + /// # Errors + /// + /// See [`SignError`] for a discussion of possible failure cases. To the + /// greatest extent possible, the implementation should check for failure + /// cases beforehand and prevent them (e.g. when the keypair is created). + fn sign_raw(&self, data: &[u8]) -> Result; +} + +//----------- Signature ------------------------------------------------------ + +/// A cryptographic signature. +/// +/// The format of the signature varies depending on the underlying algorithm: +/// +/// - RSA: the signature is a single integer `s`, which is less than the key's +/// public modulus `n`. `s` is encoded as bytes and ordered from most +/// significant to least significant digits. It must be at least 64 bytes +/// long and at most 512 bytes long. Leading zero bytes can be inserted for +/// padding. +/// +/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). +/// +/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for +/// P-384). It is the concatenation of two fixed-length integers (`r` and +/// `s`, each of equal size). +/// +/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 +/// v2.0](https://www.secg.org/sec1-v2.pdf). +/// +/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes +/// for ED448). It is the concatenation of two curve points (`R` and `S`) +/// that are encoded into bytes. +/// +/// Signatures are too big to pass by value, so they are placed on the heap. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Signature { + /// Signature using RSA and SHA-1. + RsaSha1(Box<[u8]>), + + /// Signature using RSA and SHA-1. This also signals support for NSEC3. + RsaSha1Nsec3Sha1(Box<[u8]>), + + /// Signature using RSA and SHA-256. + RsaSha256(Box<[u8]>), + + /// Signature using RSA and SHA-512. + RsaSha512(Box<[u8]>), + + /// Signature using ECDSA and SHA-256. + EcdsaP256Sha256(Box<[u8; 64]>), + + /// Signature using ECDSA and SHA-384. + EcdsaP384Sha384(Box<[u8; 96]>), + + /// Signature using Ed25519. + Ed25519(Box<[u8; 64]>), + + /// Signature using Ed448. + Ed448(Box<[u8; 114]>), +} + +impl Signature { + /// The algorithm used to make the signature. + pub fn algorithm(&self) -> SecurityAlgorithm { + match self { + Self::RsaSha1(_) => SecurityAlgorithm::RSASHA1, + Self::RsaSha1Nsec3Sha1(_) => { + SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + } + Self::RsaSha256(_) => SecurityAlgorithm::RSASHA256, + Self::RsaSha512(_) => SecurityAlgorithm::RSASHA512, + Self::EcdsaP256Sha256(_) => SecurityAlgorithm::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecurityAlgorithm::ECDSAP384SHA384, + Self::Ed25519(_) => SecurityAlgorithm::ED25519, + Self::Ed448(_) => SecurityAlgorithm::ED448, + } + } +} + +impl AsRef<[u8]> for Signature { + fn as_ref(&self) -> &[u8] { + match self { + Self::RsaSha1(s) + | Self::RsaSha1Nsec3Sha1(s) + | Self::RsaSha256(s) + | Self::RsaSha512(s) => s, + Self::EcdsaP256Sha256(s) => &**s, + Self::EcdsaP384Sha384(s) => &**s, + Self::Ed25519(s) => &**s, + Self::Ed448(s) => &**s, + } + } +} + +impl From for Box<[u8]> { + fn from(value: Signature) -> Self { + match value { + Signature::RsaSha1(s) + | Signature::RsaSha1Nsec3Sha1(s) + | Signature::RsaSha256(s) + | Signature::RsaSha512(s) => s, + Signature::EcdsaP256Sha256(s) => s as _, + Signature::EcdsaP384Sha384(s) => s as _, + Signature::Ed25519(s) => s as _, + Signature::Ed448(s) => s as _, + } + } +} + +//----------- KeyPair -------------------------------------------------------- + +/// A key pair based on a built-in backend. +/// +/// 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. +#[derive(Debug)] +// Note: ring does not implement Clone for KeyPair. +#[allow(clippy::large_enum_variant)] // TODO +pub enum KeyPair { + /// A key backed by Ring. + #[cfg(feature = "ring")] + Ring(ring::sign::KeyPair), + + /// A key backed by OpenSSL. + #[cfg(feature = "openssl")] + OpenSSL(openssl::sign::KeyPair), +} + +//--- Conversion to and from bytes + +impl KeyPair { + /// Import a key pair from bytes. + pub fn from_bytes( + secret: &SecretKeyBytes, + public: &Dnskey, + ) -> Result + where + Octs: AsRef<[u8]>, + { + // Prefer Ring if it is available. + #[cfg(feature = "ring")] + { + let fallback_to_openssl = match public.algorithm() { + SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 => { + ring::PublicKey::from_dnskey(public) + .map_err(|_| FromBytesError::InvalidKey)? + .key_size() + < 2048 + } + _ => false, + }; + + if !fallback_to_openssl { + let key = ring::sign::KeyPair::from_bytes(secret, public)?; + return Ok(Self::Ring(key)); + } + } + + // Fall back to OpenSSL. + #[cfg(feature = "openssl")] + return Ok(Self::OpenSSL(openssl::sign::KeyPair::from_bytes( + secret, public, + )?)); + + // Otherwise fail. + #[allow(unreachable_code)] + Err(FromBytesError::UnsupportedAlgorithm) + } +} + +//--- SignRaw + +impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.algorithm(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.algorithm(), + } + } + + fn dnskey(&self) -> Dnskey> { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.dnskey(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.dnskey(), + } + } + + fn sign_raw(&self, data: &[u8]) -> Result { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.sign_raw(data), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.sign_raw(data), + } + } +} + +//----------- generate() ----------------------------------------------------- + +/// Generate a new secret key for the given algorithm. +pub fn generate( + params: GenerateParams, + flags: u16, +) -> Result<(SecretKeyBytes, Dnskey>), GenerateError> { + // Use Ring if it is available. + #[cfg(feature = "ring")] + if matches!( + ¶ms, + GenerateParams::EcdsaP256Sha256 + | GenerateParams::EcdsaP384Sha384 + | GenerateParams::Ed25519 + ) { + let rng = ::ring::rand::SystemRandom::new(); + return Ok(ring::sign::generate(params, flags, &rng)?); + } + + // Fall back to OpenSSL. + #[cfg(feature = "openssl")] + { + let key = openssl::sign::generate(params, flags)?; + return Ok((key.to_bytes(), key.dnskey())); + } + + // Otherwise fail. + #[allow(unreachable_code)] + Err(GenerateError::UnsupportedAlgorithm) +} + +//----------- SecretKeyBytes ------------------------------------------------- + +/// A secret key expressed as raw bytes. +/// +/// This is a low-level generic representation of a secret key from any one of +/// the commonly supported signature algorithms. It is useful for abstracting +/// over most cryptographic implementations, and it provides functionality for +/// importing and exporting keys from and to the disk. +/// +/// # Serialization +/// +/// This type can be used to interact with private keys stored in the format +/// popularized by BIND. The format is rather under-specified, but examples +/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. +/// +/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 +/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 +/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 +/// +/// In this format, a private key is a line-oriented text file. Each line is +/// either blank (having only whitespace) or a key-value entry. Entries have +/// three components: a key, an ASCII colon, and a value. Keys contain ASCII +/// text (except for colons) and values contain any data up to the end of the +/// line. Whitespace at either end of the key and the value will be ignored. +/// +/// Every file begins with two entries: +/// +/// - `Private-key-format` specifies the format of the file. The RFC examples +/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND +/// have defined a new version 1.3 (serialized `v1.3`). +/// +/// This value should be treated akin to Semantic Versioning principles. If +/// the major version (the first number) is unknown to a parser, it should +/// fail, since it does not know the layout of the following fields. If the +/// minor version is greater than what a parser is expecting, it should +/// ignore any following fields it did not expect. +/// +/// - `Algorithm` specifies the signing algorithm used by the private key. +/// This can affect the format of later fields. The value consists of two +/// whitespace-separated words: the first is the ASCII decimal number of the +/// algorithm (see [`SecurityAlgorithm`]); the second is the name of the algorithm in +/// ASCII parentheses (with no whitespace inside). Valid combinations are: +/// +/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. +/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. +/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. +/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. +/// - `15 (ED25519)`: Ed25519. +/// - `16 (ED448)`: Ed448. +/// +/// The value of every following entry is a Base64-encoded string of variable +/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for +/// padding). It is unclear whether padding is required or optional. +/// +/// In the case of RSA, the following fields are defined (their conventional +/// symbolic names are also provided): +/// +/// - `Modulus` (n) +/// - `PublicExponent` (e) +/// - `PrivateExponent` (d) +/// - `Prime1` (p) +/// - `Prime2` (q) +/// - `Exponent1` (d_p) +/// - `Exponent2` (d_q) +/// - `Coefficient` (q_inv) +/// +/// For all other algorithms, there is a single `PrivateKey` field, whose +/// contents should be interpreted as: +/// +/// - For ECDSA, the private scalar of the key, as a fixed-width byte string +/// interpreted as a big-endian integer. +/// +/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. +#[derive(Debug)] +pub enum SecretKeyBytes { + /// An RSA/SHA-256 keypair. + RsaSha256(RsaSecretKeyBytes), + + /// An ECDSA P-256/SHA-256 keypair. + /// + /// The private key is a single 32-byte big-endian integer. + EcdsaP256Sha256(SecretBox<[u8; 32]>), + + /// An ECDSA P-384/SHA-384 keypair. + /// + /// The private key is a single 48-byte big-endian integer. + EcdsaP384Sha384(SecretBox<[u8; 48]>), + + /// An Ed25519 keypair. + /// + /// The private key is a single 32-byte string. + Ed25519(SecretBox<[u8; 32]>), + + /// An Ed448 keypair. + /// + /// The private key is a single 57-byte string. + Ed448(SecretBox<[u8; 57]>), +} + +//--- Inspection + +impl SecretKeyBytes { + /// The algorithm used by this key. + pub fn algorithm(&self) -> SecurityAlgorithm { + match self { + Self::RsaSha256(_) => SecurityAlgorithm::RSASHA256, + Self::EcdsaP256Sha256(_) => SecurityAlgorithm::ECDSAP256SHA256, + Self::EcdsaP384Sha384(_) => SecurityAlgorithm::ECDSAP384SHA384, + Self::Ed25519(_) => SecurityAlgorithm::ED25519, + Self::Ed448(_) => SecurityAlgorithm::ED448, + } + } +} + +//--- Converting to and from the BIND format + +impl SecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. + /// + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. See the type-level documentation for a description + /// of this format. + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { + writeln!(w, "Private-key-format: v1.2")?; + match self { + Self::RsaSha256(k) => { + writeln!(w, "Algorithm: 8 (RSASHA256)")?; + k.format_as_bind(w) + } + + Self::EcdsaP256Sha256(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; + 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)) + } + + Self::Ed25519(s) => { + let s = s.expose_secret(); + writeln!(w, "Algorithm: 15 (ED25519)")?; + 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)) + } + } + } + + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + /// Display type to return from this function. + struct Display<'a>(&'a SecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + + /// Parse a secret key from the conventional format used by BIND. + /// + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. See the type-level documentation + /// for a description of this format. + pub fn parse_from_bind(data: &str) -> Result { + /// Parse private keys for most algorithms (except RSA). + fn parse_pkey( + mut data: &str, + ) -> Result, BindFormatError> { + // Look for the 'PrivateKey' field. + while let Some((key, val, rest)) = parse_bind_entry(data)? { + data = rest; + + if key != "PrivateKey" { + continue; + } + + // 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)?; + + return Ok(val.into()); + } + + // The 'PrivateKey' field was not found. + Err(BindFormatError::Misformatted) + } + + // The first line should specify the key format. + let (_, _, data) = parse_bind_entry(data)? + .filter(|&(k, v, _)| { + k == "Private-key-format" + && v.strip_prefix("v1.") + .and_then(|minor| minor.parse::().ok()) + .is_some_and(|minor| minor >= 2) + }) + .ok_or(BindFormatError::UnsupportedFormat)?; + + // The second line should specify the algorithm. + let (_, val, data) = parse_bind_entry(data)? + .filter(|&(k, _, _)| k == "Algorithm") + .ok_or(BindFormatError::Misformatted)?; + + // Parse the algorithm. + let mut words = val.split_whitespace(); + let code = words + .next() + .and_then(|code| code.parse::().ok()) + .ok_or(BindFormatError::Misformatted)?; + let name = words.next().ok_or(BindFormatError::Misformatted)?; + if words.next().is_some() { + return Err(BindFormatError::Misformatted); + } + + match (code, name) { + (8, "(RSASHA256)") => { + RsaSecretKeyBytes::parse_from_bind(data).map(Self::RsaSha256) + } + (13, "(ECDSAP256SHA256)") => { + parse_pkey(data).map(Self::EcdsaP256Sha256) + } + (14, "(ECDSAP384SHA384)") => { + parse_pkey(data).map(Self::EcdsaP384Sha384) + } + (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), + (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), + _ => Err(BindFormatError::UnsupportedAlgorithm), + } + } +} + +//----------- Helpers for parsing the BIND format ---------------------------- + +/// Extract the next key-value pair in a BIND-format private key file. +pub(crate) fn parse_bind_entry( + data: &str, +) -> Result, BindFormatError> { + // TODO: Use 'trim_ascii_start()' etc. once they pass the MSRV. + + // Trim any pending newlines. + let data = data.trim_start(); + + // Stop if there's no more data. + if data.is_empty() { + return Ok(None); + } + + // Get the first line (NOTE: CR LF is handled later). + let (line, rest) = data.split_once('\n').unwrap_or((data, "")); + + // Split the line by a colon. + let (key, val) = + line.split_once(':').ok_or(BindFormatError::Misformatted)?; + + // Trim the key and value (incl. for CR LFs). + Ok(Some((key.trim(), val.trim(), rest))) +} + +//----------- RsaSecretKeyBytes ---------------------------------------------- + +/// An RSA secret key expressed as raw bytes. +/// +/// All fields here are arbitrary-precision integers in big-endian format. +/// The public values, `n` and `e`, must not have leading zeros; the remaining +/// values may be padded with leading zeros. +#[derive(Debug)] +pub struct RsaSecretKeyBytes { + /// The public modulus. + pub n: Box<[u8]>, + + /// The public exponent. + pub e: Box<[u8]>, + + /// The private exponent. + pub d: SecretBox<[u8]>, + + /// The first prime factor of `d`. + pub p: SecretBox<[u8]>, + + /// The second prime factor of `d`. + pub q: SecretBox<[u8]>, + + /// The exponent corresponding to the first prime factor of `d`. + pub d_p: SecretBox<[u8]>, + + /// The exponent corresponding to the second prime factor of `d`. + pub d_q: SecretBox<[u8]>, + + /// The inverse of the second prime factor modulo the first. + pub q_i: SecretBox<[u8]>, +} + +//--- Conversion to and from the BIND format + +impl RsaSecretKeyBytes { + /// Serialize this secret key in the conventional format used by BIND. + /// + /// The key is formatted in the private key v1.2 format and written to the + /// given formatter. Note that the header and algorithm lines are not + /// written. See the type-level documentation of [`SecretKeyBytes`] for a + /// description of this format. + pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { + w.write_str("Modulus: ")?; + writeln!(w, "{}", base64::encode_display(&self.n))?; + w.write_str("PublicExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.e))?; + w.write_str("PrivateExponent: ")?; + writeln!(w, "{}", base64::encode_display(&self.d.expose_secret()))?; + w.write_str("Prime1: ")?; + writeln!(w, "{}", base64::encode_display(&self.p.expose_secret()))?; + w.write_str("Prime2: ")?; + writeln!(w, "{}", base64::encode_display(&self.q.expose_secret()))?; + w.write_str("Exponent1: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_p.expose_secret()))?; + w.write_str("Exponent2: ")?; + writeln!(w, "{}", base64::encode_display(&self.d_q.expose_secret()))?; + w.write_str("Coefficient: ")?; + writeln!(w, "{}", base64::encode_display(&self.q_i.expose_secret()))?; + Ok(()) + } + + /// Display this secret key in the conventional format used by BIND. + /// + /// This is a simple wrapper around [`Self::format_as_bind()`]. + pub fn display_as_bind(&self) -> impl fmt::Display + '_ { + /// Display type to return from this function. + struct Display<'a>(&'a RsaSecretKeyBytes); + impl fmt::Display for Display<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.format_as_bind(f) + } + } + Display(self) + } + + /// Parse a secret key from the conventional format used by BIND. + /// + /// This parser supports the private key v1.2 format, but it should be + /// compatible with any future v1.x key. Note that the header and + /// algorithm lines are ignored. See the type-level documentation of + /// [`SecretKeyBytes`] for a description of this format. + pub fn parse_from_bind(mut data: &str) -> Result { + let mut n = None; + let mut e = None; + let mut d = None; + let mut p = None; + let mut q = None; + let mut d_p = None; + let mut d_q = None; + let mut q_i = None; + + while let Some((key, val, rest)) = parse_bind_entry(data)? { + let field = match key { + "Modulus" => &mut n, + "PublicExponent" => &mut e, + "PrivateExponent" => &mut d, + "Prime1" => &mut p, + "Prime2" => &mut q, + "Exponent1" => &mut d_p, + "Exponent2" => &mut d_q, + "Coefficient" => &mut q_i, + _ => { + data = rest; + continue; + } + }; + + if field.is_some() { + // This field has already been filled. + return Err(BindFormatError::Misformatted); + } + + let buffer: Vec = base64::decode(val) + .map_err(|_| BindFormatError::Misformatted)?; + + *field = Some(buffer.into_boxed_slice()); + data = rest; + } + + for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { + if field.is_none() { + // A field was missing. + return Err(BindFormatError::Misformatted); + } + } + + Ok(Self { + n: n.unwrap(), + e: e.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(), + }) + } +} + +//============ Error Types =================================================== + +//----------- FromBytesError ----------------------------------------------- + +/// An error in importing a key pair from bytes. +#[derive(Clone, Debug)] +pub enum FromBytesError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// The key's parameters were invalid. + InvalidKey, + + /// The implementation does not allow such weak keys. + WeakKey, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversions + +#[cfg(feature = "ring")] +impl From for FromBytesError { + fn from(value: ring::FromBytesError) -> Self { + match value { + ring::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::FromBytesError::InvalidKey => Self::InvalidKey, + ring::FromBytesError::WeakKey => Self::WeakKey, + } + } +} + +#[cfg(feature = "openssl")] +impl From for FromBytesError { + fn from(value: openssl::FromBytesError) -> Self { + match value { + openssl::FromBytesError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::FromBytesError::InvalidKey => Self::InvalidKey, + openssl::FromBytesError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for FromBytesError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::InvalidKey => "malformed or insecure private key", + Self::WeakKey => "key too weak to be supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for FromBytesError {} + +//----------- GenerateError -------------------------------------------------- + +/// An error in generating a key pair. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Conversion + +#[cfg(feature = "ring")] +impl From for GenerateError { + fn from(value: ring::GenerateError) -> Self { + match value { + ring::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + ring::GenerateError::Implementation => Self::Implementation, + } + } +} + +#[cfg(feature = "openssl")] +impl From for GenerateError { + fn from(value: openssl::GenerateError) -> Self { + match value { + openssl::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + openssl::GenerateError::Implementation => Self::Implementation, + } + } +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +//----------- SignError ------------------------------------------------------ + +/// A signature failure. +/// +/// In case such an error occurs, callers should stop using the key pair they +/// attempted to sign with. If such an error occurs with every key pair they +/// have available, or if such an error occurs with a freshly-generated key +/// pair, they should use a different cryptographic implementation. If that +/// is not possible, they must forego signing entirely. +/// +/// # Failure Cases +/// +/// Signing should be an infallible process. There are three considerable +/// failure cases for it: +/// +/// - The secret key was invalid (e.g. its parameters were inconsistent). +/// +/// Such a failure would mean that all future signing (with this key) will +/// also fail. In any case, the implementations provided by this crate try +/// to verify the key (e.g. by checking the consistency of the private and +/// public components) before any signing occurs, largely ruling this class +/// of errors out. +/// +/// - Not enough randomness could be obtained. This applies to signature +/// algorithms which use randomization (e.g. RSA and ECDSA). +/// +/// On the vast majority of platforms, randomness can always be obtained. +/// The [`getrandom` crate documentation][getrandom] notes: +/// +/// > If an error does occur, then it is likely that it will occur on every +/// > call to getrandom, hence after the first successful call one can be +/// > reasonably confident that no errors will occur. +/// +/// [getrandom]: https://docs.rs/getrandom +/// +/// Thus, in case such a failure occurs, all future signing will probably +/// also fail. +/// +/// - Not enough memory could be allocated. +/// +/// Signature algorithms have a small memory overhead, so an out-of-memory +/// condition means that the program is nearly out of allocatable space. +/// +/// Callers who do not expect allocations to fail (i.e. who are using the +/// standard memory allocation routines, not their `try_` variants) will +/// likely panic shortly after such an error. +/// +/// Callers who are aware of their memory usage will likely restrict it far +/// before they get to this point. Systems running at near-maximum load +/// tend to quickly become unresponsive and staggeringly slow. If memory +/// usage is an important consideration, programs will likely cap it before +/// the system reaches e.g. 90% memory use. +/// +/// As such, memory allocation failure should never really occur. It is far +/// more likely that one of the other errors has occurred. +/// +/// It may be reasonable to panic in any such situation, since each kind of +/// error is essentially unrecoverable. However, applications where signing +/// is an optional step, or where crashing is prohibited, may wish to recover +/// from such an error differently (e.g. by foregoing signatures or informing +/// an operator). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct SignError; + +impl fmt::Display for SignError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("could not create a cryptographic signature") + } +} + +impl std::error::Error for SignError {} + +//----------- BindFormatError ------------------------------------------------ + +/// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum BindFormatError { + /// The key file uses an unsupported version of the format. + UnsupportedFormat, + + /// The key file did not follow the DNS format correctly. + Misformatted, + + /// The key file used an unsupported algorithm. + UnsupportedAlgorithm, +} + +//--- Display + +impl fmt::Display for BindFormatError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedFormat => "unsupported format", + Self::Misformatted => "misformatted key file", + Self::UnsupportedAlgorithm => "unsupported algorithm", + }) + } +} + +//--- Error + +impl std::error::Error for BindFormatError {} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use std::string::ToString; + use std::vec::Vec; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::sign::SecretKeyBytes; + + const KEYS: &[(SecurityAlgorithm, u16)] = &[ + (SecurityAlgorithm::RSASHA256, 60616), + (SecurityAlgorithm::ECDSAP256SHA256, 42253), + (SecurityAlgorithm::ECDSAP384SHA384, 33566), + (SecurityAlgorithm::ED25519, 56037), + (SecurityAlgorithm::ED448, 7379), + ]; + + #[test] + fn secret_from_dns() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + assert_eq!(key.algorithm(), algorithm); + } + } + + #[test] + fn secret_roundtrip() { + for &(algorithm, key_tag) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + let path = format!("test-data/dnssec-keys/K{}.private", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + let same = key.display_as_bind().to_string(); + let data = data.lines().collect::>(); + let same = same.lines().collect::>(); + assert_eq!(data, same); + } + } +} diff --git a/src/dnssec/common.rs b/src/dnssec/common.rs new file mode 100644 index 000000000..3e4661d37 --- /dev/null +++ b/src/dnssec/common.rs @@ -0,0 +1,362 @@ +//! DNSSEC code that is used both by DNSSEC signing and DNSSEC validation. + +#![cfg(any(feature = "ring", feature = "openssl"))] +#![cfg_attr(docsrs, doc(cfg(any(feature = "ring", feature = "openssl"))))] +#![warn(missing_docs)] +#![warn(clippy::missing_docs_in_private_items)] + +use crate::base::iana::{Class, Nsec3HashAlgorithm}; +use crate::base::scan::{IterScanner, Scanner}; +use crate::base::wire::Composer; +use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt}; +use crate::base::{Name, Record, Rtype, ToName, Ttl}; +use crate::crypto::common::{DigestBuilder, DigestType}; +use crate::dep::octseq::{ + EmptyBuilder, FromBuilder, OctetsBuilder, Truncate, +}; +use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; +use crate::rdata::{Dnskey, Nsec3param}; + +use std::error; +use std::fmt; + +//------------ Nsec3HashError ------------------------------------------------- + +/// An error when creating an NSEC3 hash. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum Nsec3HashError { + /// The requested algorithm for NSEC3 hashing is not supported. + UnsupportedAlgorithm, + + /// Data could not be appended to a buffer. + /// + /// This could indicate an out of memory condition. + AppendError, + + /// The hashing process produced an invalid owner hash. + /// + /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) + OwnerHashError, + + /// The hashing process produced a hash that already exists. + CollisionDetected, + + /// The hash provider did not provide a hash for the given owner name. + MissingHash, +} + +//--- Display + +impl std::fmt::Display for Nsec3HashError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Nsec3HashError::UnsupportedAlgorithm => { + f.write_str("Unsupported algorithm") + } + Nsec3HashError::AppendError => { + f.write_str("Append error: out of memory?") + } + Nsec3HashError::OwnerHashError => { + f.write_str("Hashing produced an invalid owner hash") + } + Nsec3HashError::CollisionDetected => { + f.write_str("Hash collision detected") + } + Nsec3HashError::MissingHash => { + f.write_str("Missing hash for owner name") + } + } + } +} + +/// Compute an [RFC 5155] NSEC3 hash using default settings. +/// +/// See: [Nsec3param::default]. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_default_hash( + owner: N, +) -> Result, Nsec3HashError> +where + N: ToName, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + let params = Nsec3param::::default(); + nsec3_hash( + owner, + params.hash_algorithm(), + params.iterations(), + params.salt(), + ) +} + +/// Compute an [RFC 5155] NSEC3 hash. +/// +/// Computes an NSEC3 hash according to [RFC 5155] section 5: +/// +/// > IH(salt, x, 0) = H(x || salt) +/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 +/// +/// Then the calculated hash of an owner name is: +/// +/// > IH(salt, owner name, iterations), +/// +/// Note that the `iterations` parameter is the number of _additional_ +/// iterations as defined in [RFC 5155] section 3.1.3. +/// +/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 +pub fn nsec3_hash( + owner: N, + algorithm: Nsec3HashAlgorithm, + iterations: u16, + salt: &Nsec3Salt, +) -> Result, Nsec3HashError> +where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, +{ + if algorithm != Nsec3HashAlgorithm::SHA1 { + return Err(Nsec3HashError::UnsupportedAlgorithm); + } + + /// Compute the hash octets. + fn mk_hash( + owner: N, + iterations: u16, + salt: &Nsec3Salt, + ) -> Result + where + N: ToName, + SaltOcts: AsRef<[u8]>, + HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, + for<'a> HashOcts: From<&'a [u8]>, + { + let mut canonical_owner = HashOcts::empty(); + owner.compose_canonical(&mut canonical_owner)?; + + let mut ctx = DigestBuilder::new(DigestType::Sha1); + ctx.update(canonical_owner.as_ref()); + ctx.update(salt.as_slice()); + let mut h = ctx.finish(); + + for _ in 0..iterations { + let mut ctx = DigestBuilder::new(DigestType::Sha1); + ctx.update(h.as_ref()); + ctx.update(salt.as_slice()); + h = ctx.finish(); + } + + Ok(h.as_ref().into()) + } + + let hash = mk_hash(owner, iterations, salt) + .map_err(|_| Nsec3HashError::AppendError)?; + + let owner_hash = OwnerHash::from_octets(hash) + .map_err(|_| Nsec3HashError::OwnerHashError)?; + + Ok(owner_hash) +} + +//------------ parse_from_bind ----------------------------------------------- + +/// Parse a DNSSEC key from the conventional format used by BIND. +/// +/// See the type-level documentation for a description of this format. +pub fn parse_from_bind( + data: &str, +) -> Result, Dnskey>, ParseDnskeyTextError> +where + Octs: FromBuilder, + Octs::Builder: EmptyBuilder + Composer, +{ + /// Find the next non-blank line in the file. + fn next_line(mut data: &str) -> Option<(&str, &str)> { + let mut line; + while !data.is_empty() { + (line, data) = + data.trim_start().split_once('\n').unwrap_or((data, "")); + if !line.is_empty() && !line.starts_with(';') { + // We found a line that does not start with a comment. + line = line + .split_once(';') + .map_or(line, |(line, _)| line) + .trim_end(); + return Some((line, data)); + } + } + + None + } + + // Ensure there is a single DNSKEY record line in the input. + let (line, rest) = next_line(data).ok_or(ParseDnskeyTextError)?; + if next_line(rest).is_some() { + return Err(ParseDnskeyTextError); + } + + // Parse the entire record. + let mut scanner = IterScanner::new(line.split_ascii_whitespace()); + + let name = scanner.scan_name().map_err(|_| ParseDnskeyTextError)?; + + let _ = Class::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?; + + if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { + return Err(ParseDnskeyTextError); + } + + let data = + Dnskey::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?; + + Ok(Record::new(name, Class::IN, Ttl::ZERO, data)) +} + +//------------ format_as_bind ------------------------------------------------ +// # Serialization +// +// Keys can be parsed from or written in the conventional format used by the +// BIND name server. This is a simplified version of the zonefile format. +// +// In this format, a public key is a line-oriented text file. Each line is +// either blank (having only whitespace) or a single DNSKEY record in the +// presentation format. In either case, the line may end with a comment (an +// ASCII semicolon followed by arbitrary content until the end of the line). +// The file must contain a single DNSKEY record line. +// +// The DNSKEY record line contains the following fields, separated by ASCII +// whitespace: +// +// - The owner name. This is an absolute name ending with a dot. +// - Optionally, the class of the record (usually `IN`). +// - The record type (which must be `DNSKEY`). +// - The DNSKEY record data, which has the following sub-fields: +// - The key flags, which describe the key's uses. +// - The protocol used (expected to be `3`). +// - The key algorithm (see [`SecurityAlgorithm`]). +// - The public key encoded as a Base64 string. + +/// Serialize this key in the conventional format used by BIND. +/// +/// See the type-level documentation for a description of this format. +fn format_as_bind( + record: &Record>, + mut w: impl fmt::Write, +) -> fmt::Result +where + N: ToName, + O: AsRef<[u8]>, +{ + writeln!( + w, + "{} IN DNSKEY {}", + record.owner().fmt_with_dot(), + record.data().display_zonefile(DisplayKind::Simple), + ) +} + +//------------ display_as_bind ----------------------------------------------- +/// Display this key in the conventional format used by BIND. +/// +/// See the type-level documentation for a description of this format. +pub fn display_as_bind( + record: &Record>, +) -> impl fmt::Display + '_ +where + N: ToName, + O: AsRef<[u8]>, +{ + /// Display type to return. + struct Display<'a, N, O>(&'a Record>); + impl fmt::Display for Display<'_, N, O> + where + N: ToName, + O: AsRef<[u8]>, + { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + format_as_bind(self.0, f) + } + } + Display(record) +} + +//----------- ParseDnskeyTextError ------------------------------------------- + +#[derive(Clone, Debug)] +/// Error from parsing a DNSKEY record in presentation format. +pub struct ParseDnskeyTextError; + +//--- Display, Error + +impl fmt::Display for ParseDnskeyTextError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("misformatted DNSKEY record") + } +} + +impl error::Error for ParseDnskeyTextError {} + +//============ Test ========================================================== + +#[cfg(test)] +#[cfg(feature = "std")] +mod test { + use std::string::ToString; + use std::vec::Vec; + + use crate::base::iana::SecurityAlgorithm; + use crate::dnssec::common::{display_as_bind, parse_from_bind}; + + const KEYS: &[(SecurityAlgorithm, u16, usize)] = &[ + (SecurityAlgorithm::RSASHA1, 439, 2048), + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, 22204, 2048), + (SecurityAlgorithm::RSASHA256, 60616, 2048), + (SecurityAlgorithm::ECDSAP256SHA256, 42253, 256), + (SecurityAlgorithm::ECDSAP384SHA384, 33566, 384), + (SecurityAlgorithm::ED25519, 56037, 256), + (SecurityAlgorithm::ED448, 7379, 456), + ]; + + #[test] + fn test_parse_from_bind() { + for &(algorithm, key_tag, _) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let _ = parse_from_bind::>(&data).unwrap(); + } + } + + #[test] + fn key_tag() { + for &(algorithm, key_tag, _) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = parse_from_bind::>(&data).unwrap(); + assert_eq!(key.data().key_tag(), key_tag); + } + } + + #[test] + fn bind_format_roundtrip() { + for &(algorithm, key_tag, _) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = parse_from_bind::>(&data).unwrap(); + let bind_fmt_key = display_as_bind(&key).to_string(); + let same = parse_from_bind::>(&bind_fmt_key).unwrap(); + assert_eq!(key, same); + } + } +} diff --git a/src/dnssec/mod.rs b/src/dnssec/mod.rs new file mode 100644 index 000000000..6d1520919 --- /dev/null +++ b/src/dnssec/mod.rs @@ -0,0 +1,46 @@ +//! DNSSEC signing and validation +//! +#![cfg_attr(any(feature = "ring", feature = "openssl"), doc = "* [common]:")] +#![cfg_attr( + not(any(feature = "ring", feature = "openssl")), + doc = "* common:" +)] +//! Types and functions that are common between signing and validation. +#![cfg_attr( + all( + feature = "unstable-sign", + any(feature = "ring", feature = "openssl") + ), + doc = "* [sign]:" +)] +#![cfg_attr( + not(all( + feature = "unstable-sign", + any(feature = "ring", feature = "openssl") + )), + doc = "* sign:" +)] +//! Experimental support for DNSSEC signing. +#![cfg_attr( + all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ), + doc = "* [validator]:" +)] +#![cfg_attr( + not(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + )), + doc = "* validator:" +)] +//! Experimental support for DNSSEC validation. +//! +//! Note that in addition to the feature flags that enable the various modules +//! (`unstable-sig`, `unstable-validator`), at least one cryptographic +//! backend needs to be selected (currently there are `ring` and `openssl`). + +pub mod common; +pub mod sign; +pub mod validator; diff --git a/src/dnssec/sign/config.rs b/src/dnssec/sign/config.rs new file mode 100644 index 000000000..ea1b92ed1 --- /dev/null +++ b/src/dnssec/sign/config.rs @@ -0,0 +1,43 @@ +//! Types for tuning configurable aspects of DNSSEC signing. +use core::marker::PhantomData; + +use super::denial::config::DenialConfig; +use super::records::Sorter; +use crate::rdata::dnssec::Timestamp; + +//------------ SigningConfig ------------------------------------------------- + +/// Signing configuration for a DNSSEC signed zone. +pub struct SigningConfig +where + Octs: AsRef<[u8]> + From<&'static [u8]>, + Sort: Sorter, +{ + /// Authenticated denial of existence mechanism configuration. + pub denial: DenialConfig, + + pub inception: Timestamp, + + pub expiration: Timestamp, + + _phantom: PhantomData, +} + +impl SigningConfig +where + Octs: AsRef<[u8]> + From<&'static [u8]>, + Sort: Sorter, +{ + pub fn new( + denial: DenialConfig, + inception: Timestamp, + expiration: Timestamp, + ) -> Self { + Self { + denial, + inception, + expiration, + _phantom: PhantomData, + } + } +} diff --git a/src/dnssec/sign/denial/config.rs b/src/dnssec/sign/denial/config.rs new file mode 100644 index 000000000..e5b5dc386 --- /dev/null +++ b/src/dnssec/sign/denial/config.rs @@ -0,0 +1,62 @@ +use core::convert::From; + +use super::nsec::GenerateNsecConfig; +use super::nsec3::GenerateNsec3Config; +use crate::dnssec::sign::records::DefaultSorter; +use octseq::{EmptyBuilder, FromBuilder}; + +//------------ DenialConfig -------------------------------------------------- + +/// Authenticated denial of existence configuration for a DNSSEC signed zone. +/// +/// A DNSSEC signed zone must have either `NSEC` or `NSEC3` records to enable +/// the server to authenticate responses for names or record types that are +/// not present in the zone. +/// +/// This type can be used to choose which denial mechanism should be used when +/// DNSSEC signing a zone. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum DenialConfig +where + O: AsRef<[u8]> + From<&'static [u8]>, +{ + /// The zone already has the necessary NSEC(3) records. + AlreadyPresent, + + /// The zone already has NSEC records. + Nsec(GenerateNsecConfig), + + /// The zone already has NSEC3 records. + /// + /// Note: While zones can have multiple NSEC3 chains, only the configuraton + /// for a single chain can be expressed using this type. + /// + /// https://datatracker.ietf.org/doc/html/rfc5155#section-7.3 + /// 7.3. Secondary Servers + /// ... + /// "If there are multiple NSEC3PARAM RRs present, there are multiple + /// valid NSEC3 chains present. The server must choose one of them, + /// but may use any criteria to do so." + /// + /// https://datatracker.ietf.org/doc/html/rfc5155#section-12.1.3 + /// 12.1.3. Transitioning to a New Hash Algorithm + /// "Although the NSEC3 and NSEC3PARAM RR formats include a hash + /// algorithm parameter, this document does not define a particular + /// mechanism for safely transitioning from one NSEC3 hash algorithm to + /// another. When specifying a new hash algorithm for use with NSEC3, + /// a transition mechanism MUST also be defined. It is possible that + /// the only practical and palatable transition mechanisms may require + /// an intermediate transition to an insecure state, or to a state that + /// uses NSEC records instead of NSEC3." + Nsec3(GenerateNsec3Config), +} + +impl Default for DenialConfig +where + O: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, + ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, +{ + fn default() -> Self { + Self::Nsec(GenerateNsecConfig::default()) + } +} diff --git a/src/sign/denial/mod.rs b/src/dnssec/sign/denial/mod.rs similarity index 100% rename from src/sign/denial/mod.rs rename to src/dnssec/sign/denial/mod.rs diff --git a/src/sign/denial/nsec.rs b/src/dnssec/sign/denial/nsec.rs similarity index 79% rename from src/sign/denial/nsec.rs rename to src/dnssec/sign/denial/nsec.rs index cc5064537..86f098b17 100644 --- a/src/sign/denial/nsec.rs +++ b/src/dnssec/sign/denial/nsec.rs @@ -1,5 +1,5 @@ use core::cmp::min; -use core::fmt::Debug; +use core::fmt::{Debug, Display}; use std::vec::Vec; @@ -8,10 +8,10 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use crate::base::iana::Rtype; use crate::base::name::ToName; use crate::base::record::Record; +use crate::dnssec::sign::error::SigningError; +use crate::dnssec::sign::records::RecordsIter; use crate::rdata::dnssec::RtypeBitmap; use crate::rdata::{Nsec, ZoneRecordData}; -use crate::sign::error::SigningError; -use crate::sign::records::RecordsIter; //----------- GenerateNsec3Config -------------------------------------------- @@ -67,16 +67,27 @@ impl Default for GenerateNsecConfig { // TODO: Add (mutable?) iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_nsecs( - records: RecordsIter<'_, N, ZoneRecordData>, + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, config: &GenerateNsecConfig, ) -> Result>>, SigningError> where - N: ToName + Clone + PartialEq, + N: ToName + Clone + Display + PartialEq, Octs: FromBuilder, Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, { - let mut res = Vec::new(); + // The generated collection of NSEC RRs that will be returned to the + // caller. + let mut nsecs = Vec::new(); + + // The CLASS to use for NSEC records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut zone_class = None; + + // The TTL to use for NSEC records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut nsec_ttl = None; // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; @@ -84,16 +95,14 @@ where // Because of the next name thing, we need to keep the last NSEC around. let mut prev: Option<(N, RtypeBitmap)> = None; - // We also need the apex for the last NSEC. - let first_rr = records.first(); - let apex_owner = first_rr.owner().clone(); - let zone_class = first_rr.class(); - let mut ttl = None; + // Skip any glue or other out-of-zone records that sort earlier than + // the zone apex. + records.skip_before(apex_owner); for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(&apex_owner) { + if !owner_rrs.is_in_zone(apex_owner) { break; } @@ -110,18 +119,19 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(&apex_owner) { + cut = if owner_rrs.is_zone_cut(apex_owner) { Some(name.clone()) } else { None }; if let Some((prev_name, bitmap)) = prev.take() { - // SAFETY: ttl will be set below before prev is set to Some. - res.push(Record::new( + // SAFETY: nsec_ttl and zone_class will be set below before prev + // is set to Some. + nsecs.push(Record::new( prev_name.clone(), - zone_class, - ttl.unwrap(), + zone_class.unwrap(), + nsec_ttl.unwrap(), Nsec::new(name.clone(), bitmap), )); } @@ -135,7 +145,7 @@ where bitmap.add(Rtype::RRSIG).unwrap(); if config.assume_dnskeys_will_be_added - && owner_rrs.owner() == &apex_owner + && owner_rrs.owner() == apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); @@ -180,11 +190,13 @@ where // say that the "TTL of the NSEC(3) RR that is returned MUST // be the lesser of the MINIMUM field of the SOA record and // the TTL of the SOA itself". - ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + nsec_ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + + zone_class = Some(rrset.class()); } } - if ttl.is_none() { + if nsec_ttl.is_none() { return Err(SigningError::SoaRecordCouldNotBeDetermined); } @@ -192,28 +204,31 @@ where } if let Some((prev_name, bitmap)) = prev { - res.push(Record::new( + // SAFETY: nsec_ttl and zone_class will be set above before prev + // is set to Some. + nsecs.push(Record::new( prev_name.clone(), - zone_class, - ttl.unwrap(), + zone_class.unwrap(), + nsec_ttl.unwrap(), Nsec::new(apex_owner.clone(), bitmap), )); } - Ok(res) + Ok(nsecs) } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; - use crate::base::Ttl; - use crate::sign::records::SortedRecords; - use crate::sign::test_util::*; + use crate::base::{Name, Ttl}; + use crate::dnssec::sign::records::SortedRecords; + use crate::dnssec::sign::test_util::*; use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; use super::*; + use core::str::FromStr; type StoredSortedRecords = SortedRecords; @@ -221,8 +236,9 @@ mod tests { fn soa_is_required() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([mk_a_rr("some_a.a.")]); - let res = generate_nsecs(records.owner_rrs(), &cfg); + let res = generate_nsecs(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -233,11 +249,12 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_soa_rr("a.", "d.", "e."), ]); - let res = generate_nsecs(records.owner_rrs(), &cfg); + let res = generate_nsecs(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -248,6 +265,8 @@ mod tests { fn records_outside_zone_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let a_apex = Name::from_str("a.").unwrap(); + let b_apex = Name::from_str("b.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("b.", "d.", "e."), mk_a_rr("some_a.b."), @@ -255,12 +274,9 @@ mod tests { mk_a_rr("some_a.a."), ]); - // First generate NSECs for the total record collection. As the - // collection is sorted in canonical order the a zone preceeds the b - // zone and NSECs should only be generated for the first zone in the - // collection. - let a_and_b_records = records.owner_rrs(); - let nsecs = generate_nsecs(a_and_b_records, &cfg).unwrap(); + // Generate NSEs for the a. zone. + let nsecs = + generate_nsecs(&a_apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -270,11 +286,9 @@ mod tests { ] ); - // Now skip the a zone in the collection and generate NSECs for the - // remaining records which should only generate NSECs for the b zone. - let mut b_records_only = records.owner_rrs(); - b_records_only.skip_before(&mk_name("b.")); - let nsecs = generate_nsecs(b_records_only, &cfg).unwrap(); + // Generate NSECs for the b. zone. + let nsecs = + generate_nsecs(&b_apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -285,17 +299,48 @@ mod tests { ); } + #[test] + fn glue_records_are_ignored() { + let cfg = GenerateNsecConfig::default() + .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("example.").unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Generate NSEs for the a. zone. + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec_rr( + "example.", + "in_zone.example.", + "NS SOA RRSIG NSEC" + ), + mk_nsec_rr("in_zone.example.", "example.", "A RRSIG NSEC"), + ] + ); + } + #[test] fn occluded_records_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("some_ns.a.", "some_a.other.b."), mk_a_rr("some_a.some_ns.a."), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); // Implicit negative test. assert_eq!( @@ -314,12 +359,13 @@ mod tests { fn expect_dnskeys_at_the_apex() { let cfg = GenerateNsecConfig::default(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -337,11 +383,12 @@ mod tests { // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A let zonefile = include_bytes!( - "../../../test-data/zonefiles/rfc4035-appendix-A.zone" + "../../../../test-data/zonefiles/rfc4035-appendix-A.zone" ); + let apex = Name::from_str("example.").unwrap(); let records = bytes_to_records(&zonefile[..]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!(nsecs.len(), 10); @@ -453,6 +500,7 @@ mod tests { fn existing_nsec_records_are_ignored() { let cfg = GenerateNsecConfig::default(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), @@ -460,7 +508,7 @@ mod tests { mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, diff --git a/src/sign/denial/nsec3.rs b/src/dnssec/sign/denial/nsec3.rs similarity index 77% rename from src/sign/denial/nsec3.rs rename to src/dnssec/sign/denial/nsec3.rs index e8dbcaa3c..b734490ef 100644 --- a/src/sign/denial/nsec3.rs +++ b/src/dnssec/sign/denial/nsec3.rs @@ -12,23 +12,22 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::OctetsFrom; use tracing::{debug, trace}; -use crate::base::iana::{Class, Nsec3HashAlg, Rtype}; +use crate::base::iana::{Class, Nsec3HashAlgorithm, Rtype}; use crate::base::name::{ToLabelIter, ToName}; use crate::base::{CanonicalOrd, Name, NameBuilder, Record, Ttl}; +use crate::dnssec::common::{nsec3_hash, Nsec3HashError}; +use crate::dnssec::sign::error::SigningError; +use crate::dnssec::sign::records::{DefaultSorter, RecordsIter, Sorter}; use crate::rdata::dnssec::{RtypeBitmap, RtypeBitmapBuilder}; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{Nsec3, Nsec3param, ZoneRecordData}; -use crate::sign::error::SigningError; -use crate::sign::records::{DefaultSorter, RecordsIter, Sorter}; use crate::utils::base32; -use crate::validate::{nsec3_hash, Nsec3HashError}; //----------- GenerateNsec3Config -------------------------------------------- #[derive(Clone, Debug, PartialEq, Eq)] -pub struct GenerateNsec3Config +pub struct GenerateNsec3Config where - HashProvider: Nsec3HashProvider, Octs: AsRef<[u8]> + From<&'static [u8]>, { /// Whether to assume that the final zone will one or more DNSKEY RRs at @@ -68,35 +67,17 @@ where /// Which TTL value should be used for the NSEC3PARAM RR. pub nsec3param_ttl_mode: Nsec3ParamTtlMode, - /// Which [`Nsec3HashProvider`] impl should be used to generate NSEC3 - /// hashes. - /// - /// By default the [`OnDemandNsec3HashProvider`] impl is used. - /// - /// Users may override this with their own impl. The primary use case - /// evisioned for this is to track the relationship between the original - /// owner names and the hashes generated for them in order to be able to - /// output diagnostic information about generated NSEC3 RRs for diagnostic - /// purposes. - pub hash_provider: HashProvider, - - _phantom: PhantomData<(N, Sort)>, + _phantom: PhantomData, } -impl - GenerateNsec3Config +impl GenerateNsec3Config where - HashProvider: Nsec3HashProvider, Octs: AsRef<[u8]> + From<&'static [u8]>, { - pub fn new( - params: Nsec3param, - hash_provider: HashProvider, - ) -> Self { + pub fn new(params: Nsec3param) -> Self { Self { assume_dnskeys_will_be_added: true, params, - hash_provider, nsec3param_ttl_mode: Default::default(), opt_out_exclude_owner_names_of_unsigned_delegations: true, _phantom: Default::default(), @@ -126,43 +107,31 @@ where } } -impl Default - for GenerateNsec3Config< - N, - Octs, - OnDemandNsec3HashProvider, - DefaultSorter, - > +impl Default for GenerateNsec3Config where - N: ToName + From>, Octs: AsRef<[u8]> + From<&'static [u8]> + Clone + FromBuilder, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, { fn default() -> Self { let params = Nsec3param::default(); - let hash_provider = OnDemandNsec3HashProvider::new( - params.hash_algorithm(), - params.iterations(), - params.salt().clone(), - ); Self { assume_dnskeys_will_be_added: true, params, nsec3param_ttl_mode: Default::default(), opt_out_exclude_owner_names_of_unsigned_delegations: true, - hash_provider, _phantom: Default::default(), } } } -/// Generate [RFC5155] NSEC3 and NSEC3PARAM records for this record set. +/// Generate RFC5155 NSEC3 and NSEC3PARAM records for this record set. /// -/// This function does NOT enforce use of current best practice settings, as -/// defined by [RFC 5155], [RFC 9077] and [RFC 9276] which state that: +/// This function enforces [RFC 9077] when it says that the "TTL of the +/// NSEC(3) RR that is returned MUST be the lesser of the MINIMUM field of the +/// SOA record and the TTL of the SOA itself". /// -/// - The `ttl` should be the _"lesser of the MINIMUM field of the zone SOA RR -/// and the TTL of the zone SOA RR itself"_. +/// This function does NOT enforce the use of [RFC 9276] best practices which +/// state that: /// /// - The `params` should be set to _"SHA-1, no extra iterations, empty salt"_ /// and zero flags. See [`Nsec3param::default()`]. @@ -172,14 +141,14 @@ where /// This function may panic if the input records are not sorted in DNSSEC /// canonical order (see [`CanonicalOrd`]). /// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. // TODO: Get rid of &mut for GenerateNsec3Config. -pub fn generate_nsec3s( - records: RecordsIter<'_, N, ZoneRecordData>, - config: &mut GenerateNsec3Config, +pub fn generate_nsec3s( + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, + config: &GenerateNsec3Config, ) -> Result, SigningError> where N: ToName + Clone + Display + Ord + Hash + Send + From>, @@ -191,7 +160,6 @@ where + Send, Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, - HashProvider: Nsec3HashProvider, Sort: Sorter, { // RFC 5155 7.1 step 2: @@ -200,31 +168,46 @@ where config.params.opt_out_flag() && config.opt_out_exclude_owner_names_of_unsigned_delegations; + // The generated collection of NSEC3 RRs that will be returned to the + // caller. let mut nsec3s = Vec::>>::new(); + + // A collection of empty non-terminal names (ENTs) discovered while + // walking the zone. NSEC3 RRs will be generated for these RRs as well as + // the RRs explicitly present in the zone. let mut ents = Vec::::new(); + // The number of labels in the apex name. Used when discovering ENTs. + let apex_label_count = apex_owner.iter_labels().count(); + + // The stack of non-empty non-terminal labels currently being walked in the + // zone. Used for implementing RFC 5155 7.1 step 4. + let mut last_nent_stack: Vec = vec![]; + // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - let first_rr = records.first(); - let apex_owner = first_rr.owner().clone(); - let apex_label_count = apex_owner.iter_labels().count(); + // The TTL to use for NSEC3 records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut nsec3_ttl = None; - let mut last_nent_stack: Vec = vec![]; - let mut ttl = None; + // The TTL value to be used for the NSEC3PARAM RR. Determined once + // nsec3_ttl is known. let mut nsec3param_ttl = None; + // Skip any glue records that sort earlier than the zone apex. + records.skip_before(apex_owner); + // RFC 5155 7.1 step 2 // For each unique original owner name in the zone add an NSEC3 RR. - for owner_rrs in records { trace!("Owner: {}", owner_rrs.owner()); - // If the owner is out of zone, we have moved out of our zone and are - // done. - if !owner_rrs.is_in_zone(&apex_owner) { + // If the owner is out of zone, we might have moved out of our zone + // and are done. + if !owner_rrs.is_in_zone(apex_owner) { debug!( - "Stopping NSEC3 generation at out-of-zone owner {}", + "Stopping at owner {} as it is out of zone and assumed to trail the zone", owner_rrs.owner() ); break; @@ -249,7 +232,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(&apex_owner) { + cut = if owner_rrs.is_zone_cut(apex_owner) { trace!("Zone cut detected at owner {}", owner_rrs.owner()); Some(name.clone()) } else { @@ -459,7 +442,7 @@ where // say that the "TTL of the NSEC(3) RR that is returned MUST // be the lesser of the MINIMUM field of the SOA record and // the TTL of the SOA itself". - ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + nsec3_ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); nsec3param_ttl = match config.nsec3param_ttl_mode { Nsec3ParamTtlMode::Fixed(ttl) => Some(ttl), @@ -469,7 +452,7 @@ where } } - if ttl.is_none() { + if nsec3_ttl.is_none() { return Err(SigningError::SoaRecordCouldNotBeDetermined); } @@ -485,15 +468,13 @@ where // SAFETY: ttl will be set above before we get here. let rec: Record> = mk_nsec3( &name, - &mut config.hash_provider, config.params.hash_algorithm(), config.params.flags(), config.params.iterations(), config.params.salt(), - &apex_owner, + apex_owner, bitmap, - ttl.unwrap(), - false, + nsec3_ttl.unwrap(), )?; // Store the record by order of its owner name. @@ -505,6 +486,10 @@ where last_nent_stack.push(name.clone()); } + let Some(nsec3param_ttl) = nsec3param_ttl else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + }; + for name in ents { // Create the type bitmap, empty for an ENT NSEC3. let bitmap = RtypeBitmap::::builder(); @@ -513,15 +498,13 @@ where // SAFETY: ttl will be set below before prev is set to Some. let rec = mk_nsec3( &name, - &mut config.hash_provider, config.params.hash_algorithm(), config.params.flags(), config.params.iterations(), config.params.salt(), - &apex_owner, + apex_owner, bitmap, - ttl.unwrap(), - true, + nsec3_ttl.unwrap(), )?; // Store the record by order of its owner name. @@ -628,10 +611,6 @@ where nsec3.data_mut().set_next_owner(next_hashed_owner_name); } - let Some(nsec3param_ttl) = nsec3param_ttl else { - return Err(SigningError::SoaRecordCouldNotBeDetermined); - }; - // RFC 5155 7.1 step 8: // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." @@ -662,30 +641,24 @@ where // records if the unhashed owner name is an ENT or not, so we pass this flag // to the hash provider and it can record it if wanted. #[allow(clippy::too_many_arguments)] -fn mk_nsec3( +fn mk_nsec3( name: &N, - hash_provider: &mut HashProvider, - alg: Nsec3HashAlg, + alg: Nsec3HashAlgorithm, flags: u8, iterations: u16, salt: &Nsec3Salt, apex_owner: &N, bitmap: RtypeBitmapBuilder<::Builder>, ttl: Ttl, - unhashed_owner_name_is_ent: bool, ) -> Result>, Nsec3HashError> where N: ToName + From>, Octs: FromBuilder + Clone + Default, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]> + Truncate, - HashProvider: Nsec3HashProvider, { - let owner_name = hash_provider.get_or_create( - apex_owner, - name, - unhashed_owner_name_is_ent, - )?; + let owner_name = + mk_hashed_nsec3_owner_name(name, alg, iterations, salt, apex_owner)?; // RFC 5155 7.1. step 2: // "The Next Hashed Owner Name field is left blank for the moment." @@ -715,7 +688,7 @@ where pub fn mk_hashed_nsec3_owner_name( name: &N, - alg: Nsec3HashAlg, + alg: Nsec3HashAlgorithm, iterations: u16, salt: &Nsec3Salt, apex_owner: &N, @@ -728,6 +701,13 @@ where { let base32hex_label = mk_base32hex_label_for_name(name, alg, iterations, salt)?; + #[cfg(test)] + if tests::NSEC3_TEST_MODE + .with(|n| *n.borrow() == tests::Nsec3TestMode::NoHash) + { + let name = N::from(name.try_to_name().ok().unwrap()); + return Ok(name); + } Ok(append_origin(base32hex_label, apex_owner)) } @@ -746,7 +726,7 @@ where fn mk_base32hex_label_for_name( name: &N, - alg: Nsec3HashAlg, + alg: Nsec3HashAlgorithm, iterations: u16, salt: &Nsec3Salt, ) -> Result @@ -756,76 +736,17 @@ where { let hash_octets: Vec = nsec3_hash(name, alg, iterations, salt)?.into_octets(); + #[cfg(test)] + let hash_octets = if tests::NSEC3_TEST_MODE + .with(|n| *n.borrow() == tests::Nsec3TestMode::Colliding) + { + vec![0; hash_octets.len()] + } else { + hash_octets + }; Ok(base32::encode_string_hex(&hash_octets).to_ascii_lowercase()) } -//------------ Nsec3HashProvider --------------------------------------------- - -pub trait Nsec3HashProvider { - fn get_or_create( - &mut self, - apex_owner: &N, - unhashed_owner_name: &N, - unhashed_owner_name_is_ent: bool, - ) -> Result; -} - -pub struct OnDemandNsec3HashProvider { - alg: Nsec3HashAlg, - iterations: u16, - salt: Nsec3Salt, -} - -impl OnDemandNsec3HashProvider { - pub fn new( - alg: Nsec3HashAlg, - iterations: u16, - salt: Nsec3Salt, - ) -> Self { - Self { - alg, - iterations, - salt, - } - } - - pub fn algorithm(&self) -> Nsec3HashAlg { - self.alg - } - - pub fn iterations(&self) -> u16 { - self.iterations - } - - pub fn salt(&self) -> &Nsec3Salt { - &self.salt - } -} - -impl Nsec3HashProvider - for OnDemandNsec3HashProvider -where - N: ToName + From>, - Octs: FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - SaltOcts: AsRef<[u8]> + From<&'static [u8]>, -{ - fn get_or_create( - &mut self, - apex_owner: &N, - unhashed_owner_name: &N, - _unhashed_owner_name_is_ent: bool, - ) -> Result { - mk_hashed_nsec3_owner_name( - unhashed_owner_name, - self.alg, - self.iterations, - &self.salt, - apex_owner, - ) - } -} - //----------- Nsec3ParamTtlMode ---------------------------------------------- /// The TTL to use for the NSEC3PARAM RR. @@ -841,13 +762,13 @@ where /// /// RFC 1034 says when _"When a name server loads a zone, it forces the TTL of /// all authoritative RRs to be at least the MINIMUM field of the SOA"_ so an -/// approach used by some zone signers (e.g. PowerDNS [1]) is to use the SOA +/// approach used by some zone signers (e.g. PowerDNS) is to use the SOA /// MINIMUM as the TTL for the NSEC3PARAM. /// /// An alternative approach used by some zone signers is to use a fixed TTL /// for the NSEC3PARAM TTL, e.g. BIND, dnssec-signzone and OpenDNSSEC -/// reportedly use 0 [1] while ldns-signzone uses 3600 [2] (as does an example -/// in the BIND documentation [3]). +/// reportedly use 0 while ldns-signzone uses 3600 (as does an example +/// in the BIND documentation). /// /// The default approach used here is to use the TTL of the SOA RR, NOT the /// SOA MINIMUM. This is consistent with how a TTL is chosen by tools such as @@ -939,22 +860,34 @@ mod tests { // order for us. use core::str::FromStr; - use bytes::Bytes; + use std::cell::RefCell; + use pretty_assertions::assert_eq; - use crate::sign::records::SortedRecords; - use crate::sign::test_util::*; - use crate::zonetree::StoredName; + use crate::dnssec::sign::records::SortedRecords; + use crate::dnssec::sign::test_util::*; use super::*; + #[derive(PartialEq)] + pub(super) enum Nsec3TestMode { + Normal, + Colliding, + NoHash, + } + + thread_local! { + pub(super) static NSEC3_TEST_MODE: RefCell = const { RefCell::new(Nsec3TestMode::Normal) }; + } + #[test] fn soa_is_required() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([mk_a_rr("some_a.a.")]); - let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -963,13 +896,14 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_soa_rr("a.", "d.", "e."), ]); - let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -978,8 +912,10 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let a_apex = Name::from_str("a.").unwrap(); + let b_apex = Name::from_str("b.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("b.", "d.", "e."), mk_soa_rr("a.", "b.", "c."), @@ -987,14 +923,9 @@ mod tests { mk_a_rr("some_a.b."), ]); - // First generate NSEC3s for the total record collection. As the - // collection is sorted in canonical order the a zone preceeds the b - // zone and NSEC3s should only be generated for the first zone in the - // collection. - let a_and_b_records = records.owner_rrs(); - + // Generate NSEC3s for the a. zone. let generated_records = - generate_nsec3s(a_and_b_records, &mut cfg).unwrap(); + generate_nsec3s(&a_apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1009,13 +940,9 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); - // Now skip the a zone in the collection and generate NSEC3s for the - // remaining records which should only generate NSEC3s for the b zone. - let mut b_records_only = records.owner_rrs(); - b_records_only.skip_before(&mk_name("b.")); - + // Generate NSEC3s for the b. zone. let generated_records = - generate_nsec3s(b_records_only, &mut cfg).unwrap(); + generate_nsec3s(&b_apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1032,10 +959,49 @@ mod tests { assert!(!generated_records.nsec3param.data().opt_out_flag()); } + #[test] + fn glue_records_are_ignored() { + let cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("example.").unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Generate NSEs for the a. zone. + let generated_records = + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); + + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "example.", + "example.", + "in_zone.example.", + "NS SOA RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr( + "example.", + "in_zone.example.", + "example.", + "A RRSIG", + &cfg, + ), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } + #[test] fn occluded_records_are_ignored() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("some_ns.a.", "some_a.other.b."), @@ -1043,7 +1009,7 @@ mod tests { ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1074,15 +1040,16 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { - let mut cfg = GenerateNsec3Config::default(); + let cfg = GenerateNsec3Config::default(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1104,29 +1071,24 @@ mod tests { // These NSEC3 settings match those of the NSEC3PARAM record shown in // https://datatracker.ietf.org/doc/html/rfc5155#appendix-A. let nsec3params = Nsec3param::new( - Nsec3HashAlg::SHA1, + Nsec3HashAlgorithm::SHA1, 1, // opt-out 12, Nsec3Salt::from_str("aabbccdd").unwrap(), ); - let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( - nsec3params.clone(), - OnDemandNsec3HashProvider::new( - nsec3params.hash_algorithm(), - nsec3params.iterations(), - nsec3params.salt().clone(), - ), - ) - .without_assuming_dnskeys_will_be_added(); + let cfg = + GenerateNsec3Config::<_, DefaultSorter>::new(nsec3params.clone()) + .without_assuming_dnskeys_will_be_added(); // See https://datatracker.ietf.org/doc/html/rfc5155#appendix-A let zonefile = include_bytes!( - "../../../test-data/zonefiles/rfc5155-appendix-A.zone" + "../../../../test-data/zonefiles/rfc5155-appendix-A.zone" ); + let apex = Name::from_str("example.").unwrap(); let records = bytes_to_records(&zonefile[..]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); // Generate the expected NSEC3 RRs. The hashes used match those listed // in https://datatracker.ietf.org/doc/html/rfc5155#appendix-A and can @@ -1211,7 +1173,7 @@ mod tests { mk_precalculated_nsec3_rr( // from: ai.example. to: y.w.example. "gjeqe526plbf1g8mklp59enfd789njgi.example.", - "jPai6neoaepv8b5o6k4ev33abha8ht9fgc", + "ji6neoaepv8b5o6k4ev33abha8ht9fgc", "A HINFO AAAA RRSIG", &cfg, ), @@ -1240,62 +1202,6 @@ mod tests { "", &cfg, ), - ]); - - assert_eq!(generated_records.nsec3s, expected_records.into_inner()); - - let expected_nsec3param = mk_nsec3param_rr("example.", &cfg); - assert_eq!(generated_records.nsec3param, expected_nsec3param); - - // TTLs are not compared by the eq check above so check them - // explicitly now. - // - // RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to say that - // the "TTL of the NSEC(3) RR that is returned MUST be the lesser of - // the MINIMUM field of the SOA record and the TTL of the SOA itself". - // - // So in our case that is min(1800, 3600) = 1800. - for nsec3 in &generated_records.nsec3s { - assert_eq!(nsec3.ttl(), Ttl::from_secs(1800)); - } - } - - #[test] - fn rfc_5155_and_9077_compliant_opt_out_enabled_flags_only() { - let nsec3params = Nsec3param::new( - Nsec3HashAlg::SHA1, - 1, // enable opt-out - 12, // do 12 extra hashing iterations - Nsec3Salt::from_str("aabbccdd").unwrap(), - ); - - let mut cfg = GenerateNsec3Config::< - StoredName, - Bytes, - OnDemandNsec3HashProvider, - DefaultSorter, - >::new( - nsec3params.clone(), - OnDemandNsec3HashProvider::new( - nsec3params.hash_algorithm(), - nsec3params.iterations(), - nsec3params.salt().clone(), - ), - ) - .without_assuming_dnskeys_will_be_added() - .without_opt_out_excluding_owner_names_of_unsigned_delegations(); - - // See https://datatracker.ietf.org/doc/html/rfc5155#appendix-A - let zonefile = include_bytes!( - "../../../test-data/zonefiles/rfc5155-appendix-A.zone" - ); - - let records = bytes_to_records(&zonefile[..]); - let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); - - // Generate the expected NSEC3 RRs. - let expected_records = SortedRecords::<_, _>::from_iter([ mk_precalculated_nsec3_rr( // from: ns2.example. to: *.w.example. "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", @@ -1317,98 +1223,6 @@ mod tests { "A HINFO AAAA RRSIG", &cfg, ), - mk_precalculated_nsec3_rr( - // ns1.example. -> x.y.w.example. - "2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.", - "2vptu5timamqttgl4luu9kg21e0aor3s", - "A RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // x.y.w.example. -> a.example. - "2vptu5timamqttgl4luu9kg21e0aor3s.example.", - "35mthgpgcu1qg68fab165klnsnk3dpvl", - "MX RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // a.example. -> c.example. - "35mthgpgcu1qg68fab165klnsnk3dpvl.example.", - "4g6p9u5gvfshp30pqecj98b3maqbn1ck", - "NS DS RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // c.example. -> x.w.example. - // Note: as this is an insecure delegation and NSEC3 opt-out - // is disabled the c.example. RRSET has an NSEC3 RR but its - // type bitmap lacks the RRSIG RTYPE as insecure delegations - // are not signed. - "4g6p9u5gvfshp30pqecj98b3maqbn1ck.example.", - "b4um86eghhds6nea196smvmlo4ors995", - "NS", - &cfg, - ), - mk_precalculated_nsec3_rr( - // x.w.example. -> ai.example. - "b4um86eghhds6nea196smvmlo4ors995.example.", - "gjeqe526plbf1g8mklp59enfd789njgi", - "MX RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // ai.example. -> y.w.example. - "gjeqe526plbf1g8mklp59enfd789njgi.example.", - "ji6neoaepv8b5o6k4ev33abha8ht9fgc", - "A HINFO AAAA RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // y.w.example -> w.example. - "ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.", - "k8udemvp1j2f7eg6jebps17vp3n8i58h", - "", - &cfg, - ), - // Unlike NSEC, with NSEC3 empty non-terminals must also have - // NSEC3 RRs: - // - // https://www.rfc-editor.org/rfc/rfc5155#section-7.1 - // 7.1. Zone Signing - // .. - // "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." - // - // ENT NSEC3 RRs have an empty Type Bit Map. - mk_precalculated_nsec3_rr( - // w.example. -> ns2.example. - "k8udemvp1j2f7eg6jebps17vp3n8i58h.example.", - "q04jkcevqvmu85r014c7dkba38o0ji5r", - "", - &cfg, - ), - mk_precalculated_nsec3_rr( - // ns2.example. -> *.w.example. - "q04jkcevqvmu85r014c7dkba38o0ji5r.example.", - "r53bq7cc2uvmubfu5ocmm6pers9tk9en", - "A RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // *.w.example. -> xx.example. - "r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.", - "t644ebqk9bibcna874givr6joj62mlhv", - "MX RRSIG", - &cfg, - ), - mk_precalculated_nsec3_rr( - // xx.example. -> example. - "t644ebqk9bibcna874givr6joj62mlhv.example.", - "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom", - "A HINFO AAAA RRSIG", - &cfg, - ), ]); assert_eq!(generated_records.nsec3s, expected_records.into_inner()); @@ -1451,17 +1265,18 @@ mod tests { // This test tests opt-out with exclusion, i.e. opt-out that excludes // an unsigned delegation and thus there "MUST be an Opt-Out NSEC3 // RR...". - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .with_opt_out() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([mk_nsec3_rr( @@ -1488,20 +1303,21 @@ mod tests { // // This test tests opt-out with_out_ exclusion, i.e. opt-out that // creates an NSEC RR for an unsigned delegation. - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .with_opt_out() .without_opt_out_excluding_owner_names_of_unsigned_delegations() .without_assuming_dnskeys_will_be_added(); // This also tests the case of handling a single NSEC3 as only the SOA // RR gets an NSEC3, the NS RR does not. + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1523,9 +1339,10 @@ mod tests { expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?" )] fn generating_nsec3s_for_unordered_input_should_panic() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = vec![ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), @@ -1533,23 +1350,24 @@ mod tests { mk_aaaa_rr("some_a.a."), ]; - let _res = generate_nsec3s(RecordsIter::new(&records), &mut cfg); + let _res = generate_nsec3s(&apex, RecordsIter::new(&records), &cfg); } #[test] fn test_nsec3_hash_collision_handling() { - let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( + let cfg = GenerateNsec3Config::<_, DefaultSorter>::new( Nsec3param::default(), - CollidingHashProvider, ); + NSEC3_TEST_MODE.replace(Nsec3TestMode::Colliding); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); assert!(matches!( - generate_nsec3s(records.owner_rrs(), &mut cfg), + generate_nsec3s(&apex, records.owner_rrs(), &cfg), Err(SigningError::Nsec3HashingError( Nsec3HashError::CollisionDetected )) @@ -1558,49 +1376,22 @@ mod tests { #[test] fn test_nsec3_hashing_failure() { - let mut cfg = GenerateNsec3Config::<_, _, _, DefaultSorter>::new( + let cfg = GenerateNsec3Config::<_, DefaultSorter>::new( Nsec3param::default(), - NonHashingHashProvider, ); + NSEC3_TEST_MODE.replace(Nsec3TestMode::NoHash); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); assert!(matches!( - generate_nsec3s(records.owner_rrs(), &mut cfg), + generate_nsec3s(&apex, records.owner_rrs(), &cfg), Err(SigningError::Nsec3HashingError( Nsec3HashError::OwnerHashError )) )); } - - //------------ Test helpers ---------------------------------------------- - - struct CollidingHashProvider; - - impl Nsec3HashProvider for CollidingHashProvider { - fn get_or_create( - &mut self, - _apex_owner: &StoredName, - _unhashed_owner_name: &StoredName, - _unhashed_owner_name_is_ent: bool, - ) -> Result { - Ok(StoredName::root()) - } - } - - struct NonHashingHashProvider; - - impl Nsec3HashProvider for NonHashingHashProvider { - fn get_or_create( - &mut self, - _apex_owner: &StoredName, - unhashed_owner_name: &StoredName, - _unhashed_owner_name_is_ent: bool, - ) -> Result { - Ok(unhashed_owner_name.clone()) - } - } } diff --git a/src/dnssec/sign/error.rs b/src/dnssec/sign/error.rs new file mode 100644 index 000000000..1a0791000 --- /dev/null +++ b/src/dnssec/sign/error.rs @@ -0,0 +1,81 @@ +//! Signing related errors. +use core::fmt::{Debug, Display}; + +use crate::crypto::sign::SignError; +use crate::dnssec::common::Nsec3HashError; +use crate::rdata::dnssec::Timestamp; + +//------------ SigningError -------------------------------------------------- + +#[derive(Copy, Clone, Debug)] +pub enum SigningError { + /// One or more keys does not have a signature validity period defined. + NoSignatureValidityPeriodProvided, + + /// TODO + OutOfMemory, + + // The zone either lacks a SOA record or has more than one SOA record. + SoaRecordCouldNotBeDetermined, + + /// Cannot create an Rrset from an empty slice. + EmptyRecordSlice, + + // TODO + Nsec3HashingError(Nsec3HashError), + + /// TODO + /// + /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 + /// 2.2. Including RRSIG RRs in a Zone + /// ... + /// "An RRSIG RR itself MUST NOT be signed" + RrsigRrsMustNotBeSigned, + + // TODO + InvalidSignatureValidityPeriod(Timestamp, Timestamp), + + // TODO + SigningError(SignError), +} + +impl Display for SigningError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + SigningError::NoSignatureValidityPeriodProvided => { + f.write_str("No signature validity period found for key") + } + SigningError::OutOfMemory => f.write_str("Out of memory"), + SigningError::SoaRecordCouldNotBeDetermined => { + f.write_str("No apex SOA or too many apex SOA records found") + } + SigningError::EmptyRecordSlice => { + f.write_str("Empty slice of Record") + } + SigningError::Nsec3HashingError(err) => { + f.write_fmt(format_args!("NSEC3 hashing error: {err}")) + } + SigningError::RrsigRrsMustNotBeSigned => f.write_str( + "RFC 4035 violation: RRSIG RRs MUST NOT be signed", + ), + SigningError::InvalidSignatureValidityPeriod(inception, expiration) => f.write_fmt( + format_args!("RFC 4034 violation: RRSIG validity period ({inception} <= {expiration}) is invalid"), + ), + SigningError::SigningError(err) => { + f.write_fmt(format_args!("Signing error: {err}")) + } + } + } +} + +impl From for SigningError { + fn from(err: SignError) -> Self { + Self::SigningError(err) + } +} + +impl From for SigningError { + fn from(err: Nsec3HashError) -> Self { + Self::Nsec3HashingError(err) + } +} diff --git a/src/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs similarity index 99% rename from src/sign/keys/keyset.rs rename to src/dnssec/sign/keys/keyset.rs index 4ebeef87c..8a12ff63d 100644 --- a/src/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -7,7 +7,7 @@ //! //! ```no_run //! use domain::base::Name; -//! use domain::sign::keys::keyset::{KeySet, RollType, UnixTime}; +//! use domain::dnssec::sign::keys::keyset::{KeySet, RollType, UnixTime}; //! use std::fs::File; //! use std::io::Write; //! use std::str::FromStr; @@ -1517,7 +1517,7 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { #[cfg(test)] mod tests { use crate::base::Name; - use crate::sign::keys::keyset::{ + use crate::dnssec::sign::keys::keyset::{ Action, KeySet, KeyType, RollType, UnixTime, }; use crate::std::string::ToString; diff --git a/src/sign/keys/mod.rs b/src/dnssec/sign/keys/mod.rs similarity index 58% rename from src/sign/keys/mod.rs rename to src/dnssec/sign/keys/mod.rs index fd297de85..1602360f6 100644 --- a/src/sign/keys/mod.rs +++ b/src/dnssec/sign/keys/mod.rs @@ -2,11 +2,6 @@ //! //! # Importing and Exporting //! -//! The [`SecretKeyBytes`] type is a generic representation of a secret key as -//! a byte slice. While it does not offer any cryptographic functionality, it -//! is useful to transfer secret keys stored in memory, independent of any -//! cryptographic backend. -//! //! The `KeyPair` types of the cryptographic backends in this module each //! support a `from_bytes()` function that parses the generic representation //! into a functional cryptographic key. Importantly, these functions require @@ -14,11 +9,6 @@ //! for consistency. In some cases, it may also be possible to serialize an //! existing cryptographic key back to the generic bytes representation. //! -//! [`SecretKeyBytes`] also supports importing and exporting keys from and to -//! the conventional private-key format popularized by BIND. This format is -//! used by a variety of tools for storing DNSSEC keys on disk. See the -//! type-level documentation for a specification of the format. -//! //! # Key Sets and Key Lifetime //! //! The [`keyset`] module provides a way to keep track of the collection of @@ -30,11 +20,7 @@ //! //! -pub mod bytes; -pub mod keymeta; pub mod keyset; pub mod signingkey; -pub use self::bytes::SecretKeyBytes; -pub use self::keymeta::DnssecSigningKey; pub use self::signingkey::SigningKey; diff --git a/src/sign/keys/signingkey.rs b/src/dnssec/sign/keys/signingkey.rs similarity index 83% rename from src/sign/keys/signingkey.rs rename to src/dnssec/sign/keys/signingkey.rs index 67ec92675..dd27fdc96 100644 --- a/src/sign/keys/signingkey.rs +++ b/src/dnssec/sign/keys/signingkey.rs @@ -1,14 +1,21 @@ -use crate::base::iana::SecAlg; +use crate::base::iana::SecurityAlgorithm; use crate::base::Name; -use crate::sign::{PublicKeyBytes, SignRaw}; -use crate::validate::Key; +use crate::crypto::sign::SignRaw; +use crate::rdata::Dnskey; +use std::fmt::Debug; +use std::vec::Vec; //----------- SigningKey ----------------------------------------------------- /// A signing key. /// /// This associates important metadata with a raw cryptographic secret key. -pub struct SigningKey { +// Make debugging easier. Octets support Debug, we just need to require it. +#[derive(Debug)] +pub struct SigningKey +where + Octs: AsRef<[u8]> + Debug, +{ /// The owner of the key. owner: Name, @@ -23,7 +30,10 @@ pub struct SigningKey { //--- Construction -impl SigningKey { +impl SigningKey +where + Octs: AsRef<[u8]> + Debug, +{ /// Construct a new signing key manually. pub fn new(owner: Name, flags: u16, inner: Inner) -> Self { Self { @@ -36,7 +46,10 @@ impl SigningKey { //--- Inspection -impl SigningKey { +impl SigningKey +where + Octs: AsRef<[u8]> + Debug, +{ /// The owner name attached to the key. pub fn owner(&self) -> &Name { &self.owner @@ -105,21 +118,11 @@ impl SigningKey { } /// The signing algorithm used. - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.inner.algorithm() } - /// The associated public key. - pub fn public_key(&self) -> Key<&Octs> - where - Octs: AsRef<[u8]>, - { - let owner = Name::from_octets(self.owner.as_octets()).unwrap(); - Key::new(owner, self.flags, self.inner.raw_public_key()) - } - - /// The associated raw public key. - pub fn raw_public_key(&self) -> PublicKeyBytes { - self.inner.raw_public_key() + pub fn dnskey(&self) -> Dnskey> { + self.inner.dnskey() } } diff --git a/src/sign/mod.rs b/src/dnssec/sign/mod.rs similarity index 71% rename from src/sign/mod.rs rename to src/dnssec/sign/mod.rs index 89d12f9a1..f08dbc690 100644 --- a/src/sign/mod.rs +++ b/src/dnssec/sign/mod.rs @@ -44,20 +44,22 @@ //! //! # Usage //! -//! - To generate and/or import signing keys see the [`crypto`] module. +//! - To generate and/or import signing keys see the [`crate::crypto`] module. +//! - To sign apex [`Record`]s involved in the chain of the trust to the +//! parent see the [`sign_rrset()`] function. //! - To sign a collection of [`Record`]s that represent a zone see the //! [`SignableZoneInPlace`] trait. //! - To manage the life cycle of signing keys see the [`keyset`] module. //! //! # Advanced usage //! -//! - For more control over the signing process see the [`SigningConfig`] type -//! and the [`SigningKeyUsageStrategy`] and [`DnssecSigningKey`] traits. +//! - For more control over the signing process see the [`SigningConfig`] +//! type. //! - For additional ways to sign zones see the [`SignableZone`] trait and the //! [`sign_zone()`] function. //! - To invoke specific stages of the signing process manually see the //! [`Signable`] trait and the [`generate_nsecs()`], [`generate_nsec3s()`], -//! [`generate_rrsigs()`] and [`sign_rrset()`] functions. +//! [`sign_sorted_zone_records()`] and [`sign_rrset()`] functions. //! - To generate signatures for arbitrary data see the [`SignRaw`] trait. //! //! # Limitations @@ -68,7 +70,10 @@ //! - Re-signing an already signed zone, only unsigned zones can be signed. //! - Signing of unsorted zones, record collections must be sorted according //! to [`CanonicalOrd`]. -//! - Signing of [`Zone`] types or via an [`core::iter::Iterator`] over +//! - Signing of +#![cfg_attr(feature = "unstable-zonetree", doc = "[`Zone`]")] +#![cfg_attr(not(feature = "unstable-zonetree"), doc = "`Zone`")] +//! types or via an [`core::iter::Iterator`] over //! [`Record`]s, only signing of slices is supported. //! - Signing with both `NSEC` and `NSEC3` or multiple `NSEC3` configurations //! at once. @@ -77,10 +82,10 @@ //! present if you bring your own cryptography). //! //! [`common`]: crate::sign::crypto::common -//! [`keyset`]: crate::sign::keys::keyset +//! [`keyset`]: crate::dnssec::sign::keys::keyset //! [`openssl`]: crate::sign::crypto::openssl //! [`ring`]: crate::sign::crypto::ring -//! [`sign_rrset()`]: crate::sign::signatures::rrsigs::sign_rrset +//! [`sign_rrset()`]: crate::dnssec::sign::signatures::rrsigs::sign_rrset //! [`DnssecSigningKey`]: crate::sign::keys::DnssecSigningKey //! [`Record`]: crate::base::record::Record //! [RFC 5155]: https://rfc-editor.org/rfc/rfc5155 @@ -92,20 +97,26 @@ //! https://www.rfc-editor.org/rfc/rfc9499.html#section-10 //! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams //! [`KeyPair`]: crate::sign::crypto::common::KeyPair -//! [`SigningKeyUsageStrategy`]: -//! crate::sign::signing::strategy::SigningKeyUsageStrategy -//! [`Signable`]: crate::sign::traits::Signable -//! [`SignableZone`]: crate::sign::traits::SignableZone -//! [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace +//! [`Signable`]: crate::dnssec::sign::traits::Signable +//! [`SignableZone`]: crate::dnssec::sign::traits::SignableZone +//! [`SignableZoneInPlace`]: crate::dnssec::sign::traits::SignableZoneInPlace //! [`SigningKey`]: crate::sign::keys::SigningKey //! [`SortedRecords`]: crate::sign::SortedRecords //! [`Zone`]: crate::zonetree::Zone -#![cfg(feature = "unstable-sign")] -#![cfg_attr(docsrs, doc(cfg(feature = "unstable-sign")))] +#![cfg(all( + feature = "unstable-sign", + any(feature = "ring", feature = "openssl") +))] +#![cfg_attr( + docsrs, + doc(cfg(all( + feature = "unstable-sign", + any(feature = "ring", feature = "openssl") + ))) +)] pub mod config; -pub mod crypto; pub mod denial; pub mod error; pub mod keys; @@ -116,10 +127,9 @@ pub mod traits; #[cfg(test)] pub mod test_util; -pub use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; +use crate::crypto::sign::SignRaw; pub use self::config::SigningConfig; -pub use self::keys::bytes::{RsaSecretKeyBytes, SecretKeyBytes}; use core::fmt::Display; use core::hash::Hash; @@ -131,25 +141,20 @@ use std::fmt::Debug; use std::vec::Vec; use crate::base::{CanonicalOrd, ToName}; -use crate::base::{Name, Record, Rtype}; +use crate::base::{Name, Record}; use crate::rdata::ZoneRecordData; use denial::config::DenialConfig; use denial::nsec::generate_nsecs; -use denial::nsec3::{generate_nsec3s, Nsec3HashProvider, Nsec3Records}; +use denial::nsec3::{generate_nsec3s, Nsec3Records}; use error::SigningError; -use keys::keymeta::DesignatedSigningKey; +use keys::SigningKey; use octseq::{ EmptyBuilder, FromBuilder, OctetsBuilder, OctetsFrom, Truncate, }; use records::{RecordsIter, Sorter}; -use signatures::rrsigs::{ - generate_rrsigs, GenerateRrsigConfig, RrsigRecords, -}; -use signatures::strategy::{ - RrsigValidityPeriodStrategy, SigningKeyUsageStrategy, -}; -use traits::{SignRaw, SignableZone, SortedExtend}; +use signatures::rrsigs::{sign_sorted_zone_records, GenerateRrsigConfig}; +use traits::{SignableZone, SortedExtend}; //------------ SignableZoneInOut --------------------------------------------- @@ -166,7 +171,7 @@ use traits::{SignRaw, SignableZone, SortedExtend}; /// as they handle the construction of this type and calling [`sign_zone()`]. /// /// [`Cow`]: std::borrow::Cow -/// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace +/// [`SignableZoneInPlace`]: crate::dnssec::sign::traits::SignableZoneInPlace pub enum SignableZoneInOut<'a, 'b, N, Octs, S, T, Sort> where N: Clone + ToName + From> + Ord + Hash, @@ -222,8 +227,9 @@ where impl SignableZoneInOut<'_, '_, N, Octs, S, T, Sort> where - N: Clone + ToName + From> + Ord + Hash, + N: Clone + Debug + ToName + From> + Ord + Hash, Octs: Clone + + Debug + FromBuilder + From<&'static [u8]> + Send @@ -290,9 +296,14 @@ where /// DNSSEC sign an unsigned zone using the given configuration and keys. /// /// An implementation of [RFC 4035 section 2 Zone Signing] with optional -/// support for NSEC3 ([RFC 5155]), i.e. it will generate `DNSKEY` (if -/// configured), `NSEC` or `NSEC3` (and if NSEC3 is in use then also -/// `NSEC3PARAM`), and `RRSIG` records. +/// support for NSEC3 ([RFC 5155]), i.e. it will generate `NSEC` or `NSEC3` +/// (and if NSEC3 is in use then also `NSEC3PARAM`), and `RRSIG` records. +/// +/// This function **CANNOT** be used to generate RRSIG RRs for DNSKEY, CDS and +/// CDNSKEY RRs. This function expects those RRs and their RRSIGs to already +/// be present in the zone. To sign DNSKEY, CDS and CDNSKEY RRs the lower +/// level function [`sign_rrset()`] should be used instead. For more +/// information see the [module docs]. /// /// Signing can either be done in-place (records generated by signing will be /// added to the record collection being signed) or into some other provided @@ -336,19 +347,22 @@ where /// /// - Re-signing, i.e. re-generating expired `RRSIG` signatures, updating the /// NSEC(3) chain to match added or removed records or adding signatures for -/// another key to an already signed zone e.g. to support key rollover. For -/// the latter case it does however support providing multiple sets of key -/// to sign with the [`SigningKeyUsageStrategy`] implementation being used -/// to determine which keys to use to sign which records. +/// another key to an already signed zone e.g. to support key rollover. /// /// - Signing with multiple NSEC(3) configurations at once, e.g. to migrate /// from NSEC <-> NSEC3 or between NSEC3 configurations. /// -/// - Signing of record collections stored in the [`Zone`] type as it +/// - Signing of record collections stored in the +#[cfg_attr(feature = "unstable-zonetree", doc = "[`Zone`]")] +#[cfg_attr(not(feature = "unstable-zonetree"), doc = "`Zone`")] +/// type as it /// currently only support signing of record slices whereas the records in a -/// [`Zone`] currently only supports a visitor style read interface via -/// [`ReadableZone`] whereby a callback function is invoked for each node -/// that is "walked". +#[cfg_attr(feature = "unstable-zonetree", doc = "[`Zone`]")] +#[cfg_attr(not(feature = "unstable-zonetree"), doc = "`Zone`")] +/// currently only supports a visitor style read interface via +#[cfg_attr(feature = "unstable-zonetree", doc = "[`ReadableZone`]")] +#[cfg_attr(not(feature = "unstable-zonetree"), doc = "`ReadableZone`")] +/// whereby a callback function is invoked for each node that is "walked". /// /// # Configuration /// @@ -361,41 +375,22 @@ where /// [RFC 5155]: https://www.rfc-editor.org/info/rfc5155 /// [RFC 5155 section 2 Backwards Compatibility]: /// https://www.rfc-editor.org/rfc/rfc5155.html#section-2 -/// [`SignableZoneInPlace`]: crate::sign::traits::SignableZoneInPlace -/// [`SortedRecords`]: crate::sign::records::SortedRecords +/// [`SignableZoneInPlace`]: crate::dnssec::sign::traits::SignableZoneInPlace +/// [`SortedRecords`]: crate::dnssec::sign::records::SortedRecords /// [`Zone`]: crate::zonetree::Zone -pub fn sign_zone< - N, - Octs, - S, - DSK, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP, - T, ->( +pub fn sign_zone( + apex_owner: &N, mut in_out: SignableZoneInOut, - signing_config: &mut SigningConfig< - N, - Octs, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP, - >, - signing_keys: &[DSK], + signing_config: &SigningConfig, + signing_keys: &[&SigningKey], ) -> Result<(), SigningError> where - DSK: DesignatedSigningKey, - HP: Nsec3HashProvider, - Inner: SignRaw, + Inner: Debug + SignRaw, N: Display + Send + CanonicalOrd + Clone + + Debug + ToName + From> + Ord @@ -403,13 +398,12 @@ where ::Builder: Truncate + EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy + Clone, S: SignableZone, Sort: Sorter, T: SortedExtend + ?Sized, Octs: FromBuilder + Clone + + Debug + From<&'static [u8]> + Send + OctetsFrom> @@ -417,33 +411,25 @@ where + Default, T: Deref>]>, { - // Iterate over the RR sets of the first owner name (should be the apex as - // the input should be ordered according to [`CanonicalOrd`] and should be - // a complete zone) to find the SOA record. There should be one and only - // one SOA record. - let soa_rr = get_apex_soa_rr(in_out.as_slice())?; - - let apex_owner = soa_rr.owner().clone(); - let owner_rrs = RecordsIter::new(in_out.as_slice()); - match &mut signing_config.denial { + match &signing_config.denial { DenialConfig::AlreadyPresent => { // Nothing to do. } DenialConfig::Nsec(ref cfg) => { - let nsecs = generate_nsecs(owner_rrs, cfg)?; + let nsecs = generate_nsecs(apex_owner, owner_rrs, cfg)?; in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } - DenialConfig::Nsec3(ref mut cfg, extra) if extra.is_empty() => { + DenialConfig::Nsec3(ref cfg) => { // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash // order." We store the NSEC3s as we create them and sort them // afterwards. let Nsec3Records { nsec3s, nsec3param } = - generate_nsec3s::(owner_rrs, cfg)?; + generate_nsec3s::(apex_owner, owner_rrs, cfg)?; // Add the generated NSEC3 records. in_out.sorted_extend( @@ -451,88 +437,28 @@ where .chain(nsec3s.into_iter().map(Record::from_record)), ); } - - DenialConfig::Nsec3(_nsec3_config, _extra) => { - todo!(); - } - - DenialConfig::TransitioningNsecToNsec3( - _nsec_config, - _nsec3_config, - _nsec_to_nsec3_transition_state, - ) => { - todo!(); - } - - DenialConfig::TransitioningNsec3ToNsec( - _nsec_config, - _nsec3_config, - _nsec3_to_nsec_transition_state, - ) => { - todo!(); - } } if !signing_keys.is_empty() { - let mut rrsig_config = - GenerateRrsigConfig::::new( - signing_config.rrsig_validity_period_strategy.clone(), - ); - rrsig_config.add_used_dnskeys = signing_config.add_used_dnskeys; - rrsig_config.zone_apex = Some(apex_owner); + let rrsig_config = GenerateRrsigConfig::new( + signing_config.inception, + signing_config.expiration, + ); // Sign the NSEC(3)s. let owner_rrs = RecordsIter::new(in_out.as_out_slice()); - let RrsigRecords { rrsigs, dnskeys } = - generate_rrsigs(owner_rrs, signing_keys, &rrsig_config)?; - - // Sorting may not be strictly needed, but we don't have the option to - // extend without sort at the moment. - in_out.sorted_extend( - dnskeys - .into_iter() - .map(Record::from_record) - .chain(rrsigs.into_iter().map(Record::from_record)), - ); - - // Sign the original unsigned records. - let owner_rrs = RecordsIter::new(in_out.as_slice()); - - let RrsigRecords { rrsigs, dnskeys } = - generate_rrsigs(owner_rrs, signing_keys, &rrsig_config)?; + let rrsigs = sign_sorted_zone_records( + apex_owner, + owner_rrs, + signing_keys, + &rrsig_config, + )?; // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. - in_out.sorted_extend( - dnskeys - .into_iter() - .map(Record::from_record) - .chain(rrsigs.into_iter().map(Record::from_record)), - ); + in_out.sorted_extend(rrsigs.into_iter().map(Record::from_record)); } Ok(()) } - -// Assumes that the given records are sorted in [`CanonicalOrd`] order. -fn get_apex_soa_rr( - slice: &[Record>], -) -> Result<&Record>, SigningError> -where - N: ToName, -{ - let first_owner_rrs = RecordsIter::new(slice) - .next() - .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; - let mut soa_rrs = first_owner_rrs - .records() - .filter(|rr| rr.rtype() == Rtype::SOA); - let soa_rr = soa_rrs - .next() - .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; - if soa_rrs.next().is_some() { - return Err(SigningError::SoaRecordCouldNotBeDetermined); - } - Ok(soa_rr) -} diff --git a/src/sign/records.rs b/src/dnssec/sign/records.rs similarity index 92% rename from src/sign/records.rs rename to src/dnssec/sign/records.rs index fc34e3b19..f5ba87de2 100644 --- a/src/sign/records.rs +++ b/src/dnssec/sign/records.rs @@ -16,6 +16,8 @@ use crate::base::rdata::RecordData; use crate::base::record::Record; use crate::base::Ttl; +use super::error::SigningError; + //------------ Sorter -------------------------------------------------------- /// A DNS resource record sorter. @@ -29,7 +31,7 @@ pub trait Sorter { /// The imposed order should be compatible with the ordering defined by /// RFC 8976 section 3.3.1, i.e. _"DNSSEC's canonical on-the-wire RR /// format (without name compression) and ordering as specified in - /// Sections 6.1, 6.2, and 6.3 of [RFC4034] with the additional provision + /// Sections 6.1, 6.2, and 6.3 of RFC4034 with the additional provision /// that RRsets having the same owner name MUST be numerically ordered, in /// ascending order, by their numeric RR TYPE"_. fn sort_by(records: &mut Vec>, compare: F) @@ -43,7 +45,7 @@ pub trait Sorter { /// The default [`Sorter`] implementation used by [`SortedRecords`]. /// /// The current implementation is the single threaded sort provided by Rust -/// [`std::vec::Vec::sort_by()`]. +/// [`std::vec::Vec.sort_by()`]. pub struct DefaultSorter; impl Sorter for DefaultSorter { @@ -88,7 +90,8 @@ where /// Insert a record in sorted order. /// - /// If inserting a lot of records at once prefer [`extend()`] instead + /// If inserting a lot of records at once prefer [`Extend::extend()`] + /// instead /// which will sort once after all insertions rather than once per /// insertion. pub fn insert(&mut self, record: Record) -> Result<(), Record> @@ -116,7 +119,7 @@ where /// - false: if no matching record was found pub fn remove_all_by_name_class_rtype( &mut self, - name: N, + name: &N, class: Option, rtype: Option, ) -> bool @@ -126,11 +129,7 @@ where { let mut found_one = false; loop { - if self.remove_first_by_name_class_rtype( - name.clone(), - class, - rtype, - ) { + if self.remove_first_by_name_class_rtype(name, class, rtype) { found_one = true } else { break; @@ -148,7 +147,7 @@ where /// - false: if no matching record was found pub fn remove_first_by_name_class_rtype( &mut self, - name: N, + name: &N, class: Option, rtype: Option, ) -> bool @@ -166,7 +165,7 @@ where } } - match stored.owner().name_cmp(&name) { + match stored.owner().name_cmp(name) { Ordering::Equal => {} res => return res, } @@ -202,6 +201,21 @@ where self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } + pub fn find_apex_rtype( + &self, + name: &N, + rtype: Rtype, + ) -> Option> + where + N: CanonicalOrd + ToName, + D: RecordData, + { + self.rrsets().find(|rrset| { + rrset.rtype() == rtype + && rrset.owner().canonical_cmp(name) == Ordering::Equal + }) + } + /// Update the data of an existing record. /// /// Allowing records to be mutated in-place would not be safe because it @@ -374,10 +388,6 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { OwnerRrs { slice } } - pub fn into_inner(self) -> &'a [Record] { - self.slice - } - pub fn owner(&self) -> &N { self.slice[0].owner() } @@ -414,13 +424,18 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { //------------ Rrset --------------------------------------------------------- /// A set of records with the same owner name, class, and record type. +#[derive(Debug)] pub struct Rrset<'a, N, D> { slice: &'a [Record], } impl<'a, N, D> Rrset<'a, N, D> { - pub fn new(slice: &'a [Record]) -> Self { - Rrset { slice } + pub fn new(slice: &'a [Record]) -> Result { + if slice.is_empty() { + Err(SigningError::EmptyRecordSlice) + } else { + Ok(Rrset { slice }) + } } pub fn owner(&self) -> &N { @@ -450,14 +465,11 @@ impl<'a, N, D> Rrset<'a, N, D> { self.slice.iter() } + #[allow(clippy::len_without_is_empty)] pub fn len(&self) -> usize { self.slice.len() } - pub fn is_empty(&self) -> bool { - self.slice.is_empty() - } - pub fn as_slice(&self) -> &'a [Record] { self.slice } @@ -552,7 +564,9 @@ where } let (res, slice) = self.slice.split_at(end); self.slice = slice; - Some(Rrset::new(res)) + Some( + Rrset::new(res).expect("res is not empty so new should not fail"), + ) } } @@ -588,6 +602,8 @@ where } let (res, slice) = self.slice.split_at(end); self.slice = slice; - Some(Rrset::new(res)) + Some( + Rrset::new(res).expect("res is not empty so new should not fail"), + ) } } diff --git a/src/sign/signatures/mod.rs b/src/dnssec/sign/signatures/mod.rs similarity index 70% rename from src/sign/signatures/mod.rs rename to src/dnssec/sign/signatures/mod.rs index b2750ef45..17e3410ab 100644 --- a/src/sign/signatures/mod.rs +++ b/src/dnssec/sign/signatures/mod.rs @@ -1,3 +1,2 @@ //! Signature generation. pub mod rrsigs; -pub mod strategy; diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs new file mode 100644 index 000000000..27aeb986f --- /dev/null +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -0,0 +1,1237 @@ +//! DNSSEC RRSIG generation. +use core::convert::{AsRef, From}; +use core::fmt::Display; +use core::marker::Send; + +use std::boxed::Box; +use std::cmp::Ordering; +use std::fmt::Debug; +use std::vec::Vec; + +use octseq::builder::FromBuilder; +use octseq::{OctetsFrom, OctetsInto}; +use tracing::debug; + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::Rtype; +use crate::base::name::ToName; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::record::Record; +use crate::base::Name; +use crate::crypto::sign::SignRaw; +use crate::dnssec::sign::error::SigningError; +use crate::dnssec::sign::keys::signingkey::SigningKey; +use crate::dnssec::sign::records::{RecordsIter, Rrset}; +use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; +use crate::rdata::{Rrsig, ZoneRecordData}; + +//------------ GenerateRrsigConfig ------------------------------------------- + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct GenerateRrsigConfig { + pub inception: Timestamp, + + pub expiration: Timestamp, +} + +impl GenerateRrsigConfig { + /// Create a new object. + pub fn new(inception: Timestamp, expiration: Timestamp) -> Self { + Self { + inception, + expiration, + } + } +} + +//------------ sign_sorted_zone_records -------------------------------------- + +/// Generate RRSIG records for a collection of zone records. +/// +/// An implementation of [RFC 4035 section 2.2] for generating RRSIG RRs for a +/// zone. +/// +/// This function takes DNS records and signing keys and uses the signing keys +/// to generate and output RRSIG RRs that sign the input records per [RFC +/// 9364]. +/// +/// RRSIG RRs will **NOT** be generated for records: +/// - With RTYPE RRSIG, because [RFC 4035 section 2.2] states that _"An +/// RRSIG RR itself MUST NOT be signed"_. +/// - With RTYPE DNSKEY, CDS or CDNSKEY RR, because, depending on the +/// operational practice (see [RFC 6871]), it may be that these RRs should +/// not be signed using the same key as the rest of the records in the +/// zone. To sign DNSKEY, CDS and CDNSKEY RRs see the [`sign_rrset()`] +/// function. +/// +/// Note: +/// - The input records MUST be sorted according to [`CanonicalOrd`]. +/// - The order of the output records should not be relied upon. +/// +/// # Design rationale +/// +/// The restriction to limit signing to records not involved in the chain of +/// trust with the parent zone is imposed because there is considerable +/// variation and complexity in the strategies used to protect and roll the +/// keys used to sign records in a DNSSEC signed zone. +/// +/// It is common operational practice (see [RFC 6871]) to increase security by +/// using two separate keys to sign the zone. A Key Signing Key aka KSK is +/// used to sign the keys used to establish trust with the parent zone, and a +/// Zone Signing Key aka ZSK is used to sign the rest of the records in the +/// zone, with the KSK signing the ZSK. This allows the ZSK to be rolled +/// without needing to submit information about the new key to the parent zone +/// operator. +/// +/// Deciding which key to use to sign which records at a given time, +/// especially during key rolls, can be complex. Attempting to cover all +/// possible cases in this function would increase the complexity and +/// fragility and reduce flexibility. As such it is left to the caller to +/// ensure that this is done correctly and doing so also enables the caller to +/// have complete control over the key signing strategy used. +/// +/// [RFC 4035 section 2.2]: https://www.rfc-editor.org/rfc/rfc4035#section-2.2 +/// [RFC 6871]: https://www.rfc-editor.org/rfc/rfc6871 +/// [RFC 9364]: https://www.rfc-editor.org/rfc/rfc9364 +// TODO: Add mutable iterator based variant. +#[allow(clippy::type_complexity)] +pub fn sign_sorted_zone_records( + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, + keys: &[&SigningKey], + config: &GenerateRrsigConfig, +) -> Result>>, SigningError> +where + Inner: Debug + SignRaw, + N: ToName + + PartialEq + + Clone + + Debug + + Display + + Send + + CanonicalOrd + + From>, + Octs: AsRef<[u8]> + + Debug + + From> + + Send + + OctetsFrom> + + Clone + + FromBuilder + + From<&'static [u8]>, +{ + // The generated collection of RRSIG RRs that will be returned to the + // caller. + let mut rrsigs = Vec::new(); + + // A temporary scratch buffer used when generating signatures that can be + // allocated once and reused for each new signature that we generate. + let mut reusable_scratch = Vec::new(); + + // The owner name of a zone cut if we currently are at or below one. + let mut cut: Option = None; + + // Skip any glue records that sort earlier than the zone apex. + records.skip_before(apex_owner); + + // For all records + for owner_rrs in records { + // If the owner is out of zone, we have moved out of our zone and are + // done. + if !owner_rrs.is_in_zone(apex_owner) { + break; + } + + // If the owner is below a zone cut, we must ignore it. + if let Some(ref cut) = cut { + if owner_rrs.owner().ends_with(cut) { + continue; + } + } + + // A copy of the owner name. We’ll need it later. + let name = owner_rrs.owner().clone(); + + // If this owner is the parent side of a zone cut, we keep the owner + // name for later. This also means below that if `cut.is_some()` we + // are at the parent side of a zone. + cut = if owner_rrs.is_zone_cut(apex_owner) { + Some(name.clone()) + } else { + None + }; + + for rrset in owner_rrs.rrsets() { + if cut.is_some() { + // If we are at a zone cut, we only sign DS and NSEC records. + // NS records we must not sign and everything else shouldn’t + // be here, really. + if rrset.rtype() != Rtype::DS && rrset.rtype() != Rtype::NSEC + { + continue; + } + } else if (rrset.rtype() == Rtype::DNSKEY + || rrset.rtype() == Rtype::CDS + || rrset.rtype() == Rtype::CDNSKEY) + && name.canonical_cmp(apex_owner) == Ordering::Equal + { + // Ignore the DNSKEY, CDS, and CDNSKEY RRsets at the apex. + // Sign other DNSKEY, CDS, and CDNSKEY RRsets as other + // records. + // See RFC 7344 Section 4.1 for CDS and CDNSKEY. + continue; + } else { + // Otherwise we only ignore RRSIGs. + if rrset.rtype() == Rtype::RRSIG { + continue; + } + } + + for key in keys { + let inception = config.inception; + let expiration = config.expiration; + let rrsig_rr = sign_sorted_rrset_in( + key, + &rrset, + inception, + expiration, + &mut reusable_scratch, + )?; + rrsigs.push(rrsig_rr); + debug!( + "Signed {} RRSET at {} with keytag {}", + rrset.rtype(), + rrset.owner(), + key.dnskey().key_tag() + ); + } + } + } + + debug!( + "Returning {} RRSIG RRs from signature generation", + rrsigs.len(), + ); + + Ok(rrsigs) +} + +/// Generate `RRSIG` records for a given RRset. +/// +/// See [`sign_sorted_rrset_in()`]. +/// +/// If signing multiple RRsets, calling [`sign_sorted_rrset_in()`] directly +/// will be more efficient as you can allocate the scratch buffer once +/// and re-use it across multiple calls. +/// +/// This function will sort the RRset in canonical ordering prior to signing. +pub fn sign_rrset( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + inception: Timestamp, + expiration: Timestamp, +) -> Result>, SigningError> +where + N: ToName + Debug + Clone + From>, + D: Clone + Debug + RecordData + ComposeRecordData + CanonicalOrd, + Inner: Debug + SignRaw, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, +{ + let mut records = rrset.as_slice().to_vec(); + records + .sort_by(|a, b| a.as_ref().data().canonical_cmp(b.as_ref().data())); + let rrset = Rrset::new(&records) + .expect("records is not empty so new should not fail"); + + sign_sorted_rrset_in(key, &rrset, inception, expiration, &mut vec![]) +} + +/// Generate `RRSIG` records for a given RRset. +/// +/// This function generates an `RRSIG` record for the given RRset based on the +/// given signing key, according to the rules defined in [RFC 4034 section 3] +/// _"The RRSIG Resource Record"_, [RFC 4035 section 2.2] _"Including RRSIG +/// RRs in a Zone"_ and [RFC 6840 section 5.11] _"Mandatory Algorithm Rules"_. +/// +/// The RRset must be sorted in canonical ordering before calling this +/// function. +/// +/// No checks are done on the given signing key, any key with any algorithm, +/// apex owner and flags may be used to sign the given RRset. +/// +/// When signing multiple RRsets by calling this function multiple times, the +/// `scratch` buffer parameter can be allocated once and re-used for each call +/// to avoid needing to allocate the buffer for each call. +/// +/// [RFC 4034 section 3]: +/// https://www.rfc-editor.org/rfc/rfc4034.html#section-3 +/// [RFC 4035 section 2.2]: +/// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 +/// [RFC 6840 section 5.11]: +/// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 +pub fn sign_sorted_rrset_in( + key: &SigningKey, + rrset: &Rrset<'_, N, D>, + inception: Timestamp, + expiration: Timestamp, + scratch: &mut Vec, +) -> Result>, SigningError> +where + N: ToName + Clone + Debug + From>, + D: RecordData + Debug + ComposeRecordData + CanonicalOrd, + Inner: Debug + SignRaw, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, +{ + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "An RRSIG RR itself MUST NOT be signed" + if rrset.rtype() == Rtype::RRSIG { + return Err(SigningError::RrsigRrsMustNotBeSigned); + } + + if expiration < inception { + return Err(SigningError::InvalidSignatureValidityPeriod( + inception, expiration, + )); + } + + // RFC 4034 + // 3. The RRSIG Resource Record + // "The TTL value of an RRSIG RR MUST match the TTL value of the RRset + // it covers. This is an exception to the [RFC2181] rules for TTL + // values of individual RRs within a RRset: individual RRSIG RRs with + // the same owner name will have different TTL values if the RRsets + // they cover have different TTL values." + let rrsig = ProtoRrsig::new( + rrset.rtype(), + key.algorithm(), + rrset.owner().rrsig_label_count(), + rrset.ttl(), + expiration, + inception, + key.dnskey().key_tag(), + // The fns provided by `ToName` state in their RustDoc that they + // "Converts the name into a single, uncompressed name" which matches + // the RFC 4034 section 3.1.7 requirement that "A sender MUST NOT use + // DNS name compression on the Signer's Name field when transmitting a + // RRSIG RR.". + // + // TODO: However, is this inefficient? The RFC requires it to be + // SENT uncompressed, but doesn't ban storing it in compressed from? + // + // We don't need to make sure here that the signer name is in + // canonical form as required by RFC 4034 as the call to + // `compose_canonical()` below will take care of that. + key.owner().clone().into(), + ); + + scratch.clear(); + + rrsig.compose_canonical(scratch).unwrap(); + for record in rrset.iter() { + record.compose_canonical(scratch).unwrap(); + } + let signature = key.raw_secret_key().sign_raw(&*scratch)?; + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + debug_assert!( + (rrsig.labels() as usize) < rrset.owner().iter_labels().count() + ); + + Ok(Record::new( + rrset.owner().clone(), + rrset.class(), + rrset.ttl(), + rrsig, + )) +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + use core::str::FromStr; + use pretty_assertions::assert_eq; + + use crate::base::iana::SecurityAlgorithm; + use crate::base::Serial; + use crate::crypto::sign::{KeyPair, SignError, Signature}; + use crate::dnssec::sign::records::SortedRecords; + use crate::dnssec::sign::test_util; + use crate::dnssec::sign::test_util::*; + use crate::rdata::dnssec::Timestamp; + use crate::rdata::Dnskey; + use crate::zonetree::StoredName; + + use super::*; + use crate::zonetree::types::StoredRecordData; + use rand::Rng; + + const TEST_INCEPTION: u32 = 0; + const TEST_EXPIRATION: u32 = 100; + + #[test] + fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "For example, "www.example.com." has a Labels field value of 3" + // We can use any class as RRSIGs are class independent. + let mut records = + SortedRecords::::default(); + records.insert(mk_a_rr("www.example.com.")).unwrap(); + let rrset = Rrset::new(&records).expect("records is not empty"); + + let rrsig_rr = + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + let rrsig = rrsig_rr.data(); + + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // "For each authoritative RRset in a signed zone, there MUST be at + // least one RRSIG record that meets the following requirements: + // + // o The RRSIG owner name is equal to the RRset owner name. + assert_eq!(rrsig_rr.owner(), rrset.owner()); + // + // o The RRSIG class is equal to the RRset class. + assert_eq!(rrsig_rr.class(), rrset.class()); + // + // o The RRSIG Type Covered field is equal to the RRset type. + // + assert_eq!(rrsig.type_covered(), rrset.rtype()); + // o The RRSIG Original TTL field is equal to the TTL of the + // RRset. + // + assert_eq!(rrsig.original_ttl(), rrset.ttl()); + // o The RRSIG RR's TTL is equal to the TTL of the RRset. + // + assert_eq!(rrsig_rr.ttl(), rrset.ttl()); + // o The RRSIG Labels field is equal to the number of labels in + // the RRset owner name, not counting the null root label and + // not counting the leftmost label if it is a wildcard. + assert_eq!(rrsig.labels(), 3); + // o The RRSIG Signer's Name field is equal to the name of the + // zone containing the RRset. + // + assert_eq!(rrsig.signer_name(), &apex_owner); + // o The RRSIG Algorithm, Signer's Name, and Key Tag fields + // identify a zone key DNSKEY record at the zone apex." + // ^^^ This is outside the control of the rrset_sign() function. + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + assert!((rrsig.labels() as usize) < rrset.owner().label_count()); + } + + #[test] + fn sign_rrset_with_wildcard() { + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // ""*.example.com." has a Labels field value of 2" + let mut records = + SortedRecords::::default(); + records.insert(mk_a_rr("*.example.com.")).unwrap(); + let rrset = Rrset::new(&records).expect("records is not empty"); + + let rrsig_rr = + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + let rrsig = rrsig_rr.data(); + + assert_eq!(rrsig.labels(), 2); + } + + #[test] + fn sign_rrset_must_not_sign_rrsigs() { + // RFC 4035 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "An RRSIG RR itself MUST NOT be signed" + + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); + let (inception, expiration) = + (Timestamp::from(0), Timestamp::from(0)); + let dnskey = key.dnskey().convert(); + + let mut records = + SortedRecords::::default(); + records + .insert(mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)) + .unwrap(); + let rrset = Rrset::new(&records).expect("records is not empty"); + + let res = sign_rrset(&key, &rrset, inception, expiration); + assert!(matches!(res, Err(SigningError::RrsigRrsMustNotBeSigned))); + } + + #[test] + fn sign_rrset_check_validity_period_handling() { + // RFC 4034 + // 3.1.5. Signature Expiration and Inception Fields + // ... + // "The Signature Expiration and Inception field values specify a + // date and time in the form of a 32-bit unsigned number of seconds + // elapsed since 1 January 1970 00:00:00 UTC, ignoring leap + // seconds, in network byte order. The longest interval that can + // be expressed by this format without wrapping is approximately + // 136 years. An RRSIG RR can have an Expiration field value that + // is numerically smaller than the Inception field value if the + // expiration field value is near the 32-bit wrap-around point or + // if the signature is long lived. Because of this, all + // comparisons involving these fields MUST use "Serial number + // arithmetic", as defined in [RFC1982]. As a direct consequence, + // the values contained in these fields cannot refer to dates more + // than 68 years in either the past or the future." + + let apex_owner = Name::root(); + let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); + + let mut records = + SortedRecords::::default(); + records.insert(mk_a_rr("any.")).unwrap(); + let rrset = Rrset::new(&records).expect("records is not empty"); + + fn calc_timestamps( + start: u32, + duration: u32, + ) -> (Timestamp, Timestamp) { + let start_serial = Serial::from(start); + let end = start_serial.add(duration).into_int(); + (Timestamp::from(start), Timestamp::from(end)) + } + + // Good: Expiration > Inception. + let (inception, expiration) = calc_timestamps(5, 5); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Good: Expiration == Inception. + let (inception, expiration) = calc_timestamps(10, 0); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Bad: Expiration < Inception. + let (expiration, inception) = calc_timestamps(5, 10); + let res = sign_rrset(&key, &rrset, inception, expiration); + assert!(matches!( + res, + Err(SigningError::InvalidSignatureValidityPeriod(_, _)) + )); + + // Good: Expiration > Inception with Expiration near wrap around + // point. + let (inception, expiration) = calc_timestamps(u32::MAX - 10, 10); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Good: Expiration > Inception with Inception near wrap around point. + let (inception, expiration) = calc_timestamps(0, 10); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Good: Expiration > Inception with Exception crossing the wrap + // around point. + let (inception, expiration) = calc_timestamps(u32::MAX - 10, 20); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Good: Expiration - Inception == 68 years. + let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; + let (inception, expiration) = + calc_timestamps(0, sixty_eight_years_in_secs); + sign_rrset(&key, &rrset, inception, expiration).unwrap(); + + // Bad: Expiration - Inception > 68 years. + // + // I add a rather large amount (A year) because it's unclear where the + // boundary is from the approximate text in the quoted RFC. I think + // it's at 2^31 - 1 so from that you can see how much we need to add + // to cross the boundary: + // + // ``` + // 68 years = 68 * 365 * 24 * 60 * 60 = 2144448000 + // 2^31 - 1 = 2147483647 + // 69 years = 69 * 365 * 24 * 60 * 60 = 2175984000 + // ``` + // + // But as the RFC refers to "dates more than 68 years" a value of 69 + // years is fine to test with. + let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; + let one_year_in_secs = 365 * 24 * 60 * 60; + + // We can't use calc_timestamps() here because the underlying call to + // Serial::add() panics if the value to add is > 2^31 - 1. + // + // calc_timestamps(0, sixty_eight_years_in_secs + one_year_in_secs); + // + // But Timestamp doesn't care, we can construct those just fine. + // However when sign_rrset() compares the Timestamp inception and + // expiration values it will fail because the PartialOrd impl is + // implemented in terms of Serial which detects the wrap around. + // + // I think this is all good because RFC 4034 doesn't prevent creation + // and storage of an arbitrary 32-bit unsigned number of seconds as + // the inception or expiration value, it only mandates that "all + // comparisons involving these fields MUST use "Serial number + // arithmetic", as defined in [RFC1982]" + let (inception, expiration) = ( + Timestamp::from(0), + Timestamp::from(sixty_eight_years_in_secs + one_year_in_secs), + ); + let res = sign_rrset(&key, &rrset, inception, expiration); + assert!(matches!( + res, + Err(SigningError::InvalidSignatureValidityPeriod(_, _)) + )); + } + + #[test] + fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { + let apex = Name::from_str("example.").unwrap(); + let records = + SortedRecords::::default(); + let no_keys: [&SigningKey; 0] = []; + + sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &no_keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + } + + #[test] + fn generate_rrsigs_without_keys_generates_no_rrsigs() { + let apex = Name::from_str("example.").unwrap(); + let mut records = SortedRecords::default(); + records.insert(mk_a_rr("example.")).unwrap(); + let no_keys: [&SigningKey; 0] = []; + + let rrsigs = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &no_keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + assert!(rrsigs.is_empty()); + } + + #[test] + fn generate_rrsigs_for_partial_zone_at_apex() { + generate_rrsigs_for_partial_zone("example.", "example."); + } + + #[test] + fn generate_rrsigs_for_partial_zone_beneath_apex() { + generate_rrsigs_for_partial_zone("example.", "in.example."); + } + + fn generate_rrsigs_for_partial_zone(zone_apex: &str, record_owner: &str) { + // This is an example of generating RRSIGs for something other than a + // full zone, in this case just for an A record. This test + // deliberately does not include a SOA record as the zone is partial. + let apex = Name::from_str(zone_apex).unwrap(); + let mut records = SortedRecords::default(); + records.insert(mk_a_rr(record_owner)).unwrap(); + + // Prepare a zone signing key and a key signing key. + let keys = [&mk_dnssec_signing_key(true)]; + let dnskey = keys[0].dnskey().convert(); + + // Generate RRSIGs. Use the default signing config and thus also the + // DefaultSigningKeyUsageStrategy which will honour the purpose of the + // key when selecting a key to use for signing DNSKEY RRs or other + // zone RRs. We supply the zone apex because we are not supplying an + // entire zone complete with SOA. + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + // Check the generated RRSIG records + let expected_labels = mk_name(record_owner).rrsig_label_count(); + assert_eq!(generated_records.len(), 1); + assert_eq!( + generated_records[0], + mk_rrsig_rr( + record_owner, + Rtype::A, + expected_labels, + zone_apex, + &dnskey + ) + ); + } + + #[test] + fn generate_rrsigs_ignores_records_outside_the_zone() { + let apex = Name::from_str("example.").unwrap(); + let mut records = SortedRecords::default(); + records.extend([ + mk_soa_rr("example.", "mname.", "rname."), + mk_a_rr("in_zone.example."), + mk_a_rr("out_of_zone."), + ]); + + // Prepare a zone signing key and a key signing key. + let keys = [&mk_dnssec_signing_key(true)]; + let dnskey = keys[0].dnskey().convert(); + + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + // Check the generated RRSIG records + assert_eq!( + generated_records, + [ + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr( + "in_zone.example.", + Rtype::A, + 2, + "example.", + &dnskey + ), + ] + ); + } + + #[test] + fn generate_rrsigs_ignores_glue_records() { + let apex = Name::from_str("example.").unwrap(); + let mut records = SortedRecords::default(); + records.extend([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Prepare a zone signing key and a key signing key. + let keys = [&mk_dnssec_signing_key(true)]; + let dnskey = keys[0].dnskey().convert(); + + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + // Check the generated RRSIG records + assert_eq!( + generated_records, + [ + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr( + "in_zone.example.", + Rtype::A, + 2, + "example.", + &dnskey + ), + ] + ); + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_csk() { + let keys = [&mk_dnssec_signing_key(true)]; + let cfg = GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_only_zsk() { + let keys = [&mk_dnssec_signing_key(false)]; + let cfg = GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); + } + + fn generate_rrsigs_for_complete_zone( + keys: &[&SigningKey], + _ksk_idx: usize, + zsk_idx: usize, + cfg: &GenerateRrsigConfig, + ) -> Result<(), SigningError> { + // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A + let zonefile = include_bytes!( + "../../../../test-data/zonefiles/rfc4035-appendix-A.zone" + ); + + // Load the zone to generate RRSIGs for. + let apex = Name::from_str("example.").unwrap(); + let records = bytes_to_records(&zonefile[..]); + + // Generate DNSKEYs and RRSIGs. + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + keys, + cfg, + )?; + + let dnskeys = keys + .iter() + .map(|k| k.dnskey().convert()) + .collect::>(); + + let zsk = &dnskeys[zsk_idx]; + + // Check the generated records. + let mut rrsig_iter = generated_records.iter(); + + // The records should be in a fixed canonical order because the input + // records must be in canonical order, with the exception of the added + // DNSKEY RRs which will be ordered in the order in the supplied + // collection of keys to sign with. While we tell users of + // sign_sorted_zone_records() not to rely on the order of the output, + // we assume that we know what that order is for this test, but would + // have to update this test if that order later changes. + // + // We check each record explicitly by index because assert_eq() on an + // array of objects that includes Rrsig produces hard to read output + // due to the large RRSIG signature bytes being printed one byte per + // line. It also wouldn't support dynamically checking for certain + // records based on the signing configuration used. + + // NOTE: As we only invoked sign_sorted_zone_records() and not + // generate_nsecs() there will not be any RRSIGs covering NSEC + // records. + + // -- example. + + // RRSIG records should have been generated for the zone apex records, + // one RRSIG per ZSK used. + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::MX, 1, "example.", zsk) + ); + + // -- a.example. + + // NOTE: Per RFC 4035 there is NOT an RRSIG for a.example NS because: + // + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 + // 2.2. Including RRSIG RRs in a Zone + // ... + // "The NS RRset that appears at the zone apex name MUST be signed, + // but the NS RRsets that appear at delegation points (that is, the + // NS RRsets in the parent zone that delegate the name to the child + // zone's name servers) MUST NOT be signed." + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", zsk) + ); + + // -- ns1.a.example. + // ns2.a.example. + + // NOTE: Per RFC 4035 there is NOT an RRSIG for ns1.a.example A + // or ns2.a.example because: + // + // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. + // Including RRSIG RRs in a Zone "For each authoritative RRset in a + // signed zone, there MUST be at least one RRSIG record..." ... AND + // ... "Glue address RRsets associated with delegations MUST NOT be + // signed." + // + // ns1.a.example is part of the a.example zone which was delegated + // above and so we are not authoritative for it. + // + // Further, ns1.a.example A is a glue record because a.example NS + // refers to it by name but in order for a recursive resolver to + // follow the delegation to the child zones' nameservers it has to + // know their IP address, and in this case the nameserver name falls + // inside the child zone so strictly speaking only the child zone is + // authoritative for it, yet the resolver can't ask the child zone + // nameserver unless it knows its IP address, hence the need for glue + // in the parent zone. + + // -- ai.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", zsk) + ); + + // -- b.example. + + // NOTE: There is no RRSIG for b.example NS for the same reason that + // there is no RRSIG for a.example. + // + // Also, there is no RRSIG for b.example A because b.example is + // delegated and thus we are not authoritative for records in that + // zone. + + // -- ns1.b.example. + // ns2.b.example. + + // NOTE: There is no RRSIG for ns1.b.example or ns2.b.example for + // the same reason that there are no RRSIGs ofr ns1.a.example or + // ns2.a.example, as described above. + + // -- ns1.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", zsk) + ); + + // -- ns2.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", zsk) + ); + + // -- *.w.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", zsk) + ); + + // -- x.w.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", zsk) + ); + + // -- x.y.w.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", zsk) + ); + + // -- xx.example. + + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", zsk) + ); + assert_eq!( + *rrsig_iter.next().unwrap(), + mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", zsk) + ); + + // No other records should have been generated. + + assert!(rrsig_iter.next().is_none()); + + Ok(()) + } + + #[test] + fn generate_rrsigs_for_complete_zone_with_multiple_zsks() { + let apex = "example."; + + let apex_owner = Name::from_str(apex).unwrap(); + let mut records = SortedRecords::default(); + records.extend([ + mk_soa_rr(apex, "some.mname.", "some.rname."), + mk_ns_rr(apex, "ns.example."), + mk_a_rr("ns.example."), + ]); + + let keys = + [&mk_dnssec_signing_key(false), &mk_dnssec_signing_key(false)]; + + let zsk1 = keys[0].dnskey().convert(); + let zsk2 = keys[1].dnskey().convert(); + + let generated_records = sign_sorted_zone_records( + &apex_owner, + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + // Check the generated records. + assert_eq!(generated_records.len(), 6); + + // Filter out the records one by one until there should be none left. + let it = generated_records + .iter() + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk1) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk2) + }) + .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk1)) + .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk2)) + .filter(|&rr| { + rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk1) + }) + .filter(|&rr| { + rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk2) + }); + + let mut it = it.inspect(|rr| { + eprintln!( + "Warning: Unexpected RRSIG RRs remaining after filtering: {} {} => {:?}", + rr.owner(), + rr.rtype(), + rr.data(), + ); + }); + + assert!(it.next().is_none()); + } + + #[test] + fn generate_rrsigs_for_already_signed_zone() { + let keys = [&mk_dnssec_signing_key(true)]; + + let dnskey = keys[0].dnskey().convert(); + + let apex = Name::from_str("example.").unwrap(); + let mut records = SortedRecords::default(); + records.extend([ + // -- example. + mk_soa_rr("example.", "some.mname.", "some.rname."), + mk_ns_rr("example.", "ns.example."), + mk_dnskey_rr("example.", &dnskey), + mk_nsec_rr("example", "ns.example.", "SOA NS DNSKEY NSEC RRSIG"), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey), + // -- ns.example. + mk_a_rr("ns.example."), + mk_nsec_rr("ns.example", "example.", "A NSEC RRSIG"), + mk_rrsig_rr("ns.example.", Rtype::A, 1, "example.", &dnskey), + mk_rrsig_rr("ns.example.", Rtype::NSEC, 1, "example.", &dnskey), + ]); + + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + &keys, + &GenerateRrsigConfig::new( + TEST_INCEPTION.into(), + TEST_EXPIRATION.into(), + ), + ) + .unwrap(); + + // Check the generated records. + let mut iter = generated_records.iter(); + + // The records should be in a fixed canonical order because the input + // records must be in canonical order, with the exception of the added + // DNSKEY RRs which will be ordered in the order in the supplied + // collection of keys to sign with. While we tell users of + // sign_sorted_zone_records() not to rely on the order of the output, + // we assume that we know what that order is for this test, but would + // have to update this test if that order later changes. + // + // We check each record explicitly by index because assert_eq() on an + // array of objects that includes Rrsig produces hard to read output + // due to the large RRSIG signature bytes being printed one byte per + // line. It also wouldn't support dynamically checking for certain + // records based on the signing configuration used. + + // -- example. + + // RRSIG records should have been generated for the zone apex records, + // one RRSIG per ZSK used, even if RRSIG RRs already exist. + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey) + ); + + // -- ns.example. + + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("ns.example.", Rtype::A, 2, "example.", &dnskey) + ); + assert_eq!( + *iter.next().unwrap(), + mk_rrsig_rr("ns.example.", Rtype::NSEC, 2, "example.", &dnskey) + ); + + // No other records should have been generated. + + assert!(iter.next().is_none()); + } + + //------------ Helper fns ------------------------------------------------ + + fn mk_dnssec_signing_key(make_ksk: bool) -> SigningKey { + // Note: The flags value has no impact on the role the key will play + // in signing, that is instead determined by its designated purpose + // AND the SigningKeyUsageStrategy in use. + let flags = match make_ksk { + true => 257, + false => 256, + }; + + SigningKey::new( + Name::from_str("example").unwrap(), + flags, + TestKey::default(), + ) + } + + fn mk_dnskey_rr( + name: &str, + dnskey: &Dnskey, + ) -> Record + where + R: From>, + { + test_util::mk_dnskey_rr( + name, + dnskey.flags(), + dnskey.algorithm(), + dnskey.public_key(), + ) + } + + fn mk_rrsig_rr( + name: &str, + covered_rtype: Rtype, + labels: u8, + signer_name: &str, + dnskey: &Dnskey, + ) -> Record + where + R: From>, + { + test_util::mk_rrsig_rr( + name, + covered_rtype, + &dnskey.algorithm(), + labels, + TEST_EXPIRATION, + TEST_INCEPTION, + dnskey.key_tag(), + signer_name, + TEST_SIGNATURE, + ) + } + + //------------ TestKey --------------------------------------------------- + + const TEST_SIGNATURE_RAW: [u8; 64] = [0u8; 64]; + const TEST_SIGNATURE: Bytes = Bytes::from_static(&TEST_SIGNATURE_RAW); + + #[derive(Debug)] + struct TestKey([u8; 32]); + + impl SignRaw for TestKey { + fn algorithm(&self) -> SecurityAlgorithm { + SecurityAlgorithm::ED25519 + } + + fn dnskey(&self) -> Dnskey> { + let flags = 0; + Dnskey::new(flags, 3, SecurityAlgorithm::ED25519, self.0.to_vec()) + .unwrap() + } + + fn sign_raw(&self, _data: &[u8]) -> Result { + Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) + } + } + + impl Default for TestKey { + fn default() -> Self { + Self(rand::thread_rng().gen()) + } + } +} diff --git a/src/sign/test_util/mod.rs b/src/dnssec/sign/test_util/mod.rs similarity index 80% rename from src/sign/test_util/mod.rs rename to src/dnssec/sign/test_util/mod.rs index f57c45999..b36c39d4c 100644 --- a/src/sign/test_util/mod.rs +++ b/src/dnssec/sign/test_util/mod.rs @@ -1,28 +1,27 @@ use core::str::FromStr; -use std::fmt::Debug; use std::io::Read; use std::string::ToString; use std::vec::Vec; use bytes::Bytes; -use crate::base::iana::{Class, SecAlg}; -use crate::base::name::FlattenInto; -use crate::base::{Name, Record, Rtype, Serial, ToName, Ttl}; +use crate::base::iana::{Class, SecurityAlgorithm}; +use crate::base::name::{FlattenInto, Name}; +use crate::base::{Record, Rtype, Serial, ToName, Ttl}; +use crate::dnssec::common::nsec3_hash; +use crate::dnssec::sign::denial::nsec3::mk_hashed_nsec3_owner_name; use crate::rdata::dnssec::{RtypeBitmap, Timestamp}; use crate::rdata::nsec3::OwnerHash; use crate::rdata::{ Aaaa, Dnskey, Ns, Nsec, Nsec3, Nsec3param, Rrsig, Soa, A, }; -use crate::sign::denial::nsec3::mk_hashed_nsec3_owner_name; use crate::utils::base32; -use crate::validate::nsec3_hash; use crate::zonefile::inplace::{Entry, Zonefile}; use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; -use super::denial::nsec3::{GenerateNsec3Config, Nsec3HashProvider}; +use super::denial::nsec3::GenerateNsec3Config; use super::records::SortedRecords; pub(crate) const TEST_TTL: Ttl = Ttl::from_secs(3600); @@ -66,7 +65,7 @@ where pub(crate) fn mk_dnskey_rr( owner: &str, flags: u16, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, public_key: &Bytes, ) -> Record where @@ -110,44 +109,40 @@ where mk_record(owner, Nsec::new(next_name, types).into()) } -pub(crate) fn mk_nsec3param_rr( +pub(crate) fn mk_nsec3param_rr( owner: &str, - cfg: &GenerateNsec3Config, + cfg: &GenerateNsec3Config, ) -> Record where - HP: Nsec3HashProvider, - N: FromStr + ToName + From>, R: From>, { mk_record(owner, cfg.params.clone().into()) } -pub(crate) fn mk_nsec3_rr( +pub(crate) fn mk_nsec3_rr( apex_owner: &str, owner: &str, next_owner: &str, types: &str, - cfg: &GenerateNsec3Config, + cfg: &GenerateNsec3Config, ) -> Record where - HP: Nsec3HashProvider, - N: FromStr + ToName + From>, - ::Err: Debug, R: From>, { - let hashed_owner_name = mk_hashed_nsec3_owner_name( - &N::from_str(owner).unwrap(), - cfg.params.hash_algorithm(), - cfg.params.iterations(), - cfg.params.salt(), - &N::from_str(apex_owner).unwrap(), - ) - .unwrap() - .to_name::() - .to_string(); + let hashed_owner_name = + mk_hashed_nsec3_owner_name::, Bytes, Bytes>( + &Name::from_str(owner).unwrap(), + cfg.params.hash_algorithm(), + cfg.params.iterations(), + cfg.params.salt(), + &Name::from_str(apex_owner).unwrap(), + ) + .unwrap() + .to_name::() + .to_string(); - let next_owner_hash_octets: Vec = nsec3_hash( - N::from_str(next_owner).unwrap(), + let next_owner_hash_octets: Vec = nsec3_hash::>, _, _>( + Name::from_str(next_owner).unwrap(), cfg.params.hash_algorithm(), cfg.params.iterations(), cfg.params.salt(), @@ -177,16 +172,13 @@ where ) } -pub(crate) fn mk_precalculated_nsec3_rr( +pub(crate) fn mk_precalculated_nsec3_rr( owner: &str, next_owner: &str, types: &str, - cfg: &GenerateNsec3Config, + cfg: &GenerateNsec3Config, ) -> Record where - HP: Nsec3HashProvider, - N: FromStr + ToName + From>, - ::Err: Debug, R: From>, { let mut builder = RtypeBitmap::::builder(); @@ -213,7 +205,7 @@ where pub(crate) fn mk_rrsig_rr( owner: &str, covered_rtype: Rtype, - algorithm: &SecAlg, + algorithm: &SecurityAlgorithm, labels: u8, expiration: u32, inception: u32, diff --git a/src/sign/traits.rs b/src/dnssec/sign/traits.rs similarity index 62% rename from src/sign/traits.rs rename to src/dnssec/sign/traits.rs index c09ede84d..50f53f17f 100644 --- a/src/sign/traits.rs +++ b/src/dnssec/sign/traits.rs @@ -1,7 +1,7 @@ //! Signing related traits. //! //! This module provides traits which can be used to simplify invocation of -//! [`crate::sign::sign_zone()`] for [`Record`] collection types. +//! [`crate::dnssec::sign::sign_zone()`] for [`Record`] collection types. use core::convert::From; use core::fmt::{Debug, Display}; use core::iter::Extend; @@ -16,65 +16,22 @@ use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; use octseq::OctetsFrom; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::SecAlg; use crate::base::name::ToName; use crate::base::record::Record; use crate::base::Name; -use crate::rdata::ZoneRecordData; -use crate::sign::denial::nsec3::Nsec3HashProvider; -use crate::sign::error::{SignError, SigningError}; -use crate::sign::keys::keymeta::DesignatedSigningKey; -use crate::sign::records::{ +use crate::crypto::sign::SignRaw; +use crate::dnssec::sign::error::SigningError; +use crate::dnssec::sign::keys::SigningKey; +use crate::dnssec::sign::records::{ DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, }; -use crate::sign::sign_zone; -use crate::sign::signatures::rrsigs::generate_rrsigs; -use crate::sign::signatures::rrsigs::GenerateRrsigConfig; -use crate::sign::signatures::rrsigs::RrsigRecords; -use crate::sign::signatures::strategy::RrsigValidityPeriodStrategy; -use crate::sign::signatures::strategy::SigningKeyUsageStrategy; -use crate::sign::SigningConfig; -use crate::sign::{PublicKeyBytes, SignableZoneInOut, Signature}; - -//----------- SignRaw -------------------------------------------------------- - -/// Low-level signing functionality. -/// -/// Types that implement this trait own a private key and can sign arbitrary -/// information (in the form of slices of bytes). -/// -/// 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 { - /// The signature algorithm used. - /// - /// See [RFC 8624, section 3.1] for IETF implementation recommendations. - /// - /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 - fn algorithm(&self) -> SecAlg; - - /// The raw public key. - /// - /// This can be used to verify produced signatures. It must use the same - /// algorithm as returned by [`algorithm()`]. - /// - /// [`algorithm()`]: Self::algorithm() - fn raw_public_key(&self) -> PublicKeyBytes; - - /// Sign the given bytes. - /// - /// # Errors - /// - /// See [`SignError`] for a discussion of possible failure cases. To the - /// greatest extent possible, the implementation should check for failure - /// cases beforehand and prevent them (e.g. when the keypair is created). - fn sign_raw(&self, data: &[u8]) -> Result; -} +use crate::dnssec::sign::sign_zone; +use crate::dnssec::sign::signatures::rrsigs::sign_sorted_zone_records; +use crate::dnssec::sign::signatures::rrsigs::GenerateRrsigConfig; +use crate::dnssec::sign::SignableZoneInOut; +use crate::dnssec::sign::SigningConfig; +use crate::rdata::dnssec::Timestamp; +use crate::rdata::{Rrsig, ZoneRecordData}; //------------ SortedExtend -------------------------------------------------- @@ -152,21 +109,19 @@ where /// ``` /// # use domain::base::{Name, Record, Serial, Ttl}; /// # use domain::base::iana::Class; -/// # use domain::sign::crypto::common; -/// # use domain::sign::crypto::common::GenerateParams; -/// # use domain::sign::crypto::common::KeyPair; -/// # use domain::sign::keys::SigningKey; -/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # use domain::crypto::common; +/// # use domain::crypto::sign::{generate, GenerateParams, KeyPair}; +/// # use domain::dnssec::sign::keys::SigningKey; +/// # let (sec_bytes, pub_bytes) = generate(GenerateParams::Ed25519, +/// # 256).unwrap(); /// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); /// # let root = Name::>::root(); /// # let key = SigningKey::new(root.clone(), 257, key_pair); /// use domain::rdata::{rfc1035::Soa, ZoneRecordData}; /// use domain::rdata::dnssec::Timestamp; -/// use domain::sign::keys::DnssecSigningKey; -/// use domain::sign::records::SortedRecords; -/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; -/// use domain::sign::traits::SignableZone; -/// use domain::sign::SigningConfig; +/// use domain::dnssec::sign::records::SortedRecords; +/// use domain::dnssec::sign::traits::SignableZone; +/// use domain::dnssec::sign::SigningConfig; /// /// // Create a sorted collection of records. /// // @@ -186,22 +141,22 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); +/// records.insert(Record::new(root.clone(), Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// /// // Assign signature validity period and operator intent to the keys. -/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); -/// let keys = [DnssecSigningKey::new_csk(key)]; +/// let keys = [&key]; /// /// // Create a signing configuration. -/// let mut signing_config = SigningConfig::default(validity); +/// let signing_config = SigningConfig::new(Default::default(), 0.into(), 0.into()); /// /// // Then generate the records which when added to the zone make it signed. /// let mut signer_generated_records = SortedRecords::default(); /// /// records.sign_zone( -/// &mut signing_config, +/// &root, +/// &signing_config, /// &keys, /// &mut signer_generated_records).unwrap(); /// ``` @@ -210,8 +165,9 @@ where pub trait SignableZone: Deref>]> where - N: Clone + ToName + From> + PartialEq + Ord + Hash, + N: Clone + Debug + ToName + From> + PartialEq + Ord + Hash, Octs: Clone + + Debug + FromBuilder + From<&'static [u8]> + Send @@ -227,38 +183,32 @@ where /// DNSSEC sign an unsigned zone using the given configuration and keys. /// /// This function is a convenience wrapper around calling - /// [`crate::sign::sign_zone()`] function with enum variant + /// [`crate::dnssec::sign::sign_zone()`] function with enum variant /// [`SignableZoneInOut::SignInto`]. - fn sign_zone( + fn sign_zone( &self, - signing_config: &mut SigningConfig< - N, - Octs, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP, - >, - signing_keys: &[DSK], + apex_owner: &N, + signing_config: &SigningConfig, + signing_keys: &[&SigningKey], out: &mut T, ) -> Result<(), SigningError> where - DSK: DesignatedSigningKey, - HP: Nsec3HashProvider, - Inner: SignRaw, + Inner: Debug + SignRaw, N: Display + Send + CanonicalOrd, ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy + Clone, T: Deref>]> + SortedExtend + ?Sized, Self: Sized, { let in_out = SignableZoneInOut::new_into(self, out); - sign_zone(in_out, signing_config, signing_keys) + sign_zone::( + apex_owner, + in_out, + signing_config, + signing_keys, + ) } } @@ -269,6 +219,7 @@ where impl SignableZone for T where N: Clone + + Debug + ToName + From> + PartialEq @@ -277,6 +228,7 @@ where + Ord + Hash, Octs: Clone + + Debug + FromBuilder + From<&'static [u8]> + Send @@ -304,21 +256,21 @@ where /// ``` /// # use domain::base::{Name, Record, Serial, Ttl}; /// # use domain::base::iana::Class; -/// # use domain::sign::crypto::common; -/// # use domain::sign::crypto::common::GenerateParams; -/// # use domain::sign::crypto::common::KeyPair; -/// # use domain::sign::keys::SigningKey; -/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # use domain::crypto::common; +/// # use domain::crypto::sign::{generate, GenerateParams, KeyPair}; +/// # use domain::dnssec::sign::keys::SigningKey; +/// # let (sec_bytes, pub_bytes) = generate( +/// # GenerateParams::Ed25519, +/// # 256).unwrap(); /// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); /// # let root = Name::>::root(); /// # let key = SigningKey::new(root.clone(), 257, key_pair); /// use domain::rdata::{rfc1035::Soa, ZoneRecordData}; /// use domain::rdata::dnssec::Timestamp; -/// use domain::sign::keys::DnssecSigningKey; -/// use domain::sign::records::SortedRecords; -/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; -/// use domain::sign::traits::SignableZoneInPlace; -/// use domain::sign::SigningConfig; +/// use domain::dnssec::sign::records::SortedRecords; +/// use domain::dnssec::sign::traits::SignableZoneInPlace; +/// use domain::dnssec::sign::SigningConfig; +/// use domain::dnssec::sign::records::DefaultSorter; /// /// // Create a sorted collection of records. /// // @@ -330,7 +282,7 @@ where /// let mut records = SortedRecords::default(); /// /// // Insert records into the collection. Just a dummy SOA for this example. -/// let soa = ZoneRecordData::Soa(Soa::new( +/// let soa = ZoneRecordData::, _>::Soa(Soa::new( /// root.clone(), /// root.clone(), /// Serial::now(), @@ -338,27 +290,28 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); +/// records.insert(Record::new(root.clone(), Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// /// // Assign signature validity period and operator intent to the keys. -/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); -/// let keys = [DnssecSigningKey::new_csk(key)]; +/// let keys = [&key]; /// /// // Create a signing configuration. -/// let mut signing_config = SigningConfig::default(validity); +/// let signing_config: SigningConfig, DefaultSorter> = +/// SigningConfig::new(Default::default(), 0.into(), 0.into()); /// /// // Then sign the zone in-place. -/// records.sign_zone(&mut signing_config, &keys).unwrap(); +/// records.sign_zone(&root, &signing_config, &keys).unwrap(); /// ``` /// /// [`sign_zone()`]: SignableZoneInPlace::sign_zone pub trait SignableZoneInPlace: SignableZone + SortedExtend where - N: Clone + ToName + From> + PartialEq + Ord + Hash, + N: Clone + Debug + ToName + From> + PartialEq + Ord + Hash, Octs: Clone + + Debug + FromBuilder + From<&'static [u8]> + Send @@ -373,34 +326,28 @@ where /// and keys. /// /// This function is a convenience wrapper around calling - /// [`crate::sign::sign_zone()`] function with enum variant + /// [`crate::dnssec::sign::sign_zone()`] function with enum variant /// [`SignableZoneInOut::SignInPlace`]. - fn sign_zone( + fn sign_zone( &mut self, - signing_config: &mut SigningConfig< - N, - Octs, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP, - >, - signing_keys: &[DSK], + apex_owner: &N, + signing_config: &SigningConfig, + signing_keys: &[&SigningKey], ) -> Result<(), SigningError> where - DSK: DesignatedSigningKey, - HP: Nsec3HashProvider, - Inner: SignRaw, + Inner: Debug + SignRaw, N: Display + Send + CanonicalOrd, ::Builder: Truncate, <::Builder as OctetsBuilder>::AppendError: Debug, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy + Clone, { let in_out = SignableZoneInOut::<_, _, Self, _, _>::new_in_place(self); - sign_zone(in_out, signing_config, signing_keys) + sign_zone::( + apex_owner, + in_out, + signing_config, + signing_keys, + ) } } @@ -415,6 +362,7 @@ where impl SignableZoneInPlace for T where N: Clone + + Debug + ToName + From> + PartialEq @@ -423,6 +371,7 @@ where + Hash + Ord, Octs: Clone + + Debug + FromBuilder + From<&'static [u8]> + Send @@ -452,41 +401,47 @@ where /// # Example /// /// ``` -/// # use domain::base::Name; +/// # use domain::base::{Name, Record, Ttl}; /// # use domain::base::iana::Class; -/// # use domain::sign::crypto::common; -/// # use domain::sign::crypto::common::GenerateParams; -/// # use domain::sign::crypto::common::KeyPair; -/// # use domain::sign::keys::{DnssecSigningKey, SigningKey}; -/// # use domain::sign::records::{Rrset, SortedRecords}; -/// # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); +/// # use domain::crypto::common; +/// # use domain::crypto::sign::{generate, GenerateParams, KeyPair}; +/// # use domain::dnssec::sign::keys::{SigningKey}; +/// # use domain::dnssec::sign::records::{Rrset, SortedRecords}; +/// # use domain::rdata::{A, ZoneRecordData}; +/// # use domain::zonetree::StoredName; +/// # use std::str::FromStr; +/// # let (sec_bytes, pub_bytes) = generate( +/// # GenerateParams::Ed25519, +/// # 256).unwrap(); /// # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); /// # let root = Name::>::root(); /// # let key = SigningKey::new(root, 257, key_pair); -/// # let keys = [DnssecSigningKey::from(key)]; +/// # let keys = [&key]; /// # let mut records = SortedRecords::default(); -/// use domain::sign::traits::Signable; -/// use domain::sign::signatures::strategy::DefaultSigningKeyUsageStrategy as KeyStrat; -/// use domain::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; +/// # records.insert(Record::new(Name::from_str("www.example.com.") +/// .unwrap(), Class::IN, Ttl::from_secs(3600), +/// ZoneRecordData::A(A::from_str("1.2.3.4").unwrap()))).unwrap(); +/// use domain::dnssec::sign::traits::Signable; /// let apex = Name::>::root(); -/// let rrset = Rrset::new(&records); -/// let validity = FixedRrsigValidityPeriodStrategy::from((0, 0)); -/// let generated_records = rrset.sign::(&apex, &keys, validity).unwrap(); +/// let rrset = Rrset::new(&records).expect("records is not empty"); +/// let generated_records = rrset.sign(&apex, &keys, 0.into(), 0.into()).unwrap(); /// ``` -pub trait Signable +pub trait Signable where N: ToName + CanonicalOrd + Send + + Debug + Display + Clone + PartialEq + From>, - Inner: SignRaw, + Inner: Debug + SignRaw, Octs: From> + From<&'static [u8]> + FromBuilder + Clone + + Debug + OctetsFrom> + Send, ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, @@ -496,38 +451,36 @@ where /// Generate `RRSIG` records for this type. /// - /// This function is a thin wrapper around [`generate_rrsigs()`]. + /// This function is a thin wrapper around [`sign_sorted_zone_records()`]. #[allow(clippy::type_complexity)] - fn sign( + fn sign( &self, - expected_apex: &N, - keys: &[DSK], - rrsig_validity_period_strategy: ValidityStrat, - ) -> Result, SigningError> - where - DSK: DesignatedSigningKey, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - { - let rrsig_config = - GenerateRrsigConfig::::new( - rrsig_validity_period_strategy, - ) - .with_zone_apex(expected_apex.clone()); + apex_owner: &N, + keys: &[&SigningKey], + inception: Timestamp, + expiration: Timestamp, + ) -> Result>>, SigningError> { + let rrsig_config = GenerateRrsigConfig::new(inception, expiration); - generate_rrsigs(self.owner_rrs(), keys, &rrsig_config) + sign_sorted_zone_records( + apex_owner, + self.owner_rrs(), + keys, + &rrsig_config, + ) } } //--- impl Signable for Rrset -impl Signable +impl Signable for Rrset<'_, N, ZoneRecordData> where - Inner: SignRaw, + Inner: Debug + SignRaw, N: From> + PartialEq + Clone + + Debug + Display + Send + CanonicalOrd @@ -536,6 +489,7 @@ where + Send + OctetsFrom> + Clone + + Debug + From<&'static [u8]> + From>, ::Builder: AsRef<[u8]> + AsMut<[u8]> + EmptyBuilder, diff --git a/src/validator/anchor.rs b/src/dnssec/validator/anchor.rs similarity index 97% rename from src/validator/anchor.rs rename to src/dnssec/validator/anchor.rs index ef6a70042..db3cbfebf 100644 --- a/src/validator/anchor.rs +++ b/src/dnssec/validator/anchor.rs @@ -1,6 +1,7 @@ //! Create DNSSEC trust anchors. use super::context::Error; +use crate::base::iana::Class; use crate::base::name::{Chain, Name, ToName}; use crate::base::{Record, RelativeName}; use crate::rdata::ZoneRecordData; @@ -118,6 +119,7 @@ impl TrustAnchors { let mut new_self = Self(Vec::new()); let mut zonefile = Zonefile::new(); + zonefile.set_default_class(Class::IN); zonefile.extend_from_slice(str); zonefile.extend_from_slice(b"\n"); for e in zonefile { @@ -137,6 +139,7 @@ impl TrustAnchors { /// zonefile format. pub fn add_u8(&mut self, str: &[u8]) -> Result<(), Error> { let mut zonefile = Zonefile::new(); + zonefile.set_default_class(Class::IN); zonefile.extend_from_slice(str); zonefile.extend_from_slice("\n".as_bytes()); for e in zonefile { diff --git a/src/dnssec/validator/base.rs b/src/dnssec/validator/base.rs new file mode 100644 index 000000000..85eb7170f --- /dev/null +++ b/src/dnssec/validator/base.rs @@ -0,0 +1,751 @@ +//! Base functions for DNSSEC validation. + +use crate::base::iana::{DigestAlgorithm, SecurityAlgorithm}; +use crate::base::rdata::ComposeRecordData; +use crate::base::wire::{Compose, Composer}; +use crate::base::{CanonicalOrd, Name, Record, RecordData, ToName}; +use crate::crypto::common::{ + AlgorithmError, Digest, DigestBuilder, DigestType, PublicKey, +}; +use crate::dep::octseq::builder::with_infallible; +use crate::rdata::{Dnskey, Rrsig}; + +use bytes::Bytes; + +use std::vec::Vec; + +//------------ Dnskey -------------------------------------------------------- + +/// Extensions for DNSKEY record type. +pub trait DnskeyExt { + /// Calculates a digest from DNSKEY. + /// + /// See [RFC 4034, Section 5.1.4]: + /// + /// ```text + /// 5.1.4. The Digest Field + /// The digest is calculated by concatenating the canonical form of the + /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, + /// and then applying the digest algorithm. + /// + /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); + /// + /// "|" denotes concatenation + /// + /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. + /// ``` + /// + /// [RFC 4034, Section 5.1.4]: https://tools.ietf.org/html/rfc4034#section-5.1.4 + fn digest( + &self, + name: &N, + algorithm: DigestAlgorithm, + ) -> Result; + + /// Return the key size in bits or an error if the algorithm is not + /// supported. + fn key_size(&self) -> Result; +} + +impl DnskeyExt for Dnskey +where + Octets: AsRef<[u8]>, +{ + /// Calculates a digest from DNSKEY. + /// + /// See [RFC 4034, Section 5.1.4]: + /// + /// ```text + /// 5.1.4. The Digest Field + /// The digest is calculated by concatenating the canonical form of the + /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, + /// and then applying the digest algorithm. + /// + /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); + /// + /// "|" denotes concatenation + /// + /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. + /// ``` + /// + /// [RFC 4034, Section 5.1.4]: https://tools.ietf.org/html/rfc4034#section-5.1.4 + fn digest( + &self, + name: &N, + algorithm: DigestAlgorithm, + ) -> Result { + let mut buf: Vec = Vec::new(); + with_infallible(|| { + name.compose_canonical(&mut buf)?; + self.compose_canonical_rdata(&mut buf) + }); + + let mut ctx = match algorithm { + DigestAlgorithm::SHA1 => DigestBuilder::new(DigestType::Sha1), + DigestAlgorithm::SHA256 => DigestBuilder::new(DigestType::Sha256), + DigestAlgorithm::SHA384 => DigestBuilder::new(DigestType::Sha384), + _ => { + return Err(AlgorithmError::Unsupported); + } + }; + + ctx.update(&buf); + Ok(ctx.finish()) + } + + /// The size of this key, in bits. + /// + /// For RSA keys, this measures the size of the public modulus. For all + /// other algorithms, it is the size of the fixed-width public key. + fn key_size(&self) -> Result { + match self.algorithm() { + SecurityAlgorithm::RSASHA1 + | SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + | SecurityAlgorithm::RSASHA256 + | SecurityAlgorithm::RSASHA512 => { + let data = self.public_key().as_ref(); + // The exponent length is encoded as 1 or 3 bytes. + let (exp_len, off) = if data[0] != 0 { + (data[0] as usize, 1) + } else { + // NOTE: Even though this is the extended encoding of the length, + // a user could choose to put a length less than 256 over here. + let exp_len = + u16::from_be_bytes(data[1..3].try_into().unwrap()); + (exp_len as usize, 3) + }; + let n = &data[off + exp_len..]; + Ok(n.len() * 8 - n[0].leading_zeros() as usize) + } + SecurityAlgorithm::ECDSAP256SHA256 + | SecurityAlgorithm::ECDSAP384SHA384 => { + // ECDSA public keys have two points. + Ok(self.public_key().as_ref().len() / 2 * 8) + } + SecurityAlgorithm::ED25519 | SecurityAlgorithm::ED448 => { + // EdDSA public key sizes are measured in encoded form. + Ok(self.public_key().as_ref().len() * 8) + } + _ => Err(AlgorithmError::Unsupported), + } + } +} + +/// Return whether a DigestAlgorithm is supported or not. +// This needs to match the digests supported in digest. +pub fn supported_digest(d: &DigestAlgorithm) -> bool { + *d == DigestAlgorithm::SHA1 + || *d == DigestAlgorithm::SHA256 + || *d == DigestAlgorithm::SHA384 +} + +//------------ Rrsig --------------------------------------------------------- + +/// Extensions for DNSKEY record type. +pub trait RrsigExt { + /// Compose the signed data according to [RC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2). + /// + /// ```text + /// Once the RRSIG RR has met the validity requirements described in + /// Section 5.3.1, the validator has to reconstruct the original signed + /// data. The original signed data includes RRSIG RDATA (excluding the + /// Signature field) and the canonical form of the RRset. Aside from + /// being ordered, the canonical form of the RRset might also differ from + /// the received RRset due to DNS name compression, decremented TTLs, or + /// wildcard expansion. + /// ``` + fn signed_data( + &self, + buf: &mut B, + records: &mut [impl AsRef>], + ) -> Result<(), B::AppendError> + where + D: RecordData + CanonicalOrd + ComposeRecordData + Sized; + + /// Return if records are expanded for a wildcard according to the + /// information in this signature. + fn wildcard_closest_encloser( + &self, + rr: &Record, + ) -> Option> + where + N: ToName; + + /// Attempt to use the cryptographic signature to authenticate the signed data, and thus authenticate the RRSET. + /// The signed data is expected to be calculated as per [RFC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2). + /// + /// [RFC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2): + /// ```text + /// 5.3.3. Checking the Signature + /// + /// Once the resolver has validated the RRSIG RR as described in Section + /// 5.3.1 and reconstructed the original signed data as described in + /// Section 5.3.2, the validator can attempt to use the cryptographic + /// signature to authenticate the signed data, and thus (finally!) + /// authenticate the RRset. + /// + /// The Algorithm field in the RRSIG RR identifies the cryptographic + /// algorithm used to generate the signature. The signature itself is + /// contained in the Signature field of the RRSIG RDATA, and the public + /// key used to verify the signature is contained in the Public Key field + /// of the matching DNSKEY RR(s) (found in Section 5.3.1). [RFC4034] + /// provides a list of algorithm types and provides pointers to the + /// documents that define each algorithm's use. + /// ``` + fn verify_signed_data( + &self, + dnskey: &Dnskey>, + signed_data: &impl AsRef<[u8]>, + ) -> Result<(), AlgorithmError>; +} + +impl, TN: ToName> RrsigExt for Rrsig { + fn signed_data( + &self, + buf: &mut B, + records: &mut [impl AsRef>], + ) -> Result<(), B::AppendError> + where + D: RecordData + CanonicalOrd + ComposeRecordData + Sized, + { + // signed_data = RRSIG_RDATA | RR(1) | RR(2)... where + // "|" denotes concatenation + // RRSIG_RDATA is the wire format of the RRSIG RDATA fields + // with the Signature field excluded and the Signer's Name + // in canonical form. + self.type_covered().compose(buf)?; + self.algorithm().compose(buf)?; + self.labels().compose(buf)?; + self.original_ttl().compose(buf)?; + self.expiration().compose(buf)?; + self.inception().compose(buf)?; + self.key_tag().compose(buf)?; + self.signer_name().compose_canonical(buf)?; + + // The set of all RR(i) is sorted into canonical order. + // See https://tools.ietf.org/html/rfc4034#section-6.3 + records.sort_by(|a, b| { + a.as_ref().data().canonical_cmp(b.as_ref().data()) + }); + + // RR(i) = name | type | class | OrigTTL | RDATA length | RDATA + for rr in records.iter().map(|r| r.as_ref()) { + // Handle expanded wildcards as per [RFC4035, Section 5.3.2] + // (https://tools.ietf.org/html/rfc4035#section-5.3.2). + let rrsig_labels = usize::from(self.labels()); + let fqdn = rr.owner(); + // Subtract the root label from count as the algorithm doesn't + // accomodate that. + let fqdn_labels = fqdn.iter_labels().count() - 1; + if rrsig_labels < fqdn_labels { + // name = "*." | the rightmost rrsig_label labels of the fqdn + buf.append_slice(b"\x01*")?; + match fqdn + .to_cow() + .iter_suffixes() + .nth(fqdn_labels - rrsig_labels) + { + Some(name) => name.compose_canonical(buf)?, + None => fqdn.compose_canonical(buf)?, + }; + } else { + fqdn.compose_canonical(buf)?; + } + + rr.rtype().compose(buf)?; + rr.class().compose(buf)?; + self.original_ttl().compose(buf)?; + rr.data().compose_canonical_len_rdata(buf)?; + } + Ok(()) + } + + fn wildcard_closest_encloser( + &self, + rr: &Record, + ) -> Option> + where + N: ToName, + { + // Handle expanded wildcards as per [RFC4035, Section 5.3.2] + // (https://tools.ietf.org/html/rfc4035#section-5.3.2). + let rrsig_labels = usize::from(self.labels()); + let fqdn = rr.owner(); + // Subtract the root label from count as the algorithm doesn't + // accomodate that. + let fqdn_labels = fqdn.iter_labels().count() - 1; + if rrsig_labels < fqdn_labels { + // name = "*." | the rightmost rrsig_label labels of the fqdn + Some( + match fqdn + .to_cow() + .iter_suffixes() + .nth(fqdn_labels - rrsig_labels) + { + Some(name) => Name::from_octets(Bytes::copy_from_slice( + name.as_octets(), + )) + .unwrap(), + None => fqdn.to_bytes(), + }, + ) + } else { + None + } + } + + fn verify_signed_data( + &self, + dnskey: &Dnskey>, + signed_data: &impl AsRef<[u8]>, + ) -> Result<(), AlgorithmError> { + let signature = self.signature().as_ref(); + let signed_data = signed_data.as_ref(); + + // Caller needs to ensure that the signature matches the key, but enforce the algorithm match + if self.algorithm() != dnskey.algorithm() { + return Err(AlgorithmError::InvalidData); + } + + let public_key = PublicKey::from_dnskey(dnskey)?; + public_key.verify(signed_data, signature) + } +} + +/// Report whether an algorithm is supported or not. +// This needs to match the algorithms supported in signed_data. +pub fn supported_algorithm(a: &SecurityAlgorithm) -> bool { + *a == SecurityAlgorithm::RSASHA1 + || *a == SecurityAlgorithm::RSASHA1_NSEC3_SHA1 + || *a == SecurityAlgorithm::RSASHA256 + || *a == SecurityAlgorithm::RSASHA512 + || *a == SecurityAlgorithm::ECDSAP256SHA256 +} + +//============ Test ========================================================== + +#[cfg(test)] +#[cfg(feature = "std")] +mod test { + use super::*; + use crate::base::iana::{Class, Rtype, SecurityAlgorithm}; + use crate::base::scan::{IterScanner, Scanner}; + use crate::base::Ttl; + use crate::dnssec::common::parse_from_bind; + use crate::rdata::dnssec::Timestamp; + use crate::rdata::{Mx, ZoneRecordData}; + use crate::utils::base64; + + use std::str::FromStr; + + type Dnskey = crate::rdata::Dnskey>; + type Ds = crate::rdata::Ds>; + type Name = crate::base::name::Name>; + type Rrsig = crate::rdata::Rrsig, Name>; + + // Returns current root KSK/ZSK for testing (2048b) + fn root_pubkey() -> (Dnskey, Dnskey) { + let ksk = base64::decode::>( + "\ + AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/\ + 4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMt\ + NROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwV\ + N8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK\ + 6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+c\ + n8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=", + ) + .unwrap(); + let zsk = base64::decode::>( + "\ + AwEAAeVDC34GZILwsQJy97K2Fst4P3XYZrXLyrkausYzSqEjSUulgh+iLgH\ + g0y7FIF890+sIjXsk7KLJUmCOWfYWPorNKEOKLk5Zx/4M6D3IHZE3O3m/Ea\ + hrc28qQzmTLxiMZAW65MvR2UO3LxVtYOPBEBiDgAQD47x2JLsJYtavCzNL5\ + WiUk59OgvHmDqmcC7VXYBhK8V8Tic089XJgExGeplKWUt9yyc31ra1swJX5\ + 1XsOaQz17+vyLVH8AZP26KvKFiZeoRbaq6vl+hc8HQnI2ug5rA2zoz3MsSQ\ + BvP1f/HvqsWxLqwXXKyDD1QM639U+XzVB8CYigyscRP22QCnwKIU=", + ) + .unwrap(); + ( + Dnskey::new(257, 3, SecurityAlgorithm::RSASHA256, ksk).unwrap(), + Dnskey::new(256, 3, SecurityAlgorithm::RSASHA256, zsk).unwrap(), + ) + } + + // Returns the current net KSK/ZSK for testing (1024b) + fn net_pubkey() -> (Dnskey, Dnskey) { + let ksk = base64::decode::>( + "AQOYBnzqWXIEj6mlgXg4LWC0HP2n8eK8XqgHlmJ/69iuIHsa1TrHDG6TcOra/pyeGKwH0nKZhTmXSuUFGh9BCNiwVDuyyb6OBGy2Nte9Kr8NwWg4q+zhSoOf4D+gC9dEzg0yFdwT0DKEvmNPt0K4jbQDS4Yimb+uPKuF6yieWWrPYYCrv8C9KC8JMze2uT6NuWBfsl2fDUoV4l65qMww06D7n+p7RbdwWkAZ0fA63mXVXBZF6kpDtsYD7SUB9jhhfLQE/r85bvg3FaSs5Wi2BaqN06SzGWI1DHu7axthIOeHwg00zxlhTpoYCH0ldoQz+S65zWYi/fRJiyLSBb6JZOvn", + ) + .unwrap(); + let zsk = base64::decode::>( + "AQPW36Zs2vsDFGgdXBlg8RXSr1pSJ12NK+u9YcWfOr85we2z5A04SKQlIfyTK37dItGFcldtF7oYwPg11T3R33viKV6PyASvnuRl8QKiLk5FfGUDt1sQJv3S/9wT22Le1vnoE/6XFRyeb8kmJgz0oQB1VAO9b0l6Vm8KAVeOGJ+Qsjaq0O0aVzwPvmPtYm/i3qoAhkaMBUpg6RrF5NKhRyG3", + ) + .unwrap(); + ( + Dnskey::new(257, 3, SecurityAlgorithm::RSASHA256, ksk).unwrap(), + Dnskey::new(256, 3, SecurityAlgorithm::RSASHA256, zsk).unwrap(), + ) + } + + #[test] + fn dnskey_digest() { + let (dnskey, _) = root_pubkey(); + let owner = Name::root(); + let expected = Ds::new( + 20326, + SecurityAlgorithm::RSASHA256, + DigestAlgorithm::SHA256, + base64::decode::>( + "4G1EuAuPHTmpXAsNfGXQhFjogECbvGg0VxBCN8f47I0=", + ) + .unwrap(), + ) + .unwrap(); + assert_eq!( + dnskey + .digest(&owner, DigestAlgorithm::SHA256) + .unwrap() + .as_ref(), + expected.digest() + ); + } + + #[test] + fn rrsig_verify_rsa_sha256() { + // Test 2048b long key + let (ksk, zsk) = root_pubkey(); + let rrsig = Rrsig::new( + Rtype::DNSKEY, + SecurityAlgorithm::RSASHA256, + 0, + Ttl::from_secs(172800), + 1560211200.into(), + 1558396800.into(), + 20326, + Name::root(), + base64::decode::>( + "otBkINZAQu7AvPKjr/xWIEE7+SoZtKgF8bzVynX6bfJMJuPay8jPvNmwXkZOdSoYlvFp0bk9JWJKCh8y5uoNfMFkN6OSrDkr3t0E+c8c0Mnmwkk5CETH3Gqxthi0yyRX5T4VlHU06/Ks4zI+XAgl3FBpOc554ivdzez8YCjAIGx7XgzzooEb7heMSlLc7S7/HNjw51TPRs4RxrAVcezieKCzPPpeWBhjE6R3oiSwrl0SBD4/yplrDlr7UHs/Atcm3MSgemdyr2sOoOUkVQCVpcj3SQQezoD2tCM7861CXEQdg5fjeHDtz285xHt5HJpA5cOcctRo4ihybfow/+V7AQ==", + ) + .unwrap() + ).unwrap(); + rrsig_verify_dnskey(ksk, zsk, rrsig); + + // Test 1024b long key + let (ksk, zsk) = net_pubkey(); + let rrsig = Rrsig::new( + Rtype::DNSKEY, + SecurityAlgorithm::RSASHA256, + 1, + Ttl::from_secs(86400), + Timestamp::from_str("20210921162830").unwrap(), + Timestamp::from_str("20210906162330").unwrap(), + 35886, + "net.".parse::().unwrap(), + base64::decode::>( + "j1s1IPMoZd0mbmelNVvcbYNe2tFCdLsLpNCnQ8xW6d91ujwPZ2yDlc3lU3hb+Jq3sPoj+5lVgB7fZzXQUQTPFWLF7zvW49da8pWuqzxFtg6EjXRBIWH5rpEhOcr+y3QolJcPOTx+/utCqt2tBKUUy3LfM6WgvopdSGaryWdwFJPW7qKHjyyLYxIGx5AEuLfzsA5XZf8CmpUheSRH99GRZoIB+sQzHuelWGMQ5A42DPvOVZFmTpIwiT2QaIpid4nJ7jNfahfwFrCoS+hvqjK9vktc5/6E/Mt7DwCQDaPt5cqDfYltUitQy+YA5YP5sOhINChYadZe+2N80OA+RKz0mA==", + ) + .unwrap() + ).unwrap(); + rrsig_verify_dnskey(ksk, zsk, rrsig.clone()); + + // Test that 512b short RSA DNSKEY is not supported (too short) + let data = base64::decode::>( + "AwEAAcFcGsaxxdgiuuGmCkVImy4h99CqT7jwY3pexPGcnUFtR2Fh36BponcwtkZ4cAgtvd4Qs8PkxUdp6p/DlUmObdk=", + ) + .unwrap(); + + let short_key = + Dnskey::new(256, 3, SecurityAlgorithm::RSASHA256, data).unwrap(); + let err = rrsig + .verify_signed_data(&short_key, &vec![0; 100]) + .unwrap_err(); + assert_eq!(err, AlgorithmError::Unsupported); + } + + #[test] + fn rrsig_verify_ecdsap256_sha256() { + let (ksk, zsk) = ( + Dnskey::new( + 257, + 3, + SecurityAlgorithm::ECDSAP256SHA256, + base64::decode::>( + "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAe\ + F+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", + ) + .unwrap(), + ) + .unwrap(), + Dnskey::new( + 256, + 3, + SecurityAlgorithm::ECDSAP256SHA256, + base64::decode::>( + "oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IR\ + d8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==", + ) + .unwrap(), + ) + .unwrap(), + ); + + let owner = Name::from_str("cloudflare.com.").unwrap(); + let rrsig = Rrsig::new( + Rtype::DNSKEY, + SecurityAlgorithm::ECDSAP256SHA256, + 2, + Ttl::from_secs(3600), + 1560314494.into(), + 1555130494.into(), + 2371, + owner, + base64::decode::>( + "8jnAGhG7O52wmL065je10XQztRX1vK8P8KBSyo71Z6h5wAT9+GFxKBaE\ + zcJBLvRmofYFDAhju21p1uTfLaYHrg==", + ) + .unwrap(), + ) + .unwrap(); + rrsig_verify_dnskey(ksk, zsk, rrsig); + } + + #[test] + fn rrsig_verify_ed25519() { + let (ksk, zsk) = ( + Dnskey::new( + 257, + 3, + SecurityAlgorithm::ED25519, + base64::decode::>( + "m1NELLVVQKl4fHVn/KKdeNO0PrYKGT3IGbYseT8XcKo=", + ) + .unwrap(), + ) + .unwrap(), + Dnskey::new( + 256, + 3, + SecurityAlgorithm::ED25519, + base64::decode::>( + "2tstZAjgmlDTePn0NVXrAHBJmg84LoaFVxzLl1anjGI=", + ) + .unwrap(), + ) + .unwrap(), + ); + + let owner = + Name::from_octets(Vec::from(b"\x07ED25519\x02nl\x00".as_ref())) + .unwrap(); + let rrsig = Rrsig::new( + Rtype::DNSKEY, + SecurityAlgorithm::ED25519, + 2, + Ttl::from_secs(3600), + 1559174400.into(), + 1557360000.into(), + 45515, + owner, + base64::decode::>( + "hvPSS3E9Mx7lMARqtv6IGiw0NE0uz0mZewndJCHTkhwSYqlasUq7KfO5\ + QdtgPXja7YkTaqzrYUbYk01J8ICsAA==", + ) + .unwrap(), + ) + .unwrap(); + rrsig_verify_dnskey(ksk, zsk, rrsig); + } + + #[test] + fn rrsig_verify_generic_type() { + let (ksk, zsk) = root_pubkey(); + let rrsig = Rrsig::new( + Rtype::DNSKEY, + SecurityAlgorithm::RSASHA256, + 0, + Ttl::from_secs(172800), + 1560211200.into(), + 1558396800.into(), + 20326, + Name::root(), + base64::decode::>( + "otBkINZAQu7AvPKjr/xWIEE7+SoZtKgF8bzVynX6bfJMJuPay8jPvNmwXkZ\ + OdSoYlvFp0bk9JWJKCh8y5uoNfMFkN6OSrDkr3t0E+c8c0Mnmwkk5CETH3Gq\ + xthi0yyRX5T4VlHU06/Ks4zI+XAgl3FBpOc554ivdzez8YCjAIGx7XgzzooE\ + b7heMSlLc7S7/HNjw51TPRs4RxrAVcezieKCzPPpeWBhjE6R3oiSwrl0SBD4\ + /yplrDlr7UHs/Atcm3MSgemdyr2sOoOUkVQCVpcj3SQQezoD2tCM7861CXEQ\ + dg5fjeHDtz285xHt5HJpA5cOcctRo4ihybfow/+V7AQ==", + ) + .unwrap(), + ) + .unwrap(); + + let mut records: Vec, Name>>> = + [&ksk, &zsk] + .iter() + .cloned() + .map(|x| { + let data = ZoneRecordData::from(x.clone()); + Record::new( + rrsig.signer_name().clone(), + Class::IN, + Ttl::from_secs(0), + data, + ) + }) + .collect(); + + let signed_data = { + let mut buf = Vec::new(); + rrsig.signed_data(&mut buf, records.as_mut_slice()).unwrap(); + Bytes::from(buf) + }; + + assert!(rrsig.verify_signed_data(&ksk, &signed_data).is_ok()); + } + + #[test] + fn rrsig_verify_wildcard() { + let key = Dnskey::new( + 256, + 3, + SecurityAlgorithm::RSASHA1, + base64::decode::>( + "AQOy1bZVvpPqhg4j7EJoM9rI3ZmyEx2OzDBVrZy/lvI5CQePxX\ + HZS4i8dANH4DX3tbHol61ek8EFMcsGXxKciJFHyhl94C+NwILQd\ + zsUlSFovBZsyl/NX6yEbtw/xN9ZNcrbYvgjjZ/UVPZIySFNsgEY\ + vh0z2542lzMKR4Dh8uZffQ==", + ) + .unwrap(), + ) + .unwrap(); + let rrsig = Rrsig::new( + Rtype::MX, + SecurityAlgorithm::RSASHA1, + 2, + Ttl::from_secs(3600), + Timestamp::from_str("20040509183619").unwrap(), + Timestamp::from_str("20040409183619").unwrap(), + 38519, + Name::from_str("example.").unwrap(), + base64::decode::>( + "OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2f9AMN1CytCd10cYI\ + SAxfAdvXSZ7xujKAtPbctvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhq\ + e2XTjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw4kX18MMR34i8lC\ + 36SR5xBni8vHI=", + ) + .unwrap(), + ) + .unwrap(); + let record = Record::new( + Name::from_str("a.z.w.example.").unwrap(), + Class::IN, + Ttl::from_secs(3600), + Mx::new(1, Name::from_str("ai.example.").unwrap()), + ); + let signed_data = { + let mut buf = Vec::new(); + rrsig.signed_data(&mut buf, &mut [record]).unwrap(); + Bytes::from(buf) + }; + + // Test that the key matches RRSIG + assert_eq!(key.key_tag(), rrsig.key_tag()); + + // Test verifier + assert_eq!(rrsig.verify_signed_data(&key, &signed_data), Ok(())); + } + + fn rrsig_verify_dnskey(ksk: Dnskey, zsk: Dnskey, rrsig: Rrsig) { + let mut records: Vec<_> = [&ksk, &zsk] + .iter() + .cloned() + .map(|x| { + Record::new( + rrsig.signer_name().clone(), + Class::IN, + Ttl::from_secs(0), + x.clone(), + ) + }) + .collect(); + let signed_data = { + let mut buf = Vec::new(); + rrsig.signed_data(&mut buf, records.as_mut_slice()).unwrap(); + Bytes::from(buf) + }; + + // Test that the KSK is sorted after ZSK key + assert_eq!(ksk.key_tag(), rrsig.key_tag()); + assert_eq!(ksk.key_tag(), records[1].data().key_tag()); + + // Test verifier + assert!(rrsig.verify_signed_data(&ksk, &signed_data).is_ok()); + assert!(rrsig.verify_signed_data(&zsk, &signed_data).is_err()); + } + + #[test] + fn dnskey_digest_unsupported() { + let (dnskey, _) = root_pubkey(); + let owner = Name::root(); + assert!(dnskey.digest(&owner, DigestAlgorithm::GOST).is_err()); + } + + const KEYS: &[(SecurityAlgorithm, u16, usize)] = &[ + (SecurityAlgorithm::RSASHA1, 439, 2048), + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, 22204, 2048), + (SecurityAlgorithm::RSASHA256, 60616, 2048), + (SecurityAlgorithm::ECDSAP256SHA256, 42253, 256), + (SecurityAlgorithm::ECDSAP384SHA384, 33566, 384), + (SecurityAlgorithm::ED25519, 56037, 256), + (SecurityAlgorithm::ED448, 7379, 456), + ]; + + #[test] + fn key_size() { + for &(algorithm, key_tag, key_size) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = parse_from_bind::>(&data).unwrap(); + assert_eq!(key.data().key_size(), Ok(key_size)); + } + } + + #[test] + fn digest() { + for &(algorithm, key_tag, _) in KEYS { + let name = + format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let key = parse_from_bind::>(&data).unwrap(); + + // Scan the DS record from the file. + let path = format!("test-data/dnssec-keys/K{}.ds", name); + let data = std::fs::read_to_string(path).unwrap(); + let mut scanner = IterScanner::new(data.split_ascii_whitespace()); + let _ = scanner.scan_name().unwrap(); + let _ = Class::scan(&mut scanner).unwrap(); + assert_eq!(Rtype::scan(&mut scanner).unwrap(), Rtype::DS); + let ds = Ds::scan(&mut scanner).unwrap(); + + let key_ds = Ds::new( + key.data().key_tag(), + key.data().algorithm(), + ds.digest_type(), + key.data() + .digest(key.owner(), ds.digest_type()) + .unwrap() + .as_ref() + .to_vec(), + ) + .unwrap(); + + assert_eq!(key_ds, ds); + } + } +} diff --git a/src/validator/context.rs b/src/dnssec/validator/context.rs similarity index 99% rename from src/validator/context.rs rename to src/dnssec/validator/context.rs index d9e44ba1a..20e034f55 100644 --- a/src/validator/context.rs +++ b/src/dnssec/validator/context.rs @@ -5,6 +5,7 @@ //! or evaluated results. use super::anchor::{TrustAnchor, TrustAnchors}; +use super::base::{supported_algorithm, supported_digest, DnskeyExt}; use super::group::{Group, GroupSet, SigCache, ValidatedGroup}; use super::nsec::{ cached_nsec3_hash, nsec3_for_nodata, nsec3_for_nodata_wildcard, @@ -35,8 +36,6 @@ use crate::net::client::request::{ }; use crate::rdata::{AllRecordData, Dnskey, Ds, ZoneRecordData}; use crate::utils::config::DefMinMax; -use crate::validate::DnskeyExt; -use crate::validate::{supported_algorithm, supported_digest}; use crate::zonefile::inplace; use bytes::Bytes; use moka::future::Cache; @@ -2286,7 +2285,7 @@ where //----------- Error ---------------------------------------------------------- /// Various errors that can be returned by function in the -/// [validator](crate::validator) module. +/// [validator](crate::dnssec::validator) module. #[derive(Clone, Debug)] pub enum Error { /// Badly formed DNS message. diff --git a/src/validator/group.rs b/src/dnssec/validator/group.rs similarity index 99% rename from src/validator/group.rs rename to src/dnssec/validator/group.rs index 85b9e2407..626e7cb70 100644 --- a/src/validator/group.rs +++ b/src/dnssec/validator/group.rs @@ -17,15 +17,15 @@ use crate::base::name::ToName; use crate::base::opt::exterr::ExtendedError; use crate::base::rdata::ComposeRecordData; use crate::base::{Name, ParsedName, ParsedRecord, Record, Rtype, Ttl}; +use crate::crypto::common::{DigestBuilder, DigestType}; use crate::dep::octseq::builder::with_infallible; use crate::dep::octseq::{Octets, OctetsFrom}; +use crate::dnssec::validator::base::RrsigExt; use crate::net::client::request::{RequestMessage, SendRequest}; use crate::rdata::dnssec::Timestamp; use crate::rdata::{AllRecordData, Dnskey, Rrsig}; -use crate::validate::RrsigExt; use bytes::Bytes; use moka::future::Cache; -use ring::digest; use std::cmp::{max, min}; use std::fmt::Debug; use std::slice::Iter; @@ -651,13 +651,13 @@ impl Group { let mut buf: Vec = Vec::new(); with_infallible(|| key.compose_canonical_rdata(&mut buf)); - let mut ctx = digest::Context::new(&digest::SHA256); + let mut ctx = DigestBuilder::new(DigestType::Sha256); ctx.update(&buf); let key_hash = ctx.finish(); let mut buf: Vec = Vec::new(); with_infallible(|| sig.data().compose_canonical_rdata(&mut buf)); - let mut ctx = digest::Context::new(&digest::SHA256); + let mut ctx = DigestBuilder::new(DigestType::Sha256); ctx.update(&buf); let sig_hash = ctx.finish(); diff --git a/src/validator/mod.rs b/src/dnssec/validator/mod.rs similarity index 90% rename from src/validator/mod.rs rename to src/dnssec/validator/mod.rs index af70e18f9..33bf13353 100644 --- a/src/validator/mod.rs +++ b/src/dnssec/validator/mod.rs @@ -1,6 +1,16 @@ // Validator -#![cfg(feature = "unstable-validator")] +#![cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") +))] +#![cfg_attr( + docsrs, + doc(cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ))) +)] //! This module provides a DNSSEC validator as described in RFCs //! [4033](https://www.rfc-editor.org/info/rfc4033), @@ -18,6 +28,9 @@ //! method [validate_msg()](context::ValidationContext::validate_msg()) to //! validate a reply message. //! +//! Low-level operations for computing the hash of a DNSKEY or verifying an +//! RRSIG record are provided by the module [`base`]. +//! //! # Caching //! The validator has four caches: //! 1) A `node` cache that caches the DNSSEC status and (if needed) DNSKEY @@ -58,8 +71,8 @@ //! # use domain::net::client::dgram_stream; //! # use domain::net::client::protocol::{TcpConnect, UdpConnect}; //! # use domain::net::client::request::{ComposeRequest, RequestMessage, SendRequest}; -//! # use domain::validator::anchor::TrustAnchors; -//! # use domain::validator::context::{Config, ValidationContext}; +//! # use domain::dnssec::validator::anchor::TrustAnchors; +//! # use domain::dnssec::validator::context::{Config, ValidationContext}; //! # use std::net::{IpAddr, SocketAddr}; //! # use std::str::FromStr; //! # @@ -106,6 +119,7 @@ #![warn(clippy::missing_docs_in_private_items)] pub mod anchor; +pub mod base; pub mod context; mod group; mod nsec; diff --git a/src/validator/nsec.rs b/src/dnssec/validator/nsec.rs similarity index 99% rename from src/validator/nsec.rs rename to src/dnssec/validator/nsec.rs index 87ce0e901..5b1ae351e 100644 --- a/src/validator/nsec.rs +++ b/src/dnssec/validator/nsec.rs @@ -8,14 +8,14 @@ use std::vec::Vec; use bytes::Bytes; use moka::future::Cache; -use crate::base::iana::{ExtendedErrorCode, Nsec3HashAlg}; +use crate::base::iana::{ExtendedErrorCode, Nsec3HashAlgorithm}; use crate::base::name::{Label, ToName}; use crate::base::opt::ExtendedError; use crate::base::{Name, ParsedName, Rtype}; use crate::dep::octseq::Octets; +use crate::dnssec::common::nsec3_hash; use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; use crate::rdata::{AllRecordData, Nsec, Nsec3}; -use crate::validate::nsec3_hash; use super::context::{Config, ValidationState}; use super::group::ValidatedGroup; @@ -937,7 +937,7 @@ pub async fn nsec3_for_nxdomain( /// The key of the NSEC3 cache. The name that needs to be hash, together /// with the hash algorithm, the number of iterations and the salt. #[derive(Eq, Hash, PartialEq)] -struct Nsec3CacheKey(Name, Nsec3HashAlg, u16, Nsec3Salt); +struct Nsec3CacheKey(Name, Nsec3HashAlgorithm, u16, Nsec3Salt); /// The NSEC3 hash cache. pub struct Nsec3Cache { @@ -956,14 +956,14 @@ impl Nsec3Cache { /// Return if the NSEC3 hash algorithm is supported by the nsec3_hash /// function. -pub fn supported_nsec3_hash(h: Nsec3HashAlg) -> bool { - h == Nsec3HashAlg::SHA1 +pub fn supported_nsec3_hash(h: Nsec3HashAlgorithm) -> bool { + h == Nsec3HashAlgorithm::SHA1 } /// Return an NSEC3 hash using a cache. pub async fn cached_nsec3_hash( owner: &Name, - algorithm: Nsec3HashAlg, + algorithm: Nsec3HashAlgorithm, iterations: u16, salt: &Nsec3Salt, cache: &Nsec3Cache, diff --git a/src/validator/utilities.rs b/src/dnssec/validator/utilities.rs similarity index 100% rename from src/validator/utilities.rs rename to src/dnssec/validator/utilities.rs diff --git a/src/lib.rs b/src/lib.rs index ff1b81e00..624e6b793 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ //! A DNS library for Rust. //! //! This crate provides a number of building blocks for developing -//! functionality related to the -//! [Domain Name System (DNS)](https://www.rfc-editor.org/rfc/rfc9499.html). +//! functionality related to the [Domain Name System (DNS)][dns]. +//! +//! [dns]: https://www.rfc-editor.org/rfc/rfc9499.html //! //! The crate uses feature flags to allow you to select only those modules -//! you need for you particular project. In most cases, the feature names +//! you need for your particular project. In most cases, the feature names //! are equal to the module they enable. //! //! # Modules @@ -18,13 +19,8 @@ //! * [rdata] contains types and implementations for a growing number of //! record types. //! -//! In addition to those two basic modules, there are a number of modules for -//! more specific features that are not required in all applications. In order -//! to keep the amount of code to be compiled and the number of dependencies -//! small, these are hidden behind feature flags through which they can be -//! enabled if required. The flags have the same names as the modules. -//! -//! Currently, there are the following modules: +//! The following additional modules exist, although they are gated behind +//! feature flags (with the same names as the modules): //! #![cfg_attr(feature = "net", doc = "* [net]:")] #![cfg_attr(not(feature = "net"), doc = "* net:")] @@ -33,29 +29,36 @@ #![cfg_attr(not(feature = "resolv"), doc = "* resolv:")] //! An asynchronous DNS resolver based on the //! [Tokio](https://tokio.rs/) async runtime. -#![cfg_attr(feature = "unstable-sign", doc = "* [sign]:")] -#![cfg_attr(not(feature = "unstable-sign"), doc = "* sign:")] -//! Experimental support for DNSSEC signing. +#![cfg_attr(feature = "unstable-crypto", doc = "* [crypto]:")] +#![cfg_attr(not(feature = "unstable-crypto"), doc = "* crypto:")] +//! Experimental support for cryptographic backends, key generation and +//! import. Gated behind the `unstable-crypto` flag. +//! * [dnssec]: DNSSEC signing and validation. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig"), doc = "* tsig:")] //! Support for securing DNS transactions with TSIG records. -#![cfg_attr(feature = "unstable-validate", doc = "* [validate]:")] -#![cfg_attr(not(feature = "unstable-validate"), doc = "* validate:")] -//! Experimental support for DNSSEC validation. -#![cfg_attr(feature = "unstable-validator", doc = "* [validator]:")] -#![cfg_attr(not(feature = "unstable-validator"), doc = "* validator:")] -//! A DNSSEC validator. #![cfg_attr(feature = "zonefile", doc = "* [zonefile]:")] #![cfg_attr(not(feature = "zonefile"), doc = "* zonefile:")] //! Experimental reading and writing of zone files, i.e. the textual //! representation of DNS data. #![cfg_attr(feature = "unstable-zonetree", doc = "* [zonetree]:")] #![cfg_attr(not(feature = "unstable-zonetree"), doc = "* zonetree:")] -//! Experimental storing and querying of zone trees. +//! Experimental storing and querying of zone trees. Gated behind the +//! `unstable-zonetree` flag. //! //! Finally, the [dep] module contains re-exports of some important //! dependencies to help avoid issues with multiple versions of a crate. //! +//! # The `new` Module +//! +//! The API of `domain` is undergoing several large-scale changes, that are +//! collected under the `new` module. It is gated behind the `unstable-new` +//! flag. +#![cfg_attr( + feature = "unstable-new", + doc = "See [its documentation][new] for more information." +)] +//! //! # Reference of feature flags //! //! Several feature flags simply enable support for other crates, e.g. by @@ -152,16 +155,18 @@ //! 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-crypto`: this feature flag needs to be combined with one or +//! more feature flags that enable cryptographic backends (currently `ring` +//! and `openssl`). This feature flags enables all parts of the crypto +//! module except for private key generation and signing. +//! * `unstable-crypto-sign`: this feature flag needs to be combined with one +//! or more feature flags that enable cryptographic backends. This feature +//! flag enables all parts of the crypto 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")] +//! `dnssec::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. +//! and `openssl`). Enabling this will also enable `unstable-crypto-sign`. //! * `unstable-validator`: a DNSSEC validator, primarily the `validator` //! and the `net::client::validator` modules. //! * `unstable-xfr`: zone transfer related functionality.. @@ -179,24 +184,40 @@ #![allow(clippy::uninlined_format_args)] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "alloc")] +extern crate alloc; + #[cfg(feature = "std")] #[allow(unused_imports)] // Import macros even if unused. #[macro_use] extern crate std; -#[macro_use] -extern crate core; +// The 'domain-macros' crate introduces 'derive' macros which can be used by +// users of the 'domain' crate, but also by the 'domain' crate itself. Within +// those macros, references to declarations in the 'domain' crate are written +// as '::domain::*' ... but this doesn't work when those proc macros are used +// in the 'domain' crate itself. The alias introduced here fixes this: now +// '::domain' means the same thing within this crate as in dependents of it. +extern crate self as domain; + +// Re-export 'core' for use in macros. +#[doc(hidden)] +pub use core as __core; pub mod base; +pub mod crypto; pub mod dep; +pub mod dnssec; pub mod net; pub mod rdata; pub mod resolv; -pub mod sign; pub mod stelline; pub mod tsig; pub mod utils; -pub mod validate; -pub mod validator; pub mod zonefile; pub mod zonetree; + +#[cfg(feature = "unstable-new")] +pub mod new; + +mod logging; diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 000000000..1c1e7c32f --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,21 @@ +//! Common logging functions + +/// Setup logging of events reported by domain and the test suite. +/// +/// Use the RUST_LOG environment variable to override the defaults. +/// +/// E.g. To enable debug level logging: +/// +/// ```bash +/// RUST_LOG=DEBUG +/// ``` +#[cfg(feature = "tracing-subscriber")] +pub fn init_logging() { + use tracing_subscriber::EnvFilter; + tracing_subscriber::fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_thread_ids(true) + .without_time() + .try_init() + .ok(); +} diff --git a/src/net/client/dgram.rs b/src/net/client/dgram.rs index e771fc372..2e7a3de03 100644 --- a/src/net/client/dgram.rs +++ b/src/net/client/dgram.rs @@ -25,6 +25,7 @@ use std::boxed::Box; use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use std::vec::Vec; use std::{error, io}; use tokio::sync::Semaphore; use tokio::time::{timeout_at, Duration, Instant}; @@ -229,15 +230,15 @@ where mut request: Req, ) -> Result, Error> { // Acquire the semaphore or wait for it. - let _ = self + let _permit = self .state .semaphore .acquire() .await .expect("semaphore closed"); - // A place to store the receive buffer for reuse. - let mut reuse_buf = None; + // The buffer we will reuse on subsequent requests + let mut buf = Vec::new(); // Transmit loop. for _ in 0..1 + self.state.config.max_retries { @@ -267,10 +268,10 @@ where // Receive loop. It may at most take read_timeout time. let deadline = Instant::now() + self.state.config.read_timeout; while deadline > Instant::now() { - let mut buf = reuse_buf.take().unwrap_or_else(|| { - // XXX use uninit'ed mem here. - vec![0; self.state.config.recv_size] - }); + // The buffer might have been truncated in a previous + // iteration. + buf.resize(self.state.config.recv_size, 0); + let len = match timeout_at(deadline, sock.recv(&mut buf)).await { Ok(Ok(len)) => len, @@ -292,10 +293,10 @@ where // thing. let answer = match Message::try_from_octets(buf) { Ok(answer) => answer, - Err(buf) => { + Err(old_buf) => { // Just go back to receiving. trace!("Received bytes were garbage, reading more"); - reuse_buf = Some(buf); + buf = old_buf; continue; } }; @@ -303,7 +304,7 @@ where if !request.is_answer(answer.for_slice()) { // Wrong answer, go back to receiving trace!("Received message is not the answer we were waiting for, reading more"); - reuse_buf = Some(answer.into_octets()); + buf = answer.into_octets(); continue; } @@ -406,7 +407,7 @@ impl QueryError { fn short_send() -> Self { Self::new( QueryErrorKind::Send, - io::Error::new(io::ErrorKind::Other, "short request sent"), + io::Error::other("short request sent"), ) } diff --git a/src/net/client/mod.rs b/src/net/client/mod.rs index 8b3a48087..4b8220095 100644 --- a/src/net/client/mod.rs +++ b/src/net/client/mod.rs @@ -25,7 +25,9 @@ //! transport connections. The [load_balancer] transport favors connections //! with the shortest outstanding request queue. Any of the other transports //! can be added as upstream transports. -//! * [cache] This is a simple message cache provided as a pass through +#![cfg_attr(feature = "unstable-client-cache", doc = "* [cache]:")] +#![cfg_attr(not(feature = "unstable-client-cache",), doc = "* cache:")] +//! This is a simple message cache provided as a pass through //! transport. The cache works with any of the other transports. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig",), doc = "* tsig:")] @@ -33,8 +35,20 @@ //! pass through transport. The tsig transport works with any upstream //! transports so long as they don't modify the message once signed nor //! modify the response before it can be verified. -#![cfg_attr(feature = "unstable-validator", doc = "* [validator]:")] -#![cfg_attr(not(feature = "unstable-validator",), doc = "* validator:")] +#![cfg_attr( + all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ), + doc = "* [validator]:" +)] +#![cfg_attr( + not(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + )), + doc = "* validator:" +)] //! This is a DNSSEC validator provided as a pass through transport. //! The validator works with any of the other transports. //! @@ -203,7 +217,14 @@ //! * The [multi_stream] transport does not support timeouts or other limits on //! the number of attempts to open a connection. The caller has to //! implement a timeout mechanism. -//! * The [cache] transport does not support: +#![cfg_attr( + feature = "unstable-client-cache", + doc = "* The [cache] transport does not support:" +)] +#![cfg_attr( + not(feature = "unstable-client-cache"), + doc = "* The cache transport does not support:" +)] //! * Prefetching. In this context, prefetching means updating a cache entry //! before it expires. //! * [RFC 8767](https://tools.ietf.org/html/rfc8767) @@ -223,6 +244,7 @@ #![warn(missing_docs)] #![warn(clippy::missing_docs_in_private_items)] +#[cfg(feature = "unstable-client-cache")] pub mod cache; pub mod dgram; pub mod dgram_stream; @@ -232,8 +254,6 @@ pub mod protocol; pub mod redundant; pub mod request; pub mod stream; -#[cfg(feature = "tsig")] pub mod tsig; -#[cfg(feature = "unstable-validator")] pub mod validator; pub mod validator_test; diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index c45db3726..5d0652164 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -724,5 +724,5 @@ fn retry_time(retries: u64) -> Duration { /// Helper function to create an empty future that is compatible with the /// future returned by a connection stream. async fn stream_nop() -> Result { - Err(io::Error::new(io::ErrorKind::Other, "nop")) + Err(io::Error::other("nop")) } diff --git a/src/net/client/request.rs b/src/net/client/request.rs index 534cb558a..39e81df1f 100644 --- a/src/net/client/request.rs +++ b/src/net/client/request.rs @@ -692,9 +692,12 @@ pub enum Error { /// TSIG authentication failed. Authentication(tsig::ValidationError), - #[cfg(feature = "unstable-validator")] + #[cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ))] /// An error happened during DNSSEC validation. - Validation(crate::validator::context::Error), + Validation(crate::dnssec::validator::context::Error), } impl From for Error { @@ -721,9 +724,12 @@ impl From for Error { } } -#[cfg(feature = "unstable-validator")] -impl From for Error { - fn from(err: crate::validator::context::Error) -> Self { +#[cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") +))] +impl From for Error { + fn from(err: crate::dnssec::validator::context::Error) -> Self { Self::Validation(err) } } @@ -783,7 +789,10 @@ impl fmt::Display for Error { #[cfg(feature = "tsig")] Error::Authentication(err) => fmt::Display::fmt(err, f), - #[cfg(feature = "unstable-validator")] + #[cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ))] Error::Validation(_) => { write!(f, "error validating response") } @@ -828,7 +837,10 @@ impl error::Error for Error { #[cfg(feature = "tsig")] Error::Authentication(e) => Some(e), - #[cfg(feature = "unstable-validator")] + #[cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ))] Error::Validation(e) => Some(e), } } diff --git a/src/net/client/stream.rs b/src/net/client/stream.rs index 18ed58d38..884b9edca 100644 --- a/src/net/client/stream.rs +++ b/src/net/client/stream.rs @@ -1336,7 +1336,7 @@ impl Queries { // index needs to fit in an u16. For efficiency we want to // keep the vector half empty. So we return a failure if // 2*count > u16::MAX - if 2 * self.count > u16::MAX.into() { + if 2 * self.count > u16::MAX as usize { return Err(req); } diff --git a/src/net/client/validator.rs b/src/net/client/validator.rs index e5a1031ad..c6c05eb6e 100644 --- a/src/net/client/validator.rs +++ b/src/net/client/validator.rs @@ -6,7 +6,7 @@ //! a validation status to a result code (server failure) and setting or //! clearing the AD flag. //! For details of the validator see the -//! [validator](crate::validator) module. +//! [validator](crate::dnssec::validator) module. //! //! # Upstream transports //! @@ -30,7 +30,7 @@ //! some amount of validating for each request. //! //! The validator has some internal caches (see the -//! [validator](crate::validator) module) so +//! [validator](crate::dnssec::validator) module) so //! there is no direct need for a cache upstream of the validator. Caching //! becomes more complex if there is validator that uses the validator. //! in that case, the downstream validator will likely issues requests for @@ -46,8 +46,8 @@ //! # use domain::net::client::protocol::{TcpConnect, UdpConnect}; //! # use domain::net::client::request::{RequestMessage, SendRequest}; //! # use domain::net::client::validator; -//! # use domain::validator::anchor::TrustAnchors; -//! # use domain::validator::context::ValidationContext; +//! # use domain::dnssec::validator::anchor::TrustAnchors; +//! # use domain::dnssec::validator::context::ValidationContext; //! # use std::net::{IpAddr, SocketAddr}; //! # use std::str::FromStr; //! # use std::sync::Arc; @@ -87,17 +87,29 @@ //! } //! ``` +#![cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") +))] +#![cfg_attr( + docsrs, + doc(cfg(all( + feature = "unstable-validator", + any(feature = "ring", feature = "openssl") + ))) +)] + use crate::base::iana::Rcode; use crate::base::opt::{AllOptData, ExtendedError}; use crate::base::{ Message, MessageBuilder, ParsedName, Rtype, StaticCompressor, }; use crate::dep::octseq::{Octets, OctetsFrom, OctetsInto}; +use crate::dnssec::validator::context::{ValidationContext, ValidationState}; use crate::net::client::request::{ ComposeRequest, Error, GetResponse, RequestMessage, SendRequest, }; use crate::rdata::AllRecordData; -use crate::validator::context::{ValidationContext, ValidationState}; use bytes::Bytes; use std::boxed::Box; use std::fmt::{Debug, Formatter}; diff --git a/src/net/client/validator_test.rs b/src/net/client/validator_test.rs index d02168d52..ace82f7b8 100644 --- a/src/net/client/validator_test.rs +++ b/src/net/client/validator_test.rs @@ -22,10 +22,10 @@ use tracing::instrument; // use domain::net::client::clock::{Clock, FakeClock}; use crate::base::scan::IterScanner; +use crate::dnssec::validator::anchor::TrustAnchors; +use crate::dnssec::validator::context::ValidationContext; use crate::net::client::{multi_stream, validator}; use crate::rdata::dnssec::Timestamp; -use crate::validator::anchor::TrustAnchors; -use crate::validator::context::ValidationContext; use lazy_static::lazy_static; @@ -108,7 +108,7 @@ fn parse_server_config(config: &Config) -> TrustAnchors { ta.add_u8(a.trim_matches('"').as_bytes()).unwrap(); } _ => { - eprintln!("Ignoring unknown server setting '{setting}' with value: {value}"); + eprintln!("Ignoring unknown server setting '{setting}' with value: {value:?}"); } } } diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index bd2150a30..772fa183d 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -935,7 +935,6 @@ where /// Handle I/O errors by deciding whether to log them, and whethr to /// continue or abort. - #[must_use] fn process_io_error(err: io::Error) -> ControlFlow { match err.kind() { io::ErrorKind::UnexpectedEof => { diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 6ff795e42..bab33cda1 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -640,7 +640,7 @@ where let sent = send_res?; if sent != data.len() { - Err(io::Error::new(io::ErrorKind::Other, "short send")) + Err(io::Error::other("short send")) } else { Ok(()) } diff --git a/src/net/server/middleware/xfr/tests.rs b/src/net/server/middleware/xfr/tests.rs index ddad7e7b3..79beff5d6 100644 --- a/src/net/server/middleware/xfr/tests.rs +++ b/src/net/server/middleware/xfr/tests.rs @@ -17,7 +17,9 @@ use octseq::Octets; use tokio::sync::Semaphore; use tokio::time::Instant; -use crate::base::iana::{Class, DigestAlg, OptRcode, Rcode, SecAlg}; +use crate::base::iana::{ + Class, DigestAlgorithm, OptRcode, Rcode, SecurityAlgorithm, +}; use crate::base::{ Message, MessageBuilder, Name, ParsedName, Rtype, Serial, ToName, Ttl, }; @@ -89,8 +91,8 @@ async fn axfr_with_example_zone() { n("signed.example.com"), Ds::new( 60485, - SecAlg::RSASHA1, - DigestAlg::SHA1, + SecurityAlgorithm::RSASHA1, + DigestAlgorithm::SHA1, crate::utils::base16::decode( "2BB183AF5F22588179A53B0A98631FAD1A292118", ) diff --git a/src/net/server/tests/integration.rs b/src/net/server/tests/integration.rs index 178517a70..5daf9cf07 100644 --- a/src/net/server/tests/integration.rs +++ b/src/net/server/tests/integration.rs @@ -24,6 +24,7 @@ use crate::base::name::ToName; use crate::base::net::IpAddr; use crate::base::Name; use crate::base::Rtype; +use crate::logging::init_logging; use crate::net::client::request::{RequestMessage, RequestMessageMulti}; use crate::net::client::{dgram, stream, tsig}; use crate::net::server; @@ -69,15 +70,7 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) { // which responses will be expected, and how the server that answers them // should be configured. - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step - // numbers and types as they are being executed. - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); + init_logging(); // Load the test .rpl file that determines which queries will be sent // and which responses will be expected, and how the server that @@ -274,7 +267,7 @@ fn mk_client_factory( // query, and (b) if the query specifies "MATCHES TCP". Clients created by // this factory connect to the TCP server created above. let only_for_tcp_queries = |entry: &parse_stelline::Entry| { - matches!(entry.matches, Some(Matches { tcp: true, .. })) + matches!(entry.matches, Matches { tcp: true, .. }) }; let tcp_key_store = key_store.clone(); @@ -300,11 +293,9 @@ fn mk_client_factory( let conn = Box::new(tsig::Connection::new(key, conn)); - if let Some(sections) = &entry.sections { - if let Some(q) = sections.question.first() { - if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { - return Client::Multi(conn); - } + if let Some(q) = entry.sections.question.first() { + if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { + return Client::Multi(conn); } } Client::Single(conn) @@ -318,11 +309,9 @@ fn mk_client_factory( let conn = Box::new(conn); - if let Some(sections) = &entry.sections { - if let Some(q) = sections.question.first() { - if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { - return Client::Multi(conn); - } + if let Some(q) = entry.sections.question.first() { + if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { + return Client::Multi(conn); } } Client::Single(conn) @@ -346,33 +335,27 @@ fn mk_client_factory( }); if let Some(key) = key { - match entry.matches.as_ref().map(|v| v.mock_client) { - Some(true) => { - Client::Single(Box::new(tsig::Connection::new( - key, - simple_dgram_client::Connection::new(connect), - ))) - } - - _ => Client::Single(Box::new(tsig::Connection::new( + if entry.matches.mock_client { + Client::Single(Box::new(tsig::Connection::new( + key, + simple_dgram_client::Connection::new(connect), + ))) + } else { + Client::Single(Box::new(tsig::Connection::new( key, dgram::Connection::new(connect), - ))), + ))) } + } else if entry.matches.mock_client { + Client::Single(Box::new( + simple_dgram_client::Connection::new(connect), + )) } else { - match entry.matches.as_ref().map(|v| v.mock_client) { - Some(true) => Client::Single(Box::new( - simple_dgram_client::Connection::new(connect), - )), - - _ => { - let mut config = dgram::Config::new(); - config.set_max_retries(0); - Client::Single(Box::new( - dgram::Connection::with_config(connect, config), - )) - } - } + let mut config = dgram::Config::new(); + config.set_max_retries(0); + Client::Single(Box::new(dgram::Connection::with_config( + connect, config, + ))) } }, for_all_other_queries, @@ -548,7 +531,7 @@ fn parse_server_config(config: &Config) -> ServerConfig { zone_name = Some(v.to_string()); } _ => { - eprintln!("Ignoring unknown server setting '{setting}' with value: {value}"); + eprintln!("Ignoring unknown server setting '{setting}' with value: {value:?}"); } } } diff --git a/src/net/server/tests/unit.rs b/src/net/server/tests/unit.rs index 89139439b..7a5c9f081 100644 --- a/src/net/server/tests/unit.rs +++ b/src/net/server/tests/unit.rs @@ -15,13 +15,13 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio::time::sleep; use tokio::time::Instant; use tracing::trace; -use tracing_subscriber::EnvFilter; use crate::base::MessageBuilder; use crate::base::Name; use crate::base::Rtype; use crate::base::StaticCompressor; use crate::base::StreamTarget; +use crate::logging::init_logging; use crate::net::server::buf::BufSource; use crate::net::server::message::Request; use crate::net::server::middleware::mandatory::MandatoryMiddlewareSvc; @@ -382,14 +382,7 @@ fn mk_query() -> StreamTarget> { // waiting to allow time to elapse. #[tokio::test(flavor = "current_thread", start_paused = true)] async fn tcp_service_test() { - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); + init_logging(); let (srv_handle, server_status_printer_handle) = { let fast_client = MockClientConfig { @@ -481,14 +474,7 @@ async fn tcp_service_test() { #[tokio::test(flavor = "current_thread", start_paused = true)] async fn tcp_client_disconnect_test() { - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); + init_logging(); let (srv_handle, server_status_printer_handle) = { let fast_client = MockClientConfig { diff --git a/src/net/xfr/protocol/iterator.rs b/src/net/xfr/protocol/iterator.rs index a0beec3d8..83f05b3d7 100644 --- a/src/net/xfr/protocol/iterator.rs +++ b/src/net/xfr/protocol/iterator.rs @@ -24,7 +24,14 @@ pub struct XfrZoneUpdateIterator<'a, 'b> { iter: RecordIter<'b, Bytes, ZoneRecordData>>, /// TODO - saved_update: Option, ZoneRecordData>>>>, + saved_update: Option< + ZoneUpdate< + Record< + ParsedName, + ZoneRecordData>, + >, + >, + >, } impl<'a, 'b> XfrZoneUpdateIterator<'a, 'b> { @@ -61,7 +68,11 @@ impl<'a, 'b> XfrZoneUpdateIterator<'a, 'b> { }; } - Ok(Self { state, iter, saved_update: None }) + Ok(Self { + state, + iter, + saved_update: None, + }) } } @@ -77,7 +88,7 @@ impl Iterator for XfrZoneUpdateIterator<'_, '_> { // wanted to still be able to detect the first call to next() and // handle it specially for AXFR. self.state.rr_count += 1; - + if self.state.actual_xfr_type == XfrType::Axfr { // For AXFR we're not making incremental changes to a zone, // we're replacing its entire contents, so before returning @@ -96,7 +107,8 @@ impl Iterator for XfrZoneUpdateIterator<'_, '_> { trace!("XFR record {}: {record:?}", self.state.rr_count); let update = self.state.process_record(record); - if is_first_rr && self.state.actual_xfr_type == XfrType::Axfr { + if is_first_rr && self.state.actual_xfr_type == XfrType::Axfr + { // We didn't return DeleteAllRecords above because the // transfer was thought to be IXFR rather than AXFR, but // now that the next record has been processed we have had diff --git a/src/net/xfr/protocol/tests.rs b/src/net/xfr/protocol/tests.rs index 0bcfc5826..93aad37be 100644 --- a/src/net/xfr/protocol/tests.rs +++ b/src/net/xfr/protocol/tests.rs @@ -15,6 +15,7 @@ use crate::base::{ Message, MessageBuilder, ParsedName, Record, Rtype, Serial, Ttl, }; use crate::base::{Name, ToName}; +use crate::logging::init_logging; use crate::rdata::{Aaaa, Soa, ZoneRecordData, A}; use crate::zonetree::types::{ZoneUpdate, ZoneUpdate as ZU}; @@ -449,18 +450,6 @@ fn mk_second_ixfr_response( //------------ Helper functions ------------------------------------------- -fn init_logging() { - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step - // numbers and types as they are being executed. - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); -} - fn mk_request(qname: &str, qtype: Rtype) -> QuestionBuilder { let req = MessageBuilder::new_bytes(); let mut req = req.question(); diff --git a/src/new/base/build/message.rs b/src/new/base/build/message.rs new file mode 100644 index 000000000..2cddc9122 --- /dev/null +++ b/src/new/base/build/message.rs @@ -0,0 +1,411 @@ +//! Building whole DNS messages. + +use core::fmt; + +use crate::{ + new::base::{ + wire::{ParseBytesZC, U16}, + Header, HeaderFlags, Message, MessageItem, Question, Record, + SectionCounts, + }, + new::edns::EdnsRecord, +}; + +use super::{BuildBytes, BuildInMessage, NameCompressor, TruncationError}; + +//----------- MessageBuilder ------------------------------------------------- + +/// A builder for a whole DNS message. +pub struct MessageBuilder<'b, 'c> { + /// The message being built. + message: &'b mut Message, + + /// The offset data is being written to. + offset: usize, + + /// The name compressor. + compressor: &'c mut NameCompressor, +} + +//--- Initialization + +impl<'b, 'c> MessageBuilder<'b, 'c> { + /// Begin building a DNS message. + /// + /// The buffer will be initialized with the given message ID and flags. + /// The name compressor will be reset in case it was used before. + /// + /// # Panics + /// + /// Panics if the buffer is less than 12 bytes long (which is the minimum + /// possible size for a DNS message). + #[must_use] + pub fn new( + buffer: &'b mut [u8], + compressor: &'c mut NameCompressor, + id: U16, + flags: HeaderFlags, + ) -> Self { + let message = Message::parse_bytes_in(buffer) + .expect("The caller's buffer is at least 12 bytes big"); + message.header = Header { + id, + flags, + counts: SectionCounts::default(), + }; + // TODO: Reset the name compressor. + Self { + message, + offset: 0, + compressor, + } + } +} + +//--- Inspection + +impl MessageBuilder<'_, '_> { + /// The message header. + #[must_use] + pub fn header(&self) -> &Header { + &self.message.header + } + + /// The message header, mutably. + #[must_use] + pub fn header_mut(&mut self) -> &mut Header { + &mut self.message.header + } + + /// The message built thus far. + #[must_use] + pub fn message(&self) -> &Message { + self.message.truncate(self.offset) + } + + /// The message built thus far, mutably. + /// + /// Compressed names in the message must not be modified here, as the name + /// compressor relies on them. Modifying them will break name compression + /// and result in misformatted messages. + #[must_use] + pub fn message_mut(&mut self) -> &mut Message { + self.message.truncate_mut(self.offset) + } + + /// The name compressor. + #[must_use] + pub fn compressor(&self) -> &NameCompressor { + self.compressor + } +} + +//--- Interaction + +impl<'b> MessageBuilder<'b, '_> { + /// End the builder, returning the built message. + #[must_use] + pub fn finish(self) -> &'b mut Message { + self.message.truncate_mut(self.offset) + } + + /// Reborrow the builder with a shorter lifetime. + #[must_use] + pub fn reborrow(&mut self) -> MessageBuilder<'_, '_> { + MessageBuilder { + message: self.message, + offset: self.offset, + compressor: self.compressor, + } + } + + /// Limit the total message size. + /// + /// The message will not be allowed to exceed the given size, in bytes. + /// Only the message header and contents are counted; the enclosing UDP + /// or TCP packet size is not considered. If the message already exceeds + /// this size, a [`TruncationError`] is returned. + pub fn limit_to(&mut self, size: usize) -> Result<(), TruncationError> { + if 12 + self.offset <= size { + // Move out of 'message' so that the full lifetime is available. + // See the 'replace_with' and 'take_mut' crates. + let size = (size - 12).min(self.message.contents.len()); + let message = unsafe { core::ptr::read(&self.message) }; + // NOTE: Precondition checked, will not panic. + let message = message.truncate_mut(size); + unsafe { core::ptr::write(&mut self.message, message) }; + Ok(()) + } else { + Err(TruncationError) + } + } + + /// Truncate the message. + /// + /// This will remove all message contents and mark it as truncated. + pub fn truncate(&mut self) { + self.message.header.flags.set_tc(true); + self.offset = 0; + // TODO: Reset the name compressor. + } + + /// Append a message item. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push( + &mut self, + item: &MessageItem, + ) -> Result<(), MessageBuildError> + where + N: BuildInMessage, + RD: BuildInMessage, + ED: BuildBytes, + { + // Determine the section number. + let section = match item { + MessageItem::Question(_) => 0, + MessageItem::Answer(_) => 1, + MessageItem::Authority(_) => 2, + MessageItem::Additional(_) => 3, + MessageItem::Edns(_) => 3, + }; + + // Make sure this item is not misplaced. + let counts = self.message.header.counts.as_array_mut(); + if counts[section + 1..].iter().any(|c| c.get() != 0) { + return Err(MessageBuildError::Misplaced); + } + + // Try to build the item. + self.offset = item.build_in_message( + &mut self.message.contents, + self.offset, + self.compressor, + )?; + + // TODO: Reset the name compressor in case of failure. + + // Update the section counts, now that we have succeeded. + counts[section] += 1; + + Ok(()) + } + + /// Append a question. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_question( + &mut self, + question: &Question, + ) -> Result<(), MessageBuildError> { + let question = question.transform_ref(|n| n); + self.push(&MessageItem::<&N, (), ()>::Question(question)) + } + + /// Append an answer record. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_answer( + &mut self, + answer: &Record, + ) -> Result<(), MessageBuildError> { + let answer = answer.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Answer(answer)) + } + + /// Append an authority record. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_authority( + &mut self, + authority: &Record, + ) -> Result<(), MessageBuildError> { + let authority = authority.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Authority(authority)) + } + + /// Append an additional record. + /// + /// ## Errors + /// + /// If the item does not fit in the message buffer, [`TruncationError`] is + /// returned. + pub fn push_additional( + &mut self, + additional: &Record, + ) -> Result<(), TruncationError> { + let additional = additional.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Additional(additional)) + .map_err(|err| match err { + MessageBuildError::Misplaced => { + unreachable!("An additional record is never misplaced") + } + MessageBuildError::Truncated(err) => err, + }) + } + + /// Append an EDNS record. + /// + /// ## Errors + /// + /// If the item does not fit in the message buffer, [`TruncationError`] is + /// returned. + pub fn push_edns( + &mut self, + edns: &EdnsRecord, + ) -> Result<(), TruncationError> { + let edns = edns.transform_ref(|d| d); + self.push(&MessageItem::<(), (), &D>::Edns(edns)) + .map_err(|err| match err { + MessageBuildError::Misplaced => { + unreachable!("An additional record is never misplaced") + } + MessageBuildError::Truncated(err) => err, + }) + } +} + +//----------- MessageBuildError ---------------------------------------------- + +/// A component of a DNS message could not be built. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum MessageBuildError { + /// A message item was placed in the wrong section. + /// + /// DNS message items (questions, answers, additionals, etc.) must come in + /// a fixed order; this error is returned if an item could not be added in + /// the right order (i.e. items from later sections would come before it). + Misplaced, + + /// A message item was too large to fit. + Truncated(TruncationError), +} + +#[cfg(feature = "std")] +impl std::error::Error for MessageBuildError {} + +impl From for MessageBuildError { + fn from(value: TruncationError) -> Self { + Self::Truncated(value) + } +} + +//--- Formatting + +impl fmt::Display for MessageBuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Misplaced => { + "a DNS message item was placed in the wrong order" + } + Self::Truncated(_) => "a DNS message item was too large to fit", + }) + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod test { + use crate::new::base::name::RevNameBuf; + use crate::new::base::wire::U16; + use crate::new::base::{ + HeaderFlags, QClass, QType, Question, RClass, RType, Record, TTL, + }; + use crate::new::rdata::{RecordData, A}; + + use super::{MessageBuilder, NameCompressor}; + + #[test] + fn new() { + let mut buffer = [0u8; 12]; + let mut compressor = NameCompressor::default(); + + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + assert_eq!(&builder.message().contents, &[] as &[u8]); + assert_eq!(&builder.message_mut().contents, &[] as &[u8]); + } + + #[test] + fn build_question() { + let mut buffer = [0u8; 33]; + let mut compressor = NameCompressor::default(); + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + let question = Question:: { + qname: "www.example.org".parse().unwrap(), + qtype: QType::A, + qclass: QClass::IN, + }; + builder.push_question(&question).unwrap(); + + let contents = b"\x03www\x07example\x03org\x00\x00\x01\x00\x01"; + assert_eq!(&builder.message().contents, contents); + } + + #[test] + fn build_record() { + let mut buffer = [0u8; 43]; + let mut compressor = NameCompressor::default(); + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + let record = Record:: { + rname: "www.example.org".parse().unwrap(), + rtype: RType::A, + rclass: RClass::IN, + ttl: TTL::from(42), + rdata: RecordData::<()>::A(A { + octets: [127, 0, 0, 1], + }), + }; + builder.push_answer(&record).unwrap(); + + assert_eq!(builder.message().header.counts.answers.get(), 1); + let contents = b"\x03www\x07example\x03org\x00\x00\x01\x00\x01\x00\x00\x00\x2A\x00\x04\x7F\x00\x00\x01"; + assert_eq!(&builder.message().contents, contents.as_slice()); + } +} diff --git a/src/new/base/build/mod.rs b/src/new/base/build/mod.rs new file mode 100644 index 000000000..a445879a8 --- /dev/null +++ b/src/new/base/build/mod.rs @@ -0,0 +1,210 @@ +//! Building DNS messages in the wire format. +//! +//! The [`wire`](super::wire) module provides basic serialization capability, +//! but it is not specialized to DNS messages. This module provides that +//! specialization within an ergonomic interface. +//! +//! The core of the high-level interface is [`MessageBuilder`]. It provides +//! the most intuitive methods for appending whole questions and records. +//! +//! ``` +//! use domain::new::base::{ +//! Header, HeaderFlags, Message, +//! Question, QType, QClass, +//! Record, RType, RClass, +//! }; +//! use domain::new::base::build::{AsBytes, MessageBuilder, NameCompressor}; +//! use domain::new::base::name::RevNameBuf; +//! use domain::new::base::wire::U16; +//! use domain::new::rdata::RecordData; +//! +//! // Initialize a DNS message builder. +//! let mut buffer = [0u8; 512]; +//! let mut compressor = NameCompressor::default(); +//! let mut builder = MessageBuilder::new( +//! &mut buffer, +//! &mut compressor, +//! // Select a randomized ID here. +//! U16::new(1234), +//! // A response to a recursive query for authoritative data. +//! *HeaderFlags::default() +//! .set_qr(true) +//! .set_opcode(0) +//! .set_aa(true) +//! .set_rd(true) +//! .set_rcode(0)); +//! +//! // Add a question for an A record. +//! builder.push_question(&Question { +//! qname: "www.example.org".parse::().unwrap(), +//! qtype: QType::A, +//! qclass: QClass::IN, +//! }).unwrap(); +//! +//! // Add an answer. +//! builder.push_answer(&Record { +//! rname: "www.example.org".parse::().unwrap(), +//! rtype: RType::A, +//! rclass: RClass::IN, +//! ttl: 3600.into(), +//! rdata: >::A("127.0.0.1".parse().unwrap()), +//! }).unwrap(); +//! +//! // Use the built message (e.g. send it). +//! let message: &mut Message = builder.finish(); +//! let bytes: &[u8] = message.as_bytes(); +//! # let _ = bytes; +//! ``` + +mod message; +pub use message::{MessageBuildError, MessageBuilder}; + +pub use super::name::NameCompressor; +pub use super::wire::{AsBytes, BuildBytes, TruncationError}; + +//----------- BuildInMessage ------------------------------------------------- + +/// Building into a DNS message. +pub trait BuildInMessage { + /// Write this object in a DNS message. + /// + /// The contents of the DNS message (i.e. the data after the 12-byte + /// header) are stored in a byte buffer, provided here as `contents`. + /// `self` will be serialized and written to `contents[start..]`. + /// + /// Upon success, the position future content should be written to is + /// returned (i.e. `start` + the number of bytes written here). + /// + /// ## Errors + /// + /// Fails if the message buffer is too small to fit the object. Parts of + /// the message buffer (anything after `start`) may have been modified, + /// but should not be considered part of the initialized message. The + /// caller should explicitly reset the name compressor to `start` to undo + /// the effects of this function. + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result; +} + +impl BuildInMessage for &T { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(*self, contents, start, compressor) + } +} + +impl BuildInMessage for () { + fn build_in_message( + &self, + _contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + Ok(start) + } +} + +impl BuildInMessage for u8 { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + match contents.get_mut(start..) { + Some(&mut [ref mut b, ..]) => { + *b = *self; + Ok(start + 1) + } + _ => Err(TruncationError), + } + } +} + +impl BuildInMessage for [T] { + /// Write a sequence of elements to a DNS message. + /// + /// If an element cannot be written due to a truncation error, the whole + /// sequence is considered to have failed. For more nuanced behaviour on + /// truncation, build each element manually. + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + for item in self { + start = item.build_in_message(contents, start, compressor)?; + } + Ok(start) + } +} + +impl BuildInMessage for [T; N] { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.as_slice() + .build_in_message(contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::boxed::Box { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::rc::Rc { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::sync::Arc { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::vec::Vec { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.as_slice() + .build_in_message(contents, start, compressor) + } +} diff --git a/src/new/base/charstr.rs b/src/new/base/charstr.rs new file mode 100644 index 000000000..596dff529 --- /dev/null +++ b/src/new/base/charstr.rs @@ -0,0 +1,467 @@ +//! DNS "character strings". + +use core::borrow::{Borrow, BorrowMut}; +use core::fmt; +use core::ops::{Deref, DerefMut}; +use core::str::FromStr; + +use crate::utils::dst::{UnsizedCopy, UnsizedCopyFrom}; + +use super::{ + build::{BuildInMessage, NameCompressor}, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError}, +}; + +//----------- CharStr -------------------------------------------------------- + +/// A DNS "character string". +#[derive(UnsizedCopy)] +#[repr(transparent)] +pub struct CharStr { + /// The underlying octets. + /// + /// This is at most 255 bytes. It does not include the length octet that + /// precedes the character string when serialized in the wire format. + pub octets: [u8], +} + +//--- Construction + +impl CharStr { + /// Assume a byte sequence is a valid [`CharStr`]. + /// + /// # Safety + /// + /// The byte sequence does not include the length octet; it simply must be + /// 255 bytes in length or shorter. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'CharStr' is sound. + core::mem::transmute(bytes) + } + + /// Assume a mutable byte sequence is a valid [`CharStr`]. + /// + /// # Safety + /// + /// The byte sequence does not include the length octet; it simply must be + /// 255 bytes in length or shorter. + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'CharStr' is sound. + core::mem::transmute(bytes) + } +} + +//--- Inspection + +impl CharStr { + /// The length of the [`CharStr`]. + /// + /// This is always less than 256 -- it is guaranteed to fit in a [`u8`]. + pub const fn len(&self) -> usize { + self.octets.len() + } + + /// Whether the [`CharStr`] is empty. + pub const fn is_empty(&self) -> bool { + self.octets.is_empty() + } +} + +//--- Parsing from DNS messages + +impl<'a> SplitMessageBytes<'a> for &'a CharStr { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +impl<'a> ParseMessageBytes<'a> for &'a CharStr { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CharStr { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.len() + 1; + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[0] = self.len() as u8; + bytes[1..].copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for &'a CharStr { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let (&length, rest) = bytes.split_first().ok_or(ParseError)?; + if length as usize > rest.len() { + return Err(ParseError); + } + let (bytes, rest) = rest.split_at(length as usize); + + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]'. + Ok((unsafe { core::mem::transmute::<&[u8], Self>(bytes) }, rest)) + } +} + +impl<'a> ParseBytes<'a> for &'a CharStr { + fn parse_bytes(bytes: &'a [u8]) -> Result { + let (&length, rest) = bytes.split_first().ok_or(ParseError)?; + if length as usize != rest.len() { + return Err(ParseError); + } + + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], Self>(rest) }) + } +} + +//--- Building into byte sequences + +impl BuildBytes for CharStr { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let (length, bytes) = + bytes.split_first_mut().ok_or(TruncationError)?; + *length = self.octets.len() as u8; + self.octets.build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + 1 + self.octets.len() + } +} + +//--- Equality + +impl PartialEq for CharStr { + fn eq(&self, other: &Self) -> bool { + self.octets.eq_ignore_ascii_case(&other.octets) + } +} + +impl Eq for CharStr {} + +//--- Formatting + +impl fmt::Debug for CharStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use fmt::Write; + + struct Native<'a>(&'a [u8]); + impl fmt::Debug for Native<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("b\"")?; + for &b in self.0 { + f.write_str(match b { + b'"' => "\\\"", + b' ' => " ", + b'\n' => "\\n", + b'\r' => "\\r", + b'\t' => "\\t", + b'\\' => "\\\\", + + _ => { + if b.is_ascii_graphic() { + f.write_char(b as char)?; + } else { + write!(f, "\\x{:02X}", b)?; + } + continue; + } + })?; + } + f.write_char('"')?; + Ok(()) + } + } + + f.debug_struct("CharStr") + .field("content", &Native(&self.octets)) + .finish() + } +} + +//----------- CharStrBuf ----------------------------------------------------- + +/// A 256-byte buffer for a character string. +#[derive(Clone)] +#[repr(C)] // make layout compatible with '[u8; 256]' +pub struct CharStrBuf { + /// The length of the string, in bytes. + size: u8, + + /// The string contents. + data: [u8; 255], +} + +//--- Construction + +impl CharStrBuf { + /// Construct an empty, invalid buffer. + const fn empty() -> Self { + Self { + size: 0, + data: [0u8; 255], + } + } + + /// Copy a [`CharStrBuf`] into a buffer. + pub fn copy_from(string: &CharStr) -> Self { + let mut this = Self::empty(); + this.size = string.len() as u8; + this.data[..string.len()].copy_from_slice(&string.octets); + this + } +} + +impl UnsizedCopyFrom for CharStrBuf { + type Source = CharStr; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Inspection + +impl CharStrBuf { + /// The wire format for this character string. + pub fn wire_bytes(&self) -> &[u8] { + let ptr = self as *const _ as *const u8; + let len = self.len() + 1; + // SAFETY: 'Self' is 'repr(C)' and contains no padding. It can be + // interpreted as a 256-byte array. + unsafe { core::slice::from_raw_parts(ptr, len) } + } +} + +//--- Parsing from DNS messages + +impl SplitMessageBytes<'_> for CharStrBuf { + fn split_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + <&CharStr>::split_message_bytes(contents, start) + .map(|(this, rest)| (Self::copy_from(this), rest)) + } +} + +impl ParseMessageBytes<'_> for CharStrBuf { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + <&CharStr>::parse_message_bytes(contents, start).map(Self::copy_from) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CharStrBuf { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + name: &mut NameCompressor, + ) -> Result { + CharStr::build_in_message(self, contents, start, name) + } +} + +//--- Parsing from bytes + +impl SplitBytes<'_> for CharStrBuf { + fn split_bytes(bytes: &'_ [u8]) -> Result<(Self, &'_ [u8]), ParseError> { + <&CharStr>::split_bytes(bytes) + .map(|(this, rest)| (Self::copy_from(this), rest)) + } +} + +impl ParseBytes<'_> for CharStrBuf { + fn parse_bytes(bytes: &'_ [u8]) -> Result { + <&CharStr>::parse_bytes(bytes).map(Self::copy_from) + } +} + +//--- Building into byte sequences + +impl BuildBytes for CharStrBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Parsing from strings + +impl FromStr for CharStrBuf { + type Err = CharStrParseError; + + /// Parse a DNS "character-string" from a string. + /// + /// This is intended for easily constructing hard-coded character strings. + /// This function cannot parse all valid character strings; if exceptional + /// instances are needed, use [`CharStr::from_bytes_unchecked()`]. + fn from_str(s: &str) -> Result { + if s.as_bytes().contains(&b'\\') { + Err(CharStrParseError::InvalidChar) + } else if s.len() > 255 { + Err(CharStrParseError::Overlong) + } else { + // SAFETY: 's' is 255 bytes or shorter. + let s = unsafe { CharStr::from_bytes_unchecked(s.as_bytes()) }; + Ok(Self::copy_from(s)) + } + } +} + +//--- Access to the underlying 'CharStr' + +impl Deref for CharStrBuf { + type Target = CharStr; + + fn deref(&self) -> &Self::Target { + let name = &self.data[..self.size as usize]; + // SAFETY: A 'CharStrBuf' always contains a valid 'CharStr'. + unsafe { CharStr::from_bytes_unchecked(name) } + } +} + +impl DerefMut for CharStrBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let name = &mut self.data[..self.size as usize]; + // SAFETY: A 'CharStrBuf' always contains a valid 'CharStr'. + unsafe { CharStr::from_bytes_unchecked_mut(name) } + } +} + +impl Borrow for CharStrBuf { + fn borrow(&self) -> &CharStr { + self + } +} + +impl BorrowMut for CharStrBuf { + fn borrow_mut(&mut self) -> &mut CharStr { + self + } +} + +impl AsRef for CharStrBuf { + fn as_ref(&self) -> &CharStr { + self + } +} + +impl AsMut for CharStrBuf { + fn as_mut(&mut self) -> &mut CharStr { + self + } +} + +//--- Forwarding equality and formatting + +impl PartialEq for CharStrBuf { + fn eq(&self, that: &Self) -> bool { + **self == **that + } +} + +impl Eq for CharStrBuf {} + +impl fmt::Debug for CharStrBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +//----------- CharStrParseError ---------------------------------------------- + +/// An error in parsing a [`CharStr`] from a string. +/// +/// This can be returned by [`CharStrBuf::from_str()`]. It is not used when +/// parsing character strings from the zonefile format, which uses a different +/// mechanism. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CharStrParseError { + /// The character string was too large. + /// + /// Valid character strings are between 0 and 255 bytes, inclusive. + Overlong, + + /// The input contained an invalid character. + InvalidChar, +} + +// TODO(1.81.0): Use 'core::error::Error' instead. +#[cfg(feature = "std")] +impl std::error::Error for CharStrParseError {} + +impl fmt::Display for CharStrParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Overlong => "the character string was too long", + Self::InvalidChar => { + "the character string contained an invalid character" + } + }) + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod test { + use super::CharStr; + + use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, + }; + + #[test] + fn parse_build() { + let bytes = b"\x05Hello!"; + let (charstr, rest) = <&CharStr>::split_bytes(bytes).unwrap(); + assert_eq!(&charstr.octets, b"Hello"); + assert_eq!(rest, b"!"); + + assert_eq!(<&CharStr>::parse_bytes(bytes), Err(ParseError)); + assert!(<&CharStr>::parse_bytes(&bytes[..6]).is_ok()); + + let mut buffer = [0u8; 6]; + assert_eq!( + charstr.build_bytes(&mut buffer), + Ok(&mut [] as &mut [u8]) + ); + assert_eq!(buffer, &bytes[..6]); + } +} diff --git a/src/new/base/message.rs b/src/new/base/message.rs new file mode 100644 index 000000000..411c75be4 --- /dev/null +++ b/src/new/base/message.rs @@ -0,0 +1,674 @@ +//! DNS message headers. + +use core::fmt; + +use domain_macros::*; + +use crate::new::edns::EdnsRecord; + +use super::build::{BuildInMessage, NameCompressor}; +use super::parse::MessageParser; +use super::wire::{AsBytes, BuildBytes, ParseBytesZC, TruncationError, U16}; +use super::{Question, Record}; + +//----------- Message -------------------------------------------------------- + +/// A DNS message. +#[derive(AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy)] +#[repr(C, packed)] +pub struct Message { + /// The message header. + pub header: Header, + + /// The message contents. + pub contents: [u8], +} + +//--- Inspection + +impl Message { + /// Represent this as a mutable byte sequence. + /// + /// Given `&mut self`, it is already possible to individually modify the + /// message header and contents; since neither has invalid instances, it + /// is safe to represent the entire object as mutable bytes. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + // SAFETY: + // - 'Self' has no padding bytes and no interior mutability. + // - Its size in memory is exactly 'size_of_val(self)'. + unsafe { + core::slice::from_raw_parts_mut( + self as *mut Self as *mut u8, + core::mem::size_of_val(self), + ) + } + } +} + +//--- Parsing + +impl Message { + /// Parse the questions and records in this message. + /// + /// This returns a fallible iterator of [`MessageItem`]s. + pub const fn parse(&self) -> MessageParser<'_> { + MessageParser::for_message(self) + } +} + +//--- Interaction + +impl Message { + /// Truncate the contents of this message to the given size. + /// + /// The returned value will have a `contents` field of the given size. + pub fn truncate(&self, size: usize) -> &Self { + let bytes = &self.as_bytes()[..12 + size]; + // SAFETY: 'bytes' is at least 12 bytes, making it a valid 'Message'. + unsafe { Self::parse_bytes_by_ref(bytes).unwrap_unchecked() } + } + + /// Truncate the contents of this message to the given size, mutably. + /// + /// The returned value will have a `contents` field of the given size. + pub fn truncate_mut(&mut self, size: usize) -> &mut Self { + let bytes = &mut self.as_bytes_mut()[..12 + size]; + // SAFETY: 'bytes' is at least 12 bytes, making it a valid 'Message'. + unsafe { Self::parse_bytes_in(bytes).unwrap_unchecked() } + } + + /// Truncate the contents of this message to the given size, by pointer. + /// + /// The returned value will have a `contents` field of the given size. + /// + /// # Safety + /// + /// This method uses `pointer::offset()`: `self` must be "derived from a + /// pointer to some allocated object". There must be at least 12 bytes + /// between `self` and the end of that allocated object. A reference to + /// `Message` will always result in a pointer satisfying this. + pub unsafe fn truncate_ptr(this: *mut Message, size: usize) -> *mut Self { + // Extract the metadata from 'this'. We know it's slice metadata. + // + // SAFETY: '[()]' is a zero-sized type and references to it can be + // created from arbitrary pointers, since every pointer is valid for + // zero-sized reads. + let len = unsafe { &*(this as *mut [()]) }.len(); + // Replicate the range check performed by normal indexing operations. + debug_assert!(size <= len); + core::ptr::slice_from_raw_parts_mut(this.cast::(), size) + as *mut Self + } +} + +//----------- Header --------------------------------------------------------- + +/// A DNS message header. +#[derive( + Copy, + Clone, + Debug, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct Header { + /// A unique identifier for the message. + pub id: U16, + + /// Properties of the message. + pub flags: HeaderFlags, + + /// Counts of objects in the message. + pub counts: SectionCounts, +} + +//--- Formatting + +impl fmt::Display for Header { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} of ID {:04X} ({})", + self.flags, + self.id.get(), + self.counts + ) + } +} + +//----------- HeaderFlags ---------------------------------------------------- + +/// DNS message header flags. +/// +/// This 16-bit field provides information about the containing DNS message. +/// Its contents define the purpose of the message, e.g. whether it is a query +/// or a response. Due to its small size, it doesn't cover everything; the +/// OPT record may provide additional information, if it is present. +/// +/// # Specification +/// +// TODO: Update regularly. +// +/// The header field has been updated by several RFCs and the interpretation +/// of its bits has changed in some places. The following is a collection of +/// the relevant RFC notes; it is up-to-date as of *2025-04-03*. +/// +/// The descriptions here are specific to the `QUERY` opcode, which is by far +/// the most common. Other opcodes can change the interpretation of the bits +/// here. +/// +/// ```text +/// 15 14 13 12 11 10 9 8 +/// +----+----+----+----+----+----+----+----+ +/// | QR | OPCODE | AA | TC | RD | } MSB +/// +----+----+----+----+----+----+----+----+ +/// | RA | | AD | CD | RCODE | } LSB +/// +----+----+----+----+----+----+----+----+ +/// 7 6 5 4 3 2 1 0 +/// ``` +/// +/// Here is a short description of each field. +/// +/// - `QR` (Query or Response): set if and only if the message is a response. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `OPCODE`: the specific operation requested by the DNS client. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `AA`: whether the DNS server is authoritative for the primary answer. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `TC`: whether the response is truncated (due to channel limitations). +/// +/// Specified by [RFC 1035, section 4.1.1]. Behaviour clarified by [RFC +/// 2181, section 9]. Behaviour for DNSSEC servers specified by [RFC 4035, +/// section 3.1]. +/// +/// - `RD`: whether the DNS client wishes for a recursively resolved answer. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `RA`: whether the DNS server supports recursive resolution. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `AD`: whether the DNS server has authenticated the answer. +/// +/// Defined by [RFC 2535, section 6.1]. Behaviour for authoritative name +/// servers specified by [RFC 4035, section 3.1.6]. Behaviour for recursive +/// name servers specified by [RFC 4035, section 3.2.3] and updated by [RFC +/// 6840, section 5.8]. Behaviour for DNS clients specified by [RFC 6840, +/// section 5.7]. +/// +/// - `CD`: whether the DNS server should avoid authenticating the answer. +/// +/// Defined by [RFC 2535, section 6.1]. Behaviour for authoritative name +/// servers specified by [RFC 4035, section 3.1.6]. Behaviour for recursive +/// name servers specified by [RFC 4035, section 3.2.2] and updated by [RFC +/// 6840, section 5.9]. +/// +/// - `RCODE`: the response status of the DNS server. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// [RFC 1035, section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 +/// [RFC 2181, section 9]: https://datatracker.ietf.org/doc/html/rfc2181#section-9 +/// [RFC 2535, section 6.1]: https://datatracker.ietf.org/doc/html/rfc2535#section-6.1 +/// [RFC 4035, section 3.1]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.1 +/// [RFC 4035, section 3.1.6]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.1.6 +/// [RFC 4035, section 3.2.2]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.2.2 +/// [RFC 4035, section 3.2.3]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.2.3 +/// [RFC 6840, section 5.7]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.7 +/// [RFC 6840, section 5.8]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.8 +/// [RFC 6840, section 5.9]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.9 +#[derive( + Copy, + Clone, + Default, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct HeaderFlags { + /// The raw flag bits. + inner: U16, +} + +//--- Interaction + +impl HeaderFlags { + /// Get the specified flag bit. + const fn get_flag(&self, pos: u32) -> bool { + self.inner.get() & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(&mut self, pos: u32, value: bool) -> &mut Self { + self.inner &= !(1 << pos); + self.inner |= (value as u16) << pos; + self + } + + /// The raw flags bits. + pub const fn bits(&self) -> u16 { + self.inner.get() + } + + /// The QR bit. + pub const fn qr(&self) -> bool { + self.get_flag(15) + } + + /// Set the QR bit. + pub fn set_qr(&mut self, value: bool) -> &mut Self { + self.set_flag(15, value) + } + + /// The OPCODE field. + pub const fn opcode(&self) -> u8 { + (self.inner.get() >> 11) as u8 & 0xF + } + + /// Set the OPCODE field. + pub fn set_opcode(&mut self, value: u8) -> &mut Self { + debug_assert!(value < 16); + self.inner &= !(0xF << 11); + self.inner |= (value as u16) << 11; + self + } + + /// The AA bit. + pub fn aa(&self) -> bool { + self.get_flag(10) + } + + /// Set the AA bit. + pub fn set_aa(&mut self, value: bool) -> &mut Self { + self.set_flag(10, value) + } + + /// The TC bit. + pub fn tc(&self) -> bool { + self.get_flag(9) + } + + /// Set the TC bit. + pub fn set_tc(&mut self, value: bool) -> &mut Self { + self.set_flag(9, value) + } + + /// The RD bit. + pub fn rd(&self) -> bool { + self.get_flag(8) + } + + /// Set the RD bit. + pub fn set_rd(&mut self, value: bool) -> &mut Self { + self.set_flag(8, value) + } + + /// The RA bit. + pub fn ra(&self) -> bool { + self.get_flag(7) + } + + /// Set the RA bit. + pub fn set_ra(&mut self, value: bool) -> &mut Self { + self.set_flag(7, value) + } + + /// The AD bit. + pub fn ad(&self) -> bool { + self.get_flag(5) + } + + /// Set the AD bit. + pub fn set_ad(&mut self, value: bool) -> &mut Self { + self.set_flag(5, value) + } + + /// The CD bit. + pub fn cd(&self) -> bool { + self.get_flag(4) + } + + /// Set the CD bit. + pub fn set_cd(&mut self, value: bool) -> &mut Self { + self.set_flag(4, value) + } + + /// The RCODE field. + pub const fn rcode(&self) -> u8 { + self.inner.get() as u8 & 0xF + } + + /// Set the RCODE field. + pub fn set_rcode(&mut self, value: u8) -> &mut Self { + debug_assert!(value < 16); + self.inner &= !0xF; + self.inner |= value as u16; + self + } +} + +//--- Formatting + +impl fmt::Debug for HeaderFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HeaderFlags") + .field("qr", &self.qr()) + .field("opcode", &self.opcode()) + .field("aa", &self.aa()) + .field("tc", &self.tc()) + .field("rd", &self.rd()) + .field("ra", &self.ra()) + .field("rcode", &self.rcode()) + .field("bits", &self.bits()) + .finish() + } +} + +impl fmt::Display for HeaderFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.qr() { + if self.rd() { + f.write_str("recursive ")?; + } + write!(f, "query (opcode {})", self.opcode())?; + if self.cd() { + f.write_str(" (checking disabled)")?; + } + } else { + if self.ad() { + f.write_str("authentic ")?; + } + if self.aa() { + f.write_str("authoritative ")?; + } + if self.rd() && self.ra() { + f.write_str("recursive ")?; + } + write!(f, "response (rcode {})", self.rcode())?; + } + + if self.tc() { + f.write_str(" (message truncated)")?; + } + + Ok(()) + } +} + +//----------- SectionCounts -------------------------------------------------- + +/// Counts of objects in a DNS message. +#[derive( + Copy, + Clone, + Debug, + Default, + PartialEq, + Eq, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct SectionCounts { + /// The number of questions in the message. + pub questions: U16, + + /// The number of answer records in the message. + pub answers: U16, + + /// The number of name server records in the message. + pub authorities: U16, + + /// The number of additional records in the message. + pub additionals: U16, +} + +//--- Interaction + +impl SectionCounts { + /// Represent these counts as an array. + pub fn as_array(&self) -> &[U16; 4] { + // SAFETY: 'SectionCounts' has the same layout as '[U16; 4]'. + unsafe { core::mem::transmute(self) } + } + + /// Represent these counts as a mutable array. + pub fn as_array_mut(&mut self) -> &mut [U16; 4] { + // SAFETY: 'SectionCounts' has the same layout as '[U16; 4]'. + unsafe { core::mem::transmute(self) } + } +} + +//--- Formatting + +impl fmt::Display for SectionCounts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut some = false; + + for (num, single, many) in [ + (self.questions.get(), "question", "questions"), + (self.answers.get(), "answer", "answers"), + (self.authorities.get(), "authority", "authorities"), + (self.additionals.get(), "additional", "additionals"), + ] { + // Add a comma if we have printed something before. + if some && num > 0 { + f.write_str(", ")?; + } + + // Print a count of this section. + match num { + 0 => {} + 1 => write!(f, "1 {single}")?, + n => write!(f, "{n} {many}")?, + } + + some |= num > 0; + } + + if !some { + f.write_str("empty")?; + } + + Ok(()) + } +} + +//----------- MessageItem ---------------------------------------------------- + +/// A question or a record. +/// +/// This is useful for building and parsing the contents of a [`Message`] +/// ergonomically and efficiently. An iterator of [`MessageItem`]s can be +/// retrieved using [`Message::parse()`]. +#[derive(Clone, Debug)] +pub enum MessageItem { + /// A question. + Question(Question), + + /// An answer record. + Answer(Record), + + /// An authority record. + Authority(Record), + + /// An additional record. + /// + /// This does not include EDNS records. + Additional(Record), + + /// An EDNS record. + /// + /// This is a record in the additional section. It uses a distinct type + /// as the class and TTL fields of the record are interpreted differently. + Edns(EdnsRecord), +} + +//--- Transformation + +impl MessageItem { + /// Transform this type's generic parameters. + pub fn transform( + self, + name_map: impl FnOnce(N) -> NN, + rdata_map: impl FnOnce(RD) -> NRD, + edata_map: impl FnOnce(ED) -> NED, + ) -> MessageItem { + match self { + Self::Question(this) => { + MessageItem::Question(this.transform(name_map)) + } + Self::Answer(this) => { + MessageItem::Answer(this.transform(name_map, rdata_map)) + } + Self::Authority(this) => { + MessageItem::Authority(this.transform(name_map, rdata_map)) + } + Self::Additional(this) => { + MessageItem::Additional(this.transform(name_map, rdata_map)) + } + Self::Edns(this) => MessageItem::Edns(this.transform(edata_map)), + } + } + + /// Transform this type's generic parameters by reference. + pub fn transform_ref<'a, NN, NRD, NED>( + &'a self, + name_map: impl FnOnce(&'a N) -> NN, + rdata_map: impl FnOnce(&'a RD) -> NRD, + edata_map: impl FnOnce(&'a ED) -> NED, + ) -> MessageItem { + match self { + Self::Question(this) => { + MessageItem::Question(this.transform_ref(name_map)) + } + Self::Answer(this) => { + MessageItem::Answer(this.transform_ref(name_map, rdata_map)) + } + Self::Authority(this) => MessageItem::Authority( + this.transform_ref(name_map, rdata_map), + ), + Self::Additional(this) => MessageItem::Additional( + this.transform_ref(name_map, rdata_map), + ), + Self::Edns(this) => { + MessageItem::Edns(this.transform_ref(edata_map)) + } + } + } +} + +//--- Equality + +impl PartialEq> + for MessageItem +where + N: PartialEq, + RD: PartialEq, + LED: PartialEq, +{ + fn eq(&self, other: &MessageItem) -> bool { + match (self, other) { + (MessageItem::Question(l), MessageItem::Question(r)) => l == r, + (MessageItem::Answer(l), MessageItem::Answer(r)) => l == r, + (MessageItem::Authority(l), MessageItem::Authority(r)) => l == r, + (MessageItem::Additional(l), MessageItem::Additional(r)) => { + l == r + } + (MessageItem::Edns(l), MessageItem::Edns(r)) => l == r, + _ => false, + } + } +} + +impl Eq for MessageItem {} + +//--- Building into DNS messages + +impl BuildInMessage for MessageItem +where + N: BuildInMessage, + RD: BuildInMessage, + ED: BuildBytes, +{ + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + match self { + Self::Question(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Answer(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Authority(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Additional(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Edns(i) => i.build_in_message(contents, start, compressor), + } + } +} + +//--- Building into bytes + +impl BuildBytes for MessageItem +where + N: BuildBytes, + RD: BuildBytes, + ED: BuildBytes, +{ + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + Self::Question(this) => this.build_bytes(bytes), + Self::Answer(this) => this.build_bytes(bytes), + Self::Authority(this) => this.build_bytes(bytes), + Self::Additional(this) => this.build_bytes(bytes), + Self::Edns(this) => this.build_bytes(bytes), + } + } + + fn built_bytes_size(&self) -> usize { + match self { + Self::Question(this) => this.built_bytes_size(), + Self::Answer(this) => this.built_bytes_size(), + Self::Authority(this) => this.built_bytes_size(), + Self::Additional(this) => this.built_bytes_size(), + Self::Edns(this) => this.built_bytes_size(), + } + } +} diff --git a/src/new/base/mod.rs b/src/new/base/mod.rs new file mode 100644 index 000000000..f74299135 --- /dev/null +++ b/src/new/base/mod.rs @@ -0,0 +1,362 @@ +//! Basic DNS. +//! +//! This module provides the essential types and functionality for working +//! with DNS. In particular, it allows building and parsing DNS messages to +//! and from the wire format. +//! +//! This provides a mid-level and low-level API. It guides users towards the +//! most efficient solutions for their needs, and (where necessary) provides +//! fallbacks that trade efficiency for flexibility and/or ergonomics. +//! +//! # A quick reference on types +//! +//! [`Message`] is the top-level type, representing a whole DNS message. It +//! stores data in the wire format, making it trivial to parse into or build +//! from. It can provide direct access to the message [`Header`], and the +//! questions and records within it (collectively called [`MessageItem`]s) can +//! be parsed/traversed using [`Message::parse()`]. +//! +//! [`Question`] and [`Record`] are exactly what they look like, and are +//! simple `struct`s so they can be constructed and inspected easily. They +//! are generic over a _domain name type_ (discussed below), which you will +//! need to pick explicitly. [`Record`] is also generic over the record data +//! type; you probably want [`new::rdata::RecordData`]. See the documentation +//! on [`Record`] and [`new::rdata`] for more information. +//! +//! [`new::rdata`]: crate::new::rdata +//! [`new::rdata::RecordData`]: crate::new::rdata::RecordData +//! +//! The [`name`] module provides various types that represent domain +//! names, and describes the situations each type is most appropriate +//! for. As a quick summary: try to use [`RevNameBuf`] by default, or +//! Box<[RevName]> if lots of domain names need to be +//! stored. If DNSSEC canonical ordering is necessary, use [`NameBuf`] or +//! Box<[Name]> respectively. There are more efficient +//! alternatives in some cases; see [`name`]. +//! +//! [Name]: name::Name +//! [RevName]: name::RevName +//! [`NameBuf`]: name::NameBuf +//! [`RevNameBuf`]: name::RevNameBuf +//! +//! # Parsing DNS messages +//! +//! The [`parse`] module provides mid-level and low-level APIs for parsing +//! DNS messages from the wire format. To parse the questions and records in +//! a [`Message`], use [`Message::parse()`]. To parse a message (including +//! questions and records) from bytes, use [`MessageParser::new()`]. +//! +//! [`MessageParser::new()`]: parse::MessageParser::new() +//! +//! ``` +//! # use domain::new::base::MessageItem; +//! # use domain::new::base::parse::MessageParser; +//! # +//! // The bytes to be parsed. +//! let bytes: &[u8] = &[ +//! // The message header. +//! 0, 42, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, +//! // A question: www.example.org. A IN +//! 3, b'w', b'w', b'w', +//! 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', +//! 3, b'o', b'r', b'g', 0, +//! 0, 1, 0, 1, +//! // An answer: www.example.org. A IN 3600 127.0.0.1 +//! 192, 12, 0, 1, 0, 1, 0, 0, 14, 16, 0, 4, 127, 0, 0, 1, +//! // An OPT record. +//! 0, 0, 41, 4, 208, 0, 0, 128, 0, 0, 12, +//! // An EDNS client cookie. +//! 0, 10, 0, 8, 6, 148, 57, 104, 176, 18, 234, 57, +//! ]; +//! +//! // Construct a 'MessageParser' directly from bytes. +//! let Ok(mut message) = MessageParser::new(bytes) else { +//! panic!("'bytes' was too small to be a valid 'Message'") +//! }; +//! println!("Header: {:?}", message.header()); +//! while let Some(item) = message.next() { +//! let Ok(item) = item else { +//! panic!("Could not parse a message item (at offset {})", +//! message.offset()); +//! }; +//! +//! match item { +//! MessageItem::Question(question) => { +//! println!("Got a question: {question:?}"); +//! } +//! MessageItem::Answer(answer) => { +//! println!("Got an answer record: {answer:?}"); +//! } +//! MessageItem::Authority(authority) => { +//! println!("Got an authority record: {authority:?}"); +//! } +//! MessageItem::Additional(additional) => { +//! println!("Got an additional record: {additional:?}"); +//! } +//! MessageItem::Edns(edns) => { +//! println!("Got an EDNS record: {edns:?}"); +//! } +//! } +//! } +//! ``` +//! +//! # Building DNS messages +//! +//! The [`build`] module provides mid-level and low-level APIs for building +//! DNS messages in the wire format. [`MessageBuilder`] is the primary entry +//! point; it writes into a user-provided byte buffer. To begin building a +//! DNS message, use [`MessageBuilder::new()`]. +//! +//! [`MessageBuilder`]: build::MessageBuilder +//! [`MessageBuilder::new()`]: build::MessageBuilder::new() +//! +//! ``` +//! use domain::new::base::{Header, HeaderFlags, Message, Question, QType, QClass}; +//! use domain::new::base::build::{AsBytes, MessageBuilder, NameCompressor}; +//! use domain::new::base::name::RevNameBuf; +//! use domain::new::base::wire::U16; +//! +//! // Initialize a DNS message builder. +//! let mut buffer = [0u8; 512]; +//! let mut compressor = NameCompressor::default(); +//! let mut builder = MessageBuilder::new( +//! &mut buffer, +//! &mut compressor, +//! // Select a randomized ID here. +//! U16::new(1234), +//! // A recursive query for authoritative data. +//! *HeaderFlags::default() +//! .set_qr(false) +//! .set_opcode(0) +//! .set_aa(true) +//! .set_rd(true)); +//! +//! // Add a question for an A record. +//! builder.push_question(&Question { +//! qname: "www.example.org".parse::().unwrap(), +//! qtype: QType::A, +//! qclass: QClass::IN, +//! }).unwrap(); +//! +//! // Use the built message (e.g. send it). +//! let message: &mut Message = builder.finish(); +//! let bytes: &[u8] = message.as_bytes(); +//! # let _ = bytes; +//! ``` +//! +//! # Representing variable-length DNS data +//! +//! In order to efficiently serialize and deserialize DNS messages, and to be +//! easier to approach for users already familiar with DNS, this module +//! structures its DNS types to match the underlying wire format. +//! +//! Because many elements of DNS messages have variable-length encodings in +//! the wire format, this module relies on Rust's language support for +//! _dynamically sized types_ (DSTs) to represent them. The top-level +//! [`Message`] type, [`CharStr`], [`Name`], etc. are all DSTs. +//! +//! [`Name`]: name::Name +//! +//! DSTs cannot be passed around by value because the compiler needs to know +//! (at compile-time) how much stack space to allocate for them. As such, a +//! DST has to be held indirectly, by reference or in a container like +//! [`Box`]. The former work well in "short-term" contexts (e.g. within a +//! function), while the latter are necessary in long-term contexts. +//! +//! [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +//! +//! Container types that implement [`UnsizedCopyFrom`] automatically work with +//! any [`UnsizedCopy`] types. This trait allows DSTs to be copied into such +//! container types, which is especially useful to store a DST for long-term +//! use. It is already implemented for [`Box`], [`Arc`], [`Vec`], etc., and +//! users can implement it on their own container types too. +//! +//! [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html +//! [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html +//! [`UnsizedCopy`]: crate::utils::dst::UnsizedCopy +//! [`UnsizedCopyFrom`]: crate::utils::dst::UnsizedCopyFrom + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] + +//--- DNS messages + +mod message; +pub use message::{Header, HeaderFlags, Message, MessageItem, SectionCounts}; + +mod question; +pub use question::{QClass, QType, Question}; + +mod record; +pub use record::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RClass, + RType, Record, UnparsedRecordData, TTL, +}; + +//--- Elements of DNS messages + +pub mod name; + +mod charstr; +pub use charstr::{CharStr, CharStrBuf, CharStrParseError}; + +mod serial; +pub use serial::Serial; + +//--- Wire format + +pub mod build; +pub mod parse; +pub mod wire; + +//--- Compatibility exports + +/// A compatibility module with [`domain::base`]. +/// +/// This re-exports a large part of the `new::base` API surface using the same +/// import paths as the old `base` module. It is a stopgap measure to help +/// users port existing code over to `new::base`. Every export comes with a +/// deprecation message to help users switch to the right tools. +pub mod compat { + #![allow(deprecated)] + #![allow(missing_docs)] + + #[deprecated = "use 'crate::new::base::HeaderFlags' instead."] + pub use header::Flags; + + #[deprecated = "use 'crate::new::base::Header' instead."] + pub use header::HeaderSection; + + #[deprecated = "use 'crate::new::base::SectionCounts' instead."] + pub use header::HeaderCounts; + + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use iana::rtype::Rtype; + + #[deprecated = "use 'crate::new::base::name::Label' instead."] + pub use name::Label; + + #[deprecated = "use 'crate::new::base::name::Name' instead."] + pub use name::Name; + + #[deprecated = "use 'crate::new::base::Question' instead."] + pub use question::Question; + + #[deprecated = "use 'crate::new::base::ParseRecordData' instead."] + pub use rdata::ParseRecordData; + + #[deprecated = "use 'crate::new::rdata::UnknownRecordData' instead."] + pub use rdata::UnknownRecordData; + + #[deprecated = "use 'crate::new::base::Record' instead."] + pub use record::Record; + + #[deprecated = "use 'crate::new::base::TTL' instead."] + pub use record::Ttl; + + #[deprecated = "use 'crate::new::base::Serial' instead."] + pub use serial::Serial; + + pub mod header { + #[deprecated = "use 'crate::new::base::HeaderFlags' instead."] + pub use crate::new::base::HeaderFlags as Flags; + + #[deprecated = "use 'crate::new::base::Header' instead."] + pub use crate::new::base::Header as HeaderSection; + + #[deprecated = "use 'crate::new::base::SectionCounts' instead."] + pub use crate::new::base::SectionCounts as HeaderCounts; + } + + pub mod iana { + #[deprecated = "use 'crate::new::base::RClass' instead."] + pub use class::Class; + + #[deprecated = "use 'crate::new::rdata::DigestType' instead."] + pub use digestalg::DigestAlg; + + #[deprecated = "use 'crate::new::rdata::NSec3HashAlg' instead."] + pub use nsec3::Nsec3HashAlg; + + #[deprecated = "use 'crate::new::edns::OptionCode' instead."] + pub use opt::OptionCode; + + #[deprecated = "for now, just use 'u8', but a better API is coming."] + pub use rcode::Rcode; + + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use rtype::Rtype; + + #[deprecated = "use 'crate::new::rdata::SecAlg' instead."] + pub use secalg::SecAlg; + + pub mod class { + #[deprecated = "use 'crate::new::base::RClass' instead."] + pub use crate::new::base::RClass as Class; + } + + pub mod digestalg { + #[deprecated = "use 'crate::new::rdata::DigestType' instead."] + pub use crate::new::rdata::DigestType as DigestAlg; + } + + pub mod nsec3 { + #[deprecated = "use 'crate::new::rdata::NSec3HashAlg' instead."] + pub use crate::new::rdata::NSec3HashAlg as Nsec3HashAlg; + } + + pub mod opt { + #[deprecated = "use 'crate::new::edns::OptionCode' instead."] + pub use crate::new::edns::OptionCode; + } + + pub mod rcode { + #[deprecated = "for now, just use 'u8', but a better API is coming."] + pub use u8 as Rcode; + } + + pub mod rtype { + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use crate::new::base::RType as Rtype; + } + + pub mod secalg { + #[deprecated = "use 'crate::new::rdata::SecAlg' instead."] + pub use crate::new::rdata::SecAlg; + } + } + + pub mod name { + #[deprecated = "use 'crate::new::base::name::Label' instead."] + pub use crate::new::base::name::Label; + + #[deprecated = "use 'crate::new::base::name::Name' instead."] + pub use crate::new::base::name::Name; + } + + pub mod question { + #[deprecated = "use 'crate::new::base::Question' instead."] + pub use crate::new::base::Question; + } + + pub mod rdata { + #[deprecated = "use 'crate::new::base::ParseRecordData' instead."] + pub use crate::new::base::ParseRecordData; + + #[deprecated = "use 'crate::new::rdata::UnknownRecordData' instead."] + pub use crate::new::rdata::UnknownRecordData; + } + + pub mod record { + #[deprecated = "use 'crate::new::base::Record' instead."] + pub use crate::new::base::Record; + + #[deprecated = "use 'crate::new::base::TTL' instead."] + pub use crate::new::base::TTL as Ttl; + } + + pub mod serial { + #[deprecated = "use 'crate::new::base::Serial' instead."] + pub use crate::new::base::Serial; + } +} diff --git a/src/new/base/name/absolute.rs b/src/new/base/name/absolute.rs new file mode 100644 index 000000000..a6d22cd57 --- /dev/null +++ b/src/new/base/name/absolute.rs @@ -0,0 +1,694 @@ +//! Absolute domain names. + +use core::{ + borrow::{Borrow, BorrowMut}, + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use crate::{ + new::base::{ + build::BuildInMessage, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, + TruncationError, + }, + }, + utils::dst::{UnsizedCopy, UnsizedCopyFrom}, +}; + +use super::{ + CanonicalName, Label, LabelBuf, LabelIter, LabelParseError, + NameCompressor, +}; + +//----------- Name ----------------------------------------------------------- + +/// An absolute domain name. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Name([u8]); + +//--- Constants + +impl Name { + /// The maximum size of a domain name. + pub const MAX_SIZE: usize = 255; + + /// The root name. + pub const ROOT: &'static Self = { + // SAFETY: A root label is the shortest valid name. + unsafe { Self::from_bytes_unchecked(&[0u8]) } + }; +} + +//--- Construction + +impl Name { + /// Assume a byte sequence is a valid [`Name`]. + /// + /// # Safety + /// + /// The byte sequence must represent a valid uncompressed domain name in + /// the conventional wire format (a sequence of labels terminating with a + /// root label, totalling 255 bytes or less). + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Name' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'Name' is sound. + core::mem::transmute(bytes) + } + + /// Assume a mutable byte sequence is a valid [`Name`]. + /// + /// # Safety + /// + /// The byte sequence must represent a valid uncompressed domain name in + /// the conventional wire format (a sequence of labels terminating with a + /// root label, totalling 255 bytes or less). + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'Name' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'Name' is sound. + core::mem::transmute(bytes) + } +} + +//--- Inspection + +impl Name { + /// The size of this name in the wire format. + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Whether this is the root label. + pub const fn is_root(&self) -> bool { + self.0.len() == 1 + } + + /// A byte representation of the [`Name`]. + pub const fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// The labels in the [`Name`]. + pub const fn labels(&self) -> LabelIter<'_> { + // SAFETY: A 'Name' always contains valid encoded labels. + unsafe { LabelIter::new_unchecked(self.as_bytes()) } + } +} + +//--- Canonical operations + +impl CanonicalName for Name { + fn cmp_composed(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } + + fn cmp_lowercase_composed(&self, other: &Self) -> Ordering { + self.as_bytes() + .iter() + .map(u8::to_ascii_lowercase) + .cmp(other.as_bytes().iter().map(u8::to_ascii_lowercase)) + } +} + +//--- Parsing from bytes + +impl<'a> ParseBytes<'a> for &'a Name { + fn parse_bytes(bytes: &'a [u8]) -> Result { + match Self::split_bytes(bytes) { + Ok((this, &[])) => Ok(this), + _ => Err(ParseError), + } + } +} + +impl<'a> SplitBytes<'a> for &'a Name { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let mut offset = 0usize; + while offset < 255 { + match *bytes.get(offset..).ok_or(ParseError)? { + [0, ..] => { + // Found the root, stop. + let (name, rest) = bytes.split_at(offset + 1); + + // SAFETY: 'name' follows the wire format and is 255 bytes + // or shorter. + let name = unsafe { Name::from_bytes_unchecked(name) }; + return Ok((name, rest)); + } + + [l @ 1..=63, ref rest @ ..] if rest.len() >= l as usize => { + // This looks like a regular label. + offset += 1 + l as usize; + } + + _ => return Err(ParseError), + } + } + + Err(ParseError) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for Name { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + if let Some((rest, addr)) = + compressor.compress_name(&contents[..start], self) + { + // The name was compressed, and 'rest' is the uncompressed part. + let end = start + rest.len() + 2; + let bytes = + contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[..rest.len()].copy_from_slice(rest); + + // Add the top bits and the 12-byte offset for the message header. + let addr = (addr + 0xC00C).to_be_bytes(); + bytes[rest.len()..].copy_from_slice(&addr); + Ok(end) + } else { + // The name could not be compressed. + let end = start + self.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(self.as_bytes()); + Ok(end) + } + } +} + +//--- Equality + +impl PartialEq for Name { + fn eq(&self, other: &Self) -> bool { + // Instead of iterating labels, blindly iterate bytes. The locations + // of labels don't matter since we're testing everything for equality. + + // NOTE: Label lengths (which are less than 64) aren't affected by + // 'to_ascii_lowercase', so this method can be applied uniformly. + // This gives the compiler opportunities to vectorize the code. + let lhs = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let rhs = other.as_bytes().iter().map(u8::to_ascii_lowercase); + + lhs.eq(rhs) + } +} + +impl Eq for Name {} + +//--- Comparison + +impl PartialOrd for Name { + fn partial_cmp(&self, that: &Self) -> Option { + Some(self.cmp(that)) + } +} + +impl Ord for Name { + fn cmp(&self, that: &Self) -> Ordering { + // We wish to compare the labels in these names in reverse order. + // Unfortunately, labels in absolute names cannot be traversed + // backwards efficiently. We need to try harder. + // + // Consider two names that are not equal. This means that one name is + // a strict suffix of the other, or that the two had different labels + // at some position. Following this mismatched label is a suffix of + // labels that both names do agree on. + // + // We traverse the bytes in the names in reverse order and find the + // length of their shared suffix. The actual shared suffix, in units + // of labels, may be shorter than this (because the last bytes of the + // mismatched labels could be the same). + // + // Then, we traverse the labels of both names in forward order, until + // we hit the shared suffix territory. We try to match up the names + // in order to discover the real shared suffix. Once the suffix is + // found, the immediately preceding label (if there is one) contains + // the inequality, and can be compared as usual. + + let suffix_len = core::iter::zip( + self.as_bytes().iter().rev().map(u8::to_ascii_lowercase), + that.as_bytes().iter().rev().map(u8::to_ascii_lowercase), + ) + .position(|(a, b)| a != b); + + let Some(suffix_len) = suffix_len else { + // 'iter::zip()' simply ignores unequal iterators, stopping when + // either iterator finishes. Even though the two names had no + // mismatching bytes, one could be longer than the other. + return self.len().cmp(&that.len()); + }; + + // Prepare for forward traversal. + let (mut lhs, mut rhs) = (self.labels(), that.labels()); + // SAFETY: There is at least one unequal byte, and it cannot be the + // root label, so both names have at least one additional label. + let mut prev = unsafe { + (lhs.next().unwrap_unchecked(), rhs.next().unwrap_unchecked()) + }; + + // Traverse both names in lockstep, trying to match their lengths. + loop { + let (llen, rlen) = (lhs.remaining().len(), rhs.remaining().len()); + if llen == rlen && llen <= suffix_len { + // We're in shared suffix territory, and 'lhs' and 'rhs' have + // the same length. Thus, they must be identical, and we have + // found the shared suffix. + break prev.0.cmp(prev.1); + } else if llen > rlen { + // Try to match the lengths by shortening 'lhs'. + + // SAFETY: 'llen > rlen >= 1', thus 'lhs' contains at least + // one additional label before the root. + prev.0 = unsafe { lhs.next().unwrap_unchecked() }; + } else { + // Try to match the lengths by shortening 'rhs'. + + // SAFETY: Either: + // - '1 <= llen < rlen', thus 'rhs' contains at least one + // additional label before the root. + // - 'llen == rlen > suffix_len >= 1', thus 'rhs' contains at + // least one additional label before the root. + prev.1 = unsafe { rhs.next().unwrap_unchecked() }; + } + } + } +} + +//--- Hashing + +impl Hash for Name { + fn hash(&self, state: &mut H) { + for byte in self.as_bytes() { + // NOTE: Label lengths (which are less than 64) aren't affected by + // 'to_ascii_lowercase', so this method can be applied uniformly. + state.write_u8(byte.to_ascii_lowercase()) + } + } +} + +//--- Formatting + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + self.labels().try_for_each(|label| { + if !first { + f.write_str(".")?; + } else { + first = false; + } + + label.fmt(f) + }) + } +} + +impl fmt::Debug for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Name({})", self) + } +} + +//----------- NameBuf -------------------------------------------------------- + +/// A 256-byte buffer containing a [`Name`]. +#[derive(Clone)] +#[repr(C)] // make layout compatible with '[u8; 256]' +pub struct NameBuf { + /// The size of the encoded name. + size: u8, + + /// The buffer containing the [`Name`]. + buffer: [u8; 255], +} + +//--- Construction + +impl NameBuf { + /// Construct an empty, invalid buffer. + const fn empty() -> Self { + Self { + size: 0, + buffer: [0; 255], + } + } + + /// Copy a [`Name`] into a buffer. + pub fn copy_from(name: &Name) -> Self { + let mut buffer = [0u8; 255]; + buffer[..name.len()].copy_from_slice(name.as_bytes()); + Self { + size: name.len() as u8, + buffer, + } + } +} + +impl UnsizedCopyFrom for NameBuf { + type Source = Name; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Parsing from DNS messages + +impl<'a> SplitMessageBytes<'a> for NameBuf { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + // NOTE: The input may be controlled by an attacker. Compression + // pointers can be arranged to cause loops or to access every byte in + // the message in random order. Instead of performing complex loop + // detection, which would probably perform allocations, we simply + // disallow a name to point to data _after_ it. Standard name + // compressors will never generate such pointers. + + let mut buffer = Self::empty(); + + // Perform the first iteration early, to catch the end of the name. + let bytes = contents.get(start..).ok_or(ParseError)?; + let (mut pointer, rest) = parse_segment(bytes, &mut buffer)?; + let orig_end = contents.len() - rest.len(); + + // Traverse compression pointers. + let mut old_start = start; + while let Some(start) = pointer.map(usize::from) { + // Ensure the referenced position comes earlier. + if start >= old_start { + return Err(ParseError); + } + + // Keep going, from the referenced position. + let start = start.checked_sub(12).ok_or(ParseError)?; + let bytes = contents.get(start..).ok_or(ParseError)?; + (pointer, _) = parse_segment(bytes, &mut buffer)?; + old_start = start; + continue; + } + + // Stop and return the original end. + // NOTE: 'buffer' is now well-formed because we only stop when we + // reach a root label (which has been appended into it). + Ok((buffer, orig_end)) + } +} + +impl<'a> ParseMessageBytes<'a> for NameBuf { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + // See 'split_from_message()' for details. The only differences are + // in the range of the first iteration, and the check that the first + // iteration exactly covers the input range. + + let mut buffer = Self::empty(); + + // Perform the first iteration early, to catch the end of the name. + let bytes = contents.get(start..).ok_or(ParseError)?; + let (mut pointer, rest) = parse_segment(bytes, &mut buffer)?; + + if !rest.is_empty() { + // The name didn't reach the end of the input range, fail. + return Err(ParseError); + } + + // Traverse compression pointers. + let mut old_start = start; + while let Some(start) = pointer.map(usize::from) { + // Ensure the referenced position comes earlier. + if start >= old_start { + return Err(ParseError); + } + + // Keep going, from the referenced position. + let start = start.checked_sub(12).ok_or(ParseError)?; + let bytes = contents.get(start..).ok_or(ParseError)?; + (pointer, _) = parse_segment(bytes, &mut buffer)?; + old_start = start; + continue; + } + + // NOTE: 'buffer' is now well-formed because we only stop when we + // reach a root label (which has been appended into it). + Ok(buffer) + } +} + +/// Parse an encoded and potentially-compressed domain name, without +/// following any compression pointer. +fn parse_segment<'a>( + mut bytes: &'a [u8], + buffer: &mut NameBuf, +) -> Result<(Option, &'a [u8]), ParseError> { + loop { + match *bytes { + [0, ref rest @ ..] => { + // Found the root, stop. + buffer.append_bytes(&[0u8]); + return Ok((None, rest)); + } + + [l, ..] if l < 64 => { + // This looks like a regular label. + + if bytes.len() < 1 + l as usize { + // The input doesn't contain the whole label. + return Err(ParseError); + } else if 255 - buffer.size < 2 + l { + // The output name would exceed 254 bytes (this isn't + // the root label, so it can't fill the 255th byte). + return Err(ParseError); + } + + let (label, rest) = bytes.split_at(1 + l as usize); + buffer.append_bytes(label); + bytes = rest; + } + + [hi, lo, ref rest @ ..] if hi >= 0xC0 => { + let pointer = u16::from_be_bytes([hi, lo]); + + // NOTE: We don't verify the pointer here, that's left to + // the caller (since they have to actually use it). + return Ok((Some(pointer & 0x3FFF), rest)); + } + + _ => return Err(ParseError), + } + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for NameBuf { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + <&Name>::split_bytes(bytes) + .map(|(name, rest)| (NameBuf::copy_from(name), rest)) + } +} + +impl<'a> ParseBytes<'a> for NameBuf { + fn parse_bytes(bytes: &'a [u8]) -> Result { + <&Name>::parse_bytes(bytes).map(NameBuf::copy_from) + } +} + +//--- Building into byte sequences + +impl BuildBytes for NameBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Interaction + +impl NameBuf { + /// Append bytes to this buffer. + /// + /// This is an internal convenience function used while building buffers. + fn append_bytes(&mut self, bytes: &[u8]) { + self.buffer[self.size as usize..][..bytes.len()] + .copy_from_slice(bytes); + self.size += bytes.len() as u8; + } + + /// Append a label to this buffer. + /// + /// This is an internal convenience function used while building buffers. + fn append_label(&mut self, label: &Label) { + self.append_bytes(label.as_bytes()); + } +} + +//--- Parsing from strings + +impl FromStr for NameBuf { + type Err = NameParseError; + + /// Parse a name from a string. + /// + /// This is intended for easily constructing hard-coded domain names. The + /// labels in the name should be given in the conventional order (i.e. not + /// reversed), and should be separated by ASCII periods. The labels will + /// be parsed using [`LabelBuf::from_str()`]; see its documentation. This + /// function cannot parse all valid domain names; if an exceptional name + /// needs to be parsed, use [`Name::from_bytes_unchecked()`]. If the + /// input is empty, the root name is returned. + fn from_str(s: &str) -> Result { + let mut this = Self::empty(); + for label in s.split('.') { + let label = + label.parse::().map_err(NameParseError::Label)?; + if 255 - this.size < 1 + label.as_bytes().len() as u8 { + return Err(NameParseError::Overlong); + } + this.append_label(&label); + } + this.append_label(Label::ROOT); + Ok(this) + } +} + +//--- Access to the underlying 'Name' + +impl Deref for NameBuf { + type Target = Name; + + fn deref(&self) -> &Self::Target { + let name = &self.buffer[..self.size as usize]; + // SAFETY: A 'NameBuf' always contains a valid 'Name'. + unsafe { Name::from_bytes_unchecked(name) } + } +} + +impl DerefMut for NameBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let name = &mut self.buffer[..self.size as usize]; + // SAFETY: A 'NameBuf' always contains a valid 'Name'. + unsafe { Name::from_bytes_unchecked_mut(name) } + } +} + +impl Borrow for NameBuf { + fn borrow(&self) -> &Name { + self + } +} + +impl BorrowMut for NameBuf { + fn borrow_mut(&mut self) -> &mut Name { + self + } +} + +impl AsRef for NameBuf { + fn as_ref(&self) -> &Name { + self + } +} + +impl AsMut for NameBuf { + fn as_mut(&mut self) -> &mut Name { + self + } +} + +//--- Forwarding equality, comparison, hashing, and formatting + +impl PartialEq for NameBuf { + fn eq(&self, that: &Self) -> bool { + **self == **that + } +} + +impl Eq for NameBuf {} + +impl PartialOrd for NameBuf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for NameBuf { + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for NameBuf { + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl fmt::Display for NameBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Debug for NameBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +//------------ NameParseError ------------------------------------------------ + +/// An error in parsing a [`Name`] from a string. +/// +/// This can be returned by [`NameBuf::from_str()`]. It is not used when +/// parsing names from the zonefile format, which uses a different mechanism. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NameParseError { + /// The name was too large. + /// + /// Valid names are between 1 and 255 bytes, inclusive. + Overlong, + + /// A label in the name could not be parsed. + Label(LabelParseError), +} + +// TODO(1.81.0): Use 'core::error::Error' instead. +#[cfg(feature = "std")] +impl std::error::Error for NameParseError {} + +impl fmt::Display for NameParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Overlong => "the domain name was too long", + Self::Label(LabelParseError::Overlong) => "a label was too long", + Self::Label(LabelParseError::Empty) => "a label was empty", + Self::Label(LabelParseError::InvalidChar) => { + "the domain name contained an invalid character" + } + }) + } +} diff --git a/src/new/base/name/compressor.rs b/src/new/base/name/compressor.rs new file mode 100644 index 000000000..d87cc8db1 --- /dev/null +++ b/src/new/base/name/compressor.rs @@ -0,0 +1,699 @@ +//! Name compression. + +use crate::new::base::name::{Label, LabelIter}; + +use super::{Name, RevName}; + +/// A domain name compressor. +/// +/// This struct provides name compression functionality when building DNS +/// messages. It compares domain names to those already in the message, and +/// if a shared suffix is found, the newly-inserted name will point at the +/// existing instance of the suffix. +/// +/// This struct stores the positions of domain names already present in the +/// DNS message, as it is otherwise impossible to differentiate domain names +/// from other bytes. Only recently-inserted domain names are stored, and +/// only from the first 16KiB of the message (as compressed names cannot point +/// any further). This is good enough for building small and large messages. +#[repr(align(64))] // align to a typical cache line +pub struct NameCompressor { + /// The last use position of every entry. + /// + /// Every time an entry is used (directly or indirectly), its last use + /// position is updated so that more stale entries are evicted before it. + /// + /// The last use position is calculated somewhat approximately; it would + /// most appropriately be '(number of children, position of inserted + /// name)', but it is approximated as 'position of inserted name + offset + /// into the name if it were uncompressed'. In either formula, the entry + /// with the minimum value would be evicted first. + /// + /// Both formulae guarantee that entries will be evicted before any of + /// their dependencies. The former formula requires at least 19 bits of + /// storage, while the latter requires less than 15 bits. The latter can + /// prioritize a deeply-nested name suffix over a slightly more recently + /// used name that is less nested, but this should be quite rare. + /// + /// Valid values: + /// - Initialized entries: `[1, 16383+253]`. + /// - Uninitialized entries: 0. + last_use: [u16; 32], + + /// The position of each entry. + /// + /// This is a byte offset from the message contents (zero represents the + /// first byte after the 12-byte message header). + /// + /// Valid values: + /// - Initialized entries: `[0, 16383]`. + /// - Uninitialized entries: 0. + pos: [u16; 32], + + /// The length of the relative domain name in each entry. + /// + /// This is the length of the domain name each entry represents, up to + /// (but excluding) the root label or compression pointer. It is used to + /// quickly find the end of the domain name for matching suffixes. + /// + /// Valid values: + /// - Initialized entries: `[2, 253]`. + /// - Uninitialized entries: 0. + len: [u8; 32], + + /// The parent of this entry, if any. + /// + /// If this entry represents a compressed domain name, this value stores + /// the index of that entry. + /// + /// Valid values: + /// - Initialized entries with parents: `[32, 63]`. + /// - Initialized entries without parents: 64. + /// - Uninitialized entries: 0. + parent: [u8; 32], + + /// A 16-bit hash of the entry's last label. + /// + /// An existing entry will be used for compressing a new domain name when + /// the last label in both of them is identical. This field stores a hash + /// over the last label in every entry, to speed up lookups. + /// + /// Valid values: + /// - Initialized entries: `[0, 65535]`. + /// - Uninitialized entries: 0. + hash: [u16; 32], +} + +impl NameCompressor { + /// Construct an empty [`NameCompressor`]. + pub const fn new() -> Self { + Self { + last_use: [0u16; 32], + pos: [0u16; 32], + len: [0u8; 32], + parent: [0u8; 32], + hash: [0u16; 32], + } + } + + /// Compress a [`RevName`]. + /// + /// This is a low-level function; use [`RevName::build_in_message()`] to + /// write a [`RevName`] into a DNS message. + /// + /// Given the contents of the DNS message, determine how to compress the + /// given domain name. If a suitable compression for the name could be + /// found, this function returns the length of the uncompressed suffix as + /// well as the address of the compressed prefix. + /// + /// The contents slice should begin immediately after the 12-byte message + /// header. It must end at the position the name will be inserted. It is + /// assumed that the domain names inserted in these contents still exist + /// from previous calls to [`compress_name()`] and related methods. If + /// this is not true, panics or silently invalid results may occur. + /// + /// The compressor's state will be updated to assume the provided name was + /// inserted into the message. + pub fn compress_revname<'n>( + &mut self, + contents: &[u8], + name: &'n RevName, + ) -> Option<(&'n [u8], u16)> { + // Treat the name as a byte sequence without the root label. + let mut name = &name.as_bytes()[1..]; + + if name.is_empty() { + // Root names are never compressed. + return None; + } + + let mut parent = 64u8; + let mut parent_offset = None; + + // Repeatedly look up entries that could be used for compression. + while !name.is_empty() { + match self.lookup_entry_for_revname(contents, name, parent) { + Some(entry) => { + let tmp; + (parent, name, tmp) = entry; + parent_offset = Some(tmp); + + // This entry was successfully used for compression. + // Record its use at this (approximate) position. + let use_pos = contents.len() + name.len(); + let use_pos = use_pos.max(1); + if use_pos < 16383 + 253 { + self.last_use[parent as usize] = use_pos as u16; + } + } + None => break, + } + } + + // If there is a non-empty uncompressed prefix, register it as a new + // entry here. + if !name.is_empty() && contents.len() < 16384 { + // SAFETY: 'name' is a non-empty sequence of labels. + let first = unsafe { + LabelIter::new_unchecked(name).next().unwrap_unchecked() + }; + + // Pick the entry that was least recently used (or uninitialized). + // + // By the invariants of 'last_use', it is guaranteed that this + // entry is not the parent of any others. + let index = (0usize..32) + .min_by_key(|&i| self.last_use[i]) + .expect("the iterator has 32 elements"); + + self.last_use[index] = contents.len() as u16; + self.pos[index] = contents.len() as u16; + self.len[index] = name.len() as u8; + self.parent[index] = parent; + self.hash[index] = Self::hash_label(first); + } + + // If 'parent_offset' is 'Some', then at least one entry was found, + // and so the name was compressed. + parent_offset.map(|offset| (name, offset)) + } + + /// Look up entries which share a suffix with the given reversed name. + /// + /// At most one entry ends with a complete label matching the given name. + /// We will match suffixes using a linear-time algorithm. + /// + /// On success, the entry's index, the remainder of the name, and the + /// offset of the referenced domain name are returned. + fn lookup_entry_for_revname<'n>( + &self, + contents: &[u8], + name: &'n [u8], + parent: u8, + ) -> Option<(u8, &'n [u8], u16)> { + // SAFETY: 'name' is a sequence of labels. + let mut name_labels = unsafe { LabelIter::new_unchecked(name) }; + // SAFETY: 'name' is non-empty. + let first = unsafe { name_labels.next().unwrap_unchecked() }; + let hash = Self::hash_label(first); + + // Search for an entry with a matching hash and parent. + for i in 0..32 { + // Check the hash first, as it's less likely to match. It's also + // okay if both checks are performed unconditionally. + if self.hash[i] != hash || self.parent[i] != parent { + continue; + }; + + // Look up the entry in the message contents. + let (pos, len) = (self.pos[i] as usize, self.len[i] as usize); + debug_assert_ne!(len, 0); + let mut entry = contents.get(pos..pos + len) + .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state")); + + // Find a shared suffix between the entry and the name. + // + // Comparing a 'Name' to a 'RevName' properly is difficult. We're + // just going for the lazy and not-pedantically-correct version, + // where we blindly match 'RevName' labels against the end of the + // 'Name'. The bytes are definitely correct, but there's a small + // chance that we aren't consistent with label boundaries. + + // TODO(1.80): Use 'slice::split_at_checked()'. + if entry.len() < first.as_bytes().len() + || !entry[entry.len() - first.as_bytes().len()..] + .eq_ignore_ascii_case(first.as_bytes()) + { + continue; + } + entry = &entry[..entry.len() - first.as_bytes().len()]; + + for label in name_labels.clone() { + if entry.len() < label.as_bytes().len() + || !entry[entry.len() - label.as_bytes().len()..] + .eq_ignore_ascii_case(label.as_bytes()) + { + break; + } + entry = &entry[..entry.len() - label.as_bytes().len()]; + } + + // Suffixes from 'entry' that were also in 'name' have been + // removed. The remainder of 'entry' does not match with 'name'. + // 'name' can be compressed using this entry. + let rest = name_labels.remaining(); + let pos = pos + entry.len(); + return Some((i as u8, rest, pos as u16)); + } + + None + } + + /// Compress a [`Name`]. + /// + /// This is a low-level function; use [`Name::build_in_message()`] to + /// write a [`Name`] into a DNS message. + /// + /// Given the contents of the DNS message, determine how to compress the + /// given domain name. If a suitable compression for the name could be + /// found, this function returns the length of the uncompressed prefix as + /// well as the address of the suffix. + /// + /// The contents slice should begin immediately after the 12-byte message + /// header. It must end at the position the name will be inserted. It is + /// assumed that the domain names inserted in these contents still exist + /// from previous calls to [`compress_name()`] and related methods. If + /// this is not true, panics or silently invalid results may occur. + /// + /// The compressor's state will be updated to assume the provided name was + /// inserted into the message. + pub fn compress_name<'n>( + &mut self, + contents: &[u8], + name: &'n Name, + ) -> Option<(&'n [u8], u16)> { + // Treat the name as a byte sequence without the root label. + let mut name = &name.as_bytes()[..name.len() - 1]; + + if name.is_empty() { + // Root names are never compressed. + return None; + } + + let mut hash = Self::hash_label(Self::last_label(name)); + let mut parent = 64u8; + let mut parent_offset = None; + + // Repeatedly look up entries that could be used for compression. + while !name.is_empty() { + match self.lookup_entry_for_name(contents, name, parent, hash) { + Some(entry) => { + let tmp; + (parent, name, hash, tmp) = entry; + parent_offset = Some(tmp); + + // This entry was successfully used for compression. + // Record its use at this (approximate) position. + let use_pos = contents.len() + name.len(); + let use_pos = use_pos.max(1); + if use_pos < 16383 + 253 { + self.last_use[parent as usize] = use_pos as u16; + } + } + None => break, + } + } + + // If there is a non-empty uncompressed prefix, register it as a new + // entry here. We already know what the hash of its last label is. + if !name.is_empty() && contents.len() < 16384 { + // Pick the entry that was least recently used (or uninitialized). + // + // By the invariants of 'last_use', it is guaranteed that this + // entry is not the parent of any others. + let index = (0usize..32) + .min_by_key(|&i| self.last_use[i]) + .expect("the iterator has 32 elements"); + + self.last_use[index] = contents.len() as u16; + self.pos[index] = contents.len() as u16; + self.len[index] = name.len() as u8; + self.parent[index] = parent; + self.hash[index] = hash; + } + + // If 'parent_offset' is 'Some', then at least one entry was found, + // and so the name was compressed. + parent_offset.map(|offset| (name, offset)) + } + + /// Look up entries which share a suffix with the given name. + /// + /// At most one entry ends with a complete label matching the given name. + /// We will carefully match suffixes using a linear-time algorithm. + /// + /// On success, the entry's index, the remainder of the name, the hash of + /// the last label in the remainder of the name (if any), and the offset + /// of the referenced domain name are returned. + fn lookup_entry_for_name<'n>( + &self, + contents: &[u8], + name: &'n [u8], + parent: u8, + hash: u16, + ) -> Option<(u8, &'n [u8], u16, u16)> { + // SAFETY: 'name' is a non-empty sequence of labels. + let name_labels = unsafe { LabelIter::new_unchecked(name) }; + + // Search for an entry with a matching hash and parent. + for i in 0..32 { + // Check the hash first, as it's less likely to match. It's also + // okay if both checks are performed unconditionally. + if self.hash[i] != hash || self.parent[i] != parent { + continue; + }; + + // Look up the entry in the message contents. + let (pos, len) = (self.pos[i] as usize, self.len[i] as usize); + debug_assert_ne!(len, 0); + let entry = contents.get(pos..pos + len) + .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state")); + + // Find a shared suffix between the entry and the name. + // + // We're going to use a not-pendantically-correct implementation + // where we blindly match the ends of the names. The bytes are + // definitely correct, but there's a small chance we aren't + // consistent with label boundaries. + + let suffix_len = core::iter::zip( + name.iter().rev().map(u8::to_ascii_lowercase), + entry.iter().rev().map(u8::to_ascii_lowercase), + ) + .position(|(a, b)| a != b); + + let Some(suffix_len) = suffix_len else { + // 'iter::zip()' simply ignores unequal iterators, stopping + // when either iterator finishes. Even though the two names + // had no mismatching bytes, one could be longer than the + // other. + if name.len() > entry.len() { + // 'entry' is a proper suffix of 'name'. 'name' can be + // compressed using 'entry', and will have at least one + // more label before it. This label needs to be found and + // hashed. + + let rest = &name[..name.len() - entry.len()]; + let hash = Self::hash_label(Self::last_label(rest)); + return Some((i as u8, rest, hash, pos as u16)); + } else { + // 'name' is a suffix of 'entry'. 'name' can be + // compressed using 'entry', and no labels will be left. + let rest = &name[..0]; + let hash = 0u16; + let pos = pos + len - name.len(); + return Some((i as u8, rest, hash, pos as u16)); + } + }; + + // Walk 'name' until we reach the shared suffix region. + + // NOTE: + // - 'suffix_len < min(name.len(), entry.len())'. + // - 'name_labels.remaining.len() == name.len()'. + // - Thus 'suffix_len < name_labels.remaining.len()'. + // - Thus we can move the first statement of the loop here. + // SAFETY: + // - 'name' and 'entry' have a corresponding but unequal byte. + // - Thus 'name' has at least one byte. + // - Thus 'name' has at least one label. + let mut name_labels = name_labels.clone(); + let mut prev_in_name = + unsafe { name_labels.next().unwrap_unchecked() }; + while name_labels.remaining().len() > suffix_len { + // SAFETY: + // - 'LabelIter' is only empty once 'remaining' is empty. + // - 'remaining > suffix_len >= 0'. + prev_in_name = + unsafe { name_labels.next().unwrap_unchecked() }; + } + + // 'entry' and 'name' share zero or more labels, and this shared + // suffix is equal to 'name_labels'. The 'name_label' bytes might + // not lie on the correct label boundaries in 'entry', but this is + // not problematic. If 'name_labels' is non-empty, 'name' can be + // compressed using this entry. + + let suffix_len = name_labels.remaining().len(); + if suffix_len == 0 { + continue; + } + + let rest = &name[..name.len() - suffix_len]; + let hash = Self::hash_label(prev_in_name); + let pos = pos + len - suffix_len; + return Some((i as u8, rest, hash, pos as u16)); + } + + None + } + + /// Find the last label of a domain name. + /// + /// The name must be a valid non-empty sequence of labels. + fn last_label(name: &[u8]) -> &Label { + // The last label begins with a length octet and is followed by + // the corresponding number of bytes. While the length octet + // could look like a valid ASCII character, it would have to be + // 45 (ASCII '-') or above; most labels are not that long. + // + // We will search backwards for a byte that could be the length + // octet of the last label. It is highly likely that exactly one + // match will be found; this is guaranteed to be the right result. + // If more than one match is found, we will fall back to searching + // from the beginning. + // + // It is possible (although unlikely) for LLVM to vectorize this + // process, since it performs 64 unconditional byte comparisons + // over a fixed array. A manually vectorized implementation would + // generate a 64-byte mask for the valid bytes in 'name', load all + // 64 bytes blindly, then do a masked comparison against iota. + + name.iter() + // Take the last 64 bytes of the name. + .rev() + .take(64) + // Compare those bytes against valid length octets. + .enumerate() + .filter_map(|(i, &b)| (i == b as usize).then_some(b)) + // Look for a single valid length octet. + .try_fold(None, |acc, len| match acc { + None => Ok(Some(len)), + Some(_) => Err(()), + }) + // Unwrap the 'Option' since it's guaranteed to be 'Some'. + .transpose() + .unwrap_or_else(|| { + unreachable!("a valid last label could not be found") + }) + // Locate the selected bytes. + .map(|len| { + let bytes = &name[name.len() - len as usize - 1..]; + + // SAFETY: 'name' is a non-empty sequence of labels, and + // we have correctly selected the last label within it. + unsafe { Label::from_bytes_unchecked(bytes) } + }) + // Otherwise, fall back to a forward traversal. + .unwrap_or_else(|()| { + // SAFETY: 'name' is a non-empty sequence of labels. + unsafe { LabelIter::new_unchecked(name) } + .last() + .expect("'name' is not '.'") + }) + } + + /// Hash a label. + fn hash_label(label: &Label) -> u16 { + // This code is copied from the 'hash_bytes()' function of + // 'rustc-hash' v2.1.1, with helpers. The codebase is dual-licensed + // under Apache-2.0 and MIT, with no explicit copyright statement. + // + // 'hash_bytes()' is described as "a wyhash-inspired + // non-collision-resistant hash for strings/slices designed by Orson + // Peters, with a focus on small strings and small codesize." + // + // While the output of 'hash_bytes()' would pass through an additional + // multiplication in 'add_to_hash()', manual testing on some sample + // zonefiles showed that the top 16 bits of the 'hash_bytes()' output + // was already very uniform. + // + // Source: + // + // In order to hash case-insensitively, we aggressively transform the + // input bytes. We cause some collisions, but only in characters we + // don't expect to see in domain names. We do this by mapping bytes + // from 'XX0X_XXXX' to 'XX1X_XXXX'. A full list of effects: + // + // - Control characters (0x00..0x20) become symbols and digits. We + // weren't expecting any control characters to appear anyway. + // + // - Uppercase ASCII characters become lowercased. + // + // - '@[\]^_' become '`{|}~' and DEL. Underscores can occur, but DEL + // is not expected, so the collision is not problematic. + // + // - Half of the non-ASCII space gets folded. Unicode sequences get + // mapped into ASCII using Punycode, so the chance of a non-ASCII + // character here is very low. + + #[cfg(target_pointer_width = "64")] + fn multiply_mix(x: u64, y: u64) -> u64 { + let prod = (x as u128) * (y as u128); + (prod as u64) ^ ((prod >> 64) as u64) + } + + #[cfg(target_pointer_width = "32")] + fn multiply_mix(x: u64, y: u64) -> u64 { + let a = (x & u32::MAX as u64) * (y >> 32); + let b = (y & u32::MAX as u64) * (x >> 32); + a ^ b.rotate_right(32) + } + + const SEED1: u64 = 0x243f6a8885a308d3; + const SEED2: u64 = 0x13198a2e03707344; + const M: u64 = 0xa4093822299f31d0; + + let bytes = label.as_bytes(); + let len = bytes.len(); + let mut s = (SEED1, SEED2); + + if len <= 16 { + // XOR the input into s0, s1. + if len >= 8 { + let i = [&bytes[..8], &bytes[len - 8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + + s.0 ^= i[0]; + s.1 ^= i[1]; + } else if len >= 4 { + let i = [&bytes[..4], &bytes[len - 4..]] + .map(|i| u32::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020); + + s.0 ^= i[0] as u64; + s.1 ^= i[1] as u64; + } else if len > 0 { + let lo = bytes[0] as u64 | 0x20; + let mid = bytes[len / 2] as u64 | 0x20; + let hi = bytes[len - 1] as u64 | 0x20; + s.0 ^= lo; + s.1 ^= (hi << 8) | mid; + } + } else { + // Handle bulk (can partially overlap with suffix). + let mut off = 0; + while off < len - 16 { + let bytes = &bytes[off..off + 16]; + let i = [&bytes[..8], &bytes[8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + + // Replace s1 with a mix of s0, x, and y, and s0 with s1. + // This ensures the compiler can unroll this loop into two + // independent streams, one operating on s0, the other on s1. + // + // Since zeroes are a common input we prevent an immediate + // trivial collapse of the hash function by XOR'ing a constant + // with y. + let t = multiply_mix(s.0 ^ i[0], M ^ i[1]); + s.0 = s.1; + s.1 = t; + off += 16; + } + + let bytes = &bytes[len - 16..]; + let i = [&bytes[..8], &bytes[8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + s.0 ^= i[0]; + s.1 ^= i[1]; + } + + (multiply_mix(s.0, s.1) >> 48) as u16 + } +} + +impl Default for NameCompressor { + fn default() -> Self { + Self::new() + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use crate::new::base::{build::BuildInMessage, name::NameBuf}; + + use super::NameCompressor; + + #[test] + fn no_compression() { + let mut buffer = [0u8; 26]; + let mut compressor = NameCompressor::new(); + + // The TLD is different, so they cannot be compressed together. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "example.com".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07example\x03com\x00" + ); + } + + #[test] + fn single_shared_label() { + let mut buffer = [0u8; 23]; + let mut compressor = NameCompressor::new(); + + // Only the TLD will be shared. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "unequal.org".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07unequal\xC0\x14" + ); + } + + #[test] + fn case_insensitive() { + let mut buffer = [0u8; 23]; + let mut compressor = NameCompressor::new(); + + // The TLD should be shared, even if it differs in case. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "unequal.ORG".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07unequal\xC0\x14" + ); + } +} diff --git a/src/new/base/name/label.rs b/src/new/base/name/label.rs new file mode 100644 index 000000000..e7eac887b --- /dev/null +++ b/src/new/base/name/label.rs @@ -0,0 +1,596 @@ +//! Labels in domain names. + +use core::{ + borrow::{Borrow, BorrowMut}, + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + iter::FusedIterator, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::utils::dst::{UnsizedCopy, UnsizedCopyFrom}; + +//----------- Label ---------------------------------------------------------- + +/// A label in a domain name. +/// +/// A label contains up to 63 bytes of arbitrary data, prefixed with its the +/// number of those bytes. +#[derive(AsBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Label([u8]); + +//--- Associated Constants + +impl Label { + /// The root label. + pub const ROOT: &'static Self = { + // SAFETY: This is a correctly encoded label. + unsafe { Self::from_bytes_unchecked(&[0]) } + }; + + /// The wildcard label. + pub const WILDCARD: &'static Self = { + // SAFETY: This is a correctly encoded label. + unsafe { Self::from_bytes_unchecked(&[1, b'*']) } + }; +} + +//--- Construction + +impl Label { + /// Assume a byte slice is a valid label. + /// + /// # Safety + /// + /// The following conditions must hold for this call to be sound: + /// - `bytes.len() <= 64` + /// - `bytes[0] as usize + 1 == bytes.len()` + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Label' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute(bytes) } + } + + /// Assume a mutable byte slice is a valid label. + /// + /// # Safety + /// + /// The following conditions must hold for this call to be sound: + /// - `bytes.len() <= 64` + /// - `bytes[0] as usize + 1 == bytes.len()` + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'Label' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute(bytes) } + } +} + +//--- Parsing from DNS messages + +impl<'a> ParseMessageBytes<'a> for &'a Label { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +impl<'a> SplitMessageBytes<'a> for &'a Label { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Label { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = &self.0; + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for &'a Label { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let &size = bytes.first().ok_or(ParseError)?; + if size < 64 && bytes.len() > size as usize { + let (label, rest) = bytes.split_at(1 + size as usize); + // SAFETY: + // - 'label.len() = 1 + size <= 64' + // - 'label[0] = size + 1 == label.len()' + Ok((unsafe { Label::from_bytes_unchecked(label) }, rest)) + } else { + Err(ParseError) + } + } +} + +impl<'a> ParseBytes<'a> for &'a Label { + fn parse_bytes(bytes: &'a [u8]) -> Result { + match Self::split_bytes(bytes) { + Ok((this, &[])) => Ok(this), + _ => Err(ParseError), + } + } +} + +//--- Building into byte sequences + +impl BuildBytes for Label { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.0.build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + self.0.len() + } +} + +//--- Inspection + +impl Label { + /// Whether this is the root label. + pub const fn is_root(&self) -> bool { + self.0.len() == 1 + } + + /// Whether this is a wildcard label. + pub const fn is_wildcard(&self) -> bool { + matches!(self.0, [1, b'*']) + } + + /// The bytes making up this label. + pub const fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +//--- Access to the underlying bytes + +impl AsRef<[u8]> for Label { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'a> From<&'a Label> for &'a [u8] { + fn from(value: &'a Label) -> Self { + &value.0 + } +} + +//--- Comparison + +impl PartialEq for Label { + /// Compare two labels for equality. + /// + /// Labels are compared ASCII-case-insensitively. + fn eq(&self, other: &Self) -> bool { + let this = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let that = other.as_bytes().iter().map(u8::to_ascii_lowercase); + this.eq(that) + } +} + +impl Eq for Label {} + +//--- Ordering + +impl PartialOrd for Label { + /// Determine the order between labels. + /// + /// Any uppercase ASCII characters in the labels are treated as if they + /// were lowercase. The first unequal byte between two labels determines + /// its ordering: the label with the smaller byte value is the lesser. If + /// two labels have all the same bytes, the shorter label is lesser; if + /// they are the same length, they are equal. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Label { + /// Determine the order between labels. + /// + /// Any uppercase ASCII characters in the labels are treated as if they + /// were lowercase. The first unequal byte between two labels determines + /// its ordering: the label with the smaller byte value is the lesser. If + /// two labels have all the same bytes, the shorter label is lesser; if + /// they are the same length, they are equal. + fn cmp(&self, other: &Self) -> Ordering { + let this = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let that = other.as_bytes().iter().map(u8::to_ascii_lowercase); + this.cmp(that) + } +} + +//--- Hashing + +impl Hash for Label { + /// Hash this label. + /// + /// All uppercase ASCII characters are lowercased beforehand. This way, + /// the hash of a label is case-independent, consistent with how labels + /// are compared and ordered. + /// + /// The label is hashed as if it were a name containing a single label -- + /// the length octet is thus included. This makes the hashing consistent + /// between names and tuples (not slices!) of labels. + fn hash(&self, state: &mut H) { + for &byte in self.as_bytes() { + state.write_u8(byte.to_ascii_lowercase()) + } + } +} + +//--- Formatting + +impl fmt::Display for Label { + /// Print a label. + /// + /// The label is printed in the conventional zone file format, with bytes + /// outside printable ASCII formatted as `\\DDD` (a backslash followed by + /// three zero-padded decimal digits), and `.` and `\\` simply escaped by + /// a backslash. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_bytes().iter().try_for_each(|&byte| { + if b".\\".contains(&byte) { + write!(f, "\\{}", byte as char) + } else if byte.is_ascii_graphic() { + write!(f, "{}", byte as char) + } else { + write!(f, "\\{:03}", byte) + } + }) + } +} + +impl fmt::Debug for Label { + /// Print a label for debugging purposes. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Label") + .field(&format_args!("{}", self)) + .finish() + } +} + +//----------- LabelBuf ------------------------------------------------------- + +/// A 64-byte buffer holding a [`Label`]. +#[derive(Clone)] +#[repr(transparent)] +pub struct LabelBuf { + /// The label bytes. + data: [u8; 64], +} + +//--- Construction + +impl LabelBuf { + /// Copy a [`Label`] into a buffer. + pub fn copy_from(label: &Label) -> Self { + let bytes = label.as_bytes(); + let mut data = [0u8; 64]; + data[..bytes.len()].copy_from_slice(bytes); + Self { data } + } +} + +impl UnsizedCopyFrom for LabelBuf { + type Source = Label; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Parsing from strings + +impl FromStr for LabelBuf { + type Err = LabelParseError; + + /// Parse a label from a string. + /// + /// This is intended for easily constructing hard-coded labels. The input + /// is not expected to be in the zonefile format; it should simply contain + /// 1 to 63 characters, each being a plain ASCII alphanumeric or a hyphen. + /// To construct a label containing bytes outside this range, use + /// [`Label::from_bytes_unchecked()`]. To construct a root label, use + /// [`Label::ROOT`]. + fn from_str(s: &str) -> Result { + if s == "*" { + Ok(Self::copy_from(Label::WILDCARD)) + } else if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') { + Err(LabelParseError::InvalidChar) + } else if s.is_empty() { + Err(LabelParseError::Empty) + } else if s.len() > 63 { + Err(LabelParseError::Overlong) + } else { + let bytes = s.as_bytes(); + let mut data = [0u8; 64]; + data[0] = bytes.len() as u8; + data[1..1 + bytes.len()].copy_from_slice(bytes); + Ok(Self { data }) + } + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for LabelBuf { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +impl SplitMessageBytes<'_> for LabelBuf { + fn split_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for LabelBuf { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + Label::build_in_message(self, contents, start, compressor) + } +} + +//--- Parsing from byte sequences + +impl ParseBytes<'_> for LabelBuf { + fn parse_bytes(bytes: &[u8]) -> Result { + <&Label>::parse_bytes(bytes).map(Self::copy_from) + } +} + +impl SplitBytes<'_> for LabelBuf { + fn split_bytes(bytes: &'_ [u8]) -> Result<(Self, &'_ [u8]), ParseError> { + <&Label>::split_bytes(bytes) + .map(|(label, rest)| (Self::copy_from(label), rest)) + } +} + +//--- Building into byte sequences + +impl BuildBytes for LabelBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Access to the underlying 'Label' + +impl Deref for LabelBuf { + type Target = Label; + + fn deref(&self) -> &Self::Target { + let size = self.data[0] as usize; + let label = &self.data[..1 + size]; + // SAFETY: A 'LabelBuf' always contains a valid 'Label'. + unsafe { Label::from_bytes_unchecked(label) } + } +} + +impl DerefMut for LabelBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let size = self.data[0] as usize; + let label = &mut self.data[..1 + size]; + // SAFETY: A 'LabelBuf' always contains a valid 'Label'. + unsafe { Label::from_bytes_unchecked_mut(label) } + } +} + +impl Borrow
for Ipv4Addr { + fn from(value: A) -> Self { + Self::from(value.octets) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for A { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.octets.cmp(&other.octets) + } +} + +//--- Parsing from a string + +impl FromStr for A { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ipv4Addr::from_str(s).map(A::from) + } +} + +//--- Formatting + +impl fmt::Debug for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "A({self})") + } +} + +impl fmt::Display for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ipv4Addr::from(*self).fmt(f) + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for A { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for A { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + core::mem::size_of::(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing record data + +impl ParseRecordData<'_> for A {} + +impl ParseRecordDataBytes<'_> for A { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::A => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} + +impl<'a> ParseRecordData<'a> for &'a A {} + +impl<'a> ParseRecordDataBytes<'a> for &'a A { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::A => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/cname.rs b/src/new/rdata/basic/cname.rs new file mode 100644 index 000000000..7b9ff4c61 --- /dev/null +++ b/src/new/rdata/basic/cname.rs @@ -0,0 +1,199 @@ +//! The CNAME record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- CName ---------------------------------------------------------- + +/// The canonical name for this domain. +/// +/// A [`CName`] record indicates that a domain name is an alias. Any data +/// associated with that domain name originates from the "canonical" domain +/// name (with a few DNSSEC-related exceptions). If a domain name is an +/// alias, it has a single canonical name (see [RFC 2181, section 10.1]); it +/// cannot have multiple distinct [`CName`] records. +/// +/// [RFC 2181, section 10.1]: https://datatracker.ietf.org/doc/html/rfc2181#section-10.1 +/// +/// [`CName`] is specified by [RFC 1035, section 3.3.1]. The behaviour of DNS +/// lookups and name servers is specified by [RFC 1034, section 3.6.2]. +/// +/// [RFC 1034, section 3.6.2]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 +/// [RFC 1035, section 3.3.1]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.1 +/// +/// ## Wire Format +/// +/// The wire format of a [`CName`] record is simply the canonical domain name. +/// This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`CName`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`CName`], it's first important to choose a domain +/// name type. For short-term usage (where the [`CName`] is a local +/// variable), it is common to pick [`RevNameBuf`]. If the [`CName`] will +/// be placed on the heap, Box<[`RevName`]> will be more +/// efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`CName`] is to construct each +/// field manually. To parse a [`CName`] from a DNS message, use +/// [`ParseMessageBytes`]. In case the input bytes don't use name +/// compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::CName; +/// # +/// // Build a 'CName' manually: +/// let manual: CName = CName { +/// name: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'CName' from the wire format, without name decompression: +/// let from_wire: CName = CName::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`CName`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`CName::map_name()`] and +/// [`CName::map_name_by_ref()`]. +/// +/// For debugging, [`CName`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`CName`] in the wire format, use [`BuildInMessage`] +/// (which supports name compression). If name compression is not desired, +/// use [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct CName { + /// The canonical name. + pub name: N, +} + +//--- Interaction + +impl CName { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> CName { + CName { + name: (f)(self.name), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> CName { + CName { + name: (f)(&self.name), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for CName { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.name.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.name.cmp_lowercase_composed(&other.name) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for CName { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|name| Self { name }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CName { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.name.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for CName { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::CNAME => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for CName { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::CNAME => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/hinfo.rs b/src/new/rdata/basic/hinfo.rs new file mode 100644 index 000000000..0fd8367a9 --- /dev/null +++ b/src/new/rdata/basic/hinfo.rs @@ -0,0 +1,229 @@ +//! The HINFO record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, +}; +use crate::new::base::{ + CanonicalRecordData, CharStr, ParseRecordData, ParseRecordDataBytes, + RType, +}; + +//----------- HInfo ---------------------------------------------------------- + +/// Information about the host computer. +/// +/// [`HInfo`] describes the hardware and software of the server associated +/// with the domain name. It is not commonly used for its original purpose, +/// given several issues: +/// +/// 1. A domain name can be associated with multiple servers (due to having +/// multiple IP addresses or using IP anycast), but [`HInfo`] does not +/// provide a way to associate the information it provides with a specific +/// server (or at least IP address). +/// +/// 2. The CPU and OS name are expected to be standardized, but given the +/// massive (and growing) number of both, it would be impossible to cover +/// every possibility. [RFC 1010] listed the initial set of names, and it +/// has evolved into the online lists of [operating system names] (last +/// updated in 2010) and [machine names] (last updated in 2001). +/// +/// 3. As documented by [RFC 1035], the "main use" for [`HInfo`] records was +/// "for protocols such as FTP that can use special procedures when talking +/// between machines or operating systems of the same type". But given the +/// portabilitiy of most protocols across machines and operating systems, +/// [`HInfo`] is not very informative. Protocols typically provide +/// extension mechanisms in-band instead of relying on out-of-band DNS +/// information. +/// +/// 4. [RFC 8482, section 6] states that "the HINFO RRTYPE is believed to be +/// rarely used in the DNS at the time of writing, based on observations +/// made in passive DNS and at recursive and authoritative DNS servers". +/// +/// [RFC 1010]: https://datatracker.ietf.org/doc/html/rfc1010 +/// [RFC 1035]: https://datatracker.ietf.org/doc/html/rfc1035 +/// [RFC 8482, section 6]: https://datatracker.ietf.org/doc/html/rfc8482#section-6 +/// [operating system names]: https://www.iana.org/assignments/operating-system-names/operating-system-names.xhtml +/// [machine names]: https://www.iana.org/assignments/machine-names/machine-names.xhtml +/// +/// Recently, [`HInfo`] has gained new use, as a potential fallback response +/// for [`QType::ANY`] queries. [RFC 8482] specifies that name servers +/// wishing to avoid answering [`QType::ANY`] queries (which are expensive +/// to look up, have an amplifying network effect, and can be abused for DoS +/// attacks) can respond with a synthesized [`HInfo`] record instead. +/// +/// [`QType::ANY`]: crate::new::base::QType::ANY +/// [RFC 8482]: https://datatracker.ietf.org/doc/html/rfc8482 +/// +/// [`HInfo`] is specified by [RFC 1035, section 3.3.2]. Its use as an +/// alternative response to [`QType::ANY`] queries is documented by [RFC 8482, +/// section 4.2]. +/// +/// [RFC 1035, section 3.3.2]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.2 +/// [RFC 8482, section 4.2]: https://datatracker.ietf.org/doc/html/rfc8482#section-4.2 +/// +/// ## Wire Format +/// +/// The wire format of an [`HInfo`] record is the concatenation of two +/// "character strings" (see [`CharStr`]). The first specifies the "machine +/// name" of the host computer, and the second specifies the name of the +/// operating system it is running. +/// +/// ## Usage +/// +/// Because [`HInfo`] is a record data type, it is usually handled within an +/// enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// There's a few ways to build an [`HInfo`]: +/// +/// ``` +/// # use domain::new::base::wire::{BuildBytes, ParseBytes}; +/// # use domain::new::rdata::HInfo; +/// # +/// use domain::new::base::CharStrBuf; +/// +/// // Build an 'HInfo' manually. +/// let cpu: CharStrBuf = "DEC-2060".parse().unwrap(); +/// let os: CharStrBuf = "TOPS20".parse().unwrap(); +/// let manual: HInfo<'_> = HInfo { cpu: &*cpu, os: &*os }; +/// +/// let bytes = b"\x08DEC-2060\x06TOPS20"; +/// # let mut buffer = [0u8; 16]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'HInfo' from the DNS wire format. +/// let from_wire: HInfo<'_> = HInfo::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// ``` +/// +/// Since [`HInfo`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, it is bound by +/// the lifetime of the borrowed character strings. At the moment, there is +/// no perfect way to own an [`HInfo`] without a lifetime restriction (largely +/// because it is not commonly used), however: +/// +#[cfg_attr(feature = "alloc", doc = " - [`BoxedRecordData`] ")] +#[cfg_attr(not(feature = "alloc"), doc = " - `BoxedRecordData` ")] +/// is capable of doing so, but it does not guarantee that it holds an +/// [`HInfo`] (it can hold any record data type). +/// +/// - If [`bumpalo`] is being used, +#[cfg_attr(feature = "bumpalo", doc = " [`HInfo::clone_to_bump()`]")] +#[cfg_attr(not(feature = "bumpalo"), doc = " `HInfo::clone_to_bump()`")] +/// can clone an [`HInfo`] over to a bump allocator. This may extend its +/// lifetime sufficiently for some use cases. +/// +#[cfg_attr( + not(feature = "bumpalo"), + doc = "[`bumpalo`]: https://docs.rs/bumpalo/latest/bumpalo/" +)] +#[cfg_attr( + feature = "alloc", + doc = "[`BoxedRecordData`]: crate::new::rdata::BoxedRecordData" +)] +/// +/// For debugging [`HInfo`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`HInfo`] in the wire format, use [`BuildBytes`]. It also +/// supports [`BuildInMessage`]. +#[derive( + Copy, Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes, SplitBytes, +)] +pub struct HInfo<'a> { + /// The type of the machine hosting the domain name. + pub cpu: &'a CharStr, + + /// The type of the operating system hosting the domain name. + pub os: &'a CharStr, +} + +//--- Interaction + +impl HInfo<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> HInfo<'r> { + use crate::utils::dst::copy_to_bump; + + HInfo { + cpu: copy_to_bump(self.cpu, bump), + os: copy_to_bump(self.os, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for HInfo<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this = ( + self.cpu.len(), + &self.cpu.octets, + self.os.len(), + &self.os.octets, + ); + let that = ( + that.cpu.len(), + &that.cpu.octets, + that.os.len(), + &that.os.octets, + ); + this.cmp(&that) + } +} + +//--- Parsing from DNS messages + +impl<'a> ParseMessageBytes<'a> for HInfo<'a> { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for HInfo<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self.cpu.build_in_message(contents, start, compressor)?; + start = self.os.build_in_message(contents, start, compressor)?; + Ok(start) + } +} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for HInfo<'a> {} + +impl<'a> ParseRecordDataBytes<'a> for HInfo<'a> { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::HINFO => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/mod.rs b/src/new/rdata/basic/mod.rs new file mode 100644 index 000000000..d633d50a7 --- /dev/null +++ b/src/new/rdata/basic/mod.rs @@ -0,0 +1,27 @@ +//! Core record data types. +//! +//! See [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035). + +mod a; +pub use a::A; + +mod ns; +pub use ns::Ns; + +mod cname; +pub use cname::CName; + +mod soa; +pub use soa::Soa; + +mod ptr; +pub use ptr::Ptr; + +mod hinfo; +pub use hinfo::HInfo; + +mod mx; +pub use mx::Mx; + +mod txt; +pub use txt::Txt; diff --git a/src/new/rdata/basic/mx.rs b/src/new/rdata/basic/mx.rs new file mode 100644 index 000000000..a9b5f0ac0 --- /dev/null +++ b/src/new/rdata/basic/mx.rs @@ -0,0 +1,218 @@ +//! The MX record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Mx ------------------------------------------------------------- + +/// A host that can exchange mail for this domain. +/// +/// An [`Mx`] record indicates that a domain name can receive e-mail, and it +/// specifies (the domain name of) the mail server that e-mail for that domain +/// should be sent to. A domain name can be associated with multiple mail +/// servers (using multiple [`Mx`] records); each one is assigned a priority +/// for load balancing. +/// +// TODO: If there's a conventional algorithm for picking a mail server (i.e. +// how the probabilities are calculated for a random selection), add it here. +// +/// [`Mx`] is specified by [RFC 1035, section 3.3.9]. +/// +/// [RFC 1035, section 3.3.9]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 +/// +/// ## Wire Format +/// +/// The wire format of an [`Mx`] record is the 16-bit preference number (as a +/// big-endian integer) followed by the domain name of the mail server. This +/// domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Mx`] is a record data type, it is usually handled within an enum +/// like [`RecordData`]. This section describes how to use it independently +/// (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build an [`Mx`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Mx`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Mx`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Mx`] is to construct each field manually. +/// To parse an [`Mx`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Mx; +/// # +/// // Build an 'Mx' manually: +/// let manual: Mx = Mx { +/// preference: 10.into(), +/// exchange: "example.org".parse().unwrap(), +/// }; +/// +/// let bytes = b"\x00\x0A\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 15]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'Mx' from the wire format, without name decompression: +/// let from_wire: Mx = Mx::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Mx`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Mx::map_name()`] and +/// [`Mx::map_name_by_ref()`]. +/// +/// For debugging, [`Mx`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`Mx`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(C)] +pub struct Mx { + /// The preference for this host over others. + pub preference: U16, + + /// The domain name of the mail exchanger. + pub exchange: N, +} + +//--- Interaction + +impl Mx { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Mx { + Mx { + preference: self.preference, + exchange: (f)(self.exchange), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Mx { + Mx { + preference: self.preference, + exchange: (f)(&self.exchange), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Mx { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let bytes = self.preference.build_bytes(bytes)?; + let bytes = self.exchange.build_lowercased_bytes(bytes)?; + Ok(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.preference.cmp(&other.preference).then_with(|| { + self.exchange.cmp_lowercase_composed(&other.exchange) + }) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Mx { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + let (&preference, rest) = + <&U16>::split_message_bytes(contents, start)?; + let exchange = N::parse_message_bytes(contents, rest)?; + Ok(Self { + preference, + exchange, + }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Mx { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self + .preference + .as_bytes() + .build_in_message(contents, start, compressor)?; + start = self + .exchange + .build_in_message(contents, start, compressor)?; + Ok(start) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Mx { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::MX => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Mx { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::MX => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/ns.rs b/src/new/rdata/basic/ns.rs new file mode 100644 index 000000000..a4040ba14 --- /dev/null +++ b/src/new/rdata/basic/ns.rs @@ -0,0 +1,204 @@ +//! The NS record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Ns ------------------------------------------------------------- + +/// The authoritative name server for this domain. +/// +/// An [`Ns`] record indicates that a domain name is the apex of a DNS zone, +/// and it specifies (the domain name of) the name server that queries about +/// the domain name (and its descendants) should be sent to. A domain name +/// can be associated with multiple name servers (using multiple [`Ns`] +/// records). +/// +/// DNS is designed around the concept of delegating responsibility for domain +/// names. If a name server responds to a query with an empty answer section, +/// but with [`Ns`] records in the authority section, it is claiming to not be +/// the authoritative source of information about the queried domain name; +/// the [`Ns`] records specify name servers to whom that authority has been +/// delegated. +/// +/// While [`Ns`] records are typically served by a name server to indicate a +/// zone cut, that name server is not authoritative for the record; the [`Ns`] +/// record belongs to the delegated zone and the delegated name server(s). +/// +/// [`Ns`] is specified by [RFC 1035, section 3.3.11]. +/// +/// [RFC 1035, section 3.3.11]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 +/// +/// ## Wire format +/// +/// The wire format of an [`Ns`] record is simply the domain name of the name +/// server. This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Ns`] is a record data type, it is usually handled within an enum +/// like [`RecordData`]. This section describes how to use it independently +/// (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build an [`Ns`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Ns`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Ns`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Ns`] is to construct each field manually. +/// To parse an [`Ns`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Ns; +/// # +/// // Build an 'Ns' manually: +/// let manual: Ns = Ns { +/// server: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'Ns' from the wire format, without name decompression: +/// let from_wire: Ns = Ns::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Ns`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Ns::map_name()`] and +/// [`Ns::map_name_by_ref()`]. +/// +/// For debugging, [`Ns`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`Ns`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct Ns { + /// The name of the authoritative server. + pub server: N, +} + +//--- Interaction + +impl Ns { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Ns { + Ns { + server: (f)(self.server), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Ns { + Ns { + server: (f)(&self.server), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Ns { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.server.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.server.cmp_lowercase_composed(&other.server) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Ns { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|server| Self { server }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Ns { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.server.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Ns { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::NS => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Ns { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::NS => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/ptr.rs b/src/new/rdata/basic/ptr.rs new file mode 100644 index 000000000..7312a406c --- /dev/null +++ b/src/new/rdata/basic/ptr.rs @@ -0,0 +1,193 @@ +//! The PTR record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Ptr ------------------------------------------------------------ + +/// A pointer to another domain name. +/// +/// A [`Ptr`] record is used with special domain names for pointing to other +/// locations in the domain name space. It is conventionally used for reverse +/// lookups: for example, the [`Ptr`] record for `.in-addr.arpa` points +/// to the domain name using the IPv4 `` in an [`A`] record. The same +/// technique works with `.ip6.arpa` for IPv6 addresses. +/// +/// [`A`]: crate::new::rdata::A +/// +/// [`Ptr`] is specified by [RFC 1035, section 3.3.12]. +/// +/// [RFC 1035, section 3.3.12]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.12 +/// +/// ## Wire format +/// +/// The wire format of a [`Ptr`] record is simply the domain name of the name +/// server. This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Ptr`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`Ptr`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Ptr`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Ptr`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Ptr`] is to construct each field manually. +/// To parse a [`Ptr`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Ptr; +/// # +/// // Build a 'Ptr' manually: +/// let manual: Ptr = Ptr { +/// name: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'Ptr' from the wire format, without name decompression: +/// let from_wire: Ptr = Ptr::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Ptr`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Ptr::map_name()`] and +/// [`Ptr::map_name_by_ref()`]. +/// +/// For debugging, [`Ptr`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`Ptr`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct Ptr { + /// The referenced domain name. + pub name: N, +} + +//--- Interaction + +impl Ptr { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Ptr { + Ptr { + name: (f)(self.name), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Ptr { + Ptr { + name: (f)(&self.name), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Ptr { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.name.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.name.cmp_lowercase_composed(&other.name) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Ptr { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|name| Self { name }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Ptr { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.name.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Ptr { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::PTR => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Ptr { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::PTR => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/soa.rs b/src/new/rdata/basic/soa.rs new file mode 100644 index 000000000..b4b18b48f --- /dev/null +++ b/src/new/rdata/basic/soa.rs @@ -0,0 +1,359 @@ +//! The SOA record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::{ + wire::*, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::new::base::{CanonicalRecordData, Serial}; + +//----------- Soa ------------------------------------------------------------ + +/// The start of a zone of authority. +/// +/// A [`Soa`] record indicates that a domain name is the apex of a DNS zone. +/// It provides several parameters to describe how the zone should be used, +/// e.g. how often it should be refreshed. +/// +/// [`Soa`]'s most important use is to detect changes to the zone. Whenever +/// the zone is changed, [`Soa::serial`] is incremented; secondary DNS servers +/// (which cache and redistribute the contents of the zone) can thus detect +/// whether they need to update their cache. +/// +// TODO: Is there a strict definition to "whenever the zone is changed"? +// +/// Every zone has exactly one [`Soa`] record, and it is located at the apex. +/// The zone (along with its authoritative name servers) is authoritative for +/// the record. +/// +/// [`Soa`] is specified by [RFC 1035, section 3.3.13]. The behaviour of +/// secondary name servers using [`Soa`] to check for updates to a zone is +/// specified by [RFC 1034, section 4.3.5]. +/// +/// [RFC 1034, section 4.3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-4.3.5 +/// [RFC 1035, section 3.3.13]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 +/// +/// ## Wire format +/// +/// The wire format of a [`Soa`] record is the concatenation of its fields, in +/// the same order as the `struct` definition. The domain names within a +/// [`Soa`] may be compressed in DNS messages. Every other field is an +/// unsigned 32-bit big-endian integer. +/// +/// ## Usage +/// +/// Because [`Soa`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`Soa`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Soa`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Soa`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Soa`] is to construct each +/// field manually. To parse a [`Soa`] from a DNS message, use +/// [`ParseMessageBytes`]. In case the input bytes don't use name +/// compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Soa; +/// # +/// // Build a 'Soa' manually: +/// let manual: Soa = Soa { +/// mname: "ns.example.org".parse().unwrap(), +/// rname: "admin.example.org".parse().unwrap(), +/// serial: 42.into(), +/// refresh: 3600.into(), +/// retry: 600.into(), +/// expire: 18000.into(), +/// minimum: 150.into(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\ +/// \x02ns\x07example\x03org\x00\ +/// \x05admin\x07example\x03org\x00\ +/// \x00\x00\x00\x2A\ +/// \x00\x00\x0E\x10\ +/// \x00\x00\x02\x58\ +/// \x00\x00\x46\x50\ +/// \x00\x00\x00\x96"; +/// # let mut buffer = [0u8; 55]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'Soa' from the wire format, without name decompression: +/// let from_wire: Soa = Soa::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Soa`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Soa::map_names()`] and +/// [`Soa::map_names_by_ref()`]. +/// +/// For debugging, [`Soa`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`Soa`] in the wire format, use [`BuildInMessage`] +/// (which supports name compression). If name compression is not desired, +/// use [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +pub struct Soa { + /// The original/primary name server for this zone. + /// + /// This domain name should point to a name server that is authoritative + /// for this zone -- more specifically, that is the original source of + /// information that all other name servers are (directly or indirectly) + /// loading this zone from. This need not be listed in the [`Ns`] records + /// for this zone, if it is not intended for public querying. + /// + /// [`Ns`]: crate::new::rdata::Ns + pub mname: N, + + /// The mailbox of the maintainer of this zone. + /// + /// The first label here is the username (i.e. local part) of the e-mail + /// address, and the remaining labels make up the mail domain name. For + /// example, would be represented as + /// `hostmaster.sri-nic.arpa`. This convention is specified in [RFC 1034, + /// section 3.3]. + /// + /// [RFC 1034, section 3.3]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.3 + pub rname: N, + + /// The version number of the original copy of this zone. + /// + /// This value is increased when the contents of the zone change. If a + /// secondary name server wishes to cache the contents of this zone, it + /// can periodically check the version number from the primary name server + /// to determine whether it needs to update its cache. + /// + /// There are multiple conventions for versioning strategies. Some zones + /// will increase this value by 1 when a change occurs; some set it to the + /// Unix timestamp of the latest change; others set it so that the decimal + /// representation includes the current date. As long as the version + /// number increases (by a relatively small value) upon every change, any + /// strategy is valid. + /// + /// This field is represented using [`Serial`], which provides special + /// "sequence space arithmetic". This ensures that ordering comparisons + /// are well-defined even if the number overflows modulo `2^32`. See its + /// documentation for more information. + pub serial: Serial, + + /// The number of seconds to wait until refreshing the zone. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. The server is + /// expected to wait this long (in seconds) after the last successful + /// check, before checking again. + /// + /// If checking fails, however, the server uses a different periodicity; + /// see [`Soa::retry`]. + /// + /// Note that there are alternative means for keeping up to date with a + /// primary name server -- see DNS NOTIFY ([RFC 1996]). + /// + /// [RFC 1996]: https://datatracker.ietf.org/doc/html/rfc1996 + pub refresh: U32, + + /// The number of seconds to wait until retrying a failed refresh. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. The server is + /// expected to wait this long (in seconds) after the last _failing_ check + /// before trying again. + /// + /// Once a check is successful, the server should resume using the + /// [`Soa::refresh`] time. + pub retry: U32, + + /// The number of seconds until the zone is considered expired. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. If the server + /// fails to check for or retrieve updates to the zone for this period of + /// time (in seconds), it should consider its copy of the zone obsolete + /// and should discard it. + pub expire: U32, + + /// The minimum TTL for any record in this zone. + /// + /// The meaning of this field has changed over time. According to [RFC + /// 2308, section 4], it is the time for which a negative response (i.e. + /// that a certain record does not exist) should be cached. [RFC 4035, + /// section 2.3] likewise states that the [`NSec`] records for a zone + /// should have a TTL of this value. + /// + /// [`NSec`]: crate::new::rdata::NSec + /// [RFC 2308, section 4]: https://datatracker.ietf.org/doc/html/rfc2308#section-4 + /// [RFC 4035, section 2.3]: https://datatracker.ietf.org/doc/html/rfc4035#section-2.3 + pub minimum: U32, +} + +//--- Interaction + +impl Soa { + /// Map the domain names within to another type. + pub fn map_names R>(self, mut f: F) -> Soa { + Soa { + mname: (f)(self.mname), + rname: (f)(self.rname), + serial: self.serial, + refresh: self.refresh, + retry: self.retry, + expire: self.expire, + minimum: self.minimum, + } + } + + /// Map references to the domain names within to another type. + pub fn map_names_by_ref<'r, R, F: FnMut(&'r N) -> R>( + &'r self, + mut f: F, + ) -> Soa { + Soa { + mname: (f)(&self.mname), + rname: (f)(&self.rname), + serial: self.serial, + refresh: self.refresh, + retry: self.retry, + expire: self.expire, + minimum: self.minimum, + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Soa { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let bytes = self.mname.build_lowercased_bytes(bytes)?; + let bytes = self.rname.build_lowercased_bytes(bytes)?; + let bytes = self.serial.build_bytes(bytes)?; + let bytes = self.refresh.build_bytes(bytes)?; + let bytes = self.retry.build_bytes(bytes)?; + let bytes = self.expire.build_bytes(bytes)?; + let bytes = self.minimum.build_bytes(bytes)?; + Ok(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.mname + .cmp_lowercase_composed(&other.mname) + .then_with(|| self.rname.cmp_lowercase_composed(&other.rname)) + .then_with(|| self.serial.as_bytes().cmp(other.serial.as_bytes())) + .then_with(|| self.refresh.cmp(&other.refresh)) + .then_with(|| self.retry.cmp(&other.retry)) + .then_with(|| self.expire.cmp(&other.expire)) + .then_with(|| self.minimum.cmp(&other.minimum)) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: SplitMessageBytes<'a>> ParseMessageBytes<'a> for Soa { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + let (mname, rest) = N::split_message_bytes(contents, start)?; + let (rname, rest) = N::split_message_bytes(contents, rest)?; + let (&serial, rest) = <&Serial>::split_message_bytes(contents, rest)?; + let (&refresh, rest) = <&U32>::split_message_bytes(contents, rest)?; + let (&retry, rest) = <&U32>::split_message_bytes(contents, rest)?; + let (&expire, rest) = <&U32>::split_message_bytes(contents, rest)?; + let &minimum = <&U32>::parse_message_bytes(contents, rest)?; + + Ok(Self { + mname, + rname, + serial, + refresh, + retry, + expire, + minimum, + }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Soa { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self.mname.build_in_message(contents, start, compressor)?; + start = self.rname.build_in_message(contents, start, compressor)?; + // Build the remaining bytes manually. + let end = start + 20; + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[0..4].copy_from_slice(self.serial.as_bytes()); + bytes[4..8].copy_from_slice(self.refresh.as_bytes()); + bytes[8..12].copy_from_slice(self.retry.as_bytes()); + bytes[12..16].copy_from_slice(self.expire.as_bytes()); + bytes[16..20].copy_from_slice(self.minimum.as_bytes()); + Ok(end) + } +} + +//--- Parsing record data + +impl<'a, N: SplitMessageBytes<'a>> ParseRecordData<'a> for Soa { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::SOA => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: SplitBytes<'a>> ParseRecordDataBytes<'a> for Soa { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::SOA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/txt.rs b/src/new/rdata/basic/txt.rs new file mode 100644 index 000000000..3b02801d3 --- /dev/null +++ b/src/new/rdata/basic/txt.rs @@ -0,0 +1,238 @@ +//! The TXT record data type. + +use core::{cmp::Ordering, fmt}; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, CharStr, ParseRecordData, ParseRecordDataBytes, + RType, +}; +use crate::utils::dst::UnsizedCopy; + +//----------- Txt ------------------------------------------------------------ + +/// Free-form text strings about this domain. +/// +/// A [`Txt`] record holds a collection of "strings" (really byte sequences), +/// with no fixed purpose. Usually, a [`Txt`] record holds a single string; +/// if data has to be stored for different purposes, multiple [`Txt`] records +/// would be used. +/// +/// Currently, [`Txt`] records are used systematically for e-mail security, +/// e.g. in SPF ([RFC 7208, section 3]), DKIM ([RFC 6376, section 3.6.2]), and +/// DMARC ([RFC 7489, section 6.1]). As a record data type with no strict +/// semantics and arbitrary data storage, it is likely to continue being +/// used. +/// +/// [RFC 6376, section 3.6.2]: https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.2 +/// [RFC 7208, section 3]: https://datatracker.ietf.org/doc/html/rfc7208#section-3 +/// [RFC 7489, section 6.1]: https://datatracker.ietf.org/doc/html/rfc7489#section-6.1 +/// +/// [`Txt`] is specified by [RFC 1035, section 3.3.14]. +/// +/// [RFC 1035, section 3.3.14]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 +/// +/// ## Wire Format +/// +/// The wire format of a [`Txt`] record is the concatenation of a (non-empty) +/// sequence of "character strings" (see [`CharStr`]). A character string is +/// serialized as a 1-byte length, followed by up to 255 bytes of content. +/// +/// The memory layout of the [`Txt`] type is identical to its serialization in +/// the wire format. This means it can be parsed from the wire format in a +/// zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Txt`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// [`Txt`] is a _dynamically sized type_ (DST). It is not possible to store +/// a [`Txt`] in place (e.g. in a local variable); it must be held indirectly, +/// via a reference or a smart pointer type like [`Box`]. This makes it more +/// difficult to _create_ new [`Txt`]s; but once they are placed somewhere, +/// they can be used by reference (i.e. `&Txt`) exactly like any other type. +/// +/// [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +/// +/// It is currently a bit difficult to build a new [`Txt`] from scratch. It +/// is easiest to build the wire format representation of the [`Txt`] manually +/// (by building a sequence of [`CharStr`]s) and then to parse it. +/// +/// ``` +/// # use domain::new::base::CharStrBuf; +/// # use domain::new::base::wire::ParseBytesZC; +/// # use domain::new::rdata::Txt; +/// # +/// // From an existing wire-format representation. +/// let bytes = b"\x0DHello, World!\x0AAnd again!"; +/// let from_bytes: &Txt = Txt::parse_bytes_by_ref(bytes).unwrap(); +/// // It is also possible to use '<&Txt>::parse_bytes()'. +/// +/// // To build a wire-format representation manually: +/// let strings: [CharStrBuf; 2] = [ +/// "Hello, World!".parse().unwrap(), +/// "And again!".parse().unwrap(), +/// ]; +/// let mut buffer: Vec = Vec::new(); +/// for string in &strings { +/// buffer.extend_from_slice(string.wire_bytes()); +/// } +/// assert_eq!(buffer.as_slice(), bytes); +/// +/// // From an existing wire-format representation, but on the heap: +/// let buffer: Box<[u8]> = buffer.into_boxed_slice(); +/// let from_boxed_bytes: Box = Txt::parse_bytes_in(buffer).unwrap(); +/// assert_eq!(from_bytes, &*from_boxed_bytes); +/// ``` +/// +/// As a DST, [`Txt`] does not implement [`Copy`] or [`Clone`]. Instead, it +/// implements [`UnsizedCopy`]. A [`Txt`], held by reference, can be copied +/// into a different container (e.g. `Box`) using [`unsized_copy_into()`]. +/// +/// [`unsized_copy_into()`]: UnsizedCopy::unsized_copy_into() +/// +/// For debugging, [`Txt`] can be formatted using [`fmt::Debug`]. +/// +/// To serialize a [`Txt`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Txt`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Txt { + /// The text strings, as concatenated [`CharStr`]s. + content: [u8], +} + +//--- Construction + +impl Txt { + /// Assume a byte sequence is a valid [`Txt`]. + /// + /// ## Safety + /// + /// The byte sequence must a valid instance of [`Txt`] in the wire format; + /// it must contain one or more serialized [`CharStr`]s, concatenated + /// together. The byte sequence must be at most 65,535 bytes long. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Txt' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute::<&[u8], &Txt>(bytes) } + } +} + +//--- Interaction + +impl Txt { + /// Iterate over the [`CharStr`]s in this record. + pub fn iter(&self) -> impl Iterator + '_ { + // NOTE: A TXT record always has at least one 'CharStr' within. + let first = <&CharStr>::split_bytes(&self.content) + .expect("'Txt' records always contain valid 'CharStr's"); + core::iter::successors(Some(first), |(_, rest)| { + (!rest.is_empty()).then(|| { + <&CharStr>::split_bytes(rest) + .expect("'Txt' records always contain valid 'CharStr's") + }) + }) + .map(|(elem, _rest)| elem) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Txt { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.content.cmp(&other.content) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Txt { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.content.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.content); + Ok(end) + } +} + +//--- Parsing from bytes + +// SAFETY: The implementations of 'parse_bytes_by_{ref,mut}()' always parse +// the entirety of the input on success, satisfying the safety requirements. +unsafe impl ParseBytesZC for Txt { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + // Make sure the slice is 64KiB or less. + if bytes.len() > 65535 { + return Err(ParseError); + } + + // The input must contain at least one 'CharStr'. + let (_, mut rest) = <&CharStr>::split_bytes(bytes)?; + while !rest.is_empty() { + (_, rest) = <&CharStr>::split_bytes(rest)?; + } + + // SAFETY: 'Txt' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], &Self>(bytes) }) + } +} + +//--- Formatting + +impl fmt::Debug for Txt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct Content<'a>(&'a Txt); + impl fmt::Debug for Content<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } + } + + f.debug_tuple("Txt").field(&Content(self)).finish() + } +} + +//--- Equality + +impl PartialEq for Txt { + /// Compare two [`Txt`]s for equality. + /// + /// Two [`Txt`]s are considered equal if they have an equal sequence of + /// character strings, laid out in the same order; corresponding character + /// strings are compared ASCII-case-insensitively. + fn eq(&self, other: &Self) -> bool { + self.iter().eq(other.iter()) + } +} + +impl Eq for Txt {} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for &'a Txt {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Txt { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::TXT => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/dnssec/dnskey.rs b/src/new/rdata/dnssec/dnskey.rs new file mode 100644 index 000000000..7bc1ce582 --- /dev/null +++ b/src/new/rdata/dnssec/dnskey.rs @@ -0,0 +1,139 @@ +//! The DNSKEY record data type. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::{ + build::BuildInMessage, + name::NameCompressor, + wire::{AsBytes, TruncationError, U16}, + CanonicalRecordData, +}; + +use super::SecAlg; + +//----------- DNSKey --------------------------------------------------------- + +/// A cryptographic key for DNS security. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(C)] +pub struct DNSKey { + /// Flags describing the usage of the key. + pub flags: DNSKeyFlags, + + /// The protocol version of the key. + pub protocol: u8, + + /// The cryptographic algorithm used by this key. + pub algorithm: SecAlg, + + /// The serialized public key. + pub key: [u8], +} + +//--- Canonical operations + +impl CanonicalRecordData for DNSKey { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for DNSKey { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- DNSKeyFlags ---------------------------------------------------- + +/// Flags describing a [`DNSKey`]. +#[derive( + Copy, + Clone, + Default, + Hash, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct DNSKeyFlags { + /// The raw flag bits. + inner: U16, +} + +//--- Interaction + +impl DNSKeyFlags { + /// Get the specified flag bit. + fn get_flag(&self, pos: u32) -> bool { + self.inner.get() & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(mut self, pos: u32, value: bool) -> Self { + self.inner &= !(1 << pos); + self.inner |= (value as u16) << pos; + self + } + + /// The raw flags bits. + pub fn bits(&self) -> u16 { + self.inner.get() + } + + /// Whether this key is used for signing DNS records. + pub fn is_zone_key(&self) -> bool { + self.get_flag(8) + } + + /// Make this key usable for signing DNS records. + pub fn set_zone_key(self, value: bool) -> Self { + self.set_flag(8, value) + } + + /// Whether external entities are expected to point to this key. + pub fn is_secure_entry_point(&self) -> bool { + self.get_flag(0) + } + + /// Expect external entities to point to this key. + pub fn set_secure_entry_point(self, value: bool) -> Self { + self.set_flag(0, value) + } +} + +//--- Formatting + +impl fmt::Debug for DNSKeyFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DNSKeyFlags") + .field("zone_key", &self.is_zone_key()) + .field("secure_entry_point", &self.is_secure_entry_point()) + .field("bits", &self.bits()) + .finish() + } +} diff --git a/src/new/rdata/dnssec/ds.rs b/src/new/rdata/dnssec/ds.rs new file mode 100644 index 000000000..f68becb69 --- /dev/null +++ b/src/new/rdata/dnssec/ds.rs @@ -0,0 +1,104 @@ +//! The DS record data type. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::wire::{AsBytes, U16}; +use crate::new::base::CanonicalRecordData; + +use super::SecAlg; + +//----------- Ds ------------------------------------------------------------- + +/// The signing key for a delegated zone. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(C)] +pub struct Ds { + /// The key tag of the signing key. + pub keytag: U16, + + /// The cryptographic algorithm used by the signing key. + pub algorithm: SecAlg, + + /// The algorithm used to calculate the key digest. + pub digest_type: DigestType, + + /// A serialized digest of the signing key. + pub digest: [u8], +} + +//--- Canonical operations + +impl CanonicalRecordData for Ds { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for Ds { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- DigestType ----------------------------------------------------- + +/// A cryptographic digest algorithm. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct DigestType { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl DigestType { + /// The SHA-1 algorithm. + pub const SHA1: Self = Self { code: 1 }; +} + +//--- Formatting + +impl fmt::Debug for DigestType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::SHA1 => "DigestType::SHA1", + _ => return write!(f, "DigestType({})", self.code), + }) + } +} diff --git a/src/new/rdata/dnssec/mod.rs b/src/new/rdata/dnssec/mod.rs new file mode 100644 index 000000000..d21311369 --- /dev/null +++ b/src/new/rdata/dnssec/mod.rs @@ -0,0 +1,69 @@ +//! Record types relating to DNSSEC. + +use core::fmt; + +use domain_macros::*; + +//----------- Submodules ----------------------------------------------------- + +mod dnskey; +pub use dnskey::{DNSKey, DNSKeyFlags}; + +mod rrsig; +pub use rrsig::RRSig; + +mod nsec; +pub use nsec::{NSec, TypeBitmaps}; + +mod nsec3; +pub use nsec3::{NSec3, NSec3Flags, NSec3HashAlg, NSec3Param}; + +mod ds; +pub use ds::{DigestType, Ds}; + +//----------- SecAlg --------------------------------------------------------- + +/// A cryptographic algorithm for DNS security. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct SecAlg { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl SecAlg { + /// The DSA/SHA-1 algorithm. + pub const DSA_SHA1: Self = Self { code: 3 }; + + /// The RSA/SHA-1 algorithm. + pub const RSA_SHA1: Self = Self { code: 5 }; +} + +//--- Formatting + +impl fmt::Debug for SecAlg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::DSA_SHA1 => "SecAlg::DSA_SHA1", + Self::RSA_SHA1 => "SecAlg::RSA_SHA1", + _ => return write!(f, "SecAlg({})", self.code), + }) + } +} diff --git a/src/new/rdata/dnssec/nsec.rs b/src/new/rdata/dnssec/nsec.rs new file mode 100644 index 000000000..a83f910f6 --- /dev/null +++ b/src/new/rdata/dnssec/nsec.rs @@ -0,0 +1,179 @@ +//! The NSEC record data type. + +use core::{cmp::Ordering, fmt}; + +use crate::new::base::build::BuildInMessage; +use crate::new::base::name::{CanonicalName, Name, NameCompressor}; +use crate::new::base::wire::*; +use crate::new::base::{CanonicalRecordData, RType}; +use crate::utils::dst::UnsizedCopy; + +//----------- NSec ----------------------------------------------------------- + +/// An indication of the non-existence of a set of DNS records (version 1). +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes)] +pub struct NSec<'a> { + /// The name of the next existing DNS record. + pub next: &'a Name, + + /// The types of the records that exist at this owner name. + pub types: &'a TypeBitmaps, +} + +//--- Interaction + +impl NSec<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> NSec<'r> { + use crate::utils::dst::copy_to_bump; + + NSec { + next: copy_to_bump(self.next, bump), + types: copy_to_bump(self.types, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for NSec<'_> { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.next + .cmp_composed(other.next) + .then_with(|| self.types.as_bytes().cmp(other.types.as_bytes())) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} + +//--- Parsing from byte sequences + +impl<'a> ParseBytes<'a> for NSec<'a> { + fn parse_bytes(bytes: &'a [u8]) -> Result { + let (next, bytes) = <&Name>::split_bytes(bytes)?; + if bytes.is_empty() { + // An empty type bitmap is not allowed for NSEC. + return Err(ParseError); + } + let types = <&TypeBitmaps>::parse_bytes(bytes)?; + Ok(Self { next, types }) + } +} + +//----------- TypeBitmaps ---------------------------------------------------- + +/// A bitmap of DNS record types. +#[derive(PartialEq, Eq, AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct TypeBitmaps { + /// The bitmap data, encoded in the wire format. + octets: [u8], +} + +//--- Inspection + +impl TypeBitmaps { + /// The types in this bitmap. + pub fn types(&self) -> impl Iterator + '_ { + fn split_window(octets: &[u8]) -> Option<(u8, &[u8], &[u8])> { + let &[num, len, ref rest @ ..] = octets else { + return None; + }; + + let (bits, rest) = rest.split_at(len as usize); + Some((num, bits, rest)) + } + + core::iter::successors(split_window(&self.octets), |(_, _, rest)| { + split_window(rest) + }) + .flat_map(move |(num, bits, _)| { + bits.iter().enumerate().flat_map(move |(i, &b)| { + (0..8).filter(move |&j| ((b >> j) & 1) != 0).map(move |j| { + RType::from(u16::from_be_bytes([num, (i * 8 + j) as u8])) + }) + }) + }) + } +} + +//--- Formatting + +impl fmt::Debug for TypeBitmaps { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_set().entries(self.types()).finish() + } +} + +//--- Parsing + +impl TypeBitmaps { + /// Validate the given bytes as a bitmap in the wire format. + fn validate_bytes(mut octets: &[u8]) -> Result<(), ParseError> { + // NOTE: NSEC records require at least one type in the bitmap, while + // NSEC3 records can have an empty bitmap (see RFC 6840, section 6.4). + + // The window number (i.e. the high byte of the type). + let mut num = None; + while let Some(&next) = octets.first() { + // Make sure that the window number increases. + // NOTE: 'None < Some(_)', for the first iteration. + if num.replace(next) > Some(next) { + return Err(ParseError); + } + + octets = Self::validate_window_bytes(octets)?; + } + + Ok(()) + } + + /// Validate the given bytes as a bitmap window in the wire format. + fn validate_window_bytes(octets: &[u8]) -> Result<&[u8], ParseError> { + let &[_num, len, ref rest @ ..] = octets else { + return Err(ParseError); + }; + + // At most 32 bytes are necessary, to cover the 256 types that could + // be stored in this window. And empty windows are not allowed. + if !(1..=32).contains(&len) || rest.len() < len as usize { + return Err(ParseError); + } + + // TODO(1.80): Use 'split_at_checked()' and eliminate the previous + // conditional (move the range check into the 'let-else'). + let (bits, rest) = rest.split_at(len as usize); + if bits.last() == Some(&0) { + // Trailing zeros are not allowed. + return Err(ParseError); + } + + Ok(rest) + } +} + +// SAFETY: The implementations of 'parse_bytes_by_{ref,mut}()' always parse +// the entirety of the input on success, satisfying the safety requirements. +unsafe impl ParseBytesZC for TypeBitmaps { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + Self::validate_bytes(bytes)?; + + // SAFETY: 'TypeBitmaps' is 'repr(transparent)' to '[u8]', and so + // references to '[u8]' can be transmuted to 'TypeBitmaps' soundly. + unsafe { core::mem::transmute(bytes) } + } +} diff --git a/src/new/rdata/dnssec/nsec3.rs b/src/new/rdata/dnssec/nsec3.rs new file mode 100644 index 000000000..1a3a4f720 --- /dev/null +++ b/src/new/rdata/dnssec/nsec3.rs @@ -0,0 +1,262 @@ +//! The NSEC3 and NSEC3PARAM record data types. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::{ + build::BuildInMessage, + name::NameCompressor, + wire::{AsBytes, BuildBytes, SizePrefixed, TruncationError, U16}, + CanonicalRecordData, +}; + +use super::TypeBitmaps; + +//----------- NSec3 ---------------------------------------------------------- + +/// An indication of the non-existence of a set of DNS records (version 3). +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes)] +pub struct NSec3<'a> { + /// The algorithm used to hash names. + pub algorithm: NSec3HashAlg, + + /// Flags modifying the behaviour of the record. + pub flags: NSec3Flags, + + /// The number of iterations of the underlying hash function per name. + pub iterations: U16, + + /// The salt used to randomize the hash function. + pub salt: &'a SizePrefixed, + + /// The name of the next existing DNS record. + pub next: &'a SizePrefixed, + + /// The types of the records that exist at this owner name. + pub types: &'a TypeBitmaps, +} + +//--- Interaction + +impl NSec3<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> NSec3<'r> { + use crate::utils::dst::copy_to_bump; + + NSec3 { + algorithm: self.algorithm, + flags: self.flags, + iterations: self.iterations, + salt: copy_to_bump(self.salt, bump), + next: copy_to_bump(self.next, bump), + types: copy_to_bump(self.types, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for NSec3<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this = ( + self.algorithm, + self.flags.as_bytes(), + self.iterations, + self.salt.len(), + self.salt, + self.next.len(), + self.next, + self.types.as_bytes(), + ); + let that = ( + that.algorithm, + that.flags.as_bytes(), + that.iterations, + that.salt.len(), + that.salt, + that.next.len(), + that.next, + that.types.as_bytes(), + ); + this.cmp(&that) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec3<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} + +//----------- NSec3Param ----------------------------------------------------- + +/// Parameters for computing [`NSec3`] records. +#[derive( + Debug, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytesZC, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct NSec3Param { + /// The algorithm used to hash names. + pub algorithm: NSec3HashAlg, + + /// Flags modifying the behaviour of the record. + pub flags: NSec3Flags, + + /// The number of iterations of the underlying hash function per name. + pub iterations: U16, + + /// The salt used to randomize the hash function. + pub salt: SizePrefixed, +} + +impl CanonicalRecordData for NSec3Param { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec3Param { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- NSec3HashAlg --------------------------------------------------- + +/// The hash algorithm used with [`NSec3`] records. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct NSec3HashAlg { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl NSec3HashAlg { + /// The SHA-1 algorithm. + pub const SHA1: Self = Self { code: 1 }; +} + +//--- Formatting + +impl fmt::Debug for NSec3HashAlg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::SHA1 => "NSec3HashAlg::SHA1", + _ => return write!(f, "NSec3HashAlg({})", self.code), + }) + } +} + +//----------- NSec3Flags ----------------------------------------------------- + +/// Flags modifying the behaviour of an [`NSec3`] record. +#[derive( + Copy, + Clone, + Default, + Hash, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct NSec3Flags { + /// The raw flag bits. + inner: u8, +} + +//--- Interaction + +impl NSec3Flags { + /// Get the specified flag bit. + fn get_flag(&self, pos: u32) -> bool { + self.inner & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(mut self, pos: u32, value: bool) -> Self { + self.inner &= !(1 << pos); + self.inner |= (value as u8) << pos; + self + } + + /// The raw flags bits. + pub fn bits(&self) -> u8 { + self.inner + } + + /// Whether unsigned delegations can exist in the covered range. + pub fn is_optout(&self) -> bool { + self.get_flag(0) + } + + /// Allow unsigned delegations to exist in the covered raneg. + pub fn set_optout(self, value: bool) -> Self { + self.set_flag(0, value) + } +} + +//--- Formatting + +impl fmt::Debug for NSec3Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NSec3Flags") + .field("optout", &self.is_optout()) + .field("bits", &self.bits()) + .finish() + } +} diff --git a/src/new/rdata/dnssec/rrsig.rs b/src/new/rdata/dnssec/rrsig.rs new file mode 100644 index 000000000..5138c65c5 --- /dev/null +++ b/src/new/rdata/dnssec/rrsig.rs @@ -0,0 +1,105 @@ +//! The RRSIG record data type. + +use core::cmp::Ordering; + +use domain_macros::*; + +use crate::new::base::build::BuildInMessage; +use crate::new::base::name::{CanonicalName, Name, NameCompressor}; +use crate::new::base::wire::{AsBytes, BuildBytes, TruncationError, U16}; +use crate::new::base::{CanonicalRecordData, RType, Serial, TTL}; + +use super::SecAlg; + +//----------- RRSig ---------------------------------------------------------- + +/// A cryptographic signature on a DNS record set. +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes)] +pub struct RRSig<'a> { + /// The type of the RRset being signed. + pub rtype: RType, + + /// The cryptographic algorithm used to construct the signature. + pub algorithm: SecAlg, + + /// The number of labels in the signed RRset's owner name. + pub labels: u8, + + /// The (original) TTL of the signed RRset. + pub ttl: TTL, + + /// The point in time when the signature expires. + pub expiration: Serial, + + /// The point in time when the signature was created. + pub inception: Serial, + + /// The key tag of the key used to make the signature. + pub keytag: U16, + + /// The name identifying the signer. + pub signer: &'a Name, + + /// The serialized cryptographic signature. + pub signature: &'a [u8], +} + +//--- Interaction + +impl RRSig<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> RRSig<'r> { + use crate::utils::dst::copy_to_bump; + + RRSig { + signer: copy_to_bump(self.signer, bump), + signature: bump.alloc_slice_copy(self.signature), + ..self.clone() + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for RRSig<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this_initial = ( + self.rtype, + self.algorithm, + self.labels, + self.ttl, + self.expiration.as_bytes(), + self.inception.as_bytes(), + self.keytag, + ); + let that_initial = ( + that.rtype, + that.algorithm, + that.labels, + that.ttl, + that.expiration.as_bytes(), + that.inception.as_bytes(), + that.keytag, + ); + this_initial + .cmp(&that_initial) + .then_with(|| self.signer.cmp_lowercase_composed(that.signer)) + .then_with(|| self.signature.cmp(that.signature)) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for RRSig<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} diff --git a/src/new/rdata/edns.rs b/src/new/rdata/edns.rs new file mode 100644 index 000000000..4218bbaf6 --- /dev/null +++ b/src/new/rdata/edns.rs @@ -0,0 +1,368 @@ +//! Record data types for EDNS (Extension Mechanism for DNS). +//! +//! See [RFC 6891](https://datatracker.ietf.org/doc/html/rfc6891). + +use core::cmp::Ordering; +use core::fmt; +use core::iter::FusedIterator; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytesZC, ParseError, SplitBytesZC, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::new::edns::{EdnsOption, UnparsedEdnsOption}; +use crate::utils::dst::UnsizedCopy; + +//----------- Opt ------------------------------------------------------------ + +/// EDNS options. +/// +/// An [`Opt`] record holds an unordered set of [`EdnsOption`]s, which provide +/// additional non-critical information about the containing DNS message. It +/// has fairly different semantics from other record data types, since it only +/// exists for communication between peers (it is not part of any zone, and it +/// is not cached). As such, it is often called a "pseudo-RR". +/// +/// A record containing [`Opt`] data is interpreted differently from records +/// containing normal data types (its class and TTL fields are different). +/// [`EdnsRecord`] provides this interpretation and offers way to convert to +/// and from normal [`Record`]s. +/// +/// [`EdnsRecord`]: crate::new::edns::EdnsRecord +/// [`Record`]: crate::new::base::Record +/// +/// [`Opt`] is specified by [RFC 6891, section 6]. For more information about +/// EDNS, see [`crate::new::edns`]. +/// +/// [RFC 6891, section 6]: https://datatracker.ietf.org/doc/html/rfc6891#section-6 +/// +/// ## Wire Format +/// +/// The wire format of an [`Opt`] record is the concatenation of zero or more +/// EDNS options. An EDNS option is serialized as a 16-bit big-endian code +/// (specifying the meaning of the option), a 16-bit big-endian size (the size +/// of the option data), and the variable-length option data. +/// +/// The memory layout of the [`Opt`] type is identical to its serialization in +/// the wire format. This means that it can be parsed from the wire format in +/// a zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Opt`] is a record data type, it is usually handled within an +/// enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// [`Opt`] is a _dynamically sized type_ (DST). It is not possible to +/// store an [`Opt`] in place (e.g. in a local variable); it must be held +/// indirectly, via a reference or a smart pointer type like [`Box`]. This +/// makes it more difficult to _create_ new [`Opt`]s; but once they are placed +/// somewhere, they can be used by reference (i.e. `&Opt`) exactly like any +/// other type. +/// +/// [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +/// +/// It is currently a bit difficult to build a new [`Opt`] from scratch. It +/// is easiest to build the wire format representation of the [`Opt`] manually +/// (by building a sequence of [`EdnsOption`]s) and then to parse it. +/// +/// ``` +/// # use domain::new::base::wire::{BuildBytes, ParseBytesZC, U16}; +/// # use domain::new::edns::{EdnsOption, OptionCode, UnknownOptionData}; +/// # use domain::new::rdata::Opt; +/// # +/// // Parse an 'Opt' from the DNS wire format: +/// let bytes = [0, 10, 0, 8, 248, 80, 41, 151, 244, 171, 53, 202, 0, 0, 0, 0]; +/// let from_bytes: &Opt = Opt::parse_bytes_by_ref(&bytes).unwrap(); +/// // It is also possible to use '<&Opt>::parse_bytes()'. +/// +/// let cookie = [248, 80, 41, 151, 244, 171, 53, 202].into(); +/// let options = [ +/// EdnsOption::ClientCookie(cookie), +/// EdnsOption::Unknown( +/// OptionCode { code: U16::new(0) }, +/// UnknownOptionData::parse_bytes_by_ref(&[]).unwrap(), +/// ), +/// ]; +/// +/// // Iterate over the options in an 'Opt': +/// for (l, r) in from_bytes.options().zip(&options) { +/// assert_eq!(l.as_ref(), Ok(r)); +/// } +/// +/// // Build the DNS wire format for an 'Opt' manually: +/// let mut buffer = vec![0u8; options.built_bytes_size()]; +/// options.build_bytes(&mut buffer).unwrap(); +/// assert_eq!(buffer, bytes); +/// +/// // Parse an 'Opt' from the wire format, but on the heap: +/// let buffer: Box<[u8]> = buffer.into_boxed_slice(); +/// let from_boxed_bytes: Box = Opt::parse_bytes_in(buffer).unwrap(); +/// assert_eq!(from_bytes, &*from_boxed_bytes); +/// ``` +/// +/// As a DST, [`Opt`] does not implement [`Copy`] or [`Clone`]. Instead, it +/// implements [`UnsizedCopy`]. An [`Opt`], held by reference, can be copied +/// into a different container (e.g. `Box`) using [`unsized_copy_into()`] +/// +/// [`unsized_copy_into()`]: UnsizedCopy::unsized_copy_into() +/// +/// For debugging, [`Opt`] can be formatted using [`fmt::Debug`]. +/// +/// To serialize a [`Opt`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Opt`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Opt { + /// The raw serialized options. + contents: [u8], +} + +//--- Associated Constants + +impl Opt { + /// Empty OPT record data. + pub const EMPTY: &'static Self = + unsafe { core::mem::transmute(&[] as &[u8]) }; +} + +//--- Construction + +impl Opt { + /// Assume a byte sequence is a valid [`Opt`]. + /// + /// ## Safety + /// + /// The byte sequence must a valid instance of [`Opt`] in the wire format; + /// it must contain a sequence of [`EdnsOption`]s, concatenated together. + /// The contents of each [`EdnsOption`] need not be valid (i.e. they can + /// be incorrect with respect to the underlying option type). The byte + /// sequence must be at most 65,535 bytes long. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Opt' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute::<&[u8], &Opt>(bytes) } + } +} + +//--- Inspection + +impl Opt { + /// Traverse the options in this record. + /// + /// Options that cannot be parsed are returned as [`UnparsedEdnsOption`]s. + pub fn options(&self) -> EdnsOptionsIter<'_> { + EdnsOptionsIter::new(&self.contents) + } +} + +//--- Equality + +impl PartialEq for Opt { + /// Compare two [`Opt`] records. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &Self) -> bool { + self.options().eq(other.options()) + } +} + +impl PartialEq<[EdnsOption<'_>]> for Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>]) -> bool { + self.options().eq(other.iter().map(|opt| Ok(opt.clone()))) + } +} + +impl PartialEq<[EdnsOption<'_>; N]> for Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>; N]) -> bool { + *self == *other.as_slice() + } +} + +impl PartialEq<[EdnsOption<'_>]> for &Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>]) -> bool { + **self == *other + } +} + +impl PartialEq<[EdnsOption<'_>; N]> for &Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>; N]) -> bool { + **self == *other.as_slice() + } +} + +impl Eq for Opt {} + +//--- Canonical operations + +impl CanonicalRecordData for Opt { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.contents.cmp(&other.contents) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Opt { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.contents.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.contents); + Ok(end) + } +} + +//--- Parsing from bytes + +unsafe impl ParseBytesZC for Opt { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + // Make sure the slice is 64KiB or less. + if bytes.len() > 65535 { + return Err(ParseError); + } + + let mut offset = 0usize; + while offset < bytes.len() { + // NOTE: We don't check the code here, since we won't validate the + // option by its actual type (even if we know how to). + offset += 2; + + let size = bytes.get(offset..offset + 2).ok_or(ParseError)?; + let size: usize = u16::from_be_bytes([size[0], size[1]]).into(); + offset += 2; + + // Make sure the entire data section exists. + let _ = bytes.get(offset..offset + size).ok_or(ParseError)?; + offset += size; + } + + // Now, 'offset == bytes.len()', and the whole slice is valid. + + // SAFETY: 'Opt' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], &Opt>(bytes) }) + } +} + +//--- Formatting + +impl fmt::Debug for Opt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Opt").field(&self.options()).finish() + } +} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for &'a Opt {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Opt { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::OPT => Opt::parse_bytes_by_ref(bytes), + _ => Err(ParseError), + } + } +} + +//----------- EdnsOptionsIter ------------------------------------------------ + +/// An iterator over EDNS options in an [`Opt`] record. +#[derive(Clone)] +pub struct EdnsOptionsIter<'a> { + /// The serialized options to parse from. + options: &'a [u8], +} + +//--- Construction + +impl<'a> EdnsOptionsIter<'a> { + /// Construct a new [`EdnsOptionsIter`]. + pub const fn new(options: &'a [u8]) -> Self { + Self { options } + } +} + +//--- Inspection + +impl<'a> EdnsOptionsIter<'a> { + /// The serialized options yet to be parsed. + pub const fn remaining(&self) -> &'a [u8] { + self.options + } +} + +//--- Formatting + +impl fmt::Debug for EdnsOptionsIter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = f.debug_set(); + for option in self.clone() { + match option { + Ok(option) => entries.entry(&option), + Err(_err) => entries.entry(&format_args!("")), + }; + } + entries.finish() + } +} + +//--- Iteration + +impl<'a> Iterator for EdnsOptionsIter<'a> { + type Item = Result, &'a UnparsedEdnsOption>; + + fn next(&mut self) -> Option { + if !self.options.is_empty() { + let (option, rest) = UnparsedEdnsOption::split_bytes_by_ref( + self.options, + ) + .expect("An 'Opt' always contains valid 'UnparsedEdnsOption's"); + self.options = rest; + Some(EdnsOption::try_from(option).map_err(|_| option)) + } else { + None + } + } +} + +impl FusedIterator for EdnsOptionsIter<'_> {} diff --git a/src/new/rdata/ipv6.rs b/src/new/rdata/ipv6.rs new file mode 100644 index 000000000..15fc42155 --- /dev/null +++ b/src/new/rdata/ipv6.rs @@ -0,0 +1,227 @@ +//! IPv6 record data types. +//! +//! See [RFC 3596](https://datatracker.ietf.org/doc/html/rfc3596). + +use core::cmp::Ordering; +use core::fmt; +use core::net::Ipv6Addr; +use core::str::FromStr; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytes, ParseBytesZC, ParseError, SplitBytes, + SplitBytesZC, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::utils::dst::UnsizedCopy; + +//----------- Aaaa ----------------------------------------------------------- + +/// The IPv6 address of a host responsible for this domain. +/// +/// A [`Aaaa`] record indicates that a domain name is backed by a server that +/// can be reached over the Internet at the specified IPv6 address. It does +/// not specify the server's capabilities (e.g. what protocols it supports); +/// those have to be communicated elsewhere. +/// +/// [`Aaaa`] is specified by [RFC 3596, section 2]. It works identically to +/// the [`A`] record. +/// +/// [`A`]: crate::new::rdata::A +/// [RFC 3596, section 2]: https://datatracker.ietf.org/doc/html/rfc3596#section-2 +/// +/// ## Wire Format +/// +/// The wire format of a [`Aaaa`] record is the 16 bytes of its IPv6 address, +/// in conventional order (from most to least significant). For example, +/// `2001::db8::` would be serialized as `20 01 0D B8 00 00 00 00 00 00 00 00 +/// 00 00 00 00`. +/// +/// The memory layout of the [`Aaaa`] type is identical to its serialization +/// in the wire format. This means it can be parsed from the wire format in a +/// zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Aaaa`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// There's a few ways to build an [`Aaaa`]: +/// +/// ``` +/// # use domain::new::base::wire::{ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Aaaa; +/// # +/// use core::net::Ipv6Addr; +/// +/// // Build a 'Aaaa' from the raw bytes. +/// let from_raw = Aaaa { +/// octets: [0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +/// }; +/// +/// // Convert an 'Ipv6Addr' into a 'Aaaa'. +/// let from_addr: Aaaa = Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0).into(); +/// # assert_eq!(from_raw, from_addr); +/// +/// // Parse a 'Aaaa' from a string. +/// let from_str: Aaaa = "2001:db8::".parse().unwrap(); +/// # assert_eq!(from_raw, from_str); +/// +/// // Parse a 'Aaaaa' from the DNS wire format. +/// let bytes = [0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +/// let from_wire: Aaaa = Aaaa::parse_bytes(&bytes).unwrap(); +/// # assert_eq!(from_raw, from_wire); +/// +/// // ... even by reference (this is zero-copy). +/// let ref_from_wire: &Aaaa = Aaaa::parse_bytes_by_ref(&bytes).unwrap(); +/// // It is also possible to use '<&Aaaa>::parse_bytes()'. +/// # assert_eq!(from_raw, *ref_from_wire); +/// ``` +/// +/// Since [`Aaaa`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. +/// +/// For debugging and logging, [`Aaaa`] can be formatted using [`fmt::Debug`] +/// and [`fmt::Display`]. +/// +/// To serialize a [`Aaaa`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Aaaa`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct Aaaa { + /// The IPv6 address octets. + pub octets: [u8; 16], +} + +//--- Converting to and from 'Ipv6Addr' + +impl From for Aaaa { + fn from(value: Ipv6Addr) -> Self { + Self { + octets: value.octets(), + } + } +} + +impl From for Ipv6Addr { + fn from(value: Aaaa) -> Self { + Self::from(value.octets) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Aaaa { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.octets.cmp(&other.octets) + } +} + +//--- Parsing from a string + +impl FromStr for Aaaa { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ipv6Addr::from_str(s).map(Aaaa::from) + } +} + +//--- Formatting + +impl fmt::Debug for Aaaa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Aaaa({self})") + } +} + +impl fmt::Display for Aaaa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ipv6Addr::from(*self).fmt(f) + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for Aaaa { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Aaaa { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.octets.len(); + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes.copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing record data + +impl ParseRecordData<'_> for Aaaa {} + +impl ParseRecordDataBytes<'_> for Aaaa { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::AAAA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} + +impl<'a> ParseRecordData<'a> for &'a Aaaa {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Aaaa { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::AAAA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/mod.rs b/src/new/rdata/mod.rs new file mode 100644 index 000000000..c9073e4f0 --- /dev/null +++ b/src/new/rdata/mod.rs @@ -0,0 +1,746 @@ +//! Record data types. +//! +//! ## Containers for record data +//! +//! When you need data for a particular record type, you can use the matching +//! concrete type for it. Otherwise, the record data can be held in one of +//! the following types: +//! +//! - [`RecordData`] is useful for short-term usage, e.g. when manipulating a +//! DNS message or parsing into a custom representation. It can be parsed +//! from the wire format very efficiently. +//! +#![cfg_attr(feature = "alloc", doc = " - [`BoxedRecordData`] ")] +#![cfg_attr(not(feature = "alloc"), doc = " - `BoxedRecordData` ")] +//! is useful for long-term storage. For long-term storage of a whole DNS +//! zone, it's more advisable to use the "zone tree" types provided by this +//! crate. +//! +//! - [`UnparsedRecordData`] is a niche type, useful for low-level +//! manipulation of the DNS wire format. Beware that it can contain +//! unresolved name compression pointers. +//! +//! - [`UnknownRecordData`] can be used to represent data types that aren't +//! supported yet. It functions similarly to [`UnparsedRecordData`], but +//! it can't be used for many "basic" record data types, like [`Soa`] and +//! [`Mx`]. These types come with many special cases that +//! [`UnknownRecordData`] doesn't try to account for. +//! +//! [`UnparsedRecordData`]: crate::new::base::UnparsedRecordData +//! +//! ## Supported data types +//! +//! The following record data types are supported. They are enumerated by +//! [`RecordData`], which can store any one of them at a time. +//! +//! Basic record types (RFC 1035): +//! - [`A`] +//! - [`Ns`] +//! - [`CName`] +//! - [`Soa`] +//! - [`Ptr`] +//! - [`HInfo`] +//! - [`Mx`] +//! - [`Txt`] +//! +//! IPv6 support (RFC 3596): +//! - [`Aaaa`] +//! +//! EDNS support (RFC 6891): +//! - [`Opt`] +//! +//! DNSSEC support (RFC 4034, RFC 5155): +//! - [`DNSKey`] +//! - [`RRSig`] +//! - [`NSec`] +//! - [`NSec3`] +//! - [`Ds`] + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] + +use core::cmp::Ordering; + +#[cfg(feature = "alloc")] +use core::fmt; + +#[cfg(feature = "alloc")] +use alloc::boxed::Box; + +use domain_macros::*; + +use crate::new::base::{ + build::{BuildInMessage, NameCompressor}, + name::CanonicalName, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, + TruncationError, + }, + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +#[cfg(feature = "alloc")] +use crate::new::base::name::{Name, NameBuf}; + +//----------- Concrete record data types ------------------------------------- + +mod basic; +pub use basic::{CName, HInfo, Mx, Ns, Ptr, Soa, Txt, A}; + +mod ipv6; +pub use ipv6::Aaaa; + +mod edns; +pub use edns::{EdnsOptionsIter, Opt}; + +mod dnssec; +pub use dnssec::{ + DNSKey, DNSKeyFlags, DigestType, Ds, NSec, NSec3, NSec3Flags, + NSec3HashAlg, NSec3Param, RRSig, SecAlg, TypeBitmaps, +}; + +//----------- RecordData ----------------------------------------------------- + +/// A helper macro to handle boilerplate in defining [`RecordData`]. +macro_rules! define_record_data { + { + $(#[$attr:meta])* + $vis:vis enum $name:ident<$a:lifetime, $N:ident> { + $( + $(#[$v_attr:meta])* + $v_name:ident ($v_type:ty) = $v_disc:ident + ),*; + + $(#[$u_attr:meta])* + Unknown(RType, $u_type:ty) + } + } => { + // The primary type definition. + $(#[$attr])* + $vis enum $name<$a, $N> { + $($(#[$v_attr])* $v_name ($v_type),)* + $(#[$u_attr])* Unknown(RType, $u_type), + } + + //--- Inspection + + impl<$N> $name<'_, $N> { + /// The record data type. + pub const fn rtype(&self) -> RType { + match *self { + $(Self::$v_name(_) => RType::$v_disc,)* + Self::Unknown(rtype, _) => rtype, + } + } + } + + //--- Conversion from concrete types + + $(impl<$a, $N> From<$v_type> for $name<$a, $N> { + fn from(value: $v_type) -> Self { + Self::$v_name(value) + } + })* + + //--- Canonical operations + + impl<$N: CanonicalName> CanonicalRecordData for $name<'_, $N> { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + $(Self::$v_name(r) => r.build_canonical_bytes(bytes),)* + Self::Unknown(_, rd) => rd.build_canonical_bytes(bytes), + } + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + if self.rtype() != other.rtype() { + return self.rtype().cmp(&other.rtype()); + } + + match (self, other) { + $((Self::$v_name(l), Self::$v_name(r)) + => l.cmp_canonical(r),)* + (Self::Unknown(_, l), Self::Unknown(_, r)) + => l.cmp_canonical(r), + _ => unreachable!("'self' and 'other' had the same rtype but were different enum variants"), + } + } + } + + //--- Parsing record data + + impl<$a, $N: SplitBytes<$a>> ParseRecordDataBytes<$a> for $name<$a, $N> { + fn parse_record_data_bytes( + bytes: &$a [u8], + rtype: RType, + ) -> Result { + Ok(match rtype { + $(RType::$v_disc => Self::$v_name(ParseBytes::parse_bytes(bytes)?),)* + _ => Self::Unknown(rtype, ParseBytes::parse_bytes(bytes)?), + }) + } + } + + //--- Building record data + + impl<$N: BuildInMessage> BuildInMessage for $name<'_, $N> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + name: &mut NameCompressor, + ) -> Result { + match *self { + $(Self::$v_name(ref r) => r.build_in_message(contents, start, name),)* + Self::Unknown(_, r) => r.build_in_message(contents, start, name), + } + } + } + + impl<$N: BuildBytes> BuildBytes for $name<'_, $N> { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + $(Self::$v_name(r) => r.build_bytes(bytes),)* + Self::Unknown(_, r) => r.build_bytes(bytes), + } + } + + fn built_bytes_size(&self) -> usize { + match self { + $(Self::$v_name(r) => r.built_bytes_size(),)* + Self::Unknown(_, r) => r.built_bytes_size(), + } + } + } + }; +} + +define_record_data! { + /// DNS record data. + #[derive(Clone, Debug, PartialEq, Eq)] + #[non_exhaustive] + pub enum RecordData<'a, N> { + /// The IPv4 address of a host responsible for this domain. + A(A) = A, + + /// The authoritative name server for this domain. + Ns(Ns) = NS, + + /// The canonical name for this domain. + CName(CName) = CNAME, + + /// The start of a zone of authority. + Soa(Soa) = SOA, + + /// A pointer to another domain name. + Ptr(Ptr) = PTR, + + /// Information about the host computer. + HInfo(HInfo<'a>) = HINFO, + + /// A host that can exchange mail for this domain. + Mx(Mx) = MX, + + /// Free-form text strings about this domain. + Txt(&'a Txt) = TXT, + + /// The IPv6 address of a host responsible for this domain. + Aaaa(Aaaa) = AAAA, + + /// Extended DNS options. + Opt(&'a Opt) = OPT, + + /// The signing key of a delegated zone. + Ds(&'a Ds) = DS, + + /// A cryptographic signature on a DNS record set. + RRSig(RRSig<'a>) = RRSIG, + + /// An indication of the non-existence of a set of DNS records (version 1). + NSec(NSec<'a>) = NSEC, + + /// A cryptographic key for DNS security. + DNSKey(&'a DNSKey) = DNSKEY, + + /// An indication of the non-existence of a set of DNS records (version 3). + NSec3(NSec3<'a>) = NSEC3, + + /// Parameters for computing [`NSec3`] records. + NSec3Param(&'a NSec3Param) = NSEC3PARAM; + + /// Data for an unknown DNS record type. + Unknown(RType, &'a UnknownRecordData) + } +} + +//--- Interaction + +impl<'a, N> RecordData<'a, N> { + /// Map the domain names within to another type. + pub fn map_names R>(self, f: F) -> RecordData<'a, R> { + match self { + Self::A(r) => RecordData::A(r), + Self::Ns(r) => RecordData::Ns(r.map_name(f)), + Self::CName(r) => RecordData::CName(r.map_name(f)), + Self::Soa(r) => RecordData::Soa(r.map_names(f)), + Self::Ptr(r) => RecordData::Ptr(r.map_name(f)), + Self::HInfo(r) => RecordData::HInfo(r), + Self::Mx(r) => RecordData::Mx(r.map_name(f)), + Self::Txt(r) => RecordData::Txt(r), + Self::Aaaa(r) => RecordData::Aaaa(r), + Self::Opt(r) => RecordData::Opt(r), + Self::Ds(r) => RecordData::Ds(r), + Self::RRSig(r) => RecordData::RRSig(r), + Self::NSec(r) => RecordData::NSec(r), + Self::DNSKey(r) => RecordData::DNSKey(r), + Self::NSec3(r) => RecordData::NSec3(r), + Self::NSec3Param(r) => RecordData::NSec3Param(r), + Self::Unknown(rt, rd) => RecordData::Unknown(rt, rd), + } + } + + /// Map references to the domain names within to another type. + pub fn map_names_by_ref<'r, R, F: FnMut(&'r N) -> R>( + &'r self, + f: F, + ) -> RecordData<'r, R> { + match self { + Self::A(r) => RecordData::A(*r), + Self::Ns(r) => RecordData::Ns(r.map_name_by_ref(f)), + Self::CName(r) => RecordData::CName(r.map_name_by_ref(f)), + Self::Soa(r) => RecordData::Soa(r.map_names_by_ref(f)), + Self::Ptr(r) => RecordData::Ptr(r.map_name_by_ref(f)), + Self::HInfo(r) => RecordData::HInfo(*r), + Self::Mx(r) => RecordData::Mx(r.map_name_by_ref(f)), + Self::Txt(r) => RecordData::Txt(r), + Self::Aaaa(r) => RecordData::Aaaa(*r), + Self::Opt(r) => RecordData::Opt(r), + Self::Ds(r) => RecordData::Ds(r), + Self::RRSig(r) => RecordData::RRSig(r.clone()), + Self::NSec(r) => RecordData::NSec(r.clone()), + Self::DNSKey(r) => RecordData::DNSKey(r), + Self::NSec3(r) => RecordData::NSec3(r.clone()), + Self::NSec3Param(r) => RecordData::NSec3Param(r), + Self::Unknown(rt, rd) => RecordData::Unknown(*rt, rd), + } + } + + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>( + &self, + bump: &'r bumpalo::Bump, + ) -> RecordData<'r, N> + where + N: Clone, + { + use crate::utils::dst::copy_to_bump; + + match self { + Self::A(r) => RecordData::A(*r), + Self::Ns(r) => RecordData::Ns(r.clone()), + Self::CName(r) => RecordData::CName(r.clone()), + Self::Soa(r) => RecordData::Soa(r.clone()), + Self::Ptr(r) => RecordData::Ptr(r.clone()), + Self::HInfo(r) => RecordData::HInfo(r.clone_to_bump(bump)), + Self::Mx(r) => RecordData::Mx(r.clone()), + Self::Txt(r) => RecordData::Txt(copy_to_bump(*r, bump)), + Self::Aaaa(r) => RecordData::Aaaa(*r), + Self::Opt(r) => RecordData::Opt(copy_to_bump(*r, bump)), + Self::Ds(r) => RecordData::Ds(copy_to_bump(*r, bump)), + Self::RRSig(r) => RecordData::RRSig(r.clone_to_bump(bump)), + Self::NSec(r) => RecordData::NSec(r.clone_to_bump(bump)), + Self::DNSKey(r) => RecordData::DNSKey(copy_to_bump(*r, bump)), + Self::NSec3(r) => RecordData::NSec3(r.clone_to_bump(bump)), + Self::NSec3Param(r) => { + RecordData::NSec3Param(copy_to_bump(*r, bump)) + } + Self::Unknown(rt, rd) => { + RecordData::Unknown(*rt, rd.clone_to_bump(bump)) + } + } + } +} + +//--- Parsing record data + +impl<'a, N: SplitMessageBytes<'a>> ParseRecordData<'a> for RecordData<'a, N> { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::A => A::parse_bytes(&contents[start..]).map(Self::A), + RType::NS => { + Ns::parse_message_bytes(contents, start).map(Self::Ns) + } + RType::CNAME => { + CName::parse_message_bytes(contents, start).map(Self::CName) + } + RType::SOA => { + Soa::parse_message_bytes(contents, start).map(Self::Soa) + } + RType::PTR => { + Ptr::parse_message_bytes(contents, start).map(Self::Ptr) + } + RType::HINFO => { + HInfo::parse_bytes(&contents[start..]).map(Self::HInfo) + } + RType::MX => { + Mx::parse_message_bytes(contents, start).map(Self::Mx) + } + RType::TXT => { + <&Txt>::parse_bytes(&contents[start..]).map(Self::Txt) + } + RType::AAAA => { + Aaaa::parse_bytes(&contents[start..]).map(Self::Aaaa) + } + RType::OPT => { + <&Opt>::parse_bytes(&contents[start..]).map(Self::Opt) + } + RType::DS => <&Ds>::parse_bytes(&contents[start..]).map(Self::Ds), + RType::RRSIG => { + RRSig::parse_bytes(&contents[start..]).map(Self::RRSig) + } + RType::NSEC => { + NSec::parse_bytes(&contents[start..]).map(Self::NSec) + } + RType::DNSKEY => { + <&DNSKey>::parse_bytes(&contents[start..]).map(Self::DNSKey) + } + RType::NSEC3 => { + NSec3::parse_bytes(&contents[start..]).map(Self::NSec3) + } + RType::NSEC3PARAM => { + <&NSec3Param>::parse_bytes(&contents[start..]) + .map(Self::NSec3Param) + } + _ => <&UnknownRecordData>::parse_bytes(&contents[start..]) + .map(|data| Self::Unknown(rtype, data)), + } + } +} + +//----------- BoxedRecordData ------------------------------------------------ + +/// A heap-allocated container for [`RecordData`]. +/// +/// This is an efficient heap-allocated container for DNS record data. While +/// it does not directly provide much functionality, it has getters to access +/// the [`RecordData`] within. +/// +/// ## Performance +/// +/// On 64-bit machines, [`BoxedRecordData`] has a size of 16 bytes. This is +/// significantly better than [`RecordData`], which is usually 64 bytes in +/// size. Since [`BoxedRecordData`] is intended for long-term storage and +/// use, it trades off ergonomics for lower memory usage. +#[cfg(feature = "alloc")] +pub struct BoxedRecordData { + /// A pointer to the record data. + /// + /// This is the raw pointer backing a `Box<[u8]>` (its size is stored in + /// the `size` field). It is owned by this type. + data: *mut u8, + + /// The record data type. + /// + /// The stored bytes represent a valid instance of this record data type, + /// at least for all known record data types. + rtype: RType, + + /// The size of the record data. + size: u16, +} + +//--- Inspection + +#[cfg(feature = "alloc")] +impl BoxedRecordData { + /// The record data type. + pub const fn rtype(&self) -> RType { + self.rtype + } + + /// The wire format of the record data. + pub const fn bytes(&self) -> &[u8] { + // SAFETY: + // + // As documented on 'BoxedRecordData', 'data' and 'size' form the + // pointer and length of a 'Box<[u8]>'. This pointer is identical to + // the pointer returned by 'Box::deref()', so we use it directly. + // + // The lifetime of the returned slice is within the lifetime of 'self' + // which is a shared borrow of the 'BoxedRecordData'. As such, the + // underlying 'Box<[u8]>' outlives the returned slice. + unsafe { core::slice::from_raw_parts(self.data, self.size as usize) } + } + + /// Access the [`RecordData`] within. + pub fn get(&self) -> RecordData<'_, &'_ Name> { + let (rtype, bytes) = (self.rtype, self.bytes()); + // SAFETY: As documented on 'BoxedRecordData', the referenced bytes + // are known to be a valid instance of the record data type (for all + // known record data types). As such, this function will succeed. + unsafe { + RecordData::parse_record_data_bytes(bytes, rtype) + .unwrap_unchecked() + } + } +} + +//--- Formatting + +#[cfg(feature = "alloc")] +impl fmt::Debug for BoxedRecordData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This should concatenate to form 'BoxedRecordData'. + f.write_str("Boxed")?; + self.get().fmt(f) + } +} + +//--- Equality + +#[cfg(feature = "alloc")] +impl PartialEq for BoxedRecordData { + fn eq(&self, other: &Self) -> bool { + self.get().eq(&other.get()) + } +} + +#[cfg(feature = "alloc")] +impl Eq for BoxedRecordData {} + +//--- Clone + +#[cfg(feature = "alloc")] +impl Clone for BoxedRecordData { + fn clone(&self) -> Self { + let bytes: Box<[u8]> = self.bytes().into(); + let data = Box::into_raw(bytes).cast::(); + let (rtype, size) = (self.rtype, self.size); + Self { data, rtype, size } + } +} + +//--- Drop + +#[cfg(feature = "alloc")] +impl Drop for BoxedRecordData { + fn drop(&mut self) { + // Reconstruct the 'Box' and drop it. + let slice = core::ptr::slice_from_raw_parts_mut( + self.data, + self.size as usize, + ); + + // SAFETY: As documented on 'BoxedRecordData', 'data' and 'size' form + // the pointer and length of a 'Box<[u8]>'. Reconstructing the 'Box' + // moves out of 'self', but this is sound because 'self' is dropped. + let _ = unsafe { Box::from_raw(slice) }; + } +} + +//--- Send and Sync + +// SAFETY: 'BoxedRecordData' is equivalent to '(RType, Box<[u8]>)' with a +// custom representation. It cannot cause data races. +#[cfg(feature = "alloc")] +unsafe impl Send for BoxedRecordData {} + +// SAFETY: 'BoxedRecordData' is equivalent to '(RType, Box<[u8]>)' with a +// custom representation. It cannot cause data races. +#[cfg(feature = "alloc")] +unsafe impl Sync for BoxedRecordData {} + +//--- Conversion from 'RecordData' + +#[cfg(feature = "alloc")] +impl From> for BoxedRecordData { + /// Build a [`RecordData`] into a heap allocation. + /// + /// # Panics + /// + /// Panics if the [`RecordData`] does not fit in a 64KiB buffer, or if the + /// serialized bytes cannot be parsed back into `RecordData<'_, &Name>`. + fn from(value: RecordData<'_, N>) -> Self { + // TODO: Determine the size of the record data upfront, and only + // allocate that much. Maybe as a new method on 'BuildBytes'... + let mut buffer = vec![0u8; 65535]; + let rest_len = value + .build_bytes(&mut buffer) + .expect("A 'RecordData' could not be built into a 64KiB buffer") + .len(); + let len = buffer.len() - rest_len; + buffer.truncate(len); + let buffer: Box<[u8]> = buffer.into_boxed_slice(); + + // Verify that the built bytes can be parsed correctly. + let _rdata: RecordData<'_, &Name> = + RecordData::parse_record_data_bytes(&buffer, value.rtype()) + .expect("A serialized 'RecordData' could not be parsed back"); + + // Construct the internal representation. + let size = buffer.len() as u16; + let data = Box::into_raw(buffer).cast::(); + let rtype = value.rtype(); + Self { data, rtype, size } + } +} + +// TODO: Convert from 'Box', 'Box', etc. + +//--- Canonical operations + +#[cfg(feature = "alloc")] +impl CanonicalRecordData for BoxedRecordData { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + if self.rtype.uses_lowercase_canonical_form() { + // Forward to the semantically correct operation. + self.get().build_canonical_bytes(bytes) + } else { + // The canonical format is the same as the wire format. + self.bytes().build_bytes(bytes) + } + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + // Compare record data types. + if self.rtype != other.rtype { + return self.rtype.cmp(&other.rtype); + } + + if self.rtype.uses_lowercase_canonical_form() { + // Forward to the semantically correct operation. + self.get().cmp_canonical(&other.get()) + } else { + // Compare raw byte sequences. + self.bytes().cmp(other.bytes()) + } + } +} + +//--- Parsing record data + +#[cfg(feature = "alloc")] +impl ParseRecordData<'_> for BoxedRecordData { + fn parse_record_data( + contents: &'_ [u8], + start: usize, + rtype: RType, + ) -> Result { + RecordData::<'_, NameBuf>::parse_record_data(contents, start, rtype) + .map(BoxedRecordData::from) + } +} + +#[cfg(feature = "alloc")] +impl ParseRecordDataBytes<'_> for BoxedRecordData { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + // Ensure the bytes form valid 'RecordData'. + let _rdata: RecordData<'_, &Name> = + RecordData::parse_record_data_bytes(bytes, rtype)?; + + // Ensure the data size is valid. + let size = u16::try_from(bytes.len()).map_err(|_| ParseError)?; + + // Construct the 'BoxedRecordData' manually. + let bytes: Box<[u8]> = bytes.into(); + let data = Box::into_raw(bytes).cast::(); + Ok(Self { data, rtype, size }) + } +} + +//--- Building record data + +// TODO: 'impl BuildInMessage for BoxedRecordData' will require implementing +// 'impl BuildInMessage for Name', which is difficult because it is hard on +// name compression. + +#[cfg(feature = "alloc")] +impl BuildBytes for BoxedRecordData { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.bytes().build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + self.bytes().len() + } +} + +//----------- UnknownRecordData ---------------------------------------------- + +/// Data for an unknown DNS record type. +/// +/// This is a fallback type, used for record types not known to the current +/// implementation. It must not be used for well-known record types, because +/// some of them have special rules that this type does not follow. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(transparent)] +pub struct UnknownRecordData { + /// The unparsed option data. + pub octets: [u8], +} + +//--- Interaction + +impl UnknownRecordData { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + #[allow(clippy::mut_from_ref)] // using a memory allocator + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> &'r mut Self { + use crate::new::base::wire::{AsBytes, ParseBytesZC}; + + let bytes = bump.alloc_slice_copy(self.as_bytes()); + // SAFETY: 'ParseBytesZC' and 'AsBytes' are inverses. + unsafe { Self::parse_bytes_in(bytes).unwrap_unchecked() } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for UnknownRecordData { + fn cmp_canonical(&self, other: &Self) -> Ordering { + // Since this is not a well-known record data type, embedded domain + // names do not need to be lowercased. + self.octets.cmp(&other.octets) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for UnknownRecordData { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.octets.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.octets); + Ok(end) + } +} diff --git a/src/rdata/cds.rs b/src/rdata/cds.rs index 3dd6df2b0..23e35a221 100644 --- a/src/rdata/cds.rs +++ b/src/rdata/cds.rs @@ -2,7 +2,7 @@ //! //! [RFC 7344]: https://tools.ietf.org/html/rfc7344 use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlg, Rtype, SecAlg}; +use crate::base::iana::{DigestAlgorithm, Rtype, SecurityAlgorithm}; use crate::base::rdata::{ ComposeRecordData, LongRecordData, ParseRecordData, RecordData, }; @@ -38,7 +38,7 @@ use octseq::parse::Parser; pub struct Cdnskey { flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, #[cfg_attr( feature = "serde", serde(with = "crate::utils::base64::serde") @@ -55,7 +55,7 @@ impl Cdnskey { pub fn new( flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, public_key: Octs, ) -> Result where @@ -63,7 +63,7 @@ impl Cdnskey { { LongRecordData::check_len( usize::from( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecAlg::COMPOSE_LEN, + u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, ) .checked_add(public_key.as_ref().len()) .expect("long key"), @@ -82,7 +82,7 @@ impl Cdnskey { pub unsafe fn new_unchecked( flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, public_key: Octs, ) -> Self { Cdnskey { @@ -101,7 +101,7 @@ impl Cdnskey { self.protocol } - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } @@ -139,7 +139,7 @@ impl Cdnskey { Self::new_unchecked( u16::parse(parser)?, u8::parse(parser)?, - SecAlg::parse(parser)?, + SecurityAlgorithm::parse(parser)?, parser.parse_octets(len)?, ) }) @@ -154,7 +154,7 @@ impl Cdnskey { Self::new( u16::scan(scanner)?, u8::scan(scanner)?, - SecAlg::scan(scanner)?, + SecurityAlgorithm::scan(scanner)?, scanner.convert_entry(base64::SymbolConverter::new())?, ) .map_err(|err| S::Error::custom(err.as_str())) @@ -281,7 +281,7 @@ impl> ComposeRecordData for Cdnskey { u16::try_from(self.public_key.as_ref().len()) .expect("long key") .checked_add( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecAlg::COMPOSE_LEN, + u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, ) .expect("long key"), ) @@ -362,8 +362,8 @@ impl> ZonefileFmt for Cdnskey { )] pub struct Cds { key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, #[cfg_attr( feature = "serde", serde(with = "crate::utils::base64::serde") @@ -379,8 +379,8 @@ impl Cds<()> { impl Cds { pub fn new( key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, digest: Octs, ) -> Result where @@ -389,8 +389,8 @@ impl Cds { LongRecordData::check_len( usize::from( u16::COMPOSE_LEN - + SecAlg::COMPOSE_LEN - + DigestAlg::COMPOSE_LEN, + + SecurityAlgorithm::COMPOSE_LEN + + DigestAlgorithm::COMPOSE_LEN, ) .checked_add(digest.as_ref().len()) .expect("long digest"), @@ -408,8 +408,8 @@ impl Cds { /// record data is at most 65,535 octets long. pub unsafe fn new_unchecked( key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, digest: Octs, ) -> Self { Cds { @@ -424,11 +424,11 @@ impl Cds { self.key_tag } - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } - pub fn digest_type(&self) -> DigestAlg { + pub fn digest_type(&self) -> DigestAlgorithm { self.digest_type } @@ -469,8 +469,8 @@ impl Cds { Ok(unsafe { Self::new_unchecked( u16::parse(parser)?, - SecAlg::parse(parser)?, - DigestAlg::parse(parser)?, + SecurityAlgorithm::parse(parser)?, + DigestAlgorithm::parse(parser)?, parser.parse_octets(len)?, ) }) @@ -484,8 +484,8 @@ impl Cds { { Self::new( u16::scan(scanner)?, - SecAlg::scan(scanner)?, - DigestAlg::scan(scanner)?, + SecurityAlgorithm::scan(scanner)?, + DigestAlgorithm::scan(scanner)?, scanner.convert_entry(base16::SymbolConverter::new())?, ) .map_err(|err| S::Error::custom(err.as_str())) @@ -621,8 +621,8 @@ impl> ComposeRecordData for Cds { Some( u16::checked_add( u16::COMPOSE_LEN - + SecAlg::COMPOSE_LEN - + DigestAlg::COMPOSE_LEN, + + SecurityAlgorithm::COMPOSE_LEN + + DigestAlgorithm::COMPOSE_LEN, self.digest.as_ref().len().try_into().expect("long digest"), ) .expect("long digest"), @@ -711,7 +711,7 @@ mod test { #[test] #[allow(clippy::redundant_closure)] // lifetimes ... fn cdnskey_compose_parse_scan() { - let rdata = Cdnskey::new(10, 11, SecAlg::RSASHA1, b"key").unwrap(); + let rdata = Cdnskey::new(10, 11, SecurityAlgorithm::RSASHA1, b"key").unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Cdnskey::parse(parser)); test_scan(&["10", "11", "5", "a2V5"], Cdnskey::scan, &rdata); @@ -723,7 +723,7 @@ mod test { #[allow(clippy::redundant_closure)] // lifetimes ... fn cds_compose_parse_scan() { let rdata = - Cds::new(10, SecAlg::RSASHA1, DigestAlg::SHA256, b"key").unwrap(); + Cds::new(10, SecurityAlgorithm::RSASHA1, DigestAlgorithm::SHA256, b"key").unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Cds::parse(parser)); test_scan(&["10", "5", "2", "6b6579"], Cds::scan, &rdata); diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 242b94374..68470c824 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -5,7 +5,7 @@ //! [RFC 4034]: https://tools.ietf.org/html/rfc4034 use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlg, Rtype, SecAlg}; +use crate::base::iana::{DigestAlgorithm, Rtype, SecurityAlgorithm}; use crate::base::name::{FlattenInto, ParsedName, ToName}; use crate::base::rdata::{ ComposeRecordData, LongRecordData, ParseRecordData, RecordData, @@ -50,7 +50,7 @@ use time::{Date, Month, PrimitiveDateTime, Time}; pub struct Dnskey { flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, #[cfg_attr( feature = "serde", serde(with = "crate::utils::base64::serde") @@ -67,7 +67,7 @@ impl Dnskey { pub fn new( flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, public_key: Octs, ) -> Result where @@ -75,7 +75,7 @@ impl Dnskey { { LongRecordData::check_len( usize::from( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecAlg::COMPOSE_LEN, + u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, ) .checked_add(public_key.as_ref().len()) .expect("long key"), @@ -97,7 +97,7 @@ impl Dnskey { pub unsafe fn new_unchecked( flags: u16, protocol: u8, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, public_key: Octs, ) -> Self { Dnskey { @@ -116,7 +116,7 @@ impl Dnskey { self.protocol } - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } @@ -175,7 +175,7 @@ impl Dnskey { where Octs: AsRef<[u8]>, { - if self.algorithm == SecAlg::RSAMD5 { + if self.algorithm == SecurityAlgorithm::RSAMD5 { // The key tag is third-to-last and second-to-last octets of the // key as a big-endian u16. If we don’t have enough octets in the // key, we return 0. @@ -246,7 +246,7 @@ impl Dnskey { Self::new_unchecked( u16::parse(parser)?, u8::parse(parser)?, - SecAlg::parse(parser)?, + SecurityAlgorithm::parse(parser)?, parser.parse_octets(len)?, ) }) @@ -261,7 +261,7 @@ impl Dnskey { Self::new( u16::scan(scanner)?, u8::scan(scanner)?, - SecAlg::scan(scanner)?, + SecurityAlgorithm::scan(scanner)?, scanner.convert_entry(base64::SymbolConverter::new())?, ) .map_err(|err| S::Error::custom(err.as_str())) @@ -386,7 +386,7 @@ impl> ComposeRecordData for Dnskey { u16::try_from(self.public_key.as_ref().len()) .expect("long key") .checked_add( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecAlg::COMPOSE_LEN, + u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, ) .expect("long key"), ) @@ -464,7 +464,7 @@ impl> ZonefileFmt for Dnskey { #[derive(Clone)] pub struct ProtoRrsig { type_covered: Rtype, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, labels: u8, original_ttl: Ttl, expiration: Timestamp, @@ -477,7 +477,7 @@ impl ProtoRrsig { #[allow(clippy::too_many_arguments)] // XXX Consider changing. pub fn new( type_covered: Rtype, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, labels: u8, original_ttl: Ttl, expiration: Timestamp, @@ -844,7 +844,7 @@ fn u32_from_buf(buf: &[u8]) -> u32 { )] pub struct Rrsig { type_covered: Rtype, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, labels: u8, original_ttl: Ttl, expiration: Timestamp, @@ -867,7 +867,7 @@ impl Rrsig { #[allow(clippy::too_many_arguments)] // XXX Consider changing. pub fn new( type_covered: Rtype, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, labels: u8, original_ttl: Ttl, expiration: Timestamp, @@ -883,7 +883,7 @@ impl Rrsig { LongRecordData::check_len( usize::from( Rtype::COMPOSE_LEN - + SecAlg::COMPOSE_LEN + + SecurityAlgorithm::COMPOSE_LEN + u8::COMPOSE_LEN + u32::COMPOSE_LEN + Timestamp::COMPOSE_LEN @@ -918,7 +918,7 @@ impl Rrsig { #[allow(clippy::too_many_arguments)] // XXX Consider changing. pub unsafe fn new_unchecked( type_covered: Rtype, - algorithm: SecAlg, + algorithm: SecurityAlgorithm, labels: u8, original_ttl: Ttl, expiration: Timestamp, @@ -944,7 +944,7 @@ impl Rrsig { self.type_covered } - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } @@ -1033,7 +1033,7 @@ impl Rrsig { { Self::new( Rtype::scan(scanner)?, - SecAlg::scan(scanner)?, + SecurityAlgorithm::scan(scanner)?, u8::scan(scanner)?, Ttl::scan(scanner)?, Timestamp::scan(scanner)?, @@ -1051,7 +1051,7 @@ impl Rrsig> { parser: &mut Parser<'a, Src>, ) -> Result { let type_covered = Rtype::parse(parser)?; - let algorithm = SecAlg::parse(parser)?; + let algorithm = SecurityAlgorithm::parse(parser)?; let labels = u8::parse(parser)?; let original_ttl = Ttl::parse(parser)?; let expiration = Timestamp::parse(parser)?; @@ -1293,7 +1293,7 @@ where fn rdlen(&self, _compress: bool) -> Option { Some( (Rtype::COMPOSE_LEN - + SecAlg::COMPOSE_LEN + + SecurityAlgorithm::COMPOSE_LEN + u8::COMPOSE_LEN + u32::COMPOSE_LEN + Timestamp::COMPOSE_LEN @@ -1730,8 +1730,8 @@ where )] pub struct Ds { key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, #[cfg_attr( feature = "serde", serde(with = "crate::utils::base16::serde") @@ -1747,8 +1747,8 @@ impl Ds<()> { impl Ds { pub fn new( key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, digest: Octs, ) -> Result where @@ -1757,8 +1757,8 @@ impl Ds { LongRecordData::check_len( usize::from( u16::COMPOSE_LEN - + SecAlg::COMPOSE_LEN - + DigestAlg::COMPOSE_LEN, + + SecurityAlgorithm::COMPOSE_LEN + + DigestAlgorithm::COMPOSE_LEN, ) .checked_add(digest.as_ref().len()) .expect("long digest"), @@ -1776,8 +1776,8 @@ impl Ds { /// record data is at most 65,535 octets long. pub unsafe fn new_unchecked( key_tag: u16, - algorithm: SecAlg, - digest_type: DigestAlg, + algorithm: SecurityAlgorithm, + digest_type: DigestAlgorithm, digest: Octs, ) -> Self { Ds { @@ -1792,11 +1792,11 @@ impl Ds { self.key_tag } - pub fn algorithm(&self) -> SecAlg { + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } - pub fn digest_type(&self) -> DigestAlg { + pub fn digest_type(&self) -> DigestAlgorithm { self.digest_type } @@ -1837,8 +1837,8 @@ impl Ds { Ok(unsafe { Self::new_unchecked( u16::parse(parser)?, - SecAlg::parse(parser)?, - DigestAlg::parse(parser)?, + SecurityAlgorithm::parse(parser)?, + DigestAlgorithm::parse(parser)?, parser.parse_octets(len)?, ) }) @@ -1852,8 +1852,8 @@ impl Ds { { Self::new( u16::scan(scanner)?, - SecAlg::scan(scanner)?, - DigestAlg::scan(scanner)?, + SecurityAlgorithm::scan(scanner)?, + DigestAlgorithm::scan(scanner)?, scanner.convert_entry(base16::SymbolConverter::new())?, ) .map_err(|err| S::Error::custom(err.as_str())) @@ -1989,8 +1989,8 @@ impl> ComposeRecordData for Ds { Some( u16::checked_add( u16::COMPOSE_LEN - + SecAlg::COMPOSE_LEN - + DigestAlg::COMPOSE_LEN, + + SecurityAlgorithm::COMPOSE_LEN + + DigestAlgorithm::COMPOSE_LEN, self.digest.as_ref().len().try_into().expect("long digest"), ) .expect("long digest"), @@ -2752,7 +2752,7 @@ mod test { #[test] #[allow(clippy::redundant_closure)] // lifetimes ... fn dnskey_compose_parse_scan() { - let rdata = Dnskey::new(10, 11, SecAlg::RSASHA1, b"key0").unwrap(); + let rdata = Dnskey::new(10, 11, SecurityAlgorithm::RSASHA1, b"key0").unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Dnskey::parse(parser)); test_scan(&["10", "11", "5", "a2V5MA=="], Dnskey::scan, &rdata); @@ -2765,7 +2765,7 @@ mod test { fn rrsig_compose_parse_scan() { let rdata = Rrsig::new( Rtype::A, - SecAlg::RSASHA1, + SecurityAlgorithm::RSASHA1, 3, Ttl::from_secs(12), Timestamp::from(13), @@ -2824,7 +2824,7 @@ mod test { #[allow(clippy::redundant_closure)] // lifetimes ... fn ds_compose_parse_scan() { let rdata = - Ds::new(10, SecAlg::RSASHA1, DigestAlg::SHA256, b"key").unwrap(); + Ds::new(10, SecurityAlgorithm::RSASHA1, DigestAlgorithm::SHA256, b"key").unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Ds::parse(parser)); test_scan(&["10", "5", "2", "6b6579"], Ds::scan, &rdata); @@ -2912,7 +2912,7 @@ mod test { Dnskey::new( 256, 3, - SecAlg::RSASHA256, + SecurityAlgorithm::RSASHA256, base64::decode::>( "AwEAAcTQyaIe6nt3xSPOG2L/YfwBkOVTJN6mlnZ249O5Rtt3ZSRQHxQS\ W61AODYw6bvgxrrGq8eeOuenFjcSYgNAMcBYoEYYmKDW6e9EryW4ZaT/\ @@ -2931,7 +2931,7 @@ mod test { Dnskey::new( 257, 3, - SecAlg::RSASHA256, + SecurityAlgorithm::RSASHA256, base64::decode::>( "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTO\ iW1vkIbzxeF3+/4RgWOq7HrxRixHlFlExOLAJr5emLvN\ @@ -2952,7 +2952,7 @@ mod test { Dnskey::new( 257, 3, - SecAlg::RSAMD5, + SecurityAlgorithm::RSAMD5, base64::decode::>( "AwEAAcVaA4jSBIGRrSzpecoJELvKE9+OMuFnL8mmUBsY\ lB6epN1CqX7NzwjDpi6VySiEXr0C4uTYkU/L1uMv2mHE\ @@ -2970,7 +2970,7 @@ mod test { #[test] fn dnskey_flags() { let dnskey = - Dnskey::new(257, 3, SecAlg::RSASHA256, bytes::Bytes::new()) + Dnskey::new(257, 3, SecurityAlgorithm::RSASHA256, bytes::Bytes::new()) .unwrap(); assert!(dnskey.is_zone_key()); assert!(dnskey.is_secure_entry_point()); diff --git a/src/rdata/nsec3.rs b/src/rdata/nsec3.rs index 3e551b43f..60112eea1 100644 --- a/src/rdata/nsec3.rs +++ b/src/rdata/nsec3.rs @@ -6,7 +6,7 @@ use super::dnssec::RtypeBitmap; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Nsec3HashAlg, Rtype}; +use crate::base::iana::{Nsec3HashAlgorithm, Rtype}; use crate::base::rdata::{ComposeRecordData, ParseRecordData, RecordData}; use crate::base::scan::{ ConvertSymbols, EntrySymbol, Scan, Scanner, ScannerError, @@ -46,7 +46,7 @@ use octseq::serde::{DeserializeOctets, SerializeOctets}; )) )] pub struct Nsec3 { - hash_algorithm: Nsec3HashAlg, + hash_algorithm: Nsec3HashAlgorithm, flags: u8, iterations: u16, salt: Nsec3Salt, @@ -61,7 +61,7 @@ impl Nsec3<()> { impl Nsec3 { pub fn new( - hash_algorithm: Nsec3HashAlg, + hash_algorithm: Nsec3HashAlgorithm, flags: u8, iterations: u16, salt: Nsec3Salt, @@ -78,7 +78,7 @@ impl Nsec3 { } } - pub fn hash_algorithm(&self) -> Nsec3HashAlg { + pub fn hash_algorithm(&self) -> Nsec3HashAlgorithm { self.hash_algorithm } @@ -140,7 +140,7 @@ impl Nsec3 { scanner: &mut S, ) -> Result { Ok(Self::new( - Nsec3HashAlg::scan(scanner)?, + Nsec3HashAlgorithm::scan(scanner)?, u8::scan(scanner)?, u16::scan(scanner)?, Nsec3Salt::scan(scanner)?, @@ -154,7 +154,7 @@ impl> Nsec3 { pub fn parse<'a, Src: Octets = Octs> + ?Sized>( parser: &mut Parser<'a, Src>, ) -> Result { - let hash_algorithm = Nsec3HashAlg::parse(parser)?; + let hash_algorithm = Nsec3HashAlgorithm::parse(parser)?; let flags = u8::parse(parser)?; let iterations = u16::parse(parser)?; let salt = Nsec3Salt::parse(parser)?; @@ -319,7 +319,7 @@ impl> ComposeRecordData for Nsec3 { fn rdlen(&self, _compress: bool) -> Option { Some( u16::checked_add( - Nsec3HashAlg::COMPOSE_LEN + Nsec3HashAlgorithm::COMPOSE_LEN + u8::COMPOSE_LEN + u16::COMPOSE_LEN, self.salt.compose_len(), @@ -432,7 +432,7 @@ pub struct Nsec3param { /// 3.1.1. Hash Algorithm /// "The Hash Algorithm field identifies the cryptographic hash /// algorithm used to construct the hash-value." - hash_algorithm: Nsec3HashAlg, + hash_algorithm: Nsec3HashAlgorithm, /// https://www.rfc-editor.org/rfc/rfc5155.html#section-3.1.2 /// 3.1.2. Flags @@ -487,7 +487,7 @@ impl Nsec3param<()> { impl Nsec3param { pub fn new( - hash_algorithm: Nsec3HashAlg, + hash_algorithm: Nsec3HashAlgorithm, flags: u8, iterations: u16, salt: Nsec3Salt, @@ -500,7 +500,7 @@ impl Nsec3param { } } - pub fn hash_algorithm(&self) -> Nsec3HashAlg { + pub fn hash_algorithm(&self) -> Nsec3HashAlgorithm { self.hash_algorithm } @@ -552,7 +552,7 @@ impl Nsec3param { parser: &mut Parser<'a, Src>, ) -> Result { Ok(Self::new( - Nsec3HashAlg::parse(parser)?, + Nsec3HashAlgorithm::parse(parser)?, u8::parse(parser)?, u16::parse(parser)?, Nsec3Salt::parse(parser)?, @@ -563,7 +563,7 @@ impl Nsec3param { scanner: &mut S, ) -> Result { Ok(Self::new( - Nsec3HashAlg::scan(scanner)?, + Nsec3HashAlgorithm::scan(scanner)?, u8::scan(scanner)?, u16::scan(scanner)?, Nsec3Salt::scan(scanner)?, @@ -592,7 +592,7 @@ where /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html fn default() -> Self { Self { - hash_algorithm: Nsec3HashAlg::SHA1, + hash_algorithm: Nsec3HashAlgorithm::SHA1, flags: 0, iterations: 0, salt: Nsec3Salt::empty(), @@ -740,7 +740,7 @@ impl> ComposeRecordData for Nsec3param { fn rdlen(&self, _compress: bool) -> Option { Some( u16::checked_add( - Nsec3HashAlg::COMPOSE_LEN + Nsec3HashAlgorithm::COMPOSE_LEN + u8::COMPOSE_LEN + u16::COMPOSE_LEN, self.salt.compose_len(), @@ -1023,7 +1023,7 @@ where Octs: FromBuilder, ::Builder: EmptyBuilder, { - type Err = base16::DecodeError; + type Err = Nsec3SaltFromStrError; fn from_str(s: &str) -> Result { if s == "-" { @@ -1032,7 +1032,11 @@ where }) } else { base16::decode(s) - .map(|octets| unsafe { Self::from_octets_unchecked(octets) }) + .map_err(Nsec3SaltFromStrError::DecodeError) + .and_then(|octets| { + Self::from_octets(octets) + .map_err(Nsec3SaltFromStrError::Nsec3SaltError) + }) } } } @@ -1251,6 +1255,27 @@ where } } +//------------ Nsec3SaltFromStrError ----------------------------------------- + +/// An error happened while parsing an NSEC3 salt from a string. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Nsec3SaltFromStrError { + DecodeError(base16::DecodeError), + Nsec3SaltError(Nsec3SaltError), +} + +impl fmt::Display for Nsec3SaltFromStrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Nsec3SaltFromStrError::DecodeError(err) => err.fmt(f), + Nsec3SaltFromStrError::Nsec3SaltError(err) => err.fmt(f), + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for Nsec3SaltFromStrError {} + //------------ OwnerHash ----------------------------------------------------- /// The hash over the next owner name. @@ -1662,7 +1687,7 @@ mod test { rtype.add(Rtype::A).unwrap(); rtype.add(Rtype::SRV).unwrap(); let rdata = Nsec3::new( - Nsec3HashAlg::SHA1, + Nsec3HashAlgorithm::SHA1, 10, 11, Nsec3Salt::from_octets(Vec::from("bar")).unwrap(), @@ -1689,7 +1714,7 @@ mod test { rtype.add(Rtype::A).unwrap(); rtype.add(Rtype::SRV).unwrap(); let rdata = Nsec3::new( - Nsec3HashAlg::SHA1, + Nsec3HashAlgorithm::SHA1, 10, 11, Nsec3Salt::empty(), @@ -1713,7 +1738,7 @@ mod test { #[allow(clippy::redundant_closure)] // lifetimes ... fn nsec3param_compose_parse_scan() { let rdata = Nsec3param::new( - Nsec3HashAlg::SHA1, + Nsec3HashAlgorithm::SHA1, 10, 11, Nsec3Salt::from_octets(Vec::from("bar")).unwrap(), diff --git a/src/rdata/zonemd.rs b/src/rdata/zonemd.rs index 025dae200..5404485b1 100644 --- a/src/rdata/zonemd.rs +++ b/src/rdata/zonemd.rs @@ -9,12 +9,12 @@ #![allow(clippy::needless_maybe_sized)] use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Rtype, ZonemdAlg, ZonemdScheme}; +use crate::base::iana::{Rtype, ZonemdAlgorithm, ZonemdScheme}; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::scan::{Scan, Scanner}; use crate::base::serial::Serial; -use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::utils::base16; use core::cmp::Ordering; use core::{fmt, hash}; @@ -30,7 +30,7 @@ const DIGEST_MIN_LEN: usize = 12; pub struct Zonemd { serial: Serial, scheme: ZonemdScheme, - algo: ZonemdAlg, + algo: ZonemdAlgorithm, #[cfg_attr( feature = "serde", serde( @@ -55,7 +55,7 @@ impl Zonemd { pub fn new( serial: Serial, scheme: ZonemdScheme, - algo: ZonemdAlg, + algo: ZonemdAlgorithm, digest: Octs, ) -> Self { Self { @@ -77,7 +77,7 @@ impl Zonemd { } /// Get the hash algorithm field. - pub fn algorithm(&self) -> ZonemdAlg { + pub fn algorithm(&self) -> ZonemdAlgorithm { self.algo } @@ -338,7 +338,7 @@ mod test { #[cfg(feature = "zonefile")] #[test] fn zonemd_parse_zonefile() { - use crate::base::iana::ZonemdAlg; + use crate::base::iana::ZonemdAlgorithm; use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; @@ -372,7 +372,7 @@ ns2 3600 IN AAAA 2001:db8::63 ZoneRecordData::Zonemd(rd) => { assert_eq!(2018031900, rd.serial().into_int()); assert_eq!(ZonemdScheme::SIMPLE, rd.scheme()); - assert_eq!(ZonemdAlg::SHA384, rd.algorithm()); + assert_eq!(ZonemdAlgorithm::SHA384, rd.algorithm()); } _ => panic!(), } diff --git a/src/resolv/stub/mod.rs b/src/resolv/stub/mod.rs index a1f46aabb..a41484517 100644 --- a/src/resolv/stub/mod.rs +++ b/src/resolv/stub/mod.rs @@ -410,20 +410,19 @@ impl<'a> Query<'a> { let msg = Message::from_octets(message.as_target().to_vec()) .expect("Message::from_octets should not fail"); - let request_msg = RequestMessage::new(msg).map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + let request_msg = RequestMessage::new(msg) + .map_err(|e| io::Error::other(e.to_string()))?; - let transport = self.resolver.get_transport().await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + let transport = self + .resolver + .get_transport() + .await + .map_err(|e| io::Error::other(e.to_string()))?; let mut gr_fut = transport.send_request(request_msg); let reply = timeout(self.resolver.options.timeout, gr_fut.get_response()) .await? - .map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + .map_err(|e| io::Error::other(e.to_string()))?; Ok(Answer { message: reply }) } diff --git a/src/sign/config.rs b/src/sign/config.rs deleted file mode 100644 index 43e23b905..000000000 --- a/src/sign/config.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Types for tuning configurable aspects of DNSSEC signing. -use core::marker::PhantomData; - -use octseq::{EmptyBuilder, FromBuilder}; - -use crate::base::{Name, ToName}; -use crate::sign::denial::config::DenialConfig; -use crate::sign::denial::nsec3::{ - Nsec3HashProvider, OnDemandNsec3HashProvider, -}; -use crate::sign::records::{DefaultSorter, Sorter}; -use crate::sign::signatures::strategy::DefaultSigningKeyUsageStrategy; -use crate::sign::signatures::strategy::RrsigValidityPeriodStrategy; -use crate::sign::signatures::strategy::SigningKeyUsageStrategy; -use crate::sign::SignRaw; - -//------------ SigningConfig ------------------------------------------------- - -/// Signing configuration for a DNSSEC signed zone. -pub struct SigningConfig< - N, - Octs, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP = OnDemandNsec3HashProvider, -> where - HP: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - Sort: Sorter, -{ - /// Authenticated denial of existing mechanism configuration. - pub denial: DenialConfig, - - /// Should keys used to sign the zone be added as DNSKEY RRs? - pub add_used_dnskeys: bool, - - pub rrsig_validity_period_strategy: ValidityStrat, - - _phantom: PhantomData<(Inner, KeyStrat, Sort)>, -} - -impl - SigningConfig -where - HP: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - Sort: Sorter, -{ - pub fn new( - denial: DenialConfig, - add_used_dnskeys: bool, - rrsig_validity_period_strategy: ValidityStrat, - ) -> Self { - Self { - denial, - add_used_dnskeys, - rrsig_validity_period_strategy, - _phantom: PhantomData, - } - } - - pub fn set_rrsig_validity_period_strategy( - &mut self, - rrsig_validity_period_strategy: ValidityStrat, - ) { - self.rrsig_validity_period_strategy = rrsig_validity_period_strategy; - } -} - -impl - SigningConfig< - N, - Octs, - Inner, - DefaultSigningKeyUsageStrategy, - ValidityStrat, - DefaultSorter, - OnDemandNsec3HashProvider, - > -where - N: ToName + From>, - Octs: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, - Inner: SignRaw, - ValidityStrat: RrsigValidityPeriodStrategy, -{ - pub fn default(rrsig_validity_period_strategy: ValidityStrat) -> Self { - Self { - denial: Default::default(), - add_used_dnskeys: true, - rrsig_validity_period_strategy, - _phantom: Default::default(), - } - } -} diff --git a/src/sign/crypto/common.rs b/src/sign/crypto/common.rs deleted file mode 100644 index 7442d36a5..000000000 --- a/src/sign/crypto/common.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! DNSSEC signing using built-in backends. -//! -//! This backend supports all the algorithms supported by Ring and OpenSSL, -//! depending on whether the respective crate features are enabled. See the -//! documentation for each backend for more information. - -use core::fmt; -use std::sync::Arc; - -use ::ring::rand::SystemRandom; - -use crate::base::iana::SecAlg; -use crate::sign::error::{FromBytesError, GenerateError, SignError}; -use crate::sign::{SecretKeyBytes, SignRaw}; -use crate::validate::{PublicKeyBytes, Signature}; - -#[cfg(feature = "openssl")] -use crate::sign::crypto::openssl; - -#[cfg(feature = "ring")] -use crate::sign::crypto::ring; - -//----------- KeyPair -------------------------------------------------------- - -/// A key pair based on a built-in backend. -/// -/// 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")] - Ring(ring::KeyPair), - - /// A key backed by OpenSSL. - #[cfg(feature = "openssl")] - OpenSSL(openssl::KeyPair), -} - -//--- Conversion to and from bytes - -impl KeyPair { - /// Import a key pair from bytes. - pub fn from_bytes( - secret: &SecretKeyBytes, - public: &PublicKeyBytes, - ) -> Result { - // Prefer Ring if it is available. - #[cfg(feature = "ring")] - match public { - PublicKeyBytes::RsaSha1(k) - | PublicKeyBytes::RsaSha1Nsec3Sha1(k) - | PublicKeyBytes::RsaSha256(k) - | PublicKeyBytes::RsaSha512(k) - if k.n.len() >= 2048 / 8 => - { - let rng = Arc::new(SystemRandom::new()); - let key = ring::KeyPair::from_bytes(secret, public, rng)?; - return Ok(Self::Ring(key)); - } - - PublicKeyBytes::EcdsaP256Sha256(_) - | PublicKeyBytes::EcdsaP384Sha384(_) => { - let rng = Arc::new(SystemRandom::new()); - let key = ring::KeyPair::from_bytes(secret, public, rng)?; - return Ok(Self::Ring(key)); - } - - PublicKeyBytes::Ed25519(_) => { - let rng = Arc::new(SystemRandom::new()); - let key = ring::KeyPair::from_bytes(secret, public, rng)?; - return Ok(Self::Ring(key)); - } - - _ => {} - } - - // Fall back to OpenSSL. - #[cfg(feature = "openssl")] - return Ok(Self::OpenSSL(openssl::KeyPair::from_bytes( - secret, public, - )?)); - - // Otherwise fail. - #[allow(unreachable_code)] - Err(FromBytesError::UnsupportedAlgorithm) - } -} - -//--- SignRaw - -impl SignRaw for KeyPair { - fn algorithm(&self) -> SecAlg { - match self { - #[cfg(feature = "ring")] - Self::Ring(key) => key.algorithm(), - #[cfg(feature = "openssl")] - Self::OpenSSL(key) => key.algorithm(), - } - } - - fn raw_public_key(&self) -> PublicKeyBytes { - match self { - #[cfg(feature = "ring")] - Self::Ring(key) => key.raw_public_key(), - #[cfg(feature = "openssl")] - Self::OpenSSL(key) => key.raw_public_key(), - } - } - - fn sign_raw(&self, data: &[u8]) -> Result { - match self { - #[cfg(feature = "ring")] - Self::Ring(key) => key.sign_raw(data), - #[cfg(feature = "openssl")] - Self::OpenSSL(key) => key.sign_raw(data), - } - } -} - -//----------- GenerateParams ------------------------------------------------- - -/// Parameters for generating a secret key. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum GenerateParams { - /// Generate an RSA/SHA-256 keypair. - RsaSha256 { - /// The number of bits in the public modulus. - /// - /// 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, - }, - - /// Generate an ECDSA P-256/SHA-256 keypair. - EcdsaP256Sha256, - - /// Generate an ECDSA P-384/SHA-384 keypair. - EcdsaP384Sha384, - - /// Generate an Ed25519 keypair. - Ed25519, - - /// An Ed448 keypair. - Ed448, -} - -//--- Inspection - -impl GenerateParams { - /// The algorithm of the generated key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256 { .. } => SecAlg::RSASHA256, - Self::EcdsaP256Sha256 => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384 => SecAlg::ECDSAP384SHA384, - Self::Ed25519 => SecAlg::ED25519, - Self::Ed448 => SecAlg::ED448, - } - } -} - -//----------- generate() ----------------------------------------------------- - -/// Generate a new secret key for the given algorithm. -pub fn generate( - params: GenerateParams, -) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { - // Use Ring if it is available. - #[cfg(feature = "ring")] - if matches!( - ¶ms, - GenerateParams::EcdsaP256Sha256 - | GenerateParams::EcdsaP384Sha384 - | GenerateParams::Ed25519 - ) { - let rng = ::ring::rand::SystemRandom::new(); - return Ok(ring::generate(params, &rng)?); - } - - // Fall back to OpenSSL. - #[cfg(feature = "openssl")] - { - let key = openssl::generate(params)?; - return Ok((key.to_bytes(), key.raw_public_key())); - } - - // Otherwise fail. - #[allow(unreachable_code)] - Err(GenerateError::UnsupportedAlgorithm) -} - -//--- Formatting - -impl fmt::Display for GenerateError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for GenerateError {} diff --git a/src/sign/crypto/mod.rs b/src/sign/crypto/mod.rs deleted file mode 100644 index e6dd0cefd..000000000 --- a/src/sign/crypto/mod.rs +++ /dev/null @@ -1,103 +0,0 @@ -//! Cryptographic backends, key generation and import. -//! -//! 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 (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 ([`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 the [`SignRaw`] trait. -//! -//! While each cryptographic backend can support a limited number of signature -//! algorithms, even the types independent of a cryptographic backend (e.g. -//! [`SecretKeyBytes`] and [`GenerateParams`]) support a limited number of -//! algorithms. Even with custom cryptographic backends, this module can only -//! support these algorithms. -//! -//! # Importing keys -//! -//! Keys can be imported from files stored on disk in the conventional BIND -//! format. -//! -//! ``` -//! # use domain::base::iana::SecAlg; -//! # use domain::validate; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::{SecretKeyBytes, SigningKey}; -//! // Load an Ed25519 key named 'Ktest.+015+56037'. -//! let base = "test-data/dnssec-keys/Ktest.+015+56037"; -//! let sec_text = std::fs::read_to_string(format!("{base}.private")).unwrap(); -//! let sec_bytes = SecretKeyBytes::parse_from_bind(&sec_text).unwrap(); -//! let pub_text = std::fs::read_to_string(format!("{base}.key")).unwrap(); -//! let pub_key = validate::Key::>::parse_from_bind(&pub_text).unwrap(); -//! -//! // Parse the key into Ring or OpenSSL. -//! let key_pair = KeyPair::from_bytes(&sec_bytes, pub_key.raw_public_key()).unwrap(); -//! -//! // Associate the key with important metadata. -//! let key = SigningKey::new(pub_key.owner().clone(), pub_key.flags(), key_pair); -//! -//! // Check that the owner, algorithm, and key tag matched expectations. -//! assert_eq!(key.owner().to_string(), "test"); -//! assert_eq!(key.algorithm(), SecAlg::ED25519); -//! assert_eq!(key.public_key().key_tag(), 56037); -//! ``` -//! -//! # Generating keys -//! -//! Keys can also be generated. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! // Generate a new Ed25519 key. -//! let params = GenerateParams::Ed25519; -//! let (sec_bytes, pub_bytes) = common::generate(params).unwrap(); -//! -//! // Parse the key into Ring or OpenSSL. -//! let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! -//! // Associate the key with important metadata. -//! let owner: Name> = "www.example.org.".parse().unwrap(); -//! let flags = 257; // key signing key -//! let key = SigningKey::new(owner, flags, key_pair); -//! -//! // Access the public key (with metadata). -//! let pub_key = key.public_key(); -//! println!("{:?}", pub_key); -//! ``` -//! -//! # Signing data -//! -//! Given some data and a key, the data can be signed with the key. -//! -//! ``` -//! # use domain::base::Name; -//! # use domain::sign::crypto::common; -//! # use domain::sign::crypto::common::GenerateParams; -//! # use domain::sign::crypto::common::KeyPair; -//! # use domain::sign::keys::SigningKey; -//! # use domain::sign::traits::SignRaw; -//! # let (sec_bytes, pub_bytes) = common::generate(GenerateParams::Ed25519).unwrap(); -//! # let key_pair = KeyPair::from_bytes(&sec_bytes, &pub_bytes).unwrap(); -//! # let key = SigningKey::new(Name::>::root(), 257, key_pair); -//! // Sign arbitrary byte sequences with the key. -//! let sig = key.raw_secret_key().sign_raw(b"Hello, World!").unwrap(); -//! println!("{:?}", sig); -//! ``` -//! -//! [`SignRaw`]: crate::sign::traits::SignRaw -//! [`GenerateParams`]: crate::sign::crypto::common::GenerateParams -//! [`SecretKeyBytes`]: crate::sign::keys::SecretKeyBytes -pub mod common; -pub mod openssl; -pub mod ring; diff --git a/src/sign/crypto/openssl.rs b/src/sign/crypto/openssl.rs deleted file mode 100644 index 2699df447..000000000 --- a/src/sign/crypto/openssl.rs +++ /dev/null @@ -1,567 +0,0 @@ -//! DNSSEC signing using OpenSSL. -//! -//! This backend supports the following algorithms: -//! -//! - RSA/SHA-256 (512-bit keys or larger) -//! - ECDSA P-256/SHA-256 -//! - ECDSA P-384/SHA-384 -//! - Ed25519 -//! - Ed448 - -#![cfg(feature = "openssl")] -#![cfg_attr(docsrs, doc(cfg(feature = "openssl")))] - -use core::fmt; - -use std::{boxed::Box, vec::Vec}; - -use openssl::{ - bn::BigNum, - ecdsa::EcdsaSig, - error::ErrorStack, - pkey::{self, PKey, Private}, -}; -use secrecy::ExposeSecret; - -use crate::base::iana::SecAlg; -use crate::sign::crypto::common::GenerateParams; -use crate::sign::error::SignError; -use crate::sign::{RsaSecretKeyBytes, SecretKeyBytes, SignRaw}; -use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; - -//----------- KeyPair -------------------------------------------------------- - -/// A key pair backed by OpenSSL. -pub struct KeyPair { - /// The algorithm used by the key. - algorithm: SecAlg, - - /// The private key. - pkey: PKey, -} - -//--- Conversion to and from bytes - -impl KeyPair { - /// Import a key pair from bytes into OpenSSL. - pub fn from_bytes( - secret: &SecretKeyBytes, - public: &PublicKeyBytes, - ) -> Result { - fn num(slice: &[u8]) -> Result { - let mut v = BigNum::new()?; - v.copy_from_slice(slice)?; - Ok(v) - } - - fn secure_num(slice: &[u8]) -> Result { - let mut v = BigNum::new_secure()?; - v.copy_from_slice(slice)?; - Ok(v) - } - - let pkey = match (secret, public) { - (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { - // Ensure that the public and private key match. - if p != &RsaPublicKeyBytes::from(s) { - return Err(FromBytesError::InvalidKey); - } - - let n = num(&s.n)?; - let e = num(&s.e)?; - 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 - // deprecated methods called here. - - let key = openssl::rsa::Rsa::from_private_components( - n, e, d, p, q, d_p, d_q, q_i, - )?; - - if !key.check_key()? { - return Err(FromBytesError::InvalidKey); - } - - PKey::from_rsa(key)? - } - - ( - SecretKeyBytes::EcdsaP256Sha256(s), - PublicKeyBytes::EcdsaP256Sha256(p), - ) => { - use openssl::{bn, ec, nid}; - - 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.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)?; - PKey::from_ec_key(k)? - } - - ( - SecretKeyBytes::EcdsaP384Sha384(s), - PublicKeyBytes::EcdsaP384Sha384(p), - ) => { - use openssl::{bn, ec, nid}; - - 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.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)?; - PKey::from_ec_key(k)? - } - - (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { - use openssl::memcmp; - - let id = pkey::Id::ED25519; - 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 { - return Err(FromBytesError::InvalidKey); - } - } - - (SecretKeyBytes::Ed448(s), PublicKeyBytes::Ed448(p)) => { - use openssl::memcmp; - - let id = pkey::Id::ED448; - 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 { - return Err(FromBytesError::InvalidKey); - } - } - - // The public and private key types did not match. - _ => return Err(FromBytesError::InvalidKey), - }; - - Ok(Self { - algorithm: secret.algorithm(), - pkey, - }) - } - - /// Export the secret key into bytes. - /// - /// # Panics - /// - /// Panics if OpenSSL fails or if memory could not be allocated. - pub fn to_bytes(&self) -> SecretKeyBytes { - // TODO: Consider security implications of secret data in 'Vec's. - match self.algorithm { - SecAlg::RSASHA256 => { - let key = self.pkey.rsa().unwrap(); - SecretKeyBytes::RsaSha256(RsaSecretKeyBytes { - n: key.n().to_vec().into(), - e: key.e().to_vec().into(), - d: key.d().to_vec().into(), - p: key.p().unwrap().to_vec().into(), - q: key.q().unwrap().to_vec().into(), - d_p: key.dmp1().unwrap().to_vec().into(), - d_q: key.dmq1().unwrap().to_vec().into(), - q_i: key.iqmp().unwrap().to_vec().into(), - }) - } - SecAlg::ECDSAP256SHA256 => { - let key = self.pkey.ec_key().unwrap(); - let key = key.private_key().to_vec_padded(32).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(); - let key: Box<[u8; 48]> = key.try_into().unwrap(); - SecretKeyBytes::EcdsaP384Sha384(key.into()) - } - SecAlg::ED25519 => { - let key = self.pkey.raw_private_key().unwrap(); - let key: Box<[u8; 32]> = key.try_into().unwrap(); - SecretKeyBytes::Ed25519(key.into()) - } - SecAlg::ED448 => { - let key = self.pkey.raw_private_key().unwrap(); - let key: Box<[u8; 57]> = key.try_into().unwrap(); - SecretKeyBytes::Ed448(key.into()) - } - _ => unreachable!(), - } - } -} - -//--- Signing - -impl KeyPair { - fn sign(&self, data: &[u8]) -> Result, ErrorStack> { - use openssl::hash::MessageDigest; - use openssl::sign::Signer; - - match self.algorithm { - SecAlg::RSASHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - s.set_rsa_padding(openssl::rsa::Padding::PKCS1)?; - s.sign_oneshot_to_vec(data) - } - - SecAlg::ECDSAP256SHA256 => { - let mut s = Signer::new(MessageDigest::sha256(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; - // Convert from DER to the fixed representation. - let signature = EcdsaSig::from_der(&signature)?; - let mut r = signature.r().to_vec_padded(32)?; - let mut s = signature.s().to_vec_padded(32)?; - r.append(&mut s); - Ok(r) - } - SecAlg::ECDSAP384SHA384 => { - let mut s = Signer::new(MessageDigest::sha384(), &self.pkey)?; - let signature = s.sign_oneshot_to_vec(data)?; - // Convert from DER to the fixed representation. - let signature = EcdsaSig::from_der(&signature)?; - let mut r = signature.r().to_vec_padded(48)?; - let mut s = signature.s().to_vec_padded(48)?; - r.append(&mut s); - Ok(r) - } - - SecAlg::ED25519 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) - } - SecAlg::ED448 => { - let mut s = Signer::new_without_digest(&self.pkey)?; - s.sign_oneshot_to_vec(data) - } - - _ => unreachable!(), - } - } -} - -//--- SignRaw - -impl SignRaw for KeyPair { - fn algorithm(&self) -> SecAlg { - self.algorithm - } - - fn raw_public_key(&self) -> PublicKeyBytes { - match self.algorithm { - SecAlg::RSASHA256 => { - let key = self.pkey.rsa().unwrap(); - PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { - n: key.n().to_vec().into(), - e: key.e().to_vec().into(), - }) - } - SecAlg::ECDSAP256SHA256 => { - let key = self.pkey.ec_key().unwrap(); - let form = openssl::ec::PointConversionForm::UNCOMPRESSED; - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let key = key - .public_key() - .to_bytes(key.group(), form, &mut ctx) - .unwrap(); - PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) - } - SecAlg::ECDSAP384SHA384 => { - let key = self.pkey.ec_key().unwrap(); - let form = openssl::ec::PointConversionForm::UNCOMPRESSED; - let mut ctx = openssl::bn::BigNumContext::new().unwrap(); - let key = key - .public_key() - .to_bytes(key.group(), form, &mut ctx) - .unwrap(); - PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) - } - SecAlg::ED25519 => { - let key = self.pkey.raw_public_key().unwrap(); - PublicKeyBytes::Ed25519(key.try_into().unwrap()) - } - SecAlg::ED448 => { - let key = self.pkey.raw_public_key().unwrap(); - PublicKeyBytes::Ed448(key.try_into().unwrap()) - } - _ => unreachable!(), - } - } - - fn sign_raw(&self, data: &[u8]) -> Result { - let signature = self - .sign(data) - .map(Vec::into_boxed_slice) - .map_err(|_| SignError)?; - - match self.algorithm { - SecAlg::RSASHA256 => Ok(Signature::RsaSha256(signature)), - - SecAlg::ECDSAP256SHA256 => signature - .try_into() - .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError), - SecAlg::ECDSAP384SHA384 => signature - .try_into() - .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError), - - SecAlg::ED25519 => signature - .try_into() - .map(Signature::Ed25519) - .map_err(|_| SignError), - SecAlg::ED448 => signature - .try_into() - .map(Signature::Ed448) - .map_err(|_| SignError), - - _ => unreachable!(), - } - } -} - -//----------- generate() ----------------------------------------------------- - -/// Generate a new secret key for the given algorithm. -pub fn generate(params: GenerateParams) -> Result { - let algorithm = params.algorithm(); - let pkey = match params { - GenerateParams::RsaSha256 { bits } => { - openssl::rsa::Rsa::generate(bits).and_then(PKey::from_rsa)? - } - GenerateParams::EcdsaP256Sha256 => { - let group = openssl::nid::Nid::X9_62_PRIME256V1; - let group = openssl::ec::EcGroup::from_curve_name(group)?; - PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? - } - GenerateParams::EcdsaP384Sha384 => { - let group = openssl::nid::Nid::SECP384R1; - let group = openssl::ec::EcGroup::from_curve_name(group)?; - PKey::from_ec_key(openssl::ec::EcKey::generate(&group)?)? - } - GenerateParams::Ed25519 => PKey::generate_ed25519()?, - GenerateParams::Ed448 => PKey::generate_ed448()?, - }; - - Ok(KeyPair { algorithm, pkey }) -} - -//============ Error Types =================================================== - -//----------- FromBytesError ----------------------------------------------- - -/// An error in importing a key pair from bytes into OpenSSL. -#[derive(Clone, Debug)] -pub enum FromBytesError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// The key's parameters were invalid. - InvalidKey, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversion - -impl From for FromBytesError { - fn from(_: ErrorStack) -> Self { - Self::Implementation - } -} - -//--- Formatting - -impl fmt::Display for FromBytesError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::InvalidKey => "malformed or insecure private key", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for FromBytesError {} - -//----------- GenerateError -------------------------------------------------- - -/// An error in generating a key pair with OpenSSL. -#[derive(Clone, Debug)] -pub enum GenerateError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversion - -impl From for GenerateError { - fn from(_: ErrorStack) -> Self { - Self::Implementation - } -} - -//--- Formatting - -impl fmt::Display for GenerateError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for GenerateError {} - -//============ Tests ========================================================= - -#[cfg(test)] -mod tests { - use std::{string::ToString, vec::Vec}; - - use crate::base::iana::SecAlg; - use crate::sign::crypto::common::GenerateParams; - use crate::sign::{SecretKeyBytes, SignRaw}; - use crate::validate::Key; - - use super::KeyPair; - - const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 60616), - (SecAlg::ECDSAP256SHA256, 42253), - (SecAlg::ECDSAP384SHA384, 33566), - (SecAlg::ED25519, 56037), - (SecAlg::ED448, 7379), - ]; - - #[test] - fn generate() { - for &(algorithm, _) in KEYS { - let params = match algorithm { - SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, - SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, - SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, - SecAlg::ED25519 => GenerateParams::Ed25519, - SecAlg::ED448 => GenerateParams::Ed448, - _ => unreachable!(), - }; - - let _ = super::generate(params).unwrap(); - } - } - - #[test] - fn generated_roundtrip() { - for &(algorithm, _) in KEYS { - let params = match algorithm { - SecAlg::RSASHA256 => GenerateParams::RsaSha256 { bits: 3072 }, - SecAlg::ECDSAP256SHA256 => GenerateParams::EcdsaP256Sha256, - SecAlg::ECDSAP384SHA384 => GenerateParams::EcdsaP384Sha384, - SecAlg::ED25519 => GenerateParams::Ed25519, - SecAlg::ED448 => GenerateParams::Ed448, - _ => unreachable!(), - }; - - let key = super::generate(params).unwrap(); - let gen_key = key.to_bytes(); - let pub_key = key.raw_public_key(); - let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); - assert!(key.pkey.public_eq(&equiv.pkey)); - } - } - - #[test] - fn imported_roundtrip() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_from_bind(&data).unwrap(); - let pub_key = pub_key.raw_public_key(); - - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); - - let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); - let same = key.to_bytes().display_as_bind().to_string(); - - let data = data.lines().collect::>(); - let same = same.lines().collect::>(); - assert_eq!(data, same); - } - } - - #[test] - fn public_key() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_from_bind(&data).unwrap(); - let pub_key = pub_key.raw_public_key(); - - let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); - - assert_eq!(key.raw_public_key(), *pub_key); - } - } - - #[test] - fn sign() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_from_bind(&data).unwrap(); - let pub_key = pub_key.raw_public_key(); - - let key = KeyPair::from_bytes(&gen_key, pub_key).unwrap(); - - let _ = key.sign_raw(b"Hello, World!").unwrap(); - } - } -} diff --git a/src/sign/crypto/ring.rs b/src/sign/crypto/ring.rs deleted file mode 100644 index 6663e61b0..000000000 --- a/src/sign/crypto/ring.rs +++ /dev/null @@ -1,442 +0,0 @@ -//! DNSSEC signing using `ring`. -//! -//! This backend supports the following algorithms: -//! -//! - RSA/SHA-256 (2048-bit keys or larger) -//! - ECDSA P-256/SHA-256 -//! - ECDSA P-384/SHA-384 -//! - Ed25519 - -#![cfg(feature = "ring")] -#![cfg_attr(docsrs, doc(cfg(feature = "ring")))] - -use core::fmt; - -use std::{boxed::Box, sync::Arc, vec::Vec}; - -use ring::signature::{ - EcdsaKeyPair, Ed25519KeyPair, KeyPair as _, RsaKeyPair, -}; -use secrecy::ExposeSecret; - -use crate::base::iana::SecAlg; -use crate::sign::crypto::common::GenerateParams; -use crate::sign::error::SignError; -use crate::sign::{SecretKeyBytes, SignRaw}; -use crate::validate::{PublicKeyBytes, RsaPublicKeyBytes, Signature}; - -//----------- KeyPair -------------------------------------------------------- - -/// A key pair backed by `ring`. -pub enum KeyPair { - /// An RSA/SHA-256 keypair. - RsaSha256 { - key: RsaKeyPair, - rng: Arc, - }, - - /// An ECDSA P-256/SHA-256 keypair. - EcdsaP256Sha256 { - key: EcdsaKeyPair, - rng: Arc, - }, - - /// An ECDSA P-384/SHA-384 keypair. - EcdsaP384Sha384 { - key: EcdsaKeyPair, - rng: Arc, - }, - - /// An Ed25519 keypair. - Ed25519(Ed25519KeyPair), -} - -//--- Conversion from bytes - -impl KeyPair { - /// Import a key pair from bytes into OpenSSL. - pub fn from_bytes( - secret: &SecretKeyBytes, - public: &PublicKeyBytes, - rng: Arc, - ) -> Result { - match (secret, public) { - (SecretKeyBytes::RsaSha256(s), PublicKeyBytes::RsaSha256(p)) => { - // Ensure that the public and private key match. - if p != &RsaPublicKeyBytes::from(s) { - return Err(FromBytesError::InvalidKey); - } - - // Ensure that the key is strong enough. - if p.n.len() < 2048 / 8 { - return Err(FromBytesError::WeakKey); - } - - let components = ring::rsa::KeyPairComponents { - public_key: ring::rsa::PublicKeyComponents { - n: s.n.as_ref(), - e: s.e.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) - .map(|key| Self::RsaSha256 { key, rng }) - } - - ( - SecretKeyBytes::EcdsaP256Sha256(s), - PublicKeyBytes::EcdsaP256Sha256(p), - ) => { - let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; - EcdsaKeyPair::from_private_key_and_public_key( - alg, - s.expose_secret(), - p.as_slice(), - &*rng, - ) - .map_err(|_| FromBytesError::InvalidKey) - .map(|key| Self::EcdsaP256Sha256 { key, rng }) - } - - ( - SecretKeyBytes::EcdsaP384Sha384(s), - PublicKeyBytes::EcdsaP384Sha384(p), - ) => { - let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; - EcdsaKeyPair::from_private_key_and_public_key( - alg, - s.expose_secret(), - p.as_slice(), - &*rng, - ) - .map_err(|_| FromBytesError::InvalidKey) - .map(|key| Self::EcdsaP384Sha384 { key, rng }) - } - - (SecretKeyBytes::Ed25519(s), PublicKeyBytes::Ed25519(p)) => { - Ed25519KeyPair::from_seed_and_public_key( - s.expose_secret(), - p.as_slice(), - ) - .map_err(|_| FromBytesError::InvalidKey) - .map(Self::Ed25519) - } - - (SecretKeyBytes::Ed448(_), PublicKeyBytes::Ed448(_)) => { - Err(FromBytesError::UnsupportedAlgorithm) - } - - // The public and private key types did not match. - _ => Err(FromBytesError::InvalidKey), - } - } -} - -//--- SignRaw - -impl SignRaw for KeyPair { - fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256 { .. } => SecAlg::RSASHA256, - Self::EcdsaP256Sha256 { .. } => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384 { .. } => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - } - } - - fn raw_public_key(&self) -> PublicKeyBytes { - match self { - Self::RsaSha256 { key, rng: _ } => { - let components: ring::rsa::PublicKeyComponents> = - key.public().into(); - PublicKeyBytes::RsaSha256(RsaPublicKeyBytes { - n: components.n.into(), - e: components.e.into(), - }) - } - - Self::EcdsaP256Sha256 { key, rng: _ } => { - let key = key.public_key().as_ref(); - let key = Box::<[u8]>::from(key); - PublicKeyBytes::EcdsaP256Sha256(key.try_into().unwrap()) - } - - Self::EcdsaP384Sha384 { key, rng: _ } => { - let key = key.public_key().as_ref(); - let key = Box::<[u8]>::from(key); - PublicKeyBytes::EcdsaP384Sha384(key.try_into().unwrap()) - } - - Self::Ed25519(key) => { - let key = key.public_key().as_ref(); - let key = Box::<[u8]>::from(key); - PublicKeyBytes::Ed25519(key.try_into().unwrap()) - } - } - } - - fn sign_raw(&self, data: &[u8]) -> Result { - match self { - Self::RsaSha256 { key, rng } => { - let mut buf = vec![0u8; key.public().modulus_len()]; - let pad = &ring::signature::RSA_PKCS1_SHA256; - key.sign(pad, &**rng, data, &mut buf) - .map(|()| Signature::RsaSha256(buf.into_boxed_slice())) - .map_err(|_| SignError) - } - - Self::EcdsaP256Sha256 { key, rng } => key - .sign(&**rng, data) - .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) - .and_then(|buf| { - buf.try_into() - .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError) - }), - - Self::EcdsaP384Sha384 { key, rng } => key - .sign(&**rng, data) - .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) - .and_then(|buf| { - buf.try_into() - .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError) - }), - - Self::Ed25519(key) => { - let sig = key.sign(data); - let buf: Box<[u8]> = sig.as_ref().into(); - buf.try_into() - .map(Signature::Ed25519) - .map_err(|_| SignError) - } - } - } -} - -//----------- generate() ----------------------------------------------------- - -/// Generate a new key pair for the given algorithm. -/// -/// While this uses Ring internally, the opaque nature of Ring means that it -/// is not possible to export a secret key from [`KeyPair`]. Thus, the bytes -/// of the secret key are returned directly. -pub fn generate( - params: GenerateParams, - rng: &dyn ring::rand::SecureRandom, -) -> Result<(SecretKeyBytes, PublicKeyBytes), GenerateError> { - match params { - GenerateParams::EcdsaP256Sha256 => { - // Generate a key and a PKCS#8 document out of Ring. - let alg = &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING; - let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; - - // Manually parse the PKCS#8 document for the private key. - let sk: Box<[u8]> = Box::from(&doc.as_ref()[36..68]); - 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]); - let pk = pk.try_into().unwrap(); - let pk = PublicKeyBytes::EcdsaP256Sha256(pk); - - Ok((sk, pk)) - } - - GenerateParams::EcdsaP384Sha384 => { - // Generate a key and a PKCS#8 document out of Ring. - let alg = &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING; - let doc = EcdsaKeyPair::generate_pkcs8(alg, rng)?; - - // Manually parse the PKCS#8 document for the private key. - let sk: Box<[u8]> = Box::from(&doc.as_ref()[35..83]); - 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]); - let pk = pk.try_into().unwrap(); - let pk = PublicKeyBytes::EcdsaP384Sha384(pk); - - Ok((sk, pk)) - } - - GenerateParams::Ed25519 => { - // Generate a key and a PKCS#8 document out of Ring. - let doc = Ed25519KeyPair::generate_pkcs8(rng)?; - - // Manually parse the PKCS#8 document for the private key. - let sk: Box<[u8]> = Box::from(&doc.as_ref()[16..48]); - 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]); - let pk = pk.try_into().unwrap(); - let pk = PublicKeyBytes::Ed25519(pk); - - Ok((sk, pk)) - } - - _ => Err(GenerateError::UnsupportedAlgorithm), - } -} - -//============ Error Types =================================================== - -/// An error in importing a key pair from bytes into Ring. -#[derive(Clone, Debug)] -pub enum FromBytesError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// The provided keypair was invalid. - InvalidKey, - - /// The implementation does not allow such weak keys. - WeakKey, -} - -//--- Formatting - -impl fmt::Display for FromBytesError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::InvalidKey => "malformed or insecure private key", - Self::WeakKey => "key too weak to be supported", - }) - } -} - -//--- Error - -impl std::error::Error for FromBytesError {} - -//----------- GenerateError -------------------------------------------------- - -/// An error in generating a key pair with Ring. -#[derive(Clone, Debug)] -pub enum GenerateError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversion - -impl From for GenerateError { - fn from(_: ring::error::Unspecified) -> Self { - Self::Implementation - } -} - -//--- Formatting - -impl fmt::Display for GenerateError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for GenerateError {} - -//============ Tests ========================================================= - -#[cfg(test)] -mod tests { - use std::{sync::Arc, vec::Vec}; - - use crate::base::iana::SecAlg; - use crate::sign::crypto::common::GenerateParams; - use crate::sign::{SecretKeyBytes, SignRaw}; - use crate::validate::Key; - - use super::KeyPair; - - const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 60616), - (SecAlg::ECDSAP256SHA256, 42253), - (SecAlg::ECDSAP384SHA384, 33566), - (SecAlg::ED25519, 56037), - ]; - - const GENERATE_PARAMS: &[GenerateParams] = &[ - GenerateParams::EcdsaP256Sha256, - GenerateParams::EcdsaP384Sha384, - GenerateParams::Ed25519, - ]; - - #[test] - fn public_key() { - let rng = Arc::new(ring::rand::SystemRandom::new()); - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_from_bind(&data).unwrap(); - let pub_key = pub_key.raw_public_key(); - - let key = - KeyPair::from_bytes(&gen_key, pub_key, rng.clone()).unwrap(); - - assert_eq!(key.raw_public_key(), *pub_key); - } - } - - #[test] - fn generated_roundtrip() { - let rng = Arc::new(ring::rand::SystemRandom::new()); - for params in GENERATE_PARAMS { - let (sk, pk) = super::generate(params.clone(), &*rng).unwrap(); - let key = KeyPair::from_bytes(&sk, &pk, rng.clone()).unwrap(); - assert_eq!(key.raw_public_key(), pk); - } - } - - #[test] - fn sign() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - let rng = Arc::new(ring::rand::SystemRandom::new()); - - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let pub_key = Key::>::parse_from_bind(&data).unwrap(); - let pub_key = pub_key.raw_public_key(); - - let key = KeyPair::from_bytes(&gen_key, pub_key, rng).unwrap(); - - let _ = key.sign_raw(b"Hello, World!").unwrap(); - } - } -} diff --git a/src/sign/denial/config.rs b/src/sign/denial/config.rs deleted file mode 100644 index 9a271246b..000000000 --- a/src/sign/denial/config.rs +++ /dev/null @@ -1,145 +0,0 @@ -use core::convert::From; - -use std::vec::Vec; - -use super::nsec::GenerateNsecConfig; -use super::nsec3::{ - GenerateNsec3Config, Nsec3HashProvider, OnDemandNsec3HashProvider, -}; -use crate::base::{Name, ToName}; -use crate::sign::records::DefaultSorter; -use octseq::{EmptyBuilder, FromBuilder}; - -//------------ NsecToNsec3TransitionState ------------------------------------ - -/// The current state of an RFC 5155 section 10.4 NSEC to NSEC3 transition. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum NsecToNsec3TransitionState { - /// 1. Transition all DNSKEYs to DNSKEYs using the algorithm aliases - /// described in Section 2. The actual method for safely and securely - /// changing the DNSKEY RRSet of the zone is outside the scope of this - /// specification. However, the end result MUST be that all DS RRs in - /// the parent use the specified algorithm aliases. - /// - /// After this transition is complete, all NSEC3-unaware clients will - /// treat the zone as insecure. At this point, the authoritative - /// server still returns negative and wildcard responses that contain - /// NSEC RRs. - TransitioningDnskeys, - - /// 2. Add signed NSEC3 RRs to the zone, either incrementally or all at - /// once. If adding incrementally, then the last RRSet added MUST be - /// the NSEC3PARAM RRSet. - /// - /// 3. Upon the addition of the NSEC3PARAM RRSet, the server switches to - /// serving negative and wildcard responses with NSEC3 RRs according - /// to this specification. - AddingNsec3Records, - - /// 4. Remove the NSEC RRs either incrementally or all at once. - RemovingNsecRecords, - - /// 5. Done. - Transitioned, -} - -//------------ Nsec3ToNsecTransitionState ------------------------------------ - -/// The current state of an RFC 5155 section 10.5 NSEC3 to NSEC transition. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum Nsec3ToNsecTransitionState { - /// 1. Add NSEC RRs incrementally or all at once. - AddingNsecRecords, - - /// 2. Remove the NSEC3PARAM RRSet. This will signal the server to use - /// the NSEC RRs for negative and wildcard responses. - RemovingNsec3ParamRecord, - - /// 3. Remove the NSEC3 RRs either incrementally or all at once. - RemovingNsec3Records, - - /// 4. Transition all of the DNSKEYs to DNSSEC algorithm identifiers. - /// After this transition is complete, all NSEC3-unaware clients will - /// treat the zone as secure. - TransitioningDnskeys, - - /// 5. Done. - Transitioned, -} - -//------------ DenialConfig -------------------------------------------------- - -/// Authenticated denial of existence configuration for a DNSSEC signed zone. -/// -/// A DNSSEC signed zone must have either `NSEC` or `NSEC3` records to enable -/// the server to authenticate responses for names or record types that are -/// not present in the zone. -/// -/// This type can be used to choose which denial mechanism should be used when -/// DNSSEC signing a zone. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum DenialConfig< - N, - O, - HP = OnDemandNsec3HashProvider, - Sort = DefaultSorter, -> where - HP: Nsec3HashProvider, - O: AsRef<[u8]> + From<&'static [u8]>, -{ - /// The zone already has the necessary NSEC(3) records. - AlreadyPresent, - - /// The zone already has NSEC records. - Nsec(GenerateNsecConfig), - - /// The zone already has NSEC3 records, possibly more than one set. - /// - /// https://datatracker.ietf.org/doc/html/rfc5155#section-7.3 - /// 7.3. Secondary Servers - /// ... - /// "If there are multiple NSEC3PARAM RRs present, there are multiple - /// valid NSEC3 chains present. The server must choose one of them, - /// but may use any criteria to do so." - /// - /// https://datatracker.ietf.org/doc/html/rfc5155#section-12.1.3 - /// 12.1.3. Transitioning to a New Hash Algorithm - /// "Although the NSEC3 and NSEC3PARAM RR formats include a hash - /// algorithm parameter, this document does not define a particular - /// mechanism for safely transitioning from one NSEC3 hash algorithm to - /// another. When specifying a new hash algorithm for use with NSEC3, - /// a transition mechanism MUST also be defined. It is possible that - /// the only practical and palatable transition mechanisms may require - /// an intermediate transition to an insecure state, or to a state that - /// uses NSEC records instead of NSEC3." - Nsec3( - GenerateNsec3Config, - Vec>, - ), - - /// The zone is transitioning from NSEC to NSEC3. - TransitioningNsecToNsec3( - GenerateNsecConfig, - GenerateNsec3Config, - NsecToNsec3TransitionState, - ), - - /// The zone is transitioning from NSEC3 to NSEC. - TransitioningNsec3ToNsec( - GenerateNsecConfig, - GenerateNsec3Config, - Nsec3ToNsecTransitionState, - ), -} - -impl Default - for DenialConfig, DefaultSorter> -where - N: ToName + From>, - O: AsRef<[u8]> + From<&'static [u8]> + FromBuilder, - ::Builder: EmptyBuilder + AsRef<[u8]> + AsMut<[u8]>, -{ - fn default() -> Self { - Self::Nsec(GenerateNsecConfig::default()) - } -} diff --git a/src/sign/error.rs b/src/sign/error.rs deleted file mode 100644 index d0a4889cc..000000000 --- a/src/sign/error.rs +++ /dev/null @@ -1,270 +0,0 @@ -//! Signing related errors. -use core::fmt::{self, Debug, Display}; - -#[cfg(feature = "openssl")] -use crate::sign::crypto::openssl; - -#[cfg(feature = "ring")] -use crate::sign::crypto::ring; - -use crate::rdata::dnssec::Timestamp; -use crate::validate::Nsec3HashError; - -//------------ SigningError -------------------------------------------------- - -#[derive(Copy, Clone, Debug)] -pub enum SigningError { - /// One or more keys does not have a signature validity period defined. - NoSignatureValidityPeriodProvided, - - /// TODO - OutOfMemory, - - /// At least one key must be provided to sign with. - NoKeysProvided, - - /// None of the provided keys were deemed suitable by the - /// [`SigningKeyUsageStrategy`] used. - NoSuitableKeysFound, - - // The zone either lacks a SOA record or has more than one SOA record. - SoaRecordCouldNotBeDetermined, - - // TODO - Nsec3HashingError(Nsec3HashError), - - /// TODO - /// - /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 - /// 2.2. Including RRSIG RRs in a Zone - /// ... - /// "An RRSIG RR itself MUST NOT be signed" - RrsigRrsMustNotBeSigned, - - // TODO - InvalidSignatureValidityPeriod(Timestamp, Timestamp), - - // TODO - SigningError(SignError), -} - -impl Display for SigningError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - SigningError::NoSignatureValidityPeriodProvided => { - f.write_str("No signature validity period found for key") - } - SigningError::OutOfMemory => f.write_str("Out of memory"), - SigningError::NoKeysProvided => { - f.write_str("No signing keys provided") - } - SigningError::NoSuitableKeysFound => { - f.write_str("No suitable keys found") - } - SigningError::SoaRecordCouldNotBeDetermined => { - f.write_str("No apex SOA or too many apex SOA records found") - } - SigningError::Nsec3HashingError(err) => { - f.write_fmt(format_args!("NSEC3 hashing error: {err}")) - } - SigningError::RrsigRrsMustNotBeSigned => f.write_str( - "RFC 4035 violation: RRSIG RRs MUST NOT be signed", - ), - SigningError::InvalidSignatureValidityPeriod(inception, expiration) => f.write_fmt( - format_args!("RFC 4034 violation: RRSIG validity period ({inception} <= {expiration}) is invalid"), - ), - SigningError::SigningError(err) => { - f.write_fmt(format_args!("Signing error: {err}")) - } - } - } -} - -impl From for SigningError { - fn from(err: SignError) -> Self { - Self::SigningError(err) - } -} - -impl From for SigningError { - fn from(err: Nsec3HashError) -> Self { - Self::Nsec3HashingError(err) - } -} - -//----------- SignError ------------------------------------------------------ - -/// A signature failure. -/// -/// In case such an error occurs, callers should stop using the key pair they -/// attempted to sign with. If such an error occurs with every key pair they -/// have available, or if such an error occurs with a freshly-generated key -/// pair, they should use a different cryptographic implementation. If that -/// is not possible, they must forego signing entirely. -/// -/// # Failure Cases -/// -/// Signing should be an infallible process. There are three considerable -/// failure cases for it: -/// -/// - The secret key was invalid (e.g. its parameters were inconsistent). -/// -/// Such a failure would mean that all future signing (with this key) will -/// also fail. In any case, the implementations provided by this crate try -/// to verify the key (e.g. by checking the consistency of the private and -/// public components) before any signing occurs, largely ruling this class -/// of errors out. -/// -/// - Not enough randomness could be obtained. This applies to signature -/// algorithms which use randomization (e.g. RSA and ECDSA). -/// -/// On the vast majority of platforms, randomness can always be obtained. -/// The [`getrandom` crate documentation][getrandom] notes: -/// -/// > If an error does occur, then it is likely that it will occur on every -/// > call to getrandom, hence after the first successful call one can be -/// > reasonably confident that no errors will occur. -/// -/// [getrandom]: https://docs.rs/getrandom -/// -/// Thus, in case such a failure occurs, all future signing will probably -/// also fail. -/// -/// - Not enough memory could be allocated. -/// -/// Signature algorithms have a small memory overhead, so an out-of-memory -/// condition means that the program is nearly out of allocatable space. -/// -/// Callers who do not expect allocations to fail (i.e. who are using the -/// standard memory allocation routines, not their `try_` variants) will -/// likely panic shortly after such an error. -/// -/// Callers who are aware of their memory usage will likely restrict it far -/// before they get to this point. Systems running at near-maximum load -/// tend to quickly become unresponsive and staggeringly slow. If memory -/// usage is an important consideration, programs will likely cap it before -/// the system reaches e.g. 90% memory use. -/// -/// As such, memory allocation failure should never really occur. It is far -/// more likely that one of the other errors has occurred. -/// -/// It may be reasonable to panic in any such situation, since each kind of -/// error is essentially unrecoverable. However, applications where signing -/// is an optional step, or where crashing is prohibited, may wish to recover -/// from such an error differently (e.g. by foregoing signatures or informing -/// an operator). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct SignError; - -impl fmt::Display for SignError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("could not create a cryptographic signature") - } -} - -impl std::error::Error for SignError {} - -//----------- FromBytesError ----------------------------------------------- - -/// An error in importing a key pair from bytes. -#[derive(Clone, Debug)] -pub enum FromBytesError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// The key's parameters were invalid. - InvalidKey, - - /// The implementation does not allow such weak keys. - WeakKey, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversions - -#[cfg(feature = "ring")] -impl From for FromBytesError { - fn from(value: ring::FromBytesError) -> Self { - match value { - ring::FromBytesError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - ring::FromBytesError::InvalidKey => Self::InvalidKey, - ring::FromBytesError::WeakKey => Self::WeakKey, - } - } -} - -#[cfg(feature = "openssl")] -impl From for FromBytesError { - fn from(value: openssl::FromBytesError) -> Self { - match value { - openssl::FromBytesError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - openssl::FromBytesError::InvalidKey => Self::InvalidKey, - openssl::FromBytesError::Implementation => Self::Implementation, - } - } -} - -//--- Formatting - -impl fmt::Display for FromBytesError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::InvalidKey => "malformed or insecure private key", - Self::WeakKey => "key too weak to be supported", - Self::Implementation => "an internal error occurred", - }) - } -} - -//--- Error - -impl std::error::Error for FromBytesError {} - -//----------- GenerateError -------------------------------------------------- - -/// An error in generating a key pair. -#[derive(Clone, Debug)] -pub enum GenerateError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, - - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, -} - -//--- Conversion - -#[cfg(feature = "ring")] -impl From for GenerateError { - fn from(value: ring::GenerateError) -> Self { - match value { - ring::GenerateError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - ring::GenerateError::Implementation => Self::Implementation, - } - } -} - -#[cfg(feature = "openssl")] -impl From for GenerateError { - fn from(value: openssl::GenerateError) -> Self { - match value { - openssl::GenerateError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm - } - openssl::GenerateError::Implementation => Self::Implementation, - } - } -} diff --git a/src/sign/keys/bytes.rs b/src/sign/keys/bytes.rs deleted file mode 100644 index a4fb79c1a..000000000 --- a/src/sign/keys/bytes.rs +++ /dev/null @@ -1,509 +0,0 @@ -//! A generic representation of secret keys. - -use core::{fmt, str}; - -use secrecy::{ExposeSecret, SecretBox}; -use std::boxed::Box; -use std::vec::Vec; - -use crate::base::iana::SecAlg; -use crate::utils::base64; -use crate::validate::RsaPublicKeyBytes; - -//----------- SecretKeyBytes ------------------------------------------------- - -/// A secret key expressed as raw bytes. -/// -/// This is a low-level generic representation of a secret key from any one of -/// the commonly supported signature algorithms. It is useful for abstracting -/// over most cryptographic implementations, and it provides functionality for -/// importing and exporting keys from and to the disk. -/// -/// # Serialization -/// -/// This type can be used to interact with private keys stored in the format -/// popularized by BIND. The format is rather under-specified, but examples -/// of it are available in [RFC 5702], [RFC 6605], and [RFC 8080]. -/// -/// [RFC 5702]: https://www.rfc-editor.org/rfc/rfc5702 -/// [RFC 6605]: https://www.rfc-editor.org/rfc/rfc6605 -/// [RFC 8080]: https://www.rfc-editor.org/rfc/rfc8080 -/// -/// In this format, a private key is a line-oriented text file. Each line is -/// either blank (having only whitespace) or a key-value entry. Entries have -/// three components: a key, an ASCII colon, and a value. Keys contain ASCII -/// text (except for colons) and values contain any data up to the end of the -/// line. Whitespace at either end of the key and the value will be ignored. -/// -/// Every file begins with two entries: -/// -/// - `Private-key-format` specifies the format of the file. The RFC examples -/// above use version 1.2 (serialised `v1.2`), but recent versions of BIND -/// have defined a new version 1.3 (serialized `v1.3`). -/// -/// This value should be treated akin to Semantic Versioning principles. If -/// the major version (the first number) is unknown to a parser, it should -/// fail, since it does not know the layout of the following fields. If the -/// minor version is greater than what a parser is expecting, it should -/// ignore any following fields it did not expect. -/// -/// - `Algorithm` specifies the signing algorithm used by the private key. -/// This can affect the format of later fields. The value consists of two -/// whitespace-separated words: the first is the ASCII decimal number of the -/// algorithm (see [`SecAlg`]); the second is the name of the algorithm in -/// ASCII parentheses (with no whitespace inside). Valid combinations are: -/// -/// - `8 (RSASHA256)`: RSA with the SHA-256 digest. -/// - `10 (RSASHA512)`: RSA with the SHA-512 digest. -/// - `13 (ECDSAP256SHA256)`: ECDSA with the P-256 curve and SHA-256 digest. -/// - `14 (ECDSAP384SHA384)`: ECDSA with the P-384 curve and SHA-384 digest. -/// - `15 (ED25519)`: Ed25519. -/// - `16 (ED448)`: Ed448. -/// -/// The value of every following entry is a Base64-encoded string of variable -/// length, using the RFC 4648 variant (i.e. with `+` and `/`, and `=` for -/// padding). It is unclear whether padding is required or optional. -/// -/// In the case of RSA, the following fields are defined (their conventional -/// symbolic names are also provided): -/// -/// - `Modulus` (n) -/// - `PublicExponent` (e) -/// - `PrivateExponent` (d) -/// - `Prime1` (p) -/// - `Prime2` (q) -/// - `Exponent1` (d_p) -/// - `Exponent2` (d_q) -/// - `Coefficient` (q_inv) -/// -/// For all other algorithms, there is a single `PrivateKey` field, whose -/// contents should be interpreted as: -/// -/// - For ECDSA, the private scalar of the key, as a fixed-width byte string -/// interpreted as a big-endian integer. -/// -/// - For EdDSA, the private scalar of the key, as a fixed-width byte string. -pub enum SecretKeyBytes { - /// An RSA/SHA-256 keypair. - RsaSha256(RsaSecretKeyBytes), - - /// An ECDSA P-256/SHA-256 keypair. - /// - /// The private key is a single 32-byte big-endian integer. - EcdsaP256Sha256(SecretBox<[u8; 32]>), - - /// An ECDSA P-384/SHA-384 keypair. - /// - /// The private key is a single 48-byte big-endian integer. - EcdsaP384Sha384(SecretBox<[u8; 48]>), - - /// An Ed25519 keypair. - /// - /// The private key is a single 32-byte string. - Ed25519(SecretBox<[u8; 32]>), - - /// An Ed448 keypair. - /// - /// The private key is a single 57-byte string. - Ed448(SecretBox<[u8; 57]>), -} - -//--- Inspection - -impl SecretKeyBytes { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } -} - -//--- Converting to and from the BIND format - -impl SecretKeyBytes { - /// Serialize this secret key in the conventional format used by BIND. - /// - /// The key is formatted in the private key v1.2 format and written to the - /// given formatter. See the type-level documentation for a description - /// of this format. - pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { - writeln!(w, "Private-key-format: v1.2")?; - match self { - Self::RsaSha256(k) => { - writeln!(w, "Algorithm: 8 (RSASHA256)")?; - k.format_as_bind(w) - } - - Self::EcdsaP256Sha256(s) => { - let s = s.expose_secret(); - writeln!(w, "Algorithm: 13 (ECDSAP256SHA256)")?; - 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)) - } - - Self::Ed25519(s) => { - let s = s.expose_secret(); - writeln!(w, "Algorithm: 15 (ED25519)")?; - 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)) - } - } - } - - /// Display this secret key in the conventional format used by BIND. - /// - /// This is a simple wrapper around [`Self::format_as_bind()`]. - pub fn display_as_bind(&self) -> impl fmt::Display + '_ { - struct Display<'a>(&'a SecretKeyBytes); - impl fmt::Display for Display<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.format_as_bind(f) - } - } - Display(self) - } - - /// Parse a secret key from the conventional format used by BIND. - /// - /// This parser supports the private key v1.2 format, but it should be - /// compatible with any future v1.x key. See the type-level documentation - /// for a description of this format. - pub fn parse_from_bind(data: &str) -> Result { - /// Parse private keys for most algorithms (except RSA). - fn parse_pkey( - mut data: &str, - ) -> Result, BindFormatError> { - // Look for the 'PrivateKey' field. - while let Some((key, val, rest)) = parse_bind_entry(data)? { - data = rest; - - if key != "PrivateKey" { - continue; - } - - // 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)?; - - return Ok(val.into()); - } - - // The 'PrivateKey' field was not found. - Err(BindFormatError::Misformatted) - } - - // The first line should specify the key format. - let (_, _, data) = parse_bind_entry(data)? - .filter(|&(k, v, _)| { - k == "Private-key-format" - && v.strip_prefix("v1.") - .and_then(|minor| minor.parse::().ok()) - .is_some_and(|minor| minor >= 2) - }) - .ok_or(BindFormatError::UnsupportedFormat)?; - - // The second line should specify the algorithm. - let (_, val, data) = parse_bind_entry(data)? - .filter(|&(k, _, _)| k == "Algorithm") - .ok_or(BindFormatError::Misformatted)?; - - // Parse the algorithm. - let mut words = val.split_whitespace(); - let code = words - .next() - .and_then(|code| code.parse::().ok()) - .ok_or(BindFormatError::Misformatted)?; - let name = words.next().ok_or(BindFormatError::Misformatted)?; - if words.next().is_some() { - return Err(BindFormatError::Misformatted); - } - - match (code, name) { - (8, "(RSASHA256)") => { - RsaSecretKeyBytes::parse_from_bind(data).map(Self::RsaSha256) - } - (13, "(ECDSAP256SHA256)") => { - parse_pkey(data).map(Self::EcdsaP256Sha256) - } - (14, "(ECDSAP384SHA384)") => { - parse_pkey(data).map(Self::EcdsaP384Sha384) - } - (15, "(ED25519)") => parse_pkey(data).map(Self::Ed25519), - (16, "(ED448)") => parse_pkey(data).map(Self::Ed448), - _ => Err(BindFormatError::UnsupportedAlgorithm), - } - } -} - -//----------- RsaSecretKeyBytes --------------------------------------------------- - -/// An RSA secret key expressed as raw bytes. -/// -/// All fields here are arbitrary-precision integers in big-endian format. -/// The public values, `n` and `e`, must not have leading zeros; the remaining -/// values may be padded with leading zeros. -pub struct RsaSecretKeyBytes { - /// The public modulus. - pub n: Box<[u8]>, - - /// The public exponent. - pub e: Box<[u8]>, - - /// The private exponent. - pub d: SecretBox<[u8]>, - - /// The first prime factor of `d`. - pub p: SecretBox<[u8]>, - - /// The second prime factor of `d`. - pub q: SecretBox<[u8]>, - - /// The exponent corresponding to the first prime factor of `d`. - pub d_p: SecretBox<[u8]>, - - /// The exponent corresponding to the second prime factor of `d`. - pub d_q: SecretBox<[u8]>, - - /// The inverse of the second prime factor modulo the first. - pub q_i: SecretBox<[u8]>, -} - -//--- Conversion to and from the BIND format - -impl RsaSecretKeyBytes { - /// Serialize this secret key in the conventional format used by BIND. - /// - /// The key is formatted in the private key v1.2 format and written to the - /// given formatter. Note that the header and algorithm lines are not - /// written. See the type-level documentation of [`SecretKeyBytes`] for a - /// description of this format. - pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { - w.write_str("Modulus: ")?; - writeln!(w, "{}", base64::encode_display(&self.n))?; - w.write_str("PublicExponent: ")?; - writeln!(w, "{}", base64::encode_display(&self.e))?; - w.write_str("PrivateExponent: ")?; - writeln!(w, "{}", base64::encode_display(&self.d.expose_secret()))?; - w.write_str("Prime1: ")?; - writeln!(w, "{}", base64::encode_display(&self.p.expose_secret()))?; - w.write_str("Prime2: ")?; - writeln!(w, "{}", base64::encode_display(&self.q.expose_secret()))?; - w.write_str("Exponent1: ")?; - writeln!(w, "{}", base64::encode_display(&self.d_p.expose_secret()))?; - w.write_str("Exponent2: ")?; - writeln!(w, "{}", base64::encode_display(&self.d_q.expose_secret()))?; - w.write_str("Coefficient: ")?; - writeln!(w, "{}", base64::encode_display(&self.q_i.expose_secret()))?; - Ok(()) - } - - /// Display this secret key in the conventional format used by BIND. - /// - /// This is a simple wrapper around [`Self::format_as_bind()`]. - pub fn display_as_bind(&self) -> impl fmt::Display + '_ { - struct Display<'a>(&'a RsaSecretKeyBytes); - impl fmt::Display for Display<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.format_as_bind(f) - } - } - Display(self) - } - - /// Parse a secret key from the conventional format used by BIND. - /// - /// This parser supports the private key v1.2 format, but it should be - /// compatible with any future v1.x key. Note that the header and - /// algorithm lines are ignored. See the type-level documentation of - /// [`SecretKeyBytes`] for a description of this format. - pub fn parse_from_bind(mut data: &str) -> Result { - let mut n = None; - let mut e = None; - let mut d = None; - let mut p = None; - let mut q = None; - let mut d_p = None; - let mut d_q = None; - let mut q_i = None; - - while let Some((key, val, rest)) = parse_bind_entry(data)? { - let field = match key { - "Modulus" => &mut n, - "PublicExponent" => &mut e, - "PrivateExponent" => &mut d, - "Prime1" => &mut p, - "Prime2" => &mut q, - "Exponent1" => &mut d_p, - "Exponent2" => &mut d_q, - "Coefficient" => &mut q_i, - _ => { - data = rest; - continue; - } - }; - - if field.is_some() { - // This field has already been filled. - return Err(BindFormatError::Misformatted); - } - - let buffer: Vec = base64::decode(val) - .map_err(|_| BindFormatError::Misformatted)?; - - *field = Some(buffer.into_boxed_slice()); - data = rest; - } - - for field in [&n, &e, &d, &p, &q, &d_p, &d_q, &q_i] { - if field.is_none() { - // A field was missing. - return Err(BindFormatError::Misformatted); - } - } - - Ok(Self { - n: n.unwrap(), - e: e.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(), - }) - } -} - -//--- Into - -impl<'a> From<&'a RsaSecretKeyBytes> for RsaPublicKeyBytes { - fn from(value: &'a RsaSecretKeyBytes) -> Self { - RsaPublicKeyBytes { - n: value.n.clone(), - e: value.e.clone(), - } - } -} - -//----------- Helpers for parsing the BIND format ---------------------------- - -/// 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. - - // Trim any pending newlines. - let data = data.trim_start(); - - // Stop if there's no more data. - if data.is_empty() { - return Ok(None); - } - - // Get the first line (NOTE: CR LF is handled later). - let (line, rest) = data.split_once('\n').unwrap_or((data, "")); - - // Split the line by a colon. - let (key, val) = - line.split_once(':').ok_or(BindFormatError::Misformatted)?; - - // Trim the key and value (incl. for CR LFs). - Ok(Some((key.trim(), val.trim(), rest))) -} - -//============ Error types =================================================== - -//----------- BindFormatError ------------------------------------------------ - -/// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub enum BindFormatError { - /// The key file uses an unsupported version of the format. - UnsupportedFormat, - - /// The key file did not follow the DNS format correctly. - Misformatted, - - /// The key file used an unsupported algorithm. - UnsupportedAlgorithm, -} - -//--- Display - -impl fmt::Display for BindFormatError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedFormat => "unsupported format", - Self::Misformatted => "misformatted key file", - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -//--- Error - -impl std::error::Error for BindFormatError {} - -//============ Tests ========================================================= - -#[cfg(test)] -mod tests { - use std::{string::ToString, vec::Vec}; - - use crate::base::iana::SecAlg; - - const KEYS: &[(SecAlg, u16)] = &[ - (SecAlg::RSASHA256, 60616), - (SecAlg::ECDSAP256SHA256, 42253), - (SecAlg::ECDSAP384SHA384, 33566), - (SecAlg::ED25519, 56037), - (SecAlg::ED448, 7379), - ]; - - #[test] - fn secret_from_dns() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); - assert_eq!(key.algorithm(), algorithm); - } - } - - #[test] - fn secret_roundtrip() { - for &(algorithm, key_tag) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - let path = format!("test-data/dnssec-keys/K{}.private", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = super::SecretKeyBytes::parse_from_bind(&data).unwrap(); - let same = key.display_as_bind().to_string(); - let data = data.lines().collect::>(); - let same = same.lines().collect::>(); - assert_eq!(data, same); - } - } -} diff --git a/src/sign/keys/keymeta.rs b/src/sign/keys/keymeta.rs deleted file mode 100644 index 88707b362..000000000 --- a/src/sign/keys/keymeta.rs +++ /dev/null @@ -1,247 +0,0 @@ -use core::convert::From; -use core::marker::PhantomData; -use core::ops::Deref; - -use std::fmt::Display; - -use crate::sign::keys::signingkey::SigningKey; -use crate::sign::SignRaw; - -//------------ DesignatedSigningKey ------------------------------------------ - -pub trait DesignatedSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - /// Should this key be used to "sign one or more other authentication keys - /// for a given zone" (RFC 4033 section 2 "Key Signing Key (KSK)"). - fn signs_keys(&self) -> bool; - - /// Should this key be used to "sign a zone" (RFC 4033 section 2 "Zone - /// Signing Key (ZSK)"). - fn signs_zone_data(&self) -> bool; - - fn signing_key(&self) -> &SigningKey; -} - -impl DesignatedSigningKey for &T -where - Octs: AsRef<[u8]>, - Inner: SignRaw, - T: DesignatedSigningKey, -{ - fn signs_keys(&self) -> bool { - (**self).signs_keys() - } - - fn signs_zone_data(&self) -> bool { - (**self).signs_zone_data() - } - - fn signing_key(&self) -> &SigningKey { - (**self).signing_key() - } -} - -//------------ IntendedKeyPurpose -------------------------------------------- - -/// The purpose of a DNSSEC key from the perspective of an operator. -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum IntendedKeyPurpose { - /// A key that signs DNSKEY RRSETs. - /// - /// RFC9499 DNS Terminology: - /// 10. General DNSSEC - /// Key signing key (KSK): DNSSEC keys that "only sign the apex DNSKEY - /// RRset in a zone." (Quoted from RFC6781, Section 3.1) - KSK, - - /// A key that signs non-DNSKEY RRSETs. - /// - /// RFC9499 DNS Terminology: - /// 10. General DNSSEC - /// Zone signing key (ZSK): "DNSSEC keys that can be used to sign all the - /// RRsets in a zone that require signatures, other than the apex DNSKEY - /// RRset." (Quoted from RFC6781, Section 3.1) Also note that a ZSK is - /// sometimes used to sign the apex DNSKEY RRset. - ZSK, - - /// A key that signs both DNSKEY and other RRSETs. - /// - /// RFC 9499 DNS Terminology: - /// 10. General DNSSEC - /// Combined signing key (CSK): In cases where the differentiation between - /// the KSK and ZSK is not made, i.e., where keys have the role of both - /// KSK and ZSK, we talk about a Single-Type Signing Scheme." (Quoted from - /// [RFC6781], Section 3.1) This is sometimes called a "combined signing - /// key" or "CSK". It is operational practice, not protocol, that - /// determines whether a particular key is a ZSK, a KSK, or a CSK. - CSK, - - /// A key that is not currently used for signing. - /// - /// This key should be added to the zone but not used to sign any RRSETs. - Inactive, -} - -//--- impl Display - -impl Display for IntendedKeyPurpose { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - IntendedKeyPurpose::KSK => f.write_str("KSK"), - IntendedKeyPurpose::ZSK => f.write_str("ZSK"), - IntendedKeyPurpose::CSK => f.write_str("CSK"), - IntendedKeyPurpose::Inactive => f.write_str("Inactive"), - } - } -} - -//------------ DnssecSigningKey ---------------------------------------------- - -/// A key that can be used for DNSSEC signing. -/// -/// This type carries metadata that signals to a DNSSEC signer how this key -/// should impact the zone to be signed. -pub struct DnssecSigningKey { - /// The key to use to make DNSSEC signatures. - key: SigningKey, - - /// The purpose for which the operator intends the key to be used. - /// - /// Defines explicitly the purpose of the key which should be used instead - /// of attempting to infer the purpose of the key (to sign keys and/or to - /// sign other records) by examining the setting of the Secure Entry Point - /// and Zone Key flags on the key (i.e. whether the key is a KSK or ZSK or - /// something else). - purpose: IntendedKeyPurpose, - - _phantom: PhantomData<(Octs, Inner)>, -} - -impl DnssecSigningKey { - /// Create a new [`DnssecSigningKey`] by assocating intent with a - /// reference to an existing key. - pub fn new( - key: SigningKey, - purpose: IntendedKeyPurpose, - ) -> Self { - Self { - key, - purpose, - _phantom: Default::default(), - } - } - - pub fn new_ksk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::KSK, - _phantom: Default::default(), - } - } - - pub fn new_zsk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::ZSK, - _phantom: Default::default(), - } - } - - pub fn new_csk(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::CSK, - _phantom: Default::default(), - } - } - - pub fn new_inactive_key(key: SigningKey) -> Self { - Self { - key, - purpose: IntendedKeyPurpose::Inactive, - _phantom: Default::default(), - } - } -} - -impl DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - pub fn purpose(&self) -> IntendedKeyPurpose { - self.purpose - } - - pub fn set_purpose(&mut self, purpose: IntendedKeyPurpose) { - self.purpose = purpose; - } - pub fn into_inner(self) -> SigningKey { - self.key - } -} - -//--- impl Deref - -impl Deref for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - type Target = SigningKey; - - fn deref(&self) -> &Self::Target { - &self.key - } -} - -//--- impl From - -impl From> - for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - fn from(key: SigningKey) -> Self { - let public_key = key.public_key(); - match ( - public_key.is_secure_entry_point(), - public_key.is_zone_signing_key(), - ) { - (true, _) => Self::new_ksk(key), - (false, true) => Self::new_zsk(key), - (false, false) => Self::new_inactive_key(key), - } - } -} - -//--- impl DesignatedSigningKey - -impl DesignatedSigningKey - for DnssecSigningKey -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - fn signs_keys(&self) -> bool { - matches!( - self.purpose, - IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK - ) - } - - fn signs_zone_data(&self) -> bool { - matches!( - self.purpose, - IntendedKeyPurpose::ZSK | IntendedKeyPurpose::CSK - ) - } - - fn signing_key(&self) -> &SigningKey { - &self.key - } -} diff --git a/src/sign/signatures/rrsigs.rs b/src/sign/signatures/rrsigs.rs deleted file mode 100644 index f6f222f73..000000000 --- a/src/sign/signatures/rrsigs.rs +++ /dev/null @@ -1,1864 +0,0 @@ -//! DNSSEC RRSIG generation. -use core::convert::{AsRef, From}; -use core::fmt::Display; -use core::marker::{PhantomData, Send}; - -use std::boxed::Box; -use std::string::ToString; -use std::vec::Vec; - -use log::Level; -use octseq::builder::FromBuilder; -use octseq::{OctetsFrom, OctetsInto}; -use smallvec::SmallVec; -use tracing::{debug, trace}; - -use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; -use crate::base::name::ToName; -use crate::base::rdata::{ComposeRecordData, RecordData}; -use crate::base::record::Record; -use crate::base::Name; -use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; -use crate::rdata::{Dnskey, Rrsig, ZoneRecordData}; -use crate::sign::denial::nsec3::Nsec3HashProvider; -use crate::sign::error::SigningError; -use crate::sign::keys::keymeta::DesignatedSigningKey; -use crate::sign::keys::signingkey::SigningKey; -use crate::sign::records::{ - DefaultSorter, RecordsIter, Rrset, SortedRecords, Sorter, -}; -use crate::sign::signatures::strategy::SigningKeyUsageStrategy; -use crate::sign::signatures::strategy::{ - DefaultSigningKeyUsageStrategy, RrsigValidityPeriodStrategy, -}; -use crate::sign::traits::SignRaw; -use crate::sign::SigningConfig; - -//------------ GenerateRrsigConfig ------------------------------------------- - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct GenerateRrsigConfig { - pub add_used_dnskeys: bool, - - pub zone_apex: Option, - - pub rrsig_validity_period_strategy: ValidityStrat, - - _phantom: PhantomData<(KeyStrat, Sort)>, -} - -impl - GenerateRrsigConfig -{ - /// Like [`Self::default()`] but gives control over the SigningKeyStrategy - /// and Sorter used. - pub fn new(rrsig_validity_period_strategy: ValidityStrat) -> Self { - Self { - add_used_dnskeys: true, - zone_apex: None, - rrsig_validity_period_strategy, - _phantom: Default::default(), - } - } - - pub fn without_adding_used_dns_keys(mut self) -> Self { - self.add_used_dnskeys = false; - self - } - - pub fn with_zone_apex(mut self, zone_apex: N) -> Self { - self.zone_apex = Some(zone_apex); - self - } -} - -impl - GenerateRrsigConfig< - N, - DefaultSigningKeyUsageStrategy, - ValidityStrat, - DefaultSorter, - > -where - ValidityStrat: RrsigValidityPeriodStrategy, -{ - pub fn default(rrsig_validity_period_strategy: ValidityStrat) -> Self { - Self::new(rrsig_validity_period_strategy) - } -} - -impl - From<&SigningConfig> - for GenerateRrsigConfig -where - HP: Nsec3HashProvider, - Octs: AsRef<[u8]> + From<&'static [u8]>, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy + Clone, - Sort: Sorter, -{ - fn from( - signing_cfg: &SigningConfig< - N, - Octs, - Inner, - KeyStrat, - ValidityStrat, - Sort, - HP, - >, - ) -> Self { - let mut rrsig_cfg = - GenerateRrsigConfig::::new( - signing_cfg.rrsig_validity_period_strategy.clone(), - ); - rrsig_cfg.add_used_dnskeys = signing_cfg.add_used_dnskeys; - rrsig_cfg - } -} - -//------------ RrsigRecords -------------------------------------------------- - -#[derive(Clone, Debug)] -pub struct RrsigRecords -where - Octs: AsRef<[u8]>, -{ - /// The RRSIG records. - pub rrsigs: Vec>>, - - /// The DNSKEY records. - pub dnskeys: Vec>>, -} - -impl RrsigRecords -where - Octs: AsRef<[u8]>, -{ - pub fn new( - rrsigs: Vec>>, - dnskeys: Vec>>, - ) -> Self { - Self { rrsigs, dnskeys } - } -} - -impl Default for RrsigRecords -where - Octs: AsRef<[u8]>, -{ - fn default() -> Self { - Self { - rrsigs: Default::default(), - dnskeys: Default::default(), - } - } -} - -//------------ generate_rrsigs() --------------------------------------------- - -/// Generate RRSIG RRs for a collection of zone records. -/// -/// Returns the collection of RRSIG and (optionally) DNSKEY RRs that must be -/// added to the input records as part of DNSSEC zone signing. -/// -/// The input records MUST be sorted according to [`CanonicalOrd`]. -/// -/// Any RRSIG records in the input will be ignored. New, and replacement (if -/// already present), RRSIGs will be generated and included in the output. -/// -/// If [`GenerateRrsigConfig::add_used_dnskeys`] is true, for the subset of -/// the input keys that are used to sign records, if they lack a corresponding -/// DNSKEY RR in the input records the missing DNSKEY RR will be generated and -/// included in the output. -/// -/// Note that the order of the output records should not be relied upon and is -/// subject to change. -// TODO: Add mutable iterator based variant. -#[allow(clippy::type_complexity)] -pub fn generate_rrsigs( - records: RecordsIter<'_, N, ZoneRecordData>, - keys: &[DSK], - config: &GenerateRrsigConfig, -) -> Result, SigningError> -where - DSK: DesignatedSigningKey, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - N: ToName - + PartialEq - + Clone - + Display - + Send - + CanonicalOrd - + From>, - Octs: AsRef<[u8]> - + From> - + Send - + OctetsFrom> - + Clone - + FromBuilder - + From<&'static [u8]>, - Sort: Sorter, -{ - debug!( - "Signer settings: add_used_dnskeys={}, strategy: {}", - config.add_used_dnskeys, - KeyStrat::NAME - ); - - // Peek at the records because we need to process the first owner records - // differently if they represent the apex of a zone (i.e. contain the SOA - // record), otherwise we process the first owner records in the same loop - // as the rest of the records beneath the apex. - let mut records = records.peekable(); - - let first_rrs = records.peek(); - - let Some(first_rrs) = first_rrs else { - // No records were provided. As we are able to generate RRSIGs for - // partial zones this is a special case of a partial zone, an empty - // input, for which there is nothing to do. - return Ok(RrsigRecords::default()); - }; - - let first_owner = first_rrs.owner().clone(); - - // If no apex was supplied, assume that because the input should be - // canonically ordered that the first record is part of the apex RRSET. - // Otherwise, check if the first record matches the given apex, if not - // that means that the input starts beneath the apex. - let (zone_apex, at_apex) = match &config.zone_apex { - Some(zone_apex) => (zone_apex, first_rrs.owner() == zone_apex), - None => (&first_owner, true), - }; - - // https://www.rfc-editor.org/rfc/rfc1034#section-6.1 - // 6.1. C.ISI.EDU name server - // ... - // "Since the class of all RRs in a zone must be the same..." - // - // We can therefore assume that the class to use for new DNSKEY records - // when we add them will be the same as the class of the first resource - // record in the zone. - let zone_class = first_rrs.class(); - - // Determine which keys to use for what. Work with indices because - // SigningKey doesn't impl PartialEq so we cannot use a HashSet to make a - // unique set of them. - - if keys.is_empty() { - return Err(SigningError::NoKeysProvided); - } - - let mut dnskey_signing_key_idxs = - KeyStrat::select_signing_keys_for_rtype(keys, Some(Rtype::DNSKEY)); - if dnskey_signing_key_idxs.is_empty() { - return Err(SigningError::NoSuitableKeysFound); - } - dnskey_signing_key_idxs.sort(); - dnskey_signing_key_idxs.dedup(); - - let mut non_dnskey_signing_key_idxs = - KeyStrat::select_signing_keys_for_rtype(keys, None); - if non_dnskey_signing_key_idxs.is_empty() { - return Err(SigningError::NoSuitableKeysFound); - } - non_dnskey_signing_key_idxs.sort(); - non_dnskey_signing_key_idxs.dedup(); - - let mut keys_in_use_idxs: SmallVec<[usize; 4]> = - non_dnskey_signing_key_idxs - .iter() - .chain(dnskey_signing_key_idxs.iter()) - .copied() - .collect(); - keys_in_use_idxs.sort(); - keys_in_use_idxs.dedup(); - - if log::log_enabled!(Level::Debug) { - log_keys_in_use( - keys, - &dnskey_signing_key_idxs, - &non_dnskey_signing_key_idxs, - &keys_in_use_idxs, - ); - } - - let mut out = RrsigRecords::default(); - let mut reusable_scratch = Vec::new(); - let mut cut: Option = None; - - if at_apex { - // Sign the apex, if it contains a SOA record, otherwise it's just the - // first in a collection of sorted records but not the apex of a zone. - generate_apex_rrsigs( - keys, - config, - &mut records, - zone_apex, - zone_class, - &dnskey_signing_key_idxs, - &non_dnskey_signing_key_idxs, - &keys_in_use_idxs, - &mut out, - &mut reusable_scratch, - )?; - } - - // For all records - for owner_rrs in records { - // If the owner is out of zone, we have moved out of our zone and are - // done. - if !owner_rrs.is_in_zone(zone_apex) { - break; - } - - // If the owner is below a zone cut, we must ignore it. - if let Some(ref cut) = cut { - if owner_rrs.owner().ends_with(cut) { - continue; - } - } - - // A copy of the owner name. We’ll need it later. - let name = owner_rrs.owner().clone(); - - // If this owner is the parent side of a zone cut, we keep the owner - // name for later. This also means below that if `cut.is_some()` we - // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(zone_apex) { - Some(name.clone()) - } else { - None - }; - - for rrset in owner_rrs.rrsets() { - if cut.is_some() { - // If we are at a zone cut, we only sign DS and NSEC records. - // NS records we must not sign and everything else shouldn’t - // be here, really. - if rrset.rtype() != Rtype::DS && rrset.rtype() != Rtype::NSEC - { - continue; - } - } else { - // Otherwise we only ignore RRSIGs. - if rrset.rtype() == Rtype::RRSIG { - continue; - } - } - - for key in - non_dnskey_signing_key_idxs.iter().map(|&idx| &keys[idx]) - { - let (inception, expiration) = config - .rrsig_validity_period_strategy - .validity_period_for_rrset(&rrset); - let rrsig_rr = sign_rrset_in( - key.signing_key(), - &rrset, - zone_apex, - inception, - expiration, - &mut reusable_scratch, - )?; - out.rrsigs.push(rrsig_rr); - debug!( - "Signed {} RRSET at {} with keytag {}", - rrset.rtype(), - rrset.owner(), - key.signing_key().public_key().key_tag() - ); - } - } - } - - debug!( - "Returning {} RRSIG RRs and {} DNSKEY RRs from signature generation", - out.rrsigs.len(), - out.dnskeys.len(), - ); - - Ok(out) -} - -fn log_keys_in_use( - keys: &[DSK], - dnskey_signing_key_idxs: &[usize], - non_dnskey_signing_key_idxs: &[usize], - keys_in_use_idxs: &[usize], -) where - DSK: DesignatedSigningKey, - Inner: SignRaw, - Octs: AsRef<[u8]>, -{ - fn debug_key, Inner: SignRaw>( - prefix: &str, - key: &SigningKey, - ) { - debug!( - "{prefix} with algorithm {}, owner={}, flags={} (SEP={}, ZSK={}) and key tag={}", - key.algorithm() - .to_mnemonic_str() - .map(|alg| format!("{alg} ({})", key.algorithm())) - .unwrap_or_else(|| key.algorithm().to_string()), - key.owner(), - key.flags(), - key.is_secure_entry_point(), - key.is_zone_signing_key(), - key.public_key().key_tag(), - ) - } - - let num_keys = keys_in_use_idxs.len(); - debug!( - "Signing with {} {}:", - num_keys, - if num_keys == 1 { "key" } else { "keys" } - ); - - for idx in keys_in_use_idxs { - let key = &keys[*idx]; - let is_dnskey_signing_key = dnskey_signing_key_idxs.contains(idx); - let is_non_dnskey_signing_key = - non_dnskey_signing_key_idxs.contains(idx); - let usage = if is_dnskey_signing_key && is_non_dnskey_signing_key { - "CSK" - } else if is_dnskey_signing_key { - "KSK" - } else if is_non_dnskey_signing_key { - "ZSK" - } else { - "Unused" - }; - debug_key(&format!("Key[{idx}]: {usage}"), key.signing_key()); - } -} - -#[allow(clippy::too_many_arguments)] -fn generate_apex_rrsigs( - keys: &[DSK], - config: &GenerateRrsigConfig, - records: &mut core::iter::Peekable< - RecordsIter<'_, N, ZoneRecordData>, - >, - zone_apex: &N, - zone_class: crate::base::iana::Class, - dnskey_signing_key_idxs: &[usize], - non_dnskey_signing_key_idxs: &[usize], - keys_in_use_idxs: &[usize], - out: &mut RrsigRecords, - reusable_scratch: &mut Vec, -) -> Result<(), SigningError> -where - DSK: DesignatedSigningKey, - Inner: SignRaw, - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - N: ToName - + PartialEq - + Clone - + Display - + Send - + CanonicalOrd - + From>, - Octs: AsRef<[u8]> - + From> - + Send - + OctetsFrom> - + Clone - + FromBuilder - + From<&'static [u8]>, - Sort: Sorter, -{ - let Some(apex_owner_rrs) = records.peek() else { - // Nothing to do. - return Ok(()); - }; - - let apex_rrsets = apex_owner_rrs - .rrsets() - .filter(|rrset| rrset.rtype() != Rtype::RRSIG); - - let soa_rrs = apex_owner_rrs - .rrsets() - .find(|rrset| rrset.rtype() == Rtype::SOA); - - let Some(soa_rrs) = soa_rrs else { - // Nothing to do, no SOA RR found. - return Ok(()); - }; - - if soa_rrs.len() > 1 { - // Too many SOA RRs found. - return Err(SigningError::SoaRecordCouldNotBeDetermined); - } - - // Get the SOA RR. - let soa_rr = soa_rrs.first(); - - // Find any existing DNSKEY RRs. - let apex_dnskey_rrset = apex_owner_rrs - .rrsets() - .find(|rrset| rrset.rtype() == Rtype::DNSKEY); - - // Determine the TTL of any existing DNSKEY RRSET and use that as the TTL - // for DNSKEY RRs that we add. If none, then fall back to the SOA TTL. - // - // https://datatracker.ietf.org/doc/html/rfc2181#section-5.2 - // 5.2. TTLs of RRs in an RRSet - // "Consequently the use of differing TTLs in an RRSet is hereby - // deprecated, the TTLs of all RRs in an RRSet must be the same." - // - // Note that while RFC 1033 says: - // RESOURCE RECORDS - // "If you leave the TTL field blank it will default to the minimum time - // specified in the SOA record (described later)." - // - // That RFC pre-dates RFC 1034, and neither dnssec-signzone nor - // ldns-signzone use the SOA MINIMUM as a default TTL, rather they use the - // TTL of the SOA RR as the default and so we will do the same. - let dnskey_rrset_ttl = if let Some(rrset) = &apex_dnskey_rrset { - rrset.ttl() - } else { - soa_rr.ttl() - }; - - // Generate or extend the DNSKEY RRSET with the keys that we will sign - // apex DNSKEY RRs and zone RRs with. - let mut augmented_apex_dnskey_rrs = if let Some(rrset) = apex_dnskey_rrset - { - SortedRecords::<_, _, Sort>::from_iter(rrset.iter().cloned()) - } else { - SortedRecords::<_, _, Sort>::new() - }; - - // https://datatracker.ietf.org/doc/html/rfc4035#section-2.1 2.1. - // Including DNSKEY RRs in a Zone. .. "For each private key used to create - // RRSIG RRs in a zone, the zone SHOULD include a zone DNSKEY RR - // containing the corresponding public key" - // - // We iterate over the DNSKEY RRs at the apex in the zone converting them - // into the correct output octets form, and if any keys we are going to - // sign the zone with do not exist we add them. - - for public_key in keys_in_use_idxs - .iter() - .map(|&idx| keys[idx].signing_key().public_key()) - { - let dnskey = public_key.to_dnskey(); - - let signing_key_dnskey_rr = Record::new( - zone_apex.clone(), - zone_class, - dnskey_rrset_ttl, - Dnskey::convert(dnskey.clone()).into(), - ); - - // Add the DNSKEY RR to the set of DNSKEY RRs to create RRSIGs for. - let is_new_dnskey = augmented_apex_dnskey_rrs - .insert(signing_key_dnskey_rr) - .is_ok(); - - if config.add_used_dnskeys && is_new_dnskey { - // Add the DNSKEY RR to the set of new RRs to output for the zone. - out.dnskeys.push(Record::new( - zone_apex.clone(), - zone_class, - dnskey_rrset_ttl, - Dnskey::convert(dnskey), - )); - } - } - - let augmented_apex_dnskey_rrset = Rrset::new(&augmented_apex_dnskey_rrs); - - // Sign the apex RRSETs in canonical order. - for rrset in apex_rrsets - .filter(|rrset| rrset.rtype() != Rtype::DNSKEY) - .chain(std::iter::once(augmented_apex_dnskey_rrset)) - { - // For the DNSKEY RRSET, use signing keys chosen for that purpose and - // sign the augmented set of DNSKEY RRs that we have generated rather - // than the original set in the zonefile. - let signing_key_idxs = if rrset.rtype() == Rtype::DNSKEY { - dnskey_signing_key_idxs - } else { - non_dnskey_signing_key_idxs - }; - - for key in signing_key_idxs.iter().map(|&idx| &keys[idx]) { - let (inception, expiration) = config - .rrsig_validity_period_strategy - .validity_period_for_rrset(&rrset); - let rrsig_rr = sign_rrset_in( - key.signing_key(), - &rrset, - zone_apex, - inception, - expiration, - reusable_scratch, - )?; - out.rrsigs.push(rrsig_rr); - trace!( - "Signed {} RRs in RRSET {} at the zone apex with keytag {}", - rrset.iter().len(), - rrset.rtype(), - key.signing_key().public_key().key_tag() - ); - } - } - - // Move the iterator past the processed apex owner RRs. - let _ = records.next(); - - Ok(()) -} - -/// Generate `RRSIG` records for a given RRset. -/// -/// See [`sign_rrset_in()`]. -/// -/// If signing multiple RRsets, calling [`sign_rrset_in()`] directly will be -/// more efficient as you can allocate the scratch buffer once and re-use it -/// across multiple calls. -pub fn sign_rrset( - key: &SigningKey, - rrset: &Rrset<'_, N, D>, - apex_owner: &N, - inception: Timestamp, - expiration: Timestamp, -) -> Result>, SigningError> -where - N: ToName + Clone, - D: RecordData + ComposeRecordData + CanonicalOrd, - Inner: SignRaw, - Octs: AsRef<[u8]> + OctetsFrom>, -{ - sign_rrset_in(key, rrset, apex_owner, inception, expiration, &mut vec![]) -} - -/// Generate `RRSIG` records for a given RRset. -/// -/// This function generates an `RRSIG` record for the given RRset based on the -/// given signing key, according to the rules defined in [RFC 4034 section 3] -/// _"The RRSIG Resource Record"_, [RFC 4035 section 2.2] _"Including RRSIG -/// RRs in a Zone"_ and [RFC 6840 section 5.11] _"Mandatory Algorithm Rules"_. -/// -/// No checks are done on the given signing key, any key with any algorithm, -/// apex owner and flags may be used to sign the given RRset. -/// -/// When signing multiple RRsets by calling this function multiple times, the -/// `scratch` buffer parameter can be allocated once and re-used for each call -/// to avoid needing to allocate the buffer for each call. -/// -/// [RFC 4034 section 3]: -/// https://www.rfc-editor.org/rfc/rfc4034.html#section-3 -/// [RFC 4035 section 2.2]: -/// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 -/// [RFC 6840 section 5.11]: -/// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 -pub fn sign_rrset_in( - key: &SigningKey, - rrset: &Rrset<'_, N, D>, - apex_owner: &N, - inception: Timestamp, - expiration: Timestamp, - scratch: &mut Vec, -) -> Result>, SigningError> -where - N: ToName + Clone, - D: RecordData + ComposeRecordData + CanonicalOrd, - Inner: SignRaw, - Octs: AsRef<[u8]> + OctetsFrom>, -{ - // RFC 4035 - // 2.2. Including RRSIG RRs in a Zone - // ... - // "An RRSIG RR itself MUST NOT be signed" - if rrset.rtype() == Rtype::RRSIG { - return Err(SigningError::RrsigRrsMustNotBeSigned); - } - - if expiration < inception { - return Err(SigningError::InvalidSignatureValidityPeriod( - inception, expiration, - )); - } - - // RFC 4034 - // 3. The RRSIG Resource Record - // "The TTL value of an RRSIG RR MUST match the TTL value of the RRset - // it covers. This is an exception to the [RFC2181] rules for TTL - // values of individual RRs within a RRset: individual RRSIG RRs with - // the same owner name will have different TTL values if the RRsets - // they cover have different TTL values." - let rrsig = ProtoRrsig::new( - rrset.rtype(), - key.algorithm(), - rrset.owner().rrsig_label_count(), - rrset.ttl(), - expiration, - inception, - key.public_key().key_tag(), - // The fns provided by `ToName` state in their RustDoc that they - // "Converts the name into a single, uncompressed name" which matches - // the RFC 4034 section 3.1.7 requirement that "A sender MUST NOT use - // DNS name compression on the Signer's Name field when transmitting a - // RRSIG RR.". - // - // TODO: However, is this inefficient? The RFC requires it to be - // SENT uncompressed, but doesn't ban storing it in compressed from? - // - // We don't need to make sure here that the signer name is in - // canonical form as required by RFC 4034 as the call to - // `compose_canonical()` below will take care of that. - apex_owner.clone(), - ); - - scratch.clear(); - - rrsig.compose_canonical(scratch).unwrap(); - for record in rrset.iter() { - record.compose_canonical(scratch).unwrap(); - } - let signature = key.raw_secret_key().sign_raw(&*scratch)?; - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(SigningError::OutOfMemory); - }; - - let rrsig = rrsig.into_rrsig(signature).expect("long signature"); - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // "The value of the Labels field MUST be less than or equal to the - // number of labels in the RRSIG owner name." - debug_assert!( - (rrsig.labels() as usize) < rrset.owner().iter_labels().count() - ); - - Ok(Record::new( - rrset.owner().clone(), - rrset.class(), - rrset.ttl(), - rrsig, - )) -} - -#[cfg(test)] -mod tests { - use bytes::Bytes; - use pretty_assertions::assert_eq; - - use crate::base::iana::SecAlg; - use crate::base::Serial; - use crate::rdata::dnssec::Timestamp; - use crate::sign::crypto::common::KeyPair; - use crate::sign::error::SignError; - use crate::sign::keys::keymeta::IntendedKeyPurpose; - use crate::sign::keys::DnssecSigningKey; - use crate::sign::test_util::*; - use crate::sign::{test_util, PublicKeyBytes, Signature}; - use crate::zonetree::StoredName; - - use super::*; - use crate::sign::signatures::strategy::FixedRrsigValidityPeriodStrategy; - use crate::zonetree::types::StoredRecordData; - use rand::Rng; - - const TEST_INCEPTION: u32 = 0; - const TEST_EXPIRATION: u32 = 100; - - #[test] - fn sign_rrset_adheres_to_rules_in_rfc_4034_and_rfc_4035() { - let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let (inception, expiration) = - (Timestamp::from(0), Timestamp::from(0)); - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // "For example, "www.example.com." has a Labels field value of 3" - // We can use any class as RRSIGs are class independent. - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("www.example.com.")).unwrap(); - let rrset = Rrset::new(&records); - - let rrsig_rr = - sign_rrset(&key, &rrset, &apex_owner, inception, expiration) - .unwrap(); - let rrsig = rrsig_rr.data(); - - // RFC 4035 - // 2.2. Including RRSIG RRs in a Zone - // "For each authoritative RRset in a signed zone, there MUST be at - // least one RRSIG record that meets the following requirements: - // - // o The RRSIG owner name is equal to the RRset owner name. - assert_eq!(rrsig_rr.owner(), rrset.owner()); - // - // o The RRSIG class is equal to the RRset class. - assert_eq!(rrsig_rr.class(), rrset.class()); - // - // o The RRSIG Type Covered field is equal to the RRset type. - // - assert_eq!(rrsig.type_covered(), rrset.rtype()); - // o The RRSIG Original TTL field is equal to the TTL of the - // RRset. - // - assert_eq!(rrsig.original_ttl(), rrset.ttl()); - // o The RRSIG RR's TTL is equal to the TTL of the RRset. - // - assert_eq!(rrsig_rr.ttl(), rrset.ttl()); - // o The RRSIG Labels field is equal to the number of labels in - // the RRset owner name, not counting the null root label and - // not counting the leftmost label if it is a wildcard. - assert_eq!(rrsig.labels(), 3); - // o The RRSIG Signer's Name field is equal to the name of the - // zone containing the RRset. - // - assert_eq!(rrsig.signer_name(), &apex_owner); - // o The RRSIG Algorithm, Signer's Name, and Key Tag fields - // identify a zone key DNSKEY record at the zone apex." - // ^^^ This is outside the control of the rrset_sign() function. - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // "The value of the Labels field MUST be less than or equal to the - // number of labels in the RRSIG owner name." - assert!((rrsig.labels() as usize) < rrset.owner().label_count()); - } - - #[test] - fn sign_rrset_with_wildcard() { - let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let (inception, expiration) = - (Timestamp::from(0), Timestamp::from(0)); - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // ""*.example.com." has a Labels field value of 2" - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("*.example.com.")).unwrap(); - let rrset = Rrset::new(&records); - - let rrsig_rr = - sign_rrset(&key, &rrset, &apex_owner, inception, expiration) - .unwrap(); - let rrsig = rrsig_rr.data(); - - assert_eq!(rrsig.labels(), 2); - } - - #[test] - fn sign_rrset_must_not_sign_rrsigs() { - // RFC 4035 - // 2.2. Including RRSIG RRs in a Zone - // ... - // "An RRSIG RR itself MUST NOT be signed" - - let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - let (inception, expiration) = - (Timestamp::from(0), Timestamp::from(0)); - let dnskey = key.public_key().to_dnskey().convert(); - - let mut records = - SortedRecords::::default(); - records - .insert(mk_rrsig_rr("any.", Rtype::A, 1, ".", &dnskey)) - .unwrap(); - let rrset = Rrset::new(&records); - - let res = - sign_rrset(&key, &rrset, &apex_owner, inception, expiration); - assert!(matches!(res, Err(SigningError::RrsigRrsMustNotBeSigned))); - } - - #[test] - fn sign_rrset_check_validity_period_handling() { - // RFC 4034 - // 3.1.5. Signature Expiration and Inception Fields - // ... - // "The Signature Expiration and Inception field values specify a - // date and time in the form of a 32-bit unsigned number of seconds - // elapsed since 1 January 1970 00:00:00 UTC, ignoring leap - // seconds, in network byte order. The longest interval that can - // be expressed by this format without wrapping is approximately - // 136 years. An RRSIG RR can have an Expiration field value that - // is numerically smaller than the Inception field value if the - // expiration field value is near the 32-bit wrap-around point or - // if the signature is long lived. Because of this, all - // comparisons involving these fields MUST use "Serial number - // arithmetic", as defined in [RFC1982]. As a direct consequence, - // the values contained in these fields cannot refer to dates more - // than 68 years in either the past or the future." - - let apex_owner = Name::root(); - let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); - - let mut records = - SortedRecords::::default(); - records.insert(mk_a_rr("any.")).unwrap(); - let rrset = Rrset::new(&records); - - fn calc_timestamps( - start: u32, - duration: u32, - ) -> (Timestamp, Timestamp) { - let start_serial = Serial::from(start); - let end = start_serial.add(duration).into_int(); - (Timestamp::from(start), Timestamp::from(end)) - } - - // Good: Expiration > Inception. - let (inception, expiration) = calc_timestamps(5, 5); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Good: Expiration == Inception. - let (inception, expiration) = calc_timestamps(10, 0); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Bad: Expiration < Inception. - let (expiration, inception) = calc_timestamps(5, 10); - let res = - sign_rrset(&key, &rrset, &apex_owner, inception, expiration); - assert!(matches!( - res, - Err(SigningError::InvalidSignatureValidityPeriod(_, _)) - )); - - // Good: Expiration > Inception with Expiration near wrap around - // point. - let (inception, expiration) = calc_timestamps(u32::MAX - 10, 10); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Good: Expiration > Inception with Inception near wrap around point. - let (inception, expiration) = calc_timestamps(0, 10); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Good: Expiration > Inception with Exception crossing the wrap - // around point. - let (inception, expiration) = calc_timestamps(u32::MAX - 10, 20); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Good: Expiration - Inception == 68 years. - let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; - let (inception, expiration) = - calc_timestamps(0, sixty_eight_years_in_secs); - sign_rrset(&key, &rrset, &apex_owner, inception, expiration).unwrap(); - - // Bad: Expiration - Inception > 68 years. - // - // I add a rather large amount (A year) because it's unclear where the - // boundary is from the approximate text in the quoted RFC. I think - // it's at 2^31 - 1 so from that you can see how much we need to add - // to cross the boundary: - // - // ``` - // 68 years = 68 * 365 * 24 * 60 * 60 = 2144448000 - // 2^31 - 1 = 2147483647 - // 69 years = 69 * 365 * 24 * 60 * 60 = 2175984000 - // ``` - // - // But as the RFC refers to "dates more than 68 years" a value of 69 - // years is fine to test with. - let sixty_eight_years_in_secs = 68 * 365 * 24 * 60 * 60; - let one_year_in_secs = 365 * 24 * 60 * 60; - - // We can't use calc_timestamps() here because the underlying call to - // Serial::add() panics if the value to add is > 2^31 - 1. - // - // calc_timestamps(0, sixty_eight_years_in_secs + one_year_in_secs); - // - // But Timestamp doesn't care, we can construct those just fine. - // However when sign_rrset() compares the Timestamp inception and - // expiration values it will fail because the PartialOrd impl is - // implemented in terms of Serial which detects the wrap around. - // - // I think this is all good because RFC 4034 doesn't prevent creation - // and storage of an arbitrary 32-bit unsigned number of seconds as - // the inception or expiration value, it only mandates that "all - // comparisons involving these fields MUST use "Serial number - // arithmetic", as defined in [RFC1982]" - let (inception, expiration) = ( - Timestamp::from(0), - Timestamp::from(sixty_eight_years_in_secs + one_year_in_secs), - ); - let res = - sign_rrset(&key, &rrset, &apex_owner, inception, expiration); - assert!(matches!( - res, - Err(SigningError::InvalidSignatureValidityPeriod(_, _)) - )); - } - - #[test] - fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let records = - SortedRecords::::default(); - let no_keys: [DnssecSigningKey; 0] = []; - - generate_rrsigs( - RecordsIter::new(&records), - &no_keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ) - .unwrap(); - } - - #[test] - fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let mut records = SortedRecords::default(); - records.insert(mk_a_rr("example.")).unwrap(); - let no_keys: [DnssecSigningKey; 0] = []; - - let res = generate_rrsigs( - RecordsIter::new(&records), - &no_keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ); - - assert!(matches!(res, Err(SigningError::NoKeysProvided))); - } - - #[test] - fn generate_rrsigs_without_suitable_keys_should_fail_for_non_empty_zone() - { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let mut records = SortedRecords::default(); - records.insert(mk_a_rr("example.")).unwrap(); - - let res = generate_rrsigs( - RecordsIter::new(&records), - &[mk_dnssec_signing_key(IntendedKeyPurpose::KSK)], - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ); - assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); - - let res = generate_rrsigs( - RecordsIter::new(&records), - &[mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)], - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ); - assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); - - let res = generate_rrsigs( - RecordsIter::new(&records), - &[mk_dnssec_signing_key(IntendedKeyPurpose::Inactive)], - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ); - assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); - } - - #[test] - fn generate_rrsigs_for_partial_zone_at_apex() { - generate_rrsigs_for_partial_zone("example.", "example."); - } - - #[test] - fn generate_rrsigs_for_partial_zone_beneath_apex() { - generate_rrsigs_for_partial_zone("example.", "in.example."); - } - - fn generate_rrsigs_for_partial_zone(zone_apex: &str, record_owner: &str) { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - - // This is an example of generating RRSIGs for something other than a - // full zone, in this case just for an A record. This test - // deliberately does not include a SOA record as the zone is partial. - let mut records = SortedRecords::default(); - records.insert(mk_a_rr(record_owner)).unwrap(); - - // Prepare a zone signing key and a key signing key. - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - let dnskey = keys[0].public_key().to_dnskey().convert(); - - // Generate RRSIGs. Use the default signing config and thus also the - // DefaultSigningKeyUsageStrategy which will honour the purpose of the - // key when selecting a key to use for signing DNSKEY RRs or other - // zone RRs. We supply the zone apex because we are not supplying an - // entire zone complete with SOA. - let generated_records = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy) - .with_zone_apex(mk_name(zone_apex)), - ) - .unwrap(); - - // Check the generated RRSIG records - let expected_labels = mk_name(record_owner).rrsig_label_count(); - assert_eq!(generated_records.rrsigs.len(), 1); - assert!(generated_records.dnskeys.is_empty()); - assert_eq!( - generated_records.rrsigs[0], - mk_rrsig_rr( - record_owner, - Rtype::A, - expected_labels, - zone_apex, - &dnskey - ) - ); - } - - #[test] - fn generate_rrsigs_ignores_records_outside_the_zone() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let mut records = SortedRecords::default(); - records.extend([ - mk_soa_rr("example.", "mname.", "rname."), - mk_a_rr("in_zone.example."), - mk_a_rr("out_of_zone."), - ]); - - // Prepare a zone signing key and a key signing key. - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - let dnskey = keys[0].public_key().to_dnskey().convert(); - - let generated_records = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ) - .unwrap(); - - // Check the generated records - assert_eq!( - generated_records.dnskeys, - [mk_dnskey_rr("example.", &dnskey),] - ); - - assert_eq!( - generated_records.rrsigs, - [ - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), - mk_rrsig_rr( - "example.", - Rtype::DNSKEY, - 1, - "example.", - &dnskey - ), - mk_rrsig_rr( - "in_zone.example.", - Rtype::A, - 2, - "example.", - &dnskey - ), - ] - ); - - // Repeat but this time passing only the out-of-zone record in and - // show that it DOES get signed if not passed together with the first - // zone. - let generated_records = generate_rrsigs( - RecordsIter::new(&records[2..]), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ) - .unwrap(); - - // Check the generated DNSKEY records - assert!(generated_records.dnskeys.is_empty()); - - // Check the generated RRSIG records - assert_eq!( - generated_records.rrsigs, - [mk_rrsig_rr( - "out_of_zone.", - Rtype::A, - 1, - "out_of_zone.", - &dnskey - )] - ); - } - - #[test] - fn generate_rrsigs_fails_with_multiple_soas_at_apex() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let mut records = SortedRecords::default(); - records.extend([ - mk_soa_rr("example.", "mname.", "rname."), - mk_soa_rr("example.", "other.mname.", "other.rname."), - mk_a_rr("in_zone.example."), - ]); - - // Prepare a zone signing key and a key signing key. - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - - let res = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ); - - assert!(matches!( - res, - Err(SigningError::SoaRecordCouldNotBeDetermined) - )); - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_ksk_and_zsk() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [ - mk_dnssec_signing_key(IntendedKeyPurpose::KSK), - mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), - ]; - let cfg = - GenerateRrsigConfig::default(rrsig_validity_period_strategy); - generate_rrsigs_for_complete_zone(&keys, 0, 1, &cfg).unwrap(); - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_csk() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - let cfg = - GenerateRrsigConfig::default(rrsig_validity_period_strategy); - generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_csk_without_adding_dnskeys() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - let cfg = - GenerateRrsigConfig::default(rrsig_validity_period_strategy) - .without_adding_used_dns_keys(); - generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_only_zsk_should_fail_by_default( - ) { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; - let cfg = - GenerateRrsigConfig::default(rrsig_validity_period_strategy); - - // This should fail as the DefaultSigningKeyUsageStrategy requires - // both ZSK and KSK, or a CSK. - let res = generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg); - assert!(matches!(res, Err(SigningError::NoSuitableKeysFound))); - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_only_zsk_and_fallback_strategy() - { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::ZSK)]; - - // Implement a strategy that falls back to the ZSK for signing zone - // keys if no KSK is available. (ala ldns-sign -A) - struct FallbackStrat; - impl SigningKeyUsageStrategy for FallbackStrat { - const NAME: &'static str = - "Fallback to ZSK usage strategy for testing"; - - fn select_signing_keys_for_rtype< - DSK: DesignatedSigningKey, - >( - candidate_keys: &[DSK], - rtype: Option, - ) -> smallvec::SmallVec<[usize; 4]> { - if core::matches!(rtype, Some(Rtype::DNSKEY)) { - Self::filter_keys(candidate_keys, |_| true) - } else { - Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) - } - } - } - - let fallback_cfg = GenerateRrsigConfig::<_, FallbackStrat, _, _>::new( - rrsig_validity_period_strategy, - ); - generate_rrsigs_for_complete_zone(&keys, 0, 0, &fallback_cfg) - .unwrap(); - } - - fn generate_rrsigs_for_complete_zone( - keys: &[DnssecSigningKey], - ksk_idx: usize, - zsk_idx: usize, - cfg: &GenerateRrsigConfig< - StoredName, - KeyStrat, - ValidityStrat, - DefaultSorter, - >, - ) -> Result<(), SigningError> - where - KeyStrat: SigningKeyUsageStrategy, - ValidityStrat: RrsigValidityPeriodStrategy, - { - // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A - let zonefile = include_bytes!( - "../../../test-data/zonefiles/rfc4035-appendix-A.zone" - ); - - // Load the zone to generate RRSIGs for. - let records = bytes_to_records(&zonefile[..]); - - // Generate DNSKEYs and RRSIGs. - let generated_records = - generate_rrsigs(RecordsIter::new(&records), keys, cfg)?; - - let dnskeys = keys - .iter() - .map(|k| k.public_key().to_dnskey().convert()) - .collect::>(); - - let ksk = &dnskeys[ksk_idx]; - let zsk = &dnskeys[zsk_idx]; - - // Check the generated records. - let mut dnskey_iter = generated_records.dnskeys.iter(); - let mut rrsig_iter = generated_records.rrsigs.iter(); - - // The records should be in a fixed canonical order because the input - // records must be in canonical order, with the exception of the added - // DNSKEY RRs which will be ordered in the order in the supplied - // collection of keys to sign with. While we tell users of - // generate_rrsigs() not to rely on the order of the output, we assume - // that we know what that order is for this test, but would have to - // update this test if that order later changes. - // - // We check each record explicitly by index because assert_eq() on an - // array of objects that includes Rrsig produces hard to read output - // due to the large RRSIG signature bytes being printed one byte per - // line. It also wouldn't support dynamically checking for certain - // records based on the signing configuration used. - - // NOTE: As we only invoked generate_rrsigs() and not generate_nsecs() - // there will not be any RRSIGs covering NSEC records. - - // -- example. - - if cfg.add_used_dnskeys { - // DNSKEY records should have been generated for the apex for both - // of the keys that we used to sign the zone. - assert_eq!( - *dnskey_iter.next().unwrap(), - mk_dnskey_rr("example.", ksk) - ); - if ksk_idx != zsk_idx { - assert_eq!( - *dnskey_iter.next().unwrap(), - mk_dnskey_rr("example.", zsk) - ); - } - } - - // RRSIG records should have been generated for the zone apex records, - // one RRSIG per ZSK used. - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::NS, 1, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::MX, 1, "example.", zsk) - ); - // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. - // Including RRSIG RRs in a Zone. .. "There MUST be an RRSIG for each - // RRset using at least one DNSKEY of each algorithm in the zone - // apex DNSKEY RRset. The apex DNSKEY RRset itself MUST be signed - // by each algorithm appearing in the DS RRset located at the - // delegating parent (if any)." - // - // In the real world a DNSSEC signed zone is only valid when part of a - // hierarchy such that the signatures can be trusted because there - // exists a valid chain of trust up to a root, and each parent zone - // specifies via one or more DS records which DNSKEY RRs the child - // zone should be signed with. - // - // In our contrived test example we don't have a hierarchy or a parent - // zone so there are no DS RRs to consider. The keys that the zone - // should be signed with are determined by the keys passed to - // generate_rrsigs(). In our case that means that the DNSKEY RR RRSIG - // should have been generated using the KSK because the - // DefaultSigningKeyUsageStrategy selects keys to sign DNSKEY RRs - // based on whether they return true or not from - // `DesignatedSigningKey::signs_keys()` and we are using the - // `DnssecSigningKey` impl of `DesignatedSigningKey` which selects - // keys based on their `IntendedKeyPurpose` which we assigned above - // when creating the keys. - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", ksk) - ); - - // -- a.example. - - // NOTE: Per RFC 4035 there is NOT an RRSIG for a.example NS because: - // - // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 - // 2.2. Including RRSIG RRs in a Zone - // ... - // "The NS RRset that appears at the zone apex name MUST be signed, - // but the NS RRsets that appear at delegation points (that is, the - // NS RRsets in the parent zone that delegate the name to the child - // zone's name servers) MUST NOT be signed." - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("a.example.", Rtype::DS, 2, "example.", zsk) - ); - - // -- ns1.a.example. - // ns2.a.example. - - // NOTE: Per RFC 4035 there is NOT an RRSIG for ns1.a.example A - // or ns2.a.example because: - // - // https://datatracker.ietf.org/doc/html/rfc4035#section-2.2 2.2. - // Including RRSIG RRs in a Zone "For each authoritative RRset in a - // signed zone, there MUST be at least one RRSIG record..." ... AND - // ... "Glue address RRsets associated with delegations MUST NOT be - // signed." - // - // ns1.a.example is part of the a.example zone which was delegated - // above and so we are not authoritative for it. - // - // Further, ns1.a.example A is a glue record because a.example NS - // refers to it by name but in order for a recursive resolver to - // follow the delegation to the child zones' nameservers it has to - // know their IP address, and in this case the nameserver name falls - // inside the child zone so strictly speaking only the child zone is - // authoritative for it, yet the resolver can't ask the child zone - // nameserver unless it knows its IP address, hence the need for glue - // in the parent zone. - - // -- ai.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("ai.example.", Rtype::A, 2, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("ai.example.", Rtype::HINFO, 2, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("ai.example.", Rtype::AAAA, 2, "example.", zsk) - ); - - // -- b.example. - - // NOTE: There is no RRSIG for b.example NS for the same reason that - // there is no RRSIG for a.example. - // - // Also, there is no RRSIG for b.example A because b.example is - // delegated and thus we are not authoritative for records in that - // zone. - - // -- ns1.b.example. - // ns2.b.example. - - // NOTE: There is no RRSIG for ns1.b.example or ns2.b.example for - // the same reason that there are no RRSIGs ofr ns1.a.example or - // ns2.a.example, as described above. - - // -- ns1.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("ns1.example.", Rtype::A, 2, "example.", zsk) - ); - - // -- ns2.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("ns2.example.", Rtype::A, 2, "example.", zsk) - ); - - // -- *.w.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("*.w.example.", Rtype::MX, 2, "example.", zsk) - ); - - // -- x.w.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("x.w.example.", Rtype::MX, 3, "example.", zsk) - ); - - // -- x.y.w.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("x.y.w.example.", Rtype::MX, 4, "example.", zsk) - ); - - // -- xx.example. - - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("xx.example.", Rtype::A, 2, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("xx.example.", Rtype::HINFO, 2, "example.", zsk) - ); - assert_eq!( - *rrsig_iter.next().unwrap(), - mk_rrsig_rr("xx.example.", Rtype::AAAA, 2, "example.", zsk) - ); - - // No other records should have been generated. - - assert!(rrsig_iter.next().is_none()); - - Ok(()) - } - - #[test] - fn generate_rrsigs_for_complete_zone_with_multiple_ksks_and_zsks() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let apex = "example."; - - let mut records = SortedRecords::default(); - records.extend([ - mk_soa_rr(apex, "some.mname.", "some.rname."), - mk_ns_rr(apex, "ns.example."), - mk_a_rr("ns.example."), - ]); - - let keys = [ - mk_dnssec_signing_key(IntendedKeyPurpose::KSK), - mk_dnssec_signing_key(IntendedKeyPurpose::KSK), - mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), - mk_dnssec_signing_key(IntendedKeyPurpose::ZSK), - ]; - - let ksk1 = keys[0].public_key().to_dnskey().convert(); - let ksk2 = keys[1].public_key().to_dnskey().convert(); - let zsk1 = keys[2].public_key().to_dnskey().convert(); - let zsk2 = keys[3].public_key().to_dnskey().convert(); - - let generated_records = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ) - .unwrap(); - - // Check the generated records. - assert_eq!(generated_records.dnskeys.len(), 4); - assert_eq!(generated_records.rrsigs.len(), 8); - - // Filter out the records one by one until there should be none left. - let it = generated_records - .dnskeys - .iter() - .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk1)) - .filter(|&rr| rr != &mk_dnskey_rr(apex, &ksk2)) - .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk1)) - .filter(|&rr| rr != &mk_dnskey_rr(apex, &zsk2)); - - let mut it = it.inspect(|rr| { - eprintln!( - "Warning: Unexpected DNSKEY RRs remaining after filtering: {} {} => {:?}", - rr.owner(), - rr.rtype(), - rr.data(), - ); - }); - - assert!(it.next().is_none()); - - let it = generated_records - .rrsigs - .iter() - .filter(|&rr| { - rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk1) - }) - .filter(|&rr| { - rr != &mk_rrsig_rr(apex, Rtype::SOA, 1, apex, &zsk2) - }) - .filter(|&rr| { - rr != &mk_rrsig_rr(apex, Rtype::DNSKEY, 1, apex, &ksk1) - }) - .filter(|&rr| { - rr != &mk_rrsig_rr(apex, Rtype::DNSKEY, 1, apex, &ksk2) - }) - .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk1)) - .filter(|&rr| rr != &mk_rrsig_rr(apex, Rtype::NS, 1, apex, &zsk2)) - .filter(|&rr| { - rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk1) - }) - .filter(|&rr| { - rr != &mk_rrsig_rr("ns.example.", Rtype::A, 2, apex, &zsk2) - }); - - let mut it = it.inspect(|rr| { - eprintln!( - "Warning: Unexpected RRSIG RRs remaining after filtering: {} {} => {:?}", - rr.owner(), - rr.rtype(), - rr.data(), - ); - }); - - assert!(it.next().is_none()); - } - - #[test] - fn generate_rrsigs_for_already_signed_zone() { - let rrsig_validity_period_strategy = - FixedRrsigValidityPeriodStrategy::from(( - TEST_INCEPTION, - TEST_EXPIRATION, - )); - let keys = [mk_dnssec_signing_key(IntendedKeyPurpose::CSK)]; - - let dnskey = keys[0].public_key().to_dnskey().convert(); - - let mut records = SortedRecords::default(); - records.extend([ - // -- example. - mk_soa_rr("example.", "some.mname.", "some.rname."), - mk_ns_rr("example.", "ns.example."), - mk_dnskey_rr("example.", &dnskey), - mk_nsec_rr("example", "ns.example.", "SOA NS DNSKEY NSEC RRSIG"), - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), - mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), - mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey), - mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey), - // -- ns.example. - mk_a_rr("ns.example."), - mk_nsec_rr("ns.example", "example.", "A NSEC RRSIG"), - mk_rrsig_rr("ns.example.", Rtype::A, 1, "example.", &dnskey), - mk_rrsig_rr("ns.example.", Rtype::NSEC, 1, "example.", &dnskey), - ]); - - let generated_records = generate_rrsigs( - RecordsIter::new(&records), - &keys, - &GenerateRrsigConfig::default(rrsig_validity_period_strategy), - ) - .unwrap(); - - // Check the generated records. - let mut iter = generated_records.rrsigs.iter(); - - // The records should be in a fixed canonical order because the input - // records must be in canonical order, with the exception of the added - // DNSKEY RRs which will be ordered in the order in the supplied - // collection of keys to sign with. While we tell users of - // generate_rrsigs() not to rely on the order of the output, we assume - // that we know what that order is for this test, but would have to - // update this test if that order later changes. - // - // We check each record explicitly by index because assert_eq() on an - // array of objects that includes Rrsig produces hard to read output - // due to the large RRSIG signature bytes being printed one byte per - // line. It also wouldn't support dynamically checking for certain - // records based on the signing configuration used. - - // -- example. - - // The DNSKEY was already present in the zone so we do NOT expect a - // DNSKEY to be included in the output. - assert!(generated_records.dnskeys.is_empty()); - - // RRSIG records should have been generated for the zone apex records, - // one RRSIG per ZSK used, even if RRSIG RRs already exist. - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey) - ); - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey) - ); - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::NSEC, 1, "example.", &dnskey) - ); - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("example.", Rtype::DNSKEY, 1, "example.", &dnskey) - ); - - // -- ns.example. - - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("ns.example.", Rtype::A, 2, "example.", &dnskey) - ); - assert_eq!( - *iter.next().unwrap(), - mk_rrsig_rr("ns.example.", Rtype::NSEC, 2, "example.", &dnskey) - ); - - // No other records should have been generated. - - assert!(iter.next().is_none()); - } - - //------------ Helper fns ------------------------------------------------ - - fn mk_dnssec_signing_key( - purpose: IntendedKeyPurpose, - ) -> DnssecSigningKey { - // Note: The flags value has no impact on the role the key will play - // in signing, that is instead determined by its designated purpose - // AND the SigningKeyUsageStrategy in use. - let flags = match purpose { - IntendedKeyPurpose::KSK => 257, - IntendedKeyPurpose::ZSK => 256, - IntendedKeyPurpose::CSK => 257, - IntendedKeyPurpose::Inactive => 0, - }; - - let key = SigningKey::new( - StoredName::root_bytes(), - flags, - TestKey::default(), - ); - - DnssecSigningKey::new(key, purpose) - } - - fn mk_dnskey_rr( - name: &str, - dnskey: &Dnskey, - ) -> Record - where - R: From>, - { - test_util::mk_dnskey_rr( - name, - dnskey.flags(), - dnskey.algorithm(), - dnskey.public_key(), - ) - } - - fn mk_rrsig_rr( - name: &str, - covered_rtype: Rtype, - labels: u8, - signer_name: &str, - dnskey: &Dnskey, - ) -> Record - where - R: From>, - { - test_util::mk_rrsig_rr( - name, - covered_rtype, - &dnskey.algorithm(), - labels, - TEST_EXPIRATION, - TEST_INCEPTION, - dnskey.key_tag(), - signer_name, - TEST_SIGNATURE, - ) - } - - //------------ TestKey --------------------------------------------------- - - const TEST_SIGNATURE_RAW: [u8; 64] = [0u8; 64]; - const TEST_SIGNATURE: Bytes = Bytes::from_static(&TEST_SIGNATURE_RAW); - - struct TestKey([u8; 32]); - - impl SignRaw for TestKey { - fn algorithm(&self) -> SecAlg { - SecAlg::ED25519 - } - - fn raw_public_key(&self) -> PublicKeyBytes { - PublicKeyBytes::Ed25519(self.0.into()) - } - - fn sign_raw(&self, _data: &[u8]) -> Result { - Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) - } - } - - impl Default for TestKey { - fn default() -> Self { - Self(rand::thread_rng().gen()) - } - } -} diff --git a/src/sign/signatures/strategy.rs b/src/sign/signatures/strategy.rs deleted file mode 100644 index 85cac39eb..000000000 --- a/src/sign/signatures/strategy.rs +++ /dev/null @@ -1,111 +0,0 @@ -use smallvec::SmallVec; - -use crate::base::Rtype; -use crate::rdata::dnssec::Timestamp; -use crate::sign::keys::keymeta::DesignatedSigningKey; -use crate::sign::records::Rrset; -use crate::sign::SignRaw; - -//------------ SigningKeyUsageStrategy --------------------------------------- - -// Ala ldns-signzone the default strategy signs with a minimal number of keys -// to keep the response size for the DNSKEY query small, only keys designated -// as being used to sign apex DNSKEY RRs (usually keys with the Secure Entry -// Point (SEP) flag set) will be used to sign DNSKEY RRs. -pub trait SigningKeyUsageStrategy -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - const NAME: &'static str; - - fn select_signing_keys_for_rtype< - DSK: DesignatedSigningKey, - >( - candidate_keys: &[DSK], - rtype: Option, - ) -> SmallVec<[usize; 4]> { - if matches!(rtype, Some(Rtype::DNSKEY)) { - Self::filter_keys(candidate_keys, |k| k.signs_keys()) - } else { - Self::filter_keys(candidate_keys, |k| k.signs_zone_data()) - } - } - - fn filter_keys>( - candidate_keys: &[DSK], - filter: fn(&DSK) -> bool, - ) -> SmallVec<[usize; 4]> { - candidate_keys - .iter() - .enumerate() - .filter_map(|(i, k)| filter(k).then_some(i)) - .collect() - } -} - -//------------ DefaultSigningKeyUsageStrategy -------------------------------- - -pub struct DefaultSigningKeyUsageStrategy; - -impl SigningKeyUsageStrategy - for DefaultSigningKeyUsageStrategy -where - Octs: AsRef<[u8]>, - Inner: SignRaw, -{ - const NAME: &'static str = "Default key usage strategy"; -} - -//------------ RrsigValidityPeriodStrategy ----------------------------------- - -/// The strategy for determining the validity period for an RRSIG for an -/// RRSET. -/// -/// Determining the right inception time and expiration time to use may depend -/// for example on the RTYPE of the RRSET being signed or on whether jitter -/// should be applied. -/// -/// See https://datatracker.ietf.org/doc/html/rfc6781#section-4.4.2. -pub trait RrsigValidityPeriodStrategy { - fn validity_period_for_rrset( - &self, - rrset: &Rrset<'_, N, D>, - ) -> (Timestamp, Timestamp); -} - -//------------ FixedRrsigValidityPeriodStrategy ------------------------------ - -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct FixedRrsigValidityPeriodStrategy { - inception: Timestamp, - expiration: Timestamp, -} - -impl FixedRrsigValidityPeriodStrategy { - pub fn new(inception: Timestamp, expiration: Timestamp) -> Self { - Self { - inception, - expiration, - } - } -} - -//--- impl From<(u32, u32)> - -impl From<(u32, u32)> for FixedRrsigValidityPeriodStrategy { - fn from((inception, expiration): (u32, u32)) -> Self { - Self::new(Timestamp::from(inception), Timestamp::from(expiration)) - } -} - -//--- impl RrsigValidityPeriodStrategy - -impl RrsigValidityPeriodStrategy for FixedRrsigValidityPeriodStrategy { - fn validity_period_for_rrset( - &self, - _rrset: &Rrset<'_, N, D>, - ) -> (Timestamp, Timestamp) { - (self.inception, self.expiration) - } -} diff --git a/src/stelline/README.md b/src/stelline/README.md index fe9491768..6f73657d7 100644 --- a/src/stelline/README.md +++ b/src/stelline/README.md @@ -19,3 +19,117 @@ In both cases real `net::client` instances handle interaction with the server. When using a mock server the replay file should define both test requests and test responses and the mock server replies with the test responses. When using a real server the replay file defines DNS requests and server configuration and real `net::server` instances reply based on their configuration. + +## Syntax of an `.rpl` file + + +The replay format used by Stelline is a line based configuration. The format supports line comments starting with `;`. + +The format contains two sections `CONFIG` and `SCENARIO`. The format of the `CONFIG` section depends on how Stelline is used (e.g. as a client or as a server). The `CONFIG` section extends from the start of the file until the line containing the `CONFIG_END` option. + +The `SCENARIO` section contains the steps to perform in the test. It starts with `SCENARIO_BEGIN` and ends with `SCENARIO_END`. Note that all `.rpl` files must end with `SCENARIO_END`. + +### Steps +A scenario consists of steps. Each step is something that is executed in the test. A step can be one of the following types: + +- `QUERY` +- `CHECK_ANSWER` +- `TIME_PASSES` +- `TRAFFIC` (TODO) +- `CHECK_TEMP_FILE` (TODO) +- `ASSIGN` (TODO) + +In general, the syntax looks like: + +```rpl +STEP id type data +``` + +where `id` is a positive integer, `type` on of the step types mentioned above, and `data` is the data for the step. + +Steps of types `QUERY` and `CHECK_ANSWER` have entries associated with them, which are textual representations of DNS messages. These entries are simply put aafter the `STEP` declaration. + +A `QUERY` step queries a server process. It can optionally have data declaring its `ADDRESS` and `KEY`: + +```rpl +STEP 1 QUERY +STEP 1 QUERY ADDRESS KEY +``` + +A `CHECK_ANSWER` step checks an incoming answer. It has no data, only entries. + +```rpl +STEP 1 CHECK_ANSWER +``` + +A `TIME_PASSES` step increments the fake system clock by the specified number of seconds. The time is given after the `ELAPSE` token. + +```rpl +STEP 1 TIME_PASSES ELAPSE +``` + +### Entries + +An entry represents a DNS message (or an expected DNS message). It starts with `ENTRY_BEGIN` and ends with `ENTRY_END`. There are several parts to an entry, which depend on the use of the entry. The simplest form is the form used the `QUERY` step: + +```rpl +ENTRY_BEGIN +REPLY +SECTION + ... +SECTION + ... +... +ENTRY_END +``` + +The `REPLY` part specifies the header information of the outgoing message. The supported values are: + + - RCODES: `NOERROR`, `FORMERR`, `SERVFAIL`, `NXDOMAIN`, `NOTIMP`, `REFUSED`, `YXDOMAIN`, `YXRRSET`, `NXRRSET`, `NOTAUTH`, `NOTZONE`, `BADVERS`, `BADCOOKIE`. + - Flags: `AA`, `AD`, `CD`, `DO`, `QR`, `RA`, `RD`, `TC`, `NOTIFY`. + +The `SECTION` directives specify which sections to fill with each section with. The content of a section is exactly like a zonefile, except for the question section, which does not require rdata. + +There are two other sections in an entry, which are only relevant to ranges (see below), which are `MATCH` and `ADJUST`. The `MATCH` directive specifies which parts of the incoming message must match the reply to match the entry. The following values are allowed: + +- `all`: same as `opcode qtype qname rcode flags answer authority additional` +- `opcode` +- `qname` +- `rcode` +- Flags: `AD`, `CD`, `DO`, `RD`, `flags` +- `question`: same as `qtype qname` +- Sections: `answer`, `authority`, `additional` +- `subdomain` +- `ttl` +- Protocol: `TCP`, `UDP` +- `server_cookie` +- `ednsdata` +- `MOCK_CLIENT` +- `CONNECTION_CLOSED` +- `EXTRA_PACKETS` +- `ANY_ANSWER` + +The `ADJUST` directive specifies which parts of the incoming message should be changed before sending. The possible values are `copy_id` and `copy_query`. + +### Ranges + +Mock responses from are defined by a `RANGE`, which specifies the reponses that are given to a "range" of queries. A range is delimited by the `RANGE_BEGIN` and `RANGE_END` tokens. + +The `RANGE_BEGIN` directive takes two positive integers: a start and end value. A `QUERY` step with an id within that range can match this range. + +After the numeric range, a range has a list of addresses that match on this range, with each address on its own line prefixed with `ADDRESS`. + +Lastly, a range contains a list of entries, following the syntax described above. + +```rpl +RANGE_BEGIN 0 100 ; begin and end of the range + ADDRESS 1.1.1.1 ; an address to match + ADDRESS 2.2.2.2 ; a second address, any number of address is allowed + +ENTRY_BEGIN +; ... +ENTRY_END + +; more entries may be added +RANGE_END +``` diff --git a/src/stelline/client.rs b/src/stelline/client.rs index 63e194b93..71834631f 100644 --- a/src/stelline/client.rs +++ b/src/stelline/client.rs @@ -15,21 +15,19 @@ use bytes::Bytes; #[cfg(all(feature = "std", test))] use mock_instant::thread_local::MockClock; use tracing::{debug, info_span, trace}; -use tracing_subscriber::EnvFilter; use crate::base::iana::{Opcode, OptionCode}; use crate::base::opt::{ComposeOptData, OptData}; use crate::base::{Message, MessageBuilder}; +use crate::logging::init_logging; use crate::net::client::request::{ ComposeRequest, ComposeRequestMulti, Error, GetResponse, GetResponseMulti, RequestMessage, RequestMessageMulti, SendRequest, SendRequestMulti, }; -use crate::stelline::matches::match_multi_msg; use crate::zonefile::inplace::Entry::Record; use super::channel::DEF_CLIENT_ADDR; -use super::matches::match_msg; use super::parse_stelline::{Entry, Reply, Sections, Stelline, StepType}; //----------- StellineError --------------------------------------------------- @@ -156,7 +154,7 @@ pub async fn do_client_simple>>>( .entry .as_ref() .ok_or(StellineErrorCause::MissingStepEntry)?; - if !match_msg(entry, &answer, true) { + if entry.match_msg(&answer).is_err() { return Err(StellineErrorCause::MismatchedAnswer); } } @@ -434,13 +432,14 @@ impl ClientFactory for QueryTailoredClientFactory { //----------- do_client() ---------------------------------------------------- -// This function handles the client part of a Stelline script. It is meant -// to test server code. This code need refactoring. The do_client_simple -// function takes care of supporting SendRequest, so no need to support that -// here. UDP support can be made simplere by removing any notion of a -// connection and associating a source address with every request. TCP -// suport can be made simpler because the test code does not have to be -// careful about the TcpKeepalive option and just keep the connection open. +/// This function handles the client part of a Stelline script. +/// +/// It is meant to test server code. This code needs refactoring. The +/// do_client_simple function takes care of supporting SendRequest, so no need +/// to support that here. UDP support can be made simpler by removing any +/// notion of a connection and associating a source address with every request. +/// TCP support can be made simpler because the test code does not have to be +/// careful about the TcpKeepalive option and just keep the connection open. pub async fn do_client<'a, T: ClientFactory>( stelline: &'a Stelline, step_value: &'a CurrStepValue, @@ -501,150 +500,90 @@ pub async fn do_client<'a, T: ClientFactory>( return Err(StellineErrorCause::MissingResponse); }; - if entry - .matches - .as_ref() - .map(|v| v.extra_packets) - .unwrap_or_default() - { + // If extra_packets is true, then the all the responses are + // checked out of order. + if entry.matches.extra_packets { // This assumes that the client used for the test knows // how to detect the last response in a set of // responses, e.g. the xfr client knows how to detect // the last response in an AXFR/IXFR response set. trace!("Awaiting an unknown number of answers"); - let mut entry = entry.clone(); + let mut matcher = entry.match_multi_msg_unordered(); loop { - let resp = match tokio::time::timeout( - Duration::from_secs(3), - send_request.get_response(), - ) - .await - .map_err(|_| StellineErrorCause::AnswerTimedOut)? - { - Err( - Error::StreamReceiveError - | Error::ConnectionClosed, - ) if entry - .matches - .as_ref() - .map(|v| v.conn_closed) - == Some(true) => - { - trace!( - "Connection terminated as expected" - ); - break; - } - other => other, - }?; + let resp = + get_next_response(&mut send_request).await?; + let resp = check_connection_closed(entry, resp)?; - if resp.is_none() { + let Some(resp) = resp else { trace!("Stream complete"); - if !entry.sections.as_ref().unwrap().answer[0] - .is_empty() - { + if matcher.finish().is_err() { return Err( StellineErrorCause::MismatchedAnswer, ); } else { break; } - } + }; - let resp = resp.unwrap(); + trace!("Received answer."); + trace!(?resp); - if entry.matches.as_ref().map(|v| v.conn_closed) - == Some(true) - { + if matcher.match_msg(&resp).is_err() { return Err( - StellineErrorCause::MissingTermination, + StellineErrorCause::MismatchedAnswer, ); - } - - trace!("Received answer."); - trace!(?resp); + }; - let mut out_entry = Some(vec![]); - match_multi_msg( - &entry, - 0, - &resp, - true, - &mut out_entry, + trace!( + "Answer RRs remaining = {}", + matcher.answer_records_left() ); - let num_rrs_remaining_after = out_entry - .as_ref() - .map(|entries| entries.len()) - .unwrap_or_default(); - if let Some(section) = &mut entry.sections { - section.answer[0] = out_entry.unwrap(); + } + } else if entry.matches.any_answer { + let resp = + get_next_response(&mut send_request).await?; + let resp = check_connection_closed(entry, resp)?; + + let Some(resp) = resp else { + if !entry.matches.conn_closed { + return Err( + StellineErrorCause::MissingResponse, + ); } - trace!("Answer RRs remaining = {num_rrs_remaining_after}"); + break; + }; + + if entry.match_msg(&resp).is_err() { + return Err(StellineErrorCause::MismatchedAnswer); } } else { - let num_expected_answers = if entry - .matches - .as_ref() - .map(|v| v.any_answer) - .unwrap_or_default() - { - 1 - } else { - entry - .sections - .as_ref() - .map(|section| section.answer.len()) - .unwrap_or_default() - }; + let num_expected_answers = + entry.sections.answer.len(); - for idx in 0..num_expected_answers { + let mut matcher = entry.match_multi_msg_ordered(); + + for idx in 1..=num_expected_answers { trace!( - "Awaiting answer {}/{num_expected_answers}...", - idx + 1 + "Awaiting answer {idx}/{num_expected_answers}..." ); - let resp = match tokio::time::timeout( - Duration::from_secs(3), - send_request.get_response(), - ) - .await - .map_err(|_| StellineErrorCause::AnswerTimedOut)? - { - Err( - Error::StreamReceiveError - | Error::ConnectionClosed, - ) if entry - .matches - .as_ref() - .map(|v| v.conn_closed) - == Some(true) => - { - trace!( - "Connection terminated as expected" + let resp = + get_next_response(&mut send_request).await?; + let resp = check_connection_closed(entry, resp)?; + + let Some(resp) = resp else { + trace!("Stream complete"); + if matcher.finish().is_err() { + return Err( + StellineErrorCause::MismatchedAnswer, ); + } else { break; } - other => other, - }?; - - let Some(resp) = resp else { - return Err( - StellineErrorCause::MissingResponse, - ); }; - if entry.matches.as_ref().map(|v| v.conn_closed) - == Some(true) - { - return Err( - StellineErrorCause::MissingTermination, - ); - } - trace!("Received answer."); trace!(?resp); - if !match_multi_msg( - entry, idx, &resp, true, &mut None, - ) { + if matcher.match_msg(&resp).is_err() { return Err( StellineErrorCause::MismatchedAnswer, ); @@ -690,25 +629,32 @@ pub async fn do_client<'a, T: ClientFactory>( } } -/// Setup logging of events reported by domain and the test suite. -/// -/// Use the RUST_LOG environment variable to override the defaults. -/// -/// E.g. To enable debug level logging: -/// RUST_LOG=DEBUG -/// -/// Or to log only the steps processed by the Stelline client: -/// RUST_LOG=net_server::net::stelline::client=DEBUG -/// -/// Or to enable trace level logging but not for the test suite itself: -/// RUST_LOG=TRACE,net_server=OFF -fn init_logging() { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); +async fn get_next_response( + send_request: &mut Response, +) -> Result>, StellineErrorCause> { + let resp = tokio::time::timeout( + Duration::from_secs(3), + send_request.get_response(), + ) + .await + .map_err(|_| StellineErrorCause::AnswerTimedOut)?; + + if let Err(Error::StreamReceiveError | Error::ConnectionClosed) = resp { + Ok(None) + } else { + dbg!(resp.map_err(StellineErrorCause::ClientError)) + } +} + +fn check_connection_closed( + entry: &Entry, + resp: Option, +) -> Result, StellineErrorCause> { + let conn_closed = entry.matches.conn_closed; + match resp { + Some(_) if conn_closed => Err(StellineErrorCause::MissingTermination), + x => Ok(x), + } } fn entry2reqmsg(entry: &Entry) -> RequestMessage> { @@ -716,12 +662,7 @@ fn entry2reqmsg(entry: &Entry) -> RequestMessage> { let mut reqmsg = RequestMessage::new(msg) .expect("should not fail unless the request is AXFR"); - if !entry - .matches - .as_ref() - .map(|v| v.mock_client) - .unwrap_or_default() - { + if !entry.matches.mock_client { reqmsg.set_dnssec_ok(reply.fl_do); } @@ -742,12 +683,7 @@ fn entry2reqmsg_multi(entry: &Entry) -> RequestMessageMulti> { let (sections, reply, msg) = entry2msg(entry); let mut reqmsg = RequestMessageMulti::new(msg).unwrap(); - if !entry - .matches - .as_ref() - .map(|v| v.mock_client) - .unwrap_or_default() - { + if !entry.matches.mock_client { reqmsg.set_dnssec_ok(reply.fl_do); } if reply.notify { @@ -763,8 +699,8 @@ fn entry2reqmsg_multi(entry: &Entry) -> RequestMessageMulti> { reqmsg } -fn entry2msg(entry: &Entry) -> (&Sections, Reply, Message>) { - let sections = entry.sections.as_ref().unwrap(); +fn entry2msg(entry: &Entry) -> (&Sections, &Reply, Message>) { + let sections = &entry.sections; let mut msg = MessageBuilder::new_vec().question(); if let Some(opcode) = entry.opcode { msg.header_mut().set_opcode(opcode); @@ -788,10 +724,7 @@ fn entry2msg(entry: &Entry) -> (&Sections, Reply, Message>) { msg.push(rec).unwrap(); } } - let reply: Reply = match &entry.reply { - Some(reply) => reply.clone(), - None => Default::default(), - }; + let reply = &entry.reply; let header = msg.header_mut(); header.set_rd(reply.rd); header.set_ad(reply.ad); diff --git a/src/stelline/matches.rs b/src/stelline/matches.rs index 3aa073f70..5015ae19b 100644 --- a/src/stelline/matches.rs +++ b/src/stelline/matches.rs @@ -1,494 +1,510 @@ -use super::parse_stelline::{Entry, Matches, Question, Reply}; -use crate::base::iana::{Opcode, OptRcode, Rtype}; -use crate::base::opt::{Opt, OptRecord}; -use crate::base::{Message, ParsedName, QuestionSection, RecordSection}; +use tracing::trace; + +use super::parse_stelline::{Entry, Matches}; +use crate::base::iana::{Opcode, Rtype}; +use crate::base::opt::Opt; +use crate::base::{Message, ParsedName, ParsedRecord, RecordSection}; use crate::dep::octseq::Octets; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::Entry as ZonefileEntry; -use std::vec::Vec; - -pub fn match_msg<'a, Octs: AsRef<[u8]> + Clone + Octets + 'a>( - entry: &Entry, - msg: &'a Message, - verbose: bool, -) -> bool -where - ::Range<'a>: Clone, -{ - match_multi_msg(entry, 0, msg, verbose, &mut None) -} -pub fn match_multi_msg<'a, Octs: AsRef<[u8]> + Clone + Octets + 'a>( - entry: &Entry, - idx: usize, - msg: &'a Message, - verbose: bool, - out_answer: &mut Option>, -) -> bool -where - ::Range<'a>: Clone, -{ - let sections = entry.sections.as_ref().unwrap(); - - let mut matches: Matches = match &entry.matches { - Some(matches) => matches.clone(), - None => Default::default(), - }; - - let reply: Reply = match &entry.reply { - Some(reply) => reply.clone(), - None => Default::default(), - }; - - if matches.all { - matches.opcode = true; - matches.qtype = true; - matches.qname = true; - matches.flags = true; - matches.rcode = true; - matches.answer = true; - matches.authority = true; - matches.additional = true; +impl Matches { + pub fn set_all(&mut self) { + self.opcode = true; + self.qtype = true; + self.qname = true; + self.flags = true; + self.rcode = true; + self.answer = true; + self.authority = true; + self.additional = true; } - if matches.question { - matches.qtype = true; - matches.qname = true; + pub fn set_question(&mut self) { + self.qtype = true; + self.qname = true; } +} + +pub struct DidNotMatch; - if matches.edns_data { - matches.additional = true; +impl Entry { + pub fn match_multi_msg_ordered(&self) -> OrderedMultiMatcher { + OrderedMultiMatcher { + entry: self, + answer_idx: 0, + } } - if matches.additional { - let mut arcount = msg.header_counts().arcount(); - if msg.opt().is_some() { - arcount -= 1; + pub fn match_multi_msg_unordered(&self) -> UnorderedMultiMatcher { + let all_answers = + self.sections.answer.iter().flatten().cloned().collect(); + UnorderedMultiMatcher { + entry: self, + answers: all_answers, } - let match_edns_bytes = if matches.edns_data { - Some(sections.additional.edns_bytes.as_ref()) - } else { - None - }; - if !match_section( - sections.additional.zone_entries.clone(), - match_edns_bytes, - msg.additional().unwrap(), - arcount, - matches.ttl, - verbose, - false, - &mut None, - ) { - if verbose { - println!("match_msg: additional section does not match"); + } + + pub fn match_msg( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + self.match_flags(msg)?; + self.match_edns_data(msg)?; + self.match_opcode(msg)?; + self.match_question(msg)?; + self.match_rcode(msg)?; + + if self.matches.answer { + if self.matches.any_answer { + self.match_any_answer(msg)?; + } else { + self.match_answer(0, msg)?; } - return false; } + + self.match_authority(msg)?; + self.match_additional(msg)?; + + if self.matches.tcp { + // Note: Creation of a TCP client is handled by the client factory passed to do_client(). + // TODO: Verify that the client is actually a TCP client. + } + if self.matches.udp { + // Note: Creation of a UDP client is handled by the client factory passed to do_client(). + // TODO: Verify that the client is actually a UDP client. + } + + // All checks passed! + Ok(()) } - if matches.answer { - if matches.any_answer { - // Match any one of the available answers (additional answers can - // be provided using the EXTRA_PACKET Stelline directive). - let mut matched = false; - for answer in §ions.answer { - if !match_section( - answer.clone(), - None, - msg.answer().unwrap(), - msg.header_counts().ancount(), - matches.ttl, - verbose, - matches.extra_packets, - out_answer, - ) { - matched = true; - break; - } - } - if !matched { - if verbose { - println!("match_msg: answer section does not match"); - } - return false; - } - } else { - let Some(answer) = sections.answer.get(idx) else { - if verbose { - println!("match_msg: answer section {idx} missing"); - } - return false; + /// Match the ENDS data in the OPT record + fn match_edns_data( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if self.matches.edns_data { + let data = &self.sections.additional.edns_bytes; + let opt = Opt::from_slice(data).unwrap(); + + let Some(msg_opt) = msg.opt() else { + trace!("match_msg: an OPT record must be present"); + return Err(DidNotMatch); }; - if !match_section( - answer.clone(), - None, - msg.answer().unwrap(), - msg.header_counts().ancount(), - matches.ttl, - verbose, - matches.extra_packets, - out_answer, - ) && !matches.extra_packets - { - if verbose { - println!( - "match_msg: answer section {idx} does not match" - ); - } - return false; + + trace!("matching {:?} with {:?}", msg_opt.opt(), opt); + if msg_opt.opt() != opt { + return Err(DidNotMatch); } } - } - if matches.authority - && !match_section( - sections.authority.clone(), - None, - msg.authority().unwrap(), - msg.header_counts().nscount(), - matches.ttl, - verbose, - false, - &mut None, - ) - { - if verbose { - println!("match_msg: authority section does not match"); - } - return false; + Ok(()) } - if matches.ad && !msg.header().ad() { - if verbose { - println!("match_msg: AD not in message",); + /// Match the value of the OPCODE + fn match_opcode( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if !self.matches.opcode { + return Ok(()); } - return false; - } - if matches.cd && !msg.header().cd() { - if verbose { - println!("match_msg: CD not in message",); + + let expected_opcode = if self.reply.notify { + Opcode::NOTIFY + } else if let Some(opcode) = self.opcode { + opcode + } else { + Opcode::QUERY + }; + + if msg.header().opcode() == expected_opcode { + Ok(()) + } else { + trace!( + "Opcode does not match, got {} expected {}", + msg.header().opcode(), + expected_opcode + ); + Err(DidNotMatch) } - return false; } - if matches.fl_do { - if let Some(opt) = msg.opt() { - if !opt.dnssec_ok() { - if verbose { - println!("match_msg: DO not in message",); + + /// Match the value of the RCODE + fn match_rcode( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if self.matches.rcode { + let msg_rcode = msg.opt_rcode(); + match self.reply.rcode { + Some(reply_rcode) if reply_rcode != msg_rcode => { + trace!( + "Wrong Rcode, expected {reply_rcode}, got {msg_rcode}" + ); + return Err(DidNotMatch); } - return false; - } - } else { - if verbose { - println!("match_msg: DO not in message (not opt record)",); + _ => { /* Okay */ } } - return false; } + Ok(()) } - if matches.rd && !msg.header().rd() { - if verbose { - println!("match_msg: RD not in message",); - } - return false; - } - if matches.flags { - let header = msg.header(); - if reply.qr != header.qr() { - if verbose { - todo!(); - } - return false; + + /// Match the question section + /// + /// This checks the qname, qtype and subdomain of the records in the section + /// (if the relevant fields of `Matches` are set). + fn match_question( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + let match_section = &self.sections.question; + let msg_section = msg.question(); + + if !self.matches.qname + && !self.matches.qtype + && !self.matches.subdomain + { + // small optimization: nothing to check! + return Ok(()); } - if reply.aa != header.aa() { - if verbose { - println!( - "match_msg: AA does not match, got {}, expected {}", - header.aa(), - reply.aa - ); + + for msg_rr in msg_section { + let msg_rr = msg_rr.unwrap(); + let mat_rr = &match_section[0]; + if self.matches.qname && msg_rr.qname() != mat_rr.qname() { + return Err(DidNotMatch); } - return false; - } - if reply.tc != header.tc() { - if verbose { - println!( - "match_msg: TC does not match, got {}, expected {}", - header.tc(), - reply.tc - ); + if self.matches.subdomain + && !msg_rr.qname().ends_with(mat_rr.qname()) + { + return Err(DidNotMatch); } - return false; - } - if reply.ra != header.ra() { - if verbose { - println!( - "match_msg: RA does not match, got {}, expected {}", - header.ra(), - reply.ra - ); + if self.matches.qtype && msg_rr.qtype() != mat_rr.qtype() { + return Err(DidNotMatch); } - return false; } - if reply.rd != header.rd() { - if verbose { - println!( - "match_msg: RD does not match, got {}, expected {}", - header.rd(), - reply.rd - ); + + // All checks passed! + Ok(()) + } + + fn match_section( + &self, + match_section: &[ZonefileEntry], + msg_section: RecordSection<'_, Octs>, + msg_count: u16, + allow_partial_match: bool, + ) -> Result<(), DidNotMatch> { + if !allow_partial_match && match_section.len() != msg_count as usize { + trace!("match_section: expected section length {} doesn't match message count {}", match_section.len(), msg_count); + if !match_section.is_empty() { + trace!("expected sections:"); + for section in match_section { + trace!(" {section:?}"); + } } - return false; + return Err(DidNotMatch); } - if reply.ad != header.ad() { - if verbose { - println!( - "match_msg: AD does not match, got {}, expected {}", - header.ad(), - reply.ad - ); + + // We delete the matched sections to track which ones we've seen, so we + // clone the vec. + let mut match_section = match_section.to_vec(); + + for msg_rr in msg_section { + let msg_rr = msg_rr.unwrap(); + if msg_rr.rtype() == Rtype::OPT { + continue; } - return false; - } - if reply.cd != header.cd() { - if verbose { - println!( - "match_msg: CD does not match, got {}, expected {}", - header.cd(), - reply.cd + + if let Some(idx) = + self.find_matching_record(&match_section, &msg_rr) + { + // Delete the entry because only one record should match it + match_section.swap_remove(idx); + } else { + // Nothing matches + trace!( + "no match for record '{} {} {}'", + msg_rr.owner(), + msg_rr.class(), + msg_rr.rtype(), ); + return Err(DidNotMatch); } - return false; } + + // All entries in the reply were matched. + Ok(()) } - if matches.opcode { - let expected_opcode = if reply.notify { - Opcode::NOTIFY - } else if let Some(opcode) = entry.opcode { - opcode - } else { - Opcode::QUERY - }; - if msg.header().opcode() != expected_opcode { - if verbose { - println!( - "Opcode does not match, got {} expected {}", - msg.header().opcode(), - expected_opcode - ); + + fn find_matching_record( + &self, + entries: &[ZonefileEntry], + msg_rr: &ParsedRecord<'_, Octs>, + ) -> Option { + let msg_rdata = msg_rr + .to_record::>>() + .unwrap() + .unwrap(); + + entries.iter().position(|mat_rr| { + // Remove outer Record + let ZonefileEntry::Record(mat_rr) = mat_rr else { + panic!("include not expected"); + }; + + let owner = msg_rr.owner() == mat_rr.owner(); + let class = msg_rr.class() == mat_rr.class(); + let rtype = msg_rr.rtype() == mat_rr.rtype(); + let rdata = msg_rdata.data() == mat_rr.data(); + + if !(owner && class && rtype && rdata) { + return false; } - return false; - } + + // Found one. Check TTL + if self.matches.ttl && msg_rr.ttl() != mat_rr.ttl() { + trace!("match_section: TTL does not match for {} {} {}: got {:?} expected {:?}", + msg_rr.owner(), msg_rr.class(), msg_rr.rtype(), + msg_rr.ttl(), mat_rr.ttl()); + return false; + } + + true + }) } - if (matches.qname || matches.qtype || matches.subdomain) - && !match_question( - sections.question.clone(), - msg.question(), - matches.qname, - matches.qtype, - matches.subdomain, - ) - { - if verbose { - println!("match_msg: question section does not match"); + + /// Match the specified answer + fn match_answer( + &self, + answer_idx: usize, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if !self.matches.answer { + return Ok(()); } - return false; + + let Some(answer) = self.sections.answer.get(answer_idx) else { + trace!("match_msg: answer section {answer_idx} missing"); + return Err(DidNotMatch); + }; + + self.match_section( + answer, + msg.answer().unwrap(), + msg.header_counts().ancount(), + self.matches.extra_packets, + ) } - if matches.rcode { - let msg_rcode = - get_opt_rcode(&Message::from_octets(msg.as_slice()).unwrap()); - match (reply.rcode, msg_rcode) { - (Some(reply_rcode), msg_rcode) if reply_rcode != msg_rcode => { - if verbose { - println!( - "Wrong Rcode, expected {reply_rcode}, got {msg_rcode}" - ); - } - return false; + + /// Match any one of the available answers (additional answers can + /// be provided using the EXTRA_PACKET Stelline directive). + fn match_any_answer( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + for answer_idx in 0..self.sections.answer.len() { + if let Ok(()) = self.match_answer(answer_idx, msg) { + return Ok(()); } - _ => { /* Okay */ } } - } - if matches.tcp { - // Note: Creation of a TCP client is handled by the client factory passed to do_client(). - // TODO: Verify that the client is actually a TCP client. - } - if matches.ttl { - // Nothing to do. TTLs are checked in the relevant sections. - } - if matches.udp { - // Note: Creation of a UDP client is handled by the client factory passed to do_client(). - // TODO: Verify that the client is actually a UDP client. + Err(DidNotMatch) } - // All checks passed! - true -} + /// Match the authority section if `matches.authority` is set + fn match_authority( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if self.matches.authority { + self.match_section( + &self.sections.authority, + msg.authority().unwrap(), + msg.header_counts().nscount(), + false, + )?; + } + Ok(()) + } -#[allow(clippy::too_many_arguments)] -fn match_section< - 'a, - Octs: Clone + Octets = Octs2> + 'a, - Octs2: AsRef<[u8]> + Clone, ->( - mut match_section: Vec, - match_edns_bytes: Option<&[u8]>, - msg_section: RecordSection<'a, Octs>, - msg_count: u16, - match_ttl: bool, - verbose: bool, - allow_partial_match: bool, - out_entry: &mut Option>, -) -> bool { - let mat_opt = - match_edns_bytes.map(|bytes| Opt::from_slice(bytes).unwrap()); - - if !allow_partial_match - && match_section.len() != >::into(msg_count) - { - if verbose { - println!("match_section: expected section length {} doesn't match message count {}", match_section.len(), msg_count); - if !match_section.is_empty() { - println!("expected sections:"); - for section in &match_section { - println!(" {section:?}"); - } - } + /// Match the additional section if `matches.additional` is set + fn match_additional( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + if !self.matches.additional { + return Ok(()); } - if let Some(out_entry) = out_entry { - *out_entry = match_section; + + let mut arcount = msg.header_counts().arcount(); + if msg.opt().is_some() { + arcount -= 1; } - return false; + + self.match_section( + &self.sections.additional.zone_entries, + msg.additional().unwrap(), + arcount, + false, + ) } - 'outer: for msg_rr in msg_section { - let msg_rr = msg_rr.unwrap(); - if msg_rr.rtype() == Rtype::OPT { - if let Some(mat_opt) = mat_opt { - let record = - msg_rr.clone().into_record::>().unwrap().unwrap(); - let record = OptRecord::from_record(record); - println!("matching {:?} with {:?}", record.opt(), mat_opt); - if record.opt() == mat_opt { - continue; - } - } else { - continue; + + /// Match the flags of the incoming message including `DO` + fn match_flags( + &self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + let r = &self.reply; + let h = msg.header(); + + // These flags must be set if the value for self is true + let flags = [ + ("AD", self.matches.ad, h.ad()), + ("CD", self.matches.cd, h.cd()), + ("RD", self.matches.rd, h.rd()), + ]; + for (name, m, h) in flags { + if m && !h { + trace!("match_msg: {name} not in message",); + return Err(DidNotMatch); } } - for (index, mat_rr) in match_section.iter().enumerate() { - // Remove outer Record - let mat_rr = if let ZonefileEntry::Record(record) = mat_rr { - record - } else { - panic!("include not expected"); - }; - println!( - "matching {:?} with {:?}", - msg_rr.owner(), - mat_rr.owner() - ); - if msg_rr.owner() != mat_rr.owner() { - continue; - } - println!( - "matching {:?} with {:?}", - msg_rr.class(), - mat_rr.class() - ); - if msg_rr.class() != mat_rr.class() { - continue; - } - println!( - "matching {:?} with {:?}", - msg_rr.rtype(), - mat_rr.rtype() - ); - if msg_rr.rtype() != mat_rr.rtype() { - continue; - } - let msg_rdata = msg_rr - .clone() - .into_record::>>() - .unwrap() - .unwrap(); - println!( - "matching {:?} with {:?}", - msg_rdata.data(), - mat_rr.data() - ); - if msg_rdata.data() != mat_rr.data() { - continue; - } - // Found one. Check TTL - if match_ttl && msg_rr.ttl() != mat_rr.ttl() { - if verbose { - println!("match_section: TTL does not match for {} {} {}: got {:?} expected {:?}", - msg_rr.owner(), msg_rr.class(), msg_rr.rtype(), - msg_rr.ttl(), mat_rr.ttl()); - } - if let Some(out_entry) = out_entry { - *out_entry = match_section; - } - return false; + if self.matches.fl_do { + let do_set = msg.opt().is_some_and(|o| o.dnssec_ok()); + if !do_set { + trace!("match_msg: DO not set"); + return Err(DidNotMatch); } - // Delete this entry - match_section.swap_remove(index); - continue 'outer; } - // Nothing matches - if verbose { - println!( - "no match for record {} {} {}", - msg_rr.owner(), - msg_rr.class(), - msg_rr.rtype() - ); + + // If we don't check the other flags, we return early + if !self.matches.flags { + return Ok(()); } - if let Some(out_entry) = out_entry { - *out_entry = match_section; + + // These flags must match the reply + let flags = [ + ("QR", r.qr, h.qr()), + ("AA", r.aa, h.aa()), + ("TC", r.tc, h.tc()), + ("RA", r.ra, h.ra()), + ("RA", r.rd, h.rd()), + ("AD", r.ad, h.ad()), + ("CD", r.cd, h.cd()), + ]; + + for (name, r, h) in flags { + if r != h { + trace!("match_msg: {name} does not match, got {r:?}, expected {h:?}"); + return Err(DidNotMatch); + } } - return false; - } - // All entries in the reply were matched. - if let Some(out_entry) = out_entry { - *out_entry = match_section; + Ok(()) } - true } -fn match_question( - match_section: Vec, - msg_section: QuestionSection<'_, Octs>, - match_qname: bool, - match_qtype: bool, - match_subdomain: bool, -) -> bool { - if match_section.is_empty() { - // Nothing to match. - return true; - } - for msg_rr in msg_section { - let msg_rr = msg_rr.unwrap(); - let mat_rr = &match_section[0]; - if match_qname && msg_rr.qname() != mat_rr.qname() { - return false; - } - if match_subdomain && !msg_rr.qname().ends_with(mat_rr.qname()) { - return false; +pub struct OrderedMultiMatcher<'a> { + answer_idx: usize, + entry: &'a Entry, +} + +impl OrderedMultiMatcher<'_> { + pub fn match_msg( + &mut self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + let e = &self.entry; + + e.match_flags(msg)?; + e.match_edns_data(msg)?; + e.match_opcode(msg)?; + e.match_question(msg)?; + e.match_rcode(msg)?; + + if e.matches.answer { + e.match_answer(self.answer_idx, msg)?; } - if match_qtype && msg_rr.qtype() != mat_rr.qtype() { - return false; + + e.match_authority(msg)?; + e.match_additional(msg)?; + + self.answer_idx += 1; + Ok(()) + } + + pub fn finish(self) -> Result<(), DidNotMatch> { + let answer = &self.entry.sections.answer; + + // Special case for when we don't have to check anything + if self.answer_idx == 0 && answer.len() == 1 && answer[0].is_empty() { + Ok(()) + } else if self.answer_idx < answer.len() { + Err(DidNotMatch) + } else { + Ok(()) } } - // All entries in the reply were matched. - true } -fn get_opt_rcode(msg: &Message) -> OptRcode { - let opt = msg.opt(); - match opt { - Some(opt) => opt.rcode(msg.header()), - None => OptRcode::from_rcode(msg.header().rcode()), +pub struct UnorderedMultiMatcher<'a> { + entry: &'a Entry, + answers: std::vec::Vec, +} + +impl UnorderedMultiMatcher<'_> { + pub fn answer_records_left(&self) -> usize { + self.answers.len() + } + + pub fn match_msg( + &mut self, + msg: &Message, + ) -> Result<(), DidNotMatch> { + let e = &self.entry; + + e.match_flags(msg)?; + e.match_edns_data(msg)?; + e.match_opcode(msg)?; + e.match_question(msg)?; + e.match_rcode(msg)?; + + for msg_rr in msg.answer().unwrap() { + let msg_rr = msg_rr.unwrap(); + if msg_rr.rtype() == Rtype::OPT { + continue; + } + + if let Some(idx) = + self.entry.find_matching_record(&self.answers, &msg_rr) + { + // Delete the entry because only one record should match it + self.answers.swap_remove(idx); + } else { + // Nothing matches + trace!( + "no match for record {} {} {}", + msg_rr.owner(), + msg_rr.class(), + msg_rr.rtype(), + ); + return Err(DidNotMatch); + } + } + + e.match_authority(msg)?; + e.match_additional(msg)?; + + Ok(()) + } + + pub fn finish(self) -> Result<(), DidNotMatch> { + if self.answers.is_empty() { + Ok(()) + } else { + Err(DidNotMatch) + } } } diff --git a/src/stelline/mod.rs b/src/stelline/mod.rs index da5581f33..1428b09bf 100644 --- a/src/stelline/mod.rs +++ b/src/stelline/mod.rs @@ -1,3 +1,4 @@ +#![doc = include_str!("README.md")] #![cfg(feature = "unstable-stelline")] mod matches; diff --git a/src/stelline/parse_stelline.rs b/src/stelline/parse_stelline.rs index 4df5f34b2..b9946c3a6 100644 --- a/src/stelline/parse_stelline.rs +++ b/src/stelline/parse_stelline.rs @@ -1,3 +1,4 @@ +use core::str::SplitWhitespace; use std::default::Default; use std::fmt::Debug; use std::io::{self, BufRead, Read}; @@ -45,6 +46,8 @@ const STEP_TYPE_ASSIGN: &str = "ASSIGN"; const HEX_EDNSDATA_BEGIN: &str = "HEX_EDNSDATA_BEGIN"; const HEX_EDNSDATA_END: &str = "HEX_EDNSDATA_END"; +/// A section in a DNS message +#[derive(PartialEq, Eq)] enum Section { Question, Answer, @@ -52,6 +55,7 @@ enum Section { Additional, } +/// A type of step in a Stelline scenario #[derive(Clone, Debug)] pub enum StepType { Query, @@ -86,17 +90,23 @@ impl Config { } } +/// The main Stelline type containing the full configuration #[derive(Clone, Debug)] pub struct Stelline { + /// The name of the scenario pub name: String, + /// General configuration of the scenario pub config: Config, + /// The scenario to run pub scenario: Scenario, } + +/// Parse a `.rpl` file into a [`Stelline`] configuration pub fn parse_file( file: F, name: T, ) -> Stelline { - let mut lines = io::BufReader::new(file).lines(); + let mut lines = io::BufReader::new(file).lines().map(|l| l.unwrap()); Stelline { name: name.to_string(), config: parse_config(&mut lines), @@ -104,17 +114,18 @@ pub fn parse_file( } } -fn parse_config>>( - l: &mut Lines, -) -> Config { +/// Parse the configuration part of a `.rpl` file +/// +/// This consumes the iterator of lines until the `CONFIG END` token is +/// found. +fn parse_config>(l: &mut Lines) -> Config { let mut config: Config = Default::default(); loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()).trim(); + if clean_line.is_empty() { continue; } - let clean_line = clean_line.unwrap(); if clean_line == CONFIG_END { break; } @@ -129,22 +140,18 @@ pub struct Scenario { pub steps: Vec, } -pub fn parse_scenario< - Lines: Iterator>, ->( +pub fn parse_scenario>( l: &mut Lines, ) -> Scenario { let mut scenario: Scenario = Default::default(); // Find SCENARIO_BEGIN loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == SCENARIO_BEGIN { break; } @@ -154,14 +161,12 @@ pub fn parse_scenario< // Find RANGE_BEGIN, STEP, or SCENARIO_END loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == RANGE_BEGIN { scenario.ranges.push(parse_range(tokens, l)); continue; @@ -186,8 +191,8 @@ pub struct Range { pub entry: Vec, } -fn parse_range>>( - mut tokens: LineTokens<'_>, +fn parse_range>( + mut tokens: SplitWhitespace<'_>, l: &mut Lines, ) -> Range { let mut range: Range = Range { @@ -196,14 +201,12 @@ fn parse_range>>( ..Default::default() }; loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == ADDRESS { let addr_str = tokens.next().unwrap(); range.addr = Some(addr_str.parse().unwrap()); @@ -218,7 +221,6 @@ fn parse_range>>( } todo!(); } - //println!("parse_range: {:?}", range); range } @@ -230,28 +232,24 @@ pub struct Step { pub time_passes: Option, } -fn parse_step>>( - mut tokens: LineTokens<'_>, +fn parse_step>( + mut tokens: SplitWhitespace<'_>, l: &mut Lines, ) -> Step { let mut step_client_address = None; let mut step_key_name = None; let step_value = tokens.next().unwrap().parse::().unwrap(); let step_type_str = tokens.next().unwrap(); - let step_type = if step_type_str == STEP_TYPE_QUERY { - StepType::Query - } else if step_type_str == STEP_TYPE_CHECK_ANSWER { - StepType::CheckAnswer - } else if step_type_str == STEP_TYPE_TIME_PASSES { - StepType::TimePasses - } else if step_type_str == STEP_TYPE_TRAFFIC { - StepType::Traffic - } else if step_type_str == STEP_TYPE_CHECK_TEMPFILE { - StepType::CheckTempfile - } else if step_type_str == STEP_TYPE_ASSIGN { - StepType::Assign - } else { - todo!(); + let step_type = match step_type_str { + STEP_TYPE_QUERY => StepType::Query, + STEP_TYPE_CHECK_ANSWER => StepType::CheckAnswer, + STEP_TYPE_TIME_PASSES => StepType::TimePasses, + STEP_TYPE_TRAFFIC => StepType::Traffic, + STEP_TYPE_CHECK_TEMPFILE => StepType::CheckTempfile, + STEP_TYPE_ASSIGN => StepType::Assign, + _ => { + todo!(); + } }; let mut step = Step { step_value, @@ -319,20 +317,17 @@ fn parse_step>>( } loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == ENTRY_BEGIN { step.entry = Some(parse_entry(l)); let entry = step.entry.as_mut().unwrap(); entry.client_addr = step_client_address; entry.key_name = step_key_name; - //println!("parse_step: {:?}", step); return step; } todo!(); @@ -343,62 +338,64 @@ fn parse_step>>( pub struct Entry { pub client_addr: Option, pub key_name: Option, - pub matches: Option, - pub adjust: Option, + pub matches: Matches, + pub adjust: Adjust, pub opcode: Option, - pub reply: Option, - pub sections: Option, + pub reply: Reply, + pub sections: Sections, } -fn parse_entry>>( - l: &mut Lines, -) -> Entry { - let mut entry = Entry::default(); +fn parse_entry>(l: &mut Lines) -> Entry { + let mut opcode = None; + let mut matches = None; + let mut adjust = None; + let mut reply = None; + let mut sections = None; + loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == OPCODE { - entry.opcode = - Some(Opcode::from_str(tokens.next().unwrap()).unwrap()); + opcode = Some(Opcode::from_str(tokens.next().unwrap()).unwrap()); continue; } if token == MATCH { - entry.matches = Some(parse_match(tokens)); + matches = Some(parse_match(tokens)); continue; } if token == ADJUST { - entry.adjust = Some(parse_adjust(tokens)); + adjust = Some(parse_adjust(tokens)); continue; } if token == REPLY { - entry.reply = Some(parse_reply(tokens)); + reply = Some(parse_reply(tokens)); continue; } if token == SECTION { - let (sections, line) = parse_section(tokens, l); - //println!("parse_entry: sections {:?}", sections); - entry.sections = Some(sections); - let clean_line = get_clean_line(line.as_ref()); - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); - if token == ENTRY_END { - break; - } - todo!(); + sections = Some(parse_section(tokens, l)); + // the sections extend until the end of the entry, so we can + // break safely here. + break; } if token == ENTRY_END { break; } todo!("Unsupported token '{token}'"); } - entry + + Entry { + client_addr: Default::default(), + key_name: Default::default(), + matches: matches.unwrap_or_default(), + adjust: adjust.unwrap_or_default(), + reply: reply.unwrap_or_default(), + opcode, + sections: sections.expect("at least one section must be given"), + } } #[derive(Clone, Debug, Default)] @@ -429,10 +426,10 @@ impl Default for Sections { pub type Name = base::Name; pub type Question = base::Question; -fn parse_section>>( - mut tokens: LineTokens<'_>, +fn parse_section>( + mut tokens: SplitWhitespace<'_>, l: &mut Lines, -) -> (Sections, String) { +) -> Sections { let mut sections = Sections::default(); let next = tokens.next().unwrap(); let mut answer_idx = 0; @@ -444,32 +441,26 @@ fn parse_section>>( }; // Should extract which section loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()).trim(); + let mut tokens = clean_line.split_whitespace(); + let Some(token) = tokens.next() else { continue; - } - let clean_line = clean_line.unwrap(); - let mut tokens = LineTokens::new(clean_line); - let token = tokens.next().unwrap(); + }; if token == SECTION { let next = tokens.next().unwrap(); - section = if next == QUESTION { - Section::Question - } else if next == ANSWER { - Section::Answer - } else if next == AUTHORITY { - Section::Authority - } else if next == ADDITIONAL { - Section::Additional - } else { - panic!("Bad section {next}"); + section = match next { + QUESTION => Section::Question, + ANSWER => Section::Answer, + AUTHORITY => Section::Authority, + ADDITIONAL => Section::Additional, + _ => panic!("Bad section {next}"), }; origin = ".".to_string(); continue; } if token == ENTRY_END { - return (sections, line); + return sections; } if token == EXTRA_PACKET { answer_idx += 1; @@ -483,16 +474,15 @@ fn parse_section>>( .push(Question::from_str(clean_line).unwrap()); } Section::Answer | Section::Authority | Section::Additional => { - if matches!(section, Section::Additional) + if section == Section::Additional && clean_line == HEX_EDNSDATA_BEGIN { loop { - let line = l.next().unwrap().unwrap(); - let clean_line = get_clean_line(line.as_ref()); - if clean_line.is_none() { + let line = l.next().unwrap(); + let clean_line = remove_comment(line.as_ref()).trim(); + if clean_line.is_empty() { continue; } - let clean_line = clean_line.unwrap(); if clean_line == HEX_EDNSDATA_END { break; } @@ -512,7 +502,7 @@ fn parse_section>>( origin = new_origin.to_string(); } } else { - let mut zonefile = Zonefile::new(); + let mut zonefile = Zonefile::new().allow_invalid(); zonefile.extend_from_slice( format!("$ORIGIN {origin}\n").as_bytes(), ); @@ -553,7 +543,6 @@ fn parse_section>>( #[derive(Clone, Debug, Default)] pub struct Matches { pub additional: bool, - pub all: bool, pub answer: bool, pub authority: bool, pub ad: bool, @@ -564,7 +553,6 @@ pub struct Matches { pub opcode: bool, pub qname: bool, pub qtype: bool, - pub question: bool, pub rcode: bool, pub subdomain: bool, pub tcp: bool, @@ -578,68 +566,43 @@ pub struct Matches { pub any_answer: bool, } -fn parse_match(mut tokens: LineTokens<'_>) -> Matches { +fn parse_match(tokens: SplitWhitespace<'_>) -> Matches { let mut matches: Matches = Default::default(); - loop { - let token = match tokens.next() { - None => return matches, - Some(token) => token, - }; - - if token == "all" { - matches.all = true; - } else if token == "AD" { - matches.ad = true; - } else if token == "additional" { - matches.additional = true; - } else if token == "answer" { - matches.answer = true; - } else if token == "authority" { - matches.authority = true; - } else if token == "CD" { - matches.cd = true; - } else if token == "DO" { - matches.fl_do = true; - } else if token == "RD" { - matches.rd = true; - } else if token == "opcode" { - matches.opcode = true; - } else if token == "flags" { - matches.flags = true; - } else if token == "qname" { - matches.qname = true; - } else if token == "question" { - matches.question = true; - } else if token == "qtype" { - matches.qtype = true; - } else if token == "rcode" { - matches.rcode = true; - } else if token == "subdomain" { - matches.subdomain = true; - } else if token == "TCP" { - matches.tcp = true; - } else if token == "ttl" { - matches.ttl = true; - } else if token == "UDP" { - matches.tcp = false; - } else if token == "server_cookie" { - matches.server_cookie = true; - } else if token == "ednsdata" { - matches.edns_data = true; - } else if token == "MOCK_CLIENT" { - matches.mock_client = true; - } else if token == "CONNECTION_CLOSED" { - matches.conn_closed = true; - } else if token == "EXTRA_PACKETS" { - matches.extra_packets = true; - } else if token == "ANY_ANSWER" { - matches.any_answer = true; - } else { - println!("should handle match {token:?}"); - todo!(); + for token in tokens { + match token { + "all" => matches.set_all(), + "AD" => matches.ad = true, + "additional" => matches.additional = true, + "answer" => matches.answer = true, + "authority" => matches.authority = true, + "CD" => matches.cd = true, + "DO" => matches.fl_do = true, + "RD" => matches.rd = true, + "opcode" => matches.opcode = true, + "flags" => matches.flags = true, + "qname" => matches.qname = true, + "question" => matches.set_question(), + "qtype" => matches.qtype = true, + "rcode" => matches.rcode = true, + "subdomain" => matches.subdomain = true, + "TCP" => matches.tcp = true, + "ttl" => matches.ttl = true, + "UDP" => matches.tcp = false, + "server_cookie" => matches.server_cookie = true, + "ednsdata" => matches.edns_data = true, + "MOCK_CLIENT" => matches.mock_client = true, + "CONNECTION_CLOSED" => matches.conn_closed = true, + "EXTRA_PACKETS" => matches.extra_packets = true, + "ANY_ANSWER" => matches.any_answer = true, + _ => { + println!("should handle match {token:?}"); + todo!(); + } } } + + matches } #[derive(Clone, Debug, Default)] @@ -648,24 +611,21 @@ pub struct Adjust { pub copy_query: bool, } -fn parse_adjust(mut tokens: LineTokens<'_>) -> Adjust { +fn parse_adjust(tokens: SplitWhitespace<'_>) -> Adjust { let mut adjust: Adjust = Default::default(); - loop { - let token = match tokens.next() { - None => return adjust, - Some(token) => token, - }; - - if token == "copy_id" { - adjust.copy_id = true; - } else if token == "copy_query" { - adjust.copy_query = true; - } else { - println!("should handle adjust {token:?}"); - todo!(); + for token in tokens { + match token { + "copy_id" => adjust.copy_id = true, + "copy_query" => adjust.copy_query = true, + token => { + println!("should handle adjust {token:?}"); + todo!(); + } } } + + adjust } #[derive(Clone, Debug, Default)] @@ -688,103 +648,40 @@ pub struct Reply { pub notify: bool, } -fn parse_reply(mut tokens: LineTokens<'_>) -> Reply { +/// Parse the reply section +fn parse_reply(tokens: SplitWhitespace<'_>) -> Reply { let mut reply: Reply = Default::default(); - loop { - let token = match tokens.next() { - None => return reply, - Some(token) => token, - }; - - if token == "AA" { - reply.aa = true; - } else if token == "AD" { - reply.ad = true; - } else if token == "CD" { - reply.cd = true; - } else if token == "DO" { - reply.fl_do = true; - } else if token == "QR" { - reply.qr = true; - } else if token == "RA" { - reply.ra = true; - } else if token == "RD" { - reply.rd = true; - } else if token == "TC" { - reply.tc = true; - } else if let Ok(rcode) = token.parse() { + for token in tokens { + if let Ok(rcode) = token.parse() { reply.rcode = Some(rcode); - } else if token == "NOTIFY" { - reply.notify = true; - } else { - println!("should handle reply {token:?}"); - todo!(); + continue; } - } -} - -fn get_clean_line(line: &str) -> Option<&str> { - //println!("get clean line for {:?}", line); - let opt_comment = line.find(';'); - let line = if let Some(index) = opt_comment { - &line[0..index] - } else { - line - }; - let trimmed = line.trim(); - //println!("line after trim() {:?}", trimmed); - - if trimmed.is_empty() { - None - } else { - Some(trimmed) + match token { + "AA" => reply.aa = true, + "AD" => reply.ad = true, + "CD" => reply.cd = true, + "DO" => reply.fl_do = true, + "QR" => reply.qr = true, + "RA" => reply.ra = true, + "RD" => reply.rd = true, + "TC" => reply.tc = true, + "NOTIFY" => reply.notify = true, + token => { + println!("should handle reply {token:?}"); + todo!(); + } + } } -} - -struct LineTokens<'a> { - str: &'a str, - curr_index: usize, -} -impl<'a> LineTokens<'a> { - fn new(str: &'a str) -> Self { - Self { str, curr_index: 0 } - } + reply } -impl<'a> Iterator for LineTokens<'a> { - type Item = &'a str; - fn next(&mut self) -> Option { - let cur_str = &self.str[self.curr_index..]; - - if cur_str.is_empty() { - return None; - } - - // Assume cur_str starts with a token - for (index, char) in cur_str.char_indices() { - if !char.is_whitespace() { - continue; - } - let start_index = self.curr_index; - let end_index = start_index + index; - - let space_str = &self.str[end_index..]; - - for (index, char) in space_str.char_indices() { - if char.is_whitespace() { - continue; - } - - self.curr_index = end_index + index; - return Some(&self.str[start_index..end_index]); - } - - todo!(); - } - self.curr_index = self.str.len(); - Some(cur_str) +/// Remove a comment from the line if present +fn remove_comment(line: &str) -> &str { + match line.split_once(';') { + Some((before_comment, _)) => before_comment, + None => line, } } diff --git a/src/stelline/server.rs b/src/stelline/server.rs index b9d06da9d..300c93b68 100644 --- a/src/stelline/server.rs +++ b/src/stelline/server.rs @@ -11,9 +11,8 @@ use crate::dep::octseq::Octets; use crate::zonefile::inplace::Entry as ZonefileEntry; use super::client::CurrStepValue; -use super::matches::match_msg; use super::parse_stelline; -use super::parse_stelline::{Adjust, Reply, Stelline}; +use super::parse_stelline::Stelline; pub fn do_server<'a, Oct, Target>( msg: &'a Message, @@ -47,7 +46,7 @@ where continue; } for entry in &range.entry { - if match_msg(entry, msg, false) { + if entry.match_msg(msg).is_ok() { trace!("Match found"); opt_entry = Some(entry); } @@ -76,15 +75,11 @@ where Target: Composer + Default + OctetsBuilder + Truncate, ::AppendError: Debug, { - let sections = entry.sections.as_ref().unwrap(); - let adjust: Adjust = match &entry.adjust { - Some(adjust) => adjust.clone(), - None => Default::default(), - }; + let sections = &entry.sections; let mut msg = MessageBuilder::from_target(Target::default()) .unwrap() .question(); - if adjust.copy_query { + if entry.adjust.copy_query { for q in reqmsg.question() { msg.push(q.unwrap()).unwrap(); } @@ -120,10 +115,7 @@ where }; msg.push(rec).unwrap(); } - let reply: Reply = match &entry.reply { - Some(reply) => reply.clone(), - None => Default::default(), - }; + let reply = &entry.reply; let header = msg.header_mut(); header.set_aa(reply.aa); header.set_ad(reply.ad); @@ -137,7 +129,7 @@ where if reply.notify { header.set_opcode(Opcode::NOTIFY); } - if adjust.copy_id { + if entry.adjust.copy_id { header.set_id(reqmsg.header().id()); } else { todo!(); diff --git a/src/utils/dst.rs b/src/utils/dst.rs new file mode 100644 index 000000000..cc47a581a --- /dev/null +++ b/src/utils/dst.rs @@ -0,0 +1,447 @@ +//! Working with dynamically sized types (DSTs). +//! +//! DSTs are types whose size is known at run-time instead of compile-time. +//! The primary examples of this are slices and [`str`]. While Rust provides +//! relatively good support for DSTs (e.g. they can be held by reference like +//! any other type), it has some rough edges. The standard library tries to +//! paper over these with helpful functions and trait impls, but it does not +//! account for custom DST types. In particular, [`new::base`] introduces a +//! large number of user-facing DSTs and needs to paper over the same rough +//! edges for all of them. +//! +//! [`new::base`]: crate::new::base +//! +//! ## Coping DSTs +//! +//! Because DSTs cannot be held by value, they must be handled and manipulated +//! through an indirection (a reference or a smart pointer of some kind). +//! Copying a DST into new container (e.g. [`Box`]) requires explicit support +//! from that container type. +//! +//! [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +//! +//! This module introduces the [`UnsizedCopy`] trait (and a derive macro) that +//! types like [`str`] implement. Container types that can support copying +//! DSTs implement [`UnsizedCopyFrom`]. +// +// TODO: Example + +//----------- UnsizedCopy ---------------------------------------------------- + +/// An extension of [`Copy`] to dynamically sized types. +/// +/// This is a generalization of [`Copy`]. It is intended to simplify working +/// with DSTs that support zero-copy parsing techniques (as these are built +/// from byte sequences, they are inherently trivial to copy). +/// +/// # Usage +/// +/// To copy a type, call [`UnsizedCopy::unsized_copy_into()`] on the DST being +/// copied, or call [`UnsizedCopyFrom::unsized_copy_from()`] on the container +/// type to copy into. The two function identically. +/// +#[cfg_attr( + feature = "bumpalo", + doc = "The [`copy_to_bump()`] function is useful for copying data into [`bumpalo`]-based allocations." +)] +/// +/// # Safety +/// +/// A type `T` can implement `UnsizedCopy` if all of the following hold: +/// +/// - It is an aggregate type (`struct`, `enum`, or `union`) and every field +/// implements [`UnsizedCopy`]. +/// +/// - `T::Alignment` has exactly the same alignment as `T`. +/// +/// - `T::ptr_with_addr()` satisfies the documented invariants. +pub unsafe trait UnsizedCopy { + /// Copy `self` into a new container. + /// + /// A new container of the specified type (which is usually inferred) is + /// allocated, and the contents of `self` are copied into it. This is a + /// convenience method that calls [`unsized_copy_from()`]. + /// + /// [`unsized_copy_from()`]: UnsizedCopyFrom::unsized_copy_from(). + #[inline] + fn unsized_copy_into>(&self) -> T { + T::unsized_copy_from(self) + } + + /// Copy `self` and return it by value. + /// + /// This offers equivalent functionality to the regular [`Copy`] trait, + /// which is also why it has the same [`Sized`] bound. + #[inline] + fn copy(&self) -> Self + where + Self: Sized, + { + // The compiler can't tell that 'Self' is 'Copy', so we're just going + // to copy it manually. Hopefully this optimizes fine. + + // SAFETY: 'self' is a valid reference, and is thus safe for reads. + unsafe { core::ptr::read(self) } + } + + /// A type with the same alignment as `Self`. + /// + /// At the moment, Rust does not provide a way to determine the alignment + /// of a dynamically sized type at compile-time. This restriction exists + /// because trait objects (which count as DSTs, but are not supported by + /// [`UnsizedCopy`]) have an alignment determined by their implementation + /// (which can vary at runtime). + /// + /// This associated type papers over this limitation, by simply requiring + /// every implementation of [`UnsizedCopy`] to specify a type with the + /// same alignment here. This is used by internal plumbing code to know + /// the alignment of `Self` at compile-time. + /// + /// ## Invariants + /// + /// The alignment of `Self::Alignment` must be the same as that of `Self`. + type Alignment: Sized; + + /// Change the address of a pointer to `Self`. + /// + /// `Self` may be a DST, which means that references (and pointers) to it + /// store metadata alongside the usual memory address. For example, the + /// metadata for a slice type is its length. In order to construct a new + /// instance of `Self` (as is done by copying), a new pointer must be + /// created, and the appropriate metadata must be inserted. + /// + /// At the moment, Rust does not provide a way to examine this metadata + /// for an arbitrary type. This method papers over this limitation, and + /// provides a way to copy the metadata from an existing pointer while + /// changing the pointer address. + /// + /// # Implementing + /// + /// Most users will derive [`UnsizedCopy`] and so don't need to worry + /// about this. In any case, when Rust builds in support for extracting + /// metadata, this function will gain a default implementation, and will + /// eventually be deprecated. + /// + /// For manual implementations for unsized types: + /// + /// ```no_run + /// # use domain::utils::dst::UnsizedCopy; + /// # + /// pub struct Foo { + /// a: i32, + /// b: [u8], + /// } + /// + /// unsafe impl UnsizedCopy for Foo { + /// // We would like to write 'Alignment = Self' here, but we can't + /// // because 'Self' is not 'Sized'. However, 'Self' is a 'struct' + /// // using 'repr(Rust)'; the following tuple (which implicitly also + /// // uses 'repr(Rust)') has the same alignment as it. + /// type Alignment = (i32, u8); + /// + /// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + /// // Delegate to the same function on the last field. + /// // + /// // Rust knows that 'Self' has the same metadata as '[u8]', + /// // and so permits casting pointers between those types. + /// self.b.ptr_with_addr(addr) as *const Self + /// } + /// } + /// ``` + /// + /// For manual implementations for sized types: + /// + /// ```no_run + /// # use domain::utils::dst::UnsizedCopy; + /// # + /// pub struct Foo { + /// a: i32, + /// b: Option, + /// } + /// + /// unsafe impl UnsizedCopy for Foo { + /// // Because 'Foo' is a sized type, we can use it here directly. + /// type Alignment = Self; + /// + /// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + /// // Since 'Self' is 'Sized', there is no metadata. + /// addr.cast::() + /// } + /// } + /// ``` + /// + /// # Invariants + /// + /// For the statement `let result = Self::ptr_with_addr(ptr, addr);`, the + /// following always hold: + /// + /// - `result as usize == addr as usize`. + /// - `core::ptr::metadata(result) == core::ptr::metadata(ptr)`. + /// + /// It is undefined behaviour for an implementation of [`UnsizedCopy`] to + /// break these invariants. + fn ptr_with_addr(&self, addr: *const ()) -> *const Self; +} + +/// Deriving [`UnsizedCopy`] automatically. +/// +/// [`UnsizedCopy`] can be derived on any aggregate type. `enum`s and +/// `union`s are inherently [`Sized`] types, and [`UnsizedCopy`] will simply +/// require every field to implement [`Copy`] on them. For `struct`s, all but +/// the last field need to implement [`Copy`]; the last field needs to +/// implement [`UnsizedCopy`]. +/// +/// Here's a simple example: +/// +/// ```no_run +/// # use domain::utils::dst::UnsizedCopy; +/// struct Foo { +/// a: u32, +/// b: Bar, +/// } +/// +/// # struct Bar { data: T } +/// +/// // The generated impl with 'derive(UnsizedCopy)': +/// unsafe impl UnsizedCopy for Foo +/// where +/// u32: Copy, +/// Bar: UnsizedCopy, +/// { +/// // This type has the same alignment as 'Foo'. +/// type Alignment = (u32, as UnsizedCopy>::Alignment); +/// +/// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { +/// self.b.ptr_with_addr(addr) as *const Self +/// } +/// } +/// ``` +pub use domain_macros::UnsizedCopy; + +macro_rules! impl_primitive_unsized_copy { + ($($type:ty),+) => { + $(unsafe impl UnsizedCopy for $type { + type Alignment = Self; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } + })+ + }; +} + +impl_primitive_unsized_copy!((), bool, char); +impl_primitive_unsized_copy!(u8, u16, u32, u64, u128, usize); +impl_primitive_unsized_copy!(i8, i16, i32, i64, i128, isize); +impl_primitive_unsized_copy!(f32, f64); + +unsafe impl UnsizedCopy for &T { + type Alignment = Self; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } +} + +unsafe impl UnsizedCopy for str { + // 'str' has no alignment. + type Alignment = u8; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + // NOTE: The Rust Reference indicates that 'str' has the same layout + // as '[u8]' [1]. This is also the most natural layout for it. Since + // there's no way to construct a '*const str' from raw parts, we will + // just construct a raw slice and transmute it. + // + // [1]: https://doc.rust-lang.org/reference/type-layout.html#str-layout + + self.as_bytes().ptr_with_addr(addr) as *const Self + } +} + +unsafe impl UnsizedCopy for [T] { + type Alignment = T; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + core::ptr::slice_from_raw_parts(addr.cast::(), self.len()) + } +} + +unsafe impl UnsizedCopy for [T; N] { + type Alignment = T; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } +} + +macro_rules! impl_unsized_copy_tuple { + ($($type:ident),*; $last:ident) => { + unsafe impl<$($type: Copy,)* $last: ?Sized + UnsizedCopy> + UnsizedCopy for ($($type,)* $last,) { + type Alignment = ($($type,)* <$last>::Alignment,); + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + let (.., last) = self; + last.ptr_with_addr(addr) as *const Self + } + } + }; +} + +impl_unsized_copy_tuple!(; A); +impl_unsized_copy_tuple!(A; B); +impl_unsized_copy_tuple!(A, B; C); +impl_unsized_copy_tuple!(A, B, C; D); +impl_unsized_copy_tuple!(A, B, C, D; E); +impl_unsized_copy_tuple!(A, B, C, D, E; F); +impl_unsized_copy_tuple!(A, B, C, D, E, F; G); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G; H); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H; I); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I; J); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I, J; K); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I, J, K; L); + +//----------- UnsizedCopyFrom ------------------------------------------------ + +/// A container type that can be copied into. +pub trait UnsizedCopyFrom: Sized { + /// The source type to copy from. + type Source: ?Sized + UnsizedCopy; + + /// Create a new `Self` by copying the given value. + fn unsized_copy_from(value: &Self::Source) -> Self; +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::boxed::Box { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use std::alloc; + + let layout = alloc::Layout::for_value(value); + let ptr = unsafe { alloc::alloc(layout) }; + if ptr.is_null() { + alloc::handle_alloc_error(layout); + } + let src = value as *const _ as *const u8; + unsafe { core::ptr::copy_nonoverlapping(src, ptr, layout.size()) }; + let ptr = value.ptr_with_addr(ptr.cast()).cast_mut(); + unsafe { std::boxed::Box::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::rc::Rc { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use core::mem::MaybeUninit; + + /// A [`u8`] with a custom alignment. + #[derive(Copy, Clone)] + #[repr(C)] + struct AlignedU8([T; 0], u8); + + // TODO(1.82): Use 'Rc::new_uninit_slice()'. + // 'impl FromIterator for Rc' describes performance characteristics. + // For efficiency, the iterator should implement 'TrustedLen', which + // is (currently) a nightly-only trait. However, we can use the + // existing 'std' types which happen to implement it. + let size = core::mem::size_of_val(value); + let rc: std::rc::Rc<[MaybeUninit>]> = + (0..size).map(|_| MaybeUninit::uninit()).collect(); + + let src = value as *const _ as *const u8; + let dst = std::rc::Rc::into_raw(rc).cast_mut(); + // SAFETY: 'rc' was just constructed and has never been copied. Thus, + // its contents can be mutated without violating any references. + unsafe { core::ptr::copy_nonoverlapping(src, dst.cast(), size) }; + + let ptr = value.ptr_with_addr(dst.cast()); + unsafe { std::rc::Rc::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::sync::Arc { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use core::mem::MaybeUninit; + + /// A [`u8`] with a custom alignment. + #[derive(Copy, Clone)] + #[repr(C)] + struct AlignedU8([T; 0], u8); + + // TODO(1.82): Use 'Arc::new_uninit_slice()'. + // 'impl FromIterator for Arc' describes performance characteristics. + // For efficiency, the iterator should implement 'TrustedLen', which + // is (currently) a nightly-only trait. However, we can use the + // existing 'std' types which happen to implement it. + let size = core::mem::size_of_val(value); + let arc: std::sync::Arc<[MaybeUninit>]> = + (0..size).map(|_| MaybeUninit::uninit()).collect(); + + let src = value as *const _ as *const u8; + let dst = std::sync::Arc::into_raw(arc).cast_mut(); + // SAFETY: 'arc' was just constructed and has never been copied. Thus, + // its contents can be mutated without violating any references. + unsafe { core::ptr::copy_nonoverlapping(src, dst.cast(), size) }; + + let ptr = value.ptr_with_addr(dst.cast()); + unsafe { std::sync::Arc::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::vec::Vec { + type Source = [T]; + + fn unsized_copy_from(value: &Self::Source) -> Self { + // We can't use 'impl From<&[T]> for Vec', because that requires + // 'T' to implement 'Clone'. We could reuse the 'UnsizedCopyFrom' + // impl for 'Box', but a manual implementation is probably better. + + let mut this = Self::with_capacity(value.len()); + let src = value.as_ptr(); + let dst = this.spare_capacity_mut() as *mut _ as *mut T; + unsafe { core::ptr::copy_nonoverlapping(src, dst, value.len()) }; + // SAFETY: The first 'value.len()' elements are now initialized. + unsafe { this.set_len(value.len()) }; + this + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::string::String { + type Source = str; + + fn unsized_copy_from(value: &Self::Source) -> Self { + value.into() + } +} + +//----------- copy_to_bump --------------------------------------------------- + +/// Copy a value into a [`Bump`] allocator. +/// +/// This works with [`UnsizedCopy`] values, which extends [`Bump`]'s native +/// functionality. +/// +/// [`Bump`]: bumpalo::Bump +#[cfg(feature = "bumpalo")] +#[allow(clippy::mut_from_ref)] // using a memory allocator +pub fn copy_to_bump<'a, T: ?Sized + UnsizedCopy>( + value: &T, + bump: &'a bumpalo::Bump, +) -> &'a mut T { + let layout = std::alloc::Layout::for_value(value); + let ptr = bump.alloc_layout(layout).as_ptr(); + let src = value as *const _ as *const u8; + unsafe { core::ptr::copy_nonoverlapping(src, ptr, layout.size()) }; + let ptr = value.ptr_with_addr(ptr.cast()).cast_mut(); + unsafe { &mut *ptr } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 584724715..a9ae063c2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,5 +4,7 @@ pub mod base16; pub mod base32; pub mod base64; +pub mod dst; + #[cfg(feature = "net")] pub(crate) mod config; diff --git a/src/validate.rs b/src/validate.rs deleted file mode 100644 index 4a60876ae..000000000 --- a/src/validate.rs +++ /dev/null @@ -1,1881 +0,0 @@ -//! DNSSEC validation. -//! -//! **This module is experimental and likely to change significantly.** -#![cfg(feature = "unstable-validate")] -#![cfg_attr(docsrs, doc(cfg(feature = "unstable-validate")))] -use std::boxed::Box; -use std::vec::Vec; -use std::{error, fmt}; - -use bytes::Bytes; -use octseq::builder::with_infallible; -use octseq::{EmptyBuilder, FromBuilder, OctetsBuilder, Truncate}; -use ring::digest::SHA1_FOR_LEGACY_USE_ONLY; -use ring::{digest, signature}; - -use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{Class, DigestAlg, Nsec3HashAlg, SecAlg}; -use crate::base::name::Name; -use crate::base::name::ToName; -use crate::base::rdata::{ComposeRecordData, RecordData}; -use crate::base::record::Record; -use crate::base::scan::{IterScanner, Scanner}; -use crate::base::wire::{Compose, Composer}; -use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt}; -use crate::base::Rtype; -use crate::rdata::nsec3::{Nsec3Salt, OwnerHash}; -use crate::rdata::{Dnskey, Ds, Nsec3param, Rrsig}; - -//----------- Key ------------------------------------------------------------ - -/// A DNSSEC key for a particular zone. -/// -/// # Serialization -/// -/// Keys can be parsed from or written in the conventional format used by the -/// BIND name server. This is a simplified version of the zonefile format. -/// -/// In this format, a public key is a line-oriented text file. Each line is -/// either blank (having only whitespace) or a single DNSKEY record in the -/// presentation format. In either case, the line may end with a comment (an -/// ASCII semicolon followed by arbitrary content until the end of the line). -/// The file must contain a single DNSKEY record line. -/// -/// The DNSKEY record line contains the following fields, separated by ASCII -/// whitespace: -/// -/// - The owner name. This is an absolute name ending with a dot. -/// - Optionally, the class of the record (usually `IN`). -/// - The record type (which must be `DNSKEY`). -/// - The DNSKEY record data, which has the following sub-fields: -/// - The key flags, which describe the key's uses. -/// - The protocol used (expected to be `3`). -/// - The key algorithm (see [`SecAlg`]). -/// - The public key encoded as a Base64 string. -#[derive(Clone)] -pub struct Key { - /// The owner of the key. - owner: Name, - - /// The flags associated with the key. - /// - /// These flags are stored in the DNSKEY record. - flags: u16, - - /// The public key, in bytes. - /// - /// This identifies the key and can be used for signatures. - key: PublicKeyBytes, -} - -//--- Construction - -impl Key { - /// Construct a new DNSSEC key manually. - pub fn new(owner: Name, flags: u16, key: PublicKeyBytes) -> Self { - Self { owner, flags, key } - } -} - -//--- Inspection - -impl Key { - /// The owner name attached to the key. - pub fn owner(&self) -> &Name { - &self.owner - } - - /// The flags attached to the key. - pub fn flags(&self) -> u16 { - self.flags - } - - /// The raw public key. - pub fn raw_public_key(&self) -> &PublicKeyBytes { - &self.key - } - - /// The signing algorithm used. - pub fn algorithm(&self) -> SecAlg { - self.key.algorithm() - } - - /// The size of this key, in bits. - pub fn key_size(&self) -> usize { - self.key.key_size() - } - - /// Whether this is a zone signing key. - /// - /// From [RFC 4034, section 2.1.1]: - /// - /// > Bit 7 of the Flags field is the Zone Key flag. If bit 7 has value - /// > 1, then the DNSKEY record holds a DNS zone key, and the DNSKEY RR's - /// > owner name MUST be the name of a zone. If bit 7 has value 0, then - /// > the DNSKEY record holds some other type of DNS public key and MUST - /// > NOT be used to verify RRSIGs that cover RRsets. - /// - /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 - pub fn is_zone_signing_key(&self) -> bool { - self.flags & (1 << 8) != 0 - } - - /// Whether this key has been revoked. - /// - /// From [RFC 5011, section 3]: - /// - /// > Bit 8 of the DNSKEY Flags field is designated as the 'REVOKE' flag. - /// > If this bit is set to '1', AND the resolver sees an RRSIG(DNSKEY) - /// > signed by the associated key, then the resolver MUST consider this - /// > key permanently invalid for all purposes except for validating the - /// > revocation. - /// - /// [RFC 5011, section 3]: https://datatracker.ietf.org/doc/html/rfc5011#section-3 - pub fn is_revoked(&self) -> bool { - self.flags & (1 << 7) != 0 - } - - /// Whether this is a secure entry point. - /// - /// From [RFC 4034, section 2.1.1]: - /// - /// > Bit 15 of the Flags field is the Secure Entry Point flag, described - /// > in [RFC3757]. If bit 15 has value 1, then the DNSKEY record holds a - /// > key intended for use as a secure entry point. This flag is only - /// > intended to be a hint to zone signing or debugging software as to - /// > the intended use of this DNSKEY record; validators MUST NOT alter - /// > their behavior during the signature validation process in any way - /// > based on the setting of this bit. This also means that a DNSKEY RR - /// > with the SEP bit set would also need the Zone Key flag set in order - /// > to be able to generate signatures legally. A DNSKEY RR with the SEP - /// > set and the Zone Key flag not set MUST NOT be used to verify RRSIGs - /// > that cover RRsets. - /// - /// [RFC 4034, section 2.1.1]: https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.1 - /// [RFC3757]: https://datatracker.ietf.org/doc/html/rfc3757 - pub fn is_secure_entry_point(&self) -> bool { - self.flags & 1 != 0 - } - - /// The key tag. - pub fn key_tag(&self) -> u16 { - // NOTE: RSA/MD5 uses a different algorithm. - - // NOTE: A u32 can fit the sum of 65537 u16s without overflowing. A - // key can never exceed 64KiB anyway, so we won't even get close to - // the limit. Let's just add into a u32 and normalize it after. - let mut res = 0u32; - - // Add basic DNSKEY fields. - res += self.flags as u32; - res += u16::from_be_bytes([3, self.algorithm().to_int()]) as u32; - - // Add the raw key tag from the public key. - res += self.key.raw_key_tag(); - - // Normalize and return the result. - (res as u16).wrapping_add((res >> 16) as u16) - } - - /// The digest of this key. - pub fn digest( - &self, - algorithm: DigestAlg, - ) -> Result>, DigestError> - where - Octs: AsRef<[u8]>, - { - let mut context = ring::digest::Context::new(match algorithm { - DigestAlg::SHA1 => &ring::digest::SHA1_FOR_LEGACY_USE_ONLY, - DigestAlg::SHA256 => &ring::digest::SHA256, - DigestAlg::SHA384 => &ring::digest::SHA384, - _ => return Err(DigestError::UnsupportedAlgorithm), - }); - - // Add the owner name. - if self - .owner - .as_slice() - .iter() - .any(|&b| b.is_ascii_uppercase()) - { - let mut owner = [0u8; 256]; - owner[..self.owner.len()].copy_from_slice(self.owner.as_slice()); - owner.make_ascii_lowercase(); - context.update(&owner[..self.owner.len()]); - } else { - context.update(self.owner.as_slice()); - } - - // Add basic DNSKEY fields. - context.update(&self.flags.to_be_bytes()); - context.update(&[3, self.algorithm().to_int()]); - - // Add the public key. - self.key.digest(&mut context); - - // Finalize the digest. - let digest = context.finish().as_ref().into(); - Ok(Ds::new(self.key_tag(), self.algorithm(), algorithm, digest) - .unwrap()) - } -} - -//--- Conversion to and from DNSKEYs - -impl> Key { - /// Deserialize a key from DNSKEY record data. - /// - /// # Errors - /// - /// Fails if the DNSKEY uses an unknown protocol or contains an invalid - /// public key (e.g. one of the wrong size for the signature algorithm). - pub fn from_dnskey( - owner: Name, - dnskey: Dnskey, - ) -> Result { - if dnskey.protocol() != 3 { - return Err(FromDnskeyError::UnsupportedProtocol); - } - - let flags = dnskey.flags(); - let algorithm = dnskey.algorithm(); - let key = dnskey.public_key().as_ref(); - let key = PublicKeyBytes::from_dnskey_format(algorithm, key)?; - Ok(Self { owner, flags, key }) - } - - /// Serialize the key into DNSKEY record data. - /// - /// The owner name can be combined with the returned record to serialize a - /// complete DNS record if necessary. - pub fn to_dnskey(&self) -> Dnskey> { - Dnskey::new( - self.flags, - 3, - self.key.algorithm(), - self.key.to_dnskey_format(), - ) - .expect("long public key") - } - - /// Parse a DNSSEC key from the conventional format used by BIND. - /// - /// See the type-level documentation for a description of this format. - pub fn parse_from_bind(data: &str) -> Result - where - Octs: FromBuilder, - Octs::Builder: EmptyBuilder + Composer, - { - /// Find the next non-blank line in the file. - fn next_line(mut data: &str) -> Option<(&str, &str)> { - let mut line; - while !data.is_empty() { - (line, data) = - data.trim_start().split_once('\n').unwrap_or((data, "")); - if !line.is_empty() && !line.starts_with(';') { - // We found a line that does not start with a comment. - line = line - .split_once(';') - .map_or(line, |(line, _)| line) - .trim_end(); - return Some((line, data)); - } - } - - None - } - - // Ensure there is a single DNSKEY record line in the input. - let (line, rest) = - next_line(data).ok_or(ParseDnskeyTextError::Misformatted)?; - if next_line(rest).is_some() { - return Err(ParseDnskeyTextError::Misformatted); - } - - // Parse the entire record. - let mut scanner = IterScanner::new(line.split_ascii_whitespace()); - - let name = scanner - .scan_name() - .map_err(|_| ParseDnskeyTextError::Misformatted)?; - - let _ = Class::scan(&mut scanner) - .map_err(|_| ParseDnskeyTextError::Misformatted)?; - - if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { - return Err(ParseDnskeyTextError::Misformatted); - } - - let data = Dnskey::scan(&mut scanner) - .map_err(|_| ParseDnskeyTextError::Misformatted)?; - - Self::from_dnskey(name, data) - .map_err(ParseDnskeyTextError::FromDnskey) - } - - /// Serialize this key in the conventional format used by BIND. - /// - /// See the type-level documentation for a description of this format. - pub fn format_as_bind(&self, mut w: impl fmt::Write) -> fmt::Result { - writeln!( - w, - "{} IN DNSKEY {}", - self.owner().fmt_with_dot(), - self.to_dnskey().display_zonefile(DisplayKind::Simple), - ) - } - - /// Display this key in the conventional format used by BIND. - /// - /// See the type-level documentation for a description of this format. - pub fn display_as_bind(&self) -> impl fmt::Display + '_ { - struct Display<'a, Octs>(&'a Key); - impl> fmt::Display for Display<'_, Octs> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.format_as_bind(f) - } - } - Display(self) - } -} - -//--- Comparison - -impl> PartialEq for Key { - fn eq(&self, other: &Self) -> bool { - self.owner() == other.owner() - && self.flags() == other.flags() - && self.raw_public_key() == other.raw_public_key() - } -} - -//--- Debug - -impl> fmt::Debug for Key { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Key") - .field("owner", self.owner()) - .field("flags", &self.flags()) - .field("raw_public_key", self.raw_public_key()) - .finish() - } -} - -//----------- RsaPublicKeyBytes ---------------------------------------------- - -/// A low-level public key. -#[derive(Clone, Debug)] -pub enum PublicKeyBytes { - /// An RSA/SHA-1 public key. - RsaSha1(RsaPublicKeyBytes), - - /// An RSA/SHA-1 with NSEC3 public key. - RsaSha1Nsec3Sha1(RsaPublicKeyBytes), - - /// An RSA/SHA-256 public key. - RsaSha256(RsaPublicKeyBytes), - - /// An RSA/SHA-512 public key. - RsaSha512(RsaPublicKeyBytes), - - /// An ECDSA P-256/SHA-256 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (32 bytes). - /// - The encoding of the `y` coordinate (32 bytes). - EcdsaP256Sha256(Box<[u8; 65]>), - - /// An ECDSA P-384/SHA-384 public key. - /// - /// The public key is stored in uncompressed format: - /// - /// - A single byte containing the value 0x04. - /// - The encoding of the `x` coordinate (48 bytes). - /// - The encoding of the `y` coordinate (48 bytes). - EcdsaP384Sha384(Box<[u8; 97]>), - - /// An Ed25519 public key. - /// - /// The public key is a 32-byte encoding of the public point. - Ed25519(Box<[u8; 32]>), - - /// An Ed448 public key. - /// - /// The public key is a 57-byte encoding of the public point. - Ed448(Box<[u8; 57]>), -} - -//--- Inspection - -impl PublicKeyBytes { - /// The algorithm used by this key. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } - - /// The size of this key, in bits. - /// - /// For RSA keys, this measures the size of the public modulus. For all - /// other algorithms, it is the size of the fixed-width public key. - pub fn key_size(&self) -> usize { - match self { - Self::RsaSha1(k) - | Self::RsaSha1Nsec3Sha1(k) - | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.key_size(), - - // ECDSA public keys have a marker byte and two points. - Self::EcdsaP256Sha256(k) => (k.len() - 1) / 2 * 8, - Self::EcdsaP384Sha384(k) => (k.len() - 1) / 2 * 8, - - // EdDSA public key sizes are measured in encoded form. - Self::Ed25519(k) => k.len() * 8, - Self::Ed448(k) => k.len() * 8, - } - } - - /// The raw key tag computation for this value. - fn raw_key_tag(&self) -> u32 { - fn compute(data: &[u8]) -> u32 { - data.chunks(2) - .map(|chunk| { - let mut buf = [0u8; 2]; - // A 0 byte is appended for an incomplete chunk. - buf[..chunk.len()].copy_from_slice(chunk); - u16::from_be_bytes(buf) as u32 - }) - .sum() - } - - match self { - Self::RsaSha1(k) - | Self::RsaSha1Nsec3Sha1(k) - | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.raw_key_tag(), - - Self::EcdsaP256Sha256(k) => compute(&k[1..]), - Self::EcdsaP384Sha384(k) => compute(&k[1..]), - Self::Ed25519(k) => compute(&**k), - Self::Ed448(k) => compute(&**k), - } - } - - /// Compute a digest of this public key. - fn digest(&self, context: &mut ring::digest::Context) { - match self { - Self::RsaSha1(k) - | Self::RsaSha1Nsec3Sha1(k) - | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.digest(context), - - Self::EcdsaP256Sha256(k) => context.update(&k[1..]), - Self::EcdsaP384Sha384(k) => context.update(&k[1..]), - Self::Ed25519(k) => context.update(&**k), - Self::Ed448(k) => context.update(&**k), - } - } -} - -//--- Conversion to and from DNSKEYs - -impl PublicKeyBytes { - /// Parse a public key as stored in a DNSKEY record. - pub fn from_dnskey_format( - algorithm: SecAlg, - data: &[u8], - ) -> Result { - match algorithm { - SecAlg::RSASHA1 => { - RsaPublicKeyBytes::from_dnskey_format(data).map(Self::RsaSha1) - } - SecAlg::RSASHA1_NSEC3_SHA1 => { - RsaPublicKeyBytes::from_dnskey_format(data) - .map(Self::RsaSha1Nsec3Sha1) - } - SecAlg::RSASHA256 => RsaPublicKeyBytes::from_dnskey_format(data) - .map(Self::RsaSha256), - SecAlg::RSASHA512 => RsaPublicKeyBytes::from_dnskey_format(data) - .map(Self::RsaSha512), - - SecAlg::ECDSAP256SHA256 => { - let mut key = Box::new([0u8; 65]); - if key.len() == 1 + data.len() { - key[0] = 0x04; - key[1..].copy_from_slice(data); - Ok(Self::EcdsaP256Sha256(key)) - } else { - Err(FromDnskeyError::InvalidKey) - } - } - SecAlg::ECDSAP384SHA384 => { - let mut key = Box::new([0u8; 97]); - if key.len() == 1 + data.len() { - key[0] = 0x04; - key[1..].copy_from_slice(data); - Ok(Self::EcdsaP384Sha384(key)) - } else { - Err(FromDnskeyError::InvalidKey) - } - } - - SecAlg::ED25519 => Box::<[u8]>::from(data) - .try_into() - .map(Self::Ed25519) - .map_err(|_| FromDnskeyError::InvalidKey), - SecAlg::ED448 => Box::<[u8]>::from(data) - .try_into() - .map(Self::Ed448) - .map_err(|_| FromDnskeyError::InvalidKey), - - _ => Err(FromDnskeyError::UnsupportedAlgorithm), - } - } - - /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey_format(&self) -> Box<[u8]> { - match self { - Self::RsaSha1(k) - | Self::RsaSha1Nsec3Sha1(k) - | Self::RsaSha256(k) - | Self::RsaSha512(k) => k.to_dnskey_format(), - - // From my reading of RFC 6605, the marker byte is not included. - Self::EcdsaP256Sha256(k) => k[1..].into(), - Self::EcdsaP384Sha384(k) => k[1..].into(), - - Self::Ed25519(k) => k.as_slice().into(), - Self::Ed448(k) => k.as_slice().into(), - } - } -} - -//--- Comparison - -impl PartialEq for PublicKeyBytes { - fn eq(&self, other: &Self) -> bool { - use ring::constant_time::verify_slices_are_equal; - - match (self, other) { - (Self::RsaSha1(a), Self::RsaSha1(b)) => a == b, - (Self::RsaSha1Nsec3Sha1(a), Self::RsaSha1Nsec3Sha1(b)) => a == b, - (Self::RsaSha256(a), Self::RsaSha256(b)) => a == b, - (Self::RsaSha512(a), Self::RsaSha512(b)) => a == b, - (Self::EcdsaP256Sha256(a), Self::EcdsaP256Sha256(b)) => { - verify_slices_are_equal(&**a, &**b).is_ok() - } - (Self::EcdsaP384Sha384(a), Self::EcdsaP384Sha384(b)) => { - verify_slices_are_equal(&**a, &**b).is_ok() - } - (Self::Ed25519(a), Self::Ed25519(b)) => { - verify_slices_are_equal(&**a, &**b).is_ok() - } - (Self::Ed448(a), Self::Ed448(b)) => { - verify_slices_are_equal(&**a, &**b).is_ok() - } - _ => false, - } - } -} - -impl Eq for PublicKeyBytes {} - -//----------- RsaPublicKeyBytes --------------------------------------------------- - -/// A generic RSA public key. -/// -/// All fields here are arbitrary-precision integers in big-endian format, -/// without any leading zero bytes. -#[derive(Clone, Debug)] -pub struct RsaPublicKeyBytes { - /// The public modulus. - pub n: Box<[u8]>, - - /// The public exponent. - pub e: Box<[u8]>, -} - -//--- Inspection - -impl RsaPublicKeyBytes { - /// The size of the public modulus, in bits. - pub fn key_size(&self) -> usize { - self.n.len() * 8 - self.n[0].leading_zeros() as usize - } - - /// The raw key tag computation for this value. - fn raw_key_tag(&self) -> u32 { - let mut res = 0u32; - - // Extended exponent lengths start with '00 (exp_len >> 8)', which is - // just zero for shorter exponents. That doesn't affect the result, - // so let's just do it unconditionally. - res += (self.e.len() >> 8) as u32; - res += u16::from_be_bytes([self.e.len() as u8, self.e[0]]) as u32; - - let mut chunks = self.e[1..].chunks_exact(2); - res += chunks - .by_ref() - .map(|chunk| u16::from_be_bytes(chunk.try_into().unwrap()) as u32) - .sum::(); - - let n = if !chunks.remainder().is_empty() { - res += - u16::from_be_bytes([chunks.remainder()[0], self.n[0]]) as u32; - &self.n[1..] - } else { - &self.n - }; - - res += n - .chunks(2) - .map(|chunk| { - let mut buf = [0u8; 2]; - buf[..chunk.len()].copy_from_slice(chunk); - u16::from_be_bytes(buf) as u32 - }) - .sum::(); - - res - } - - /// Compute a digest of this public key. - fn digest(&self, context: &mut ring::digest::Context) { - // Encode the exponent length. - if let Ok(exp_len) = u8::try_from(self.e.len()) { - context.update(&[exp_len]); - } else if let Ok(exp_len) = u16::try_from(self.e.len()) { - context.update(&[0u8, (exp_len >> 8) as u8, exp_len as u8]); - } else { - unreachable!("RSA exponents are (much) shorter than 64KiB") - } - - context.update(&self.e); - context.update(&self.n); - } -} - -//--- Conversion to and from DNSKEYs - -impl RsaPublicKeyBytes { - /// Parse an RSA public key as stored in a DNSKEY record. - pub fn from_dnskey_format(data: &[u8]) -> Result { - if data.len() < 3 { - return Err(FromDnskeyError::InvalidKey); - } - - // The exponent length is encoded as 1 or 3 bytes. - let (exp_len, off) = if data[0] != 0 { - (data[0] as usize, 1) - } else if data[1..3] != [0, 0] { - // NOTE: Even though this is the extended encoding of the length, - // a user could choose to put a length less than 256 over here. - let exp_len = u16::from_be_bytes(data[1..3].try_into().unwrap()); - (exp_len as usize, 3) - } else { - // The extended encoding of the length just held a zero value. - return Err(FromDnskeyError::InvalidKey); - }; - - // NOTE: off <= 3 so is safe to index up to. - let e: Box<[u8]> = data[off..] - .get(..exp_len) - .ok_or(FromDnskeyError::InvalidKey)? - .into(); - - // NOTE: The previous statement indexed up to 'exp_len'. - let n: Box<[u8]> = data[off + exp_len..].into(); - - // Empty values and leading zeros are not allowed. - if e.is_empty() || n.is_empty() || e[0] == 0 || n[0] == 0 { - return Err(FromDnskeyError::InvalidKey); - } - - Ok(Self { n, e }) - } - - /// Serialize this public key as stored in a DNSKEY record. - pub fn to_dnskey_format(&self) -> Box<[u8]> { - let mut key = Vec::new(); - - // Encode the exponent length. - if let Ok(exp_len) = u8::try_from(self.e.len()) { - key.reserve_exact(1 + self.e.len() + self.n.len()); - key.push(exp_len); - } else if let Ok(exp_len) = u16::try_from(self.e.len()) { - key.reserve_exact(3 + self.e.len() + self.n.len()); - key.push(0u8); - key.extend(&exp_len.to_be_bytes()); - } else { - unreachable!("RSA exponents are (much) shorter than 64KiB") - } - - key.extend(&*self.e); - key.extend(&*self.n); - key.into_boxed_slice() - } -} - -//--- Comparison - -impl PartialEq for RsaPublicKeyBytes { - fn eq(&self, other: &Self) -> bool { - use ring::constant_time::verify_slices_are_equal; - - verify_slices_are_equal(&self.n, &other.n).is_ok() - && verify_slices_are_equal(&self.e, &other.e).is_ok() - } -} - -impl Eq for RsaPublicKeyBytes {} - -//----------- Signature ------------------------------------------------------ - -/// A cryptographic signature. -/// -/// The format of the signature varies depending on the underlying algorithm: -/// -/// - RSA: the signature is a single integer `s`, which is less than the key's -/// public modulus `n`. `s` is encoded as bytes and ordered from most -/// significant to least significant digits. It must be at least 64 bytes -/// long and at most 512 bytes long. Leading zero bytes can be inserted for -/// padding. -/// -/// See [RFC 3110](https://datatracker.ietf.org/doc/html/rfc3110). -/// -/// - ECDSA: the signature has a fixed length (64 bytes for P-256, 96 for -/// P-384). It is the concatenation of two fixed-length integers (`r` and -/// `s`, each of equal size). -/// -/// See [RFC 6605](https://datatracker.ietf.org/doc/html/rfc6605) and [SEC 1 -/// v2.0](https://www.secg.org/sec1-v2.pdf). -/// -/// - EdDSA: the signature has a fixed length (64 bytes for ED25519, 114 bytes -/// for ED448). It is the concatenation of two curve points (`R` and `S`) -/// that are encoded into bytes. -/// -/// Signatures are too big to pass by value, so they are placed on the heap. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Signature { - RsaSha1(Box<[u8]>), - RsaSha1Nsec3Sha1(Box<[u8]>), - RsaSha256(Box<[u8]>), - RsaSha512(Box<[u8]>), - EcdsaP256Sha256(Box<[u8; 64]>), - EcdsaP384Sha384(Box<[u8; 96]>), - Ed25519(Box<[u8; 64]>), - Ed448(Box<[u8; 114]>), -} - -impl Signature { - /// The algorithm used to make the signature. - pub fn algorithm(&self) -> SecAlg { - match self { - Self::RsaSha1(_) => SecAlg::RSASHA1, - Self::RsaSha1Nsec3Sha1(_) => SecAlg::RSASHA1_NSEC3_SHA1, - Self::RsaSha256(_) => SecAlg::RSASHA256, - Self::RsaSha512(_) => SecAlg::RSASHA512, - Self::EcdsaP256Sha256(_) => SecAlg::ECDSAP256SHA256, - Self::EcdsaP384Sha384(_) => SecAlg::ECDSAP384SHA384, - Self::Ed25519(_) => SecAlg::ED25519, - Self::Ed448(_) => SecAlg::ED448, - } - } -} - -impl AsRef<[u8]> for Signature { - fn as_ref(&self) -> &[u8] { - match self { - Self::RsaSha1(s) - | Self::RsaSha1Nsec3Sha1(s) - | Self::RsaSha256(s) - | Self::RsaSha512(s) => s, - Self::EcdsaP256Sha256(s) => &**s, - Self::EcdsaP384Sha384(s) => &**s, - Self::Ed25519(s) => &**s, - Self::Ed448(s) => &**s, - } - } -} - -impl From for Box<[u8]> { - fn from(value: Signature) -> Self { - match value { - Signature::RsaSha1(s) - | Signature::RsaSha1Nsec3Sha1(s) - | Signature::RsaSha256(s) - | Signature::RsaSha512(s) => s, - Signature::EcdsaP256Sha256(s) => s as _, - Signature::EcdsaP384Sha384(s) => s as _, - Signature::Ed25519(s) => s as _, - Signature::Ed448(s) => s as _, - } - } -} - -//------------ Dnskey -------------------------------------------------------- - -/// Extensions for DNSKEY record type. -pub trait DnskeyExt { - /// Calculates a digest from DNSKEY. - /// - /// See [RFC 4034, Section 5.1.4]: - /// - /// ```text - /// 5.1.4. The Digest Field - /// The digest is calculated by concatenating the canonical form of the - /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, - /// and then applying the digest algorithm. - /// - /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); - /// - /// "|" denotes concatenation - /// - /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. - /// ``` - /// - /// [RFC 4034, Section 5.1.4]: https://tools.ietf.org/html/rfc4034#section-5.1.4 - fn digest( - &self, - name: &N, - algorithm: DigestAlg, - ) -> Result; -} - -impl DnskeyExt for Dnskey -where - Octets: AsRef<[u8]>, -{ - /// Calculates a digest from DNSKEY. - /// - /// See [RFC 4034, Section 5.1.4]: - /// - /// ```text - /// 5.1.4. The Digest Field - /// The digest is calculated by concatenating the canonical form of the - /// fully qualified owner name of the DNSKEY RR with the DNSKEY RDATA, - /// and then applying the digest algorithm. - /// - /// digest = digest_algorithm( DNSKEY owner name | DNSKEY RDATA); - /// - /// "|" denotes concatenation - /// - /// DNSKEY RDATA = Flags | Protocol | Algorithm | Public Key. - /// ``` - /// - /// [RFC 4034, Section 5.1.4]: https://tools.ietf.org/html/rfc4034#section-5.1.4 - fn digest( - &self, - name: &N, - algorithm: DigestAlg, - ) -> Result { - let mut buf: Vec = Vec::new(); - with_infallible(|| { - name.compose_canonical(&mut buf)?; - self.compose_canonical_rdata(&mut buf) - }); - - let mut ctx = match algorithm { - DigestAlg::SHA1 => { - digest::Context::new(&digest::SHA1_FOR_LEGACY_USE_ONLY) - } - DigestAlg::SHA256 => digest::Context::new(&digest::SHA256), - DigestAlg::SHA384 => digest::Context::new(&digest::SHA384), - _ => { - return Err(AlgorithmError::Unsupported); - } - }; - - ctx.update(&buf); - Ok(ctx.finish()) - } -} - -// This needs to match the digests supported in digest. -pub fn supported_digest(d: &DigestAlg) -> bool { - *d == DigestAlg::SHA1 - || *d == DigestAlg::SHA256 - || *d == DigestAlg::SHA384 -} - -//------------ Rrsig --------------------------------------------------------- - -/// Extensions for DNSKEY record type. -pub trait RrsigExt { - /// Compose the signed data according to [RC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2). - /// - /// ```text - /// Once the RRSIG RR has met the validity requirements described in - /// Section 5.3.1, the validator has to reconstruct the original signed - /// data. The original signed data includes RRSIG RDATA (excluding the - /// Signature field) and the canonical form of the RRset. Aside from - /// being ordered, the canonical form of the RRset might also differ from - /// the received RRset due to DNS name compression, decremented TTLs, or - /// wildcard expansion. - /// ``` - fn signed_data( - &self, - buf: &mut B, - records: &mut [impl AsRef>], - ) -> Result<(), B::AppendError> - where - D: RecordData + CanonicalOrd + ComposeRecordData + Sized; - - /// Return if records are expanded for a wildcard according to the - /// information in this signature. - fn wildcard_closest_encloser( - &self, - rr: &Record, - ) -> Option> - where - N: ToName; - - /// Attempt to use the cryptographic signature to authenticate the signed data, and thus authenticate the RRSET. - /// The signed data is expected to be calculated as per [RFC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2). - /// - /// [RFC4035, Section 5.3.2](https://tools.ietf.org/html/rfc4035#section-5.3.2): - /// ```text - /// 5.3.3. Checking the Signature - /// - /// Once the resolver has validated the RRSIG RR as described in Section - /// 5.3.1 and reconstructed the original signed data as described in - /// Section 5.3.2, the validator can attempt to use the cryptographic - /// signature to authenticate the signed data, and thus (finally!) - /// authenticate the RRset. - /// - /// The Algorithm field in the RRSIG RR identifies the cryptographic - /// algorithm used to generate the signature. The signature itself is - /// contained in the Signature field of the RRSIG RDATA, and the public - /// key used to verify the signature is contained in the Public Key field - /// of the matching DNSKEY RR(s) (found in Section 5.3.1). [RFC4034] - /// provides a list of algorithm types and provides pointers to the - /// documents that define each algorithm's use. - /// ``` - fn verify_signed_data( - &self, - dnskey: &Dnskey>, - signed_data: &impl AsRef<[u8]>, - ) -> Result<(), AlgorithmError>; -} - -impl, TN: ToName> RrsigExt for Rrsig { - fn signed_data( - &self, - buf: &mut B, - records: &mut [impl AsRef>], - ) -> Result<(), B::AppendError> - where - D: RecordData + CanonicalOrd + ComposeRecordData + Sized, - { - // signed_data = RRSIG_RDATA | RR(1) | RR(2)... where - // "|" denotes concatenation - // RRSIG_RDATA is the wire format of the RRSIG RDATA fields - // with the Signature field excluded and the Signer's Name - // in canonical form. - self.type_covered().compose(buf)?; - self.algorithm().compose(buf)?; - self.labels().compose(buf)?; - self.original_ttl().compose(buf)?; - self.expiration().compose(buf)?; - self.inception().compose(buf)?; - self.key_tag().compose(buf)?; - self.signer_name().compose_canonical(buf)?; - - // The set of all RR(i) is sorted into canonical order. - // See https://tools.ietf.org/html/rfc4034#section-6.3 - records.sort_by(|a, b| { - a.as_ref().data().canonical_cmp(b.as_ref().data()) - }); - - // RR(i) = name | type | class | OrigTTL | RDATA length | RDATA - for rr in records.iter().map(|r| r.as_ref()) { - // Handle expanded wildcards as per [RFC4035, Section 5.3.2] - // (https://tools.ietf.org/html/rfc4035#section-5.3.2). - let rrsig_labels = usize::from(self.labels()); - let fqdn = rr.owner(); - // Subtract the root label from count as the algorithm doesn't - // accomodate that. - let fqdn_labels = fqdn.iter_labels().count() - 1; - if rrsig_labels < fqdn_labels { - // name = "*." | the rightmost rrsig_label labels of the fqdn - buf.append_slice(b"\x01*")?; - match fqdn - .to_cow() - .iter_suffixes() - .nth(fqdn_labels - rrsig_labels) - { - Some(name) => name.compose_canonical(buf)?, - None => fqdn.compose_canonical(buf)?, - }; - } else { - fqdn.compose_canonical(buf)?; - } - - rr.rtype().compose(buf)?; - rr.class().compose(buf)?; - self.original_ttl().compose(buf)?; - rr.data().compose_canonical_len_rdata(buf)?; - } - Ok(()) - } - - fn wildcard_closest_encloser( - &self, - rr: &Record, - ) -> Option> - where - N: ToName, - { - // Handle expanded wildcards as per [RFC4035, Section 5.3.2] - // (https://tools.ietf.org/html/rfc4035#section-5.3.2). - let rrsig_labels = usize::from(self.labels()); - let fqdn = rr.owner(); - // Subtract the root label from count as the algorithm doesn't - // accomodate that. - let fqdn_labels = fqdn.iter_labels().count() - 1; - if rrsig_labels < fqdn_labels { - // name = "*." | the rightmost rrsig_label labels of the fqdn - Some( - match fqdn - .to_cow() - .iter_suffixes() - .nth(fqdn_labels - rrsig_labels) - { - Some(name) => Name::from_octets(Bytes::copy_from_slice( - name.as_octets(), - )) - .unwrap(), - None => fqdn.to_bytes(), - }, - ) - } else { - None - } - } - - fn verify_signed_data( - &self, - dnskey: &Dnskey>, - signed_data: &impl AsRef<[u8]>, - ) -> Result<(), AlgorithmError> { - let signature = self.signature().as_ref(); - let signed_data = signed_data.as_ref(); - - // Caller needs to ensure that the signature matches the key, but enforce the algorithm match - if self.algorithm() != dnskey.algorithm() { - return Err(AlgorithmError::InvalidData); - } - - // Note: Canonicalize the algorithm, otherwise matching named variants against Int(_) is not going to work - let sec_alg = SecAlg::from_int(self.algorithm().to_int()); - match sec_alg { - SecAlg::RSASHA1 - | SecAlg::RSASHA1_NSEC3_SHA1 - | SecAlg::RSASHA256 - | SecAlg::RSASHA512 => { - let (algorithm, min_bytes) = match sec_alg { - SecAlg::RSASHA1 | SecAlg::RSASHA1_NSEC3_SHA1 => ( - &signature::RSA_PKCS1_1024_8192_SHA1_FOR_LEGACY_USE_ONLY, - 1024 / 8, - ), - SecAlg::RSASHA256 => ( - &signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, - 1024 / 8, - ), - SecAlg::RSASHA512 => ( - &signature::RSA_PKCS1_1024_8192_SHA512_FOR_LEGACY_USE_ONLY, - 1024 / 8, - ), - _ => unreachable!(), - }; - - // The key isn't available in either PEM or DER, so use the - // direct RSA verifier. - let (e, n) = rsa_exponent_modulus(dnskey, min_bytes)?; - let public_key = - signature::RsaPublicKeyComponents { n: &n, e: &e }; - public_key - .verify(algorithm, signed_data, signature) - .map_err(|_| AlgorithmError::BadSig) - } - SecAlg::ECDSAP256SHA256 | SecAlg::ECDSAP384SHA384 => { - let algorithm = match sec_alg { - SecAlg::ECDSAP256SHA256 => { - &signature::ECDSA_P256_SHA256_FIXED - } - SecAlg::ECDSAP384SHA384 => { - &signature::ECDSA_P384_SHA384_FIXED - } - _ => unreachable!(), - }; - - // Add 0x4 identifier to the ECDSA pubkey as expected by ring. - let public_key = dnskey.public_key().as_ref(); - let mut key = Vec::with_capacity(public_key.len() + 1); - key.push(0x4); - key.extend_from_slice(public_key); - - signature::UnparsedPublicKey::new(algorithm, &key) - .verify(signed_data, signature) - .map_err(|_| AlgorithmError::BadSig) - } - SecAlg::ED25519 => { - let key = dnskey.public_key(); - signature::UnparsedPublicKey::new(&signature::ED25519, &key) - .verify(signed_data, signature) - .map_err(|_| AlgorithmError::BadSig) - } - _ => Err(AlgorithmError::Unsupported), - } - } -} - -// This needs to match the algorithms supported in signed_data. -pub fn supported_algorithm(a: &SecAlg) -> bool { - *a == SecAlg::RSASHA1 - || *a == SecAlg::RSASHA1_NSEC3_SHA1 - || *a == SecAlg::RSASHA256 - || *a == SecAlg::RSASHA512 - || *a == SecAlg::ECDSAP256SHA256 -} - -/// Return the RSA exponent and modulus components from DNSKEY record data. -fn rsa_exponent_modulus( - dnskey: &Dnskey>, - min_len: usize, -) -> Result<(&[u8], &[u8]), AlgorithmError> { - let public_key = dnskey.public_key().as_ref(); - if public_key.len() <= 3 { - return Err(AlgorithmError::InvalidData); - } - - let (pos, exp_len) = match public_key[0] { - 0 => ( - 3, - (usize::from(public_key[1]) << 8) | usize::from(public_key[2]), - ), - len => (1, usize::from(len)), - }; - - // Check if there's enough space for exponent and modulus. - if public_key[pos..].len() < pos + exp_len { - return Err(AlgorithmError::InvalidData); - }; - - // Check for minimum supported key size - if public_key[pos..].len() < min_len { - return Err(AlgorithmError::Unsupported); - } - - Ok(public_key[pos..].split_at(exp_len)) -} - -//============ Error Types =================================================== - -//----------- DigestError ---------------------------------------------------- - -/// An error when computing a digest. -#[derive(Clone, Debug)] -pub enum DigestError { - UnsupportedAlgorithm, -} - -//--- Display, Error - -impl fmt::Display for DigestError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "unsupported algorithm", - }) - } -} - -impl error::Error for DigestError {} - -//----------- FromDnskeyError ------------------------------------------------ - -/// An error in reading a DNSKEY record. -#[derive(Clone, Debug)] -pub enum FromDnskeyError { - UnsupportedAlgorithm, - UnsupportedProtocol, - InvalidKey, -} - -//--- Display, Error - -impl fmt::Display for FromDnskeyError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "unsupported algorithm", - Self::UnsupportedProtocol => "unsupported protocol", - Self::InvalidKey => "malformed key", - }) - } -} - -impl error::Error for FromDnskeyError {} - -//----------- ParseDnskeyTextError ------------------------------------------- - -#[derive(Clone, Debug)] -pub enum ParseDnskeyTextError { - Misformatted, - FromDnskey(FromDnskeyError), -} - -//--- Display, Error - -impl fmt::Display for ParseDnskeyTextError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::Misformatted => "misformatted DNSKEY record", - Self::FromDnskey(e) => return e.fmt(f), - }) - } -} - -impl error::Error for ParseDnskeyTextError {} - -//------------ AlgorithmError ------------------------------------------------ - -/// An algorithm error during verification. -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum AlgorithmError { - Unsupported, - BadSig, - InvalidData, -} - -//--- Display, Error - -impl fmt::Display for AlgorithmError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - AlgorithmError::Unsupported => "unsupported algorithm", - AlgorithmError::BadSig => "bad signature", - AlgorithmError::InvalidData => "invalid data", - }) - } -} - -impl error::Error for AlgorithmError {} - -//============ Test ========================================================== - -#[cfg(test)] -#[cfg(feature = "std")] -mod test { - use super::*; - use crate::base::iana::{Class, Rtype}; - use crate::base::Ttl; - use crate::rdata::dnssec::Timestamp; - use crate::rdata::{Mx, ZoneRecordData}; - use crate::utils::base64; - use bytes::Bytes; - use std::str::FromStr; - use std::string::ToString; - - type Name = crate::base::name::Name>; - type Ds = crate::rdata::Ds>; - type Dnskey = crate::rdata::Dnskey>; - type Rrsig = crate::rdata::Rrsig, Name>; - - const KEYS: &[(SecAlg, u16, usize)] = &[ - (SecAlg::RSASHA1, 439, 2048), - (SecAlg::RSASHA1_NSEC3_SHA1, 22204, 2048), - (SecAlg::RSASHA256, 60616, 2048), - (SecAlg::ECDSAP256SHA256, 42253, 256), - (SecAlg::ECDSAP384SHA384, 33566, 384), - (SecAlg::ED25519, 56037, 256), - (SecAlg::ED448, 7379, 456), - ]; - - // Returns current root KSK/ZSK for testing (2048b) - fn root_pubkey() -> (Dnskey, Dnskey) { - let ksk = base64::decode::>( - "\ - AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3+/\ - 4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kvArMt\ - NROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF0jLHwV\ - N8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+eoZG+SrDK\ - 6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfdRUfhHdY6+c\ - n8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwNR1AkUTV74bU=", - ) - .unwrap(); - let zsk = base64::decode::>( - "\ - AwEAAeVDC34GZILwsQJy97K2Fst4P3XYZrXLyrkausYzSqEjSUulgh+iLgH\ - g0y7FIF890+sIjXsk7KLJUmCOWfYWPorNKEOKLk5Zx/4M6D3IHZE3O3m/Ea\ - hrc28qQzmTLxiMZAW65MvR2UO3LxVtYOPBEBiDgAQD47x2JLsJYtavCzNL5\ - WiUk59OgvHmDqmcC7VXYBhK8V8Tic089XJgExGeplKWUt9yyc31ra1swJX5\ - 1XsOaQz17+vyLVH8AZP26KvKFiZeoRbaq6vl+hc8HQnI2ug5rA2zoz3MsSQ\ - BvP1f/HvqsWxLqwXXKyDD1QM639U+XzVB8CYigyscRP22QCnwKIU=", - ) - .unwrap(); - ( - Dnskey::new(257, 3, SecAlg::RSASHA256, ksk).unwrap(), - Dnskey::new(256, 3, SecAlg::RSASHA256, zsk).unwrap(), - ) - } - - // Returns the current net KSK/ZSK for testing (1024b) - fn net_pubkey() -> (Dnskey, Dnskey) { - let ksk = base64::decode::>( - "AQOYBnzqWXIEj6mlgXg4LWC0HP2n8eK8XqgHlmJ/69iuIHsa1TrHDG6TcOra/pyeGKwH0nKZhTmXSuUFGh9BCNiwVDuyyb6OBGy2Nte9Kr8NwWg4q+zhSoOf4D+gC9dEzg0yFdwT0DKEvmNPt0K4jbQDS4Yimb+uPKuF6yieWWrPYYCrv8C9KC8JMze2uT6NuWBfsl2fDUoV4l65qMww06D7n+p7RbdwWkAZ0fA63mXVXBZF6kpDtsYD7SUB9jhhfLQE/r85bvg3FaSs5Wi2BaqN06SzGWI1DHu7axthIOeHwg00zxlhTpoYCH0ldoQz+S65zWYi/fRJiyLSBb6JZOvn", - ) - .unwrap(); - let zsk = base64::decode::>( - "AQPW36Zs2vsDFGgdXBlg8RXSr1pSJ12NK+u9YcWfOr85we2z5A04SKQlIfyTK37dItGFcldtF7oYwPg11T3R33viKV6PyASvnuRl8QKiLk5FfGUDt1sQJv3S/9wT22Le1vnoE/6XFRyeb8kmJgz0oQB1VAO9b0l6Vm8KAVeOGJ+Qsjaq0O0aVzwPvmPtYm/i3qoAhkaMBUpg6RrF5NKhRyG3", - ) - .unwrap(); - ( - Dnskey::new(257, 3, SecAlg::RSASHA256, ksk).unwrap(), - Dnskey::new(256, 3, SecAlg::RSASHA256, zsk).unwrap(), - ) - } - - #[test] - fn parse_from_bind() { - for &(algorithm, key_tag, _) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let _ = Key::>::parse_from_bind(&data).unwrap(); - } - } - - #[test] - fn key_size() { - for &(algorithm, key_tag, key_size) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_from_bind(&data).unwrap(); - assert_eq!(key.key_size(), key_size); - } - } - - #[test] - fn key_tag() { - for &(algorithm, key_tag, _) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_from_bind(&data).unwrap(); - assert_eq!(key.to_dnskey().key_tag(), key_tag); - assert_eq!(key.key_tag(), key_tag); - } - } - - #[test] - fn digest() { - for &(algorithm, key_tag, _) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_from_bind(&data).unwrap(); - - // Scan the DS record from the file. - let path = format!("test-data/dnssec-keys/K{}.ds", name); - let data = std::fs::read_to_string(path).unwrap(); - let mut scanner = IterScanner::new(data.split_ascii_whitespace()); - let _ = scanner.scan_name().unwrap(); - let _ = Class::scan(&mut scanner).unwrap(); - assert_eq!(Rtype::scan(&mut scanner).unwrap(), Rtype::DS); - let ds = Ds::scan(&mut scanner).unwrap(); - - assert_eq!(key.digest(ds.digest_type()).unwrap(), ds); - } - } - - #[test] - fn dnskey_roundtrip() { - for &(algorithm, key_tag, _) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_from_bind(&data).unwrap(); - let dnskey = key.to_dnskey().convert(); - let same = Key::from_dnskey(key.owner().clone(), dnskey).unwrap(); - assert_eq!(key, same); - } - } - - #[test] - fn bind_format_roundtrip() { - for &(algorithm, key_tag, _) in KEYS { - let name = - format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag); - - let path = format!("test-data/dnssec-keys/K{}.key", name); - let data = std::fs::read_to_string(path).unwrap(); - let key = Key::>::parse_from_bind(&data).unwrap(); - let bind_fmt_key = key.display_as_bind().to_string(); - let same = Key::parse_from_bind(&bind_fmt_key).unwrap(); - assert_eq!(key, same); - } - } - - #[test] - fn dnskey_digest() { - let (dnskey, _) = root_pubkey(); - let owner = Name::root(); - let expected = Ds::new( - 20326, - SecAlg::RSASHA256, - DigestAlg::SHA256, - base64::decode::>( - "4G1EuAuPHTmpXAsNfGXQhFjogECbvGg0VxBCN8f47I0=", - ) - .unwrap(), - ) - .unwrap(); - assert_eq!( - dnskey.digest(&owner, DigestAlg::SHA256).unwrap().as_ref(), - expected.digest() - ); - } - - #[test] - fn dnskey_digest_unsupported() { - let (dnskey, _) = root_pubkey(); - let owner = Name::root(); - assert!(dnskey.digest(&owner, DigestAlg::GOST).is_err()); - } - - fn rrsig_verify_dnskey(ksk: Dnskey, zsk: Dnskey, rrsig: Rrsig) { - let mut records: Vec<_> = [&ksk, &zsk] - .iter() - .cloned() - .map(|x| { - Record::new( - rrsig.signer_name().clone(), - Class::IN, - Ttl::from_secs(0), - x.clone(), - ) - }) - .collect(); - let signed_data = { - let mut buf = Vec::new(); - rrsig.signed_data(&mut buf, records.as_mut_slice()).unwrap(); - Bytes::from(buf) - }; - - // Test that the KSK is sorted after ZSK key - assert_eq!(ksk.key_tag(), rrsig.key_tag()); - assert_eq!(ksk.key_tag(), records[1].data().key_tag()); - - // Test verifier - assert!(rrsig.verify_signed_data(&ksk, &signed_data).is_ok()); - assert!(rrsig.verify_signed_data(&zsk, &signed_data).is_err()); - } - - #[test] - fn rrsig_verify_rsa_sha256() { - // Test 2048b long key - let (ksk, zsk) = root_pubkey(); - let rrsig = Rrsig::new( - Rtype::DNSKEY, - SecAlg::RSASHA256, - 0, - Ttl::from_secs(172800), - 1560211200.into(), - 1558396800.into(), - 20326, - Name::root(), - base64::decode::>( - "otBkINZAQu7AvPKjr/xWIEE7+SoZtKgF8bzVynX6bfJMJuPay8jPvNmwXkZOdSoYlvFp0bk9JWJKCh8y5uoNfMFkN6OSrDkr3t0E+c8c0Mnmwkk5CETH3Gqxthi0yyRX5T4VlHU06/Ks4zI+XAgl3FBpOc554ivdzez8YCjAIGx7XgzzooEb7heMSlLc7S7/HNjw51TPRs4RxrAVcezieKCzPPpeWBhjE6R3oiSwrl0SBD4/yplrDlr7UHs/Atcm3MSgemdyr2sOoOUkVQCVpcj3SQQezoD2tCM7861CXEQdg5fjeHDtz285xHt5HJpA5cOcctRo4ihybfow/+V7AQ==", - ) - .unwrap() - ).unwrap(); - rrsig_verify_dnskey(ksk, zsk, rrsig); - - // Test 1024b long key - let (ksk, zsk) = net_pubkey(); - let rrsig = Rrsig::new( - Rtype::DNSKEY, - SecAlg::RSASHA256, - 1, - Ttl::from_secs(86400), - Timestamp::from_str("20210921162830").unwrap(), - Timestamp::from_str("20210906162330").unwrap(), - 35886, - "net.".parse::().unwrap(), - base64::decode::>( - "j1s1IPMoZd0mbmelNVvcbYNe2tFCdLsLpNCnQ8xW6d91ujwPZ2yDlc3lU3hb+Jq3sPoj+5lVgB7fZzXQUQTPFWLF7zvW49da8pWuqzxFtg6EjXRBIWH5rpEhOcr+y3QolJcPOTx+/utCqt2tBKUUy3LfM6WgvopdSGaryWdwFJPW7qKHjyyLYxIGx5AEuLfzsA5XZf8CmpUheSRH99GRZoIB+sQzHuelWGMQ5A42DPvOVZFmTpIwiT2QaIpid4nJ7jNfahfwFrCoS+hvqjK9vktc5/6E/Mt7DwCQDaPt5cqDfYltUitQy+YA5YP5sOhINChYadZe+2N80OA+RKz0mA==", - ) - .unwrap() - ).unwrap(); - rrsig_verify_dnskey(ksk, zsk, rrsig.clone()); - - // Test that 512b short RSA DNSKEY is not supported (too short) - let data = base64::decode::>( - "AwEAAcFcGsaxxdgiuuGmCkVImy4h99CqT7jwY3pexPGcnUFtR2Fh36BponcwtkZ4cAgtvd4Qs8PkxUdp6p/DlUmObdk=", - ) - .unwrap(); - - let short_key = Dnskey::new(256, 3, SecAlg::RSASHA256, data).unwrap(); - let err = rrsig - .verify_signed_data(&short_key, &vec![0; 100]) - .unwrap_err(); - assert_eq!(err, AlgorithmError::Unsupported); - } - - #[test] - fn rrsig_verify_ecdsap256_sha256() { - let (ksk, zsk) = ( - Dnskey::new( - 257, - 3, - SecAlg::ECDSAP256SHA256, - base64::decode::>( - "mdsswUyr3DPW132mOi8V9xESWE8jTo0dxCjjnopKl+GqJxpVXckHAe\ - F+KkxLbxILfDLUT0rAK9iUzy1L53eKGQ==", - ) - .unwrap(), - ) - .unwrap(), - Dnskey::new( - 256, - 3, - SecAlg::ECDSAP256SHA256, - base64::decode::>( - "oJMRESz5E4gYzS/q6XDrvU1qMPYIjCWzJaOau8XNEZeqCYKD5ar0IR\ - d8KqXXFJkqmVfRvMGPmM1x8fGAa2XhSA==", - ) - .unwrap(), - ) - .unwrap(), - ); - - let owner = Name::from_str("cloudflare.com.").unwrap(); - let rrsig = Rrsig::new( - Rtype::DNSKEY, - SecAlg::ECDSAP256SHA256, - 2, - Ttl::from_secs(3600), - 1560314494.into(), - 1555130494.into(), - 2371, - owner, - base64::decode::>( - "8jnAGhG7O52wmL065je10XQztRX1vK8P8KBSyo71Z6h5wAT9+GFxKBaE\ - zcJBLvRmofYFDAhju21p1uTfLaYHrg==", - ) - .unwrap(), - ) - .unwrap(); - rrsig_verify_dnskey(ksk, zsk, rrsig); - } - - #[test] - fn rrsig_verify_ed25519() { - let (ksk, zsk) = ( - Dnskey::new( - 257, - 3, - SecAlg::ED25519, - base64::decode::>( - "m1NELLVVQKl4fHVn/KKdeNO0PrYKGT3IGbYseT8XcKo=", - ) - .unwrap(), - ) - .unwrap(), - Dnskey::new( - 256, - 3, - SecAlg::ED25519, - base64::decode::>( - "2tstZAjgmlDTePn0NVXrAHBJmg84LoaFVxzLl1anjGI=", - ) - .unwrap(), - ) - .unwrap(), - ); - - let owner = - Name::from_octets(Vec::from(b"\x07ED25519\x02nl\x00".as_ref())) - .unwrap(); - let rrsig = Rrsig::new( - Rtype::DNSKEY, - SecAlg::ED25519, - 2, - Ttl::from_secs(3600), - 1559174400.into(), - 1557360000.into(), - 45515, - owner, - base64::decode::>( - "hvPSS3E9Mx7lMARqtv6IGiw0NE0uz0mZewndJCHTkhwSYqlasUq7KfO5\ - QdtgPXja7YkTaqzrYUbYk01J8ICsAA==", - ) - .unwrap(), - ) - .unwrap(); - rrsig_verify_dnskey(ksk, zsk, rrsig); - } - - #[test] - fn rrsig_verify_generic_type() { - let (ksk, zsk) = root_pubkey(); - let rrsig = Rrsig::new( - Rtype::DNSKEY, - SecAlg::RSASHA256, - 0, - Ttl::from_secs(172800), - 1560211200.into(), - 1558396800.into(), - 20326, - Name::root(), - base64::decode::>( - "otBkINZAQu7AvPKjr/xWIEE7+SoZtKgF8bzVynX6bfJMJuPay8jPvNmwXkZ\ - OdSoYlvFp0bk9JWJKCh8y5uoNfMFkN6OSrDkr3t0E+c8c0Mnmwkk5CETH3Gq\ - xthi0yyRX5T4VlHU06/Ks4zI+XAgl3FBpOc554ivdzez8YCjAIGx7XgzzooE\ - b7heMSlLc7S7/HNjw51TPRs4RxrAVcezieKCzPPpeWBhjE6R3oiSwrl0SBD4\ - /yplrDlr7UHs/Atcm3MSgemdyr2sOoOUkVQCVpcj3SQQezoD2tCM7861CXEQ\ - dg5fjeHDtz285xHt5HJpA5cOcctRo4ihybfow/+V7AQ==", - ) - .unwrap(), - ) - .unwrap(); - - let mut records: Vec, Name>>> = - [&ksk, &zsk] - .iter() - .cloned() - .map(|x| { - let data = ZoneRecordData::from(x.clone()); - Record::new( - rrsig.signer_name().clone(), - Class::IN, - Ttl::from_secs(0), - data, - ) - }) - .collect(); - - let signed_data = { - let mut buf = Vec::new(); - rrsig.signed_data(&mut buf, records.as_mut_slice()).unwrap(); - Bytes::from(buf) - }; - - assert!(rrsig.verify_signed_data(&ksk, &signed_data).is_ok()); - } - - #[test] - fn rrsig_verify_wildcard() { - let key = Dnskey::new( - 256, - 3, - SecAlg::RSASHA1, - base64::decode::>( - "AQOy1bZVvpPqhg4j7EJoM9rI3ZmyEx2OzDBVrZy/lvI5CQePxX\ - HZS4i8dANH4DX3tbHol61ek8EFMcsGXxKciJFHyhl94C+NwILQd\ - zsUlSFovBZsyl/NX6yEbtw/xN9ZNcrbYvgjjZ/UVPZIySFNsgEY\ - vh0z2542lzMKR4Dh8uZffQ==", - ) - .unwrap(), - ) - .unwrap(); - let rrsig = Rrsig::new( - Rtype::MX, - SecAlg::RSASHA1, - 2, - Ttl::from_secs(3600), - Timestamp::from_str("20040509183619").unwrap(), - Timestamp::from_str("20040409183619").unwrap(), - 38519, - Name::from_str("example.").unwrap(), - base64::decode::>( - "OMK8rAZlepfzLWW75Dxd63jy2wswESzxDKG2f9AMN1CytCd10cYI\ - SAxfAdvXSZ7xujKAtPbctvOQ2ofO7AZJ+d01EeeQTVBPq4/6KCWhq\ - e2XTjnkVLNvvhnc0u28aoSsG0+4InvkkOHknKxw4kX18MMR34i8lC\ - 36SR5xBni8vHI=", - ) - .unwrap(), - ) - .unwrap(); - let record = Record::new( - Name::from_str("a.z.w.example.").unwrap(), - Class::IN, - Ttl::from_secs(3600), - Mx::new(1, Name::from_str("ai.example.").unwrap()), - ); - let signed_data = { - let mut buf = Vec::new(); - rrsig.signed_data(&mut buf, &mut [record]).unwrap(); - Bytes::from(buf) - }; - - // Test that the key matches RRSIG - assert_eq!(key.key_tag(), rrsig.key_tag()); - - // Test verifier - assert_eq!(rrsig.verify_signed_data(&key, &signed_data), Ok(())); - } -} - -//------------ Nsec3HashError ------------------------------------------------- - -/// An error when creating an NSEC3 hash. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum Nsec3HashError { - /// The requested algorithm for NSEC3 hashing is not supported. - UnsupportedAlgorithm, - - /// Data could not be appended to a buffer. - /// - /// This could indicate an out of memory condition. - AppendError, - - /// The hashing process produced an invalid owner hash. - /// - /// See: [OwnerHashError](crate::rdata::nsec3::OwnerHashError) - OwnerHashError, - - /// The hashing process produced a hash that already exists. - CollisionDetected, - - /// The hash provider did not provide a hash for the given owner name. - MissingHash, -} - -//--- Display - -impl std::fmt::Display for Nsec3HashError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - Nsec3HashError::UnsupportedAlgorithm => { - f.write_str("Unsupported algorithm") - } - Nsec3HashError::AppendError => { - f.write_str("Append error: out of memory?") - } - Nsec3HashError::OwnerHashError => { - f.write_str("Hashing produced an invalid owner hash") - } - Nsec3HashError::CollisionDetected => { - f.write_str("Hash collision detected") - } - Nsec3HashError::MissingHash => { - f.write_str("Missing hash for owner name") - } - } - } -} - -/// Compute an [RFC 5155] NSEC3 hash using default settings. -/// -/// See: [Nsec3param::default]. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_default_hash( - owner: N, -) -> Result, Nsec3HashError> -where - N: ToName, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - let params = Nsec3param::::default(); - nsec3_hash( - owner, - params.hash_algorithm(), - params.iterations(), - params.salt(), - ) -} - -/// Compute an [RFC 5155] NSEC3 hash. -/// -/// Computes an NSEC3 hash according to [RFC 5155] section 5: -/// -/// > IH(salt, x, 0) = H(x || salt) -/// > IH(salt, x, k) = H(IH(salt, x, k-1) || salt), if k > 0 -/// -/// Then the calculated hash of an owner name is: -/// -/// > IH(salt, owner name, iterations), -/// -/// Note that the `iterations` parameter is the number of _additional_ -/// iterations as defined in [RFC 5155] section 3.1.3. -/// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155 -pub fn nsec3_hash( - owner: N, - algorithm: Nsec3HashAlg, - iterations: u16, - salt: &Nsec3Salt, -) -> Result, Nsec3HashError> -where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, -{ - if algorithm != Nsec3HashAlg::SHA1 { - return Err(Nsec3HashError::UnsupportedAlgorithm); - } - - fn mk_hash( - owner: N, - iterations: u16, - salt: &Nsec3Salt, - ) -> Result - where - N: ToName, - SaltOcts: AsRef<[u8]>, - HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate, - for<'a> HashOcts: From<&'a [u8]>, - { - let mut canonical_owner = HashOcts::empty(); - owner.compose_canonical(&mut canonical_owner)?; - - let mut ctx = ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(canonical_owner.as_ref()); - ctx.update(salt.as_slice()); - let mut h = ctx.finish(); - - for _ in 0..iterations { - let mut ctx = - ring::digest::Context::new(&SHA1_FOR_LEGACY_USE_ONLY); - ctx.update(h.as_ref()); - ctx.update(salt.as_slice()); - h = ctx.finish(); - } - - Ok(h.as_ref().into()) - } - - let hash = mk_hash(owner, iterations, salt) - .map_err(|_| Nsec3HashError::AppendError)?; - - let owner_hash = OwnerHash::from_octets(hash) - .map_err(|_| Nsec3HashError::OwnerHashError)?; - - Ok(owner_hash) -} diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index 98451d8ef..7914cc1a2 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -55,9 +55,14 @@ pub type ScannedString = Str; /// into the memory buffer. The function [`load`][Self::load] can be used to /// create a value directly from a reader. /// -/// Once data has been added, you can simply iterate over the value to -/// get entries. The [`next_entry`][Self::next_entry] method provides an +/// Once data has been added, you can simply iterate over the value to get +/// entries. The [`next_entry`][Self::next_entry] method provides an /// alternative with a more question mark friendly signature. +/// +/// By default RFC 1035 validity checks are enabled. At present only the first +/// check is implemented: "1. All RRs in the zonefile should have the same +/// class". To disable strict validation call [`allow_invalid()`] prior to +/// calling [`load()`]. #[derive(Clone, Debug)] pub struct Zonefile { /// This is where we keep the data of the next entry. @@ -73,7 +78,11 @@ pub struct Zonefile { last_ttl: Ttl, /// The last class. - last_class: Class, + last_class: Option, + + /// Whether the loaded zonefile should be required to pass RFC 1035 + /// validity checks. + require_valid: bool, } impl Zonefile { @@ -89,6 +98,12 @@ impl Zonefile { ))) } + /// Disables RFC 1035 section 5.2 zonefile validity checks. + pub fn allow_invalid(mut self) -> Self { + self.require_valid = false; + self + } + /// Creates a new value using the given buffer. fn with_buf(buf: SourceBuf) -> Self { Zonefile { @@ -96,7 +111,8 @@ impl Zonefile { origin: None, last_owner: None, last_ttl: Ttl::from_secs(3600), - last_class: Class::IN, + last_class: None, + require_valid: true, } } @@ -168,7 +184,19 @@ impl Zonefile { /// any relative names encountered will cause iteration to terminate with /// a missing origin error. pub fn set_origin(&mut self, origin: Name) { - self.origin = Some(origin) + self.origin = Some(origin); + } + + /// Set a default class to use. + /// + /// RFC 1035 does not define a default class for zone file records to use, + /// it only states that the class field for a record is optional with + /// omitted class values defaulting to the last explicitly stated value. + /// + /// If no last explicitly stated value exists, the class passed to this + /// function will be used, otherwise an error will be raised. + pub fn set_default_class(&mut self, class: Class) { + self.last_class = Some(class); } /// Returns the next entry in the zonefile. @@ -237,6 +265,8 @@ pub enum Entry { /// This includes all the entry types that we can handle internally and don’t /// have to bubble up to the user. #[derive(Clone, Debug)] +// 'Entry' is the largest variant, but is also the most common. +#[allow(clippy::large_enum_variant)] enum ScannedEntry { /// An entry that should be handed to the user. Entry(Entry), @@ -342,12 +372,38 @@ impl<'a> EntryScanner<'a> { self.zonefile.last_owner = Some(owner.clone()); } - let class = match class { - Some(class) => { - self.zonefile.last_class = class; + let class = match (class, self.zonefile.last_class) { + // https://www.rfc-editor.org/rfc/rfc1035#section-5.2 + // 5.2. Use of master files to define zones + // .. + // "1. All RRs in the file should have the same class." + (Some(class), Some(last_class)) => { + if self.zonefile.require_valid && class != last_class { + return Err(EntryError::different_class( + last_class, class, + )); + } + class + } + + // Record lacks a class but a last class is known, use it. + // + // https://www.rfc-editor.org/rfc/rfc1035#section-5.2 + // 5.1. Format + // .. + // "Omitted class and TTL values are default to the last + // explicitly stated values." + (None, Some(last_class)) => last_class, + + // Record specifies a class, use it. + (Some(class), None) => { + self.zonefile.last_class = Some(class); class } - None => self.zonefile.last_class, + + // Record lacks a class and no last class is known, raise an + // error. + (None, None) => return Err(EntryError::missing_last_class()), }; let ttl = match ttl { @@ -472,7 +528,7 @@ impl<'a> EntryScanner<'a> { self.zonefile.buf.require_line_feed()?; Ok(ScannedEntry::Ttl(Ttl::from_secs(ttl))) } else { - Err(EntryError::unknown_control()) + Err(EntryError::unknown_control(ctrl)) } } } @@ -543,10 +599,10 @@ impl Scanner for EntryScanner<'_> { let mut write = 0; let mut builder = None; loop { - self.convert_one_token(&mut convert, &mut write, &mut builder)?; if self.zonefile.buf.is_line_feed() { break; } + self.convert_one_token(&mut convert, &mut write, &mut builder)?; } if let Some(data) = convert.process_tail()? { self.append_data(data, &mut write, &mut builder); @@ -1438,75 +1494,137 @@ enum ItemCat { /// An error returned by the entry scanner. #[derive(Clone, Debug)] -pub struct EntryError(&'static str); +pub struct EntryError { + msg: &'static str, + + #[cfg(feature = "std")] + context: Option, +} impl EntryError { fn bad_symbol(_err: SymbolOctetsError) -> Self { - EntryError("bad symbol") + EntryError { + msg: "bad symbol", + #[cfg(feature = "std")] + context: Some(format!("{}", _err)), + } } fn bad_charstr() -> Self { - EntryError("bad charstr") + EntryError { + msg: "bad charstr", + #[cfg(feature = "std")] + context: None, + } } fn bad_name() -> Self { - EntryError("bad name") + EntryError { + msg: "bad name", + #[cfg(feature = "std")] + context: None, + } } fn unbalanced_parens() -> Self { - EntryError("unbalanced parens") + EntryError { + msg: "unbalanced parens", + #[cfg(feature = "std")] + context: None, + } } fn missing_last_owner() -> Self { - EntryError("missing last owner") + EntryError { + msg: "missing last owner", + #[cfg(feature = "std")] + context: None, + } + } + + fn missing_last_class() -> Self { + EntryError { + msg: "missing last class", + #[cfg(feature = "std")] + context: None, + } } fn missing_origin() -> Self { - EntryError("missing origin") + EntryError { + msg: "missing origin", + #[cfg(feature = "std")] + context: None, + } } fn expected_rtype() -> Self { - EntryError("expected rtype") + EntryError { + msg: "expected rtype", + #[cfg(feature = "std")] + context: None, + } } - fn unknown_control() -> Self { - EntryError("unknown control") + fn unknown_control(ctrl: Str) -> Self { + EntryError { + msg: "unknown control", + #[cfg(feature = "std")] + context: Some(format!("{}", ctrl)), + } + } + + fn different_class(expected_class: Class, found_class: Class) -> Self { + EntryError { + msg: "different class", + #[cfg(feature = "std")] + context: Some(format!("{found_class} != {expected_class}")), + } } } impl ScannerError for EntryError { fn custom(msg: &'static str) -> Self { - EntryError(msg) + EntryError { + msg, + #[cfg(feature = "std")] + context: None, + } } fn end_of_entry() -> Self { - Self("unexpected end of entry") + Self::custom("unexpected end of entry") } fn short_buf() -> Self { - Self("short buffer") + Self::custom("short buffer") } fn trailing_tokens() -> Self { - Self("trailing tokens") + Self::custom("trailing tokens") } } impl From for EntryError { - fn from(_: SymbolOctetsError) -> Self { - EntryError("symbol octets error") + fn from(err: SymbolOctetsError) -> Self { + Self::bad_symbol(err) } } impl From for EntryError { fn from(_: BadSymbol) -> Self { - EntryError("bad symbol") + Self::custom("bad symbol") } } impl fmt::Display for EntryError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(self.0.as_ref()) + f.write_str(self.msg)?; + #[cfg(feature = "std")] + if let Some(context) = &self.context { + write!(f, ": {}", context)?; + } + Ok(()) } } @@ -1601,16 +1719,31 @@ mod test { #[allow(clippy::type_complexity)] struct TestCase { origin: Name, + default_class: Option, zonefile: std::string::String, result: Vec, ZoneRecordData>>>, + #[serde(default)] + allow_invalid: bool, + } + + impl From<&str> for TestCase { + fn from(yaml: &str) -> Self { + serde_yaml::from_str(yaml).unwrap() + } } impl TestCase { - fn test(yaml: &str) { - let case = serde_yaml::from_str::(yaml).unwrap(); + fn test>(case: T) { + let case = case.into(); let mut input = case.zonefile.as_bytes(); let mut zone = Zonefile::load(&mut input).unwrap(); + if case.allow_invalid { + zone = zone.allow_invalid(); + } zone.set_origin(case.origin); + if let Some(class) = case.default_class { + zone.set_default_class(class); + } let mut result = case.result.as_slice(); while let Some(entry) = zone.next_entry().unwrap() { match entry { @@ -1665,6 +1798,37 @@ mod test { )); } + #[test] + fn test_unknown_zero_length_yaml() { + TestCase::test(include_str!( + "../../test-data/zonefiles/unknown-zero-length.yaml" + )); + } + + #[test] + fn test_default_and_last_class() { + TestCase::test(include_str!( + "../../test-data/zonefiles/defaultclass.yaml" + )); + } + + #[test] + #[should_panic(expected = "different class")] + fn test_rfc1035_same_class_validity_check() { + TestCase::test(include_str!( + "../../test-data/zonefiles/mixedclass.yaml" + )); + } + + #[test] + fn test_rfc1035_validity_checks_override() { + let mut case = TestCase::from(include_str!( + "../../test-data/zonefiles/mixedclass.yaml" + )); + case.allow_invalid = true; + TestCase::test(case); + } + #[test] fn test_chrstr_decoding() { TestCase::test(include_str!("../../test-data/zonefiles/strlen.yaml")); diff --git a/src/zonetree/error.rs b/src/zonetree/error.rs index 4fd092d16..7e7903d94 100644 --- a/src/zonetree/error.rs +++ b/src/zonetree/error.rs @@ -289,10 +289,10 @@ impl From for io::Error { match src { ZoneTreeModificationError::Io(err) => err, ZoneTreeModificationError::ZoneDoesNotExist => { - io::Error::new(io::ErrorKind::Other, "zone does not exist") + io::Error::other("zone does not exist") } ZoneTreeModificationError::ZoneExists => { - io::Error::new(io::ErrorKind::Other, "zone exists") + io::Error::other("zone exists") } } } diff --git a/src/zonetree/in_memory/read.rs b/src/zonetree/in_memory/read.rs index 080f2f173..cb3c06b1c 100644 --- a/src/zonetree/in_memory/read.rs +++ b/src/zonetree/in_memory/read.rs @@ -410,3 +410,81 @@ impl NodeAnswer { self.answer } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::base::iana::Class; + use crate::base::name::OwnedLabel; + use crate::base::Ttl; + use crate::rdata::{ZoneRecordData, A}; + use crate::zonetree::StoredName; + use core::str::FromStr; + use core::sync::atomic::{AtomicU8, Ordering}; + use std::boxed::Box; + + #[test] + fn should_walk_below_ents() { + let apex = ZoneApex::new(Name::from_str("c.").unwrap(), Class::IN); + + let version = Version::default(); + let mut rrset = Rrset::new(Rtype::A, Ttl::HOUR); + rrset.push_data(ZoneRecordData::A(A::from_str("1.2.3.4").unwrap())); + let rrset = SharedRrset::new(rrset); + apex.rrsets().update(rrset, version); + + apex.children().with_or_default( + &OwnedLabel::from_str("b").unwrap(), + |node, _bool| { + // TODO: I'm not sure it's good that I have to forcibly mark + // this as NXDOMAIN. I'm concerned that at present edits to a + // zone via the write interface will cause Special::NXDOMAIN + // to be set but that initial zone construction via + // ZoneBuilder will not. That might need to be investigated. + // On the other hand I'm not aware of any actual problems at + // the moment and we're going to revisit the zone + // construction, iterating and querying anyway. Without this + // line the bug this test was created to verify doesn't + // happen, namely that when handling the Special::NxDomain + // case query_node_here_and_below() doesn't descend below an + // ENT. + node.update_special( + Version::default(), + Some(Special::NxDomain), + ); + node.children().with_or_default( + &OwnedLabel::from_str("a").unwrap(), + |node, _bool| { + let version = Version::default(); + let mut rrset = Rrset::new(Rtype::A, Ttl::HOUR); + rrset.push_data(ZoneRecordData::A( + A::from_str("1.2.3.4").unwrap(), + )); + let rrset = SharedRrset::new(rrset); + node.rrsets().update(rrset, version); + }, + ); + }, + ); + + let apex = Arc::new(apex); + + let count = Arc::new(AtomicU8::new(0)); + let count_clone = count.clone(); + + let read = + ReadZone::new(apex, Version::default(), VersionMarker.into()); + let op = Box::new( + move |_owner: StoredName, + _rrset: &SharedRrset, + _at_zone_cut: bool| { + count_clone.fetch_add(1, Ordering::SeqCst); + }, + ); + read.walk(op); + + // Count should be two: c. and a.b.c. + // I.e. a.b.c. isn't missed because it is below the ENT b.c. + assert_eq!(count.load(Ordering::SeqCst), 2); + } +} diff --git a/src/zonetree/in_memory/write.rs b/src/zonetree/in_memory/write.rs index d99631cab..4a92eddbc 100644 --- a/src/zonetree/in_memory/write.rs +++ b/src/zonetree/in_memory/write.rs @@ -265,12 +265,7 @@ impl WritableZone for WriteZone { let res = new_apex .map(|node| Box::new(node) as Box) - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Open error: {err}"), - ) - }); + .map_err(|err| io::Error::other(format!("Open error: {err}"))); Box::pin(ready(res)) } @@ -604,12 +599,7 @@ impl WriteNode { Ok(()) } } - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Write apex error: {err}"), - ) - }) + .map_err(|err| io::Error::other(format!("Write apex error: {err}"))) } fn make_cname(&self, cname: SharedRr) -> Result<(), io::Error> { @@ -623,12 +613,7 @@ impl WriteNode { Ok(()) } } - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Write apex error: {err}"), - ) - }) + .map_err(|err| io::Error::other(format!("Write apex error: {err}"))) } fn remove_all(&self) -> Result<(), io::Error> { @@ -791,10 +776,9 @@ impl From for WriteApexError { impl From for io::Error { fn from(src: WriteApexError) -> io::Error { match src { - WriteApexError::NotAllowed => io::Error::new( - io::ErrorKind::Other, - "operation not allowed at apex", - ), + WriteApexError::NotAllowed => { + io::Error::other("operation not allowed at apex") + } WriteApexError::Io(err) => err, } } diff --git a/src/zonetree/parsed.rs b/src/zonetree/parsed.rs index dc4cab13f..b48f5dd22 100644 --- a/src/zonetree/parsed.rs +++ b/src/zonetree/parsed.rs @@ -82,6 +82,7 @@ impl Zonefile { } /// Inserts the given record into the zone file. + #[allow(clippy::result_large_err)] pub fn insert( &mut self, record: StoredRecord, diff --git a/src/zonetree/update.rs b/src/zonetree/update.rs index e5a1c4865..74263be33 100644 --- a/src/zonetree/update.rs +++ b/src/zonetree/update.rs @@ -650,6 +650,7 @@ mod tests { use crate::base::{ Message, MessageBuilder, Name, ParsedName, Record, Serial, Ttl, }; + use crate::logging::init_logging; use crate::net::xfr::protocol::XfrResponseInterpreter; use crate::rdata::{Ns, Soa, A}; use crate::zonetree::ZoneBuilder; @@ -1275,18 +1276,6 @@ mod tests { //------------ Helper functions ------------------------------------------- - fn init_logging() { - // Initialize tracing based logging. Override with env var RUST_LOG, e.g. - // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step - // numbers and types as they are being executed. - tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) - .with_thread_ids(true) - .without_time() - .try_init() - .ok(); - } - fn mk_empty_zone(apex_name: &str) -> Zone { ZoneBuilder::new(Name::from_str(apex_name).unwrap(), Class::IN) .build() diff --git a/test-data/zonefiles/defaultclass.yaml b/test-data/zonefiles/defaultclass.yaml new file mode 100644 index 000000000..6c3d353f0 --- /dev/null +++ b/test-data/zonefiles/defaultclass.yaml @@ -0,0 +1,23 @@ +origin: example.com. +default_class: CH +zonefile: | + classless 3600 TXT \# 4 03 66 6f 6f + ch 3600 CH A 192.168.0.1 + defaultclass 3600 A 192.168.0.2 +result: + - owner: classless.example.com. + class: CH + ttl: 3600 + data: !Unknown + rtype: Txt + data: 03666f6f + - owner: ch.example.com. + class: CH + ttl: 3600 + data: !A + addr: 192.168.0.1 + - owner: defaultclass.example.com. + class: CH + ttl: 3600 + data: !A + addr: 192.168.0.2 diff --git a/test-data/zonefiles/mixedclass.yaml b/test-data/zonefiles/mixedclass.yaml new file mode 100644 index 000000000..1dc11e2ae --- /dev/null +++ b/test-data/zonefiles/mixedclass.yaml @@ -0,0 +1,16 @@ +origin: example.com. +zonefile: | + classless 3600 IN TXT \# 4 03 66 6f 6f + ch 3600 CH A 192.168.0.1 +result: + - owner: classless.example.com. + class: IN + ttl: 3600 + data: !Unknown + rtype: Txt + data: 03666f6f + - owner: ch.example.com. + class: CH + ttl: 3600 + data: !A + addr: 192.168.0.1 diff --git a/test-data/zonefiles/unknown-zero-length.yaml b/test-data/zonefiles/unknown-zero-length.yaml new file mode 100644 index 000000000..c98725233 --- /dev/null +++ b/test-data/zonefiles/unknown-zero-length.yaml @@ -0,0 +1,10 @@ +origin: example.com. +zonefile: | + example.com. 3600 IN TXT \# 0 +result: + - owner: example.com. + class: IN + ttl: 3600 + data: !Unknown + rtype: Txt + data: diff --git a/tests/interop.rs b/tests/interop.rs index e19d63339..53311097a 100644 --- a/tests/interop.rs +++ b/tests/interop.rs @@ -1,5 +1,5 @@ //! TSIG interop testing with other DNS implementations. -#![cfg(all(feature = "bytes", feature = "std"))] +#![cfg(all(feature = "tsig", feature = "bytes", feature = "std"))] mod common; From 16b8860e4769aeb2b4b2ad2c7f75dacb24e9b1f4 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 2 Jun 2025 14:18:52 +0200 Subject: [PATCH 440/569] Add missing into_inner() fn used by the nameshed poc. --- src/dnssec/sign/records.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dnssec/sign/records.rs b/src/dnssec/sign/records.rs index f5ba87de2..c48a09c1e 100644 --- a/src/dnssec/sign/records.rs +++ b/src/dnssec/sign/records.rs @@ -388,6 +388,10 @@ impl<'a, N, D> OwnerRrs<'a, N, D> { OwnerRrs { slice } } + pub fn into_inner(self) -> &'a [Record] { + self.slice + } + pub fn owner(&self) -> &N { self.slice[0].owner() } From 028ddc8e742acb3316ddc546fceff5dbbfe2bec7 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 2 Jun 2025 14:25:34 +0200 Subject: [PATCH 441/569] Store algorithms and key tags. Prevent duplicate key tags and accidental algorithm rolls. --- examples/keyset.rs | 28 +++++- src/dnssec/sign/keys/keyset.rs | 171 +++++++++++++++++++++++++++++---- 2 files changed, 177 insertions(+), 22 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index b437b1261..250f79055 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -1,4 +1,5 @@ //! Demonstrate the use of key sets. +use domain::base::iana::SecurityAlgorithm; use domain::base::Name; use domain::dnssec::sign::keys::keyset::{ Action, Error, KeySet, KeyType, RollType, UnixTime, @@ -127,11 +128,32 @@ fn do_addkey(filename: &str, args: &[String]) { let mut ks = load_keyset(filename); if keytype == "ksk" { - ks.add_key_ksk(pubref, privref, UnixTime::now()).unwrap(); + ks.add_key_ksk( + pubref, + privref, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); } else if keytype == "zsk" { - ks.add_key_zsk(pubref, privref, UnixTime::now()).unwrap(); + ks.add_key_zsk( + pubref, + privref, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); } else if keytype == "csk" { - ks.add_key_csk(pubref, privref, UnixTime::now()).unwrap(); + ks.add_key_csk( + pubref, + privref, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); } else { eprintln!("Unknown key type '{keytype}'"); exit(1); diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index bd2198850..6dd1cd7d4 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -6,6 +6,7 @@ //! # Example //! //! ```no_run +//! use domain::base::iana::SecurityAlgorithm; //! use domain::base::Name; //! use domain::dnssec::sign::keys::keyset::{KeySet, RollType, UnixTime}; //! use std::fs::File; @@ -18,9 +19,9 @@ //! let mut ks = KeySet::new(Name::from_str("example.com").unwrap()); //! //! // Add two keys. -//! ks.add_key_ksk("first KSK.key".to_string(), None, UnixTime::now()); +//! ks.add_key_ksk("first KSK.key".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); //! ks.add_key_zsk("first ZSK.key".to_string(), -//! Some("first ZSK.private".to_string()), UnixTime::now()); +//! Some("first ZSK.private".to_string()), SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); //! //! // Save the state. //! let json = serde_json::to_string(&ks).unwrap(); @@ -54,17 +55,17 @@ // TODO: // - add support for undo/abort. +use crate::base::iana::SecurityAlgorithm; use crate::base::Name; use crate::rdata::dnssec::Timestamp; use serde::{Deserialize, Serialize}; -use std::collections::{hash_map, HashMap}; +use std::collections::{hash_map, HashMap, HashSet}; use std::fmt; use std::fmt::{Debug, Display, Formatter}; use std::ops::Add; use std::str::FromStr; use std::string::{String, ToString}; use std::time::Duration; -use std::time::SystemTimeError; use std::vec::Vec; use time::format_description; use time::OffsetDateTime; @@ -72,8 +73,11 @@ use time::OffsetDateTime; #[cfg(test)] use mock_instant::global::{SystemTime, UNIX_EPOCH}; +#[cfg(test)] +use mock_instant::SystemTimeError; + #[cfg(not(test))] -use std::time::{SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; /// This type maintains a collection keys used to sign a zone. /// @@ -102,10 +106,21 @@ impl KeySet { &mut self, pubref: String, privref: Option, + algorithm: SecurityAlgorithm, + key_tag: u16, creation_ts: UnixTime, ) -> Result<(), Error> { + if !self.unique_key_tag(key_tag) { + return Err(Error::DuplicateKeyTag); + } let keystate: KeyState = Default::default(); - let key = Key::new(privref, KeyType::Ksk(keystate), creation_ts); + let key = Key::new( + privref, + KeyType::Ksk(keystate), + algorithm, + key_tag, + creation_ts, + ); if let hash_map::Entry::Vacant(e) = self.keys.entry(pubref) { e.insert(key); Ok(()) @@ -119,10 +134,18 @@ impl KeySet { &mut self, pubref: String, privref: Option, + algorithm: SecurityAlgorithm, + key_tag: u16, creation_ts: UnixTime, ) -> Result<(), Error> { let keystate: KeyState = Default::default(); - let key = Key::new(privref, KeyType::Zsk(keystate), creation_ts); + let key = Key::new( + privref, + KeyType::Zsk(keystate), + algorithm, + key_tag, + creation_ts, + ); if let hash_map::Entry::Vacant(e) = self.keys.entry(pubref) { e.insert(key); Ok(()) @@ -136,12 +159,16 @@ impl KeySet { &mut self, pubref: String, privref: Option, + algorithm: SecurityAlgorithm, + key_tag: u16, creation_ts: UnixTime, ) -> Result<(), Error> { let keystate: KeyState = Default::default(); let key = Key::new( privref, KeyType::Csk(keystate.clone(), keystate), + algorithm, + key_tag, creation_ts, ); if let hash_map::Entry::Vacant(e) = self.keys.entry(pubref) { @@ -152,6 +179,10 @@ impl KeySet { } } + fn unique_key_tag(&self, key_tag: u16) -> bool { + !self.keys.iter().any(|(_, k)| k.key_tag == key_tag) + } + /// Delete a key. pub fn delete_key(&mut self, pubref: &str) -> Result<(), Error> { match self.keys.get(pubref) { @@ -355,6 +386,7 @@ impl KeySet { Mode::DryRun => &mut tmpkeys, Mode::ForReal => &mut self.keys, }; + let mut algs_old = HashSet::new(); for k in old { let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -365,8 +397,12 @@ impl KeySet { // Set old for any key we find. keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); } let now = UnixTime::now(); + let mut algs_new = HashSet::new(); for k in new { let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -389,6 +425,14 @@ impl KeySet { keystate.present = true; keystate.signer = true; key.timestamps.published = Some(now.clone()); + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); } // Make sure we have at least one key in incoming state. @@ -415,6 +459,7 @@ impl KeySet { Mode::DryRun => &mut tmpkeys, Mode::ForReal => &mut self.keys, }; + let mut algs_old = HashSet::new(); for k in old { let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -425,8 +470,12 @@ impl KeySet { // Set old for any key we find. keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); } let now = UnixTime::now(); + let mut algs_new = HashSet::new(); for k in new { let Some(key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -448,6 +497,14 @@ impl KeySet { // Move key state to Incoming. keystate.present = true; key.timestamps.published = Some(now.clone()); + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); } // Make sure we have at least one key in incoming state. @@ -474,6 +531,7 @@ impl KeySet { Mode::DryRun => &mut tmpkeys, Mode::ForReal => &mut self.keys, }; + let mut algs_old = HashSet::new(); for k in old { let Some(key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -491,8 +549,12 @@ impl KeySet { return Err(Error::WrongKeyType); } } + + // Add algorithm + algs_old.insert(key.algorithm); } let now = UnixTime::now(); + let mut algs_new = HashSet::new(); for k in new { let Some(key) = keys.get_mut(&(*k).to_string()) else { return Err(Error::KeyNotFound); @@ -567,6 +629,14 @@ impl KeySet { return Err(Error::WrongKeyType); } } + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); } // Make sure we have at least one KSK key in incoming state. @@ -601,6 +671,8 @@ impl KeySet { pub struct Key { privref: Option, keytype: KeyType, + algorithm: SecurityAlgorithm, + key_tag: u16, timestamps: KeyTimestamps, } @@ -615,6 +687,16 @@ impl Key { self.keytype.clone() } + /// Return the public key algorithm. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// Return the key tag. + pub fn key_tag(&self) -> u16 { + self.key_tag + } + /// Return the timestamps. pub fn timestamps(&self) -> &KeyTimestamps { &self.timestamps @@ -623,6 +705,8 @@ impl Key { fn new( privref: Option, keytype: KeyType, + algorithm: SecurityAlgorithm, + key_tag: u16, creation_ts: UnixTime, ) -> Self { let timestamps = KeyTimestamps { @@ -632,6 +716,8 @@ impl Key { Self { privref, keytype, + algorithm, + key_tag, timestamps, } } @@ -988,6 +1074,9 @@ pub enum Error { /// The key cannot be deleted because it is not old. KeyNotOld, + /// Attempt to add key with a key tag that already exists in the KeySet. + DuplicateKeyTag, + /// The key has to wrong type. WrongKeyType, @@ -1003,6 +1092,9 @@ pub enum Error { /// A conflicting key roll is currently in progress. ConflictingRollInProgress, + /// Algorithm set mismatch in non-algorithm key-roll. + AlgorithmSetsMismatch, + /// The operation is too early. The Duration parameter specifies how long /// to wait. Wait(Duration), @@ -1017,6 +1109,7 @@ impl fmt::Display for Error { Error::KeyExists => write!(f, "key already exists"), Error::KeyNotFound => write!(f, "key not found"), Error::KeyNotOld => write!(f, "key is still in use, not old"), + Error::DuplicateKeyTag => write!(f, "Key tag already present"), Error::WrongKeyType => write!(f, "key has the wrong type"), Error::WrongKeyState => write!(f, "key is in the wrong state"), Error::NoSuitableKeyPresent => { @@ -1028,6 +1121,9 @@ impl fmt::Display for Error { Error::ConflictingRollInProgress => { write!(f, "conflicting roll is in progress") } + Error::AlgorithmSetsMismatch => { + write!(f, "algorithm set mismatch for non-algorithm key roll") + } Error::Wait(d) => write!(f, "wait for duration {d:?}"), Error::UnknownRollType => { write!(f, "unable to parse string as roll type") @@ -1544,6 +1640,7 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { #[cfg(test)] mod tests { use crate::base::Name; + use crate::dnssec::sign::keys::keyset::SecurityAlgorithm; use crate::dnssec::sign::keys::keyset::{ Action, KeySet, KeyType, RollType, UnixTime, }; @@ -1565,10 +1662,22 @@ mod tests { fn test_rolls() { let mut ks = KeySet::new(Name::from_str("example.com").unwrap()); - ks.add_key_ksk("first KSK".to_string(), None, UnixTime::now()) - .unwrap(); - ks.add_key_zsk("first ZSK".to_string(), None, UnixTime::now()) - .unwrap(); + ks.add_key_ksk( + "first KSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); + ks.add_key_zsk( + "first ZSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); let actions = ks .start_roll(RollType::CskRoll, &[], &["first KSK", "first ZSK"]) @@ -1629,10 +1738,22 @@ mod tests { let actions = ks.roll_done(RollType::CskRoll).unwrap(); assert_eq!(actions, []); - ks.add_key_ksk("second KSK".to_string(), None, UnixTime::now()) - .unwrap(); - ks.add_key_zsk("second ZSK".to_string(), None, UnixTime::now()) - .unwrap(); + ks.add_key_ksk( + "second KSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); + ks.add_key_zsk( + "second ZSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); println!("line {} = {:?}", line!(), ks.keys().get("second ZSK")); let actions = ks @@ -1750,8 +1871,14 @@ mod tests { assert_eq!(actions, []); ks.delete_key("first KSK").unwrap(); - ks.add_key_csk("first CSK".to_string(), None, UnixTime::now()) - .unwrap(); + ks.add_key_csk( + "first CSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); let actions = ks .start_roll( @@ -1820,8 +1947,14 @@ mod tests { ks.delete_key("second KSK").unwrap(); ks.delete_key("second ZSK").unwrap(); - ks.add_key_csk("second CSK".to_string(), None, UnixTime::now()) - .unwrap(); + ks.add_key_csk( + "second CSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 0, + UnixTime::now(), + ) + .unwrap(); let actions = ks .start_roll(RollType::CskRoll, &["first CSK"], &["second CSK"]) From 8b025f345876b17cbc7831c3ba971f71f5dbf53b Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 4 Jun 2025 11:28:27 +0200 Subject: [PATCH 442/569] Merge remote-tracking branch 'origin/new-zonefile' into patches-for-nameshed-prototype --- Cargo.lock | 31 +- Cargo.toml | 21 +- Changelog.md | 29 +- macros/Cargo.toml | 28 + macros/src/data.rs | 159 ++++ macros/src/impls.rs | 274 +++++++ macros/src/lib.rs | 632 +++++++++++++++ macros/src/repr.rs | 91 +++ src/base/scan.rs | 8 +- src/crypto/openssl.rs | 64 +- src/crypto/sign.rs | 1 + src/dnssec/sign/denial/nsec.rs | 128 ++- src/dnssec/sign/denial/nsec3.rs | 181 +++-- src/dnssec/sign/error.rs | 6 - src/dnssec/sign/mod.rs | 73 +- src/dnssec/sign/records.rs | 20 +- src/dnssec/sign/signatures/rrsigs.rs | 228 +++--- src/dnssec/sign/traits.rs | 33 +- src/lib.rs | 51 +- src/net/client/dgram.rs | 2 +- src/net/client/multi_stream.rs | 2 +- src/net/server/connection.rs | 1 - src/net/server/dgram.rs | 2 +- src/new/base/build/message.rs | 411 ++++++++++ src/new/base/build/mod.rs | 210 +++++ src/new/base/charstr.rs | 467 +++++++++++ src/new/base/message.rs | 674 ++++++++++++++++ src/new/base/mod.rs | 362 +++++++++ src/new/base/name/absolute.rs | 694 ++++++++++++++++ src/new/base/name/compressor.rs | 699 ++++++++++++++++ src/new/base/name/label.rs | 596 ++++++++++++++ src/new/base/name/mod.rs | 201 +++++ src/new/base/name/reversed.rs | 604 ++++++++++++++ src/new/base/name/unparsed.rs | 223 ++++++ src/new/base/parse/message.rs | 140 ++++ src/new/base/parse/mod.rs | 422 ++++++++++ src/new/base/question.rs | 294 +++++++ src/new/base/record.rs | 662 ++++++++++++++++ src/new/base/serial.rs | 125 +++ src/new/base/wire/build.rs | 291 +++++++ src/new/base/wire/ints.rs | 307 +++++++ src/new/base/wire/mod.rs | 105 +++ src/new/base/wire/parse.rs | 590 ++++++++++++++ src/new/base/wire/size_prefixed.rs | 382 +++++++++ src/new/edns/cookie.rs | 244 ++++++ src/new/edns/ext_err.rs | 224 ++++++ src/new/edns/mod.rs | 572 ++++++++++++++ src/new/mod.rs | 66 ++ src/new/rdata/basic/a.rs | 215 +++++ src/new/rdata/basic/cname.rs | 199 +++++ src/new/rdata/basic/hinfo.rs | 229 ++++++ src/new/rdata/basic/mod.rs | 27 + src/new/rdata/basic/mx.rs | 218 +++++ src/new/rdata/basic/ns.rs | 204 +++++ src/new/rdata/basic/ptr.rs | 193 +++++ src/new/rdata/basic/soa.rs | 359 +++++++++ src/new/rdata/basic/txt.rs | 238 ++++++ src/new/rdata/dnssec/dnskey.rs | 139 ++++ src/new/rdata/dnssec/ds.rs | 104 +++ src/new/rdata/dnssec/mod.rs | 69 ++ src/new/rdata/dnssec/nsec.rs | 179 +++++ src/new/rdata/dnssec/nsec3.rs | 262 ++++++ src/new/rdata/dnssec/rrsig.rs | 105 +++ src/new/rdata/edns.rs | 368 +++++++++ src/new/rdata/ipv6.rs | 227 ++++++ src/new/rdata/mod.rs | 746 ++++++++++++++++++ src/resolv/stub/mod.rs | 17 +- src/stelline/matches.rs | 8 +- src/utils/dst.rs | 447 +++++++++++ src/utils/mod.rs | 2 + src/zonefile/inplace.rs | 72 +- src/zonetree/error.rs | 4 +- src/zonetree/in_memory/read.rs | 91 ++- src/zonetree/in_memory/write.rs | 28 +- src/zonetree/parsed.rs | 1 + ...ple_dollar_ttls_multiple_missing_ttls.yaml | 35 + .../multiple_dollar_ttls_no_missing_ttls.yaml | 35 + .../no_dollar_ttl_no_missing_ttls.yaml | 33 + .../no_dollar_ttl_one_missing_ttl.yaml | 33 + .../rfc_1035_class_ttl_type_rdata.yaml | 15 + .../rfc_1035_ttl_class_type_rdata.yaml | 15 + .../top_dollar_ttl_and_missing_ttl.yaml | 34 + .../top_dollar_ttl_no_missing_ttls.yaml | 34 + 83 files changed, 16259 insertions(+), 356 deletions(-) create mode 100644 macros/Cargo.toml create mode 100644 macros/src/data.rs create mode 100644 macros/src/impls.rs create mode 100644 macros/src/lib.rs create mode 100644 macros/src/repr.rs create mode 100644 src/new/base/build/message.rs create mode 100644 src/new/base/build/mod.rs create mode 100644 src/new/base/charstr.rs create mode 100644 src/new/base/message.rs create mode 100644 src/new/base/mod.rs create mode 100644 src/new/base/name/absolute.rs create mode 100644 src/new/base/name/compressor.rs create mode 100644 src/new/base/name/label.rs create mode 100644 src/new/base/name/mod.rs create mode 100644 src/new/base/name/reversed.rs create mode 100644 src/new/base/name/unparsed.rs create mode 100644 src/new/base/parse/message.rs create mode 100644 src/new/base/parse/mod.rs create mode 100644 src/new/base/question.rs create mode 100644 src/new/base/record.rs create mode 100644 src/new/base/serial.rs create mode 100644 src/new/base/wire/build.rs create mode 100644 src/new/base/wire/ints.rs create mode 100644 src/new/base/wire/mod.rs create mode 100644 src/new/base/wire/parse.rs create mode 100644 src/new/base/wire/size_prefixed.rs create mode 100644 src/new/edns/cookie.rs create mode 100644 src/new/edns/ext_err.rs create mode 100644 src/new/edns/mod.rs create mode 100644 src/new/mod.rs create mode 100644 src/new/rdata/basic/a.rs create mode 100644 src/new/rdata/basic/cname.rs create mode 100644 src/new/rdata/basic/hinfo.rs create mode 100644 src/new/rdata/basic/mod.rs create mode 100644 src/new/rdata/basic/mx.rs create mode 100644 src/new/rdata/basic/ns.rs create mode 100644 src/new/rdata/basic/ptr.rs create mode 100644 src/new/rdata/basic/soa.rs create mode 100644 src/new/rdata/basic/txt.rs create mode 100644 src/new/rdata/dnssec/dnskey.rs create mode 100644 src/new/rdata/dnssec/ds.rs create mode 100644 src/new/rdata/dnssec/mod.rs create mode 100644 src/new/rdata/dnssec/nsec.rs create mode 100644 src/new/rdata/dnssec/nsec3.rs create mode 100644 src/new/rdata/dnssec/rrsig.rs create mode 100644 src/new/rdata/edns.rs create mode 100644 src/new/rdata/ipv6.rs create mode 100644 src/new/rdata/mod.rs create mode 100644 src/utils/dst.rs create mode 100644 test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml create mode 100644 test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml create mode 100644 test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml create mode 100644 test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml create mode 100644 test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml create mode 100644 test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml create mode 100644 test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml create mode 100644 test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml diff --git a/Cargo.lock b/Cargo.lock index 67134df8c..f1d9f80a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,12 +240,14 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "domain" -version = "0.10.3" +version = "0.11.1-dev" dependencies = [ "arbitrary", "arc-swap", + "bumpalo", "bytes", "chrono", + "domain-macros", "futures-util", "hashbrown 0.14.5", "heapless", @@ -286,6 +288,15 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "domain-macros" +version = "0.11.1-dev" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.98", +] + [[package]] name = "either" version = "1.14.0" @@ -800,9 +811,9 @@ checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e" [[package]] name = "openssl" -version = "0.10.71" +version = "0.10.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e14130c6a98cd258fdcb0fb6d744152343ff729cbfcb28c656a9d12b999fbcd" +checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" dependencies = [ "bitflags", "cfg-if", @@ -824,14 +835,24 @@ dependencies = [ "syn 2.0.98", ] +[[package]] +name = "openssl-src" +version = "300.5.0+3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" -version = "0.9.106" +version = "0.9.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bb61ea9811cc39e3c2069f40b8b8e2e70d8569b361f879786cc7ed48b777cdd" +checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] diff --git a/Cargo.toml b/Cargo.toml index 609555202..59d28c50d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,10 @@ +[workspace] +resolver = "2" +members = [".", "./macros"] + [package] name = "domain" -version = "0.10.3" +version = "0.11.1-dev" # The MSRV is at least 4 versions behind stable (about half a year). rust-version = "1.79.0" @@ -15,12 +19,11 @@ readme = "README.md" keywords = ["DNS", "domain"] license = "BSD-3-Clause" -[lib] -name = "domain" -path = "src/lib.rs" - [dependencies] +domain-macros = { path = "./macros", version = "=0.11.1-dev" } + arbitrary = { version = "1.4.1", optional = true, features = ["derive"] } +bumpalo = { version = "3.12", optional = true } octseq = { version = "0.5.2", default-features = false } time = { version = "0.3.1", default-features = false } rand = { version = "0.8", optional = true } @@ -35,7 +38,7 @@ libc = { version = "0.2.153", default-features = false, optional = tru log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } -openssl = { version = "0.10.57", optional = true } # 0.10.70 upgrades to 'bitflags' 2.x +openssl = { version = "0.10.72", optional = true } # 0.10.70 upgrades to 'bitflags' 2.x proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } @@ -54,17 +57,20 @@ tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-fil default = ["std", "rand"] # Support for libraries +alloc = [] +bumpalo = ["dep:bumpalo", "std"] bytes = ["dep:bytes", "octseq/bytes"] heapless = ["dep:heapless", "octseq/heapless"] serde = ["dep:serde", "octseq/serde"] smallvec = ["dep:smallvec", "octseq/smallvec"] -std = ["dep:hashbrown", "bytes?/std", "octseq/std", "time/std"] +std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/std", "time/std"] tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] kmip = ["dep:kmip", "dep:r2d2"] +static-openssl = ["openssl/vendored"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] @@ -74,6 +80,7 @@ tsig = ["bytes", "ring", "smallvec"] zonefile = ["bytes", "serde", "std"] # Unstable features +unstable-new = [] unstable-client-cache = ["unstable-client-transport", "moka"] unstable-client-transport = ["moka", "net", "tracing"] unstable-crypto = ["bytes"] diff --git a/Changelog.md b/Changelog.md index a9d98d320..f845bcdc7 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,9 +1,26 @@ # Change Log + ## Unreleased next version Breaking changes +New + +Bug fixes + +* In-place zone parser yields incorrect TTLs. ([538]) + +Other changes + +[#538]: https://github.com/NLnetLabs/domain/pull/538 + +## 0.11.0 + +Released 2025-05-21. + +Breaking changes + * FIX: Use base 16 per RFC 4034 for the DS digest, not base 64. ([#423]) * FIX: NSEC3 salt strings should only be accepted if within the salt size limit. (#431) * Stricter RFC 1035 compliance by default in the `Zonefile` parser. ([#477]) @@ -23,7 +40,7 @@ New running resolver. In combination with `ResolvConf::new()` this can also be used to control the connections made when testing code that uses the stub resolver. ([#440]) -* Add `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], +* Added `ZonefileFmt` trait for printing records as zonefiles. ([#379], [#446], [#463]) Bug fixes @@ -41,9 +58,13 @@ Unstable features * New unstable feature `unstable-crypto-sign` that enable cryptography support including features that rely on secret keys. This feature needs either or both of the features `ring` and `openssl` ([#416]) -* New unstable feature 'unstable-client-cache' that enable the client transport +* New unstable feature `unstable-client-cache` that enable the client transport cache. The reason is that the client cache uses the `moka` crate. +* New unstable feature `unstable-new` that introduces a new API for all of + domain (currently only with `base`, `rdata`, and `edns` modules). Also see + the [associated blog post][new-base-post]. + * `unstable-server-transport` * The trait `SingleService` which is a simplified service trait for requests that should generate a single response ([#353]). @@ -93,9 +114,11 @@ Other changes [#463]: https://github.com/NLnetLabs/domain/pull/463 [#470]: https://github.com/NLnetLabs/domain/pull/470 [#472]: https://github.com/NLnetLabs/domain/pull/472 +[#474]: https://github.com/NLnetLabs/domain/pull/474 [#475]: https://github.com/NLnetLabs/domain/pull/475 -[#4775]: https://github.com/NLnetLabs/domain/pull/477 +[#477]: https://github.com/NLnetLabs/domain/pull/477 [@weilence]: https://github.com/weilence +[new-base-post]: https://blog.nlnetlabs.nl/overhauling-domain/ ## 0.10.4 diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 000000000..a79d619a8 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "domain-macros" + +# Copied from 'domain'. +version = "0.11.1-dev" +rust-version = "1.68.2" +edition = "2021" + +authors = ["NLnet Labs "] +description = "Procedural macros for the `domain` crate." +documentation = "https://docs.rs/domain-macros" +homepage = "https://github.com/nlnetlabs/domain/" +repository = "https://github.com/nlnetlabs/domain/" +keywords = ["DNS", "domain"] +license = "BSD-3-Clause" + +[lib] +proc-macro = true + +[dependencies.proc-macro2] +version = "1.0" + +[dependencies.syn] +version = "2.0" +features = ["full", "visit"] + +[dependencies.quote] +version = "1.0" diff --git a/macros/src/data.rs b/macros/src/data.rs new file mode 100644 index 000000000..ee0c52baf --- /dev/null +++ b/macros/src/data.rs @@ -0,0 +1,159 @@ +//! Working with structs, enums, and unions. + +use std::ops::Deref; + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{spanned::Spanned, Field, Fields, Ident, Index, Member, Token}; + +//----------- Struct --------------------------------------------------------- + +/// A defined 'struct'. +pub struct Struct { + /// The identifier for this 'struct'. + ident: Ident, + + /// The fields in this 'struct'. + fields: Fields, +} + +impl Struct { + /// Construct a [`Struct`] for a 'Self'. + pub fn new_as_self(fields: &Fields) -> Self { + Self { + ident: ::default().into(), + fields: fields.clone(), + } + } + + /// Whether this 'struct' has no fields. + pub fn is_empty(&self) -> bool { + self.fields.is_empty() + } + + /// The number of fields in this 'struct'. + pub fn num_fields(&self) -> usize { + self.fields.len() + } + + /// The fields of this 'struct'. + pub fn fields(&self) -> impl Iterator + '_ { + self.fields.iter() + } + + /// The sized fields of this 'struct'. + pub fn sized_fields(&self) -> impl Iterator + '_ { + self.fields().take(self.num_fields() - 1) + } + + /// The unsized field of this 'struct'. + pub fn unsized_field(&self) -> Option<&Field> { + self.fields.iter().next_back() + } + + /// The names of the fields of this 'struct'. + pub fn members(&self) -> impl Iterator + '_ { + self.fields + .iter() + .enumerate() + .map(|(i, f)| make_member(i, f)) + } + + /// The names of the sized fields of this 'struct'. + pub fn sized_members(&self) -> impl Iterator + '_ { + self.members().take(self.num_fields() - 1) + } + + /// The name of the last field of this 'struct'. + pub fn unsized_member(&self) -> Option { + self.fields + .iter() + .next_back() + .map(|f| make_member(self.num_fields() - 1, f)) + } + + /// Construct a builder for this 'struct'. + pub fn builder Ident>( + &self, + f: F, + ) -> StructBuilder<'_, F> { + StructBuilder { + target: self, + var_fn: f, + } + } +} + +/// Construct a [`Member`] from a field and index. +fn make_member(index: usize, field: &Field) -> Member { + match &field.ident { + Some(ident) => Member::Named(ident.clone()), + None => Member::Unnamed(Index { + index: index as u32, + span: field.ty.span(), + }), + } +} + +//----------- StructBuilder -------------------------------------------------- + +/// A means of constructing a 'struct'. +pub struct StructBuilder<'a, F: Fn(Member) -> Ident> { + /// The 'struct' being constructed. + target: &'a Struct, + + /// A map from field names to constructing variables. + var_fn: F, +} + +impl Ident> StructBuilder<'_, F> { + /// The initializing variables for this 'struct'. + pub fn init_vars(&self) -> impl Iterator + '_ { + self.members().map(&self.var_fn) + } + + /// The names of the sized fields of this 'struct'. + pub fn sized_init_vars(&self) -> impl Iterator + '_ { + self.sized_members().map(&self.var_fn) + } + + /// The name of the last field of this 'struct'. + pub fn unsized_init_var(&self) -> Option { + self.unsized_member().map(&self.var_fn) + } +} + +impl Ident> Deref for StructBuilder<'_, F> { + type Target = Struct; + + fn deref(&self) -> &Self::Target { + self.target + } +} + +impl Ident> ToTokens for StructBuilder<'_, F> { + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.ident; + match self.fields { + Fields::Named(_) => { + let members = self.members(); + let init_vars = self.init_vars(); + quote! { + #ident { #(#members: #init_vars),* } + } + } + + Fields::Unnamed(_) => { + let init_vars = self.init_vars(); + quote! { + #ident ( #(#init_vars),* ) + } + } + + Fields::Unit => { + quote! { #ident } + } + } + .to_tokens(tokens); + } +} diff --git a/macros/src/impls.rs b/macros/src/impls.rs new file mode 100644 index 000000000..4c0971998 --- /dev/null +++ b/macros/src/impls.rs @@ -0,0 +1,274 @@ +//! Helpers for generating `impl` blocks. + +use proc_macro2::{Span, TokenStream}; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + punctuated::Punctuated, visit::Visit, ConstParam, GenericArgument, + GenericParam, Ident, Lifetime, LifetimeParam, Token, TypeParam, + TypeParamBound, WhereClause, WherePredicate, +}; + +//----------- ImplSkeleton --------------------------------------------------- + +/// The skeleton of an `impl` block. +pub struct ImplSkeleton { + /// Lifetime parameters for the `impl` block. + pub lifetimes: Vec, + + /// Type parameters for the `impl` block. + pub types: Vec, + + /// Const generic parameters for the `impl` block. + pub consts: Vec, + + /// Whether the `impl` is unsafe. + pub unsafety: Option, + + /// The trait being implemented. + pub bound: Option, + + /// The type being implemented on. + pub subject: syn::Path, + + /// The where clause of the `impl` block. + pub where_clause: WhereClause, + + /// The contents of the `impl`. + pub contents: syn::Block, + + /// A `const` block for asserting requirements. + pub requirements: syn::Block, +} + +impl ImplSkeleton { + /// Construct an [`ImplSkeleton`] for a [`DeriveInput`]. + pub fn new(input: &syn::DeriveInput, unsafety: bool) -> Self { + let mut lifetimes = Vec::new(); + let mut types = Vec::new(); + let mut consts = Vec::new(); + let mut subject_args = Punctuated::new(); + + for param in &input.generics.params { + match param { + GenericParam::Lifetime(value) => { + lifetimes.push(value.clone()); + let id = value.lifetime.clone(); + subject_args.push(GenericArgument::Lifetime(id)); + } + + GenericParam::Type(value) => { + types.push(value.clone()); + let id = value.ident.clone(); + let id = syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: id, + arguments: syn::PathArguments::None, + }] + .into_iter() + .collect(), + }, + }; + subject_args.push(GenericArgument::Type(id.into())); + } + + GenericParam::Const(value) => { + consts.push(value.clone()); + let id = value.ident.clone(); + let id = syn::TypePath { + qself: None, + path: syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: id, + arguments: syn::PathArguments::None, + }] + .into_iter() + .collect(), + }, + }; + subject_args.push(GenericArgument::Type(id.into())); + } + } + } + + let unsafety = unsafety.then_some(::default()); + + let subject = syn::Path { + leading_colon: None, + segments: [syn::PathSegment { + ident: input.ident.clone(), + arguments: syn::PathArguments::AngleBracketed( + syn::AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Default::default(), + args: subject_args, + gt_token: Default::default(), + }, + ), + }] + .into_iter() + .collect(), + }; + + let where_clause = + input.generics.where_clause.clone().unwrap_or(WhereClause { + where_token: Default::default(), + predicates: Punctuated::new(), + }); + + let contents = syn::Block { + brace_token: Default::default(), + stmts: Vec::new(), + }; + + let requirements = syn::Block { + brace_token: Default::default(), + stmts: Vec::new(), + }; + + Self { + lifetimes, + types, + consts, + unsafety, + bound: None, + subject, + where_clause, + contents, + requirements, + } + } + + /// Require a bound for a type. + /// + /// If the type is concrete, a verifying statement is added for it. + /// Otherwise, it is added to the where clause. + pub fn require_bound( + &mut self, + target: syn::Type, + bound: TypeParamBound, + ) { + let mut visitor = ConcretenessVisitor { + skeleton: self, + is_concrete: true, + }; + + // Concreteness applies to both the type and the bound. + visitor.visit_type(&target); + visitor.visit_type_param_bound(&bound); + + if visitor.is_concrete { + // Add a concrete requirement for this bound. + self.requirements.stmts.push(syn::parse_quote! { + const _: fn() = || { + fn assert_impl() {} + assert_impl::<#target>(); + }; + }); + } else { + // Add this bound to the `where` clause. + let mut bounds = Punctuated::new(); + bounds.push(bound); + let pred = WherePredicate::Type(syn::PredicateType { + lifetimes: None, + bounded_ty: target, + colon_token: Default::default(), + bounds, + }); + self.where_clause.predicates.push(pred); + } + } + + /// Generate a unique lifetime with the given prefix. + pub fn new_lifetime(&self, prefix: &str) -> Lifetime { + [format_ident!("{}", prefix)] + .into_iter() + .chain((0u32..).map(|i| format_ident!("{}_{}", prefix, i))) + .find(|id| self.lifetimes.iter().all(|l| l.lifetime.ident != *id)) + .map(|ident| Lifetime { + apostrophe: Span::call_site(), + ident, + }) + .unwrap() + } + + /// Generate a unique lifetime parameter with the given prefix and bounds. + pub fn new_lifetime_param( + &self, + prefix: &str, + bounds: impl IntoIterator, + ) -> (Lifetime, LifetimeParam) { + let lifetime = self.new_lifetime(prefix); + let mut bounds = bounds.into_iter().peekable(); + let param = if bounds.peek().is_some() { + syn::parse_quote! { #lifetime: #(#bounds)+* } + } else { + syn::parse_quote! { #lifetime } + }; + (lifetime, param) + } +} + +impl ToTokens for ImplSkeleton { + fn to_tokens(&self, tokens: &mut TokenStream) { + let Self { + lifetimes, + types, + consts, + unsafety, + bound, + subject, + where_clause, + contents, + requirements, + } = self; + + let target = match bound { + Some(bound) => quote!(#bound for #subject), + None => quote!(#subject), + }; + + quote! { + #unsafety + impl<#(#lifetimes,)* #(#types,)* #(#consts,)*> + #target + #where_clause + #contents + } + .to_tokens(tokens); + + if !requirements.stmts.is_empty() { + quote! { + const _: () = #requirements; + } + .to_tokens(tokens); + } + } +} + +//----------- ConcretenessVisitor -------------------------------------------- + +struct ConcretenessVisitor<'a> { + /// The `impl` skeleton being added to. + skeleton: &'a ImplSkeleton, + + /// Whether the visited type is concrete. + is_concrete: bool, +} + +impl<'ast> Visit<'ast> for ConcretenessVisitor<'_> { + fn visit_lifetime(&mut self, i: &'ast Lifetime) { + self.is_concrete = self.is_concrete + && self.skeleton.lifetimes.iter().all(|l| l.lifetime != *i); + } + + fn visit_ident(&mut self, i: &'ast Ident) { + self.is_concrete = self.is_concrete + && self.skeleton.types.iter().all(|t| t.ident != *i); + self.is_concrete = self.is_concrete + && self.skeleton.consts.iter().all(|c| c.ident != *i); + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs new file mode 100644 index 000000000..e74068c97 --- /dev/null +++ b/macros/src/lib.rs @@ -0,0 +1,632 @@ +//! Procedural macros for [`domain`]. +//! +//! [`domain`]: https://docs.rs/domain + +use proc_macro as pm; +use proc_macro2::TokenStream; +use quote::{format_ident, ToTokens}; +use syn::{Error, Ident, Result}; + +mod impls; +use impls::ImplSkeleton; + +mod data; +use data::Struct; + +mod repr; +use repr::Repr; + +//----------- SplitBytes ----------------------------------------------------- + +#[proc_macro_derive(SplitBytes)] +pub fn derive_split_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'SplitBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'SplitBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + + // Add the parsing lifetime to the 'impl'. + let (lifetime, param) = skeleton.new_lifetime_param( + "bytes", + skeleton.lifetimes.iter().map(|l| l.lifetime.clone()), + ); + skeleton.lifetimes.push(param); + skeleton.bound = Some( + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + let builder = data.builder(field_prefixed); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + } + + // Define 'parse_bytes()'. + let init_vars = builder.init_vars(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (Self, & #lifetime [::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + #(let (#init_vars, bytes) = + <#tys as ::domain::new::base::wire::SplitBytes<#lifetime>> + ::split_bytes(bytes)?;)* + Ok((#builder, bytes)) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- ParseBytes ----------------------------------------------------- + +#[proc_macro_derive(ParseBytes)] +pub fn derive_parse_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'ParseBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'ParseBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + + // Add the parsing lifetime to the 'impl'. + let (lifetime, param) = skeleton.new_lifetime_param( + "bytes", + skeleton.lifetimes.iter().map(|l| l.lifetime.clone()), + ); + skeleton.lifetimes.push(param); + skeleton.bound = Some( + syn::parse_quote!(::domain::new::base::wire::ParseBytes<#lifetime>), + ); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + let builder = data.builder(field_prefixed); + + // Establish bounds on the fields. + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytes<#lifetime>), + ); + } + if let Some(field) = data.unsized_field() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::ParseBytes<#lifetime>), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + Self, + ::domain::new::base::wire::ParseError, + > { + if bytes.is_empty() { + Ok(#builder) + } else { + Err(::domain::new::base::wire::ParseError) + } + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'parse_bytes()'. + let init_vars = builder.sized_init_vars(); + let tys = builder.sized_fields().map(|f| &f.ty); + let unsized_ty = &builder.unsized_field().unwrap().ty; + let unsized_init_var = builder.unsized_init_var().unwrap(); + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes( + bytes: & #lifetime [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + Self, + ::domain::new::base::wire::ParseError, + > { + #(let (#init_vars, bytes) = + <#tys as ::domain::new::base::wire::SplitBytes<#lifetime>> + ::split_bytes(bytes)?;)* + let #unsized_init_var = + <#unsized_ty as ::domain::new::base::wire::ParseBytes<#lifetime>> + ::parse_bytes(bytes)?; + Ok(#builder) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- SplitBytesZC --------------------------------------------------- + +#[proc_macro_derive(SplitBytesZC)] +pub fn derive_split_bytes_zc(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'SplitBytesZC' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'SplitBytesZC' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "SplitBytesZC")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::SplitBytesZC)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytesZC), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (&Self, &[::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + Ok(( + // SAFETY: 'Self' is a 'struct' with no fields, + // and so has size 0 and alignment 1. It can be + // constructed at any address. + unsafe { &*bytes.as_ptr().cast::() }, + bytes, + )) + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'split_bytes_by_ref()'. + let tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + skeleton.contents.stmts.push(syn::parse_quote! { + fn split_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + (&Self, &[::domain::__core::primitive::u8]), + ::domain::new::base::wire::ParseError, + > { + let start = bytes.as_ptr(); + #(let (_, bytes) = + <#tys as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?;)* + let (last, rest) = + <#unsized_ty as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?; + let ptr = + <#unsized_ty as ::domain::utils::dst::UnsizedCopy> + ::ptr_with_addr(last, start as *const ()); + + // SAFETY: + // - The original 'bytes' contained a valid instance of every + // field in 'Self', in succession. + // - Every field implements 'ParseBytesZC' and so has no + // alignment restriction. + // - 'Self' is unaligned, since every field is unaligned, and + // any explicit alignment modifiers only make it unaligned. + // - 'start' is thus the start of a valid instance of 'Self'. + // - 'ptr' has the same address as 'start' but can be cast to + // 'Self', since it has the right pointer metadata. + Ok((unsafe { &*(ptr as *const Self) }, rest)) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- ParseBytesZC --------------------------------------------------- + +#[proc_macro_derive(ParseBytesZC)] +pub fn derive_parse_bytes_zc(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'ParseBytesZC' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'ParseBytesZC' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "ParseBytesZC")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::ParseBytesZC)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Establish bounds on the fields. + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::SplitBytesZC), + ); + } + if let Some(field) = data.unsized_field() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::ParseBytesZC), + ); + } + + // Finish early if the 'struct' has no fields. + if data.is_empty() { + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + &Self, + ::domain::new::base::wire::ParseError, + > { + if bytes.is_empty() { + // SAFETY: 'Self' is a 'struct' with no fields, + // and so has size 0 and alignment 1. It can be + // constructed at any address. + Ok(unsafe { &*bytes.as_ptr().cast::() }) + } else { + Err(::domain::new::base::wire::ParseError) + } + } + }); + + return Ok(skeleton.into_token_stream()); + } + + // Define 'parse_bytes_by_ref()'. + let tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + skeleton.contents.stmts.push(syn::parse_quote! { + fn parse_bytes_by_ref( + bytes: &[::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + &Self, + ::domain::new::base::wire::ParseError, + > { + let start = bytes.as_ptr(); + #(let (_, bytes) = + <#tys as ::domain::new::base::wire::SplitBytesZC> + ::split_bytes_by_ref(bytes)?;)* + let last = + <#unsized_ty as ::domain::new::base::wire::ParseBytesZC> + ::parse_bytes_by_ref(bytes)?; + let ptr = + <#unsized_ty as ::domain::utils::dst::UnsizedCopy> + ::ptr_with_addr(last, start as *const ()); + + // SAFETY: + // - The original 'bytes' contained a valid instance of every + // field in 'Self', in succession. + // - Every field implements 'ParseBytesZC' and so has no + // alignment restriction. + // - 'Self' is unaligned, since every field is unaligned, and + // any explicit alignment modifiers only make it unaligned. + // - 'start' is thus the start of a valid instance of 'Self'. + // - 'ptr' has the same address as 'start' but can be cast to + // 'Self', since it has the right pointer metadata. + Ok(unsafe { &*(ptr as *const Self) }) + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- BuildBytes ----------------------------------------------------- + +#[proc_macro_derive(BuildBytes)] +pub fn derive_build_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'BuildBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'BuildBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, false); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::BuildBytes)); + + // Inspect the 'struct' fields. + let data = Struct::new_as_self(&data.fields); + + // Get a lifetime for the input buffer. + let lifetime = skeleton.new_lifetime("bytes"); + + // Establish bounds on the fields. + for field in data.fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::BuildBytes), + ); + } + + // Define 'build_bytes()'. + let members = data.members(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn build_bytes<#lifetime>( + &self, + mut bytes: & #lifetime mut [::domain::__core::primitive::u8], + ) -> ::domain::__core::result::Result< + & #lifetime mut [::domain::__core::primitive::u8], + ::domain::new::base::wire::TruncationError, + > { + #(bytes = <#tys as ::domain::new::base::wire::BuildBytes> + ::build_bytes(&self.#members, bytes)?;)* + Ok(bytes) + } + }); + + // Define 'built_bytes_size()'. + let members = data.members(); + let tys = data.fields().map(|f| &f.ty); + skeleton.contents.stmts.push(syn::parse_quote! { + fn built_bytes_size(&self) -> ::domain::__core::primitive::usize { + 0 #(+ <#tys as ::domain::new::base::wire::BuildBytes> + ::built_bytes_size(&self.#members))* + } + }); + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- AsBytes -------------------------------------------------------- + +#[proc_macro_derive(AsBytes)] +pub fn derive_as_bytes(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + let data = match &input.data { + syn::Data::Struct(data) => data, + syn::Data::Enum(data) => { + return Err(Error::new_spanned( + data.enum_token, + "'AsBytes' can only be 'derive'd for 'struct's", + )); + } + syn::Data::Union(data) => { + return Err(Error::new_spanned( + data.union_token, + "'AsBytes' can only be 'derive'd for 'struct's", + )); + } + }; + + let _ = Repr::determine(&input.attrs, "AsBytes")?; + + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::new::base::wire::AsBytes)); + + // Establish bounds on the fields. + for field in data.fields.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::new::base::wire::AsBytes), + ); + } + + // The default implementation of 'as_bytes()' works perfectly. + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- UnsizedCopy ---------------------------------------------------- + +#[proc_macro_derive(UnsizedCopy)] +pub fn derive_unsized_copy(input: pm::TokenStream) -> pm::TokenStream { + fn inner(input: syn::DeriveInput) -> Result { + // Construct an 'ImplSkeleton' so that we can add trait bounds. + let mut skeleton = ImplSkeleton::new(&input, true); + skeleton.bound = + Some(syn::parse_quote!(::domain::utils::dst::UnsizedCopy)); + + let struct_data = match &input.data { + syn::Data::Struct(data) if !data.fields.is_empty() => { + let data = Struct::new_as_self(&data.fields); + for field in data.sized_fields() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + + skeleton.require_bound( + data.unsized_field().unwrap().ty.clone(), + syn::parse_quote!(::domain::utils::dst::UnsizedCopy), + ); + + Some(data) + } + + syn::Data::Struct(_) => None, + + syn::Data::Enum(data) => { + for variant in data.variants.iter() { + for field in variant.fields.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + } + + None + } + + syn::Data::Union(data) => { + for field in data.fields.named.iter() { + skeleton.require_bound( + field.ty.clone(), + syn::parse_quote!(::domain::__core::marker::Copy), + ); + } + + None + } + }; + + if let Some(data) = struct_data { + let sized_tys = data.sized_fields().map(|f| &f.ty); + let unsized_ty = &data.unsized_field().unwrap().ty; + let unsized_member = data.unsized_member().unwrap(); + + skeleton.contents.stmts.push(syn::parse_quote! { + type Alignment = (#(#sized_tys,)* <#unsized_ty as ::domain::utils::dst::UnsizedCopy>::Alignment,); + }); + + skeleton.contents.stmts.push(syn::parse_quote! { + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + ::domain::utils::dst::UnsizedCopy::ptr_with_addr( + &self.#unsized_member, + addr, + ) as *const Self + } + }); + } else { + skeleton.contents.stmts.push(syn::parse_quote! { + type Alignment = Self; + }); + + skeleton.contents.stmts.push(syn::parse_quote! { + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr as *const Self + } + }); + } + + Ok(skeleton.into_token_stream()) + } + + let input = syn::parse_macro_input!(input as syn::DeriveInput); + inner(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +//----------- Utility Functions ---------------------------------------------- + +/// Add a `field_` prefix to member names. +fn field_prefixed(member: syn::Member) -> Ident { + format_ident!("field_{}", member) +} diff --git a/macros/src/repr.rs b/macros/src/repr.rs new file mode 100644 index 000000000..b699b571b --- /dev/null +++ b/macros/src/repr.rs @@ -0,0 +1,91 @@ +//! Determining the memory layout of a type. + +use proc_macro2::Span; +use syn::{ + punctuated::Punctuated, spanned::Spanned, Attribute, Error, LitInt, Meta, + Token, +}; + +//----------- Repr ----------------------------------------------------------- + +/// The memory representation of a type. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub enum Repr { + /// Transparent to an underlying field. + Transparent, + + /// Compatible with C. + C, +} + +impl Repr { + /// Determine the representation for a type from its attributes. + /// + /// This will fail if a stable representation cannot be found. + pub fn determine( + attrs: &[Attribute], + bound: &str, + ) -> Result { + let mut repr = None; + for attr in attrs { + if !attr.path().is_ident("repr") { + continue; + } + + let nested = attr.parse_args_with( + Punctuated::::parse_terminated, + )?; + + // We don't check for consistency in the 'repr' attributes, since + // the compiler should be doing that for us anyway. This lets us + // ignore conflicting 'repr's entirely. + for meta in nested { + match meta { + Meta::Path(p) if p.is_ident("transparent") => { + repr = Some(Repr::Transparent); + } + + Meta::Path(p) if p.is_ident("C") => { + repr = Some(Repr::C); + } + + Meta::Path(p) if p.is_ident("Rust") => { + return Err(Error::new_spanned(p, + format!("repr(Rust) is not stable, cannot derive {bound} for it"))); + } + + Meta::Path(p) if p.is_ident("packed") => { + // The alignment can be set to 1 safely. + } + + Meta::List(meta) + if meta.path.is_ident("packed") + || meta.path.is_ident("aligned") => + { + let span = meta.span(); + let lit: LitInt = syn::parse2(meta.tokens)?; + let n: usize = lit.base10_parse()?; + if n != 1 { + return Err(Error::new(span, + format!("'Self' must be unaligned to derive {bound}"))); + } + } + + meta => { + // We still need to error out here, in case a future + // version of Rust introduces more memory layout data + return Err(Error::new_spanned( + meta, + "unrecognized repr attribute", + )); + } + } + } + } + + repr.ok_or_else(|| { + Error::new(Span::call_site(), + "repr(C) or repr(transparent) must be specified to derive this") + }) + } +} diff --git a/src/base/scan.rs b/src/base/scan.rs index 85756f898..e49d540f8 100644 --- a/src/base/scan.rs +++ b/src/base/scan.rs @@ -283,7 +283,7 @@ declare_error_trait!(ScannerError: Sized + fmt::Debug + fmt::Display); #[cfg(feature = "std")] impl ScannerError for std::io::Error { fn custom(msg: &'static str) -> Self { - std::io::Error::new(std::io::ErrorKind::Other, msg) + std::io::Error::other(msg) } fn end_of_entry() -> Self { @@ -294,11 +294,11 @@ impl ScannerError for std::io::Error { } fn short_buf() -> Self { - std::io::Error::new(std::io::ErrorKind::Other, ShortBuf) + std::io::Error::other(ShortBuf) } fn trailing_tokens() -> Self { - std::io::Error::new(std::io::ErrorKind::Other, "trailing data") + std::io::Error::other("trailing data") } } @@ -1168,7 +1168,7 @@ impl std::error::Error for BadSymbol {} #[cfg(feature = "std")] impl From for std::io::Error { fn from(err: BadSymbol) -> Self { - std::io::Error::new(std::io::ErrorKind::Other, err) + std::io::Error::other(err) } } diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 978f612a7..302044cf0 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -535,13 +535,17 @@ pub mod sign { let id = pkey::Id::ED25519; let s = s.expose_secret(); let k = PKey::private_key_from_raw_bytes(s, id)?; - if memcmp::eq( - &k.raw_public_key().expect("should not fail"), - public.public_key().as_ref(), - ) { - k - } else { + + let pub1 = k.raw_public_key().expect("should not fail"); + let pub2 = public.public_key().as_ref(); + + // The OpenSSL memcmp::eq() fn requires that the given + // arguments be of equal length otherwise it will panic + // so test their length before invoking memcmp::eq(). + if pub1.len() != pub2.len() || !memcmp::eq(&pub1, pub2) { return Err(FromBytesError::InvalidKey); + } else { + k } } @@ -551,13 +555,17 @@ pub mod sign { let id = pkey::Id::ED448; let s = s.expose_secret(); let k = PKey::private_key_from_raw_bytes(s, id)?; - if memcmp::eq( - &k.raw_public_key().expect("should not fail"), - public.public_key().as_ref(), - ) { - k - } else { + + let pub1 = k.raw_public_key().expect("should not fail"); + let pub2 = public.public_key().as_ref(); + + // The OpenSSL memcmp::eq() fn requires that the given + // arguments be of equal length otherwise it will panic + // so test their length before invoking memcmp::eq(). + if pub1.len() != pub2.len() || !memcmp::eq(&pub1, pub2) { return Err(FromBytesError::InvalidKey); + } else { + k } } }; @@ -917,6 +925,38 @@ pub mod sign { } } + #[test] + fn mismatched_public_key() { + for i in 1..KEYS.len() { + if KEYS[i - 1].0 == KEYS[i].0 { + continue; + } + + // Found a pair of keys whose algorithms differ. + let alg1 = KEYS[i - 1].0; + let alg2 = KEYS[i].0; + let key_tag1 = KEYS[i - 1].1; + let key_tag2 = KEYS[i].1; + + let name1 = + format!("test.+{:03}+{:05}", alg1.to_int(), key_tag1); + let path = + format!("test-data/dnssec-keys/K{}.private", name1); + let data = std::fs::read_to_string(path).unwrap(); + let gen_key = SecretKeyBytes::parse_from_bind(&data).unwrap(); + + let name2 = + format!("test.+{:03}+{:05}", alg2.to_int(), key_tag2); + let path = format!("test-data/dnssec-keys/K{}.key", name2); + let data = std::fs::read_to_string(path).unwrap(); + let pub_key = parse_from_bind::>(&data).unwrap(); + + assert!( + KeyPair::from_bytes(&gen_key, pub_key.data()).is_err() + ); + } + } + #[test] fn sign() { for &(algorithm, key_tag) in KEYS { diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index b6d33a244..ba5920304 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -294,6 +294,7 @@ impl From for Box<[u8]> { /// insecure algorithms, that Ring does not support, OpenSSL must be used. #[derive(Debug)] // Note: ring does not implement Clone for KeyPair. +#[allow(clippy::large_enum_variant)] // TODO pub enum KeyPair { /// A key backed by Ring. #[cfg(feature = "ring")] diff --git a/src/dnssec/sign/denial/nsec.rs b/src/dnssec/sign/denial/nsec.rs index 1f00f869b..86f098b17 100644 --- a/src/dnssec/sign/denial/nsec.rs +++ b/src/dnssec/sign/denial/nsec.rs @@ -1,5 +1,5 @@ use core::cmp::min; -use core::fmt::Debug; +use core::fmt::{Debug, Display}; use std::vec::Vec; @@ -67,16 +67,27 @@ impl Default for GenerateNsecConfig { // TODO: Add (mutable?) iterator based variant. #[allow(clippy::type_complexity)] pub fn generate_nsecs( - records: RecordsIter<'_, N, ZoneRecordData>, + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, config: &GenerateNsecConfig, ) -> Result>>, SigningError> where - N: ToName + Clone + PartialEq, + N: ToName + Clone + Display + PartialEq, Octs: FromBuilder, Octs::Builder: EmptyBuilder + Truncate + AsRef<[u8]> + AsMut<[u8]>, ::AppendError: Debug, { - let mut res = Vec::new(); + // The generated collection of NSEC RRs that will be returned to the + // caller. + let mut nsecs = Vec::new(); + + // The CLASS to use for NSEC records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut zone_class = None; + + // The TTL to use for NSEC records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut nsec_ttl = None; // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; @@ -84,16 +95,14 @@ where // Because of the next name thing, we need to keep the last NSEC around. let mut prev: Option<(N, RtypeBitmap)> = None; - // We also need the apex for the last NSEC. - let first_rr = records.first(); - let apex_owner = first_rr.owner().clone(); - let zone_class = first_rr.class(); - let mut ttl = None; + // Skip any glue or other out-of-zone records that sort earlier than + // the zone apex. + records.skip_before(apex_owner); for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(&apex_owner) { + if !owner_rrs.is_in_zone(apex_owner) { break; } @@ -110,18 +119,19 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(&apex_owner) { + cut = if owner_rrs.is_zone_cut(apex_owner) { Some(name.clone()) } else { None }; if let Some((prev_name, bitmap)) = prev.take() { - // SAFETY: ttl will be set below before prev is set to Some. - res.push(Record::new( + // SAFETY: nsec_ttl and zone_class will be set below before prev + // is set to Some. + nsecs.push(Record::new( prev_name.clone(), - zone_class, - ttl.unwrap(), + zone_class.unwrap(), + nsec_ttl.unwrap(), Nsec::new(name.clone(), bitmap), )); } @@ -135,7 +145,7 @@ where bitmap.add(Rtype::RRSIG).unwrap(); if config.assume_dnskeys_will_be_added - && owner_rrs.owner() == &apex_owner + && owner_rrs.owner() == apex_owner { // Assume there's gonna be a DNSKEY. bitmap.add(Rtype::DNSKEY).unwrap(); @@ -180,11 +190,13 @@ where // say that the "TTL of the NSEC(3) RR that is returned MUST // be the lesser of the MINIMUM field of the SOA record and // the TTL of the SOA itself". - ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + nsec_ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + + zone_class = Some(rrset.class()); } } - if ttl.is_none() { + if nsec_ttl.is_none() { return Err(SigningError::SoaRecordCouldNotBeDetermined); } @@ -192,28 +204,31 @@ where } if let Some((prev_name, bitmap)) = prev { - res.push(Record::new( + // SAFETY: nsec_ttl and zone_class will be set above before prev + // is set to Some. + nsecs.push(Record::new( prev_name.clone(), - zone_class, - ttl.unwrap(), + zone_class.unwrap(), + nsec_ttl.unwrap(), Nsec::new(apex_owner.clone(), bitmap), )); } - Ok(res) + Ok(nsecs) } #[cfg(test)] mod tests { use pretty_assertions::assert_eq; - use crate::base::Ttl; + use crate::base::{Name, Ttl}; use crate::dnssec::sign::records::SortedRecords; use crate::dnssec::sign::test_util::*; use crate::zonetree::types::StoredRecordData; use crate::zonetree::StoredName; use super::*; + use core::str::FromStr; type StoredSortedRecords = SortedRecords; @@ -221,8 +236,9 @@ mod tests { fn soa_is_required() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([mk_a_rr("some_a.a.")]); - let res = generate_nsecs(records.owner_rrs(), &cfg); + let res = generate_nsecs(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -233,11 +249,12 @@ mod tests { fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_soa_rr("a.", "d.", "e."), ]); - let res = generate_nsecs(records.owner_rrs(), &cfg); + let res = generate_nsecs(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -248,6 +265,8 @@ mod tests { fn records_outside_zone_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let a_apex = Name::from_str("a.").unwrap(); + let b_apex = Name::from_str("b.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("b.", "d.", "e."), mk_a_rr("some_a.b."), @@ -255,12 +274,9 @@ mod tests { mk_a_rr("some_a.a."), ]); - // First generate NSECs for the total record collection. As the - // collection is sorted in canonical order the a zone preceeds the b - // zone and NSECs should only be generated for the first zone in the - // collection. - let a_and_b_records = records.owner_rrs(); - let nsecs = generate_nsecs(a_and_b_records, &cfg).unwrap(); + // Generate NSEs for the a. zone. + let nsecs = + generate_nsecs(&a_apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -270,11 +286,9 @@ mod tests { ] ); - // Now skip the a zone in the collection and generate NSECs for the - // remaining records which should only generate NSECs for the b zone. - let mut b_records_only = records.owner_rrs(); - b_records_only.skip_before(&mk_name("b.")); - let nsecs = generate_nsecs(b_records_only, &cfg).unwrap(); + // Generate NSECs for the b. zone. + let nsecs = + generate_nsecs(&b_apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -285,17 +299,48 @@ mod tests { ); } + #[test] + fn glue_records_are_ignored() { + let cfg = GenerateNsecConfig::default() + .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("example.").unwrap(); + let records = StoredSortedRecords::from_iter([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Generate NSEs for the a. zone. + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); + + assert_eq!( + nsecs, + [ + mk_nsec_rr( + "example.", + "in_zone.example.", + "NS SOA RRSIG NSEC" + ), + mk_nsec_rr("in_zone.example.", "example.", "A RRSIG NSEC"), + ] + ); + } + #[test] fn occluded_records_are_ignored() { let cfg = GenerateNsecConfig::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("some_ns.a.", "some_a.other.b."), mk_a_rr("some_a.some_ns.a."), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); // Implicit negative test. assert_eq!( @@ -314,12 +359,13 @@ mod tests { fn expect_dnskeys_at_the_apex() { let cfg = GenerateNsecConfig::default(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, @@ -340,8 +386,9 @@ mod tests { "../../../../test-data/zonefiles/rfc4035-appendix-A.zone" ); + let apex = Name::from_str("example.").unwrap(); let records = bytes_to_records(&zonefile[..]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!(nsecs.len(), 10); @@ -453,6 +500,7 @@ mod tests { fn existing_nsec_records_are_ignored() { let cfg = GenerateNsecConfig::default(); + let apex = Name::from_str("a.").unwrap(); let records = StoredSortedRecords::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), @@ -460,7 +508,7 @@ mod tests { mk_nsec_rr("some_a.a.", "a.", "A RRSIG NSEC"), ]); - let nsecs = generate_nsecs(records.owner_rrs(), &cfg).unwrap(); + let nsecs = generate_nsecs(&apex, records.owner_rrs(), &cfg).unwrap(); assert_eq!( nsecs, diff --git a/src/dnssec/sign/denial/nsec3.rs b/src/dnssec/sign/denial/nsec3.rs index 547f22e3b..b734490ef 100644 --- a/src/dnssec/sign/denial/nsec3.rs +++ b/src/dnssec/sign/denial/nsec3.rs @@ -126,11 +126,12 @@ where /// Generate RFC5155 NSEC3 and NSEC3PARAM records for this record set. /// -/// This function does NOT enforce use of current best practice settings, as -/// defined by [RFC 5155], [RFC 9077] and [RFC 9276] which state that: +/// This function enforces [RFC 9077] when it says that the "TTL of the +/// NSEC(3) RR that is returned MUST be the lesser of the MINIMUM field of the +/// SOA record and the TTL of the SOA itself". /// -/// - The `ttl` should be the _"lesser of the MINIMUM field of the zone SOA RR -/// and the TTL of the zone SOA RR itself"_. +/// This function does NOT enforce the use of [RFC 9276] best practices which +/// state that: /// /// - The `params` should be set to _"SHA-1, no extra iterations, empty salt"_ /// and zero flags. See [`Nsec3param::default()`]. @@ -140,14 +141,14 @@ where /// This function may panic if the input records are not sorted in DNSSEC /// canonical order (see [`CanonicalOrd`]). /// -/// [RFC 5155]: https://www.rfc-editor.org/rfc/rfc5155.html /// [RFC 9077]: https://www.rfc-editor.org/rfc/rfc9077.html /// [RFC 9276]: https://www.rfc-editor.org/rfc/rfc9276.html // TODO: Add mutable iterator based variant. // TODO: Get rid of &mut for GenerateNsec3Config. pub fn generate_nsec3s( - records: RecordsIter<'_, N, ZoneRecordData>, - config: &mut GenerateNsec3Config, + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, + config: &GenerateNsec3Config, ) -> Result, SigningError> where N: ToName + Clone + Display + Ord + Hash + Send + From>, @@ -167,31 +168,46 @@ where config.params.opt_out_flag() && config.opt_out_exclude_owner_names_of_unsigned_delegations; + // The generated collection of NSEC3 RRs that will be returned to the + // caller. let mut nsec3s = Vec::>>::new(); + + // A collection of empty non-terminal names (ENTs) discovered while + // walking the zone. NSEC3 RRs will be generated for these RRs as well as + // the RRs explicitly present in the zone. let mut ents = Vec::::new(); + // The number of labels in the apex name. Used when discovering ENTs. + let apex_label_count = apex_owner.iter_labels().count(); + + // The stack of non-empty non-terminal labels currently being walked in the + // zone. Used for implementing RFC 5155 7.1 step 4. + let mut last_nent_stack: Vec = vec![]; + // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; - let first_rr = records.first(); - let apex_owner = first_rr.owner().clone(); - let apex_label_count = apex_owner.iter_labels().count(); + // The TTL to use for NSEC3 records. This will be determined per the rules + // in RFC 9077 once the apex SOA RR is found. + let mut nsec3_ttl = None; - let mut last_nent_stack: Vec = vec![]; - let mut ttl = None; + // The TTL value to be used for the NSEC3PARAM RR. Determined once + // nsec3_ttl is known. let mut nsec3param_ttl = None; + // Skip any glue records that sort earlier than the zone apex. + records.skip_before(apex_owner); + // RFC 5155 7.1 step 2 // For each unique original owner name in the zone add an NSEC3 RR. - for owner_rrs in records { trace!("Owner: {}", owner_rrs.owner()); - // If the owner is out of zone, we have moved out of our zone and are - // done. - if !owner_rrs.is_in_zone(&apex_owner) { + // If the owner is out of zone, we might have moved out of our zone + // and are done. + if !owner_rrs.is_in_zone(apex_owner) { debug!( - "Stopping NSEC3 generation at out-of-zone owner {}", + "Stopping at owner {} as it is out of zone and assumed to trail the zone", owner_rrs.owner() ); break; @@ -216,7 +232,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(&apex_owner) { + cut = if owner_rrs.is_zone_cut(apex_owner) { trace!("Zone cut detected at owner {}", owner_rrs.owner()); Some(name.clone()) } else { @@ -426,7 +442,7 @@ where // say that the "TTL of the NSEC(3) RR that is returned MUST // be the lesser of the MINIMUM field of the SOA record and // the TTL of the SOA itself". - ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); + nsec3_ttl = Some(min(soa_data.minimum(), soa_rr.ttl())); nsec3param_ttl = match config.nsec3param_ttl_mode { Nsec3ParamTtlMode::Fixed(ttl) => Some(ttl), @@ -436,7 +452,7 @@ where } } - if ttl.is_none() { + if nsec3_ttl.is_none() { return Err(SigningError::SoaRecordCouldNotBeDetermined); } @@ -456,9 +472,9 @@ where config.params.flags(), config.params.iterations(), config.params.salt(), - &apex_owner, + apex_owner, bitmap, - ttl.unwrap(), + nsec3_ttl.unwrap(), )?; // Store the record by order of its owner name. @@ -470,6 +486,10 @@ where last_nent_stack.push(name.clone()); } + let Some(nsec3param_ttl) = nsec3param_ttl else { + return Err(SigningError::SoaRecordCouldNotBeDetermined); + }; + for name in ents { // Create the type bitmap, empty for an ENT NSEC3. let bitmap = RtypeBitmap::::builder(); @@ -482,9 +502,9 @@ where config.params.flags(), config.params.iterations(), config.params.salt(), - &apex_owner, + apex_owner, bitmap, - ttl.unwrap(), + nsec3_ttl.unwrap(), )?; // Store the record by order of its owner name. @@ -591,10 +611,6 @@ where nsec3.data_mut().set_next_owner(next_hashed_owner_name); } - let Some(nsec3param_ttl) = nsec3param_ttl else { - return Err(SigningError::SoaRecordCouldNotBeDetermined); - }; - // RFC 5155 7.1 step 8: // "Finally, add an NSEC3PARAM RR with the same Hash Algorithm, // Iterations, and Salt fields to the zone apex." @@ -866,11 +882,12 @@ mod tests { #[test] fn soa_is_required() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([mk_a_rr("some_a.a.")]); - let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -879,13 +896,14 @@ mod tests { #[test] fn multiple_soa_rrs_in_the_same_rrset_are_not_permitted() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_soa_rr("a.", "d.", "e."), ]); - let res = generate_nsec3s(records.owner_rrs(), &mut cfg); + let res = generate_nsec3s(&apex, records.owner_rrs(), &cfg); assert!(matches!( res, Err(SigningError::SoaRecordCouldNotBeDetermined) @@ -894,8 +912,10 @@ mod tests { #[test] fn records_outside_zone_are_ignored() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let a_apex = Name::from_str("a.").unwrap(); + let b_apex = Name::from_str("b.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("b.", "d.", "e."), mk_soa_rr("a.", "b.", "c."), @@ -903,14 +923,9 @@ mod tests { mk_a_rr("some_a.b."), ]); - // First generate NSEC3s for the total record collection. As the - // collection is sorted in canonical order the a zone preceeds the b - // zone and NSEC3s should only be generated for the first zone in the - // collection. - let a_and_b_records = records.owner_rrs(); - + // Generate NSEC3s for the a. zone. let generated_records = - generate_nsec3s(a_and_b_records, &mut cfg).unwrap(); + generate_nsec3s(&a_apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -925,13 +940,9 @@ mod tests { assert_eq!(generated_records.nsec3s, expected_records.into_inner()); - // Now skip the a zone in the collection and generate NSEC3s for the - // remaining records which should only generate NSEC3s for the b zone. - let mut b_records_only = records.owner_rrs(); - b_records_only.skip_before(&mk_name("b.")); - + // Generate NSEC3s for the b. zone. let generated_records = - generate_nsec3s(b_records_only, &mut cfg).unwrap(); + generate_nsec3s(&b_apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -948,10 +959,49 @@ mod tests { assert!(!generated_records.nsec3param.data().opt_out_flag()); } + #[test] + fn glue_records_are_ignored() { + let cfg = GenerateNsec3Config::default() + .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("example.").unwrap(); + let records = SortedRecords::<_, _>::from_iter([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Generate NSEs for the a. zone. + let generated_records = + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); + + let expected_records = SortedRecords::<_, _>::from_iter([ + mk_nsec3_rr( + "example.", + "example.", + "in_zone.example.", + "NS SOA RRSIG NSEC3PARAM", + &cfg, + ), + mk_nsec3_rr( + "example.", + "in_zone.example.", + "example.", + "A RRSIG", + &cfg, + ), + ]); + + assert_eq!(generated_records.nsec3s, expected_records.into_inner()); + } + #[test] fn occluded_records_are_ignored() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("some_ns.a.", "some_a.other.b."), @@ -959,7 +1009,7 @@ mod tests { ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -990,15 +1040,16 @@ mod tests { #[test] fn expect_dnskeys_at_the_apex() { - let mut cfg = GenerateNsec3Config::default(); + let cfg = GenerateNsec3Config::default(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1025,7 +1076,7 @@ mod tests { 12, Nsec3Salt::from_str("aabbccdd").unwrap(), ); - let mut cfg = + let cfg = GenerateNsec3Config::<_, DefaultSorter>::new(nsec3params.clone()) .without_assuming_dnskeys_will_be_added(); @@ -1034,9 +1085,10 @@ mod tests { "../../../../test-data/zonefiles/rfc5155-appendix-A.zone" ); + let apex = Name::from_str("example.").unwrap(); let records = bytes_to_records(&zonefile[..]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); // Generate the expected NSEC3 RRs. The hashes used match those listed // in https://datatracker.ietf.org/doc/html/rfc5155#appendix-A and can @@ -1213,17 +1265,18 @@ mod tests { // This test tests opt-out with exclusion, i.e. opt-out that excludes // an unsigned delegation and thus there "MUST be an Opt-Out NSEC3 // RR...". - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .with_opt_out() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([mk_nsec3_rr( @@ -1250,20 +1303,21 @@ mod tests { // // This test tests opt-out with_out_ exclusion, i.e. opt-out that // creates an NSEC RR for an unsigned delegation. - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .with_opt_out() .without_opt_out_excluding_owner_names_of_unsigned_delegations() .without_assuming_dnskeys_will_be_added(); // This also tests the case of handling a single NSEC3 as only the SOA // RR gets an NSEC3, the NS RR does not. + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_ns_rr("unsigned_delegation.a.", "some.other.zone."), ]); let generated_records = - generate_nsec3s(records.owner_rrs(), &mut cfg).unwrap(); + generate_nsec3s(&apex, records.owner_rrs(), &cfg).unwrap(); let expected_records = SortedRecords::<_, _>::from_iter([ mk_nsec3_rr( @@ -1285,9 +1339,10 @@ mod tests { expected = "All RTYPEs for a single owner name should have been combined into a single NSEC3 RR. Was the input NSEC3 canonically ordered?" )] fn generating_nsec3s_for_unordered_input_should_panic() { - let mut cfg = GenerateNsec3Config::default() + let cfg = GenerateNsec3Config::default() .without_assuming_dnskeys_will_be_added(); + let apex = Name::from_str("a.").unwrap(); let records = vec![ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), @@ -1295,23 +1350,24 @@ mod tests { mk_aaaa_rr("some_a.a."), ]; - let _res = generate_nsec3s(RecordsIter::new(&records), &mut cfg); + let _res = generate_nsec3s(&apex, RecordsIter::new(&records), &cfg); } #[test] fn test_nsec3_hash_collision_handling() { - let mut cfg = GenerateNsec3Config::<_, DefaultSorter>::new( + let cfg = GenerateNsec3Config::<_, DefaultSorter>::new( Nsec3param::default(), ); NSEC3_TEST_MODE.replace(Nsec3TestMode::Colliding); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); assert!(matches!( - generate_nsec3s(records.owner_rrs(), &mut cfg), + generate_nsec3s(&apex, records.owner_rrs(), &cfg), Err(SigningError::Nsec3HashingError( Nsec3HashError::CollisionDetected )) @@ -1320,18 +1376,19 @@ mod tests { #[test] fn test_nsec3_hashing_failure() { - let mut cfg = GenerateNsec3Config::<_, DefaultSorter>::new( + let cfg = GenerateNsec3Config::<_, DefaultSorter>::new( Nsec3param::default(), ); NSEC3_TEST_MODE.replace(Nsec3TestMode::NoHash); + let apex = Name::from_str("a.").unwrap(); let records = SortedRecords::<_, _>::from_iter([ mk_soa_rr("a.", "b.", "c."), mk_a_rr("some_a.a."), ]); assert!(matches!( - generate_nsec3s(records.owner_rrs(), &mut cfg), + generate_nsec3s(&apex, records.owner_rrs(), &cfg), Err(SigningError::Nsec3HashingError( Nsec3HashError::OwnerHashError )) diff --git a/src/dnssec/sign/error.rs b/src/dnssec/sign/error.rs index fdbfef897..1a0791000 100644 --- a/src/dnssec/sign/error.rs +++ b/src/dnssec/sign/error.rs @@ -15,9 +15,6 @@ pub enum SigningError { /// TODO OutOfMemory, - /// At least one key must be provided to sign with. - NoKeysProvided, - // The zone either lacks a SOA record or has more than one SOA record. SoaRecordCouldNotBeDetermined, @@ -49,9 +46,6 @@ impl Display for SigningError { f.write_str("No signature validity period found for key") } SigningError::OutOfMemory => f.write_str("Out of memory"), - SigningError::NoKeysProvided => { - f.write_str("No signing keys provided") - } SigningError::SoaRecordCouldNotBeDetermined => { f.write_str("No apex SOA or too many apex SOA records found") } diff --git a/src/dnssec/sign/mod.rs b/src/dnssec/sign/mod.rs index b8448ef70..f08dbc690 100644 --- a/src/dnssec/sign/mod.rs +++ b/src/dnssec/sign/mod.rs @@ -45,13 +45,16 @@ //! # Usage //! //! - To generate and/or import signing keys see the [`crate::crypto`] module. +//! - To sign apex [`Record`]s involved in the chain of the trust to the +//! parent see the [`sign_rrset()`] function. //! - To sign a collection of [`Record`]s that represent a zone see the //! [`SignableZoneInPlace`] trait. //! - To manage the life cycle of signing keys see the [`keyset`] module. //! //! # Advanced usage //! -//! - For more control over the signing process see the [`SigningConfig`] type. +//! - For more control over the signing process see the [`SigningConfig`] +//! type. //! - For additional ways to sign zones see the [`SignableZone`] trait and the //! [`sign_zone()`] function. //! - To invoke specific stages of the signing process manually see the @@ -138,7 +141,7 @@ use std::fmt::Debug; use std::vec::Vec; use crate::base::{CanonicalOrd, ToName}; -use crate::base::{Name, Record, Rtype}; +use crate::base::{Name, Record}; use crate::rdata::ZoneRecordData; use denial::config::DenialConfig; @@ -293,9 +296,14 @@ where /// DNSSEC sign an unsigned zone using the given configuration and keys. /// /// An implementation of [RFC 4035 section 2 Zone Signing] with optional -/// support for NSEC3 ([RFC 5155]), i.e. it will generate `DNSKEY` (if -/// configured), `NSEC` or `NSEC3` (and if NSEC3 is in use then also -/// `NSEC3PARAM`), and `RRSIG` records. +/// support for NSEC3 ([RFC 5155]), i.e. it will generate `NSEC` or `NSEC3` +/// (and if NSEC3 is in use then also `NSEC3PARAM`), and `RRSIG` records. +/// +/// This function **CANNOT** be used to generate RRSIG RRs for DNSKEY, CDS and +/// CDNSKEY RRs. This function expects those RRs and their RRSIGs to already +/// be present in the zone. To sign DNSKEY, CDS and CDNSKEY RRs the lower +/// level function [`sign_rrset()`] should be used instead. For more +/// information see the [module docs]. /// /// Signing can either be done in-place (records generated by signing will be /// added to the record collection being signed) or into some other provided @@ -354,8 +362,7 @@ where /// currently only supports a visitor style read interface via #[cfg_attr(feature = "unstable-zonetree", doc = "[`ReadableZone`]")] #[cfg_attr(not(feature = "unstable-zonetree"), doc = "`ReadableZone`")] -/// whereby a callback function is invoked for each node -/// that is "walked". +/// whereby a callback function is invoked for each node that is "walked". /// /// # Configuration /// @@ -372,8 +379,9 @@ where /// [`SortedRecords`]: crate::dnssec::sign::records::SortedRecords /// [`Zone`]: crate::zonetree::Zone pub fn sign_zone( + apex_owner: &N, mut in_out: SignableZoneInOut, - signing_config: &mut SigningConfig, + signing_config: &SigningConfig, signing_keys: &[&SigningKey], ) -> Result<(), SigningError> where @@ -403,33 +411,25 @@ where + Default, T: Deref>]>, { - // Iterate over the RR sets of the first owner name (should be the apex as - // the input should be ordered according to [`CanonicalOrd`] and should be - // a complete zone) to find the SOA record. There should be one and only - // one SOA record. - let soa_rr = get_apex_soa_rr(in_out.as_slice())?; - - let apex_owner = soa_rr.owner().clone(); - let owner_rrs = RecordsIter::new(in_out.as_slice()); - match &mut signing_config.denial { + match &signing_config.denial { DenialConfig::AlreadyPresent => { // Nothing to do. } DenialConfig::Nsec(ref cfg) => { - let nsecs = generate_nsecs(owner_rrs, cfg)?; + let nsecs = generate_nsecs(apex_owner, owner_rrs, cfg)?; in_out.sorted_extend(nsecs.into_iter().map(Record::from_record)); } - DenialConfig::Nsec3(ref mut cfg) => { + DenialConfig::Nsec3(ref cfg) => { // RFC 5155 7.1 step 5: "Sort the set of NSEC3 RRs into hash // order." We store the NSEC3s as we create them and sort them // afterwards. let Nsec3Records { nsec3s, nsec3param } = - generate_nsec3s::(owner_rrs, cfg)?; + generate_nsec3s::(apex_owner, owner_rrs, cfg)?; // Add the generated NSEC3 records. in_out.sorted_extend( @@ -440,17 +440,20 @@ where } if !signing_keys.is_empty() { - let mut rrsig_config = GenerateRrsigConfig::new( + let rrsig_config = GenerateRrsigConfig::new( signing_config.inception, signing_config.expiration, ); - rrsig_config.zone_apex = Some(&apex_owner); // Sign the NSEC(3)s. let owner_rrs = RecordsIter::new(in_out.as_out_slice()); - let rrsigs = - sign_sorted_zone_records(owner_rrs, signing_keys, &rrsig_config)?; + let rrsigs = sign_sorted_zone_records( + apex_owner, + owner_rrs, + signing_keys, + &rrsig_config, + )?; // Sorting may not be strictly needed, but we don't have the option to // extend without sort at the moment. @@ -459,25 +462,3 @@ where Ok(()) } - -// Assumes that the given records are sorted in [`CanonicalOrd`] order. -fn get_apex_soa_rr( - slice: &[Record>], -) -> Result<&Record>, SigningError> -where - N: ToName, -{ - let first_owner_rrs = RecordsIter::new(slice) - .next() - .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; - let mut soa_rrs = first_owner_rrs - .records() - .filter(|rr| rr.rtype() == Rtype::SOA); - let soa_rr = soa_rrs - .next() - .ok_or(SigningError::SoaRecordCouldNotBeDetermined)?; - if soa_rrs.next().is_some() { - return Err(SigningError::SoaRecordCouldNotBeDetermined); - } - Ok(soa_rr) -} diff --git a/src/dnssec/sign/records.rs b/src/dnssec/sign/records.rs index c9321a687..f5ba87de2 100644 --- a/src/dnssec/sign/records.rs +++ b/src/dnssec/sign/records.rs @@ -119,7 +119,7 @@ where /// - false: if no matching record was found pub fn remove_all_by_name_class_rtype( &mut self, - name: N, + name: &N, class: Option, rtype: Option, ) -> bool @@ -129,11 +129,7 @@ where { let mut found_one = false; loop { - if self.remove_first_by_name_class_rtype( - name.clone(), - class, - rtype, - ) { + if self.remove_first_by_name_class_rtype(name, class, rtype) { found_one = true } else { break; @@ -151,7 +147,7 @@ where /// - false: if no matching record was found pub fn remove_first_by_name_class_rtype( &mut self, - name: N, + name: &N, class: Option, rtype: Option, ) -> bool @@ -169,7 +165,7 @@ where } } - match stored.owner().name_cmp(&name) { + match stored.owner().name_cmp(name) { Ordering::Equal => {} res => return res, } @@ -205,13 +201,17 @@ where self.rrsets().find(|rrset| rrset.rtype() == Rtype::SOA) } - pub fn find_apex_dnskey(&self, name: &N) -> Option> + pub fn find_apex_rtype( + &self, + name: &N, + rtype: Rtype, + ) -> Option> where N: CanonicalOrd + ToName, D: RecordData, { self.rrsets().find(|rrset| { - rrset.rtype() == Rtype::DNSKEY + rrset.rtype() == rtype && rrset.owner().canonical_cmp(name) == Ordering::Equal }) } diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 00dddfad7..27aeb986f 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -28,50 +28,78 @@ use crate::rdata::{Rrsig, ZoneRecordData}; //------------ GenerateRrsigConfig ------------------------------------------- #[derive(Copy, Clone, Debug, PartialEq)] -pub struct GenerateRrsigConfig<'a, N> { - pub zone_apex: Option<&'a N>, - +pub struct GenerateRrsigConfig { pub inception: Timestamp, pub expiration: Timestamp, } -impl<'a, N> GenerateRrsigConfig<'a, N> { +impl GenerateRrsigConfig { /// Create a new object. pub fn new(inception: Timestamp, expiration: Timestamp) -> Self { Self { - zone_apex: None, inception, expiration, } } - - pub fn with_zone_apex(mut self, zone_apex: &'a N) -> Self { - self.zone_apex = Some(zone_apex); - self - } } //------------ sign_sorted_zone_records -------------------------------------- -/// Generate RRSIG RRs for a collection of zone records. +/// Generate RRSIG records for a collection of zone records. +/// +/// An implementation of [RFC 4035 section 2.2] for generating RRSIG RRs for a +/// zone. +/// +/// This function takes DNS records and signing keys and uses the signing keys +/// to generate and output RRSIG RRs that sign the input records per [RFC +/// 9364]. +/// +/// RRSIG RRs will **NOT** be generated for records: +/// - With RTYPE RRSIG, because [RFC 4035 section 2.2] states that _"An +/// RRSIG RR itself MUST NOT be signed"_. +/// - With RTYPE DNSKEY, CDS or CDNSKEY RR, because, depending on the +/// operational practice (see [RFC 6871]), it may be that these RRs should +/// not be signed using the same key as the rest of the records in the +/// zone. To sign DNSKEY, CDS and CDNSKEY RRs see the [`sign_rrset()`] +/// function. +/// +/// Note: +/// - The input records MUST be sorted according to [`CanonicalOrd`]. +/// - The order of the output records should not be relied upon. +/// +/// # Design rationale /// -/// Returns the collection of RRSIG that must be -/// added to the input records as part of DNSSEC zone signing. +/// The restriction to limit signing to records not involved in the chain of +/// trust with the parent zone is imposed because there is considerable +/// variation and complexity in the strategies used to protect and roll the +/// keys used to sign records in a DNSSEC signed zone. /// -/// The input records MUST be sorted according to [`CanonicalOrd`]. +/// It is common operational practice (see [RFC 6871]) to increase security by +/// using two separate keys to sign the zone. A Key Signing Key aka KSK is +/// used to sign the keys used to establish trust with the parent zone, and a +/// Zone Signing Key aka ZSK is used to sign the rest of the records in the +/// zone, with the KSK signing the ZSK. This allows the ZSK to be rolled +/// without needing to submit information about the new key to the parent zone +/// operator. /// -/// Any RRSIG records in the input will be ignored. New, and replacement (if -/// already present), RRSIGs will be generated and included in the output. +/// Deciding which key to use to sign which records at a given time, +/// especially during key rolls, can be complex. Attempting to cover all +/// possible cases in this function would increase the complexity and +/// fragility and reduce flexibility. As such it is left to the caller to +/// ensure that this is done correctly and doing so also enables the caller to +/// have complete control over the key signing strategy used. /// -/// Note that the order of the output records should not be relied upon and is -/// subject to change. +/// [RFC 4035 section 2.2]: https://www.rfc-editor.org/rfc/rfc4035#section-2.2 +/// [RFC 6871]: https://www.rfc-editor.org/rfc/rfc6871 +/// [RFC 9364]: https://www.rfc-editor.org/rfc/rfc9364 // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] pub fn sign_sorted_zone_records( - records: RecordsIter<'_, N, ZoneRecordData>, + apex_owner: &N, + mut records: RecordsIter<'_, N, ZoneRecordData>, keys: &[&SigningKey], - config: &GenerateRrsigConfig<'_, N>, + config: &GenerateRrsigConfig, ) -> Result>>, SigningError> where Inner: Debug + SignRaw, @@ -92,45 +120,25 @@ where + FromBuilder + From<&'static [u8]>, { - // Peek at the records because we need to process the first owner records - // differently if they represent the apex of a zone (i.e. contain the SOA - // record), otherwise we process the first owner records in the same loop - // as the rest of the records beneath the apex. - let mut records = records.peekable(); - - let first_rrs = records.peek(); - - let Some(first_rrs) = first_rrs else { - // No records were provided. As we are able to generate RRSIGs for - // partial zones this is a special case of a partial zone, an empty - // input, for which there is nothing to do. - return Ok(Vec::new()); - }; - - let first_owner = first_rrs.owner().clone(); - - // If no apex was supplied, assume that because the input should be - // canonically ordered that the first record is part of the apex RRSET. - // Otherwise, check if the first record matches the given apex, if not - // that means that the input starts beneath the apex. - let zone_apex = match config.zone_apex { - Some(zone_apex) => zone_apex, - None => &first_owner, - }; - - if keys.is_empty() { - return Err(SigningError::NoKeysProvided); - } - + // The generated collection of RRSIG RRs that will be returned to the + // caller. let mut rrsigs = Vec::new(); + + // A temporary scratch buffer used when generating signatures that can be + // allocated once and reused for each new signature that we generate. let mut reusable_scratch = Vec::new(); + + // The owner name of a zone cut if we currently are at or below one. let mut cut: Option = None; + // Skip any glue records that sort earlier than the zone apex. + records.skip_before(apex_owner); + // For all records for owner_rrs in records { // If the owner is out of zone, we have moved out of our zone and are // done. - if !owner_rrs.is_in_zone(zone_apex) { + if !owner_rrs.is_in_zone(apex_owner) { break; } @@ -147,7 +155,7 @@ where // If this owner is the parent side of a zone cut, we keep the owner // name for later. This also means below that if `cut.is_some()` we // are at the parent side of a zone. - cut = if owner_rrs.is_zone_cut(zone_apex) { + cut = if owner_rrs.is_zone_cut(apex_owner) { Some(name.clone()) } else { None @@ -162,11 +170,15 @@ where { continue; } - } else if rrset.rtype() == Rtype::DNSKEY - && name.canonical_cmp(zone_apex) == Ordering::Equal + } else if (rrset.rtype() == Rtype::DNSKEY + || rrset.rtype() == Rtype::CDS + || rrset.rtype() == Rtype::CDNSKEY) + && name.canonical_cmp(apex_owner) == Ordering::Equal { - // Ignore the DNSKEY RRset at the apex. Sign other DNSKEY - // RRsets as other records. + // Ignore the DNSKEY, CDS, and CDNSKEY RRsets at the apex. + // Sign other DNSKEY, CDS, and CDNSKEY RRsets as other + // records. + // See RFC 7344 Section 4.1 for CDS and CDNSKEY. continue; } else { // Otherwise we only ignore RRSIGs. @@ -595,11 +607,13 @@ mod tests { #[test] fn generate_rrsigs_without_keys_should_succeed_for_empty_zone() { + let apex = Name::from_str("example.").unwrap(); let records = SortedRecords::::default(); let no_keys: [&SigningKey; 0] = []; sign_sorted_zone_records( + &apex, RecordsIter::new(&records), &no_keys, &GenerateRrsigConfig::new( @@ -611,21 +625,24 @@ mod tests { } #[test] - fn generate_rrsigs_without_keys_should_fail_for_non_empty_zone() { + fn generate_rrsigs_without_keys_generates_no_rrsigs() { + let apex = Name::from_str("example.").unwrap(); let mut records = SortedRecords::default(); records.insert(mk_a_rr("example.")).unwrap(); let no_keys: [&SigningKey; 0] = []; - let res = sign_sorted_zone_records( + let rrsigs = sign_sorted_zone_records( + &apex, RecordsIter::new(&records), &no_keys, &GenerateRrsigConfig::new( TEST_INCEPTION.into(), TEST_EXPIRATION.into(), ), - ); + ) + .unwrap(); - assert!(matches!(res, Err(SigningError::NoKeysProvided))); + assert!(rrsigs.is_empty()); } #[test] @@ -642,6 +659,7 @@ mod tests { // This is an example of generating RRSIGs for something other than a // full zone, in this case just for an A record. This test // deliberately does not include a SOA record as the zone is partial. + let apex = Name::from_str(zone_apex).unwrap(); let mut records = SortedRecords::default(); records.insert(mk_a_rr(record_owner)).unwrap(); @@ -655,13 +673,13 @@ mod tests { // zone RRs. We supply the zone apex because we are not supplying an // entire zone complete with SOA. let generated_records = sign_sorted_zone_records( + &apex, RecordsIter::new(&records), &keys, &GenerateRrsigConfig::new( TEST_INCEPTION.into(), TEST_EXPIRATION.into(), - ) - .with_zone_apex(&mk_name(zone_apex)), + ), ) .unwrap(); @@ -682,6 +700,7 @@ mod tests { #[test] fn generate_rrsigs_ignores_records_outside_the_zone() { + let apex = Name::from_str("example.").unwrap(); let mut records = SortedRecords::default(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), @@ -694,6 +713,7 @@ mod tests { let dnskey = keys[0].dnskey().convert(); let generated_records = sign_sorted_zone_records( + &apex, RecordsIter::new(&records), &keys, &GenerateRrsigConfig::new( @@ -703,6 +723,7 @@ mod tests { ) .unwrap(); + // Check the generated RRSIG records assert_eq!( generated_records, [ @@ -716,12 +737,28 @@ mod tests { ), ] ); + } + + #[test] + fn generate_rrsigs_ignores_glue_records() { + let apex = Name::from_str("example.").unwrap(); + let mut records = SortedRecords::default(); + records.extend([ + mk_soa_rr("example.", "mname.", "rname."), + mk_ns_rr("example.", "early_sorting_glue."), + mk_ns_rr("example.", "late_sorting_glue."), + mk_a_rr("in_zone.example."), + mk_a_rr("early_sorting_glue."), + mk_a_rr("late_sorting_glue."), + ]); + + // Prepare a zone signing key and a key signing key. + let keys = [&mk_dnssec_signing_key(true)]; + let dnskey = keys[0].dnskey().convert(); - // Repeat but this time passing only the out-of-zone record in and - // show that it DOES get signed if not passed together with the first - // zone. let generated_records = sign_sorted_zone_records( - RecordsIter::new(&records[2..]), + &apex, + RecordsIter::new(&records), &keys, &GenerateRrsigConfig::new( TEST_INCEPTION.into(), @@ -733,13 +770,17 @@ mod tests { // Check the generated RRSIG records assert_eq!( generated_records, - [mk_rrsig_rr( - "out_of_zone.", - Rtype::A, - 1, - "example.", - &dnskey - )] + [ + mk_rrsig_rr("example.", Rtype::NS, 1, "example.", &dnskey), + mk_rrsig_rr("example.", Rtype::SOA, 1, "example.", &dnskey), + mk_rrsig_rr( + "in_zone.example.", + Rtype::A, + 2, + "example.", + &dnskey + ), + ] ); } @@ -754,23 +795,20 @@ mod tests { } #[test] - fn generate_rrsigs_for_complete_zone_with_only_zsk_and_fallback_strategy() - { + fn generate_rrsigs_for_complete_zone_with_only_zsk() { let keys = [&mk_dnssec_signing_key(false)]; - - let fallback_cfg = GenerateRrsigConfig::new( + let cfg = GenerateRrsigConfig::new( TEST_INCEPTION.into(), TEST_EXPIRATION.into(), ); - generate_rrsigs_for_complete_zone(&keys, 0, 0, &fallback_cfg) - .unwrap(); + generate_rrsigs_for_complete_zone(&keys, 0, 0, &cfg).unwrap(); } fn generate_rrsigs_for_complete_zone( keys: &[&SigningKey], _ksk_idx: usize, zsk_idx: usize, - cfg: &GenerateRrsigConfig, + cfg: &GenerateRrsigConfig, ) -> Result<(), SigningError> { // See https://datatracker.ietf.org/doc/html/rfc4035#appendix-A let zonefile = include_bytes!( @@ -778,11 +816,16 @@ mod tests { ); // Load the zone to generate RRSIGs for. + let apex = Name::from_str("example.").unwrap(); let records = bytes_to_records(&zonefile[..]); // Generate DNSKEYs and RRSIGs. - let generated_records = - sign_sorted_zone_records(RecordsIter::new(&records), keys, cfg)?; + let generated_records = sign_sorted_zone_records( + &apex, + RecordsIter::new(&records), + keys, + cfg, + )?; let dnskeys = keys .iter() @@ -798,9 +841,9 @@ mod tests { // records must be in canonical order, with the exception of the added // DNSKEY RRs which will be ordered in the order in the supplied // collection of keys to sign with. While we tell users of - // generate_rrsigs() not to rely on the order of the output, we assume - // that we know what that order is for this test, but would have to - // update this test if that order later changes. + // sign_sorted_zone_records() not to rely on the order of the output, + // we assume that we know what that order is for this test, but would + // have to update this test if that order later changes. // // We check each record explicitly by index because assert_eq() on an // array of objects that includes Rrsig produces hard to read output @@ -808,8 +851,9 @@ mod tests { // line. It also wouldn't support dynamically checking for certain // records based on the signing configuration used. - // NOTE: As we only invoked generate_rrsigs() and not generate_nsecs() - // there will not be any RRSIGs covering NSEC records. + // NOTE: As we only invoked sign_sorted_zone_records() and not + // generate_nsecs() there will not be any RRSIGs covering NSEC + // records. // -- example. @@ -961,6 +1005,7 @@ mod tests { fn generate_rrsigs_for_complete_zone_with_multiple_zsks() { let apex = "example."; + let apex_owner = Name::from_str(apex).unwrap(); let mut records = SortedRecords::default(); records.extend([ mk_soa_rr(apex, "some.mname.", "some.rname."), @@ -975,6 +1020,7 @@ mod tests { let zsk2 = keys[1].dnskey().convert(); let generated_records = sign_sorted_zone_records( + &apex_owner, RecordsIter::new(&records), &keys, &GenerateRrsigConfig::new( @@ -1023,6 +1069,7 @@ mod tests { let dnskey = keys[0].dnskey().convert(); + let apex = Name::from_str("example.").unwrap(); let mut records = SortedRecords::default(); records.extend([ // -- example. @@ -1042,6 +1089,7 @@ mod tests { ]); let generated_records = sign_sorted_zone_records( + &apex, RecordsIter::new(&records), &keys, &GenerateRrsigConfig::new( @@ -1058,9 +1106,9 @@ mod tests { // records must be in canonical order, with the exception of the added // DNSKEY RRs which will be ordered in the order in the supplied // collection of keys to sign with. While we tell users of - // generate_rrsigs() not to rely on the order of the output, we assume - // that we know what that order is for this test, but would have to - // update this test if that order later changes. + // sign_sorted_zone_records() not to rely on the order of the output, + // we assume that we know what that order is for this test, but would + // have to update this test if that order later changes. // // We check each record explicitly by index because assert_eq() on an // array of objects that includes Rrsig produces hard to read output diff --git a/src/dnssec/sign/traits.rs b/src/dnssec/sign/traits.rs index acbea146d..50f53f17f 100644 --- a/src/dnssec/sign/traits.rs +++ b/src/dnssec/sign/traits.rs @@ -141,7 +141,7 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); +/// records.insert(Record::new(root.clone(), Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// @@ -149,13 +149,14 @@ where /// let keys = [&key]; /// /// // Create a signing configuration. -/// let mut signing_config = SigningConfig::new(Default::default(), 0.into(), 0.into()); +/// let signing_config = SigningConfig::new(Default::default(), 0.into(), 0.into()); /// /// // Then generate the records which when added to the zone make it signed. /// let mut signer_generated_records = SortedRecords::default(); /// /// records.sign_zone( -/// &mut signing_config, +/// &root, +/// &signing_config, /// &keys, /// &mut signer_generated_records).unwrap(); /// ``` @@ -186,7 +187,8 @@ where /// [`SignableZoneInOut::SignInto`]. fn sign_zone( &self, - signing_config: &mut SigningConfig, + apex_owner: &N, + signing_config: &SigningConfig, signing_keys: &[&SigningKey], out: &mut T, ) -> Result<(), SigningError> @@ -202,6 +204,7 @@ where { let in_out = SignableZoneInOut::new_into(self, out); sign_zone::( + apex_owner, in_out, signing_config, signing_keys, @@ -287,7 +290,7 @@ where /// Ttl::ZERO, /// Ttl::ZERO, /// Ttl::ZERO)); -/// records.insert(Record::new(root, Class::IN, Ttl::ZERO, soa)).unwrap(); +/// records.insert(Record::new(root.clone(), Class::IN, Ttl::ZERO, soa)).unwrap(); /// /// // Generate or import signing keys (see above). /// @@ -295,11 +298,11 @@ where /// let keys = [&key]; /// /// // Create a signing configuration. -/// let mut signing_config: SigningConfig, DefaultSorter> = +/// let signing_config: SigningConfig, DefaultSorter> = /// SigningConfig::new(Default::default(), 0.into(), 0.into()); /// /// // Then sign the zone in-place. -//r records.sign_zone::(&mut signing_config, &keys).unwrap(); +/// records.sign_zone(&root, &signing_config, &keys).unwrap(); /// ``` /// /// [`sign_zone()`]: SignableZoneInPlace::sign_zone @@ -327,7 +330,8 @@ where /// [`SignableZoneInOut::SignInPlace`]. fn sign_zone( &mut self, - signing_config: &mut SigningConfig, + apex_owner: &N, + signing_config: &SigningConfig, signing_keys: &[&SigningKey], ) -> Result<(), SigningError> where @@ -339,6 +343,7 @@ where let in_out = SignableZoneInOut::<_, _, Self, _, _>::new_in_place(self); sign_zone::( + apex_owner, in_out, signing_config, signing_keys, @@ -450,15 +455,19 @@ where #[allow(clippy::type_complexity)] fn sign( &self, - expected_apex: &N, + apex_owner: &N, keys: &[&SigningKey], inception: Timestamp, expiration: Timestamp, ) -> Result>>, SigningError> { - let rrsig_config = GenerateRrsigConfig::new(inception, expiration) - .with_zone_apex(expected_apex); + let rrsig_config = GenerateRrsigConfig::new(inception, expiration); - sign_sorted_zone_records(self.owner_rrs(), keys, &rrsig_config) + sign_sorted_zone_records( + apex_owner, + self.owner_rrs(), + keys, + &rrsig_config, + ) } } diff --git a/src/lib.rs b/src/lib.rs index 70a367f41..624e6b793 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,12 @@ //! A DNS library for Rust. //! //! This crate provides a number of building blocks for developing -//! functionality related to the -//! [Domain Name System (DNS)](https://www.rfc-editor.org/rfc/rfc9499.html). +//! functionality related to the [Domain Name System (DNS)][dns]. +//! +//! [dns]: https://www.rfc-editor.org/rfc/rfc9499.html //! //! The crate uses feature flags to allow you to select only those modules -//! you need for you particular project. In most cases, the feature names +//! you need for your particular project. In most cases, the feature names //! are equal to the module they enable. //! //! # Modules @@ -18,14 +19,8 @@ //! * [rdata] contains types and implementations for a growing number of //! record types. //! -//! In addition to those two basic modules, there are a number of modules for -//! more specific features that are not required in all applications. In order -//! to keep the amount of code to be compiled and the number of dependencies -//! small, these are hidden behind feature flags through which they can be -//! enabled if required. The flags have the same names as the modules or the -//! name prefixed with 'unstable-' if the module is still under development. -//! -//! Currently, there are the following modules: +//! The following additional modules exist, although they are gated behind +//! feature flags (with the same names as the modules): //! #![cfg_attr(feature = "net", doc = "* [net]:")] #![cfg_attr(not(feature = "net"), doc = "* net:")] @@ -37,7 +32,7 @@ #![cfg_attr(feature = "unstable-crypto", doc = "* [crypto]:")] #![cfg_attr(not(feature = "unstable-crypto"), doc = "* crypto:")] //! Experimental support for cryptographic backends, key generation and -//! import. +//! import. Gated behind the `unstable-crypto` flag. //! * [dnssec]: DNSSEC signing and validation. #![cfg_attr(feature = "tsig", doc = "* [tsig]:")] #![cfg_attr(not(feature = "tsig"), doc = "* tsig:")] @@ -48,11 +43,22 @@ //! representation of DNS data. #![cfg_attr(feature = "unstable-zonetree", doc = "* [zonetree]:")] #![cfg_attr(not(feature = "unstable-zonetree"), doc = "* zonetree:")] -//! Experimental storing and querying of zone trees. +//! Experimental storing and querying of zone trees. Gated behind the +//! `unstable-zonetree` flag. //! //! Finally, the [dep] module contains re-exports of some important //! dependencies to help avoid issues with multiple versions of a crate. //! +//! # The `new` Module +//! +//! The API of `domain` is undergoing several large-scale changes, that are +//! collected under the `new` module. It is gated behind the `unstable-new` +//! flag. +#![cfg_attr( + feature = "unstable-new", + doc = "See [its documentation][new] for more information." +)] +//! //! # Reference of feature flags //! //! Several feature flags simply enable support for other crates, e.g. by @@ -178,13 +184,25 @@ #![allow(clippy::uninlined_format_args)] #![cfg_attr(docsrs, feature(doc_cfg))] +#[cfg(feature = "alloc")] +extern crate alloc; + #[cfg(feature = "std")] #[allow(unused_imports)] // Import macros even if unused. #[macro_use] extern crate std; -#[macro_use] -extern crate core; +// The 'domain-macros' crate introduces 'derive' macros which can be used by +// users of the 'domain' crate, but also by the 'domain' crate itself. Within +// those macros, references to declarations in the 'domain' crate are written +// as '::domain::*' ... but this doesn't work when those proc macros are used +// in the 'domain' crate itself. The alias introduced here fixes this: now +// '::domain' means the same thing within this crate as in dependents of it. +extern crate self as domain; + +// Re-export 'core' for use in macros. +#[doc(hidden)] +pub use core as __core; pub mod base; pub mod crypto; @@ -199,4 +217,7 @@ pub mod utils; pub mod zonefile; pub mod zonetree; +#[cfg(feature = "unstable-new")] +pub mod new; + mod logging; diff --git a/src/net/client/dgram.rs b/src/net/client/dgram.rs index 093bc232a..2e7a3de03 100644 --- a/src/net/client/dgram.rs +++ b/src/net/client/dgram.rs @@ -407,7 +407,7 @@ impl QueryError { fn short_send() -> Self { Self::new( QueryErrorKind::Send, - io::Error::new(io::ErrorKind::Other, "short request sent"), + io::Error::other("short request sent"), ) } diff --git a/src/net/client/multi_stream.rs b/src/net/client/multi_stream.rs index c45db3726..5d0652164 100644 --- a/src/net/client/multi_stream.rs +++ b/src/net/client/multi_stream.rs @@ -724,5 +724,5 @@ fn retry_time(retries: u64) -> Duration { /// Helper function to create an empty future that is compatible with the /// future returned by a connection stream. async fn stream_nop() -> Result { - Err(io::Error::new(io::ErrorKind::Other, "nop")) + Err(io::Error::other("nop")) } diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index ed5dc4848..7e29ba7e7 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -935,7 +935,6 @@ where /// Handle I/O errors by deciding whether to log them, and whethr to /// continue or abort. - #[must_use] fn process_io_error(err: io::Error) -> ControlFlow { match err.kind() { io::ErrorKind::UnexpectedEof => { diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 4da2e261c..1b63d6ffd 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -664,7 +664,7 @@ where let sent = send_res?; if sent != data.len() { - Err(io::Error::new(io::ErrorKind::Other, "short send")) + Err(io::Error::other("short send")) } else { Ok(()) } diff --git a/src/new/base/build/message.rs b/src/new/base/build/message.rs new file mode 100644 index 000000000..2cddc9122 --- /dev/null +++ b/src/new/base/build/message.rs @@ -0,0 +1,411 @@ +//! Building whole DNS messages. + +use core::fmt; + +use crate::{ + new::base::{ + wire::{ParseBytesZC, U16}, + Header, HeaderFlags, Message, MessageItem, Question, Record, + SectionCounts, + }, + new::edns::EdnsRecord, +}; + +use super::{BuildBytes, BuildInMessage, NameCompressor, TruncationError}; + +//----------- MessageBuilder ------------------------------------------------- + +/// A builder for a whole DNS message. +pub struct MessageBuilder<'b, 'c> { + /// The message being built. + message: &'b mut Message, + + /// The offset data is being written to. + offset: usize, + + /// The name compressor. + compressor: &'c mut NameCompressor, +} + +//--- Initialization + +impl<'b, 'c> MessageBuilder<'b, 'c> { + /// Begin building a DNS message. + /// + /// The buffer will be initialized with the given message ID and flags. + /// The name compressor will be reset in case it was used before. + /// + /// # Panics + /// + /// Panics if the buffer is less than 12 bytes long (which is the minimum + /// possible size for a DNS message). + #[must_use] + pub fn new( + buffer: &'b mut [u8], + compressor: &'c mut NameCompressor, + id: U16, + flags: HeaderFlags, + ) -> Self { + let message = Message::parse_bytes_in(buffer) + .expect("The caller's buffer is at least 12 bytes big"); + message.header = Header { + id, + flags, + counts: SectionCounts::default(), + }; + // TODO: Reset the name compressor. + Self { + message, + offset: 0, + compressor, + } + } +} + +//--- Inspection + +impl MessageBuilder<'_, '_> { + /// The message header. + #[must_use] + pub fn header(&self) -> &Header { + &self.message.header + } + + /// The message header, mutably. + #[must_use] + pub fn header_mut(&mut self) -> &mut Header { + &mut self.message.header + } + + /// The message built thus far. + #[must_use] + pub fn message(&self) -> &Message { + self.message.truncate(self.offset) + } + + /// The message built thus far, mutably. + /// + /// Compressed names in the message must not be modified here, as the name + /// compressor relies on them. Modifying them will break name compression + /// and result in misformatted messages. + #[must_use] + pub fn message_mut(&mut self) -> &mut Message { + self.message.truncate_mut(self.offset) + } + + /// The name compressor. + #[must_use] + pub fn compressor(&self) -> &NameCompressor { + self.compressor + } +} + +//--- Interaction + +impl<'b> MessageBuilder<'b, '_> { + /// End the builder, returning the built message. + #[must_use] + pub fn finish(self) -> &'b mut Message { + self.message.truncate_mut(self.offset) + } + + /// Reborrow the builder with a shorter lifetime. + #[must_use] + pub fn reborrow(&mut self) -> MessageBuilder<'_, '_> { + MessageBuilder { + message: self.message, + offset: self.offset, + compressor: self.compressor, + } + } + + /// Limit the total message size. + /// + /// The message will not be allowed to exceed the given size, in bytes. + /// Only the message header and contents are counted; the enclosing UDP + /// or TCP packet size is not considered. If the message already exceeds + /// this size, a [`TruncationError`] is returned. + pub fn limit_to(&mut self, size: usize) -> Result<(), TruncationError> { + if 12 + self.offset <= size { + // Move out of 'message' so that the full lifetime is available. + // See the 'replace_with' and 'take_mut' crates. + let size = (size - 12).min(self.message.contents.len()); + let message = unsafe { core::ptr::read(&self.message) }; + // NOTE: Precondition checked, will not panic. + let message = message.truncate_mut(size); + unsafe { core::ptr::write(&mut self.message, message) }; + Ok(()) + } else { + Err(TruncationError) + } + } + + /// Truncate the message. + /// + /// This will remove all message contents and mark it as truncated. + pub fn truncate(&mut self) { + self.message.header.flags.set_tc(true); + self.offset = 0; + // TODO: Reset the name compressor. + } + + /// Append a message item. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push( + &mut self, + item: &MessageItem, + ) -> Result<(), MessageBuildError> + where + N: BuildInMessage, + RD: BuildInMessage, + ED: BuildBytes, + { + // Determine the section number. + let section = match item { + MessageItem::Question(_) => 0, + MessageItem::Answer(_) => 1, + MessageItem::Authority(_) => 2, + MessageItem::Additional(_) => 3, + MessageItem::Edns(_) => 3, + }; + + // Make sure this item is not misplaced. + let counts = self.message.header.counts.as_array_mut(); + if counts[section + 1..].iter().any(|c| c.get() != 0) { + return Err(MessageBuildError::Misplaced); + } + + // Try to build the item. + self.offset = item.build_in_message( + &mut self.message.contents, + self.offset, + self.compressor, + )?; + + // TODO: Reset the name compressor in case of failure. + + // Update the section counts, now that we have succeeded. + counts[section] += 1; + + Ok(()) + } + + /// Append a question. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_question( + &mut self, + question: &Question, + ) -> Result<(), MessageBuildError> { + let question = question.transform_ref(|n| n); + self.push(&MessageItem::<&N, (), ()>::Question(question)) + } + + /// Append an answer record. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_answer( + &mut self, + answer: &Record, + ) -> Result<(), MessageBuildError> { + let answer = answer.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Answer(answer)) + } + + /// Append an authority record. + /// + /// ## Errors + /// + /// If the item cannot be appended (because it needs to come before items + /// already in the message), [`Misplaced`] is returned. If the item does + /// not fit in the message buffer, [`Truncated`] is returned. + /// + /// [`Misplaced`]: MessageBuildError::Misplaced + /// [`Truncated`]: MessageBuildError::Truncated + pub fn push_authority( + &mut self, + authority: &Record, + ) -> Result<(), MessageBuildError> { + let authority = authority.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Authority(authority)) + } + + /// Append an additional record. + /// + /// ## Errors + /// + /// If the item does not fit in the message buffer, [`TruncationError`] is + /// returned. + pub fn push_additional( + &mut self, + additional: &Record, + ) -> Result<(), TruncationError> { + let additional = additional.transform_ref(|n| n, |d| d); + self.push(&MessageItem::<&N, &D, ()>::Additional(additional)) + .map_err(|err| match err { + MessageBuildError::Misplaced => { + unreachable!("An additional record is never misplaced") + } + MessageBuildError::Truncated(err) => err, + }) + } + + /// Append an EDNS record. + /// + /// ## Errors + /// + /// If the item does not fit in the message buffer, [`TruncationError`] is + /// returned. + pub fn push_edns( + &mut self, + edns: &EdnsRecord, + ) -> Result<(), TruncationError> { + let edns = edns.transform_ref(|d| d); + self.push(&MessageItem::<(), (), &D>::Edns(edns)) + .map_err(|err| match err { + MessageBuildError::Misplaced => { + unreachable!("An additional record is never misplaced") + } + MessageBuildError::Truncated(err) => err, + }) + } +} + +//----------- MessageBuildError ---------------------------------------------- + +/// A component of a DNS message could not be built. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum MessageBuildError { + /// A message item was placed in the wrong section. + /// + /// DNS message items (questions, answers, additionals, etc.) must come in + /// a fixed order; this error is returned if an item could not be added in + /// the right order (i.e. items from later sections would come before it). + Misplaced, + + /// A message item was too large to fit. + Truncated(TruncationError), +} + +#[cfg(feature = "std")] +impl std::error::Error for MessageBuildError {} + +impl From for MessageBuildError { + fn from(value: TruncationError) -> Self { + Self::Truncated(value) + } +} + +//--- Formatting + +impl fmt::Display for MessageBuildError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Misplaced => { + "a DNS message item was placed in the wrong order" + } + Self::Truncated(_) => "a DNS message item was too large to fit", + }) + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod test { + use crate::new::base::name::RevNameBuf; + use crate::new::base::wire::U16; + use crate::new::base::{ + HeaderFlags, QClass, QType, Question, RClass, RType, Record, TTL, + }; + use crate::new::rdata::{RecordData, A}; + + use super::{MessageBuilder, NameCompressor}; + + #[test] + fn new() { + let mut buffer = [0u8; 12]; + let mut compressor = NameCompressor::default(); + + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + assert_eq!(&builder.message().contents, &[] as &[u8]); + assert_eq!(&builder.message_mut().contents, &[] as &[u8]); + } + + #[test] + fn build_question() { + let mut buffer = [0u8; 33]; + let mut compressor = NameCompressor::default(); + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + let question = Question:: { + qname: "www.example.org".parse().unwrap(), + qtype: QType::A, + qclass: QClass::IN, + }; + builder.push_question(&question).unwrap(); + + let contents = b"\x03www\x07example\x03org\x00\x00\x01\x00\x01"; + assert_eq!(&builder.message().contents, contents); + } + + #[test] + fn build_record() { + let mut buffer = [0u8; 43]; + let mut compressor = NameCompressor::default(); + let mut builder = MessageBuilder::new( + &mut buffer, + &mut compressor, + U16::new(0), + HeaderFlags::default(), + ); + + let record = Record:: { + rname: "www.example.org".parse().unwrap(), + rtype: RType::A, + rclass: RClass::IN, + ttl: TTL::from(42), + rdata: RecordData::<()>::A(A { + octets: [127, 0, 0, 1], + }), + }; + builder.push_answer(&record).unwrap(); + + assert_eq!(builder.message().header.counts.answers.get(), 1); + let contents = b"\x03www\x07example\x03org\x00\x00\x01\x00\x01\x00\x00\x00\x2A\x00\x04\x7F\x00\x00\x01"; + assert_eq!(&builder.message().contents, contents.as_slice()); + } +} diff --git a/src/new/base/build/mod.rs b/src/new/base/build/mod.rs new file mode 100644 index 000000000..a445879a8 --- /dev/null +++ b/src/new/base/build/mod.rs @@ -0,0 +1,210 @@ +//! Building DNS messages in the wire format. +//! +//! The [`wire`](super::wire) module provides basic serialization capability, +//! but it is not specialized to DNS messages. This module provides that +//! specialization within an ergonomic interface. +//! +//! The core of the high-level interface is [`MessageBuilder`]. It provides +//! the most intuitive methods for appending whole questions and records. +//! +//! ``` +//! use domain::new::base::{ +//! Header, HeaderFlags, Message, +//! Question, QType, QClass, +//! Record, RType, RClass, +//! }; +//! use domain::new::base::build::{AsBytes, MessageBuilder, NameCompressor}; +//! use domain::new::base::name::RevNameBuf; +//! use domain::new::base::wire::U16; +//! use domain::new::rdata::RecordData; +//! +//! // Initialize a DNS message builder. +//! let mut buffer = [0u8; 512]; +//! let mut compressor = NameCompressor::default(); +//! let mut builder = MessageBuilder::new( +//! &mut buffer, +//! &mut compressor, +//! // Select a randomized ID here. +//! U16::new(1234), +//! // A response to a recursive query for authoritative data. +//! *HeaderFlags::default() +//! .set_qr(true) +//! .set_opcode(0) +//! .set_aa(true) +//! .set_rd(true) +//! .set_rcode(0)); +//! +//! // Add a question for an A record. +//! builder.push_question(&Question { +//! qname: "www.example.org".parse::().unwrap(), +//! qtype: QType::A, +//! qclass: QClass::IN, +//! }).unwrap(); +//! +//! // Add an answer. +//! builder.push_answer(&Record { +//! rname: "www.example.org".parse::().unwrap(), +//! rtype: RType::A, +//! rclass: RClass::IN, +//! ttl: 3600.into(), +//! rdata: >::A("127.0.0.1".parse().unwrap()), +//! }).unwrap(); +//! +//! // Use the built message (e.g. send it). +//! let message: &mut Message = builder.finish(); +//! let bytes: &[u8] = message.as_bytes(); +//! # let _ = bytes; +//! ``` + +mod message; +pub use message::{MessageBuildError, MessageBuilder}; + +pub use super::name::NameCompressor; +pub use super::wire::{AsBytes, BuildBytes, TruncationError}; + +//----------- BuildInMessage ------------------------------------------------- + +/// Building into a DNS message. +pub trait BuildInMessage { + /// Write this object in a DNS message. + /// + /// The contents of the DNS message (i.e. the data after the 12-byte + /// header) are stored in a byte buffer, provided here as `contents`. + /// `self` will be serialized and written to `contents[start..]`. + /// + /// Upon success, the position future content should be written to is + /// returned (i.e. `start` + the number of bytes written here). + /// + /// ## Errors + /// + /// Fails if the message buffer is too small to fit the object. Parts of + /// the message buffer (anything after `start`) may have been modified, + /// but should not be considered part of the initialized message. The + /// caller should explicitly reset the name compressor to `start` to undo + /// the effects of this function. + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result; +} + +impl BuildInMessage for &T { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(*self, contents, start, compressor) + } +} + +impl BuildInMessage for () { + fn build_in_message( + &self, + _contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + Ok(start) + } +} + +impl BuildInMessage for u8 { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + match contents.get_mut(start..) { + Some(&mut [ref mut b, ..]) => { + *b = *self; + Ok(start + 1) + } + _ => Err(TruncationError), + } + } +} + +impl BuildInMessage for [T] { + /// Write a sequence of elements to a DNS message. + /// + /// If an element cannot be written due to a truncation error, the whole + /// sequence is considered to have failed. For more nuanced behaviour on + /// truncation, build each element manually. + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + for item in self { + start = item.build_in_message(contents, start, compressor)?; + } + Ok(start) + } +} + +impl BuildInMessage for [T; N] { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.as_slice() + .build_in_message(contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::boxed::Box { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::rc::Rc { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::sync::Arc { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + T::build_in_message(self, contents, start, compressor) + } +} + +#[cfg(feature = "alloc")] +impl BuildInMessage for alloc::vec::Vec { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.as_slice() + .build_in_message(contents, start, compressor) + } +} diff --git a/src/new/base/charstr.rs b/src/new/base/charstr.rs new file mode 100644 index 000000000..596dff529 --- /dev/null +++ b/src/new/base/charstr.rs @@ -0,0 +1,467 @@ +//! DNS "character strings". + +use core::borrow::{Borrow, BorrowMut}; +use core::fmt; +use core::ops::{Deref, DerefMut}; +use core::str::FromStr; + +use crate::utils::dst::{UnsizedCopy, UnsizedCopyFrom}; + +use super::{ + build::{BuildInMessage, NameCompressor}, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError}, +}; + +//----------- CharStr -------------------------------------------------------- + +/// A DNS "character string". +#[derive(UnsizedCopy)] +#[repr(transparent)] +pub struct CharStr { + /// The underlying octets. + /// + /// This is at most 255 bytes. It does not include the length octet that + /// precedes the character string when serialized in the wire format. + pub octets: [u8], +} + +//--- Construction + +impl CharStr { + /// Assume a byte sequence is a valid [`CharStr`]. + /// + /// # Safety + /// + /// The byte sequence does not include the length octet; it simply must be + /// 255 bytes in length or shorter. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'CharStr' is sound. + core::mem::transmute(bytes) + } + + /// Assume a mutable byte sequence is a valid [`CharStr`]. + /// + /// # Safety + /// + /// The byte sequence does not include the length octet; it simply must be + /// 255 bytes in length or shorter. + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'CharStr' is sound. + core::mem::transmute(bytes) + } +} + +//--- Inspection + +impl CharStr { + /// The length of the [`CharStr`]. + /// + /// This is always less than 256 -- it is guaranteed to fit in a [`u8`]. + pub const fn len(&self) -> usize { + self.octets.len() + } + + /// Whether the [`CharStr`] is empty. + pub const fn is_empty(&self) -> bool { + self.octets.is_empty() + } +} + +//--- Parsing from DNS messages + +impl<'a> SplitMessageBytes<'a> for &'a CharStr { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +impl<'a> ParseMessageBytes<'a> for &'a CharStr { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CharStr { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.len() + 1; + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[0] = self.len() as u8; + bytes[1..].copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for &'a CharStr { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let (&length, rest) = bytes.split_first().ok_or(ParseError)?; + if length as usize > rest.len() { + return Err(ParseError); + } + let (bytes, rest) = rest.split_at(length as usize); + + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]'. + Ok((unsafe { core::mem::transmute::<&[u8], Self>(bytes) }, rest)) + } +} + +impl<'a> ParseBytes<'a> for &'a CharStr { + fn parse_bytes(bytes: &'a [u8]) -> Result { + let (&length, rest) = bytes.split_first().ok_or(ParseError)?; + if length as usize != rest.len() { + return Err(ParseError); + } + + // SAFETY: 'CharStr' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], Self>(rest) }) + } +} + +//--- Building into byte sequences + +impl BuildBytes for CharStr { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let (length, bytes) = + bytes.split_first_mut().ok_or(TruncationError)?; + *length = self.octets.len() as u8; + self.octets.build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + 1 + self.octets.len() + } +} + +//--- Equality + +impl PartialEq for CharStr { + fn eq(&self, other: &Self) -> bool { + self.octets.eq_ignore_ascii_case(&other.octets) + } +} + +impl Eq for CharStr {} + +//--- Formatting + +impl fmt::Debug for CharStr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use fmt::Write; + + struct Native<'a>(&'a [u8]); + impl fmt::Debug for Native<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("b\"")?; + for &b in self.0 { + f.write_str(match b { + b'"' => "\\\"", + b' ' => " ", + b'\n' => "\\n", + b'\r' => "\\r", + b'\t' => "\\t", + b'\\' => "\\\\", + + _ => { + if b.is_ascii_graphic() { + f.write_char(b as char)?; + } else { + write!(f, "\\x{:02X}", b)?; + } + continue; + } + })?; + } + f.write_char('"')?; + Ok(()) + } + } + + f.debug_struct("CharStr") + .field("content", &Native(&self.octets)) + .finish() + } +} + +//----------- CharStrBuf ----------------------------------------------------- + +/// A 256-byte buffer for a character string. +#[derive(Clone)] +#[repr(C)] // make layout compatible with '[u8; 256]' +pub struct CharStrBuf { + /// The length of the string, in bytes. + size: u8, + + /// The string contents. + data: [u8; 255], +} + +//--- Construction + +impl CharStrBuf { + /// Construct an empty, invalid buffer. + const fn empty() -> Self { + Self { + size: 0, + data: [0u8; 255], + } + } + + /// Copy a [`CharStrBuf`] into a buffer. + pub fn copy_from(string: &CharStr) -> Self { + let mut this = Self::empty(); + this.size = string.len() as u8; + this.data[..string.len()].copy_from_slice(&string.octets); + this + } +} + +impl UnsizedCopyFrom for CharStrBuf { + type Source = CharStr; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Inspection + +impl CharStrBuf { + /// The wire format for this character string. + pub fn wire_bytes(&self) -> &[u8] { + let ptr = self as *const _ as *const u8; + let len = self.len() + 1; + // SAFETY: 'Self' is 'repr(C)' and contains no padding. It can be + // interpreted as a 256-byte array. + unsafe { core::slice::from_raw_parts(ptr, len) } + } +} + +//--- Parsing from DNS messages + +impl SplitMessageBytes<'_> for CharStrBuf { + fn split_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + <&CharStr>::split_message_bytes(contents, start) + .map(|(this, rest)| (Self::copy_from(this), rest)) + } +} + +impl ParseMessageBytes<'_> for CharStrBuf { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + <&CharStr>::parse_message_bytes(contents, start).map(Self::copy_from) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CharStrBuf { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + name: &mut NameCompressor, + ) -> Result { + CharStr::build_in_message(self, contents, start, name) + } +} + +//--- Parsing from bytes + +impl SplitBytes<'_> for CharStrBuf { + fn split_bytes(bytes: &'_ [u8]) -> Result<(Self, &'_ [u8]), ParseError> { + <&CharStr>::split_bytes(bytes) + .map(|(this, rest)| (Self::copy_from(this), rest)) + } +} + +impl ParseBytes<'_> for CharStrBuf { + fn parse_bytes(bytes: &'_ [u8]) -> Result { + <&CharStr>::parse_bytes(bytes).map(Self::copy_from) + } +} + +//--- Building into byte sequences + +impl BuildBytes for CharStrBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Parsing from strings + +impl FromStr for CharStrBuf { + type Err = CharStrParseError; + + /// Parse a DNS "character-string" from a string. + /// + /// This is intended for easily constructing hard-coded character strings. + /// This function cannot parse all valid character strings; if exceptional + /// instances are needed, use [`CharStr::from_bytes_unchecked()`]. + fn from_str(s: &str) -> Result { + if s.as_bytes().contains(&b'\\') { + Err(CharStrParseError::InvalidChar) + } else if s.len() > 255 { + Err(CharStrParseError::Overlong) + } else { + // SAFETY: 's' is 255 bytes or shorter. + let s = unsafe { CharStr::from_bytes_unchecked(s.as_bytes()) }; + Ok(Self::copy_from(s)) + } + } +} + +//--- Access to the underlying 'CharStr' + +impl Deref for CharStrBuf { + type Target = CharStr; + + fn deref(&self) -> &Self::Target { + let name = &self.data[..self.size as usize]; + // SAFETY: A 'CharStrBuf' always contains a valid 'CharStr'. + unsafe { CharStr::from_bytes_unchecked(name) } + } +} + +impl DerefMut for CharStrBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let name = &mut self.data[..self.size as usize]; + // SAFETY: A 'CharStrBuf' always contains a valid 'CharStr'. + unsafe { CharStr::from_bytes_unchecked_mut(name) } + } +} + +impl Borrow for CharStrBuf { + fn borrow(&self) -> &CharStr { + self + } +} + +impl BorrowMut for CharStrBuf { + fn borrow_mut(&mut self) -> &mut CharStr { + self + } +} + +impl AsRef for CharStrBuf { + fn as_ref(&self) -> &CharStr { + self + } +} + +impl AsMut for CharStrBuf { + fn as_mut(&mut self) -> &mut CharStr { + self + } +} + +//--- Forwarding equality and formatting + +impl PartialEq for CharStrBuf { + fn eq(&self, that: &Self) -> bool { + **self == **that + } +} + +impl Eq for CharStrBuf {} + +impl fmt::Debug for CharStrBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +//----------- CharStrParseError ---------------------------------------------- + +/// An error in parsing a [`CharStr`] from a string. +/// +/// This can be returned by [`CharStrBuf::from_str()`]. It is not used when +/// parsing character strings from the zonefile format, which uses a different +/// mechanism. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CharStrParseError { + /// The character string was too large. + /// + /// Valid character strings are between 0 and 255 bytes, inclusive. + Overlong, + + /// The input contained an invalid character. + InvalidChar, +} + +// TODO(1.81.0): Use 'core::error::Error' instead. +#[cfg(feature = "std")] +impl std::error::Error for CharStrParseError {} + +impl fmt::Display for CharStrParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Overlong => "the character string was too long", + Self::InvalidChar => { + "the character string contained an invalid character" + } + }) + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod test { + use super::CharStr; + + use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, + }; + + #[test] + fn parse_build() { + let bytes = b"\x05Hello!"; + let (charstr, rest) = <&CharStr>::split_bytes(bytes).unwrap(); + assert_eq!(&charstr.octets, b"Hello"); + assert_eq!(rest, b"!"); + + assert_eq!(<&CharStr>::parse_bytes(bytes), Err(ParseError)); + assert!(<&CharStr>::parse_bytes(&bytes[..6]).is_ok()); + + let mut buffer = [0u8; 6]; + assert_eq!( + charstr.build_bytes(&mut buffer), + Ok(&mut [] as &mut [u8]) + ); + assert_eq!(buffer, &bytes[..6]); + } +} diff --git a/src/new/base/message.rs b/src/new/base/message.rs new file mode 100644 index 000000000..411c75be4 --- /dev/null +++ b/src/new/base/message.rs @@ -0,0 +1,674 @@ +//! DNS message headers. + +use core::fmt; + +use domain_macros::*; + +use crate::new::edns::EdnsRecord; + +use super::build::{BuildInMessage, NameCompressor}; +use super::parse::MessageParser; +use super::wire::{AsBytes, BuildBytes, ParseBytesZC, TruncationError, U16}; +use super::{Question, Record}; + +//----------- Message -------------------------------------------------------- + +/// A DNS message. +#[derive(AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy)] +#[repr(C, packed)] +pub struct Message { + /// The message header. + pub header: Header, + + /// The message contents. + pub contents: [u8], +} + +//--- Inspection + +impl Message { + /// Represent this as a mutable byte sequence. + /// + /// Given `&mut self`, it is already possible to individually modify the + /// message header and contents; since neither has invalid instances, it + /// is safe to represent the entire object as mutable bytes. + pub fn as_bytes_mut(&mut self) -> &mut [u8] { + // SAFETY: + // - 'Self' has no padding bytes and no interior mutability. + // - Its size in memory is exactly 'size_of_val(self)'. + unsafe { + core::slice::from_raw_parts_mut( + self as *mut Self as *mut u8, + core::mem::size_of_val(self), + ) + } + } +} + +//--- Parsing + +impl Message { + /// Parse the questions and records in this message. + /// + /// This returns a fallible iterator of [`MessageItem`]s. + pub const fn parse(&self) -> MessageParser<'_> { + MessageParser::for_message(self) + } +} + +//--- Interaction + +impl Message { + /// Truncate the contents of this message to the given size. + /// + /// The returned value will have a `contents` field of the given size. + pub fn truncate(&self, size: usize) -> &Self { + let bytes = &self.as_bytes()[..12 + size]; + // SAFETY: 'bytes' is at least 12 bytes, making it a valid 'Message'. + unsafe { Self::parse_bytes_by_ref(bytes).unwrap_unchecked() } + } + + /// Truncate the contents of this message to the given size, mutably. + /// + /// The returned value will have a `contents` field of the given size. + pub fn truncate_mut(&mut self, size: usize) -> &mut Self { + let bytes = &mut self.as_bytes_mut()[..12 + size]; + // SAFETY: 'bytes' is at least 12 bytes, making it a valid 'Message'. + unsafe { Self::parse_bytes_in(bytes).unwrap_unchecked() } + } + + /// Truncate the contents of this message to the given size, by pointer. + /// + /// The returned value will have a `contents` field of the given size. + /// + /// # Safety + /// + /// This method uses `pointer::offset()`: `self` must be "derived from a + /// pointer to some allocated object". There must be at least 12 bytes + /// between `self` and the end of that allocated object. A reference to + /// `Message` will always result in a pointer satisfying this. + pub unsafe fn truncate_ptr(this: *mut Message, size: usize) -> *mut Self { + // Extract the metadata from 'this'. We know it's slice metadata. + // + // SAFETY: '[()]' is a zero-sized type and references to it can be + // created from arbitrary pointers, since every pointer is valid for + // zero-sized reads. + let len = unsafe { &*(this as *mut [()]) }.len(); + // Replicate the range check performed by normal indexing operations. + debug_assert!(size <= len); + core::ptr::slice_from_raw_parts_mut(this.cast::(), size) + as *mut Self + } +} + +//----------- Header --------------------------------------------------------- + +/// A DNS message header. +#[derive( + Copy, + Clone, + Debug, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct Header { + /// A unique identifier for the message. + pub id: U16, + + /// Properties of the message. + pub flags: HeaderFlags, + + /// Counts of objects in the message. + pub counts: SectionCounts, +} + +//--- Formatting + +impl fmt::Display for Header { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} of ID {:04X} ({})", + self.flags, + self.id.get(), + self.counts + ) + } +} + +//----------- HeaderFlags ---------------------------------------------------- + +/// DNS message header flags. +/// +/// This 16-bit field provides information about the containing DNS message. +/// Its contents define the purpose of the message, e.g. whether it is a query +/// or a response. Due to its small size, it doesn't cover everything; the +/// OPT record may provide additional information, if it is present. +/// +/// # Specification +/// +// TODO: Update regularly. +// +/// The header field has been updated by several RFCs and the interpretation +/// of its bits has changed in some places. The following is a collection of +/// the relevant RFC notes; it is up-to-date as of *2025-04-03*. +/// +/// The descriptions here are specific to the `QUERY` opcode, which is by far +/// the most common. Other opcodes can change the interpretation of the bits +/// here. +/// +/// ```text +/// 15 14 13 12 11 10 9 8 +/// +----+----+----+----+----+----+----+----+ +/// | QR | OPCODE | AA | TC | RD | } MSB +/// +----+----+----+----+----+----+----+----+ +/// | RA | | AD | CD | RCODE | } LSB +/// +----+----+----+----+----+----+----+----+ +/// 7 6 5 4 3 2 1 0 +/// ``` +/// +/// Here is a short description of each field. +/// +/// - `QR` (Query or Response): set if and only if the message is a response. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `OPCODE`: the specific operation requested by the DNS client. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `AA`: whether the DNS server is authoritative for the primary answer. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `TC`: whether the response is truncated (due to channel limitations). +/// +/// Specified by [RFC 1035, section 4.1.1]. Behaviour clarified by [RFC +/// 2181, section 9]. Behaviour for DNSSEC servers specified by [RFC 4035, +/// section 3.1]. +/// +/// - `RD`: whether the DNS client wishes for a recursively resolved answer. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `RA`: whether the DNS server supports recursive resolution. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// - `AD`: whether the DNS server has authenticated the answer. +/// +/// Defined by [RFC 2535, section 6.1]. Behaviour for authoritative name +/// servers specified by [RFC 4035, section 3.1.6]. Behaviour for recursive +/// name servers specified by [RFC 4035, section 3.2.3] and updated by [RFC +/// 6840, section 5.8]. Behaviour for DNS clients specified by [RFC 6840, +/// section 5.7]. +/// +/// - `CD`: whether the DNS server should avoid authenticating the answer. +/// +/// Defined by [RFC 2535, section 6.1]. Behaviour for authoritative name +/// servers specified by [RFC 4035, section 3.1.6]. Behaviour for recursive +/// name servers specified by [RFC 4035, section 3.2.2] and updated by [RFC +/// 6840, section 5.9]. +/// +/// - `RCODE`: the response status of the DNS server. +/// +/// Specified by [RFC 1035, section 4.1.1]. +/// +/// [RFC 1035, section 4.1.1]: https://datatracker.ietf.org/doc/html/rfc1035#section-4.1.1 +/// [RFC 2181, section 9]: https://datatracker.ietf.org/doc/html/rfc2181#section-9 +/// [RFC 2535, section 6.1]: https://datatracker.ietf.org/doc/html/rfc2535#section-6.1 +/// [RFC 4035, section 3.1]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.1 +/// [RFC 4035, section 3.1.6]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.1.6 +/// [RFC 4035, section 3.2.2]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.2.2 +/// [RFC 4035, section 3.2.3]: https://datatracker.ietf.org/doc/html/rfc4035#section-3.2.3 +/// [RFC 6840, section 5.7]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.7 +/// [RFC 6840, section 5.8]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.8 +/// [RFC 6840, section 5.9]: https://datatracker.ietf.org/doc/html/rfc6840#section-5.9 +#[derive( + Copy, + Clone, + Default, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct HeaderFlags { + /// The raw flag bits. + inner: U16, +} + +//--- Interaction + +impl HeaderFlags { + /// Get the specified flag bit. + const fn get_flag(&self, pos: u32) -> bool { + self.inner.get() & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(&mut self, pos: u32, value: bool) -> &mut Self { + self.inner &= !(1 << pos); + self.inner |= (value as u16) << pos; + self + } + + /// The raw flags bits. + pub const fn bits(&self) -> u16 { + self.inner.get() + } + + /// The QR bit. + pub const fn qr(&self) -> bool { + self.get_flag(15) + } + + /// Set the QR bit. + pub fn set_qr(&mut self, value: bool) -> &mut Self { + self.set_flag(15, value) + } + + /// The OPCODE field. + pub const fn opcode(&self) -> u8 { + (self.inner.get() >> 11) as u8 & 0xF + } + + /// Set the OPCODE field. + pub fn set_opcode(&mut self, value: u8) -> &mut Self { + debug_assert!(value < 16); + self.inner &= !(0xF << 11); + self.inner |= (value as u16) << 11; + self + } + + /// The AA bit. + pub fn aa(&self) -> bool { + self.get_flag(10) + } + + /// Set the AA bit. + pub fn set_aa(&mut self, value: bool) -> &mut Self { + self.set_flag(10, value) + } + + /// The TC bit. + pub fn tc(&self) -> bool { + self.get_flag(9) + } + + /// Set the TC bit. + pub fn set_tc(&mut self, value: bool) -> &mut Self { + self.set_flag(9, value) + } + + /// The RD bit. + pub fn rd(&self) -> bool { + self.get_flag(8) + } + + /// Set the RD bit. + pub fn set_rd(&mut self, value: bool) -> &mut Self { + self.set_flag(8, value) + } + + /// The RA bit. + pub fn ra(&self) -> bool { + self.get_flag(7) + } + + /// Set the RA bit. + pub fn set_ra(&mut self, value: bool) -> &mut Self { + self.set_flag(7, value) + } + + /// The AD bit. + pub fn ad(&self) -> bool { + self.get_flag(5) + } + + /// Set the AD bit. + pub fn set_ad(&mut self, value: bool) -> &mut Self { + self.set_flag(5, value) + } + + /// The CD bit. + pub fn cd(&self) -> bool { + self.get_flag(4) + } + + /// Set the CD bit. + pub fn set_cd(&mut self, value: bool) -> &mut Self { + self.set_flag(4, value) + } + + /// The RCODE field. + pub const fn rcode(&self) -> u8 { + self.inner.get() as u8 & 0xF + } + + /// Set the RCODE field. + pub fn set_rcode(&mut self, value: u8) -> &mut Self { + debug_assert!(value < 16); + self.inner &= !0xF; + self.inner |= value as u16; + self + } +} + +//--- Formatting + +impl fmt::Debug for HeaderFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HeaderFlags") + .field("qr", &self.qr()) + .field("opcode", &self.opcode()) + .field("aa", &self.aa()) + .field("tc", &self.tc()) + .field("rd", &self.rd()) + .field("ra", &self.ra()) + .field("rcode", &self.rcode()) + .field("bits", &self.bits()) + .finish() + } +} + +impl fmt::Display for HeaderFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.qr() { + if self.rd() { + f.write_str("recursive ")?; + } + write!(f, "query (opcode {})", self.opcode())?; + if self.cd() { + f.write_str(" (checking disabled)")?; + } + } else { + if self.ad() { + f.write_str("authentic ")?; + } + if self.aa() { + f.write_str("authoritative ")?; + } + if self.rd() && self.ra() { + f.write_str("recursive ")?; + } + write!(f, "response (rcode {})", self.rcode())?; + } + + if self.tc() { + f.write_str(" (message truncated)")?; + } + + Ok(()) + } +} + +//----------- SectionCounts -------------------------------------------------- + +/// Counts of objects in a DNS message. +#[derive( + Copy, + Clone, + Debug, + Default, + PartialEq, + Eq, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct SectionCounts { + /// The number of questions in the message. + pub questions: U16, + + /// The number of answer records in the message. + pub answers: U16, + + /// The number of name server records in the message. + pub authorities: U16, + + /// The number of additional records in the message. + pub additionals: U16, +} + +//--- Interaction + +impl SectionCounts { + /// Represent these counts as an array. + pub fn as_array(&self) -> &[U16; 4] { + // SAFETY: 'SectionCounts' has the same layout as '[U16; 4]'. + unsafe { core::mem::transmute(self) } + } + + /// Represent these counts as a mutable array. + pub fn as_array_mut(&mut self) -> &mut [U16; 4] { + // SAFETY: 'SectionCounts' has the same layout as '[U16; 4]'. + unsafe { core::mem::transmute(self) } + } +} + +//--- Formatting + +impl fmt::Display for SectionCounts { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut some = false; + + for (num, single, many) in [ + (self.questions.get(), "question", "questions"), + (self.answers.get(), "answer", "answers"), + (self.authorities.get(), "authority", "authorities"), + (self.additionals.get(), "additional", "additionals"), + ] { + // Add a comma if we have printed something before. + if some && num > 0 { + f.write_str(", ")?; + } + + // Print a count of this section. + match num { + 0 => {} + 1 => write!(f, "1 {single}")?, + n => write!(f, "{n} {many}")?, + } + + some |= num > 0; + } + + if !some { + f.write_str("empty")?; + } + + Ok(()) + } +} + +//----------- MessageItem ---------------------------------------------------- + +/// A question or a record. +/// +/// This is useful for building and parsing the contents of a [`Message`] +/// ergonomically and efficiently. An iterator of [`MessageItem`]s can be +/// retrieved using [`Message::parse()`]. +#[derive(Clone, Debug)] +pub enum MessageItem { + /// A question. + Question(Question), + + /// An answer record. + Answer(Record), + + /// An authority record. + Authority(Record), + + /// An additional record. + /// + /// This does not include EDNS records. + Additional(Record), + + /// An EDNS record. + /// + /// This is a record in the additional section. It uses a distinct type + /// as the class and TTL fields of the record are interpreted differently. + Edns(EdnsRecord), +} + +//--- Transformation + +impl MessageItem { + /// Transform this type's generic parameters. + pub fn transform( + self, + name_map: impl FnOnce(N) -> NN, + rdata_map: impl FnOnce(RD) -> NRD, + edata_map: impl FnOnce(ED) -> NED, + ) -> MessageItem { + match self { + Self::Question(this) => { + MessageItem::Question(this.transform(name_map)) + } + Self::Answer(this) => { + MessageItem::Answer(this.transform(name_map, rdata_map)) + } + Self::Authority(this) => { + MessageItem::Authority(this.transform(name_map, rdata_map)) + } + Self::Additional(this) => { + MessageItem::Additional(this.transform(name_map, rdata_map)) + } + Self::Edns(this) => MessageItem::Edns(this.transform(edata_map)), + } + } + + /// Transform this type's generic parameters by reference. + pub fn transform_ref<'a, NN, NRD, NED>( + &'a self, + name_map: impl FnOnce(&'a N) -> NN, + rdata_map: impl FnOnce(&'a RD) -> NRD, + edata_map: impl FnOnce(&'a ED) -> NED, + ) -> MessageItem { + match self { + Self::Question(this) => { + MessageItem::Question(this.transform_ref(name_map)) + } + Self::Answer(this) => { + MessageItem::Answer(this.transform_ref(name_map, rdata_map)) + } + Self::Authority(this) => MessageItem::Authority( + this.transform_ref(name_map, rdata_map), + ), + Self::Additional(this) => MessageItem::Additional( + this.transform_ref(name_map, rdata_map), + ), + Self::Edns(this) => { + MessageItem::Edns(this.transform_ref(edata_map)) + } + } + } +} + +//--- Equality + +impl PartialEq> + for MessageItem +where + N: PartialEq, + RD: PartialEq, + LED: PartialEq, +{ + fn eq(&self, other: &MessageItem) -> bool { + match (self, other) { + (MessageItem::Question(l), MessageItem::Question(r)) => l == r, + (MessageItem::Answer(l), MessageItem::Answer(r)) => l == r, + (MessageItem::Authority(l), MessageItem::Authority(r)) => l == r, + (MessageItem::Additional(l), MessageItem::Additional(r)) => { + l == r + } + (MessageItem::Edns(l), MessageItem::Edns(r)) => l == r, + _ => false, + } + } +} + +impl Eq for MessageItem {} + +//--- Building into DNS messages + +impl BuildInMessage for MessageItem +where + N: BuildInMessage, + RD: BuildInMessage, + ED: BuildBytes, +{ + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + match self { + Self::Question(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Answer(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Authority(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Additional(i) => { + i.build_in_message(contents, start, compressor) + } + Self::Edns(i) => i.build_in_message(contents, start, compressor), + } + } +} + +//--- Building into bytes + +impl BuildBytes for MessageItem +where + N: BuildBytes, + RD: BuildBytes, + ED: BuildBytes, +{ + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + Self::Question(this) => this.build_bytes(bytes), + Self::Answer(this) => this.build_bytes(bytes), + Self::Authority(this) => this.build_bytes(bytes), + Self::Additional(this) => this.build_bytes(bytes), + Self::Edns(this) => this.build_bytes(bytes), + } + } + + fn built_bytes_size(&self) -> usize { + match self { + Self::Question(this) => this.built_bytes_size(), + Self::Answer(this) => this.built_bytes_size(), + Self::Authority(this) => this.built_bytes_size(), + Self::Additional(this) => this.built_bytes_size(), + Self::Edns(this) => this.built_bytes_size(), + } + } +} diff --git a/src/new/base/mod.rs b/src/new/base/mod.rs new file mode 100644 index 000000000..f74299135 --- /dev/null +++ b/src/new/base/mod.rs @@ -0,0 +1,362 @@ +//! Basic DNS. +//! +//! This module provides the essential types and functionality for working +//! with DNS. In particular, it allows building and parsing DNS messages to +//! and from the wire format. +//! +//! This provides a mid-level and low-level API. It guides users towards the +//! most efficient solutions for their needs, and (where necessary) provides +//! fallbacks that trade efficiency for flexibility and/or ergonomics. +//! +//! # A quick reference on types +//! +//! [`Message`] is the top-level type, representing a whole DNS message. It +//! stores data in the wire format, making it trivial to parse into or build +//! from. It can provide direct access to the message [`Header`], and the +//! questions and records within it (collectively called [`MessageItem`]s) can +//! be parsed/traversed using [`Message::parse()`]. +//! +//! [`Question`] and [`Record`] are exactly what they look like, and are +//! simple `struct`s so they can be constructed and inspected easily. They +//! are generic over a _domain name type_ (discussed below), which you will +//! need to pick explicitly. [`Record`] is also generic over the record data +//! type; you probably want [`new::rdata::RecordData`]. See the documentation +//! on [`Record`] and [`new::rdata`] for more information. +//! +//! [`new::rdata`]: crate::new::rdata +//! [`new::rdata::RecordData`]: crate::new::rdata::RecordData +//! +//! The [`name`] module provides various types that represent domain +//! names, and describes the situations each type is most appropriate +//! for. As a quick summary: try to use [`RevNameBuf`] by default, or +//! Box<[RevName]> if lots of domain names need to be +//! stored. If DNSSEC canonical ordering is necessary, use [`NameBuf`] or +//! Box<[Name]> respectively. There are more efficient +//! alternatives in some cases; see [`name`]. +//! +//! [Name]: name::Name +//! [RevName]: name::RevName +//! [`NameBuf`]: name::NameBuf +//! [`RevNameBuf`]: name::RevNameBuf +//! +//! # Parsing DNS messages +//! +//! The [`parse`] module provides mid-level and low-level APIs for parsing +//! DNS messages from the wire format. To parse the questions and records in +//! a [`Message`], use [`Message::parse()`]. To parse a message (including +//! questions and records) from bytes, use [`MessageParser::new()`]. +//! +//! [`MessageParser::new()`]: parse::MessageParser::new() +//! +//! ``` +//! # use domain::new::base::MessageItem; +//! # use domain::new::base::parse::MessageParser; +//! # +//! // The bytes to be parsed. +//! let bytes: &[u8] = &[ +//! // The message header. +//! 0, 42, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, +//! // A question: www.example.org. A IN +//! 3, b'w', b'w', b'w', +//! 7, b'e', b'x', b'a', b'm', b'p', b'l', b'e', +//! 3, b'o', b'r', b'g', 0, +//! 0, 1, 0, 1, +//! // An answer: www.example.org. A IN 3600 127.0.0.1 +//! 192, 12, 0, 1, 0, 1, 0, 0, 14, 16, 0, 4, 127, 0, 0, 1, +//! // An OPT record. +//! 0, 0, 41, 4, 208, 0, 0, 128, 0, 0, 12, +//! // An EDNS client cookie. +//! 0, 10, 0, 8, 6, 148, 57, 104, 176, 18, 234, 57, +//! ]; +//! +//! // Construct a 'MessageParser' directly from bytes. +//! let Ok(mut message) = MessageParser::new(bytes) else { +//! panic!("'bytes' was too small to be a valid 'Message'") +//! }; +//! println!("Header: {:?}", message.header()); +//! while let Some(item) = message.next() { +//! let Ok(item) = item else { +//! panic!("Could not parse a message item (at offset {})", +//! message.offset()); +//! }; +//! +//! match item { +//! MessageItem::Question(question) => { +//! println!("Got a question: {question:?}"); +//! } +//! MessageItem::Answer(answer) => { +//! println!("Got an answer record: {answer:?}"); +//! } +//! MessageItem::Authority(authority) => { +//! println!("Got an authority record: {authority:?}"); +//! } +//! MessageItem::Additional(additional) => { +//! println!("Got an additional record: {additional:?}"); +//! } +//! MessageItem::Edns(edns) => { +//! println!("Got an EDNS record: {edns:?}"); +//! } +//! } +//! } +//! ``` +//! +//! # Building DNS messages +//! +//! The [`build`] module provides mid-level and low-level APIs for building +//! DNS messages in the wire format. [`MessageBuilder`] is the primary entry +//! point; it writes into a user-provided byte buffer. To begin building a +//! DNS message, use [`MessageBuilder::new()`]. +//! +//! [`MessageBuilder`]: build::MessageBuilder +//! [`MessageBuilder::new()`]: build::MessageBuilder::new() +//! +//! ``` +//! use domain::new::base::{Header, HeaderFlags, Message, Question, QType, QClass}; +//! use domain::new::base::build::{AsBytes, MessageBuilder, NameCompressor}; +//! use domain::new::base::name::RevNameBuf; +//! use domain::new::base::wire::U16; +//! +//! // Initialize a DNS message builder. +//! let mut buffer = [0u8; 512]; +//! let mut compressor = NameCompressor::default(); +//! let mut builder = MessageBuilder::new( +//! &mut buffer, +//! &mut compressor, +//! // Select a randomized ID here. +//! U16::new(1234), +//! // A recursive query for authoritative data. +//! *HeaderFlags::default() +//! .set_qr(false) +//! .set_opcode(0) +//! .set_aa(true) +//! .set_rd(true)); +//! +//! // Add a question for an A record. +//! builder.push_question(&Question { +//! qname: "www.example.org".parse::().unwrap(), +//! qtype: QType::A, +//! qclass: QClass::IN, +//! }).unwrap(); +//! +//! // Use the built message (e.g. send it). +//! let message: &mut Message = builder.finish(); +//! let bytes: &[u8] = message.as_bytes(); +//! # let _ = bytes; +//! ``` +//! +//! # Representing variable-length DNS data +//! +//! In order to efficiently serialize and deserialize DNS messages, and to be +//! easier to approach for users already familiar with DNS, this module +//! structures its DNS types to match the underlying wire format. +//! +//! Because many elements of DNS messages have variable-length encodings in +//! the wire format, this module relies on Rust's language support for +//! _dynamically sized types_ (DSTs) to represent them. The top-level +//! [`Message`] type, [`CharStr`], [`Name`], etc. are all DSTs. +//! +//! [`Name`]: name::Name +//! +//! DSTs cannot be passed around by value because the compiler needs to know +//! (at compile-time) how much stack space to allocate for them. As such, a +//! DST has to be held indirectly, by reference or in a container like +//! [`Box`]. The former work well in "short-term" contexts (e.g. within a +//! function), while the latter are necessary in long-term contexts. +//! +//! [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +//! +//! Container types that implement [`UnsizedCopyFrom`] automatically work with +//! any [`UnsizedCopy`] types. This trait allows DSTs to be copied into such +//! container types, which is especially useful to store a DST for long-term +//! use. It is already implemented for [`Box`], [`Arc`], [`Vec`], etc., and +//! users can implement it on their own container types too. +//! +//! [`Arc`]: https://doc.rust-lang.org/std/sync/struct.Arc.html +//! [`Vec`]: https://doc.rust-lang.org/std/vec/struct.Vec.html +//! [`UnsizedCopy`]: crate::utils::dst::UnsizedCopy +//! [`UnsizedCopyFrom`]: crate::utils::dst::UnsizedCopyFrom + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] + +//--- DNS messages + +mod message; +pub use message::{Header, HeaderFlags, Message, MessageItem, SectionCounts}; + +mod question; +pub use question::{QClass, QType, Question}; + +mod record; +pub use record::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RClass, + RType, Record, UnparsedRecordData, TTL, +}; + +//--- Elements of DNS messages + +pub mod name; + +mod charstr; +pub use charstr::{CharStr, CharStrBuf, CharStrParseError}; + +mod serial; +pub use serial::Serial; + +//--- Wire format + +pub mod build; +pub mod parse; +pub mod wire; + +//--- Compatibility exports + +/// A compatibility module with [`domain::base`]. +/// +/// This re-exports a large part of the `new::base` API surface using the same +/// import paths as the old `base` module. It is a stopgap measure to help +/// users port existing code over to `new::base`. Every export comes with a +/// deprecation message to help users switch to the right tools. +pub mod compat { + #![allow(deprecated)] + #![allow(missing_docs)] + + #[deprecated = "use 'crate::new::base::HeaderFlags' instead."] + pub use header::Flags; + + #[deprecated = "use 'crate::new::base::Header' instead."] + pub use header::HeaderSection; + + #[deprecated = "use 'crate::new::base::SectionCounts' instead."] + pub use header::HeaderCounts; + + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use iana::rtype::Rtype; + + #[deprecated = "use 'crate::new::base::name::Label' instead."] + pub use name::Label; + + #[deprecated = "use 'crate::new::base::name::Name' instead."] + pub use name::Name; + + #[deprecated = "use 'crate::new::base::Question' instead."] + pub use question::Question; + + #[deprecated = "use 'crate::new::base::ParseRecordData' instead."] + pub use rdata::ParseRecordData; + + #[deprecated = "use 'crate::new::rdata::UnknownRecordData' instead."] + pub use rdata::UnknownRecordData; + + #[deprecated = "use 'crate::new::base::Record' instead."] + pub use record::Record; + + #[deprecated = "use 'crate::new::base::TTL' instead."] + pub use record::Ttl; + + #[deprecated = "use 'crate::new::base::Serial' instead."] + pub use serial::Serial; + + pub mod header { + #[deprecated = "use 'crate::new::base::HeaderFlags' instead."] + pub use crate::new::base::HeaderFlags as Flags; + + #[deprecated = "use 'crate::new::base::Header' instead."] + pub use crate::new::base::Header as HeaderSection; + + #[deprecated = "use 'crate::new::base::SectionCounts' instead."] + pub use crate::new::base::SectionCounts as HeaderCounts; + } + + pub mod iana { + #[deprecated = "use 'crate::new::base::RClass' instead."] + pub use class::Class; + + #[deprecated = "use 'crate::new::rdata::DigestType' instead."] + pub use digestalg::DigestAlg; + + #[deprecated = "use 'crate::new::rdata::NSec3HashAlg' instead."] + pub use nsec3::Nsec3HashAlg; + + #[deprecated = "use 'crate::new::edns::OptionCode' instead."] + pub use opt::OptionCode; + + #[deprecated = "for now, just use 'u8', but a better API is coming."] + pub use rcode::Rcode; + + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use rtype::Rtype; + + #[deprecated = "use 'crate::new::rdata::SecAlg' instead."] + pub use secalg::SecAlg; + + pub mod class { + #[deprecated = "use 'crate::new::base::RClass' instead."] + pub use crate::new::base::RClass as Class; + } + + pub mod digestalg { + #[deprecated = "use 'crate::new::rdata::DigestType' instead."] + pub use crate::new::rdata::DigestType as DigestAlg; + } + + pub mod nsec3 { + #[deprecated = "use 'crate::new::rdata::NSec3HashAlg' instead."] + pub use crate::new::rdata::NSec3HashAlg as Nsec3HashAlg; + } + + pub mod opt { + #[deprecated = "use 'crate::new::edns::OptionCode' instead."] + pub use crate::new::edns::OptionCode; + } + + pub mod rcode { + #[deprecated = "for now, just use 'u8', but a better API is coming."] + pub use u8 as Rcode; + } + + pub mod rtype { + #[deprecated = "use 'crate::new::base::RType' instead."] + pub use crate::new::base::RType as Rtype; + } + + pub mod secalg { + #[deprecated = "use 'crate::new::rdata::SecAlg' instead."] + pub use crate::new::rdata::SecAlg; + } + } + + pub mod name { + #[deprecated = "use 'crate::new::base::name::Label' instead."] + pub use crate::new::base::name::Label; + + #[deprecated = "use 'crate::new::base::name::Name' instead."] + pub use crate::new::base::name::Name; + } + + pub mod question { + #[deprecated = "use 'crate::new::base::Question' instead."] + pub use crate::new::base::Question; + } + + pub mod rdata { + #[deprecated = "use 'crate::new::base::ParseRecordData' instead."] + pub use crate::new::base::ParseRecordData; + + #[deprecated = "use 'crate::new::rdata::UnknownRecordData' instead."] + pub use crate::new::rdata::UnknownRecordData; + } + + pub mod record { + #[deprecated = "use 'crate::new::base::Record' instead."] + pub use crate::new::base::Record; + + #[deprecated = "use 'crate::new::base::TTL' instead."] + pub use crate::new::base::TTL as Ttl; + } + + pub mod serial { + #[deprecated = "use 'crate::new::base::Serial' instead."] + pub use crate::new::base::Serial; + } +} diff --git a/src/new/base/name/absolute.rs b/src/new/base/name/absolute.rs new file mode 100644 index 000000000..a6d22cd57 --- /dev/null +++ b/src/new/base/name/absolute.rs @@ -0,0 +1,694 @@ +//! Absolute domain names. + +use core::{ + borrow::{Borrow, BorrowMut}, + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use crate::{ + new::base::{ + build::BuildInMessage, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, + TruncationError, + }, + }, + utils::dst::{UnsizedCopy, UnsizedCopyFrom}, +}; + +use super::{ + CanonicalName, Label, LabelBuf, LabelIter, LabelParseError, + NameCompressor, +}; + +//----------- Name ----------------------------------------------------------- + +/// An absolute domain name. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Name([u8]); + +//--- Constants + +impl Name { + /// The maximum size of a domain name. + pub const MAX_SIZE: usize = 255; + + /// The root name. + pub const ROOT: &'static Self = { + // SAFETY: A root label is the shortest valid name. + unsafe { Self::from_bytes_unchecked(&[0u8]) } + }; +} + +//--- Construction + +impl Name { + /// Assume a byte sequence is a valid [`Name`]. + /// + /// # Safety + /// + /// The byte sequence must represent a valid uncompressed domain name in + /// the conventional wire format (a sequence of labels terminating with a + /// root label, totalling 255 bytes or less). + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Name' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'Name' is sound. + core::mem::transmute(bytes) + } + + /// Assume a mutable byte sequence is a valid [`Name`]. + /// + /// # Safety + /// + /// The byte sequence must represent a valid uncompressed domain name in + /// the conventional wire format (a sequence of labels terminating with a + /// root label, totalling 255 bytes or less). + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'Name' is 'repr(transparent)' to '[u8]', so casting a + // '[u8]' into a 'Name' is sound. + core::mem::transmute(bytes) + } +} + +//--- Inspection + +impl Name { + /// The size of this name in the wire format. + #[allow(clippy::len_without_is_empty)] + pub const fn len(&self) -> usize { + self.0.len() + } + + /// Whether this is the root label. + pub const fn is_root(&self) -> bool { + self.0.len() == 1 + } + + /// A byte representation of the [`Name`]. + pub const fn as_bytes(&self) -> &[u8] { + &self.0 + } + + /// The labels in the [`Name`]. + pub const fn labels(&self) -> LabelIter<'_> { + // SAFETY: A 'Name' always contains valid encoded labels. + unsafe { LabelIter::new_unchecked(self.as_bytes()) } + } +} + +//--- Canonical operations + +impl CanonicalName for Name { + fn cmp_composed(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } + + fn cmp_lowercase_composed(&self, other: &Self) -> Ordering { + self.as_bytes() + .iter() + .map(u8::to_ascii_lowercase) + .cmp(other.as_bytes().iter().map(u8::to_ascii_lowercase)) + } +} + +//--- Parsing from bytes + +impl<'a> ParseBytes<'a> for &'a Name { + fn parse_bytes(bytes: &'a [u8]) -> Result { + match Self::split_bytes(bytes) { + Ok((this, &[])) => Ok(this), + _ => Err(ParseError), + } + } +} + +impl<'a> SplitBytes<'a> for &'a Name { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let mut offset = 0usize; + while offset < 255 { + match *bytes.get(offset..).ok_or(ParseError)? { + [0, ..] => { + // Found the root, stop. + let (name, rest) = bytes.split_at(offset + 1); + + // SAFETY: 'name' follows the wire format and is 255 bytes + // or shorter. + let name = unsafe { Name::from_bytes_unchecked(name) }; + return Ok((name, rest)); + } + + [l @ 1..=63, ref rest @ ..] if rest.len() >= l as usize => { + // This looks like a regular label. + offset += 1 + l as usize; + } + + _ => return Err(ParseError), + } + } + + Err(ParseError) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for Name { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + if let Some((rest, addr)) = + compressor.compress_name(&contents[..start], self) + { + // The name was compressed, and 'rest' is the uncompressed part. + let end = start + rest.len() + 2; + let bytes = + contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[..rest.len()].copy_from_slice(rest); + + // Add the top bits and the 12-byte offset for the message header. + let addr = (addr + 0xC00C).to_be_bytes(); + bytes[rest.len()..].copy_from_slice(&addr); + Ok(end) + } else { + // The name could not be compressed. + let end = start + self.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(self.as_bytes()); + Ok(end) + } + } +} + +//--- Equality + +impl PartialEq for Name { + fn eq(&self, other: &Self) -> bool { + // Instead of iterating labels, blindly iterate bytes. The locations + // of labels don't matter since we're testing everything for equality. + + // NOTE: Label lengths (which are less than 64) aren't affected by + // 'to_ascii_lowercase', so this method can be applied uniformly. + // This gives the compiler opportunities to vectorize the code. + let lhs = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let rhs = other.as_bytes().iter().map(u8::to_ascii_lowercase); + + lhs.eq(rhs) + } +} + +impl Eq for Name {} + +//--- Comparison + +impl PartialOrd for Name { + fn partial_cmp(&self, that: &Self) -> Option { + Some(self.cmp(that)) + } +} + +impl Ord for Name { + fn cmp(&self, that: &Self) -> Ordering { + // We wish to compare the labels in these names in reverse order. + // Unfortunately, labels in absolute names cannot be traversed + // backwards efficiently. We need to try harder. + // + // Consider two names that are not equal. This means that one name is + // a strict suffix of the other, or that the two had different labels + // at some position. Following this mismatched label is a suffix of + // labels that both names do agree on. + // + // We traverse the bytes in the names in reverse order and find the + // length of their shared suffix. The actual shared suffix, in units + // of labels, may be shorter than this (because the last bytes of the + // mismatched labels could be the same). + // + // Then, we traverse the labels of both names in forward order, until + // we hit the shared suffix territory. We try to match up the names + // in order to discover the real shared suffix. Once the suffix is + // found, the immediately preceding label (if there is one) contains + // the inequality, and can be compared as usual. + + let suffix_len = core::iter::zip( + self.as_bytes().iter().rev().map(u8::to_ascii_lowercase), + that.as_bytes().iter().rev().map(u8::to_ascii_lowercase), + ) + .position(|(a, b)| a != b); + + let Some(suffix_len) = suffix_len else { + // 'iter::zip()' simply ignores unequal iterators, stopping when + // either iterator finishes. Even though the two names had no + // mismatching bytes, one could be longer than the other. + return self.len().cmp(&that.len()); + }; + + // Prepare for forward traversal. + let (mut lhs, mut rhs) = (self.labels(), that.labels()); + // SAFETY: There is at least one unequal byte, and it cannot be the + // root label, so both names have at least one additional label. + let mut prev = unsafe { + (lhs.next().unwrap_unchecked(), rhs.next().unwrap_unchecked()) + }; + + // Traverse both names in lockstep, trying to match their lengths. + loop { + let (llen, rlen) = (lhs.remaining().len(), rhs.remaining().len()); + if llen == rlen && llen <= suffix_len { + // We're in shared suffix territory, and 'lhs' and 'rhs' have + // the same length. Thus, they must be identical, and we have + // found the shared suffix. + break prev.0.cmp(prev.1); + } else if llen > rlen { + // Try to match the lengths by shortening 'lhs'. + + // SAFETY: 'llen > rlen >= 1', thus 'lhs' contains at least + // one additional label before the root. + prev.0 = unsafe { lhs.next().unwrap_unchecked() }; + } else { + // Try to match the lengths by shortening 'rhs'. + + // SAFETY: Either: + // - '1 <= llen < rlen', thus 'rhs' contains at least one + // additional label before the root. + // - 'llen == rlen > suffix_len >= 1', thus 'rhs' contains at + // least one additional label before the root. + prev.1 = unsafe { rhs.next().unwrap_unchecked() }; + } + } + } +} + +//--- Hashing + +impl Hash for Name { + fn hash(&self, state: &mut H) { + for byte in self.as_bytes() { + // NOTE: Label lengths (which are less than 64) aren't affected by + // 'to_ascii_lowercase', so this method can be applied uniformly. + state.write_u8(byte.to_ascii_lowercase()) + } + } +} + +//--- Formatting + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + self.labels().try_for_each(|label| { + if !first { + f.write_str(".")?; + } else { + first = false; + } + + label.fmt(f) + }) + } +} + +impl fmt::Debug for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Name({})", self) + } +} + +//----------- NameBuf -------------------------------------------------------- + +/// A 256-byte buffer containing a [`Name`]. +#[derive(Clone)] +#[repr(C)] // make layout compatible with '[u8; 256]' +pub struct NameBuf { + /// The size of the encoded name. + size: u8, + + /// The buffer containing the [`Name`]. + buffer: [u8; 255], +} + +//--- Construction + +impl NameBuf { + /// Construct an empty, invalid buffer. + const fn empty() -> Self { + Self { + size: 0, + buffer: [0; 255], + } + } + + /// Copy a [`Name`] into a buffer. + pub fn copy_from(name: &Name) -> Self { + let mut buffer = [0u8; 255]; + buffer[..name.len()].copy_from_slice(name.as_bytes()); + Self { + size: name.len() as u8, + buffer, + } + } +} + +impl UnsizedCopyFrom for NameBuf { + type Source = Name; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Parsing from DNS messages + +impl<'a> SplitMessageBytes<'a> for NameBuf { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + // NOTE: The input may be controlled by an attacker. Compression + // pointers can be arranged to cause loops or to access every byte in + // the message in random order. Instead of performing complex loop + // detection, which would probably perform allocations, we simply + // disallow a name to point to data _after_ it. Standard name + // compressors will never generate such pointers. + + let mut buffer = Self::empty(); + + // Perform the first iteration early, to catch the end of the name. + let bytes = contents.get(start..).ok_or(ParseError)?; + let (mut pointer, rest) = parse_segment(bytes, &mut buffer)?; + let orig_end = contents.len() - rest.len(); + + // Traverse compression pointers. + let mut old_start = start; + while let Some(start) = pointer.map(usize::from) { + // Ensure the referenced position comes earlier. + if start >= old_start { + return Err(ParseError); + } + + // Keep going, from the referenced position. + let start = start.checked_sub(12).ok_or(ParseError)?; + let bytes = contents.get(start..).ok_or(ParseError)?; + (pointer, _) = parse_segment(bytes, &mut buffer)?; + old_start = start; + continue; + } + + // Stop and return the original end. + // NOTE: 'buffer' is now well-formed because we only stop when we + // reach a root label (which has been appended into it). + Ok((buffer, orig_end)) + } +} + +impl<'a> ParseMessageBytes<'a> for NameBuf { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + // See 'split_from_message()' for details. The only differences are + // in the range of the first iteration, and the check that the first + // iteration exactly covers the input range. + + let mut buffer = Self::empty(); + + // Perform the first iteration early, to catch the end of the name. + let bytes = contents.get(start..).ok_or(ParseError)?; + let (mut pointer, rest) = parse_segment(bytes, &mut buffer)?; + + if !rest.is_empty() { + // The name didn't reach the end of the input range, fail. + return Err(ParseError); + } + + // Traverse compression pointers. + let mut old_start = start; + while let Some(start) = pointer.map(usize::from) { + // Ensure the referenced position comes earlier. + if start >= old_start { + return Err(ParseError); + } + + // Keep going, from the referenced position. + let start = start.checked_sub(12).ok_or(ParseError)?; + let bytes = contents.get(start..).ok_or(ParseError)?; + (pointer, _) = parse_segment(bytes, &mut buffer)?; + old_start = start; + continue; + } + + // NOTE: 'buffer' is now well-formed because we only stop when we + // reach a root label (which has been appended into it). + Ok(buffer) + } +} + +/// Parse an encoded and potentially-compressed domain name, without +/// following any compression pointer. +fn parse_segment<'a>( + mut bytes: &'a [u8], + buffer: &mut NameBuf, +) -> Result<(Option, &'a [u8]), ParseError> { + loop { + match *bytes { + [0, ref rest @ ..] => { + // Found the root, stop. + buffer.append_bytes(&[0u8]); + return Ok((None, rest)); + } + + [l, ..] if l < 64 => { + // This looks like a regular label. + + if bytes.len() < 1 + l as usize { + // The input doesn't contain the whole label. + return Err(ParseError); + } else if 255 - buffer.size < 2 + l { + // The output name would exceed 254 bytes (this isn't + // the root label, so it can't fill the 255th byte). + return Err(ParseError); + } + + let (label, rest) = bytes.split_at(1 + l as usize); + buffer.append_bytes(label); + bytes = rest; + } + + [hi, lo, ref rest @ ..] if hi >= 0xC0 => { + let pointer = u16::from_be_bytes([hi, lo]); + + // NOTE: We don't verify the pointer here, that's left to + // the caller (since they have to actually use it). + return Ok((Some(pointer & 0x3FFF), rest)); + } + + _ => return Err(ParseError), + } + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for NameBuf { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + <&Name>::split_bytes(bytes) + .map(|(name, rest)| (NameBuf::copy_from(name), rest)) + } +} + +impl<'a> ParseBytes<'a> for NameBuf { + fn parse_bytes(bytes: &'a [u8]) -> Result { + <&Name>::parse_bytes(bytes).map(NameBuf::copy_from) + } +} + +//--- Building into byte sequences + +impl BuildBytes for NameBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Interaction + +impl NameBuf { + /// Append bytes to this buffer. + /// + /// This is an internal convenience function used while building buffers. + fn append_bytes(&mut self, bytes: &[u8]) { + self.buffer[self.size as usize..][..bytes.len()] + .copy_from_slice(bytes); + self.size += bytes.len() as u8; + } + + /// Append a label to this buffer. + /// + /// This is an internal convenience function used while building buffers. + fn append_label(&mut self, label: &Label) { + self.append_bytes(label.as_bytes()); + } +} + +//--- Parsing from strings + +impl FromStr for NameBuf { + type Err = NameParseError; + + /// Parse a name from a string. + /// + /// This is intended for easily constructing hard-coded domain names. The + /// labels in the name should be given in the conventional order (i.e. not + /// reversed), and should be separated by ASCII periods. The labels will + /// be parsed using [`LabelBuf::from_str()`]; see its documentation. This + /// function cannot parse all valid domain names; if an exceptional name + /// needs to be parsed, use [`Name::from_bytes_unchecked()`]. If the + /// input is empty, the root name is returned. + fn from_str(s: &str) -> Result { + let mut this = Self::empty(); + for label in s.split('.') { + let label = + label.parse::().map_err(NameParseError::Label)?; + if 255 - this.size < 1 + label.as_bytes().len() as u8 { + return Err(NameParseError::Overlong); + } + this.append_label(&label); + } + this.append_label(Label::ROOT); + Ok(this) + } +} + +//--- Access to the underlying 'Name' + +impl Deref for NameBuf { + type Target = Name; + + fn deref(&self) -> &Self::Target { + let name = &self.buffer[..self.size as usize]; + // SAFETY: A 'NameBuf' always contains a valid 'Name'. + unsafe { Name::from_bytes_unchecked(name) } + } +} + +impl DerefMut for NameBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let name = &mut self.buffer[..self.size as usize]; + // SAFETY: A 'NameBuf' always contains a valid 'Name'. + unsafe { Name::from_bytes_unchecked_mut(name) } + } +} + +impl Borrow for NameBuf { + fn borrow(&self) -> &Name { + self + } +} + +impl BorrowMut for NameBuf { + fn borrow_mut(&mut self) -> &mut Name { + self + } +} + +impl AsRef for NameBuf { + fn as_ref(&self) -> &Name { + self + } +} + +impl AsMut for NameBuf { + fn as_mut(&mut self) -> &mut Name { + self + } +} + +//--- Forwarding equality, comparison, hashing, and formatting + +impl PartialEq for NameBuf { + fn eq(&self, that: &Self) -> bool { + **self == **that + } +} + +impl Eq for NameBuf {} + +impl PartialOrd for NameBuf { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for NameBuf { + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for NameBuf { + fn hash(&self, state: &mut H) { + (**self).hash(state) + } +} + +impl fmt::Display for NameBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +impl fmt::Debug for NameBuf { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + (**self).fmt(f) + } +} + +//------------ NameParseError ------------------------------------------------ + +/// An error in parsing a [`Name`] from a string. +/// +/// This can be returned by [`NameBuf::from_str()`]. It is not used when +/// parsing names from the zonefile format, which uses a different mechanism. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum NameParseError { + /// The name was too large. + /// + /// Valid names are between 1 and 255 bytes, inclusive. + Overlong, + + /// A label in the name could not be parsed. + Label(LabelParseError), +} + +// TODO(1.81.0): Use 'core::error::Error' instead. +#[cfg(feature = "std")] +impl std::error::Error for NameParseError {} + +impl fmt::Display for NameParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::Overlong => "the domain name was too long", + Self::Label(LabelParseError::Overlong) => "a label was too long", + Self::Label(LabelParseError::Empty) => "a label was empty", + Self::Label(LabelParseError::InvalidChar) => { + "the domain name contained an invalid character" + } + }) + } +} diff --git a/src/new/base/name/compressor.rs b/src/new/base/name/compressor.rs new file mode 100644 index 000000000..d87cc8db1 --- /dev/null +++ b/src/new/base/name/compressor.rs @@ -0,0 +1,699 @@ +//! Name compression. + +use crate::new::base::name::{Label, LabelIter}; + +use super::{Name, RevName}; + +/// A domain name compressor. +/// +/// This struct provides name compression functionality when building DNS +/// messages. It compares domain names to those already in the message, and +/// if a shared suffix is found, the newly-inserted name will point at the +/// existing instance of the suffix. +/// +/// This struct stores the positions of domain names already present in the +/// DNS message, as it is otherwise impossible to differentiate domain names +/// from other bytes. Only recently-inserted domain names are stored, and +/// only from the first 16KiB of the message (as compressed names cannot point +/// any further). This is good enough for building small and large messages. +#[repr(align(64))] // align to a typical cache line +pub struct NameCompressor { + /// The last use position of every entry. + /// + /// Every time an entry is used (directly or indirectly), its last use + /// position is updated so that more stale entries are evicted before it. + /// + /// The last use position is calculated somewhat approximately; it would + /// most appropriately be '(number of children, position of inserted + /// name)', but it is approximated as 'position of inserted name + offset + /// into the name if it were uncompressed'. In either formula, the entry + /// with the minimum value would be evicted first. + /// + /// Both formulae guarantee that entries will be evicted before any of + /// their dependencies. The former formula requires at least 19 bits of + /// storage, while the latter requires less than 15 bits. The latter can + /// prioritize a deeply-nested name suffix over a slightly more recently + /// used name that is less nested, but this should be quite rare. + /// + /// Valid values: + /// - Initialized entries: `[1, 16383+253]`. + /// - Uninitialized entries: 0. + last_use: [u16; 32], + + /// The position of each entry. + /// + /// This is a byte offset from the message contents (zero represents the + /// first byte after the 12-byte message header). + /// + /// Valid values: + /// - Initialized entries: `[0, 16383]`. + /// - Uninitialized entries: 0. + pos: [u16; 32], + + /// The length of the relative domain name in each entry. + /// + /// This is the length of the domain name each entry represents, up to + /// (but excluding) the root label or compression pointer. It is used to + /// quickly find the end of the domain name for matching suffixes. + /// + /// Valid values: + /// - Initialized entries: `[2, 253]`. + /// - Uninitialized entries: 0. + len: [u8; 32], + + /// The parent of this entry, if any. + /// + /// If this entry represents a compressed domain name, this value stores + /// the index of that entry. + /// + /// Valid values: + /// - Initialized entries with parents: `[32, 63]`. + /// - Initialized entries without parents: 64. + /// - Uninitialized entries: 0. + parent: [u8; 32], + + /// A 16-bit hash of the entry's last label. + /// + /// An existing entry will be used for compressing a new domain name when + /// the last label in both of them is identical. This field stores a hash + /// over the last label in every entry, to speed up lookups. + /// + /// Valid values: + /// - Initialized entries: `[0, 65535]`. + /// - Uninitialized entries: 0. + hash: [u16; 32], +} + +impl NameCompressor { + /// Construct an empty [`NameCompressor`]. + pub const fn new() -> Self { + Self { + last_use: [0u16; 32], + pos: [0u16; 32], + len: [0u8; 32], + parent: [0u8; 32], + hash: [0u16; 32], + } + } + + /// Compress a [`RevName`]. + /// + /// This is a low-level function; use [`RevName::build_in_message()`] to + /// write a [`RevName`] into a DNS message. + /// + /// Given the contents of the DNS message, determine how to compress the + /// given domain name. If a suitable compression for the name could be + /// found, this function returns the length of the uncompressed suffix as + /// well as the address of the compressed prefix. + /// + /// The contents slice should begin immediately after the 12-byte message + /// header. It must end at the position the name will be inserted. It is + /// assumed that the domain names inserted in these contents still exist + /// from previous calls to [`compress_name()`] and related methods. If + /// this is not true, panics or silently invalid results may occur. + /// + /// The compressor's state will be updated to assume the provided name was + /// inserted into the message. + pub fn compress_revname<'n>( + &mut self, + contents: &[u8], + name: &'n RevName, + ) -> Option<(&'n [u8], u16)> { + // Treat the name as a byte sequence without the root label. + let mut name = &name.as_bytes()[1..]; + + if name.is_empty() { + // Root names are never compressed. + return None; + } + + let mut parent = 64u8; + let mut parent_offset = None; + + // Repeatedly look up entries that could be used for compression. + while !name.is_empty() { + match self.lookup_entry_for_revname(contents, name, parent) { + Some(entry) => { + let tmp; + (parent, name, tmp) = entry; + parent_offset = Some(tmp); + + // This entry was successfully used for compression. + // Record its use at this (approximate) position. + let use_pos = contents.len() + name.len(); + let use_pos = use_pos.max(1); + if use_pos < 16383 + 253 { + self.last_use[parent as usize] = use_pos as u16; + } + } + None => break, + } + } + + // If there is a non-empty uncompressed prefix, register it as a new + // entry here. + if !name.is_empty() && contents.len() < 16384 { + // SAFETY: 'name' is a non-empty sequence of labels. + let first = unsafe { + LabelIter::new_unchecked(name).next().unwrap_unchecked() + }; + + // Pick the entry that was least recently used (or uninitialized). + // + // By the invariants of 'last_use', it is guaranteed that this + // entry is not the parent of any others. + let index = (0usize..32) + .min_by_key(|&i| self.last_use[i]) + .expect("the iterator has 32 elements"); + + self.last_use[index] = contents.len() as u16; + self.pos[index] = contents.len() as u16; + self.len[index] = name.len() as u8; + self.parent[index] = parent; + self.hash[index] = Self::hash_label(first); + } + + // If 'parent_offset' is 'Some', then at least one entry was found, + // and so the name was compressed. + parent_offset.map(|offset| (name, offset)) + } + + /// Look up entries which share a suffix with the given reversed name. + /// + /// At most one entry ends with a complete label matching the given name. + /// We will match suffixes using a linear-time algorithm. + /// + /// On success, the entry's index, the remainder of the name, and the + /// offset of the referenced domain name are returned. + fn lookup_entry_for_revname<'n>( + &self, + contents: &[u8], + name: &'n [u8], + parent: u8, + ) -> Option<(u8, &'n [u8], u16)> { + // SAFETY: 'name' is a sequence of labels. + let mut name_labels = unsafe { LabelIter::new_unchecked(name) }; + // SAFETY: 'name' is non-empty. + let first = unsafe { name_labels.next().unwrap_unchecked() }; + let hash = Self::hash_label(first); + + // Search for an entry with a matching hash and parent. + for i in 0..32 { + // Check the hash first, as it's less likely to match. It's also + // okay if both checks are performed unconditionally. + if self.hash[i] != hash || self.parent[i] != parent { + continue; + }; + + // Look up the entry in the message contents. + let (pos, len) = (self.pos[i] as usize, self.len[i] as usize); + debug_assert_ne!(len, 0); + let mut entry = contents.get(pos..pos + len) + .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state")); + + // Find a shared suffix between the entry and the name. + // + // Comparing a 'Name' to a 'RevName' properly is difficult. We're + // just going for the lazy and not-pedantically-correct version, + // where we blindly match 'RevName' labels against the end of the + // 'Name'. The bytes are definitely correct, but there's a small + // chance that we aren't consistent with label boundaries. + + // TODO(1.80): Use 'slice::split_at_checked()'. + if entry.len() < first.as_bytes().len() + || !entry[entry.len() - first.as_bytes().len()..] + .eq_ignore_ascii_case(first.as_bytes()) + { + continue; + } + entry = &entry[..entry.len() - first.as_bytes().len()]; + + for label in name_labels.clone() { + if entry.len() < label.as_bytes().len() + || !entry[entry.len() - label.as_bytes().len()..] + .eq_ignore_ascii_case(label.as_bytes()) + { + break; + } + entry = &entry[..entry.len() - label.as_bytes().len()]; + } + + // Suffixes from 'entry' that were also in 'name' have been + // removed. The remainder of 'entry' does not match with 'name'. + // 'name' can be compressed using this entry. + let rest = name_labels.remaining(); + let pos = pos + entry.len(); + return Some((i as u8, rest, pos as u16)); + } + + None + } + + /// Compress a [`Name`]. + /// + /// This is a low-level function; use [`Name::build_in_message()`] to + /// write a [`Name`] into a DNS message. + /// + /// Given the contents of the DNS message, determine how to compress the + /// given domain name. If a suitable compression for the name could be + /// found, this function returns the length of the uncompressed prefix as + /// well as the address of the suffix. + /// + /// The contents slice should begin immediately after the 12-byte message + /// header. It must end at the position the name will be inserted. It is + /// assumed that the domain names inserted in these contents still exist + /// from previous calls to [`compress_name()`] and related methods. If + /// this is not true, panics or silently invalid results may occur. + /// + /// The compressor's state will be updated to assume the provided name was + /// inserted into the message. + pub fn compress_name<'n>( + &mut self, + contents: &[u8], + name: &'n Name, + ) -> Option<(&'n [u8], u16)> { + // Treat the name as a byte sequence without the root label. + let mut name = &name.as_bytes()[..name.len() - 1]; + + if name.is_empty() { + // Root names are never compressed. + return None; + } + + let mut hash = Self::hash_label(Self::last_label(name)); + let mut parent = 64u8; + let mut parent_offset = None; + + // Repeatedly look up entries that could be used for compression. + while !name.is_empty() { + match self.lookup_entry_for_name(contents, name, parent, hash) { + Some(entry) => { + let tmp; + (parent, name, hash, tmp) = entry; + parent_offset = Some(tmp); + + // This entry was successfully used for compression. + // Record its use at this (approximate) position. + let use_pos = contents.len() + name.len(); + let use_pos = use_pos.max(1); + if use_pos < 16383 + 253 { + self.last_use[parent as usize] = use_pos as u16; + } + } + None => break, + } + } + + // If there is a non-empty uncompressed prefix, register it as a new + // entry here. We already know what the hash of its last label is. + if !name.is_empty() && contents.len() < 16384 { + // Pick the entry that was least recently used (or uninitialized). + // + // By the invariants of 'last_use', it is guaranteed that this + // entry is not the parent of any others. + let index = (0usize..32) + .min_by_key(|&i| self.last_use[i]) + .expect("the iterator has 32 elements"); + + self.last_use[index] = contents.len() as u16; + self.pos[index] = contents.len() as u16; + self.len[index] = name.len() as u8; + self.parent[index] = parent; + self.hash[index] = hash; + } + + // If 'parent_offset' is 'Some', then at least one entry was found, + // and so the name was compressed. + parent_offset.map(|offset| (name, offset)) + } + + /// Look up entries which share a suffix with the given name. + /// + /// At most one entry ends with a complete label matching the given name. + /// We will carefully match suffixes using a linear-time algorithm. + /// + /// On success, the entry's index, the remainder of the name, the hash of + /// the last label in the remainder of the name (if any), and the offset + /// of the referenced domain name are returned. + fn lookup_entry_for_name<'n>( + &self, + contents: &[u8], + name: &'n [u8], + parent: u8, + hash: u16, + ) -> Option<(u8, &'n [u8], u16, u16)> { + // SAFETY: 'name' is a non-empty sequence of labels. + let name_labels = unsafe { LabelIter::new_unchecked(name) }; + + // Search for an entry with a matching hash and parent. + for i in 0..32 { + // Check the hash first, as it's less likely to match. It's also + // okay if both checks are performed unconditionally. + if self.hash[i] != hash || self.parent[i] != parent { + continue; + }; + + // Look up the entry in the message contents. + let (pos, len) = (self.pos[i] as usize, self.len[i] as usize); + debug_assert_ne!(len, 0); + let entry = contents.get(pos..pos + len) + .unwrap_or_else(|| panic!("'contents' did not correspond to the name compressor state")); + + // Find a shared suffix between the entry and the name. + // + // We're going to use a not-pendantically-correct implementation + // where we blindly match the ends of the names. The bytes are + // definitely correct, but there's a small chance we aren't + // consistent with label boundaries. + + let suffix_len = core::iter::zip( + name.iter().rev().map(u8::to_ascii_lowercase), + entry.iter().rev().map(u8::to_ascii_lowercase), + ) + .position(|(a, b)| a != b); + + let Some(suffix_len) = suffix_len else { + // 'iter::zip()' simply ignores unequal iterators, stopping + // when either iterator finishes. Even though the two names + // had no mismatching bytes, one could be longer than the + // other. + if name.len() > entry.len() { + // 'entry' is a proper suffix of 'name'. 'name' can be + // compressed using 'entry', and will have at least one + // more label before it. This label needs to be found and + // hashed. + + let rest = &name[..name.len() - entry.len()]; + let hash = Self::hash_label(Self::last_label(rest)); + return Some((i as u8, rest, hash, pos as u16)); + } else { + // 'name' is a suffix of 'entry'. 'name' can be + // compressed using 'entry', and no labels will be left. + let rest = &name[..0]; + let hash = 0u16; + let pos = pos + len - name.len(); + return Some((i as u8, rest, hash, pos as u16)); + } + }; + + // Walk 'name' until we reach the shared suffix region. + + // NOTE: + // - 'suffix_len < min(name.len(), entry.len())'. + // - 'name_labels.remaining.len() == name.len()'. + // - Thus 'suffix_len < name_labels.remaining.len()'. + // - Thus we can move the first statement of the loop here. + // SAFETY: + // - 'name' and 'entry' have a corresponding but unequal byte. + // - Thus 'name' has at least one byte. + // - Thus 'name' has at least one label. + let mut name_labels = name_labels.clone(); + let mut prev_in_name = + unsafe { name_labels.next().unwrap_unchecked() }; + while name_labels.remaining().len() > suffix_len { + // SAFETY: + // - 'LabelIter' is only empty once 'remaining' is empty. + // - 'remaining > suffix_len >= 0'. + prev_in_name = + unsafe { name_labels.next().unwrap_unchecked() }; + } + + // 'entry' and 'name' share zero or more labels, and this shared + // suffix is equal to 'name_labels'. The 'name_label' bytes might + // not lie on the correct label boundaries in 'entry', but this is + // not problematic. If 'name_labels' is non-empty, 'name' can be + // compressed using this entry. + + let suffix_len = name_labels.remaining().len(); + if suffix_len == 0 { + continue; + } + + let rest = &name[..name.len() - suffix_len]; + let hash = Self::hash_label(prev_in_name); + let pos = pos + len - suffix_len; + return Some((i as u8, rest, hash, pos as u16)); + } + + None + } + + /// Find the last label of a domain name. + /// + /// The name must be a valid non-empty sequence of labels. + fn last_label(name: &[u8]) -> &Label { + // The last label begins with a length octet and is followed by + // the corresponding number of bytes. While the length octet + // could look like a valid ASCII character, it would have to be + // 45 (ASCII '-') or above; most labels are not that long. + // + // We will search backwards for a byte that could be the length + // octet of the last label. It is highly likely that exactly one + // match will be found; this is guaranteed to be the right result. + // If more than one match is found, we will fall back to searching + // from the beginning. + // + // It is possible (although unlikely) for LLVM to vectorize this + // process, since it performs 64 unconditional byte comparisons + // over a fixed array. A manually vectorized implementation would + // generate a 64-byte mask for the valid bytes in 'name', load all + // 64 bytes blindly, then do a masked comparison against iota. + + name.iter() + // Take the last 64 bytes of the name. + .rev() + .take(64) + // Compare those bytes against valid length octets. + .enumerate() + .filter_map(|(i, &b)| (i == b as usize).then_some(b)) + // Look for a single valid length octet. + .try_fold(None, |acc, len| match acc { + None => Ok(Some(len)), + Some(_) => Err(()), + }) + // Unwrap the 'Option' since it's guaranteed to be 'Some'. + .transpose() + .unwrap_or_else(|| { + unreachable!("a valid last label could not be found") + }) + // Locate the selected bytes. + .map(|len| { + let bytes = &name[name.len() - len as usize - 1..]; + + // SAFETY: 'name' is a non-empty sequence of labels, and + // we have correctly selected the last label within it. + unsafe { Label::from_bytes_unchecked(bytes) } + }) + // Otherwise, fall back to a forward traversal. + .unwrap_or_else(|()| { + // SAFETY: 'name' is a non-empty sequence of labels. + unsafe { LabelIter::new_unchecked(name) } + .last() + .expect("'name' is not '.'") + }) + } + + /// Hash a label. + fn hash_label(label: &Label) -> u16 { + // This code is copied from the 'hash_bytes()' function of + // 'rustc-hash' v2.1.1, with helpers. The codebase is dual-licensed + // under Apache-2.0 and MIT, with no explicit copyright statement. + // + // 'hash_bytes()' is described as "a wyhash-inspired + // non-collision-resistant hash for strings/slices designed by Orson + // Peters, with a focus on small strings and small codesize." + // + // While the output of 'hash_bytes()' would pass through an additional + // multiplication in 'add_to_hash()', manual testing on some sample + // zonefiles showed that the top 16 bits of the 'hash_bytes()' output + // was already very uniform. + // + // Source: + // + // In order to hash case-insensitively, we aggressively transform the + // input bytes. We cause some collisions, but only in characters we + // don't expect to see in domain names. We do this by mapping bytes + // from 'XX0X_XXXX' to 'XX1X_XXXX'. A full list of effects: + // + // - Control characters (0x00..0x20) become symbols and digits. We + // weren't expecting any control characters to appear anyway. + // + // - Uppercase ASCII characters become lowercased. + // + // - '@[\]^_' become '`{|}~' and DEL. Underscores can occur, but DEL + // is not expected, so the collision is not problematic. + // + // - Half of the non-ASCII space gets folded. Unicode sequences get + // mapped into ASCII using Punycode, so the chance of a non-ASCII + // character here is very low. + + #[cfg(target_pointer_width = "64")] + fn multiply_mix(x: u64, y: u64) -> u64 { + let prod = (x as u128) * (y as u128); + (prod as u64) ^ ((prod >> 64) as u64) + } + + #[cfg(target_pointer_width = "32")] + fn multiply_mix(x: u64, y: u64) -> u64 { + let a = (x & u32::MAX as u64) * (y >> 32); + let b = (y & u32::MAX as u64) * (x >> 32); + a ^ b.rotate_right(32) + } + + const SEED1: u64 = 0x243f6a8885a308d3; + const SEED2: u64 = 0x13198a2e03707344; + const M: u64 = 0xa4093822299f31d0; + + let bytes = label.as_bytes(); + let len = bytes.len(); + let mut s = (SEED1, SEED2); + + if len <= 16 { + // XOR the input into s0, s1. + if len >= 8 { + let i = [&bytes[..8], &bytes[len - 8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + + s.0 ^= i[0]; + s.1 ^= i[1]; + } else if len >= 4 { + let i = [&bytes[..4], &bytes[len - 4..]] + .map(|i| u32::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020); + + s.0 ^= i[0] as u64; + s.1 ^= i[1] as u64; + } else if len > 0 { + let lo = bytes[0] as u64 | 0x20; + let mid = bytes[len / 2] as u64 | 0x20; + let hi = bytes[len - 1] as u64 | 0x20; + s.0 ^= lo; + s.1 ^= (hi << 8) | mid; + } + } else { + // Handle bulk (can partially overlap with suffix). + let mut off = 0; + while off < len - 16 { + let bytes = &bytes[off..off + 16]; + let i = [&bytes[..8], &bytes[8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + + // Replace s1 with a mix of s0, x, and y, and s0 with s1. + // This ensures the compiler can unroll this loop into two + // independent streams, one operating on s0, the other on s1. + // + // Since zeroes are a common input we prevent an immediate + // trivial collapse of the hash function by XOR'ing a constant + // with y. + let t = multiply_mix(s.0 ^ i[0], M ^ i[1]); + s.0 = s.1; + s.1 = t; + off += 16; + } + + let bytes = &bytes[len - 16..]; + let i = [&bytes[..8], &bytes[8..]] + .map(|i| u64::from_le_bytes(i.try_into().unwrap())) + .map(|i| i | 0x20202020_20202020); + s.0 ^= i[0]; + s.1 ^= i[1]; + } + + (multiply_mix(s.0, s.1) >> 48) as u16 + } +} + +impl Default for NameCompressor { + fn default() -> Self { + Self::new() + } +} + +//============ Tests ========================================================= + +#[cfg(test)] +mod tests { + use crate::new::base::{build::BuildInMessage, name::NameBuf}; + + use super::NameCompressor; + + #[test] + fn no_compression() { + let mut buffer = [0u8; 26]; + let mut compressor = NameCompressor::new(); + + // The TLD is different, so they cannot be compressed together. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "example.com".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07example\x03com\x00" + ); + } + + #[test] + fn single_shared_label() { + let mut buffer = [0u8; 23]; + let mut compressor = NameCompressor::new(); + + // Only the TLD will be shared. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "unequal.org".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07unequal\xC0\x14" + ); + } + + #[test] + fn case_insensitive() { + let mut buffer = [0u8; 23]; + let mut compressor = NameCompressor::new(); + + // The TLD should be shared, even if it differs in case. + let a: NameBuf = "example.org".parse().unwrap(); + let b: NameBuf = "unequal.ORG".parse().unwrap(); + + let mut off = 0; + off = a + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + off = b + .build_in_message(&mut buffer, off, &mut compressor) + .unwrap(); + + assert_eq!(off, buffer.len()); + assert_eq!( + &buffer, + b"\ + \x07example\x03org\x00\ + \x07unequal\xC0\x14" + ); + } +} diff --git a/src/new/base/name/label.rs b/src/new/base/name/label.rs new file mode 100644 index 000000000..e7eac887b --- /dev/null +++ b/src/new/base/name/label.rs @@ -0,0 +1,596 @@ +//! Labels in domain names. + +use core::{ + borrow::{Borrow, BorrowMut}, + cmp::Ordering, + fmt, + hash::{Hash, Hasher}, + iter::FusedIterator, + ops::{Deref, DerefMut}, + str::FromStr, +}; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::utils::dst::{UnsizedCopy, UnsizedCopyFrom}; + +//----------- Label ---------------------------------------------------------- + +/// A label in a domain name. +/// +/// A label contains up to 63 bytes of arbitrary data, prefixed with its the +/// number of those bytes. +#[derive(AsBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Label([u8]); + +//--- Associated Constants + +impl Label { + /// The root label. + pub const ROOT: &'static Self = { + // SAFETY: This is a correctly encoded label. + unsafe { Self::from_bytes_unchecked(&[0]) } + }; + + /// The wildcard label. + pub const WILDCARD: &'static Self = { + // SAFETY: This is a correctly encoded label. + unsafe { Self::from_bytes_unchecked(&[1, b'*']) } + }; +} + +//--- Construction + +impl Label { + /// Assume a byte slice is a valid label. + /// + /// # Safety + /// + /// The following conditions must hold for this call to be sound: + /// - `bytes.len() <= 64` + /// - `bytes[0] as usize + 1 == bytes.len()` + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Label' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute(bytes) } + } + + /// Assume a mutable byte slice is a valid label. + /// + /// # Safety + /// + /// The following conditions must hold for this call to be sound: + /// - `bytes.len() <= 64` + /// - `bytes[0] as usize + 1 == bytes.len()` + pub unsafe fn from_bytes_unchecked_mut(bytes: &mut [u8]) -> &mut Self { + // SAFETY: 'Label' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute(bytes) } + } +} + +//--- Parsing from DNS messages + +impl<'a> ParseMessageBytes<'a> for &'a Label { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +impl<'a> SplitMessageBytes<'a> for &'a Label { + fn split_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Label { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = &self.0; + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//--- Parsing from bytes + +impl<'a> SplitBytes<'a> for &'a Label { + fn split_bytes(bytes: &'a [u8]) -> Result<(Self, &'a [u8]), ParseError> { + let &size = bytes.first().ok_or(ParseError)?; + if size < 64 && bytes.len() > size as usize { + let (label, rest) = bytes.split_at(1 + size as usize); + // SAFETY: + // - 'label.len() = 1 + size <= 64' + // - 'label[0] = size + 1 == label.len()' + Ok((unsafe { Label::from_bytes_unchecked(label) }, rest)) + } else { + Err(ParseError) + } + } +} + +impl<'a> ParseBytes<'a> for &'a Label { + fn parse_bytes(bytes: &'a [u8]) -> Result { + match Self::split_bytes(bytes) { + Ok((this, &[])) => Ok(this), + _ => Err(ParseError), + } + } +} + +//--- Building into byte sequences + +impl BuildBytes for Label { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.0.build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + self.0.len() + } +} + +//--- Inspection + +impl Label { + /// Whether this is the root label. + pub const fn is_root(&self) -> bool { + self.0.len() == 1 + } + + /// Whether this is a wildcard label. + pub const fn is_wildcard(&self) -> bool { + matches!(self.0, [1, b'*']) + } + + /// The bytes making up this label. + pub const fn as_bytes(&self) -> &[u8] { + &self.0 + } +} + +//--- Access to the underlying bytes + +impl AsRef<[u8]> for Label { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +impl<'a> From<&'a Label> for &'a [u8] { + fn from(value: &'a Label) -> Self { + &value.0 + } +} + +//--- Comparison + +impl PartialEq for Label { + /// Compare two labels for equality. + /// + /// Labels are compared ASCII-case-insensitively. + fn eq(&self, other: &Self) -> bool { + let this = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let that = other.as_bytes().iter().map(u8::to_ascii_lowercase); + this.eq(that) + } +} + +impl Eq for Label {} + +//--- Ordering + +impl PartialOrd for Label { + /// Determine the order between labels. + /// + /// Any uppercase ASCII characters in the labels are treated as if they + /// were lowercase. The first unequal byte between two labels determines + /// its ordering: the label with the smaller byte value is the lesser. If + /// two labels have all the same bytes, the shorter label is lesser; if + /// they are the same length, they are equal. + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Label { + /// Determine the order between labels. + /// + /// Any uppercase ASCII characters in the labels are treated as if they + /// were lowercase. The first unequal byte between two labels determines + /// its ordering: the label with the smaller byte value is the lesser. If + /// two labels have all the same bytes, the shorter label is lesser; if + /// they are the same length, they are equal. + fn cmp(&self, other: &Self) -> Ordering { + let this = self.as_bytes().iter().map(u8::to_ascii_lowercase); + let that = other.as_bytes().iter().map(u8::to_ascii_lowercase); + this.cmp(that) + } +} + +//--- Hashing + +impl Hash for Label { + /// Hash this label. + /// + /// All uppercase ASCII characters are lowercased beforehand. This way, + /// the hash of a label is case-independent, consistent with how labels + /// are compared and ordered. + /// + /// The label is hashed as if it were a name containing a single label -- + /// the length octet is thus included. This makes the hashing consistent + /// between names and tuples (not slices!) of labels. + fn hash(&self, state: &mut H) { + for &byte in self.as_bytes() { + state.write_u8(byte.to_ascii_lowercase()) + } + } +} + +//--- Formatting + +impl fmt::Display for Label { + /// Print a label. + /// + /// The label is printed in the conventional zone file format, with bytes + /// outside printable ASCII formatted as `\\DDD` (a backslash followed by + /// three zero-padded decimal digits), and `.` and `\\` simply escaped by + /// a backslash. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.as_bytes().iter().try_for_each(|&byte| { + if b".\\".contains(&byte) { + write!(f, "\\{}", byte as char) + } else if byte.is_ascii_graphic() { + write!(f, "{}", byte as char) + } else { + write!(f, "\\{:03}", byte) + } + }) + } +} + +impl fmt::Debug for Label { + /// Print a label for debugging purposes. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Label") + .field(&format_args!("{}", self)) + .finish() + } +} + +//----------- LabelBuf ------------------------------------------------------- + +/// A 64-byte buffer holding a [`Label`]. +#[derive(Clone)] +#[repr(transparent)] +pub struct LabelBuf { + /// The label bytes. + data: [u8; 64], +} + +//--- Construction + +impl LabelBuf { + /// Copy a [`Label`] into a buffer. + pub fn copy_from(label: &Label) -> Self { + let bytes = label.as_bytes(); + let mut data = [0u8; 64]; + data[..bytes.len()].copy_from_slice(bytes); + Self { data } + } +} + +impl UnsizedCopyFrom for LabelBuf { + type Source = Label; + + fn unsized_copy_from(value: &Self::Source) -> Self { + Self::copy_from(value) + } +} + +//--- Parsing from strings + +impl FromStr for LabelBuf { + type Err = LabelParseError; + + /// Parse a label from a string. + /// + /// This is intended for easily constructing hard-coded labels. The input + /// is not expected to be in the zonefile format; it should simply contain + /// 1 to 63 characters, each being a plain ASCII alphanumeric or a hyphen. + /// To construct a label containing bytes outside this range, use + /// [`Label::from_bytes_unchecked()`]. To construct a root label, use + /// [`Label::ROOT`]. + fn from_str(s: &str) -> Result { + if s == "*" { + Ok(Self::copy_from(Label::WILDCARD)) + } else if !s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') { + Err(LabelParseError::InvalidChar) + } else if s.is_empty() { + Err(LabelParseError::Empty) + } else if s.len() > 63 { + Err(LabelParseError::Overlong) + } else { + let bytes = s.as_bytes(); + let mut data = [0u8; 64]; + data[0] = bytes.len() as u8; + data[1..1 + bytes.len()].copy_from_slice(bytes); + Ok(Self { data }) + } + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for LabelBuf { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + Self::parse_bytes(&contents[start..]) + } +} + +impl SplitMessageBytes<'_> for LabelBuf { + fn split_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result<(Self, usize), ParseError> { + Self::split_bytes(&contents[start..]) + .map(|(this, rest)| (this, contents.len() - start - rest.len())) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for LabelBuf { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + Label::build_in_message(self, contents, start, compressor) + } +} + +//--- Parsing from byte sequences + +impl ParseBytes<'_> for LabelBuf { + fn parse_bytes(bytes: &[u8]) -> Result { + <&Label>::parse_bytes(bytes).map(Self::copy_from) + } +} + +impl SplitBytes<'_> for LabelBuf { + fn split_bytes(bytes: &'_ [u8]) -> Result<(Self, &'_ [u8]), ParseError> { + <&Label>::split_bytes(bytes) + .map(|(label, rest)| (Self::copy_from(label), rest)) + } +} + +//--- Building into byte sequences + +impl BuildBytes for LabelBuf { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + (**self).build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + (**self).built_bytes_size() + } +} + +//--- Access to the underlying 'Label' + +impl Deref for LabelBuf { + type Target = Label; + + fn deref(&self) -> &Self::Target { + let size = self.data[0] as usize; + let label = &self.data[..1 + size]; + // SAFETY: A 'LabelBuf' always contains a valid 'Label'. + unsafe { Label::from_bytes_unchecked(label) } + } +} + +impl DerefMut for LabelBuf { + fn deref_mut(&mut self) -> &mut Self::Target { + let size = self.data[0] as usize; + let label = &mut self.data[..1 + size]; + // SAFETY: A 'LabelBuf' always contains a valid 'Label'. + unsafe { Label::from_bytes_unchecked_mut(label) } + } +} + +impl Borrow for Ipv4Addr { + fn from(value: A) -> Self { + Self::from(value.octets) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for A { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.octets.cmp(&other.octets) + } +} + +//--- Parsing from a string + +impl FromStr for A { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ipv4Addr::from_str(s).map(A::from) + } +} + +//--- Formatting + +impl fmt::Debug for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "A({self})") + } +} + +impl fmt::Display for A { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ipv4Addr::from(*self).fmt(f) + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for A { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for A { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + core::mem::size_of::(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing record data + +impl ParseRecordData<'_> for A {} + +impl ParseRecordDataBytes<'_> for A { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::A => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} + +impl<'a> ParseRecordData<'a> for &'a A {} + +impl<'a> ParseRecordDataBytes<'a> for &'a A { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::A => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/cname.rs b/src/new/rdata/basic/cname.rs new file mode 100644 index 000000000..7b9ff4c61 --- /dev/null +++ b/src/new/rdata/basic/cname.rs @@ -0,0 +1,199 @@ +//! The CNAME record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- CName ---------------------------------------------------------- + +/// The canonical name for this domain. +/// +/// A [`CName`] record indicates that a domain name is an alias. Any data +/// associated with that domain name originates from the "canonical" domain +/// name (with a few DNSSEC-related exceptions). If a domain name is an +/// alias, it has a single canonical name (see [RFC 2181, section 10.1]); it +/// cannot have multiple distinct [`CName`] records. +/// +/// [RFC 2181, section 10.1]: https://datatracker.ietf.org/doc/html/rfc2181#section-10.1 +/// +/// [`CName`] is specified by [RFC 1035, section 3.3.1]. The behaviour of DNS +/// lookups and name servers is specified by [RFC 1034, section 3.6.2]. +/// +/// [RFC 1034, section 3.6.2]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.6.2 +/// [RFC 1035, section 3.3.1]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.1 +/// +/// ## Wire Format +/// +/// The wire format of a [`CName`] record is simply the canonical domain name. +/// This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`CName`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`CName`], it's first important to choose a domain +/// name type. For short-term usage (where the [`CName`] is a local +/// variable), it is common to pick [`RevNameBuf`]. If the [`CName`] will +/// be placed on the heap, Box<[`RevName`]> will be more +/// efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`CName`] is to construct each +/// field manually. To parse a [`CName`] from a DNS message, use +/// [`ParseMessageBytes`]. In case the input bytes don't use name +/// compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::CName; +/// # +/// // Build a 'CName' manually: +/// let manual: CName = CName { +/// name: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'CName' from the wire format, without name decompression: +/// let from_wire: CName = CName::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`CName`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`CName::map_name()`] and +/// [`CName::map_name_by_ref()`]. +/// +/// For debugging, [`CName`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`CName`] in the wire format, use [`BuildInMessage`] +/// (which supports name compression). If name compression is not desired, +/// use [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct CName { + /// The canonical name. + pub name: N, +} + +//--- Interaction + +impl CName { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> CName { + CName { + name: (f)(self.name), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> CName { + CName { + name: (f)(&self.name), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for CName { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.name.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.name.cmp_lowercase_composed(&other.name) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for CName { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|name| Self { name }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for CName { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.name.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for CName { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::CNAME => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for CName { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::CNAME => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/hinfo.rs b/src/new/rdata/basic/hinfo.rs new file mode 100644 index 000000000..0fd8367a9 --- /dev/null +++ b/src/new/rdata/basic/hinfo.rs @@ -0,0 +1,229 @@ +//! The HINFO record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, +}; +use crate::new::base::{ + CanonicalRecordData, CharStr, ParseRecordData, ParseRecordDataBytes, + RType, +}; + +//----------- HInfo ---------------------------------------------------------- + +/// Information about the host computer. +/// +/// [`HInfo`] describes the hardware and software of the server associated +/// with the domain name. It is not commonly used for its original purpose, +/// given several issues: +/// +/// 1. A domain name can be associated with multiple servers (due to having +/// multiple IP addresses or using IP anycast), but [`HInfo`] does not +/// provide a way to associate the information it provides with a specific +/// server (or at least IP address). +/// +/// 2. The CPU and OS name are expected to be standardized, but given the +/// massive (and growing) number of both, it would be impossible to cover +/// every possibility. [RFC 1010] listed the initial set of names, and it +/// has evolved into the online lists of [operating system names] (last +/// updated in 2010) and [machine names] (last updated in 2001). +/// +/// 3. As documented by [RFC 1035], the "main use" for [`HInfo`] records was +/// "for protocols such as FTP that can use special procedures when talking +/// between machines or operating systems of the same type". But given the +/// portabilitiy of most protocols across machines and operating systems, +/// [`HInfo`] is not very informative. Protocols typically provide +/// extension mechanisms in-band instead of relying on out-of-band DNS +/// information. +/// +/// 4. [RFC 8482, section 6] states that "the HINFO RRTYPE is believed to be +/// rarely used in the DNS at the time of writing, based on observations +/// made in passive DNS and at recursive and authoritative DNS servers". +/// +/// [RFC 1010]: https://datatracker.ietf.org/doc/html/rfc1010 +/// [RFC 1035]: https://datatracker.ietf.org/doc/html/rfc1035 +/// [RFC 8482, section 6]: https://datatracker.ietf.org/doc/html/rfc8482#section-6 +/// [operating system names]: https://www.iana.org/assignments/operating-system-names/operating-system-names.xhtml +/// [machine names]: https://www.iana.org/assignments/machine-names/machine-names.xhtml +/// +/// Recently, [`HInfo`] has gained new use, as a potential fallback response +/// for [`QType::ANY`] queries. [RFC 8482] specifies that name servers +/// wishing to avoid answering [`QType::ANY`] queries (which are expensive +/// to look up, have an amplifying network effect, and can be abused for DoS +/// attacks) can respond with a synthesized [`HInfo`] record instead. +/// +/// [`QType::ANY`]: crate::new::base::QType::ANY +/// [RFC 8482]: https://datatracker.ietf.org/doc/html/rfc8482 +/// +/// [`HInfo`] is specified by [RFC 1035, section 3.3.2]. Its use as an +/// alternative response to [`QType::ANY`] queries is documented by [RFC 8482, +/// section 4.2]. +/// +/// [RFC 1035, section 3.3.2]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.2 +/// [RFC 8482, section 4.2]: https://datatracker.ietf.org/doc/html/rfc8482#section-4.2 +/// +/// ## Wire Format +/// +/// The wire format of an [`HInfo`] record is the concatenation of two +/// "character strings" (see [`CharStr`]). The first specifies the "machine +/// name" of the host computer, and the second specifies the name of the +/// operating system it is running. +/// +/// ## Usage +/// +/// Because [`HInfo`] is a record data type, it is usually handled within an +/// enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// There's a few ways to build an [`HInfo`]: +/// +/// ``` +/// # use domain::new::base::wire::{BuildBytes, ParseBytes}; +/// # use domain::new::rdata::HInfo; +/// # +/// use domain::new::base::CharStrBuf; +/// +/// // Build an 'HInfo' manually. +/// let cpu: CharStrBuf = "DEC-2060".parse().unwrap(); +/// let os: CharStrBuf = "TOPS20".parse().unwrap(); +/// let manual: HInfo<'_> = HInfo { cpu: &*cpu, os: &*os }; +/// +/// let bytes = b"\x08DEC-2060\x06TOPS20"; +/// # let mut buffer = [0u8; 16]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'HInfo' from the DNS wire format. +/// let from_wire: HInfo<'_> = HInfo::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// ``` +/// +/// Since [`HInfo`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, it is bound by +/// the lifetime of the borrowed character strings. At the moment, there is +/// no perfect way to own an [`HInfo`] without a lifetime restriction (largely +/// because it is not commonly used), however: +/// +#[cfg_attr(feature = "alloc", doc = " - [`BoxedRecordData`] ")] +#[cfg_attr(not(feature = "alloc"), doc = " - `BoxedRecordData` ")] +/// is capable of doing so, but it does not guarantee that it holds an +/// [`HInfo`] (it can hold any record data type). +/// +/// - If [`bumpalo`] is being used, +#[cfg_attr(feature = "bumpalo", doc = " [`HInfo::clone_to_bump()`]")] +#[cfg_attr(not(feature = "bumpalo"), doc = " `HInfo::clone_to_bump()`")] +/// can clone an [`HInfo`] over to a bump allocator. This may extend its +/// lifetime sufficiently for some use cases. +/// +#[cfg_attr( + not(feature = "bumpalo"), + doc = "[`bumpalo`]: https://docs.rs/bumpalo/latest/bumpalo/" +)] +#[cfg_attr( + feature = "alloc", + doc = "[`BoxedRecordData`]: crate::new::rdata::BoxedRecordData" +)] +/// +/// For debugging [`HInfo`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`HInfo`] in the wire format, use [`BuildBytes`]. It also +/// supports [`BuildInMessage`]. +#[derive( + Copy, Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes, SplitBytes, +)] +pub struct HInfo<'a> { + /// The type of the machine hosting the domain name. + pub cpu: &'a CharStr, + + /// The type of the operating system hosting the domain name. + pub os: &'a CharStr, +} + +//--- Interaction + +impl HInfo<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> HInfo<'r> { + use crate::utils::dst::copy_to_bump; + + HInfo { + cpu: copy_to_bump(self.cpu, bump), + os: copy_to_bump(self.os, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for HInfo<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this = ( + self.cpu.len(), + &self.cpu.octets, + self.os.len(), + &self.os.octets, + ); + let that = ( + that.cpu.len(), + &that.cpu.octets, + that.os.len(), + &that.os.octets, + ); + this.cmp(&that) + } +} + +//--- Parsing from DNS messages + +impl<'a> ParseMessageBytes<'a> for HInfo<'a> { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for HInfo<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self.cpu.build_in_message(contents, start, compressor)?; + start = self.os.build_in_message(contents, start, compressor)?; + Ok(start) + } +} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for HInfo<'a> {} + +impl<'a> ParseRecordDataBytes<'a> for HInfo<'a> { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::HINFO => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/mod.rs b/src/new/rdata/basic/mod.rs new file mode 100644 index 000000000..d633d50a7 --- /dev/null +++ b/src/new/rdata/basic/mod.rs @@ -0,0 +1,27 @@ +//! Core record data types. +//! +//! See [RFC 1035](https://datatracker.ietf.org/doc/html/rfc1035). + +mod a; +pub use a::A; + +mod ns; +pub use ns::Ns; + +mod cname; +pub use cname::CName; + +mod soa; +pub use soa::Soa; + +mod ptr; +pub use ptr::Ptr; + +mod hinfo; +pub use hinfo::HInfo; + +mod mx; +pub use mx::Mx; + +mod txt; +pub use txt::Txt; diff --git a/src/new/rdata/basic/mx.rs b/src/new/rdata/basic/mx.rs new file mode 100644 index 000000000..a9b5f0ac0 --- /dev/null +++ b/src/new/rdata/basic/mx.rs @@ -0,0 +1,218 @@ +//! The MX record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Mx ------------------------------------------------------------- + +/// A host that can exchange mail for this domain. +/// +/// An [`Mx`] record indicates that a domain name can receive e-mail, and it +/// specifies (the domain name of) the mail server that e-mail for that domain +/// should be sent to. A domain name can be associated with multiple mail +/// servers (using multiple [`Mx`] records); each one is assigned a priority +/// for load balancing. +/// +// TODO: If there's a conventional algorithm for picking a mail server (i.e. +// how the probabilities are calculated for a random selection), add it here. +// +/// [`Mx`] is specified by [RFC 1035, section 3.3.9]. +/// +/// [RFC 1035, section 3.3.9]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.9 +/// +/// ## Wire Format +/// +/// The wire format of an [`Mx`] record is the 16-bit preference number (as a +/// big-endian integer) followed by the domain name of the mail server. This +/// domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Mx`] is a record data type, it is usually handled within an enum +/// like [`RecordData`]. This section describes how to use it independently +/// (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build an [`Mx`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Mx`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Mx`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Mx`] is to construct each field manually. +/// To parse an [`Mx`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Mx; +/// # +/// // Build an 'Mx' manually: +/// let manual: Mx = Mx { +/// preference: 10.into(), +/// exchange: "example.org".parse().unwrap(), +/// }; +/// +/// let bytes = b"\x00\x0A\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 15]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'Mx' from the wire format, without name decompression: +/// let from_wire: Mx = Mx::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Mx`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Mx::map_name()`] and +/// [`Mx::map_name_by_ref()`]. +/// +/// For debugging, [`Mx`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`Mx`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(C)] +pub struct Mx { + /// The preference for this host over others. + pub preference: U16, + + /// The domain name of the mail exchanger. + pub exchange: N, +} + +//--- Interaction + +impl Mx { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Mx { + Mx { + preference: self.preference, + exchange: (f)(self.exchange), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Mx { + Mx { + preference: self.preference, + exchange: (f)(&self.exchange), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Mx { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let bytes = self.preference.build_bytes(bytes)?; + let bytes = self.exchange.build_lowercased_bytes(bytes)?; + Ok(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.preference.cmp(&other.preference).then_with(|| { + self.exchange.cmp_lowercase_composed(&other.exchange) + }) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Mx { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + let (&preference, rest) = + <&U16>::split_message_bytes(contents, start)?; + let exchange = N::parse_message_bytes(contents, rest)?; + Ok(Self { + preference, + exchange, + }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Mx { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self + .preference + .as_bytes() + .build_in_message(contents, start, compressor)?; + start = self + .exchange + .build_in_message(contents, start, compressor)?; + Ok(start) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Mx { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::MX => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Mx { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::MX => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/ns.rs b/src/new/rdata/basic/ns.rs new file mode 100644 index 000000000..a4040ba14 --- /dev/null +++ b/src/new/rdata/basic/ns.rs @@ -0,0 +1,204 @@ +//! The NS record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + BuildBytes, ParseBytes, ParseError, SplitBytes, TruncationError, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Ns ------------------------------------------------------------- + +/// The authoritative name server for this domain. +/// +/// An [`Ns`] record indicates that a domain name is the apex of a DNS zone, +/// and it specifies (the domain name of) the name server that queries about +/// the domain name (and its descendants) should be sent to. A domain name +/// can be associated with multiple name servers (using multiple [`Ns`] +/// records). +/// +/// DNS is designed around the concept of delegating responsibility for domain +/// names. If a name server responds to a query with an empty answer section, +/// but with [`Ns`] records in the authority section, it is claiming to not be +/// the authoritative source of information about the queried domain name; +/// the [`Ns`] records specify name servers to whom that authority has been +/// delegated. +/// +/// While [`Ns`] records are typically served by a name server to indicate a +/// zone cut, that name server is not authoritative for the record; the [`Ns`] +/// record belongs to the delegated zone and the delegated name server(s). +/// +/// [`Ns`] is specified by [RFC 1035, section 3.3.11]. +/// +/// [RFC 1035, section 3.3.11]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.11 +/// +/// ## Wire format +/// +/// The wire format of an [`Ns`] record is simply the domain name of the name +/// server. This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Ns`] is a record data type, it is usually handled within an enum +/// like [`RecordData`]. This section describes how to use it independently +/// (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build an [`Ns`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Ns`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Ns`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Ns`] is to construct each field manually. +/// To parse an [`Ns`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Ns; +/// # +/// // Build an 'Ns' manually: +/// let manual: Ns = Ns { +/// server: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse an 'Ns' from the wire format, without name decompression: +/// let from_wire: Ns = Ns::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Ns`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Ns::map_name()`] and +/// [`Ns::map_name_by_ref()`]. +/// +/// For debugging, [`Ns`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize an [`Ns`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct Ns { + /// The name of the authoritative server. + pub server: N, +} + +//--- Interaction + +impl Ns { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Ns { + Ns { + server: (f)(self.server), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Ns { + Ns { + server: (f)(&self.server), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Ns { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.server.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.server.cmp_lowercase_composed(&other.server) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Ns { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|server| Self { server }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Ns { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.server.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Ns { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::NS => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Ns { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::NS => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/ptr.rs b/src/new/rdata/basic/ptr.rs new file mode 100644 index 000000000..7312a406c --- /dev/null +++ b/src/new/rdata/basic/ptr.rs @@ -0,0 +1,193 @@ +//! The PTR record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +//----------- Ptr ------------------------------------------------------------ + +/// A pointer to another domain name. +/// +/// A [`Ptr`] record is used with special domain names for pointing to other +/// locations in the domain name space. It is conventionally used for reverse +/// lookups: for example, the [`Ptr`] record for `.in-addr.arpa` points +/// to the domain name using the IPv4 `` in an [`A`] record. The same +/// technique works with `.ip6.arpa` for IPv6 addresses. +/// +/// [`A`]: crate::new::rdata::A +/// +/// [`Ptr`] is specified by [RFC 1035, section 3.3.12]. +/// +/// [RFC 1035, section 3.3.12]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.12 +/// +/// ## Wire format +/// +/// The wire format of a [`Ptr`] record is simply the domain name of the name +/// server. This domain name may be compressed in DNS messages. +/// +/// ## Usage +/// +/// Because [`Ptr`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`Ptr`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Ptr`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Ptr`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Ptr`] is to construct each field manually. +/// To parse a [`Ptr`] from a DNS message, use [`ParseMessageBytes`]. In case +/// the input bytes don't use name compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Ptr; +/// # +/// // Build a 'Ptr' manually: +/// let manual: Ptr = Ptr { +/// name: "example.org".parse().unwrap(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\x07example\x03org\x00"; +/// # let mut buffer = [0u8; 13]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'Ptr' from the wire format, without name decompression: +/// let from_wire: Ptr = Ptr::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Ptr`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Ptr::map_name()`] and +/// [`Ptr::map_name_by_ref()`]. +/// +/// For debugging, [`Ptr`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`Ptr`] in the wire format, use [`BuildInMessage`] (which +/// supports name compression). If name compression is not desired, use +/// [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +#[repr(transparent)] +pub struct Ptr { + /// The referenced domain name. + pub name: N, +} + +//--- Interaction + +impl Ptr { + /// Map the domain name within to another type. + pub fn map_name R>(self, f: F) -> Ptr { + Ptr { + name: (f)(self.name), + } + } + + /// Map a reference to the domain name within to another type. + pub fn map_name_by_ref<'r, R, F: FnOnce(&'r N) -> R>( + &'r self, + f: F, + ) -> Ptr { + Ptr { + name: (f)(&self.name), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Ptr { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.name.build_lowercased_bytes(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.name.cmp_lowercase_composed(&other.name) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: ParseMessageBytes<'a>> ParseMessageBytes<'a> for Ptr { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + N::parse_message_bytes(contents, start).map(|name| Self { name }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Ptr { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + compressor: &mut NameCompressor, + ) -> Result { + self.name.build_in_message(contents, start, compressor) + } +} + +//--- Parsing record data + +impl<'a, N: ParseMessageBytes<'a>> ParseRecordData<'a> for Ptr { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::PTR => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: ParseBytes<'a>> ParseRecordDataBytes<'a> for Ptr { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::PTR => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/soa.rs b/src/new/rdata/basic/soa.rs new file mode 100644 index 000000000..b4b18b48f --- /dev/null +++ b/src/new/rdata/basic/soa.rs @@ -0,0 +1,359 @@ +//! The SOA record data type. + +use core::cmp::Ordering; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::name::CanonicalName; +use crate::new::base::parse::{ParseMessageBytes, SplitMessageBytes}; +use crate::new::base::{ + wire::*, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::new::base::{CanonicalRecordData, Serial}; + +//----------- Soa ------------------------------------------------------------ + +/// The start of a zone of authority. +/// +/// A [`Soa`] record indicates that a domain name is the apex of a DNS zone. +/// It provides several parameters to describe how the zone should be used, +/// e.g. how often it should be refreshed. +/// +/// [`Soa`]'s most important use is to detect changes to the zone. Whenever +/// the zone is changed, [`Soa::serial`] is incremented; secondary DNS servers +/// (which cache and redistribute the contents of the zone) can thus detect +/// whether they need to update their cache. +/// +// TODO: Is there a strict definition to "whenever the zone is changed"? +// +/// Every zone has exactly one [`Soa`] record, and it is located at the apex. +/// The zone (along with its authoritative name servers) is authoritative for +/// the record. +/// +/// [`Soa`] is specified by [RFC 1035, section 3.3.13]. The behaviour of +/// secondary name servers using [`Soa`] to check for updates to a zone is +/// specified by [RFC 1034, section 4.3.5]. +/// +/// [RFC 1034, section 4.3.5]: https://datatracker.ietf.org/doc/html/rfc1034#section-4.3.5 +/// [RFC 1035, section 3.3.13]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.13 +/// +/// ## Wire format +/// +/// The wire format of a [`Soa`] record is the concatenation of its fields, in +/// the same order as the `struct` definition. The domain names within a +/// [`Soa`] may be compressed in DNS messages. Every other field is an +/// unsigned 32-bit big-endian integer. +/// +/// ## Usage +/// +/// Because [`Soa`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// In order to build a [`Soa`], it's first important to choose a domain name +/// type. For short-term usage (where the [`Soa`] is a local variable), it is +/// common to pick [`RevNameBuf`]. If the [`Soa`] will be placed on the heap, +/// Box<[`RevName`]> will be more efficient. +/// +/// [`RevName`]: crate::new::base::name::RevName +/// [`RevNameBuf`]: crate::new::base::name::RevNameBuf +/// +/// The primary way to build a new [`Soa`] is to construct each +/// field manually. To parse a [`Soa`] from a DNS message, use +/// [`ParseMessageBytes`]. In case the input bytes don't use name +/// compression, [`ParseBytes`] can be used. +/// +/// ``` +/// # use domain::new::base::name::{Name, RevNameBuf}; +/// # use domain::new::base::wire::{BuildBytes, ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Soa; +/// # +/// // Build a 'Soa' manually: +/// let manual: Soa = Soa { +/// mname: "ns.example.org".parse().unwrap(), +/// rname: "admin.example.org".parse().unwrap(), +/// serial: 42.into(), +/// refresh: 3600.into(), +/// retry: 600.into(), +/// expire: 18000.into(), +/// minimum: 150.into(), +/// }; +/// +/// // Its wire format serialization looks like: +/// let bytes = b"\ +/// \x02ns\x07example\x03org\x00\ +/// \x05admin\x07example\x03org\x00\ +/// \x00\x00\x00\x2A\ +/// \x00\x00\x0E\x10\ +/// \x00\x00\x02\x58\ +/// \x00\x00\x46\x50\ +/// \x00\x00\x00\x96"; +/// # let mut buffer = [0u8; 55]; +/// # manual.build_bytes(&mut buffer).unwrap(); +/// # assert_eq!(*bytes, buffer); +/// +/// // Parse a 'Soa' from the wire format, without name decompression: +/// let from_wire: Soa = Soa::parse_bytes(bytes).unwrap(); +/// # assert_eq!(manual, from_wire); +/// +/// // See 'ParseMessageBytes' for parsing with name decompression. +/// ``` +/// +/// Since [`Soa`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. However, this depends on +/// the domain name type. It can be changed using [`Soa::map_names()`] and +/// [`Soa::map_names_by_ref()`]. +/// +/// For debugging, [`Soa`] can be formatted using [`fmt::Debug`]. +/// +/// [`fmt::Debug`]: core::fmt::Debug +/// +/// To serialize a [`Soa`] in the wire format, use [`BuildInMessage`] +/// (which supports name compression). If name compression is not desired, +/// use [`BuildBytes`]. +#[derive( + Copy, + Clone, + Debug, + PartialEq, + Eq, + Hash, + BuildBytes, + ParseBytes, + SplitBytes, +)] +pub struct Soa { + /// The original/primary name server for this zone. + /// + /// This domain name should point to a name server that is authoritative + /// for this zone -- more specifically, that is the original source of + /// information that all other name servers are (directly or indirectly) + /// loading this zone from. This need not be listed in the [`Ns`] records + /// for this zone, if it is not intended for public querying. + /// + /// [`Ns`]: crate::new::rdata::Ns + pub mname: N, + + /// The mailbox of the maintainer of this zone. + /// + /// The first label here is the username (i.e. local part) of the e-mail + /// address, and the remaining labels make up the mail domain name. For + /// example, would be represented as + /// `hostmaster.sri-nic.arpa`. This convention is specified in [RFC 1034, + /// section 3.3]. + /// + /// [RFC 1034, section 3.3]: https://datatracker.ietf.org/doc/html/rfc1034#section-3.3 + pub rname: N, + + /// The version number of the original copy of this zone. + /// + /// This value is increased when the contents of the zone change. If a + /// secondary name server wishes to cache the contents of this zone, it + /// can periodically check the version number from the primary name server + /// to determine whether it needs to update its cache. + /// + /// There are multiple conventions for versioning strategies. Some zones + /// will increase this value by 1 when a change occurs; some set it to the + /// Unix timestamp of the latest change; others set it so that the decimal + /// representation includes the current date. As long as the version + /// number increases (by a relatively small value) upon every change, any + /// strategy is valid. + /// + /// This field is represented using [`Serial`], which provides special + /// "sequence space arithmetic". This ensures that ordering comparisons + /// are well-defined even if the number overflows modulo `2^32`. See its + /// documentation for more information. + pub serial: Serial, + + /// The number of seconds to wait until refreshing the zone. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. The server is + /// expected to wait this long (in seconds) after the last successful + /// check, before checking again. + /// + /// If checking fails, however, the server uses a different periodicity; + /// see [`Soa::retry`]. + /// + /// Note that there are alternative means for keeping up to date with a + /// primary name server -- see DNS NOTIFY ([RFC 1996]). + /// + /// [RFC 1996]: https://datatracker.ietf.org/doc/html/rfc1996 + pub refresh: U32, + + /// The number of seconds to wait until retrying a failed refresh. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. The server is + /// expected to wait this long (in seconds) after the last _failing_ check + /// before trying again. + /// + /// Once a check is successful, the server should resume using the + /// [`Soa::refresh`] time. + pub retry: U32, + + /// The number of seconds until the zone is considered expired. + /// + /// If a secondary name server is caching and serving a zone, it is + /// expected to periodically check the zone's serial number in the + /// primary name server for changes to the zone contents. If the server + /// fails to check for or retrieve updates to the zone for this period of + /// time (in seconds), it should consider its copy of the zone obsolete + /// and should discard it. + pub expire: U32, + + /// The minimum TTL for any record in this zone. + /// + /// The meaning of this field has changed over time. According to [RFC + /// 2308, section 4], it is the time for which a negative response (i.e. + /// that a certain record does not exist) should be cached. [RFC 4035, + /// section 2.3] likewise states that the [`NSec`] records for a zone + /// should have a TTL of this value. + /// + /// [`NSec`]: crate::new::rdata::NSec + /// [RFC 2308, section 4]: https://datatracker.ietf.org/doc/html/rfc2308#section-4 + /// [RFC 4035, section 2.3]: https://datatracker.ietf.org/doc/html/rfc4035#section-2.3 + pub minimum: U32, +} + +//--- Interaction + +impl Soa { + /// Map the domain names within to another type. + pub fn map_names R>(self, mut f: F) -> Soa { + Soa { + mname: (f)(self.mname), + rname: (f)(self.rname), + serial: self.serial, + refresh: self.refresh, + retry: self.retry, + expire: self.expire, + minimum: self.minimum, + } + } + + /// Map references to the domain names within to another type. + pub fn map_names_by_ref<'r, R, F: FnMut(&'r N) -> R>( + &'r self, + mut f: F, + ) -> Soa { + Soa { + mname: (f)(&self.mname), + rname: (f)(&self.rname), + serial: self.serial, + refresh: self.refresh, + retry: self.retry, + expire: self.expire, + minimum: self.minimum, + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Soa { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + let bytes = self.mname.build_lowercased_bytes(bytes)?; + let bytes = self.rname.build_lowercased_bytes(bytes)?; + let bytes = self.serial.build_bytes(bytes)?; + let bytes = self.refresh.build_bytes(bytes)?; + let bytes = self.retry.build_bytes(bytes)?; + let bytes = self.expire.build_bytes(bytes)?; + let bytes = self.minimum.build_bytes(bytes)?; + Ok(bytes) + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.mname + .cmp_lowercase_composed(&other.mname) + .then_with(|| self.rname.cmp_lowercase_composed(&other.rname)) + .then_with(|| self.serial.as_bytes().cmp(other.serial.as_bytes())) + .then_with(|| self.refresh.cmp(&other.refresh)) + .then_with(|| self.retry.cmp(&other.retry)) + .then_with(|| self.expire.cmp(&other.expire)) + .then_with(|| self.minimum.cmp(&other.minimum)) + } +} + +//--- Parsing from DNS messages + +impl<'a, N: SplitMessageBytes<'a>> ParseMessageBytes<'a> for Soa { + fn parse_message_bytes( + contents: &'a [u8], + start: usize, + ) -> Result { + let (mname, rest) = N::split_message_bytes(contents, start)?; + let (rname, rest) = N::split_message_bytes(contents, rest)?; + let (&serial, rest) = <&Serial>::split_message_bytes(contents, rest)?; + let (&refresh, rest) = <&U32>::split_message_bytes(contents, rest)?; + let (&retry, rest) = <&U32>::split_message_bytes(contents, rest)?; + let (&expire, rest) = <&U32>::split_message_bytes(contents, rest)?; + let &minimum = <&U32>::parse_message_bytes(contents, rest)?; + + Ok(Self { + mname, + rname, + serial, + refresh, + retry, + expire, + minimum, + }) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Soa { + fn build_in_message( + &self, + contents: &mut [u8], + mut start: usize, + compressor: &mut NameCompressor, + ) -> Result { + start = self.mname.build_in_message(contents, start, compressor)?; + start = self.rname.build_in_message(contents, start, compressor)?; + // Build the remaining bytes manually. + let end = start + 20; + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes[0..4].copy_from_slice(self.serial.as_bytes()); + bytes[4..8].copy_from_slice(self.refresh.as_bytes()); + bytes[8..12].copy_from_slice(self.retry.as_bytes()); + bytes[12..16].copy_from_slice(self.expire.as_bytes()); + bytes[16..20].copy_from_slice(self.minimum.as_bytes()); + Ok(end) + } +} + +//--- Parsing record data + +impl<'a, N: SplitMessageBytes<'a>> ParseRecordData<'a> for Soa { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::SOA => Self::parse_message_bytes(contents, start), + _ => Err(ParseError), + } + } +} + +impl<'a, N: SplitBytes<'a>> ParseRecordDataBytes<'a> for Soa { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::SOA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/basic/txt.rs b/src/new/rdata/basic/txt.rs new file mode 100644 index 000000000..3b02801d3 --- /dev/null +++ b/src/new/rdata/basic/txt.rs @@ -0,0 +1,238 @@ +//! The TXT record data type. + +use core::{cmp::Ordering, fmt}; + +use crate::new::base::build::{BuildInMessage, NameCompressor}; +use crate::new::base::wire::*; +use crate::new::base::{ + CanonicalRecordData, CharStr, ParseRecordData, ParseRecordDataBytes, + RType, +}; +use crate::utils::dst::UnsizedCopy; + +//----------- Txt ------------------------------------------------------------ + +/// Free-form text strings about this domain. +/// +/// A [`Txt`] record holds a collection of "strings" (really byte sequences), +/// with no fixed purpose. Usually, a [`Txt`] record holds a single string; +/// if data has to be stored for different purposes, multiple [`Txt`] records +/// would be used. +/// +/// Currently, [`Txt`] records are used systematically for e-mail security, +/// e.g. in SPF ([RFC 7208, section 3]), DKIM ([RFC 6376, section 3.6.2]), and +/// DMARC ([RFC 7489, section 6.1]). As a record data type with no strict +/// semantics and arbitrary data storage, it is likely to continue being +/// used. +/// +/// [RFC 6376, section 3.6.2]: https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.2 +/// [RFC 7208, section 3]: https://datatracker.ietf.org/doc/html/rfc7208#section-3 +/// [RFC 7489, section 6.1]: https://datatracker.ietf.org/doc/html/rfc7489#section-6.1 +/// +/// [`Txt`] is specified by [RFC 1035, section 3.3.14]. +/// +/// [RFC 1035, section 3.3.14]: https://datatracker.ietf.org/doc/html/rfc1035#section-3.3.14 +/// +/// ## Wire Format +/// +/// The wire format of a [`Txt`] record is the concatenation of a (non-empty) +/// sequence of "character strings" (see [`CharStr`]). A character string is +/// serialized as a 1-byte length, followed by up to 255 bytes of content. +/// +/// The memory layout of the [`Txt`] type is identical to its serialization in +/// the wire format. This means it can be parsed from the wire format in a +/// zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Txt`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// [`Txt`] is a _dynamically sized type_ (DST). It is not possible to store +/// a [`Txt`] in place (e.g. in a local variable); it must be held indirectly, +/// via a reference or a smart pointer type like [`Box`]. This makes it more +/// difficult to _create_ new [`Txt`]s; but once they are placed somewhere, +/// they can be used by reference (i.e. `&Txt`) exactly like any other type. +/// +/// [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +/// +/// It is currently a bit difficult to build a new [`Txt`] from scratch. It +/// is easiest to build the wire format representation of the [`Txt`] manually +/// (by building a sequence of [`CharStr`]s) and then to parse it. +/// +/// ``` +/// # use domain::new::base::CharStrBuf; +/// # use domain::new::base::wire::ParseBytesZC; +/// # use domain::new::rdata::Txt; +/// # +/// // From an existing wire-format representation. +/// let bytes = b"\x0DHello, World!\x0AAnd again!"; +/// let from_bytes: &Txt = Txt::parse_bytes_by_ref(bytes).unwrap(); +/// // It is also possible to use '<&Txt>::parse_bytes()'. +/// +/// // To build a wire-format representation manually: +/// let strings: [CharStrBuf; 2] = [ +/// "Hello, World!".parse().unwrap(), +/// "And again!".parse().unwrap(), +/// ]; +/// let mut buffer: Vec = Vec::new(); +/// for string in &strings { +/// buffer.extend_from_slice(string.wire_bytes()); +/// } +/// assert_eq!(buffer.as_slice(), bytes); +/// +/// // From an existing wire-format representation, but on the heap: +/// let buffer: Box<[u8]> = buffer.into_boxed_slice(); +/// let from_boxed_bytes: Box = Txt::parse_bytes_in(buffer).unwrap(); +/// assert_eq!(from_bytes, &*from_boxed_bytes); +/// ``` +/// +/// As a DST, [`Txt`] does not implement [`Copy`] or [`Clone`]. Instead, it +/// implements [`UnsizedCopy`]. A [`Txt`], held by reference, can be copied +/// into a different container (e.g. `Box`) using [`unsized_copy_into()`]. +/// +/// [`unsized_copy_into()`]: UnsizedCopy::unsized_copy_into() +/// +/// For debugging, [`Txt`] can be formatted using [`fmt::Debug`]. +/// +/// To serialize a [`Txt`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Txt`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Txt { + /// The text strings, as concatenated [`CharStr`]s. + content: [u8], +} + +//--- Construction + +impl Txt { + /// Assume a byte sequence is a valid [`Txt`]. + /// + /// ## Safety + /// + /// The byte sequence must a valid instance of [`Txt`] in the wire format; + /// it must contain one or more serialized [`CharStr`]s, concatenated + /// together. The byte sequence must be at most 65,535 bytes long. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Txt' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute::<&[u8], &Txt>(bytes) } + } +} + +//--- Interaction + +impl Txt { + /// Iterate over the [`CharStr`]s in this record. + pub fn iter(&self) -> impl Iterator + '_ { + // NOTE: A TXT record always has at least one 'CharStr' within. + let first = <&CharStr>::split_bytes(&self.content) + .expect("'Txt' records always contain valid 'CharStr's"); + core::iter::successors(Some(first), |(_, rest)| { + (!rest.is_empty()).then(|| { + <&CharStr>::split_bytes(rest) + .expect("'Txt' records always contain valid 'CharStr's") + }) + }) + .map(|(elem, _rest)| elem) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Txt { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.content.cmp(&other.content) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Txt { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.content.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.content); + Ok(end) + } +} + +//--- Parsing from bytes + +// SAFETY: The implementations of 'parse_bytes_by_{ref,mut}()' always parse +// the entirety of the input on success, satisfying the safety requirements. +unsafe impl ParseBytesZC for Txt { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + // Make sure the slice is 64KiB or less. + if bytes.len() > 65535 { + return Err(ParseError); + } + + // The input must contain at least one 'CharStr'. + let (_, mut rest) = <&CharStr>::split_bytes(bytes)?; + while !rest.is_empty() { + (_, rest) = <&CharStr>::split_bytes(rest)?; + } + + // SAFETY: 'Txt' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], &Self>(bytes) }) + } +} + +//--- Formatting + +impl fmt::Debug for Txt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + struct Content<'a>(&'a Txt); + impl fmt::Debug for Content<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_list().entries(self.0.iter()).finish() + } + } + + f.debug_tuple("Txt").field(&Content(self)).finish() + } +} + +//--- Equality + +impl PartialEq for Txt { + /// Compare two [`Txt`]s for equality. + /// + /// Two [`Txt`]s are considered equal if they have an equal sequence of + /// character strings, laid out in the same order; corresponding character + /// strings are compared ASCII-case-insensitively. + fn eq(&self, other: &Self) -> bool { + self.iter().eq(other.iter()) + } +} + +impl Eq for Txt {} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for &'a Txt {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Txt { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::TXT => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/dnssec/dnskey.rs b/src/new/rdata/dnssec/dnskey.rs new file mode 100644 index 000000000..7bc1ce582 --- /dev/null +++ b/src/new/rdata/dnssec/dnskey.rs @@ -0,0 +1,139 @@ +//! The DNSKEY record data type. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::{ + build::BuildInMessage, + name::NameCompressor, + wire::{AsBytes, TruncationError, U16}, + CanonicalRecordData, +}; + +use super::SecAlg; + +//----------- DNSKey --------------------------------------------------------- + +/// A cryptographic key for DNS security. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(C)] +pub struct DNSKey { + /// Flags describing the usage of the key. + pub flags: DNSKeyFlags, + + /// The protocol version of the key. + pub protocol: u8, + + /// The cryptographic algorithm used by this key. + pub algorithm: SecAlg, + + /// The serialized public key. + pub key: [u8], +} + +//--- Canonical operations + +impl CanonicalRecordData for DNSKey { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for DNSKey { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- DNSKeyFlags ---------------------------------------------------- + +/// Flags describing a [`DNSKey`]. +#[derive( + Copy, + Clone, + Default, + Hash, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct DNSKeyFlags { + /// The raw flag bits. + inner: U16, +} + +//--- Interaction + +impl DNSKeyFlags { + /// Get the specified flag bit. + fn get_flag(&self, pos: u32) -> bool { + self.inner.get() & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(mut self, pos: u32, value: bool) -> Self { + self.inner &= !(1 << pos); + self.inner |= (value as u16) << pos; + self + } + + /// The raw flags bits. + pub fn bits(&self) -> u16 { + self.inner.get() + } + + /// Whether this key is used for signing DNS records. + pub fn is_zone_key(&self) -> bool { + self.get_flag(8) + } + + /// Make this key usable for signing DNS records. + pub fn set_zone_key(self, value: bool) -> Self { + self.set_flag(8, value) + } + + /// Whether external entities are expected to point to this key. + pub fn is_secure_entry_point(&self) -> bool { + self.get_flag(0) + } + + /// Expect external entities to point to this key. + pub fn set_secure_entry_point(self, value: bool) -> Self { + self.set_flag(0, value) + } +} + +//--- Formatting + +impl fmt::Debug for DNSKeyFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DNSKeyFlags") + .field("zone_key", &self.is_zone_key()) + .field("secure_entry_point", &self.is_secure_entry_point()) + .field("bits", &self.bits()) + .finish() + } +} diff --git a/src/new/rdata/dnssec/ds.rs b/src/new/rdata/dnssec/ds.rs new file mode 100644 index 000000000..f68becb69 --- /dev/null +++ b/src/new/rdata/dnssec/ds.rs @@ -0,0 +1,104 @@ +//! The DS record data type. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::wire::{AsBytes, U16}; +use crate::new::base::CanonicalRecordData; + +use super::SecAlg; + +//----------- Ds ------------------------------------------------------------- + +/// The signing key for a delegated zone. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(C)] +pub struct Ds { + /// The key tag of the signing key. + pub keytag: U16, + + /// The cryptographic algorithm used by the signing key. + pub algorithm: SecAlg, + + /// The algorithm used to calculate the key digest. + pub digest_type: DigestType, + + /// A serialized digest of the signing key. + pub digest: [u8], +} + +//--- Canonical operations + +impl CanonicalRecordData for Ds { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for Ds { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- DigestType ----------------------------------------------------- + +/// A cryptographic digest algorithm. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct DigestType { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl DigestType { + /// The SHA-1 algorithm. + pub const SHA1: Self = Self { code: 1 }; +} + +//--- Formatting + +impl fmt::Debug for DigestType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::SHA1 => "DigestType::SHA1", + _ => return write!(f, "DigestType({})", self.code), + }) + } +} diff --git a/src/new/rdata/dnssec/mod.rs b/src/new/rdata/dnssec/mod.rs new file mode 100644 index 000000000..d21311369 --- /dev/null +++ b/src/new/rdata/dnssec/mod.rs @@ -0,0 +1,69 @@ +//! Record types relating to DNSSEC. + +use core::fmt; + +use domain_macros::*; + +//----------- Submodules ----------------------------------------------------- + +mod dnskey; +pub use dnskey::{DNSKey, DNSKeyFlags}; + +mod rrsig; +pub use rrsig::RRSig; + +mod nsec; +pub use nsec::{NSec, TypeBitmaps}; + +mod nsec3; +pub use nsec3::{NSec3, NSec3Flags, NSec3HashAlg, NSec3Param}; + +mod ds; +pub use ds::{DigestType, Ds}; + +//----------- SecAlg --------------------------------------------------------- + +/// A cryptographic algorithm for DNS security. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct SecAlg { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl SecAlg { + /// The DSA/SHA-1 algorithm. + pub const DSA_SHA1: Self = Self { code: 3 }; + + /// The RSA/SHA-1 algorithm. + pub const RSA_SHA1: Self = Self { code: 5 }; +} + +//--- Formatting + +impl fmt::Debug for SecAlg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::DSA_SHA1 => "SecAlg::DSA_SHA1", + Self::RSA_SHA1 => "SecAlg::RSA_SHA1", + _ => return write!(f, "SecAlg({})", self.code), + }) + } +} diff --git a/src/new/rdata/dnssec/nsec.rs b/src/new/rdata/dnssec/nsec.rs new file mode 100644 index 000000000..a83f910f6 --- /dev/null +++ b/src/new/rdata/dnssec/nsec.rs @@ -0,0 +1,179 @@ +//! The NSEC record data type. + +use core::{cmp::Ordering, fmt}; + +use crate::new::base::build::BuildInMessage; +use crate::new::base::name::{CanonicalName, Name, NameCompressor}; +use crate::new::base::wire::*; +use crate::new::base::{CanonicalRecordData, RType}; +use crate::utils::dst::UnsizedCopy; + +//----------- NSec ----------------------------------------------------------- + +/// An indication of the non-existence of a set of DNS records (version 1). +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes)] +pub struct NSec<'a> { + /// The name of the next existing DNS record. + pub next: &'a Name, + + /// The types of the records that exist at this owner name. + pub types: &'a TypeBitmaps, +} + +//--- Interaction + +impl NSec<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> NSec<'r> { + use crate::utils::dst::copy_to_bump; + + NSec { + next: copy_to_bump(self.next, bump), + types: copy_to_bump(self.types, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for NSec<'_> { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.next + .cmp_composed(other.next) + .then_with(|| self.types.as_bytes().cmp(other.types.as_bytes())) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} + +//--- Parsing from byte sequences + +impl<'a> ParseBytes<'a> for NSec<'a> { + fn parse_bytes(bytes: &'a [u8]) -> Result { + let (next, bytes) = <&Name>::split_bytes(bytes)?; + if bytes.is_empty() { + // An empty type bitmap is not allowed for NSEC. + return Err(ParseError); + } + let types = <&TypeBitmaps>::parse_bytes(bytes)?; + Ok(Self { next, types }) + } +} + +//----------- TypeBitmaps ---------------------------------------------------- + +/// A bitmap of DNS record types. +#[derive(PartialEq, Eq, AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct TypeBitmaps { + /// The bitmap data, encoded in the wire format. + octets: [u8], +} + +//--- Inspection + +impl TypeBitmaps { + /// The types in this bitmap. + pub fn types(&self) -> impl Iterator + '_ { + fn split_window(octets: &[u8]) -> Option<(u8, &[u8], &[u8])> { + let &[num, len, ref rest @ ..] = octets else { + return None; + }; + + let (bits, rest) = rest.split_at(len as usize); + Some((num, bits, rest)) + } + + core::iter::successors(split_window(&self.octets), |(_, _, rest)| { + split_window(rest) + }) + .flat_map(move |(num, bits, _)| { + bits.iter().enumerate().flat_map(move |(i, &b)| { + (0..8).filter(move |&j| ((b >> j) & 1) != 0).map(move |j| { + RType::from(u16::from_be_bytes([num, (i * 8 + j) as u8])) + }) + }) + }) + } +} + +//--- Formatting + +impl fmt::Debug for TypeBitmaps { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_set().entries(self.types()).finish() + } +} + +//--- Parsing + +impl TypeBitmaps { + /// Validate the given bytes as a bitmap in the wire format. + fn validate_bytes(mut octets: &[u8]) -> Result<(), ParseError> { + // NOTE: NSEC records require at least one type in the bitmap, while + // NSEC3 records can have an empty bitmap (see RFC 6840, section 6.4). + + // The window number (i.e. the high byte of the type). + let mut num = None; + while let Some(&next) = octets.first() { + // Make sure that the window number increases. + // NOTE: 'None < Some(_)', for the first iteration. + if num.replace(next) > Some(next) { + return Err(ParseError); + } + + octets = Self::validate_window_bytes(octets)?; + } + + Ok(()) + } + + /// Validate the given bytes as a bitmap window in the wire format. + fn validate_window_bytes(octets: &[u8]) -> Result<&[u8], ParseError> { + let &[_num, len, ref rest @ ..] = octets else { + return Err(ParseError); + }; + + // At most 32 bytes are necessary, to cover the 256 types that could + // be stored in this window. And empty windows are not allowed. + if !(1..=32).contains(&len) || rest.len() < len as usize { + return Err(ParseError); + } + + // TODO(1.80): Use 'split_at_checked()' and eliminate the previous + // conditional (move the range check into the 'let-else'). + let (bits, rest) = rest.split_at(len as usize); + if bits.last() == Some(&0) { + // Trailing zeros are not allowed. + return Err(ParseError); + } + + Ok(rest) + } +} + +// SAFETY: The implementations of 'parse_bytes_by_{ref,mut}()' always parse +// the entirety of the input on success, satisfying the safety requirements. +unsafe impl ParseBytesZC for TypeBitmaps { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + Self::validate_bytes(bytes)?; + + // SAFETY: 'TypeBitmaps' is 'repr(transparent)' to '[u8]', and so + // references to '[u8]' can be transmuted to 'TypeBitmaps' soundly. + unsafe { core::mem::transmute(bytes) } + } +} diff --git a/src/new/rdata/dnssec/nsec3.rs b/src/new/rdata/dnssec/nsec3.rs new file mode 100644 index 000000000..1a3a4f720 --- /dev/null +++ b/src/new/rdata/dnssec/nsec3.rs @@ -0,0 +1,262 @@ +//! The NSEC3 and NSEC3PARAM record data types. + +use core::{cmp::Ordering, fmt}; + +use domain_macros::*; + +use crate::new::base::{ + build::BuildInMessage, + name::NameCompressor, + wire::{AsBytes, BuildBytes, SizePrefixed, TruncationError, U16}, + CanonicalRecordData, +}; + +use super::TypeBitmaps; + +//----------- NSec3 ---------------------------------------------------------- + +/// An indication of the non-existence of a set of DNS records (version 3). +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes)] +pub struct NSec3<'a> { + /// The algorithm used to hash names. + pub algorithm: NSec3HashAlg, + + /// Flags modifying the behaviour of the record. + pub flags: NSec3Flags, + + /// The number of iterations of the underlying hash function per name. + pub iterations: U16, + + /// The salt used to randomize the hash function. + pub salt: &'a SizePrefixed, + + /// The name of the next existing DNS record. + pub next: &'a SizePrefixed, + + /// The types of the records that exist at this owner name. + pub types: &'a TypeBitmaps, +} + +//--- Interaction + +impl NSec3<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> NSec3<'r> { + use crate::utils::dst::copy_to_bump; + + NSec3 { + algorithm: self.algorithm, + flags: self.flags, + iterations: self.iterations, + salt: copy_to_bump(self.salt, bump), + next: copy_to_bump(self.next, bump), + types: copy_to_bump(self.types, bump), + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for NSec3<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this = ( + self.algorithm, + self.flags.as_bytes(), + self.iterations, + self.salt.len(), + self.salt, + self.next.len(), + self.next, + self.types.as_bytes(), + ); + let that = ( + that.algorithm, + that.flags.as_bytes(), + that.iterations, + that.salt.len(), + that.salt, + that.next.len(), + that.next, + that.types.as_bytes(), + ); + this.cmp(&that) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec3<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} + +//----------- NSec3Param ----------------------------------------------------- + +/// Parameters for computing [`NSec3`] records. +#[derive( + Debug, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytesZC, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(C)] +pub struct NSec3Param { + /// The algorithm used to hash names. + pub algorithm: NSec3HashAlg, + + /// Flags modifying the behaviour of the record. + pub flags: NSec3Flags, + + /// The number of iterations of the underlying hash function per name. + pub iterations: U16, + + /// The salt used to randomize the hash function. + pub salt: SizePrefixed, +} + +impl CanonicalRecordData for NSec3Param { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.as_bytes().cmp(other.as_bytes()) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for NSec3Param { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = self.as_bytes(); + let end = start + bytes.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(bytes); + Ok(end) + } +} + +//----------- NSec3HashAlg --------------------------------------------------- + +/// The hash algorithm used with [`NSec3`] records. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct NSec3HashAlg { + /// The algorithm code. + pub code: u8, +} + +//--- Associated Constants + +impl NSec3HashAlg { + /// The SHA-1 algorithm. + pub const SHA1: Self = Self { code: 1 }; +} + +//--- Formatting + +impl fmt::Debug for NSec3HashAlg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + Self::SHA1 => "NSec3HashAlg::SHA1", + _ => return write!(f, "NSec3HashAlg({})", self.code), + }) + } +} + +//----------- NSec3Flags ----------------------------------------------------- + +/// Flags modifying the behaviour of an [`NSec3`] record. +#[derive( + Copy, + Clone, + Default, + Hash, + PartialEq, + Eq, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct NSec3Flags { + /// The raw flag bits. + inner: u8, +} + +//--- Interaction + +impl NSec3Flags { + /// Get the specified flag bit. + fn get_flag(&self, pos: u32) -> bool { + self.inner & (1 << pos) != 0 + } + + /// Set the specified flag bit. + fn set_flag(mut self, pos: u32, value: bool) -> Self { + self.inner &= !(1 << pos); + self.inner |= (value as u8) << pos; + self + } + + /// The raw flags bits. + pub fn bits(&self) -> u8 { + self.inner + } + + /// Whether unsigned delegations can exist in the covered range. + pub fn is_optout(&self) -> bool { + self.get_flag(0) + } + + /// Allow unsigned delegations to exist in the covered raneg. + pub fn set_optout(self, value: bool) -> Self { + self.set_flag(0, value) + } +} + +//--- Formatting + +impl fmt::Debug for NSec3Flags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NSec3Flags") + .field("optout", &self.is_optout()) + .field("bits", &self.bits()) + .finish() + } +} diff --git a/src/new/rdata/dnssec/rrsig.rs b/src/new/rdata/dnssec/rrsig.rs new file mode 100644 index 000000000..5138c65c5 --- /dev/null +++ b/src/new/rdata/dnssec/rrsig.rs @@ -0,0 +1,105 @@ +//! The RRSIG record data type. + +use core::cmp::Ordering; + +use domain_macros::*; + +use crate::new::base::build::BuildInMessage; +use crate::new::base::name::{CanonicalName, Name, NameCompressor}; +use crate::new::base::wire::{AsBytes, BuildBytes, TruncationError, U16}; +use crate::new::base::{CanonicalRecordData, RType, Serial, TTL}; + +use super::SecAlg; + +//----------- RRSig ---------------------------------------------------------- + +/// A cryptographic signature on a DNS record set. +#[derive(Clone, Debug, PartialEq, Eq, BuildBytes, ParseBytes)] +pub struct RRSig<'a> { + /// The type of the RRset being signed. + pub rtype: RType, + + /// The cryptographic algorithm used to construct the signature. + pub algorithm: SecAlg, + + /// The number of labels in the signed RRset's owner name. + pub labels: u8, + + /// The (original) TTL of the signed RRset. + pub ttl: TTL, + + /// The point in time when the signature expires. + pub expiration: Serial, + + /// The point in time when the signature was created. + pub inception: Serial, + + /// The key tag of the key used to make the signature. + pub keytag: U16, + + /// The name identifying the signer. + pub signer: &'a Name, + + /// The serialized cryptographic signature. + pub signature: &'a [u8], +} + +//--- Interaction + +impl RRSig<'_> { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> RRSig<'r> { + use crate::utils::dst::copy_to_bump; + + RRSig { + signer: copy_to_bump(self.signer, bump), + signature: bump.alloc_slice_copy(self.signature), + ..self.clone() + } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for RRSig<'_> { + fn cmp_canonical(&self, that: &Self) -> Ordering { + let this_initial = ( + self.rtype, + self.algorithm, + self.labels, + self.ttl, + self.expiration.as_bytes(), + self.inception.as_bytes(), + self.keytag, + ); + let that_initial = ( + that.rtype, + that.algorithm, + that.labels, + that.ttl, + that.expiration.as_bytes(), + that.inception.as_bytes(), + that.keytag, + ); + this_initial + .cmp(&that_initial) + .then_with(|| self.signer.cmp_lowercase_composed(that.signer)) + .then_with(|| self.signature.cmp(that.signature)) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for RRSig<'_> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let bytes = contents.get_mut(start..).ok_or(TruncationError)?; + let rest = self.build_bytes(bytes)?.len(); + Ok(contents.len() - rest) + } +} diff --git a/src/new/rdata/edns.rs b/src/new/rdata/edns.rs new file mode 100644 index 000000000..4218bbaf6 --- /dev/null +++ b/src/new/rdata/edns.rs @@ -0,0 +1,368 @@ +//! Record data types for EDNS (Extension Mechanism for DNS). +//! +//! See [RFC 6891](https://datatracker.ietf.org/doc/html/rfc6891). + +use core::cmp::Ordering; +use core::fmt; +use core::iter::FusedIterator; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytesZC, ParseError, SplitBytesZC, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::new::edns::{EdnsOption, UnparsedEdnsOption}; +use crate::utils::dst::UnsizedCopy; + +//----------- Opt ------------------------------------------------------------ + +/// EDNS options. +/// +/// An [`Opt`] record holds an unordered set of [`EdnsOption`]s, which provide +/// additional non-critical information about the containing DNS message. It +/// has fairly different semantics from other record data types, since it only +/// exists for communication between peers (it is not part of any zone, and it +/// is not cached). As such, it is often called a "pseudo-RR". +/// +/// A record containing [`Opt`] data is interpreted differently from records +/// containing normal data types (its class and TTL fields are different). +/// [`EdnsRecord`] provides this interpretation and offers way to convert to +/// and from normal [`Record`]s. +/// +/// [`EdnsRecord`]: crate::new::edns::EdnsRecord +/// [`Record`]: crate::new::base::Record +/// +/// [`Opt`] is specified by [RFC 6891, section 6]. For more information about +/// EDNS, see [`crate::new::edns`]. +/// +/// [RFC 6891, section 6]: https://datatracker.ietf.org/doc/html/rfc6891#section-6 +/// +/// ## Wire Format +/// +/// The wire format of an [`Opt`] record is the concatenation of zero or more +/// EDNS options. An EDNS option is serialized as a 16-bit big-endian code +/// (specifying the meaning of the option), a 16-bit big-endian size (the size +/// of the option data), and the variable-length option data. +/// +/// The memory layout of the [`Opt`] type is identical to its serialization in +/// the wire format. This means that it can be parsed from the wire format in +/// a zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Opt`] is a record data type, it is usually handled within an +/// enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// [`Opt`] is a _dynamically sized type_ (DST). It is not possible to +/// store an [`Opt`] in place (e.g. in a local variable); it must be held +/// indirectly, via a reference or a smart pointer type like [`Box`]. This +/// makes it more difficult to _create_ new [`Opt`]s; but once they are placed +/// somewhere, they can be used by reference (i.e. `&Opt`) exactly like any +/// other type. +/// +/// [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +/// +/// It is currently a bit difficult to build a new [`Opt`] from scratch. It +/// is easiest to build the wire format representation of the [`Opt`] manually +/// (by building a sequence of [`EdnsOption`]s) and then to parse it. +/// +/// ``` +/// # use domain::new::base::wire::{BuildBytes, ParseBytesZC, U16}; +/// # use domain::new::edns::{EdnsOption, OptionCode, UnknownOptionData}; +/// # use domain::new::rdata::Opt; +/// # +/// // Parse an 'Opt' from the DNS wire format: +/// let bytes = [0, 10, 0, 8, 248, 80, 41, 151, 244, 171, 53, 202, 0, 0, 0, 0]; +/// let from_bytes: &Opt = Opt::parse_bytes_by_ref(&bytes).unwrap(); +/// // It is also possible to use '<&Opt>::parse_bytes()'. +/// +/// let cookie = [248, 80, 41, 151, 244, 171, 53, 202].into(); +/// let options = [ +/// EdnsOption::ClientCookie(cookie), +/// EdnsOption::Unknown( +/// OptionCode { code: U16::new(0) }, +/// UnknownOptionData::parse_bytes_by_ref(&[]).unwrap(), +/// ), +/// ]; +/// +/// // Iterate over the options in an 'Opt': +/// for (l, r) in from_bytes.options().zip(&options) { +/// assert_eq!(l.as_ref(), Ok(r)); +/// } +/// +/// // Build the DNS wire format for an 'Opt' manually: +/// let mut buffer = vec![0u8; options.built_bytes_size()]; +/// options.build_bytes(&mut buffer).unwrap(); +/// assert_eq!(buffer, bytes); +/// +/// // Parse an 'Opt' from the wire format, but on the heap: +/// let buffer: Box<[u8]> = buffer.into_boxed_slice(); +/// let from_boxed_bytes: Box = Opt::parse_bytes_in(buffer).unwrap(); +/// assert_eq!(from_bytes, &*from_boxed_bytes); +/// ``` +/// +/// As a DST, [`Opt`] does not implement [`Copy`] or [`Clone`]. Instead, it +/// implements [`UnsizedCopy`]. An [`Opt`], held by reference, can be copied +/// into a different container (e.g. `Box`) using [`unsized_copy_into()`] +/// +/// [`unsized_copy_into()`]: UnsizedCopy::unsized_copy_into() +/// +/// For debugging, [`Opt`] can be formatted using [`fmt::Debug`]. +/// +/// To serialize a [`Opt`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Opt`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive(AsBytes, BuildBytes, UnsizedCopy)] +#[repr(transparent)] +pub struct Opt { + /// The raw serialized options. + contents: [u8], +} + +//--- Associated Constants + +impl Opt { + /// Empty OPT record data. + pub const EMPTY: &'static Self = + unsafe { core::mem::transmute(&[] as &[u8]) }; +} + +//--- Construction + +impl Opt { + /// Assume a byte sequence is a valid [`Opt`]. + /// + /// ## Safety + /// + /// The byte sequence must a valid instance of [`Opt`] in the wire format; + /// it must contain a sequence of [`EdnsOption`]s, concatenated together. + /// The contents of each [`EdnsOption`] need not be valid (i.e. they can + /// be incorrect with respect to the underlying option type). The byte + /// sequence must be at most 65,535 bytes long. + pub const unsafe fn from_bytes_unchecked(bytes: &[u8]) -> &Self { + // SAFETY: 'Opt' is 'repr(transparent)' to '[u8]'. + unsafe { core::mem::transmute::<&[u8], &Opt>(bytes) } + } +} + +//--- Inspection + +impl Opt { + /// Traverse the options in this record. + /// + /// Options that cannot be parsed are returned as [`UnparsedEdnsOption`]s. + pub fn options(&self) -> EdnsOptionsIter<'_> { + EdnsOptionsIter::new(&self.contents) + } +} + +//--- Equality + +impl PartialEq for Opt { + /// Compare two [`Opt`] records. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &Self) -> bool { + self.options().eq(other.options()) + } +} + +impl PartialEq<[EdnsOption<'_>]> for Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>]) -> bool { + self.options().eq(other.iter().map(|opt| Ok(opt.clone()))) + } +} + +impl PartialEq<[EdnsOption<'_>; N]> for Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>; N]) -> bool { + *self == *other.as_slice() + } +} + +impl PartialEq<[EdnsOption<'_>]> for &Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>]) -> bool { + **self == *other + } +} + +impl PartialEq<[EdnsOption<'_>; N]> for &Opt { + /// Compare an [`Opt`] to a sequence of [`EdnsOption`]s. + /// + /// This is primarily a debugging and testing aid; it will ensure that + /// both records have the same EDNS options in the same order, even though + /// order is semantically irrelevant. + fn eq(&self, other: &[EdnsOption<'_>; N]) -> bool { + **self == *other.as_slice() + } +} + +impl Eq for Opt {} + +//--- Canonical operations + +impl CanonicalRecordData for Opt { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.contents.cmp(&other.contents) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Opt { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.contents.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.contents); + Ok(end) + } +} + +//--- Parsing from bytes + +unsafe impl ParseBytesZC for Opt { + fn parse_bytes_by_ref(bytes: &[u8]) -> Result<&Self, ParseError> { + // Make sure the slice is 64KiB or less. + if bytes.len() > 65535 { + return Err(ParseError); + } + + let mut offset = 0usize; + while offset < bytes.len() { + // NOTE: We don't check the code here, since we won't validate the + // option by its actual type (even if we know how to). + offset += 2; + + let size = bytes.get(offset..offset + 2).ok_or(ParseError)?; + let size: usize = u16::from_be_bytes([size[0], size[1]]).into(); + offset += 2; + + // Make sure the entire data section exists. + let _ = bytes.get(offset..offset + size).ok_or(ParseError)?; + offset += size; + } + + // Now, 'offset == bytes.len()', and the whole slice is valid. + + // SAFETY: 'Opt' is 'repr(transparent)' to '[u8]'. + Ok(unsafe { core::mem::transmute::<&[u8], &Opt>(bytes) }) + } +} + +//--- Formatting + +impl fmt::Debug for Opt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Opt").field(&self.options()).finish() + } +} + +//--- Parsing record data + +impl<'a> ParseRecordData<'a> for &'a Opt {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Opt { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::OPT => Opt::parse_bytes_by_ref(bytes), + _ => Err(ParseError), + } + } +} + +//----------- EdnsOptionsIter ------------------------------------------------ + +/// An iterator over EDNS options in an [`Opt`] record. +#[derive(Clone)] +pub struct EdnsOptionsIter<'a> { + /// The serialized options to parse from. + options: &'a [u8], +} + +//--- Construction + +impl<'a> EdnsOptionsIter<'a> { + /// Construct a new [`EdnsOptionsIter`]. + pub const fn new(options: &'a [u8]) -> Self { + Self { options } + } +} + +//--- Inspection + +impl<'a> EdnsOptionsIter<'a> { + /// The serialized options yet to be parsed. + pub const fn remaining(&self) -> &'a [u8] { + self.options + } +} + +//--- Formatting + +impl fmt::Debug for EdnsOptionsIter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut entries = f.debug_set(); + for option in self.clone() { + match option { + Ok(option) => entries.entry(&option), + Err(_err) => entries.entry(&format_args!("")), + }; + } + entries.finish() + } +} + +//--- Iteration + +impl<'a> Iterator for EdnsOptionsIter<'a> { + type Item = Result, &'a UnparsedEdnsOption>; + + fn next(&mut self) -> Option { + if !self.options.is_empty() { + let (option, rest) = UnparsedEdnsOption::split_bytes_by_ref( + self.options, + ) + .expect("An 'Opt' always contains valid 'UnparsedEdnsOption's"); + self.options = rest; + Some(EdnsOption::try_from(option).map_err(|_| option)) + } else { + None + } + } +} + +impl FusedIterator for EdnsOptionsIter<'_> {} diff --git a/src/new/rdata/ipv6.rs b/src/new/rdata/ipv6.rs new file mode 100644 index 000000000..15fc42155 --- /dev/null +++ b/src/new/rdata/ipv6.rs @@ -0,0 +1,227 @@ +//! IPv6 record data types. +//! +//! See [RFC 3596](https://datatracker.ietf.org/doc/html/rfc3596). + +use core::cmp::Ordering; +use core::fmt; +use core::net::Ipv6Addr; +use core::str::FromStr; + +use crate::new::base::build::{ + BuildInMessage, NameCompressor, TruncationError, +}; +use crate::new::base::parse::ParseMessageBytes; +use crate::new::base::wire::{ + AsBytes, BuildBytes, ParseBytes, ParseBytesZC, ParseError, SplitBytes, + SplitBytesZC, +}; +use crate::new::base::{ + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; +use crate::utils::dst::UnsizedCopy; + +//----------- Aaaa ----------------------------------------------------------- + +/// The IPv6 address of a host responsible for this domain. +/// +/// A [`Aaaa`] record indicates that a domain name is backed by a server that +/// can be reached over the Internet at the specified IPv6 address. It does +/// not specify the server's capabilities (e.g. what protocols it supports); +/// those have to be communicated elsewhere. +/// +/// [`Aaaa`] is specified by [RFC 3596, section 2]. It works identically to +/// the [`A`] record. +/// +/// [`A`]: crate::new::rdata::A +/// [RFC 3596, section 2]: https://datatracker.ietf.org/doc/html/rfc3596#section-2 +/// +/// ## Wire Format +/// +/// The wire format of a [`Aaaa`] record is the 16 bytes of its IPv6 address, +/// in conventional order (from most to least significant). For example, +/// `2001::db8::` would be serialized as `20 01 0D B8 00 00 00 00 00 00 00 00 +/// 00 00 00 00`. +/// +/// The memory layout of the [`Aaaa`] type is identical to its serialization +/// in the wire format. This means it can be parsed from the wire format in a +/// zero-copy fashion, which is more efficient. +/// +/// ## Usage +/// +/// Because [`Aaaa`] is a record data type, it is usually handled within +/// an enum like [`RecordData`]. This section describes how to use it +/// independently (or when building new record data from scratch). +/// +/// [`RecordData`]: crate::new::rdata::RecordData +/// +/// There's a few ways to build an [`Aaaa`]: +/// +/// ``` +/// # use domain::new::base::wire::{ParseBytes, ParseBytesZC}; +/// # use domain::new::rdata::Aaaa; +/// # +/// use core::net::Ipv6Addr; +/// +/// // Build a 'Aaaa' from the raw bytes. +/// let from_raw = Aaaa { +/// octets: [0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], +/// }; +/// +/// // Convert an 'Ipv6Addr' into a 'Aaaa'. +/// let from_addr: Aaaa = Ipv6Addr::new(0x2001, 0x0DB8, 0, 0, 0, 0, 0, 0).into(); +/// # assert_eq!(from_raw, from_addr); +/// +/// // Parse a 'Aaaa' from a string. +/// let from_str: Aaaa = "2001:db8::".parse().unwrap(); +/// # assert_eq!(from_raw, from_str); +/// +/// // Parse a 'Aaaaa' from the DNS wire format. +/// let bytes = [0x20, 0x01, 0x0D, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; +/// let from_wire: Aaaa = Aaaa::parse_bytes(&bytes).unwrap(); +/// # assert_eq!(from_raw, from_wire); +/// +/// // ... even by reference (this is zero-copy). +/// let ref_from_wire: &Aaaa = Aaaa::parse_bytes_by_ref(&bytes).unwrap(); +/// // It is also possible to use '<&Aaaa>::parse_bytes()'. +/// # assert_eq!(from_raw, *ref_from_wire); +/// ``` +/// +/// Since [`Aaaa`] is a sized type, and it implements [`Copy`] and [`Clone`], +/// it's straightforward to handle and move around. +/// +/// For debugging and logging, [`Aaaa`] can be formatted using [`fmt::Debug`] +/// and [`fmt::Display`]. +/// +/// To serialize a [`Aaaa`] in the wire format, use [`BuildBytes`] (which +/// will serialize it to a given buffer) or [`AsBytes`] (which will +/// cast the [`Aaaa`] into a byte sequence in place). It also supports +/// [`BuildInMessage`]. +#[derive( + Copy, + Clone, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsBytes, + BuildBytes, + ParseBytes, + ParseBytesZC, + SplitBytes, + SplitBytesZC, + UnsizedCopy, +)] +#[repr(transparent)] +pub struct Aaaa { + /// The IPv6 address octets. + pub octets: [u8; 16], +} + +//--- Converting to and from 'Ipv6Addr' + +impl From for Aaaa { + fn from(value: Ipv6Addr) -> Self { + Self { + octets: value.octets(), + } + } +} + +impl From for Ipv6Addr { + fn from(value: Aaaa) -> Self { + Self::from(value.octets) + } +} + +//--- Canonical operations + +impl CanonicalRecordData for Aaaa { + fn cmp_canonical(&self, other: &Self) -> Ordering { + self.octets.cmp(&other.octets) + } +} + +//--- Parsing from a string + +impl FromStr for Aaaa { + type Err = ::Err; + + fn from_str(s: &str) -> Result { + Ipv6Addr::from_str(s).map(Aaaa::from) + } +} + +//--- Formatting + +impl fmt::Debug for Aaaa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Aaaa({self})") + } +} + +impl fmt::Display for Aaaa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Ipv6Addr::from(*self).fmt(f) + } +} + +//--- Parsing from DNS messages + +impl ParseMessageBytes<'_> for Aaaa { + fn parse_message_bytes( + contents: &'_ [u8], + start: usize, + ) -> Result { + contents + .get(start..) + .ok_or(ParseError) + .and_then(Self::parse_bytes) + } +} + +//--- Building into DNS messages + +impl BuildInMessage for Aaaa { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.octets.len(); + let bytes = contents.get_mut(start..end).ok_or(TruncationError)?; + bytes.copy_from_slice(&self.octets); + Ok(end) + } +} + +//--- Parsing record data + +impl ParseRecordData<'_> for Aaaa {} + +impl ParseRecordDataBytes<'_> for Aaaa { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::AAAA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} + +impl<'a> ParseRecordData<'a> for &'a Aaaa {} + +impl<'a> ParseRecordDataBytes<'a> for &'a Aaaa { + fn parse_record_data_bytes( + bytes: &'a [u8], + rtype: RType, + ) -> Result { + match rtype { + RType::AAAA => Self::parse_bytes(bytes), + _ => Err(ParseError), + } + } +} diff --git a/src/new/rdata/mod.rs b/src/new/rdata/mod.rs new file mode 100644 index 000000000..c9073e4f0 --- /dev/null +++ b/src/new/rdata/mod.rs @@ -0,0 +1,746 @@ +//! Record data types. +//! +//! ## Containers for record data +//! +//! When you need data for a particular record type, you can use the matching +//! concrete type for it. Otherwise, the record data can be held in one of +//! the following types: +//! +//! - [`RecordData`] is useful for short-term usage, e.g. when manipulating a +//! DNS message or parsing into a custom representation. It can be parsed +//! from the wire format very efficiently. +//! +#![cfg_attr(feature = "alloc", doc = " - [`BoxedRecordData`] ")] +#![cfg_attr(not(feature = "alloc"), doc = " - `BoxedRecordData` ")] +//! is useful for long-term storage. For long-term storage of a whole DNS +//! zone, it's more advisable to use the "zone tree" types provided by this +//! crate. +//! +//! - [`UnparsedRecordData`] is a niche type, useful for low-level +//! manipulation of the DNS wire format. Beware that it can contain +//! unresolved name compression pointers. +//! +//! - [`UnknownRecordData`] can be used to represent data types that aren't +//! supported yet. It functions similarly to [`UnparsedRecordData`], but +//! it can't be used for many "basic" record data types, like [`Soa`] and +//! [`Mx`]. These types come with many special cases that +//! [`UnknownRecordData`] doesn't try to account for. +//! +//! [`UnparsedRecordData`]: crate::new::base::UnparsedRecordData +//! +//! ## Supported data types +//! +//! The following record data types are supported. They are enumerated by +//! [`RecordData`], which can store any one of them at a time. +//! +//! Basic record types (RFC 1035): +//! - [`A`] +//! - [`Ns`] +//! - [`CName`] +//! - [`Soa`] +//! - [`Ptr`] +//! - [`HInfo`] +//! - [`Mx`] +//! - [`Txt`] +//! +//! IPv6 support (RFC 3596): +//! - [`Aaaa`] +//! +//! EDNS support (RFC 6891): +//! - [`Opt`] +//! +//! DNSSEC support (RFC 4034, RFC 5155): +//! - [`DNSKey`] +//! - [`RRSig`] +//! - [`NSec`] +//! - [`NSec3`] +//! - [`Ds`] + +#![deny(missing_docs)] +#![deny(clippy::missing_docs_in_private_items)] + +use core::cmp::Ordering; + +#[cfg(feature = "alloc")] +use core::fmt; + +#[cfg(feature = "alloc")] +use alloc::boxed::Box; + +use domain_macros::*; + +use crate::new::base::{ + build::{BuildInMessage, NameCompressor}, + name::CanonicalName, + parse::{ParseMessageBytes, SplitMessageBytes}, + wire::{ + AsBytes, BuildBytes, ParseBytes, ParseError, SplitBytes, + TruncationError, + }, + CanonicalRecordData, ParseRecordData, ParseRecordDataBytes, RType, +}; + +#[cfg(feature = "alloc")] +use crate::new::base::name::{Name, NameBuf}; + +//----------- Concrete record data types ------------------------------------- + +mod basic; +pub use basic::{CName, HInfo, Mx, Ns, Ptr, Soa, Txt, A}; + +mod ipv6; +pub use ipv6::Aaaa; + +mod edns; +pub use edns::{EdnsOptionsIter, Opt}; + +mod dnssec; +pub use dnssec::{ + DNSKey, DNSKeyFlags, DigestType, Ds, NSec, NSec3, NSec3Flags, + NSec3HashAlg, NSec3Param, RRSig, SecAlg, TypeBitmaps, +}; + +//----------- RecordData ----------------------------------------------------- + +/// A helper macro to handle boilerplate in defining [`RecordData`]. +macro_rules! define_record_data { + { + $(#[$attr:meta])* + $vis:vis enum $name:ident<$a:lifetime, $N:ident> { + $( + $(#[$v_attr:meta])* + $v_name:ident ($v_type:ty) = $v_disc:ident + ),*; + + $(#[$u_attr:meta])* + Unknown(RType, $u_type:ty) + } + } => { + // The primary type definition. + $(#[$attr])* + $vis enum $name<$a, $N> { + $($(#[$v_attr])* $v_name ($v_type),)* + $(#[$u_attr])* Unknown(RType, $u_type), + } + + //--- Inspection + + impl<$N> $name<'_, $N> { + /// The record data type. + pub const fn rtype(&self) -> RType { + match *self { + $(Self::$v_name(_) => RType::$v_disc,)* + Self::Unknown(rtype, _) => rtype, + } + } + } + + //--- Conversion from concrete types + + $(impl<$a, $N> From<$v_type> for $name<$a, $N> { + fn from(value: $v_type) -> Self { + Self::$v_name(value) + } + })* + + //--- Canonical operations + + impl<$N: CanonicalName> CanonicalRecordData for $name<'_, $N> { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + $(Self::$v_name(r) => r.build_canonical_bytes(bytes),)* + Self::Unknown(_, rd) => rd.build_canonical_bytes(bytes), + } + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + if self.rtype() != other.rtype() { + return self.rtype().cmp(&other.rtype()); + } + + match (self, other) { + $((Self::$v_name(l), Self::$v_name(r)) + => l.cmp_canonical(r),)* + (Self::Unknown(_, l), Self::Unknown(_, r)) + => l.cmp_canonical(r), + _ => unreachable!("'self' and 'other' had the same rtype but were different enum variants"), + } + } + } + + //--- Parsing record data + + impl<$a, $N: SplitBytes<$a>> ParseRecordDataBytes<$a> for $name<$a, $N> { + fn parse_record_data_bytes( + bytes: &$a [u8], + rtype: RType, + ) -> Result { + Ok(match rtype { + $(RType::$v_disc => Self::$v_name(ParseBytes::parse_bytes(bytes)?),)* + _ => Self::Unknown(rtype, ParseBytes::parse_bytes(bytes)?), + }) + } + } + + //--- Building record data + + impl<$N: BuildInMessage> BuildInMessage for $name<'_, $N> { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + name: &mut NameCompressor, + ) -> Result { + match *self { + $(Self::$v_name(ref r) => r.build_in_message(contents, start, name),)* + Self::Unknown(_, r) => r.build_in_message(contents, start, name), + } + } + } + + impl<$N: BuildBytes> BuildBytes for $name<'_, $N> { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + match self { + $(Self::$v_name(r) => r.build_bytes(bytes),)* + Self::Unknown(_, r) => r.build_bytes(bytes), + } + } + + fn built_bytes_size(&self) -> usize { + match self { + $(Self::$v_name(r) => r.built_bytes_size(),)* + Self::Unknown(_, r) => r.built_bytes_size(), + } + } + } + }; +} + +define_record_data! { + /// DNS record data. + #[derive(Clone, Debug, PartialEq, Eq)] + #[non_exhaustive] + pub enum RecordData<'a, N> { + /// The IPv4 address of a host responsible for this domain. + A(A) = A, + + /// The authoritative name server for this domain. + Ns(Ns) = NS, + + /// The canonical name for this domain. + CName(CName) = CNAME, + + /// The start of a zone of authority. + Soa(Soa) = SOA, + + /// A pointer to another domain name. + Ptr(Ptr) = PTR, + + /// Information about the host computer. + HInfo(HInfo<'a>) = HINFO, + + /// A host that can exchange mail for this domain. + Mx(Mx) = MX, + + /// Free-form text strings about this domain. + Txt(&'a Txt) = TXT, + + /// The IPv6 address of a host responsible for this domain. + Aaaa(Aaaa) = AAAA, + + /// Extended DNS options. + Opt(&'a Opt) = OPT, + + /// The signing key of a delegated zone. + Ds(&'a Ds) = DS, + + /// A cryptographic signature on a DNS record set. + RRSig(RRSig<'a>) = RRSIG, + + /// An indication of the non-existence of a set of DNS records (version 1). + NSec(NSec<'a>) = NSEC, + + /// A cryptographic key for DNS security. + DNSKey(&'a DNSKey) = DNSKEY, + + /// An indication of the non-existence of a set of DNS records (version 3). + NSec3(NSec3<'a>) = NSEC3, + + /// Parameters for computing [`NSec3`] records. + NSec3Param(&'a NSec3Param) = NSEC3PARAM; + + /// Data for an unknown DNS record type. + Unknown(RType, &'a UnknownRecordData) + } +} + +//--- Interaction + +impl<'a, N> RecordData<'a, N> { + /// Map the domain names within to another type. + pub fn map_names R>(self, f: F) -> RecordData<'a, R> { + match self { + Self::A(r) => RecordData::A(r), + Self::Ns(r) => RecordData::Ns(r.map_name(f)), + Self::CName(r) => RecordData::CName(r.map_name(f)), + Self::Soa(r) => RecordData::Soa(r.map_names(f)), + Self::Ptr(r) => RecordData::Ptr(r.map_name(f)), + Self::HInfo(r) => RecordData::HInfo(r), + Self::Mx(r) => RecordData::Mx(r.map_name(f)), + Self::Txt(r) => RecordData::Txt(r), + Self::Aaaa(r) => RecordData::Aaaa(r), + Self::Opt(r) => RecordData::Opt(r), + Self::Ds(r) => RecordData::Ds(r), + Self::RRSig(r) => RecordData::RRSig(r), + Self::NSec(r) => RecordData::NSec(r), + Self::DNSKey(r) => RecordData::DNSKey(r), + Self::NSec3(r) => RecordData::NSec3(r), + Self::NSec3Param(r) => RecordData::NSec3Param(r), + Self::Unknown(rt, rd) => RecordData::Unknown(rt, rd), + } + } + + /// Map references to the domain names within to another type. + pub fn map_names_by_ref<'r, R, F: FnMut(&'r N) -> R>( + &'r self, + f: F, + ) -> RecordData<'r, R> { + match self { + Self::A(r) => RecordData::A(*r), + Self::Ns(r) => RecordData::Ns(r.map_name_by_ref(f)), + Self::CName(r) => RecordData::CName(r.map_name_by_ref(f)), + Self::Soa(r) => RecordData::Soa(r.map_names_by_ref(f)), + Self::Ptr(r) => RecordData::Ptr(r.map_name_by_ref(f)), + Self::HInfo(r) => RecordData::HInfo(*r), + Self::Mx(r) => RecordData::Mx(r.map_name_by_ref(f)), + Self::Txt(r) => RecordData::Txt(r), + Self::Aaaa(r) => RecordData::Aaaa(*r), + Self::Opt(r) => RecordData::Opt(r), + Self::Ds(r) => RecordData::Ds(r), + Self::RRSig(r) => RecordData::RRSig(r.clone()), + Self::NSec(r) => RecordData::NSec(r.clone()), + Self::DNSKey(r) => RecordData::DNSKey(r), + Self::NSec3(r) => RecordData::NSec3(r.clone()), + Self::NSec3Param(r) => RecordData::NSec3Param(r), + Self::Unknown(rt, rd) => RecordData::Unknown(*rt, rd), + } + } + + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + pub fn clone_to_bump<'r>( + &self, + bump: &'r bumpalo::Bump, + ) -> RecordData<'r, N> + where + N: Clone, + { + use crate::utils::dst::copy_to_bump; + + match self { + Self::A(r) => RecordData::A(*r), + Self::Ns(r) => RecordData::Ns(r.clone()), + Self::CName(r) => RecordData::CName(r.clone()), + Self::Soa(r) => RecordData::Soa(r.clone()), + Self::Ptr(r) => RecordData::Ptr(r.clone()), + Self::HInfo(r) => RecordData::HInfo(r.clone_to_bump(bump)), + Self::Mx(r) => RecordData::Mx(r.clone()), + Self::Txt(r) => RecordData::Txt(copy_to_bump(*r, bump)), + Self::Aaaa(r) => RecordData::Aaaa(*r), + Self::Opt(r) => RecordData::Opt(copy_to_bump(*r, bump)), + Self::Ds(r) => RecordData::Ds(copy_to_bump(*r, bump)), + Self::RRSig(r) => RecordData::RRSig(r.clone_to_bump(bump)), + Self::NSec(r) => RecordData::NSec(r.clone_to_bump(bump)), + Self::DNSKey(r) => RecordData::DNSKey(copy_to_bump(*r, bump)), + Self::NSec3(r) => RecordData::NSec3(r.clone_to_bump(bump)), + Self::NSec3Param(r) => { + RecordData::NSec3Param(copy_to_bump(*r, bump)) + } + Self::Unknown(rt, rd) => { + RecordData::Unknown(*rt, rd.clone_to_bump(bump)) + } + } + } +} + +//--- Parsing record data + +impl<'a, N: SplitMessageBytes<'a>> ParseRecordData<'a> for RecordData<'a, N> { + fn parse_record_data( + contents: &'a [u8], + start: usize, + rtype: RType, + ) -> Result { + match rtype { + RType::A => A::parse_bytes(&contents[start..]).map(Self::A), + RType::NS => { + Ns::parse_message_bytes(contents, start).map(Self::Ns) + } + RType::CNAME => { + CName::parse_message_bytes(contents, start).map(Self::CName) + } + RType::SOA => { + Soa::parse_message_bytes(contents, start).map(Self::Soa) + } + RType::PTR => { + Ptr::parse_message_bytes(contents, start).map(Self::Ptr) + } + RType::HINFO => { + HInfo::parse_bytes(&contents[start..]).map(Self::HInfo) + } + RType::MX => { + Mx::parse_message_bytes(contents, start).map(Self::Mx) + } + RType::TXT => { + <&Txt>::parse_bytes(&contents[start..]).map(Self::Txt) + } + RType::AAAA => { + Aaaa::parse_bytes(&contents[start..]).map(Self::Aaaa) + } + RType::OPT => { + <&Opt>::parse_bytes(&contents[start..]).map(Self::Opt) + } + RType::DS => <&Ds>::parse_bytes(&contents[start..]).map(Self::Ds), + RType::RRSIG => { + RRSig::parse_bytes(&contents[start..]).map(Self::RRSig) + } + RType::NSEC => { + NSec::parse_bytes(&contents[start..]).map(Self::NSec) + } + RType::DNSKEY => { + <&DNSKey>::parse_bytes(&contents[start..]).map(Self::DNSKey) + } + RType::NSEC3 => { + NSec3::parse_bytes(&contents[start..]).map(Self::NSec3) + } + RType::NSEC3PARAM => { + <&NSec3Param>::parse_bytes(&contents[start..]) + .map(Self::NSec3Param) + } + _ => <&UnknownRecordData>::parse_bytes(&contents[start..]) + .map(|data| Self::Unknown(rtype, data)), + } + } +} + +//----------- BoxedRecordData ------------------------------------------------ + +/// A heap-allocated container for [`RecordData`]. +/// +/// This is an efficient heap-allocated container for DNS record data. While +/// it does not directly provide much functionality, it has getters to access +/// the [`RecordData`] within. +/// +/// ## Performance +/// +/// On 64-bit machines, [`BoxedRecordData`] has a size of 16 bytes. This is +/// significantly better than [`RecordData`], which is usually 64 bytes in +/// size. Since [`BoxedRecordData`] is intended for long-term storage and +/// use, it trades off ergonomics for lower memory usage. +#[cfg(feature = "alloc")] +pub struct BoxedRecordData { + /// A pointer to the record data. + /// + /// This is the raw pointer backing a `Box<[u8]>` (its size is stored in + /// the `size` field). It is owned by this type. + data: *mut u8, + + /// The record data type. + /// + /// The stored bytes represent a valid instance of this record data type, + /// at least for all known record data types. + rtype: RType, + + /// The size of the record data. + size: u16, +} + +//--- Inspection + +#[cfg(feature = "alloc")] +impl BoxedRecordData { + /// The record data type. + pub const fn rtype(&self) -> RType { + self.rtype + } + + /// The wire format of the record data. + pub const fn bytes(&self) -> &[u8] { + // SAFETY: + // + // As documented on 'BoxedRecordData', 'data' and 'size' form the + // pointer and length of a 'Box<[u8]>'. This pointer is identical to + // the pointer returned by 'Box::deref()', so we use it directly. + // + // The lifetime of the returned slice is within the lifetime of 'self' + // which is a shared borrow of the 'BoxedRecordData'. As such, the + // underlying 'Box<[u8]>' outlives the returned slice. + unsafe { core::slice::from_raw_parts(self.data, self.size as usize) } + } + + /// Access the [`RecordData`] within. + pub fn get(&self) -> RecordData<'_, &'_ Name> { + let (rtype, bytes) = (self.rtype, self.bytes()); + // SAFETY: As documented on 'BoxedRecordData', the referenced bytes + // are known to be a valid instance of the record data type (for all + // known record data types). As such, this function will succeed. + unsafe { + RecordData::parse_record_data_bytes(bytes, rtype) + .unwrap_unchecked() + } + } +} + +//--- Formatting + +#[cfg(feature = "alloc")] +impl fmt::Debug for BoxedRecordData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This should concatenate to form 'BoxedRecordData'. + f.write_str("Boxed")?; + self.get().fmt(f) + } +} + +//--- Equality + +#[cfg(feature = "alloc")] +impl PartialEq for BoxedRecordData { + fn eq(&self, other: &Self) -> bool { + self.get().eq(&other.get()) + } +} + +#[cfg(feature = "alloc")] +impl Eq for BoxedRecordData {} + +//--- Clone + +#[cfg(feature = "alloc")] +impl Clone for BoxedRecordData { + fn clone(&self) -> Self { + let bytes: Box<[u8]> = self.bytes().into(); + let data = Box::into_raw(bytes).cast::(); + let (rtype, size) = (self.rtype, self.size); + Self { data, rtype, size } + } +} + +//--- Drop + +#[cfg(feature = "alloc")] +impl Drop for BoxedRecordData { + fn drop(&mut self) { + // Reconstruct the 'Box' and drop it. + let slice = core::ptr::slice_from_raw_parts_mut( + self.data, + self.size as usize, + ); + + // SAFETY: As documented on 'BoxedRecordData', 'data' and 'size' form + // the pointer and length of a 'Box<[u8]>'. Reconstructing the 'Box' + // moves out of 'self', but this is sound because 'self' is dropped. + let _ = unsafe { Box::from_raw(slice) }; + } +} + +//--- Send and Sync + +// SAFETY: 'BoxedRecordData' is equivalent to '(RType, Box<[u8]>)' with a +// custom representation. It cannot cause data races. +#[cfg(feature = "alloc")] +unsafe impl Send for BoxedRecordData {} + +// SAFETY: 'BoxedRecordData' is equivalent to '(RType, Box<[u8]>)' with a +// custom representation. It cannot cause data races. +#[cfg(feature = "alloc")] +unsafe impl Sync for BoxedRecordData {} + +//--- Conversion from 'RecordData' + +#[cfg(feature = "alloc")] +impl From> for BoxedRecordData { + /// Build a [`RecordData`] into a heap allocation. + /// + /// # Panics + /// + /// Panics if the [`RecordData`] does not fit in a 64KiB buffer, or if the + /// serialized bytes cannot be parsed back into `RecordData<'_, &Name>`. + fn from(value: RecordData<'_, N>) -> Self { + // TODO: Determine the size of the record data upfront, and only + // allocate that much. Maybe as a new method on 'BuildBytes'... + let mut buffer = vec![0u8; 65535]; + let rest_len = value + .build_bytes(&mut buffer) + .expect("A 'RecordData' could not be built into a 64KiB buffer") + .len(); + let len = buffer.len() - rest_len; + buffer.truncate(len); + let buffer: Box<[u8]> = buffer.into_boxed_slice(); + + // Verify that the built bytes can be parsed correctly. + let _rdata: RecordData<'_, &Name> = + RecordData::parse_record_data_bytes(&buffer, value.rtype()) + .expect("A serialized 'RecordData' could not be parsed back"); + + // Construct the internal representation. + let size = buffer.len() as u16; + let data = Box::into_raw(buffer).cast::(); + let rtype = value.rtype(); + Self { data, rtype, size } + } +} + +// TODO: Convert from 'Box', 'Box', etc. + +//--- Canonical operations + +#[cfg(feature = "alloc")] +impl CanonicalRecordData for BoxedRecordData { + fn build_canonical_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + if self.rtype.uses_lowercase_canonical_form() { + // Forward to the semantically correct operation. + self.get().build_canonical_bytes(bytes) + } else { + // The canonical format is the same as the wire format. + self.bytes().build_bytes(bytes) + } + } + + fn cmp_canonical(&self, other: &Self) -> Ordering { + // Compare record data types. + if self.rtype != other.rtype { + return self.rtype.cmp(&other.rtype); + } + + if self.rtype.uses_lowercase_canonical_form() { + // Forward to the semantically correct operation. + self.get().cmp_canonical(&other.get()) + } else { + // Compare raw byte sequences. + self.bytes().cmp(other.bytes()) + } + } +} + +//--- Parsing record data + +#[cfg(feature = "alloc")] +impl ParseRecordData<'_> for BoxedRecordData { + fn parse_record_data( + contents: &'_ [u8], + start: usize, + rtype: RType, + ) -> Result { + RecordData::<'_, NameBuf>::parse_record_data(contents, start, rtype) + .map(BoxedRecordData::from) + } +} + +#[cfg(feature = "alloc")] +impl ParseRecordDataBytes<'_> for BoxedRecordData { + fn parse_record_data_bytes( + bytes: &'_ [u8], + rtype: RType, + ) -> Result { + // Ensure the bytes form valid 'RecordData'. + let _rdata: RecordData<'_, &Name> = + RecordData::parse_record_data_bytes(bytes, rtype)?; + + // Ensure the data size is valid. + let size = u16::try_from(bytes.len()).map_err(|_| ParseError)?; + + // Construct the 'BoxedRecordData' manually. + let bytes: Box<[u8]> = bytes.into(); + let data = Box::into_raw(bytes).cast::(); + Ok(Self { data, rtype, size }) + } +} + +//--- Building record data + +// TODO: 'impl BuildInMessage for BoxedRecordData' will require implementing +// 'impl BuildInMessage for Name', which is difficult because it is hard on +// name compression. + +#[cfg(feature = "alloc")] +impl BuildBytes for BoxedRecordData { + fn build_bytes<'b>( + &self, + bytes: &'b mut [u8], + ) -> Result<&'b mut [u8], TruncationError> { + self.bytes().build_bytes(bytes) + } + + fn built_bytes_size(&self) -> usize { + self.bytes().len() + } +} + +//----------- UnknownRecordData ---------------------------------------------- + +/// Data for an unknown DNS record type. +/// +/// This is a fallback type, used for record types not known to the current +/// implementation. It must not be used for well-known record types, because +/// some of them have special rules that this type does not follow. +#[derive( + Debug, PartialEq, Eq, AsBytes, BuildBytes, ParseBytesZC, UnsizedCopy, +)] +#[repr(transparent)] +pub struct UnknownRecordData { + /// The unparsed option data. + pub octets: [u8], +} + +//--- Interaction + +impl UnknownRecordData { + /// Copy referenced data into the given [`Bump`](bumpalo::Bump) allocator. + #[cfg(feature = "bumpalo")] + #[allow(clippy::mut_from_ref)] // using a memory allocator + pub fn clone_to_bump<'r>(&self, bump: &'r bumpalo::Bump) -> &'r mut Self { + use crate::new::base::wire::{AsBytes, ParseBytesZC}; + + let bytes = bump.alloc_slice_copy(self.as_bytes()); + // SAFETY: 'ParseBytesZC' and 'AsBytes' are inverses. + unsafe { Self::parse_bytes_in(bytes).unwrap_unchecked() } + } +} + +//--- Canonical operations + +impl CanonicalRecordData for UnknownRecordData { + fn cmp_canonical(&self, other: &Self) -> Ordering { + // Since this is not a well-known record data type, embedded domain + // names do not need to be lowercased. + self.octets.cmp(&other.octets) + } +} + +//--- Building in DNS messages + +impl BuildInMessage for UnknownRecordData { + fn build_in_message( + &self, + contents: &mut [u8], + start: usize, + _compressor: &mut NameCompressor, + ) -> Result { + let end = start + self.octets.len(); + contents + .get_mut(start..end) + .ok_or(TruncationError)? + .copy_from_slice(&self.octets); + Ok(end) + } +} diff --git a/src/resolv/stub/mod.rs b/src/resolv/stub/mod.rs index a1f46aabb..a41484517 100644 --- a/src/resolv/stub/mod.rs +++ b/src/resolv/stub/mod.rs @@ -410,20 +410,19 @@ impl<'a> Query<'a> { let msg = Message::from_octets(message.as_target().to_vec()) .expect("Message::from_octets should not fail"); - let request_msg = RequestMessage::new(msg).map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + let request_msg = RequestMessage::new(msg) + .map_err(|e| io::Error::other(e.to_string()))?; - let transport = self.resolver.get_transport().await.map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + let transport = self + .resolver + .get_transport() + .await + .map_err(|e| io::Error::other(e.to_string()))?; let mut gr_fut = transport.send_request(request_msg); let reply = timeout(self.resolver.options.timeout, gr_fut.get_response()) .await? - .map_err(|e| { - io::Error::new(io::ErrorKind::Other, e.to_string()) - })?; + .map_err(|e| io::Error::other(e.to_string()))?; Ok(Answer { message: reply }) } diff --git a/src/stelline/matches.rs b/src/stelline/matches.rs index 48fa36ff9..5015ae19b 100644 --- a/src/stelline/matches.rs +++ b/src/stelline/matches.rs @@ -50,7 +50,7 @@ impl Entry { msg: &Message, ) -> Result<(), DidNotMatch> { self.match_flags(msg)?; - self.match_ends_data(msg)?; + self.match_edns_data(msg)?; self.match_opcode(msg)?; self.match_question(msg)?; self.match_rcode(msg)?; @@ -80,7 +80,7 @@ impl Entry { } /// Match the ENDS data in the OPT record - fn match_ends_data( + fn match_edns_data( &self, msg: &Message, ) -> Result<(), DidNotMatch> { @@ -419,7 +419,7 @@ impl OrderedMultiMatcher<'_> { let e = &self.entry; e.match_flags(msg)?; - e.match_ends_data(msg)?; + e.match_edns_data(msg)?; e.match_opcode(msg)?; e.match_question(msg)?; e.match_rcode(msg)?; @@ -466,7 +466,7 @@ impl UnorderedMultiMatcher<'_> { let e = &self.entry; e.match_flags(msg)?; - e.match_ends_data(msg)?; + e.match_edns_data(msg)?; e.match_opcode(msg)?; e.match_question(msg)?; e.match_rcode(msg)?; diff --git a/src/utils/dst.rs b/src/utils/dst.rs new file mode 100644 index 000000000..cc47a581a --- /dev/null +++ b/src/utils/dst.rs @@ -0,0 +1,447 @@ +//! Working with dynamically sized types (DSTs). +//! +//! DSTs are types whose size is known at run-time instead of compile-time. +//! The primary examples of this are slices and [`str`]. While Rust provides +//! relatively good support for DSTs (e.g. they can be held by reference like +//! any other type), it has some rough edges. The standard library tries to +//! paper over these with helpful functions and trait impls, but it does not +//! account for custom DST types. In particular, [`new::base`] introduces a +//! large number of user-facing DSTs and needs to paper over the same rough +//! edges for all of them. +//! +//! [`new::base`]: crate::new::base +//! +//! ## Coping DSTs +//! +//! Because DSTs cannot be held by value, they must be handled and manipulated +//! through an indirection (a reference or a smart pointer of some kind). +//! Copying a DST into new container (e.g. [`Box`]) requires explicit support +//! from that container type. +//! +//! [`Box`]: https://doc.rust-lang.org/std/boxed/struct.Box.html +//! +//! This module introduces the [`UnsizedCopy`] trait (and a derive macro) that +//! types like [`str`] implement. Container types that can support copying +//! DSTs implement [`UnsizedCopyFrom`]. +// +// TODO: Example + +//----------- UnsizedCopy ---------------------------------------------------- + +/// An extension of [`Copy`] to dynamically sized types. +/// +/// This is a generalization of [`Copy`]. It is intended to simplify working +/// with DSTs that support zero-copy parsing techniques (as these are built +/// from byte sequences, they are inherently trivial to copy). +/// +/// # Usage +/// +/// To copy a type, call [`UnsizedCopy::unsized_copy_into()`] on the DST being +/// copied, or call [`UnsizedCopyFrom::unsized_copy_from()`] on the container +/// type to copy into. The two function identically. +/// +#[cfg_attr( + feature = "bumpalo", + doc = "The [`copy_to_bump()`] function is useful for copying data into [`bumpalo`]-based allocations." +)] +/// +/// # Safety +/// +/// A type `T` can implement `UnsizedCopy` if all of the following hold: +/// +/// - It is an aggregate type (`struct`, `enum`, or `union`) and every field +/// implements [`UnsizedCopy`]. +/// +/// - `T::Alignment` has exactly the same alignment as `T`. +/// +/// - `T::ptr_with_addr()` satisfies the documented invariants. +pub unsafe trait UnsizedCopy { + /// Copy `self` into a new container. + /// + /// A new container of the specified type (which is usually inferred) is + /// allocated, and the contents of `self` are copied into it. This is a + /// convenience method that calls [`unsized_copy_from()`]. + /// + /// [`unsized_copy_from()`]: UnsizedCopyFrom::unsized_copy_from(). + #[inline] + fn unsized_copy_into>(&self) -> T { + T::unsized_copy_from(self) + } + + /// Copy `self` and return it by value. + /// + /// This offers equivalent functionality to the regular [`Copy`] trait, + /// which is also why it has the same [`Sized`] bound. + #[inline] + fn copy(&self) -> Self + where + Self: Sized, + { + // The compiler can't tell that 'Self' is 'Copy', so we're just going + // to copy it manually. Hopefully this optimizes fine. + + // SAFETY: 'self' is a valid reference, and is thus safe for reads. + unsafe { core::ptr::read(self) } + } + + /// A type with the same alignment as `Self`. + /// + /// At the moment, Rust does not provide a way to determine the alignment + /// of a dynamically sized type at compile-time. This restriction exists + /// because trait objects (which count as DSTs, but are not supported by + /// [`UnsizedCopy`]) have an alignment determined by their implementation + /// (which can vary at runtime). + /// + /// This associated type papers over this limitation, by simply requiring + /// every implementation of [`UnsizedCopy`] to specify a type with the + /// same alignment here. This is used by internal plumbing code to know + /// the alignment of `Self` at compile-time. + /// + /// ## Invariants + /// + /// The alignment of `Self::Alignment` must be the same as that of `Self`. + type Alignment: Sized; + + /// Change the address of a pointer to `Self`. + /// + /// `Self` may be a DST, which means that references (and pointers) to it + /// store metadata alongside the usual memory address. For example, the + /// metadata for a slice type is its length. In order to construct a new + /// instance of `Self` (as is done by copying), a new pointer must be + /// created, and the appropriate metadata must be inserted. + /// + /// At the moment, Rust does not provide a way to examine this metadata + /// for an arbitrary type. This method papers over this limitation, and + /// provides a way to copy the metadata from an existing pointer while + /// changing the pointer address. + /// + /// # Implementing + /// + /// Most users will derive [`UnsizedCopy`] and so don't need to worry + /// about this. In any case, when Rust builds in support for extracting + /// metadata, this function will gain a default implementation, and will + /// eventually be deprecated. + /// + /// For manual implementations for unsized types: + /// + /// ```no_run + /// # use domain::utils::dst::UnsizedCopy; + /// # + /// pub struct Foo { + /// a: i32, + /// b: [u8], + /// } + /// + /// unsafe impl UnsizedCopy for Foo { + /// // We would like to write 'Alignment = Self' here, but we can't + /// // because 'Self' is not 'Sized'. However, 'Self' is a 'struct' + /// // using 'repr(Rust)'; the following tuple (which implicitly also + /// // uses 'repr(Rust)') has the same alignment as it. + /// type Alignment = (i32, u8); + /// + /// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + /// // Delegate to the same function on the last field. + /// // + /// // Rust knows that 'Self' has the same metadata as '[u8]', + /// // and so permits casting pointers between those types. + /// self.b.ptr_with_addr(addr) as *const Self + /// } + /// } + /// ``` + /// + /// For manual implementations for sized types: + /// + /// ```no_run + /// # use domain::utils::dst::UnsizedCopy; + /// # + /// pub struct Foo { + /// a: i32, + /// b: Option, + /// } + /// + /// unsafe impl UnsizedCopy for Foo { + /// // Because 'Foo' is a sized type, we can use it here directly. + /// type Alignment = Self; + /// + /// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + /// // Since 'Self' is 'Sized', there is no metadata. + /// addr.cast::() + /// } + /// } + /// ``` + /// + /// # Invariants + /// + /// For the statement `let result = Self::ptr_with_addr(ptr, addr);`, the + /// following always hold: + /// + /// - `result as usize == addr as usize`. + /// - `core::ptr::metadata(result) == core::ptr::metadata(ptr)`. + /// + /// It is undefined behaviour for an implementation of [`UnsizedCopy`] to + /// break these invariants. + fn ptr_with_addr(&self, addr: *const ()) -> *const Self; +} + +/// Deriving [`UnsizedCopy`] automatically. +/// +/// [`UnsizedCopy`] can be derived on any aggregate type. `enum`s and +/// `union`s are inherently [`Sized`] types, and [`UnsizedCopy`] will simply +/// require every field to implement [`Copy`] on them. For `struct`s, all but +/// the last field need to implement [`Copy`]; the last field needs to +/// implement [`UnsizedCopy`]. +/// +/// Here's a simple example: +/// +/// ```no_run +/// # use domain::utils::dst::UnsizedCopy; +/// struct Foo { +/// a: u32, +/// b: Bar, +/// } +/// +/// # struct Bar { data: T } +/// +/// // The generated impl with 'derive(UnsizedCopy)': +/// unsafe impl UnsizedCopy for Foo +/// where +/// u32: Copy, +/// Bar: UnsizedCopy, +/// { +/// // This type has the same alignment as 'Foo'. +/// type Alignment = (u32, as UnsizedCopy>::Alignment); +/// +/// fn ptr_with_addr(&self, addr: *const ()) -> *const Self { +/// self.b.ptr_with_addr(addr) as *const Self +/// } +/// } +/// ``` +pub use domain_macros::UnsizedCopy; + +macro_rules! impl_primitive_unsized_copy { + ($($type:ty),+) => { + $(unsafe impl UnsizedCopy for $type { + type Alignment = Self; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } + })+ + }; +} + +impl_primitive_unsized_copy!((), bool, char); +impl_primitive_unsized_copy!(u8, u16, u32, u64, u128, usize); +impl_primitive_unsized_copy!(i8, i16, i32, i64, i128, isize); +impl_primitive_unsized_copy!(f32, f64); + +unsafe impl UnsizedCopy for &T { + type Alignment = Self; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } +} + +unsafe impl UnsizedCopy for str { + // 'str' has no alignment. + type Alignment = u8; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + // NOTE: The Rust Reference indicates that 'str' has the same layout + // as '[u8]' [1]. This is also the most natural layout for it. Since + // there's no way to construct a '*const str' from raw parts, we will + // just construct a raw slice and transmute it. + // + // [1]: https://doc.rust-lang.org/reference/type-layout.html#str-layout + + self.as_bytes().ptr_with_addr(addr) as *const Self + } +} + +unsafe impl UnsizedCopy for [T] { + type Alignment = T; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + core::ptr::slice_from_raw_parts(addr.cast::(), self.len()) + } +} + +unsafe impl UnsizedCopy for [T; N] { + type Alignment = T; + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + addr.cast::() + } +} + +macro_rules! impl_unsized_copy_tuple { + ($($type:ident),*; $last:ident) => { + unsafe impl<$($type: Copy,)* $last: ?Sized + UnsizedCopy> + UnsizedCopy for ($($type,)* $last,) { + type Alignment = ($($type,)* <$last>::Alignment,); + + fn ptr_with_addr(&self, addr: *const ()) -> *const Self { + let (.., last) = self; + last.ptr_with_addr(addr) as *const Self + } + } + }; +} + +impl_unsized_copy_tuple!(; A); +impl_unsized_copy_tuple!(A; B); +impl_unsized_copy_tuple!(A, B; C); +impl_unsized_copy_tuple!(A, B, C; D); +impl_unsized_copy_tuple!(A, B, C, D; E); +impl_unsized_copy_tuple!(A, B, C, D, E; F); +impl_unsized_copy_tuple!(A, B, C, D, E, F; G); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G; H); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H; I); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I; J); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I, J; K); +impl_unsized_copy_tuple!(A, B, C, D, E, F, G, H, I, J, K; L); + +//----------- UnsizedCopyFrom ------------------------------------------------ + +/// A container type that can be copied into. +pub trait UnsizedCopyFrom: Sized { + /// The source type to copy from. + type Source: ?Sized + UnsizedCopy; + + /// Create a new `Self` by copying the given value. + fn unsized_copy_from(value: &Self::Source) -> Self; +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::boxed::Box { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use std::alloc; + + let layout = alloc::Layout::for_value(value); + let ptr = unsafe { alloc::alloc(layout) }; + if ptr.is_null() { + alloc::handle_alloc_error(layout); + } + let src = value as *const _ as *const u8; + unsafe { core::ptr::copy_nonoverlapping(src, ptr, layout.size()) }; + let ptr = value.ptr_with_addr(ptr.cast()).cast_mut(); + unsafe { std::boxed::Box::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::rc::Rc { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use core::mem::MaybeUninit; + + /// A [`u8`] with a custom alignment. + #[derive(Copy, Clone)] + #[repr(C)] + struct AlignedU8([T; 0], u8); + + // TODO(1.82): Use 'Rc::new_uninit_slice()'. + // 'impl FromIterator for Rc' describes performance characteristics. + // For efficiency, the iterator should implement 'TrustedLen', which + // is (currently) a nightly-only trait. However, we can use the + // existing 'std' types which happen to implement it. + let size = core::mem::size_of_val(value); + let rc: std::rc::Rc<[MaybeUninit>]> = + (0..size).map(|_| MaybeUninit::uninit()).collect(); + + let src = value as *const _ as *const u8; + let dst = std::rc::Rc::into_raw(rc).cast_mut(); + // SAFETY: 'rc' was just constructed and has never been copied. Thus, + // its contents can be mutated without violating any references. + unsafe { core::ptr::copy_nonoverlapping(src, dst.cast(), size) }; + + let ptr = value.ptr_with_addr(dst.cast()); + unsafe { std::rc::Rc::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::sync::Arc { + type Source = T; + + fn unsized_copy_from(value: &Self::Source) -> Self { + use core::mem::MaybeUninit; + + /// A [`u8`] with a custom alignment. + #[derive(Copy, Clone)] + #[repr(C)] + struct AlignedU8([T; 0], u8); + + // TODO(1.82): Use 'Arc::new_uninit_slice()'. + // 'impl FromIterator for Arc' describes performance characteristics. + // For efficiency, the iterator should implement 'TrustedLen', which + // is (currently) a nightly-only trait. However, we can use the + // existing 'std' types which happen to implement it. + let size = core::mem::size_of_val(value); + let arc: std::sync::Arc<[MaybeUninit>]> = + (0..size).map(|_| MaybeUninit::uninit()).collect(); + + let src = value as *const _ as *const u8; + let dst = std::sync::Arc::into_raw(arc).cast_mut(); + // SAFETY: 'arc' was just constructed and has never been copied. Thus, + // its contents can be mutated without violating any references. + unsafe { core::ptr::copy_nonoverlapping(src, dst.cast(), size) }; + + let ptr = value.ptr_with_addr(dst.cast()); + unsafe { std::sync::Arc::from_raw(ptr) } + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::vec::Vec { + type Source = [T]; + + fn unsized_copy_from(value: &Self::Source) -> Self { + // We can't use 'impl From<&[T]> for Vec', because that requires + // 'T' to implement 'Clone'. We could reuse the 'UnsizedCopyFrom' + // impl for 'Box', but a manual implementation is probably better. + + let mut this = Self::with_capacity(value.len()); + let src = value.as_ptr(); + let dst = this.spare_capacity_mut() as *mut _ as *mut T; + unsafe { core::ptr::copy_nonoverlapping(src, dst, value.len()) }; + // SAFETY: The first 'value.len()' elements are now initialized. + unsafe { this.set_len(value.len()) }; + this + } +} + +#[cfg(feature = "std")] +impl UnsizedCopyFrom for std::string::String { + type Source = str; + + fn unsized_copy_from(value: &Self::Source) -> Self { + value.into() + } +} + +//----------- copy_to_bump --------------------------------------------------- + +/// Copy a value into a [`Bump`] allocator. +/// +/// This works with [`UnsizedCopy`] values, which extends [`Bump`]'s native +/// functionality. +/// +/// [`Bump`]: bumpalo::Bump +#[cfg(feature = "bumpalo")] +#[allow(clippy::mut_from_ref)] // using a memory allocator +pub fn copy_to_bump<'a, T: ?Sized + UnsizedCopy>( + value: &T, + bump: &'a bumpalo::Bump, +) -> &'a mut T { + let layout = std::alloc::Layout::for_value(value); + let ptr = bump.alloc_layout(layout).as_ptr(); + let src = value as *const _ as *const u8; + unsafe { core::ptr::copy_nonoverlapping(src, ptr, layout.size()) }; + let ptr = value.ptr_with_addr(ptr.cast()).cast_mut(); + unsafe { &mut *ptr } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 584724715..a9ae063c2 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,5 +4,7 @@ pub mod base16; pub mod base32; pub mod base64; +pub mod dst; + #[cfg(feature = "net")] pub(crate) mod config; diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index d65747eb9..77021a9f5 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -77,6 +77,9 @@ pub struct Zonefile { /// The last TTL. last_ttl: Ttl, + /// The $TTL. + dollar_ttl: Option, + /// The last class. last_class: Option, @@ -111,6 +114,7 @@ impl Zonefile { origin: None, last_owner: None, last_ttl: Ttl::from_secs(3600), + dollar_ttl: None, last_class: None, require_valid: true, } @@ -212,7 +216,7 @@ impl Zonefile { match EntryScanner::new(self)?.scan_entry()? { ScannedEntry::Entry(entry) => return Ok(Some(entry)), ScannedEntry::Origin(origin) => self.origin = Some(origin), - ScannedEntry::Ttl(ttl) => self.last_ttl = ttl, + ScannedEntry::Ttl(ttl) => self.dollar_ttl = Some(ttl), ScannedEntry::Empty => {} ScannedEntry::Eof => return Ok(None), } @@ -265,6 +269,8 @@ pub enum Entry { /// This includes all the entry types that we can handle internally and don’t /// have to bubble up to the user. #[derive(Clone, Debug)] +// 'Entry' is the largest variant, but is also the most common. +#[allow(clippy::large_enum_variant)] enum ScannedEntry { /// An entry that should be handed to the user. Entry(Entry), @@ -409,7 +415,10 @@ impl<'a> EntryScanner<'a> { self.zonefile.last_ttl = ttl; ttl } - None => self.zonefile.last_ttl, + None => match self.zonefile.dollar_ttl { + Some(dollar_ttl) => dollar_ttl, + None => self.zonefile.last_ttl, + }, }; let data = ZoneRecordData::scan(rtype, self)?; @@ -1771,6 +1780,9 @@ mod test { ZoneRecordData::Unknown(_) ) { assert_eq!(first, &parsed); + // impl PartialEq for Record does NOT compare TTLs + // so check that explicitly. + assert_eq!(first.ttl(), parsed.ttl()); } } _ => panic!(), @@ -1839,4 +1851,60 @@ mod test { "../../test-data/zonefiles/stroverflow.yaml" )); } + + #[test] + fn test_multiple_dollar_ttls_multiple_missing_ttls() { + TestCase::test(include_str!( + "../../test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml" + )) + } + + #[test] + fn test_multiple_dollar_ttls_no_missing_ttls() { + TestCase::test(include_str!( + "../../test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml" + )) + } + + #[test] + fn test_no_dollar_ttl_no_missing_ttls() { + TestCase::test(include_str!( + "../../test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml" + )) + } + + #[test] + fn test_no_dollar_ttl_one_missing_ttl() { + TestCase::test(include_str!( + "../../test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml" + )) + } + + #[test] + fn test_top_dollar_ttl_and_missing_ttl() { + TestCase::test(include_str!( + "../../test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml" + )) + } + + #[test] + fn test_top_dollar_ttl_no_missing_ttls() { + TestCase::test(include_str!( + "../../test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml" + )) + } + + #[test] + fn test_rfc_1035_class_ttl_type_rdata() { + TestCase::test(include_str!( + "../../test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml" + )) + } + + #[test] + fn test_rfc_1035_ttl_class_type_rdata() { + TestCase::test(include_str!( + "../../test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml" + )) + } } diff --git a/src/zonetree/error.rs b/src/zonetree/error.rs index 4fd092d16..7e7903d94 100644 --- a/src/zonetree/error.rs +++ b/src/zonetree/error.rs @@ -289,10 +289,10 @@ impl From for io::Error { match src { ZoneTreeModificationError::Io(err) => err, ZoneTreeModificationError::ZoneDoesNotExist => { - io::Error::new(io::ErrorKind::Other, "zone does not exist") + io::Error::other("zone does not exist") } ZoneTreeModificationError::ZoneExists => { - io::Error::new(io::ErrorKind::Other, "zone exists") + io::Error::other("zone exists") } } } diff --git a/src/zonetree/in_memory/read.rs b/src/zonetree/in_memory/read.rs index 5a6afe1f4..cb3c06b1c 100644 --- a/src/zonetree/in_memory/read.rs +++ b/src/zonetree/in_memory/read.rs @@ -110,7 +110,18 @@ impl ReadZone { ) } } - Some(Special::NxDomain) => NodeAnswer::nx_domain(), + Some(Special::NxDomain) => { + if walk.enabled() { + self.query_children( + node.children(), + label, + qname, + qtype, + walk, + ); + } + NodeAnswer::nx_domain() + } Some(Special::Cname(cname)) => { if walk.enabled() { let mut rrset = Rrset::new(Rtype::CNAME, cname.ttl()); @@ -399,3 +410,81 @@ impl NodeAnswer { self.answer } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::base::iana::Class; + use crate::base::name::OwnedLabel; + use crate::base::Ttl; + use crate::rdata::{ZoneRecordData, A}; + use crate::zonetree::StoredName; + use core::str::FromStr; + use core::sync::atomic::{AtomicU8, Ordering}; + use std::boxed::Box; + + #[test] + fn should_walk_below_ents() { + let apex = ZoneApex::new(Name::from_str("c.").unwrap(), Class::IN); + + let version = Version::default(); + let mut rrset = Rrset::new(Rtype::A, Ttl::HOUR); + rrset.push_data(ZoneRecordData::A(A::from_str("1.2.3.4").unwrap())); + let rrset = SharedRrset::new(rrset); + apex.rrsets().update(rrset, version); + + apex.children().with_or_default( + &OwnedLabel::from_str("b").unwrap(), + |node, _bool| { + // TODO: I'm not sure it's good that I have to forcibly mark + // this as NXDOMAIN. I'm concerned that at present edits to a + // zone via the write interface will cause Special::NXDOMAIN + // to be set but that initial zone construction via + // ZoneBuilder will not. That might need to be investigated. + // On the other hand I'm not aware of any actual problems at + // the moment and we're going to revisit the zone + // construction, iterating and querying anyway. Without this + // line the bug this test was created to verify doesn't + // happen, namely that when handling the Special::NxDomain + // case query_node_here_and_below() doesn't descend below an + // ENT. + node.update_special( + Version::default(), + Some(Special::NxDomain), + ); + node.children().with_or_default( + &OwnedLabel::from_str("a").unwrap(), + |node, _bool| { + let version = Version::default(); + let mut rrset = Rrset::new(Rtype::A, Ttl::HOUR); + rrset.push_data(ZoneRecordData::A( + A::from_str("1.2.3.4").unwrap(), + )); + let rrset = SharedRrset::new(rrset); + node.rrsets().update(rrset, version); + }, + ); + }, + ); + + let apex = Arc::new(apex); + + let count = Arc::new(AtomicU8::new(0)); + let count_clone = count.clone(); + + let read = + ReadZone::new(apex, Version::default(), VersionMarker.into()); + let op = Box::new( + move |_owner: StoredName, + _rrset: &SharedRrset, + _at_zone_cut: bool| { + count_clone.fetch_add(1, Ordering::SeqCst); + }, + ); + read.walk(op); + + // Count should be two: c. and a.b.c. + // I.e. a.b.c. isn't missed because it is below the ENT b.c. + assert_eq!(count.load(Ordering::SeqCst), 2); + } +} diff --git a/src/zonetree/in_memory/write.rs b/src/zonetree/in_memory/write.rs index d99631cab..4a92eddbc 100644 --- a/src/zonetree/in_memory/write.rs +++ b/src/zonetree/in_memory/write.rs @@ -265,12 +265,7 @@ impl WritableZone for WriteZone { let res = new_apex .map(|node| Box::new(node) as Box) - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Open error: {err}"), - ) - }); + .map_err(|err| io::Error::other(format!("Open error: {err}"))); Box::pin(ready(res)) } @@ -604,12 +599,7 @@ impl WriteNode { Ok(()) } } - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Write apex error: {err}"), - ) - }) + .map_err(|err| io::Error::other(format!("Write apex error: {err}"))) } fn make_cname(&self, cname: SharedRr) -> Result<(), io::Error> { @@ -623,12 +613,7 @@ impl WriteNode { Ok(()) } } - .map_err(|err| { - io::Error::new( - io::ErrorKind::Other, - format!("Write apex error: {err}"), - ) - }) + .map_err(|err| io::Error::other(format!("Write apex error: {err}"))) } fn remove_all(&self) -> Result<(), io::Error> { @@ -791,10 +776,9 @@ impl From for WriteApexError { impl From for io::Error { fn from(src: WriteApexError) -> io::Error { match src { - WriteApexError::NotAllowed => io::Error::new( - io::ErrorKind::Other, - "operation not allowed at apex", - ), + WriteApexError::NotAllowed => { + io::Error::other("operation not allowed at apex") + } WriteApexError::Io(err) => err, } } diff --git a/src/zonetree/parsed.rs b/src/zonetree/parsed.rs index dc4cab13f..b48f5dd22 100644 --- a/src/zonetree/parsed.rs +++ b/src/zonetree/parsed.rs @@ -82,6 +82,7 @@ impl Zonefile { } /// Inserts the given record into the zone file. + #[allow(clippy::result_large_err)] pub fn insert( &mut self, record: StoredRecord, diff --git a/test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml b/test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml new file mode 100644 index 000000000..8d0bda17a --- /dev/null +++ b/test-data/zonefiles/multiple_dollar_ttls_multiple_missing_ttls.yaml @@ -0,0 +1,35 @@ +origin: example.com. +zonefile: | + $TTL 5555 + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. IN NS example.com. + $TTL 6666 + example.com. 3333 IN A 192.0.2.1 + example.com. IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 5555 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 3333 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 6666 + data: !Aaaa + addr: 2001:db8::3 diff --git a/test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml b/test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml new file mode 100644 index 000000000..bb6ea9e25 --- /dev/null +++ b/test-data/zonefiles/multiple_dollar_ttls_no_missing_ttls.yaml @@ -0,0 +1,35 @@ +origin: example.com. +zonefile: | + $TTL 5555 + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. 2222 IN NS example.com. + $TTL 666 + example.com. 3333 IN A 192.0.2.1 + example.com. 4444 IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 2222 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 3333 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 4444 + data: !Aaaa + addr: 2001:db8::3 diff --git a/test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml b/test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml new file mode 100644 index 000000000..7bcd92c2a --- /dev/null +++ b/test-data/zonefiles/no_dollar_ttl_no_missing_ttls.yaml @@ -0,0 +1,33 @@ +origin: example.com. +zonefile: | + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. 2222 IN NS example.com. + example.com. 3333 IN A 192.0.2.1 + example.com. 4444 IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 2222 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 3333 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 4444 + data: !Aaaa + addr: 2001:db8::3 diff --git a/test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml b/test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml new file mode 100644 index 000000000..6e5a49b9f --- /dev/null +++ b/test-data/zonefiles/no_dollar_ttl_one_missing_ttl.yaml @@ -0,0 +1,33 @@ +origin: example.com. +zonefile: | + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. 2222 IN NS example.com. + example.com. IN A 192.0.2.1 + example.com. 4444 IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 2222 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 2222 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 4444 + data: !Aaaa + addr: 2001:db8::3 diff --git a/test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml b/test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml new file mode 100644 index 000000000..df6386eb9 --- /dev/null +++ b/test-data/zonefiles/rfc_1035_class_ttl_type_rdata.yaml @@ -0,0 +1,15 @@ +origin: example.com. +zonefile: | + example.com. IN 1111 SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 diff --git a/test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml b/test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml new file mode 100644 index 000000000..d7ecda0a4 --- /dev/null +++ b/test-data/zonefiles/rfc_1035_ttl_class_type_rdata.yaml @@ -0,0 +1,15 @@ +origin: example.com. +zonefile: | + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 diff --git a/test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml b/test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml new file mode 100644 index 000000000..56e75d6d7 --- /dev/null +++ b/test-data/zonefiles/top_dollar_ttl_and_missing_ttl.yaml @@ -0,0 +1,34 @@ +origin: example.com. +zonefile: | + $TTL 5555 + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. 2222 IN NS example.com. + example.com. IN A 192.0.2.1 + example.com. 4444 IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 2222 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 5555 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 4444 + data: !Aaaa + addr: 2001:db8::3 diff --git a/test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml b/test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml new file mode 100644 index 000000000..2f0c2d4ad --- /dev/null +++ b/test-data/zonefiles/top_dollar_ttl_no_missing_ttls.yaml @@ -0,0 +1,34 @@ +origin: example.com. +zonefile: | + $TTL 5555 + example.com. 1111 IN SOA ns.example.com. noc.dns.example.org. 2020080302 7200 3600 1209600 3600 + example.com. 2222 IN NS example.com. + example.com. 3333 IN A 192.0.2.1 + example.com. 4444 IN AAAA 2001:db8::3 +result: + - owner: example.com. + class: IN + ttl: 1111 + data: !Soa + mname: ns.example.com. + rname: noc.dns.example.org. + serial: 2020080302 + refresh: 7200 + retry: 3600 + expire: 1209600 + minimum: 3600 + - owner: example.com. + class: IN + ttl: 2222 + data: !Ns + nsdname: example.com. + - owner: example.com. + class: IN + ttl: 3333 + data: !A + addr: 192.0.2.1 + - owner: example.com. + class: IN + ttl: 4444 + data: !Aaaa + addr: 2001:db8::3 From 0c76827c3c96262d7acda0690dc4b2f95ab223b0 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 4 Jun 2025 12:17:51 +0200 Subject: [PATCH 443/569] Add algorithm roll. --- examples/keyset.rs | 22 +- src/dnssec/sign/keys/keyset.rs | 374 +++++++++++++++++++++++++++++++-- 2 files changed, 373 insertions(+), 23 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index 250f79055..35f9c4235 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -211,12 +211,16 @@ fn do_start(filename: &str, args: &[String]) { None } } - RollType::CskRoll => match k.keytype() { - KeyType::Ksk(keystate) - | KeyType::Zsk(keystate) - | KeyType::Csk(keystate, _) => Some((keystate.clone(), pr)), - KeyType::Include(_) => None, - }, + RollType::CskRoll | RollType::AlgorithmRoll => { + match k.keytype() { + KeyType::Ksk(keystate) + | KeyType::Zsk(keystate) + | KeyType::Csk(keystate, _) => { + Some((keystate.clone(), pr)) + } + KeyType::Include(_) => None, + } + } }) .filter(|(keystate, _)| !keystate.old()) .map(|(keystate, pubref)| (keystate, pubref.to_string())) @@ -536,9 +540,15 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { Action::ReportDnskeyPropagated => { println!("\tReport that the DNSKEY RRset has propagated") } + Action::WaitDnskeyPropagated => { + println!("\tWait until the DNSKEY RRset has propagated") + } Action::ReportRrsigPropagated => { println!("\tReport that the RRSIG records have propagated") } + Action::WaitRrsigPropagated => { + println!("\tWait until the RRSIG records have propagated") + } Action::ReportDsPropagated => println!( "\tReport that the DS RRset has propagated at the parent" ), diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 6dd1cd7d4..976771a2f 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -138,6 +138,9 @@ impl KeySet { key_tag: u16, creation_ts: UnixTime, ) -> Result<(), Error> { + if !self.unique_key_tag(key_tag) { + return Err(Error::DuplicateKeyTag); + } let keystate: KeyState = Default::default(); let key = Key::new( privref, @@ -163,6 +166,9 @@ impl KeySet { key_tag: u16, creation_ts: UnixTime, ) -> Result<(), Error> { + if !self.unique_key_tag(key_tag) { + return Err(Error::DuplicateKeyTag); + } let keystate: KeyState = Default::default(); let key = Key::new( privref, @@ -659,6 +665,119 @@ impl KeySet { } Ok(()) } + + fn update_algorithm( + &mut self, + mode: Mode, + old: &[&str], + new: &[&str], + ) -> Result<(), Error> { + let mut tmpkeys = self.keys.clone(); + let keys: &mut HashMap = match mode { + Mode::DryRun => &mut tmpkeys, + Mode::ForReal => &mut self.keys, + }; + for k in old { + let Some(key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + match key.keytype { + KeyType::Ksk(ref mut keystate) + | KeyType::Zsk(ref mut keystate) => { + keystate.old = true; + } + KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { + ksk_keystate.old = true; + zsk_keystate.old = true; + } + KeyType::Include(_) => { + return Err(Error::WrongKeyType); + } + } + } + let now = UnixTime::now(); + for k in new { + let Some(key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + match key.keytype { + KeyType::Ksk(ref mut keystate) + | KeyType::Zsk(ref mut keystate) => { + if *keystate + != (KeyState { + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + // Move key state to Active. + keystate.present = true; + keystate.signer = true; + key.timestamps.published = Some(now.clone()); + } + KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { + if *ksk_keystate + != (KeyState { + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + // Move key state to Active. + ksk_keystate.present = true; + ksk_keystate.signer = true; + + if *zsk_keystate + != (KeyState { + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + // Move key state to Incoming. + zsk_keystate.present = true; + zsk_keystate.signer = true; + + key.timestamps.published = Some(now.clone()); + } + _ => { + return Err(Error::WrongKeyType); + } + } + } + + // Make sure we have at least one KSK key in incoming state. + if !keys.iter().any(|(_, k)| match &k.keytype { + KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { + !keystate.old && keystate.present + } + _ => false, + }) { + return Err(Error::NoSuitableKeyPresent); + } + // Make sure we have at least one ZSK key in incoming state. + if !keys.iter().any(|(_, k)| match &k.keytype { + KeyType::Zsk(keystate) | KeyType::Csk(_, keystate) => { + !keystate.old && keystate.present + } + _ => false, + }) { + return Err(Error::NoSuitableKeyPresent); + } + Ok(()) + } } /// The state of a single key. @@ -1011,6 +1130,12 @@ pub enum Action { /// DNSKEY RRset. ReportDnskeyPropagated, + /// Wait for the DNSKEY RRset to propagate before moving to the next + /// state. Waiting is not needed for the correctness of the key roll + /// algorithm. However without waiting, the state of keyset may not reflect + /// reality. + WaitDnskeyPropagated, + /// Report whether updated DS records have propagated to all /// secondaries that serve the parent zone. Also report the TTL of /// the DS records. @@ -1021,6 +1146,12 @@ pub enum Action { /// sufficient to track the signatures on the SOA record. Report the /// highest TTL among all signatures. ReportRrsigPropagated, + + /// Wait for updated RRSIG records to propagate before moving to the next + /// state. Waiting is not needed for the correctness of the key roll + /// algorithm. However without waiting, the state of keyset may not reflect + /// reality. + WaitRrsigPropagated, } /// The type of key roll to perform. @@ -1034,6 +1165,9 @@ pub enum RollType { /// A CSK roll. CskRoll, + + /// An algorithm roll. + AlgorithmRoll, } impl RollType { @@ -1042,6 +1176,7 @@ impl RollType { RollType::KskRoll => ksk_roll, RollType::ZskRoll => zsk_roll, RollType::CskRoll => csk_roll, + RollType::AlgorithmRoll => algorithm_roll, } } fn roll_actions_fn(&self) -> fn(RollState) -> Vec { @@ -1049,6 +1184,7 @@ impl RollType { RollType::KskRoll => ksk_roll_actions, RollType::ZskRoll => zsk_roll_actions, RollType::CskRoll => csk_roll_actions, + RollType::AlgorithmRoll => algorithm_roll_actions, } } } @@ -1637,6 +1773,195 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { actions } +// An algorithm roll is similar to a CSK roll. The main difference is that +// to zone is signed with all keys before introducing the DS records for +// the new KSKs or CSKs. +fn algorithm_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current algorithm-roll state is idle. We need + // to check all conflicting key rolls as well. The way we check + // is to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = ks.rollstates.keys().next() { + if *rolltype == RollType::AlgorithmRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } + // Check if we can move the states of the keys + ks.update_algorithm(Mode::DryRun, old, new)?; + // Move the states of the keys + ks.update_algorithm(Mode::ForReal, old, new) + .expect("Should have been check with DryRun"); + } + RollOp::Propagation1 => { + // Set the visible time of new KSKs, ZSKs and CSKs to the current + // time. Set signer and for new KSKs, ZSKs and CSKs. + // Set RRSIG visible for new ZSKs and CSKs. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + match &mut k.keytype { + KeyType::Ksk(keystate) => { + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + } + KeyType::Zsk(keystate) | KeyType::Csk(keystate, _) => { + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + k.timestamps.rrsig_visible = Some(now.clone()); + } + KeyType::Include(_) => (), + } + } + } + RollOp::CacheExpire1(ttl) => { + for k in ks.keys.values_mut() { + let keystate = match &k.keytype { + KeyType::Ksk(keystate) + | KeyType::Zsk(keystate) + | KeyType::Csk(keystate, _) => keystate, + KeyType::Include(_) => continue, + }; + if keystate.old || !keystate.present { + continue; + } + + let visible = k + .timestamps + .visible + .as_ref() + .expect("Should have been set in Propagation1"); + let elapsed = visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + for k in ks.keys.values_mut() { + match k.keytype { + KeyType::Ksk(ref mut keystate) + | KeyType::Csk(ref mut keystate, _) => { + if keystate.old && keystate.present { + keystate.at_parent = false; + } + + // Put Active keys at parent. + if !keystate.old && keystate.present { + keystate.at_parent = true; + } + } + KeyType::Zsk(_) | KeyType::Include(_) => (), + } + } + } + RollOp::Propagation2 => { + // Set the published time of new DS records to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + match &k.keytype { + KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.ds_visible = Some(now.clone()); + } + KeyType::Zsk(_) | KeyType::Include(_) => (), + } + } + } + RollOp::CacheExpire2(ttl) => { + for k in ks.keys.values_mut() { + let keystate = match &k.keytype { + KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { + keystate + } + KeyType::Zsk(_) | KeyType::Include(_) => continue, + }; + if keystate.old || !keystate.signer { + continue; + } + + let ds_visible = k + .timestamps + .ds_visible + .as_ref() + .expect("Should have been set in Propagation2"); + let elapsed = ds_visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move old keys out + for k in ks.keys.values_mut() { + match k.keytype { + KeyType::Ksk(ref mut keystate) + | KeyType::Zsk(ref mut keystate) => { + if keystate.old && keystate.present { + keystate.signer = false; + keystate.present = false; + k.timestamps.withdrawn = Some(UnixTime::now()); + } + } + KeyType::Csk( + ref mut ksk_keystate, + ref mut zsk_keystate, + ) => { + if ksk_keystate.old && ksk_keystate.present { + ksk_keystate.signer = false; + ksk_keystate.present = false; + zsk_keystate.signer = false; + zsk_keystate.present = false; + k.timestamps.withdrawn = Some(UnixTime::now()); + } + } + KeyType::Include(_) => (), + } + } + } + RollOp::Done => (), + } + Ok(()) +} + +fn algorithm_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportDnskeyPropagated); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::ReportDsPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => { + actions.push(Action::RemoveCdsRrset); + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::WaitDnskeyPropagated); + actions.push(Action::WaitRrsigPropagated); + } + } + actions +} + #[cfg(test)] mod tests { use crate::base::Name; @@ -1674,40 +1999,48 @@ mod tests { "first ZSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, - 0, + 1, UnixTime::now(), ) .unwrap(); let actions = ks - .start_roll(RollType::CskRoll, &[], &["first KSK", "first ZSK"]) + .start_roll( + RollType::AlgorithmRoll, + &[], + &["first KSK", "first ZSK"], + ) .unwrap(); assert_eq!( actions, - [Action::UpdateDnskeyRrset, Action::ReportDnskeyPropagated] + [ + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::ReportDnskeyPropagated, + Action::ReportRrsigPropagated + ] ); let mut dk = dnskey(&ks); dk.sort(); assert_eq!(dk, ["first KSK", "first ZSK"]); assert_eq!(dnskey_sigs(&ks), ["first KSK"]); - assert_eq!(zone_sigs(&ks), Vec::::new()); + assert_eq!(zone_sigs(&ks), ["first ZSK"]); assert_eq!(ds_keys(&ks), Vec::::new()); - let actions = - ks.propagation1_complete(RollType::CskRoll, 3600).unwrap(); + let actions = ks + .propagation1_complete(RollType::AlgorithmRoll, 3600) + .unwrap(); assert_eq!(actions, []); MockClock::advance_system_time(Duration::from_secs(3600)); - let actions = ks.cache_expired1(RollType::CskRoll).unwrap(); + let actions = ks.cache_expired1(RollType::AlgorithmRoll).unwrap(); assert_eq!( actions, [ Action::CreateCdsRrset, Action::UpdateDsRrset, - Action::UpdateRrsig, Action::ReportDsPropagated, - Action::ReportRrsigPropagated ] ); let mut dk = dnskey(&ks); @@ -1717,16 +2050,23 @@ mod tests { assert_eq!(zone_sigs(&ks), ["first ZSK"]); assert_eq!(ds_keys(&ks), ["first KSK"]); - let actions = - ks.propagation2_complete(RollType::CskRoll, 3600).unwrap(); + let actions = ks + .propagation2_complete(RollType::AlgorithmRoll, 3600) + .unwrap(); assert_eq!(actions, []); MockClock::advance_system_time(Duration::from_secs(3600)); - let actions = ks.cache_expired2(RollType::CskRoll).unwrap(); + let actions = ks.cache_expired2(RollType::AlgorithmRoll).unwrap(); assert_eq!( actions, - [Action::RemoveCdsRrset, Action::UpdateDnskeyRrset] + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::WaitDnskeyPropagated, + Action::WaitRrsigPropagated, + ] ); let mut dk = dnskey(&ks); dk.sort(); @@ -1735,14 +2075,14 @@ mod tests { assert_eq!(zone_sigs(&ks), ["first ZSK"]); assert_eq!(ds_keys(&ks), ["first KSK"]); - let actions = ks.roll_done(RollType::CskRoll).unwrap(); + let actions = ks.roll_done(RollType::AlgorithmRoll).unwrap(); assert_eq!(actions, []); ks.add_key_ksk( "second KSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, - 0, + 2, UnixTime::now(), ) .unwrap(); @@ -1750,7 +2090,7 @@ mod tests { "second ZSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, - 0, + 3, UnixTime::now(), ) .unwrap(); @@ -1951,7 +2291,7 @@ mod tests { "second CSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, - 0, + 4, UnixTime::now(), ) .unwrap(); From 1ccb9901af78df79be9a44b727c33262496feb1e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Jun 2025 11:34:04 +0200 Subject: [PATCH 444/569] WIP. --- Cargo.lock | 140 ++---- Cargo.toml | 13 +- src/crypto/kmip.rs | 503 ++++++++++++++++++++++ src/crypto/kmip/key.rs | 47 -- src/crypto/kmip/mod.rs | 4 - src/crypto/{kmip/pool.rs => kmip_pool.rs} | 14 +- src/crypto/mod.rs | 18 +- src/crypto/sign.rs | 24 ++ 8 files changed, 606 insertions(+), 157 deletions(-) create mode 100644 src/crypto/kmip.rs delete mode 100644 src/crypto/kmip/key.rs delete mode 100644 src/crypto/kmip/mod.rs rename src/crypto/{kmip/pool.rs => kmip_pool.rs} (93%) diff --git a/Cargo.lock b/Cargo.lock index 0a01d965d..e515305f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,9 +118,19 @@ dependencies = [ [[package]] name = "base64" -version = "0.13.1" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bcder" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ffdaa8c6398acd07176317eb6c1f9082869dd1cc3fee7c72c6354866b928cc" +dependencies = [ + "bytes", + "smallvec", +] [[package]] name = "bitflags" @@ -169,7 +179,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-link", ] @@ -265,9 +278,10 @@ dependencies = [ "proc-macro2", "r2d2", "rand", - "ring 0.17.14", + "ring", + "rpki", "rstest", - "rustls-pemfile 2.2.0", + "rustls-pemfile", "rustversion", "secrecy", "serde", @@ -612,8 +626,8 @@ dependencies = [ [[package]] name = "kmip-protocol" -version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#d8b74c8a7de2f93a200f9f555155006e6c639139" +version = "0.4.4-dev" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=add-ecdsa-support#e493155e9fee257e71d7630f89f432089e97dbb4" dependencies = [ "cfg-if", "enum-display-derive", @@ -621,20 +635,19 @@ dependencies = [ "kmip-ttlv", "log", "maybe-async", + "openssl", "rustc_version", - "rustls 0.19.1", - "rustls-pemfile 0.2.1", "serde", "serde_bytes", "serde_derive", "trait-set", - "webpki", ] [[package]] name = "kmip-ttlv" -version = "0.4.0" -source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#dad2803cbcb35556e35ab2fa1341e860fcecec60" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13cdaafff68ae98da73fd6dff927095849646c6eeee44bdd0a983d30192cdeb1" dependencies = [ "cfg-if", "hex", @@ -1096,21 +1109,6 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" -[[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 0.7.1", - "web-sys", - "winapi", -] - [[package]] name = "ring" version = "0.17.14" @@ -1121,10 +1119,26 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] +[[package]] +name = "rpki" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a043d99463db58c05283f5ae5d9ced858cc3483011747264e21f50b9201cdd" +dependencies = [ + "base64", + "bcder", + "bytes", + "chrono", + "log", + "ring", + "untrusted", + "uuid", +] + [[package]] name = "rstest" version = "0.23.0" @@ -1170,19 +1184,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustls" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" -dependencies = [ - "base64", - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.23.27" @@ -1191,22 +1192,13 @@ checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "log", "once_cell", - "ring 0.17.14", + "ring", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-pemfile" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" -dependencies = [ - "base64", -] - [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1231,9 +1223,9 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "ring 0.17.14", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -1269,16 +1261,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - [[package]] name = "secrecy" version = "0.10.3" @@ -1403,12 +1385,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1543,7 +1519,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.27", + "rustls", "tokio", ] @@ -1690,12 +1666,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -1798,26 +1768,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki" -version = "0.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" -dependencies = [ - "ring 0.16.20", - "untrusted 0.7.1", -] - [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/Cargo.toml b/Cargo.toml index 59d28c50d..cfb0c75de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,11 @@ chrono = { version = "0.4.35", optional = true, default-features = false futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.14.2", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing heapless = { version = "0.8", optional = true } -kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-rustls"] } # TODO: use the async feature instead +kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "add-ecdsa-support", package = "kmip-protocol", version = "0.4.4-dev", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +#kmip = { version = "0.4.3", package = "kmip-protocol", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +#kmip = { path = "../kmip-protocol2", package = "kmip-protocol", version = "0.4.4-dev", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +#kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +#kmip = { path = "../kmip-protocol", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } @@ -42,6 +46,7 @@ openssl = { version = "0.10.72", optional = true } # 0.10.70 upgrades to proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } +rpki = { version = "0.18", optional = true, features = ["crypto"] } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } @@ -67,9 +72,9 @@ std = ["alloc", "dep:hashbrown", "bumpalo?/std", "bytes?/std", "octseq/s tracing = ["dep:log", "dep:tracing"] # Cryptographic backends -ring = ["dep:ring"] -openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2"] +ring = ["dep:ring"] +openssl = ["dep:openssl"] +kmip = ["dep:kmip", "dep:r2d2", "dep:rpki"] static-openssl = ["openssl/vendored"] # Crate features diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs new file mode 100644 index 000000000..1f17c6d34 --- /dev/null +++ b/src/crypto/kmip.rs @@ -0,0 +1,503 @@ +#![cfg(feature = "kmip")] +#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] + +//============ Error Types =================================================== + +//----------- GenerateError -------------------------------------------------- + +use core::fmt; + +/// An error in generating a key pair with OpenSSL. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm was not supported. + UnsupportedAlgorithm, + + /// An implementation failure occurred. + /// + /// This includes memory allocation failures. + Implementation, +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::UnsupportedAlgorithm => "algorithm not supported", + Self::Implementation => "an internal error occurred", + }) + } +} + +//--- Error + +impl std::error::Error for GenerateError {} + +#[cfg(feature = "unstable-crypto-sign")] +pub mod sign { + use std::boxed::Box; + use std::string::{String, ToString}; + use std::time::SystemTime; + use std::vec::Vec; + + use kmip::types::common::{ + CryptographicAlgorithm, CryptographicParameters, + CryptographicUsageMask, DigitalSignatureAlgorithm, HashingAlgorithm, + KeyMaterial, ObjectType, + }; + use kmip::types::request::{ + self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, + PublicKeyTemplateAttribute, RequestPayload, + }; + use kmip::types::response::{ManagedObject, ResponsePayload}; + use log::error; + + use crate::base::iana::SecurityAlgorithm; + use crate::crypto::kmip_pool::KmipConnPool; + use crate::crypto::sign::{ + GenerateError, GenerateParams, SignError, SignRaw, Signature, + }; + use crate::rdata::Dnskey; + + #[derive(Clone, Debug)] + pub struct KeyPair { + /// The algorithm used by the key. + algorithm: SecurityAlgorithm, + + private_key_id: String, + + public_key_id: String, + + conn_pool: KmipConnPool, + + flags: u16, + } + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn dnskey(&self) -> Dnskey> { + let client = self.conn_pool.get().unwrap(); + + let res = client.get_key(&self.public_key_id).unwrap(); + + assert_eq!(res.object_type, ObjectType::PublicKey); + + let ManagedObject::PublicKey(public_key) = + res.cryptographic_object + else { + todo!(); + }; + + let octets = match public_key.key_block.key_value.key_material { + KeyMaterial::Bytes(bytes) => { + // Hmmm, this is what we get with PyKMIP, rather than + // TransparentRSAPublicKey. The Dnskey we create using + // these octets doesn't seem to render the RDATA correctly + // so do we need to do something with these bytes? + bytes + } + KeyMaterial::TransparentRSAPublicKey(key) => { + rpki::crypto::keys::PublicKey::rsa_from_components( + &key.modulus, + &key.public_exponent, + ) + .unwrap() + .bits() + .to_vec() + } + _ => todo!(), + }; + + Dnskey::new(self.flags, 3, self.algorithm, octets).unwrap() + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let client = self.conn_pool.get().map_err(|_| SignError)?; + + let signed = client.sign(&self.private_key_id, data).unwrap(); + + // TODO: For HSMs that don't support hashing do we have to do + // hashing ourselves here after signing? Note: PyKMIP doesn't + // support CryptographicParameters (and thus also not + // HashingFunction) nor does it support the Hash operation. + // Maybe via crypto::common::DigestBuilder? + + // TODO: Where do we find out what the HSM supports? Trying an + // operation then falling back each time it fails is inefficient. + // We can presumably instead discover this on first use of the + // HSM, ala how Krill does HSM probing. We would need to know the + // result of such probing, which features are supported, here. We + // only have access to the KMIP connection pool here, so I guess + // that has to be able to tell us what we want to know. + match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + Ok(Signature::RsaSha256(Box::<[u8; 64]>::new( + signed + .signature_data + .try_into() + .map_err(|_| SignError)?, + ))) + } + SecurityAlgorithm::ECDSAP256SHA256 => { + Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( + signed + .signature_data + .try_into() + .map_err(|_| SignError)?, + ))) + } + SecurityAlgorithm::ECDSAP384SHA384 => { + Ok(Signature::EcdsaP384Sha384(Box::<[u8; 96]>::new( + signed + .signature_data + .try_into() + .map_err(|_| SignError)?, + ))) + } + SecurityAlgorithm::ED25519 => { + Ok(Signature::Ed25519(Box::<[u8; 64]>::new( + signed + .signature_data + .try_into() + .map_err(|_| SignError)?, + ))) + } + SecurityAlgorithm::ED448 => { + Ok(Signature::Ed448(Box::<[u8; 114]>::new( + signed + .signature_data + .try_into() + .map_err(|_| SignError)?, + ))) + } + _ => Err(SignError)?, + } + } + } + + //----------- generate() ------------------------------------------------- + + /// Generate a new secret key for the given algorithm. + pub fn generate( + name: String, + params: GenerateParams, + flags: u16, + conn_pool: KmipConnPool, + ) -> Result { + let algorithm = params.algorithm(); + + let client = conn_pool.get().map_err(|_| SignError).unwrap(); + + // TODO: Determine this on first use of the HSM? + // PyKMIP doesn't support ActivationDate. + // Fortanix DSM does support it and creates the key in an activated state but still returns a (harmless?) error: + // Server error: Operation CreateKeyPair failed: Input field `state` is not coherent with provided activation/deactivation dates + let activate_on_create = false; + + let use_cryptographic_params = false; + + let mut common_attrs = vec![]; + let priv_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private key. + request::Attribute::Name(format!("{name}_priv")), + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Sign, + ), + ]; + let pub_key_attrs = vec![ + // Krill supplies a name at creation time. Do we need to? + // Note: Fortanix DSM requires a name for at least the private key. + request::Attribute::Name(format!("{name}_pub")), + // Krill does verification, do we need to? ODS doesn't. + // Note: PyKMIP requires a Cryptographic Usage Mask for the public + // key. + request::Attribute::CryptographicUsageMask( + CryptographicUsageMask::Verify, + ), + ]; + + // PyKMIP doesn't support CryptographicParameters so we cannot supply + // HashingAlgorithm. It also doesn't support the Hash operation. How + // do we specify SHA256 hashing? Do we have to do it ourselves + // post-signing? Can we just specify the hashing to do when invoking + // the Sign operation? + // Fortanix DSM also doesn't support Cryptographic Parameters: + // Server error: Operation CreateKeyPair failed: Don't have handling for attribute Cryptographic Parameters + + // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that + // don't support it we have to do a separate Activate operation after + // creating the key pair. + // Fortanix DSM does support ActivationDate. + + match params { + GenerateParams::RsaSha256 { bits } => { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For RSA, Cryptographic Length corresponds to the bit length of the Modulus" + + // RFC 5702 2.1 + if bits < 512 || bits > 4096 { + return Err(GenerateError::UnsupportedAlgorithm); + } + + if use_cryptographic_params { + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm(DigitalSignatureAlgorithm::SHA256WithRSAEncryption_PKCS1_v1_5) + ) + ) + } else { + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::RSA, + ), + ); + common_attrs.push( + request::Attribute::CryptographicLength( + bits.try_into().unwrap(), + ), + ); + } + } + GenerateParams::EcdsaP256Sha256 => { + // PyKMIP doesn't support ECDSA: + // "Operation CreateKeyPair failed: The cryptographic + // algorithm (CryptographicAlgorithm.ECDSA) is not a + // supported asymmetric key algorithm." + + if use_cryptographic_params { + common_attrs.push( + request::Attribute::CryptographicParameters( + CryptographicParameters::default() + .with_digital_signature_algorithm( + DigitalSignatureAlgorithm::ECDSAWithSHA256, + ), + ), + ) + } else { + // RFC 8624 3.1 DNSSEC Signing: MUST + // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 + // "For ECDSA, ECDH, and ECMQV algorithms, Cryptographic + // Length corresponds to the bit length of parameter + // Q." + common_attrs.push( + request::Attribute::CryptographicAlgorithm( + CryptographicAlgorithm::ECDSA, + ), + ); + // ODS doesn't tell PKCS#11 a Q length. I have no idea + // what value we should put here, but as Q length is + // optional let's try not passing it. + // Note: PyKMIP requires a length: use 256 from P-256? + common_attrs + .push(request::Attribute::CryptographicLength(256)); + } + } + GenerateParams::EcdsaP384Sha384 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + todo!() + } + GenerateParams::Ed25519 => { + // RFC 8624 3.1 DNSSEC Signing: RECOMMENDED + todo!() + } + GenerateParams::Ed448 => { + // RFC 8624 3.1 DNSSEC Signing: MAY + todo!() + } + }; + + if activate_on_create { + // https://docs.oasis-open.org/kmip/testcases/v1.1/kmip-testcases-v1.1.html + // shows an example including an Activation Date value of 2 noted + // as meaning Thu Jan 01 01:00:02 CET 1970. i.e. the activation + // date should be a UNIX epoch timestamp. + let time_now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + common_attrs.push(request::Attribute::ActivationDate(time_now)); + } + + let request = RequestPayload::CreateKeyPair( + Some(CommonTemplateAttribute::unnamed(common_attrs)), + Some(PrivateKeyTemplateAttribute::unnamed(priv_key_attrs)), + Some(PublicKeyTemplateAttribute::unnamed(pub_key_attrs)), + ); + + // Execute the request and capture the response + let response = client.do_request(request).map_err(|err| { + error!("KMIP request failed: {err}"); + error!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + error!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Implementation + })?; + + // Process the successful response + let ResponsePayload::CreateKeyPair(payload) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Implementation); + }; + + Ok(KeyPair { + algorithm, + private_key_id: payload.private_key_unique_identifier.to_string(), + public_key_id: payload.public_key_unique_identifier.to_string(), + conn_pool, + flags, + }) + } +} + +#[cfg(test)] +mod tests { + use core::time::Duration; + + use std::fs::File; + use std::io::{BufReader, Read}; + use std::string::ToString; + use std::vec::Vec; + + use kmip::client::ConnectionSettings; + + use crate::crypto::kmip::sign::generate; + use crate::crypto::kmip_pool::ConnectionManager; + use crate::crypto::sign::SignRaw; + use crate::logging::init_logging; + use std::time::SystemTime; + + #[test] + fn connect() { + init_logging(); + let mut cert_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.crt", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut cert_bytes).unwrap(); + + let mut key_bytes = Vec::new(); + let file = File::open( + "/home/ximon/docker_data/pykmip/pykmip-data/selfsigned.key", + ) + .unwrap(); + let mut reader = BufReader::new(file); + reader.read_to_end(&mut key_bytes).unwrap(); + + let mut conn_settings = ConnectionSettings::default(); + conn_settings.host = "localhost".to_string(); + conn_settings.port = 5696; + conn_settings.insecure = true; + conn_settings.client_cert = + Some(kmip::client::ClientCertificate::SeparatePem { + cert_bytes, + key_bytes: Some(key_bytes), + }); + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + conn_settings.into(), + 16384, + Duration::from_secs(60), + Duration::from_secs(60), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + let res = client.query(); + dbg!(&res); + res.unwrap(); + + let generated_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + generated_key_name, + //crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + dbg!(&res); + let key = res.unwrap(); + + // let dnskey = key.dnskey(); + // eprintln!("DNSKEY: {}", dnskey); + } + + #[test] + fn fortanix_dsm_test() { + // Note: keyls fails against Fortanix DSM for some reason with error: + // Error: Server error: Operation Locate failed: expected + // AttributeValue, got ObjectType, Diagnostics: + // req: 78[77[69[6Ai6Bi]0C[23[24e1:25[99tA1t]]]0Di]0F[5Ce8:79[08[0At57e4:]]]], + // resp: 7B[7A[69[6Ai6Bi]92d0Di]0F[5Ce8:7Fe1:7Ee100:7Dt]] + + init_logging(); + + let mut conn_settings = ConnectionSettings::default(); + conn_settings.host = "eu.smartkey.io".to_string(); + conn_settings.port = 5696; + conn_settings.username = + Some("2c79ae57-18a9-431a-baa1-0ef98cf88f45".to_string()); + conn_settings.password = Some("kbJcsgfVaiVOnldmyIXitwWnuIeEVHw9Jm0EoY3NA_qj-glVucT1sbRcSWGf3st7B8xWN-aKC4rmJ0gNmfQCgg".to_string()); + + eprintln!("Creating pool..."); + let pool = ConnectionManager::create_connection_pool( + conn_settings.into(), + 16384, + Duration::from_secs(60), + Duration::from_secs(60), + ) + .unwrap(); + + eprintln!("Connecting..."); + let client = pool.get().unwrap(); + + eprintln!("Connected"); + let res = client.query(); + dbg!(&res); + res.unwrap(); + + let generated_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let res = generate( + generated_key_name, + //crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + 256, + pool, + ); + dbg!(&res); + let key = res.unwrap(); + + let dnskey = key.dnskey(); + eprintln!("DNSKEY: {}", dnskey); + } +} diff --git a/src/crypto/kmip/key.rs b/src/crypto/kmip/key.rs deleted file mode 100644 index 981bc62dd..000000000 --- a/src/crypto/kmip/key.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::string::String; - -use crate::base::iana::SecurityAlgorithm; - -use super::pool::KmipConnPool; - -struct KeyPair { - /// The algorithm used by the key. - algorithm: SecurityAlgorithm, - - private_key_id: String, - - conn_pool: KmipConnPool, -} - -#[cfg(feature = "unstable-crypto-sign")] -pub mod sign { - use std::boxed::Box; - use std::vec::Vec; - - use crate::crypto::sign::{SignError, SignRaw, Signature}; - use crate::rdata::Dnskey; - use crate::base::iana::SecurityAlgorithm; - - impl SignRaw for super::KeyPair { - fn algorithm(&self) -> SecurityAlgorithm { - self.algorithm - } - - fn dnskey(&self) -> Dnskey> { - todo!() - } - - fn sign_raw(&self, data: &[u8]) -> Result { - let client = self.conn_pool.get().map_err(|_| SignError)?; - - let signed = client.sign(&self.private_key_id, data).unwrap(); - - let signature: [u8; 64] = - signed.signature_data.try_into().unwrap(); - - let sig = Signature::EcdsaP256Sha256(Box::new(signature)); - - Ok(sig) - } - } -} diff --git a/src/crypto/kmip/mod.rs b/src/crypto/kmip/mod.rs deleted file mode 100644 index d18344917..000000000 --- a/src/crypto/kmip/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -#![cfg(feature = "kmip")] -#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] -pub mod pool; -pub mod key; diff --git a/src/crypto/kmip/pool.rs b/src/crypto/kmip_pool.rs similarity index 93% rename from src/crypto/kmip/pool.rs rename to src/crypto/kmip_pool.rs index 5dca1d25b..c973dd273 100644 --- a/src/crypto/kmip/pool.rs +++ b/src/crypto/kmip_pool.rs @@ -1,3 +1,6 @@ +#![cfg(feature = "kmip")] +#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] + //! KMIP TLS connection pool //! //! Used to: @@ -10,8 +13,9 @@ use std::net::TcpStream; use std::{sync::Arc, time::Duration}; use kmip::client::{Client, ConnectionSettings}; +use openssl::ssl::SslStream; -pub type KmipTlsClient = Client>; +pub type KmipTlsClient = Client>; pub type KmipConnPool = r2d2::Pool; @@ -76,7 +80,8 @@ impl ConnectionManager { pub fn connect_one_off( settings: &ConnectionSettings, ) -> kmip::client::Result { - kmip::client::tls::rustls::connect(settings) + let conn = kmip::client::tls::openssl::connect(settings)?; + Ok(conn) } } @@ -95,7 +100,10 @@ impl r2d2::ManageConnection for ConnectionManager { /// flag is set to false when the connection pool is created. /// /// [r2d2]: https://crates.io/crates/r2d2/ - fn is_valid(&self, _conn: &mut Self::Connection) -> Result<(), Self::Error> { + fn is_valid( + &self, + _conn: &mut Self::Connection, + ) -> Result<(), Self::Error> { unreachable!() } diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 836192b87..aa39e070f 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -33,6 +33,15 @@ not(all(feature = "unstable-crypto-sign", feature = "ring")), doc = "`ring::sign`" )] +//! , +#![cfg_attr( + all(feature = "unstable-crypto-sign", feature = "kmip"), + doc = "[`kmip::sign`]" +)] +#![cfg_attr( + not(all(feature = "unstable-crypto-sign", feature = "kmip")), + doc = "`kmip::sign`" +)] //! , and #![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] #![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] @@ -73,22 +82,22 @@ //! //! In addition to private key operations, this module provides the #![cfg_attr( - any(feature = "ring", feature = "openssl"), + any(feature = "ring", feature = "openssl", feature = "kmip"), doc = "[`common::PublicKey`]" )] #![cfg_attr( - not(any(feature = "ring", feature = "openssl")), + not(any(feature = "ring", feature = "openssl", feature = "kmip")), doc = "`common::PublicKey`" )] //! type for signature verification. //! //! The module also support computing message digests using the #![cfg_attr( - any(feature = "ring", feature = "openssl"), + any(feature = "ring", feature = "openssl", feature = "kmip"), doc = "[`common::DigestBuilder`]" )] #![cfg_attr( - not(any(feature = "ring", feature = "openssl")), + not(any(feature = "ring", feature = "openssl", feature = "kmip")), doc = "`common::DigestBuilder`" )] //! type. @@ -129,6 +138,7 @@ pub mod common; pub mod kmip; +pub mod kmip_pool; pub mod openssl; pub mod ring; pub mod sign; diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index ba5920304..5cace2630 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -95,6 +95,9 @@ use super::openssl; #[cfg(feature = "ring")] use super::ring; +#[cfg(feature = "kmip")] +use super::kmip; + //----------- GenerateParams ------------------------------------------------- /// Parameters for generating a secret key. @@ -303,6 +306,9 @@ pub enum KeyPair { /// A key backed by OpenSSL. #[cfg(feature = "openssl")] OpenSSL(openssl::sign::KeyPair), + + /// A key backed by a KMIP capable HSM. + Kmip(kmip::sign::KeyPair), } //--- Conversion to and from bytes @@ -359,6 +365,8 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.algorithm(), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.algorithm(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.algorithm(), } } @@ -368,6 +376,8 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.dnskey(), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.dnskey(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.dnskey(), } } @@ -377,6 +387,8 @@ impl SignRaw for KeyPair { Self::Ring(key) => key.sign_raw(data), #[cfg(feature = "openssl")] Self::OpenSSL(key) => key.sign_raw(data), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.sign_raw(data), } } } @@ -933,6 +945,18 @@ impl From for GenerateError { } } +#[cfg(feature = "kmip")] +impl From for GenerateError { + fn from(value: kmip::GenerateError) -> Self { + match value { + kmip::GenerateError::UnsupportedAlgorithm => { + Self::UnsupportedAlgorithm + } + kmip::GenerateError::Implementation => Self::Implementation, + } + } +} + //--- Formatting impl fmt::Display for GenerateError { From 0f497108fb98e2e31fb3d1a18b64febbc1c5d1ba Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 6 Jun 2025 13:30:59 +0200 Subject: [PATCH 445/569] Delete trial creds. --- src/crypto/kmip.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 1f17c6d34..d5590b6cb 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -227,7 +227,7 @@ pub mod sign { // post-signing? Can we just specify the hashing to do when invoking // the Sign operation? // Fortanix DSM also doesn't support Cryptographic Parameters: - // Server error: Operation CreateKeyPair failed: Don't have handling for attribute Cryptographic Parameters + // Server error: Operation CreateKeyPair failed: Don't have handling for attribute Cryptographic Parameters // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that // don't support it we have to do a separate Activate operation after @@ -459,9 +459,8 @@ mod tests { let mut conn_settings = ConnectionSettings::default(); conn_settings.host = "eu.smartkey.io".to_string(); conn_settings.port = 5696; - conn_settings.username = - Some("2c79ae57-18a9-431a-baa1-0ef98cf88f45".to_string()); - conn_settings.password = Some("kbJcsgfVaiVOnldmyIXitwWnuIeEVHw9Jm0EoY3NA_qj-glVucT1sbRcSWGf3st7B8xWN-aKC4rmJ0gNmfQCgg".to_string()); + conn_settings.username = Some("*****".to_string()); + conn_settings.password = Some("*****".to_string()); eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( From bbd555f2dcaab3a2945cd017d21232c32f67fedf Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:13:38 +0200 Subject: [PATCH 446/569] WIP. --- src/crypto/kmip.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index d5590b6cb..877678f81 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -94,21 +94,13 @@ pub mod sign { let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { - // Hmmm, this is what we get with PyKMIP, rather than - // TransparentRSAPublicKey. The Dnskey we create using - // these octets doesn't seem to render the RDATA correctly - // so do we need to do something with these bytes? + // This is what we get with PyKMIP using RSASHA256 and + // Fortanix using ECDSAP256SHA256. The Dnskey we create + // using these octets doesn't seem to render the RDATA + // correctly so do we need to do something with these + // bytes? bytes } - KeyMaterial::TransparentRSAPublicKey(key) => { - rpki::crypto::keys::PublicKey::rsa_from_components( - &key.modulus, - &key.public_exponent, - ) - .unwrap() - .bits() - .to_vec() - } _ => todo!(), }; @@ -295,6 +287,13 @@ pub mod sign { // what value we should put here, but as Q length is // optional let's try not passing it. // Note: PyKMIP requires a length: use 256 from P-256? + // Note: Fortanix also requires a length and gives error + // "missing required field `elliptic_curve` in request + // body" if cryptographic length is not specified, and a + // value of 256 works fine while a value of 255 causes + // error "Unsupported length for ECC key". When using 256 + // the Fortanix UI shows the key as type EC with curve + // NistP256 so that seems good. common_attrs .push(request::Attribute::CryptographicLength(256)); } @@ -447,6 +446,7 @@ mod tests { } #[test] + #[ignore = "Requires Fortanix credentials"] fn fortanix_dsm_test() { // Note: keyls fails against Fortanix DSM for some reason with error: // Error: Server error: Operation Locate failed: expected @@ -459,8 +459,8 @@ mod tests { let mut conn_settings = ConnectionSettings::default(); conn_settings.host = "eu.smartkey.io".to_string(); conn_settings.port = 5696; - conn_settings.username = Some("*****".to_string()); - conn_settings.password = Some("*****".to_string()); + conn_settings.username = Some(env!("FORTANIX_USER").to_string()); + conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( From 612a125cbf88601ee227fe3b4db266a191e0582d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 10 Jun 2025 16:17:29 +0200 Subject: [PATCH 447/569] WIP. --- src/crypto/kmip.rs | 126 ++++++++++++++++++++++++++++++++++++++++++--- src/crypto/sign.rs | 1 + 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 877678f81..34bfd2586 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -43,8 +43,8 @@ pub mod sign { use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, - CryptographicUsageMask, DigitalSignatureAlgorithm, HashingAlgorithm, - KeyMaterial, ObjectType, + CryptographicUsageMask, Data, DigitalSignatureAlgorithm, + HashingAlgorithm, KeyMaterial, ObjectType, UniqueIdentifier, }; use kmip::types::request::{ self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, @@ -60,6 +60,8 @@ pub mod sign { }; use crate::rdata::Dnskey; + pub use kmip::client::ConnectionSettings; + #[derive(Clone, Debug)] pub struct KeyPair { /// The algorithm used by the key. @@ -74,6 +76,32 @@ pub mod sign { flags: u16, } + impl KeyPair { + pub fn new( + algorithm: SecurityAlgorithm, + flags: u16, + private_key_id: &str, + public_key_id: &str, + conn_pool: KmipConnPool, + ) -> Self { + Self { + algorithm, + private_key_id: private_key_id.to_string(), + public_key_id: public_key_id.to_string(), + conn_pool, + flags, + } + } + + pub fn private_key_id(&self) -> &str { + &self.private_key_id + } + + pub fn public_key_id(&self) -> &str { + &self.public_key_id + } + } + impl SignRaw for KeyPair { fn algorithm(&self) -> SecurityAlgorithm { self.algorithm @@ -110,7 +138,33 @@ pub mod sign { fn sign_raw(&self, data: &[u8]) -> Result { let client = self.conn_pool.get().map_err(|_| SignError)?; - let signed = client.sign(&self.private_key_id, data).unwrap(); + let (crypto_alg, hashing_alg) = match self.algorithm { + SecurityAlgorithm::RSASHA256 => { + (CryptographicAlgorithm::RSA, HashingAlgorithm::SHA256) + } + SecurityAlgorithm::ECDSAP256SHA256 => { + (CryptographicAlgorithm::ECDSA, HashingAlgorithm::SHA256) + } + _ => return Err(SignError), + }; + + let request = RequestPayload::Sign( + Some(UniqueIdentifier(self.private_key_id.clone())), + Some( + CryptographicParameters::default() + // .with_padding_method(PaddingMethod::) + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg), + ), + Data(data.to_vec()), + ); + + // Execute the request and capture the response + let res = client.do_request(request).unwrap(); + + let ResponsePayload::Sign(signed) = res else { + unreachable!(); + }; // TODO: For HSMs that don't support hashing do we have to do // hashing ourselves here after signing? Note: PyKMIP doesn't @@ -135,11 +189,16 @@ pub mod sign { ))) } SecurityAlgorithm::ECDSAP256SHA256 => { + let signature = openssl::ecdsa::EcdsaSig::from_der( + &signed.signature_data, + ) + .unwrap(); + let mut r = signature.r().to_vec_padded(32).unwrap(); + let mut s = signature.s().to_vec_padded(32).unwrap(); + r.append(&mut s); + Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( - signed - .signature_data - .try_into() - .map_err(|_| SignError)?, + r.try_into().map_err(|_| SignError)?, ))) } SecurityAlgorithm::ECDSAP384SHA384 => { @@ -376,6 +435,13 @@ mod tests { use crate::crypto::kmip_pool::ConnectionManager; use crate::crypto::sign::SignRaw; use crate::logging::init_logging; + use kmip::types::common::{ + CryptographicAlgorithm, CryptographicParameters, Data, + HashingAlgorithm, PaddingMethod, UniqueIdentifier, + }; + use kmip::types::request::RequestPayload; + use kmip::types::response::ResponsePayload; + use std::thread::sleep; use std::time::SystemTime; #[test] @@ -488,7 +554,7 @@ mod tests { ); let res = generate( generated_key_name, - //crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + // crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, pool, @@ -498,5 +564,49 @@ mod tests { let dnskey = key.dnskey(); eprintln!("DNSKEY: {}", dnskey); + + // Fortanix: Activating the public key also activates the private key. + // Attempting to then activate the private key fails as it is already + // active. Yet signing fails with "Object is not yet active"... + client.activate_key(key.public_key_id()).unwrap(); + // client.activate_key(key.private_key_id()).unwrap(); + + // This works round the not yet active yet error. + sleep(Duration::from_secs(5)); + + let request = RequestPayload::Sign( + Some(UniqueIdentifier(key.private_key_id().to_string())), + // While the KMIP 1.2 spec says crypto parameters are optional and + // if not specified those of the key will be used, Fortanix + // complains about "No cryptographic parameters specified" if this + // is None, and "Must specicify HashingAlgorithm" if that is not + // specified. + Some( + CryptographicParameters::default() + // .with_padding_method(PaddingMethod::) + .with_hashing_algorithm(HashingAlgorithm::SHA256) + .with_cryptographic_algorithm( + // CryptographicAlgorithm::RSA, + CryptographicAlgorithm::ECDSA, + ), + ), + Data("Message for ECDSA signing".as_bytes().to_vec()), + ); + + // Execute the request and capture the response + let response = client.do_request(request).unwrap(); + + let ResponsePayload::Sign(signed) = response else { + unreachable!(); + }; + + let signature = + openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) + .unwrap(); + + dbg!(signature.r().to_vec_padded(32)); + dbg!(signature.s().to_vec_padded(32)); + + // dbg!(response); } } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 5cace2630..7c32575e3 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -308,6 +308,7 @@ pub enum KeyPair { OpenSSL(openssl::sign::KeyPair), /// A key backed by a KMIP capable HSM. + #[cfg(feature = "kmip")] Kmip(kmip::sign::KeyPair), } From 76e1072892caaa349613c51f72429a837defa6b2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 10 Jun 2025 23:25:39 +0200 Subject: [PATCH 448/569] Decode the public key material as an ASN1. SubjectPublicKeyInfo data structure. --- src/crypto/kmip.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 34bfd2586..8249c0264 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -52,6 +52,7 @@ pub mod sign { }; use kmip::types::response::{ManagedObject, ResponsePayload}; use log::error; + use rpki::dep::bcder::decode::SliceSource; use crate::base::iana::SecurityAlgorithm; use crate::crypto::kmip_pool::KmipConnPool; @@ -123,11 +124,18 @@ pub mod sign { let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { // This is what we get with PyKMIP using RSASHA256 and - // Fortanix using ECDSAP256SHA256. The Dnskey we create - // using these octets doesn't seem to render the RDATA - // correctly so do we need to do something with these - // bytes? - bytes + // Fortanix using ECDSAP256SHA256. With Fortanix it + // appears to be a DER encoded SubjectPublicKeyInfo + // data structure of the form: + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + let source = SliceSource::new(&bytes); + let public_key = + rpki::crypto::PublicKey::decode(source).unwrap(); + public_key.bits().to_vec() } _ => todo!(), }; From e60197cf4e18f2b4afaeb7c9c7b1c64cf62bc19a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 11 Jun 2025 09:38:26 +0200 Subject: [PATCH 449/569] Add note about self hashing. --- src/crypto/kmip.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 8249c0264..2ddf7224e 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -156,6 +156,14 @@ pub mod sign { _ => return Err(SignError), }; + // OpenDNSSEC does its own hashing. Trying to do SHA256 hashing + // ourselves and then not passing a hashing algorithm to the Sign + // operation below results (with Fortanix at least) in error "Must + // specify HashingAlgorithm". + // let mut ctx = crate::crypto::common::DigestBuilder::new(crate::crypto::common::DigestType::Sha256); + // ctx.update(&data); + // let data = ctx.finish(); + let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some( @@ -164,7 +172,7 @@ pub mod sign { .with_hashing_algorithm(hashing_alg) .with_cryptographic_algorithm(crypto_alg), ), - Data(data.to_vec()), + Data(data.as_ref().to_vec()), ); // Execute the request and capture the response From 661e8991ee65eedc52b95839d9dece5e7e202872 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 19 Jun 2025 12:36:11 +0200 Subject: [PATCH 450/569] Fix incorrect fn arg name. --- src/crypto/kmip_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index c973dd273..7ecf7adbc 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -34,7 +34,7 @@ impl ConnectionManager { #[rustfmt::skip] pub fn create_connection_pool( conn_settings: Arc, - max_response_bytes: u32, + max_conncurrent_connections: u32, max_life_time: Duration, max_idle_time: Duration, ) -> Result { @@ -46,7 +46,7 @@ impl ConnectionManager { .min_idle(Some(0)) // Create at most this many concurrent connections to the KMIP server - .max_size(max_response_bytes) + .max_size(max_conncurrent_connections) // Don't verify that a connection is usable when fetching it from the pool (as doing so requires sending a // request to the server and we might as well just try the actual request that we want the connection for) From 5f26ed621932e72e37e133e8f4d5c8c97c6c8bba Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:59:27 +0200 Subject: [PATCH 451/569] Add a comment about how to enable more logging. --- src/logging.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/logging.rs b/src/logging.rs index 1c1e7c32f..c0af5ac2e 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -16,6 +16,8 @@ pub fn init_logging() { .with_env_filter(EnvFilter::from_default_env()) .with_thread_ids(true) .without_time() + // Useful sometimes: + // .with_span_events(tracing_subscriber::fmt::format::FmtSpan::NEW) .try_init() .ok(); } From b7ed2b401a550d07e4c493c14277d17cfb9d5867 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:00:05 +0200 Subject: [PATCH 452/569] Incorporate pending PR #541. --- src/crypto/common.rs | 48 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/src/crypto/common.rs b/src/crypto/common.rs index 8b3cd4d27..bcfe27fff 100644 --- a/src/crypto/common.rs +++ b/src/crypto/common.rs @@ -209,10 +209,22 @@ pub fn rsa_exponent_modulus( } /// Encode the RSA exponent and modulus components in DNSKEY record data -/// format. -pub fn rsa_encode(e: &[u8], n: &[u8]) -> Vec { +/// format. Leading zeroes will be ignored per RFC 3110 section 2. +pub fn rsa_encode(mut e: &[u8], mut n: &[u8]) -> Vec { + fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] { + bytes + .iter() + .position(|&v| v != 0) + .map(|idx| &bytes[idx..]) + .unwrap_or_default() + } + let mut key = Vec::new(); + // Trim leading zeroes. + e = trim_leading_zeroes(e); + n = trim_leading_zeroes(n); + // Encode the exponent length. if let Ok(exp_len) = u8::try_from(e.len()) { key.reserve_exact(1 + e.len() + n.len()); @@ -288,3 +300,35 @@ impl fmt::Display for FromDnskeyError { } impl error::Error for FromDnskeyError {} + +//----------- Tests ---------------------------------------------------------- + +#[cfg(test)] +mod tests { + use crate::crypto::common::rsa_encode; + + #[test] + fn test_rsa_encode() { + assert_eq!(rsa_encode(&[], &[]), &[0]); + + assert_eq!(rsa_encode(&[1], &[]), &[1, 1]); + assert_eq!(rsa_encode(&[], &[1]), &[0, 1]); + assert_eq!(rsa_encode(&[1], &[1]), &[1, 1, 1]); + + assert_eq!(rsa_encode(&[0, 1], &[1]), &[1, 1, 1]); + assert_eq!(rsa_encode(&[1], &[0, 1]), &[1, 1, 1]); + assert_eq!(rsa_encode(&[0, 1], &[0, 1]), &[1, 1, 1]); + + assert_eq!(rsa_encode(&[0, 0, 1], &[0, 1]), &[1, 1, 1]); + assert_eq!(rsa_encode(&[0, 1], &[0, 0, 1]), &[1, 1, 1]); + assert_eq!(rsa_encode(&[0, 1, 1], &[0, 0, 1]), &[2, 1, 1, 1]); + + assert_eq!(rsa_encode(&[1, 2], &[]), &[2, 1, 2]); + assert_eq!(rsa_encode(&[], &[1, 2]), &[0, 1, 2]); + assert_eq!(rsa_encode(&[1, 2], &[1, 2]), &[2, 1, 2, 1, 2]); + + assert_eq!(rsa_encode(&[0, 1, 2], &[1]), &[2, 1, 2, 1]); + assert_eq!(rsa_encode(&[1], &[0, 1, 2]), &[1, 1, 1, 2]); + assert_eq!(rsa_encode(&[0, 1, 2], &[0, 1, 2]), &[2, 1, 2, 1, 2]); + } +} From 67aad1ebf7737e0a5f35a522a929dd731cf03e42 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 20 Jun 2025 14:00:25 +0200 Subject: [PATCH 453/569] WIP: More KMIP support. --- Cargo.lock | 12 +- Cargo.toml | 9 +- src/crypto/kmip.rs | 372 +++++++++++++++++++++++++++++++++------------ 3 files changed, 283 insertions(+), 110 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e515305f7..58886e3b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,6 +257,7 @@ version = "0.11.1-dev" dependencies = [ "arbitrary", "arc-swap", + "bcder", "bumpalo", "bytes", "chrono", @@ -626,12 +627,13 @@ dependencies = [ [[package]] name = "kmip-protocol" -version = "0.4.4-dev" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=add-ecdsa-support#e493155e9fee257e71d7630f89f432089e97dbb4" +version = "0.5.0" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#e87e4422d117ad298360204ef61564b3a6ade3c2" dependencies = [ "cfg-if", "enum-display-derive", "enum-flags", + "hex", "kmip-ttlv", "log", "maybe-async", @@ -645,15 +647,15 @@ dependencies = [ [[package]] name = "kmip-ttlv" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13cdaafff68ae98da73fd6dff927095849646c6eeee44bdd0a983d30192cdeb1" +version = "0.4.0" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#680a9d139ca88765220ab98a96e6d52646b0551a" dependencies = [ "cfg-if", "hex", "maybe-async", "rustc_version", "serde", + "tracing", "trait-set", ] diff --git a/Cargo.toml b/Cargo.toml index ec8173138..412cd3dba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,7 @@ chrono = { version = "0.4.35", optional = true, default-features = false futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.14.2", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing heapless = { version = "0.8", optional = true } -kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "add-ecdsa-support", package = "kmip-protocol", version = "0.4.4-dev", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead -#kmip = { version = "0.4.3", package = "kmip-protocol", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead -#kmip = { path = "../kmip-protocol2", package = "kmip-protocol", version = "0.4.4-dev", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead -#kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead -#kmip = { path = "../kmip-protocol", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } @@ -46,6 +42,7 @@ openssl = { version = "0.10.72", optional = true } # 0.10.70 upgrades to proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } +bcder = { version = "0.7", optional = true } rpki = { version = "0.18", optional = true, features = ["crypto"] } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } @@ -74,7 +71,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:rpki"] +kmip = ["dep:kmip", "dep:r2d2", "dep:rpki", "dep:bcder"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 2ddf7224e..da1ac708d 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -44,7 +44,8 @@ pub mod sign { use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, DigitalSignatureAlgorithm, - HashingAlgorithm, KeyMaterial, ObjectType, UniqueIdentifier, + HashingAlgorithm, KeyMaterial, ObjectType, TransparentRSAPublicKey, + UniqueIdentifier, }; use kmip::types::request::{ self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, @@ -61,6 +62,7 @@ pub mod sign { }; use crate::rdata::Dnskey; + use crate::crypto::common::{rsa_encode, DigestBuilder, DigestType}; pub use kmip::client::ConnectionSettings; #[derive(Clone, Debug)] @@ -109,9 +111,61 @@ pub mod sign { } fn dnskey(&self) -> Dnskey> { + // https://datatracker.ietf.org/doc/html/rfc5702#section-2 + // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource + // Records for DNSSEC + // 2. DNSKEY Resource Records + // "The format of the DNSKEY RR can be found in [RFC4034]. + // [RFC3110] describes the use of RSA/SHA-1 for DNSSEC + // signatures." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 + // Resource Records for the DNS Security Extensions + // 2. The DNSKEY Resource Record + // 2.1.4. The Public Key Field + // "The Public Key Field holds the public key material. The + // format depends on the algorithm of the key being stored and + // is described in separate documents." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc3110#section-2 + // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) + // 2. RSA Public KEY Resource Records + // "... The structure of the algorithm specific portion of the + // RDATA part of such RRs is as shown below. + // + // Field Size + // ----- ---- + // exponent length 1 or 3 octets (see text) + // exponent as specified by length field + // modulus remaining space + // + // For interoperability, the exponent and modulus are each limited + // to 4096 bits in length. The public key exponent is a variable + // length unsigned integer. Its length in octets is represented + // as one octet if it is in the range of 1 to 255 and by a zero + // octet followed by a two octet unsigned length if it is longer + // than 255 bytes. The public key modulus field is a + // multiprecision unsigned integer. The length of the modulus can + // be determined from the RDLENGTH and the preceding RDATA fields + // including the exponent. Leading zero octets are prohibited in + // the exponent and modulus. + let client = self.conn_pool.get().unwrap(); - let res = client.get_key(&self.public_key_id).unwrap(); + // Note: OpenDNSSEC queries the public key ID, _unless_ it was + // configured not to store the public key in the HSM (by setting + // CKA_TOKEN false) in which case there is no public key and so it + // uses the private key object handle instead. + let res = client + .get_key(&self.public_key_id) + .inspect_err(|err| error!("{err}")) + .unwrap(); + + dbg!(&res); assert_eq!(res.object_type, ObjectType::PublicKey); @@ -121,6 +175,15 @@ pub mod sign { todo!(); }; + // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 + // "“Raw” key format is intended to be applied to symmetric keys + // and not asymmetric keys" + // + // As we deal in assymetric keys (RSA, ECDSA), not symmetric keys, + // we should not encounter public_key.key_block.key_format_type == + // KeyFormatType::Raw. However, Fortanix DSM returns + // KeyFormatType::Raw when fetching key data for an ECDSA public key. + let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { // This is what we get with PyKMIP using RSASHA256 and @@ -135,8 +198,42 @@ pub mod sign { let source = SliceSource::new(&bytes); let public_key = rpki::crypto::PublicKey::decode(source).unwrap(); - public_key.bits().to_vec() + let bits = public_key.bits().to_vec(); + + // For RSA, the bits are also DER encoded of the form: + // RSAPrivateKey SEQUENCE (2 elem) + // version Version INTEGER (1024 bit) 140670898145304244147145320460151523064481569650486421654946000437850… + // modulus INTEGER 65537 + // + // or is it really: + // RSAPrivateKey SEQUENCE (2 elem) + // modulus INTEGER + // publicExponent INTEGER + + // if public_key.algorithm() == PublicKeyFormat::Rsa { + // let source = SliceSource::new(&bits); + // let mut modulus = vec![]; + // let mut public_exponent = vec![]; + // bcder::Mode::Der.decode(source, |cons| { + // cons.take_sequence(|cons| { + // modulus = bcder::string::BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); + // public_exponent = BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); + // Ok(()) + // }) + // }).unwrap(); + // rsa_encode(&public_exponent, &modulus) + // } else { + bits + // } } + + KeyMaterial::TransparentRSAPublicKey( + TransparentRSAPublicKey { + modulus, + public_exponent, + }, + ) => rsa_encode(&public_exponent, &modulus), + _ => todo!(), }; @@ -144,26 +241,91 @@ pub mod sign { } fn sign_raw(&self, data: &[u8]) -> Result { - let client = self.conn_pool.get().map_err(|_| SignError)?; - - let (crypto_alg, hashing_alg) = match self.algorithm { - SecurityAlgorithm::RSASHA256 => { - (CryptographicAlgorithm::RSA, HashingAlgorithm::SHA256) - } - SecurityAlgorithm::ECDSAP256SHA256 => { - (CryptographicAlgorithm::ECDSA, HashingAlgorithm::SHA256) - } + // https://www.rfc-editor.org/rfc/rfc5702.html#section-3 + // 3. RRSIG Resource Records + // "The value of the signature field in the RRSIG RR follows the + // RSASSA- PKCS1-v1_5 signature scheme and is calculated as + // follows." + // ... + // hash = SHA-XXX(data) + // + // Here XXX is either 256 or 512, depending on the algorithm used, as + // specified in FIPS PUB 180-3; "data" is the wire format data of the + // resource record set that is signed, as specified in [RFC4034]. + // + // signature = ( 00 | 01 | FF* | 00 | prefix | hash ) ** e (mod n)" + // ... + // + // 3.1. RSA/SHA-256 RRSIG Resource Records + // "RSA/SHA-256 signatures are stored in the DNS using RRSIG resource + // records (RRs) with algorithm number 8. + // + // The prefix is the ASN.1 DER SHA-256 algorithm designator prefix, as + // specified in PKCS #1 v2.1 [RFC3447]: + // + // hex 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20" + // + // We assume that the HSM signing operation implements this signing + // operation according to these rules. + + eprintln!("Signing1"); + let (crypto_alg, hashing_alg, digest_type) = match self.algorithm + { + SecurityAlgorithm::RSASHA256 => ( + CryptographicAlgorithm::RSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), + SecurityAlgorithm::ECDSAP256SHA256 => ( + CryptographicAlgorithm::ECDSA, + HashingAlgorithm::SHA256, + DigestType::Sha256, + ), _ => return Err(SignError), }; - // OpenDNSSEC does its own hashing. Trying to do SHA256 hashing - // ourselves and then not passing a hashing algorithm to the Sign - // operation below results (with Fortanix at least) in error "Must - // specify HashingAlgorithm". - // let mut ctx = crate::crypto::common::DigestBuilder::new(crate::crypto::common::DigestType::Sha256); - // ctx.update(&data); - // let data = ctx.finish(); + eprintln!("Signing2"); + // TODO: For HSMs that don't support hashing do we have to do + // hashing ourselves here after signing? Note: PyKMIP doesn't + // support CryptographicParameters (and thus also not + // HashingFunction) nor does it support the Hash operation. + // Maybe via crypto::common::DigestBuilder? + // + // TODO: Where do we find out what the HSM supports? Trying an + // operation then falling back each time it fails is inefficient. + // We can presumably instead discover this on first use of the + // HSM, ala how Krill does HSM probing. We would need to know the + // result of such probing, which features are supported, here. We + // only have access to the KMIP connection pool here, so I guess + // that has to be able to tell us what we want to know. + // + // Note: OpenDNSSEC does its own hashing. Trying to do SHA256 + // hashing ourselves and then not passing a hashing algorithm to + // the Sign operation below results (with Fortanix at least) in + // error "Must specify HashingAlgorithm". OpenDNSSEC code comments + // say this is done because "some HSMs don't really handle + // CKM_SHA1_RSA_PKCS well". + let mut ctx = DigestBuilder::new(digest_type); + ctx.update(data); + let digest = ctx.finish(); + let mut data = digest.as_ref(); + + // OpenDNSSEC says that for RSA the prefix must be added to the + // buffer manually first as "CKM_RSA_PKCS does the padding, but + // cannot know the identifier prefix, so we need to add that + // ourselves." + let mut new_data; + if matches!(self.algorithm, SecurityAlgorithm::RSASHA256) { + new_data = vec![ + 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, + 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, + 0x20, + ]; + new_data.extend_from_slice(data); + data = &new_data; + } + eprintln!("Signing3: digest len={}", data.as_ref().len()); let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some( @@ -176,33 +338,28 @@ pub mod sign { ); // Execute the request and capture the response + let client = self.conn_pool.get().map_err(|_| SignError)?; let res = client.do_request(request).unwrap(); + eprintln!("Signing4"); let ResponsePayload::Sign(signed) = res else { unreachable!(); }; - // TODO: For HSMs that don't support hashing do we have to do - // hashing ourselves here after signing? Note: PyKMIP doesn't - // support CryptographicParameters (and thus also not - // HashingFunction) nor does it support the Hash operation. - // Maybe via crypto::common::DigestBuilder? - - // TODO: Where do we find out what the HSM supports? Trying an - // operation then falling back each time it fails is inefficient. - // We can presumably instead discover this on first use of the - // HSM, ala how Krill does HSM probing. We would need to know the - // result of such probing, which features are supported, here. We - // only have access to the KMIP connection pool here, so I guess - // that has to be able to tell us what we want to know. + eprintln!("Signing5: len={}", signed.signature_data.len()); match self.algorithm { SecurityAlgorithm::RSASHA256 => { - Ok(Signature::RsaSha256(Box::<[u8; 64]>::new( - signed - .signature_data - .try_into() - .map_err(|_| SignError)?, - ))) + eprintln!("Signing6"); + // Ok(Signature::RsaSha256(Box::<[u8; 64]>::new( + // signed + // .signature_data + // .into_boxed_slice() + // .inspect_err(|err| eprintln!("Signing7: Error")) + // .map_err(|_| SignError)?, + // ))) + Ok(Signature::RsaSha256( + signed.signature_data.into_boxed_slice(), + )) } SecurityAlgorithm::ECDSAP256SHA256 => { let signature = openssl::ecdsa::EcdsaSig::from_der( @@ -307,8 +464,12 @@ pub mod sign { // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 // "For RSA, Cryptographic Length corresponds to the bit length of the Modulus" - // RFC 5702 2.1 - if bits < 512 || bits > 4096 { + // https://www.rfc-editor.org/rfc/rfc5702.html#section-2.1 + // 2.1. RSA/SHA-256 DNSKEY Resource Records + // "For interoperability, as in [RFC3110], the key size of + // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST + // NOT be more than 4096 bits." + if !(512..=4096).contains(&bits) { return Err(GenerateError::UnsupportedAlgorithm); } @@ -401,9 +562,9 @@ pub mod sign { } let request = RequestPayload::CreateKeyPair( - Some(CommonTemplateAttribute::unnamed(common_attrs)), - Some(PrivateKeyTemplateAttribute::unnamed(priv_key_attrs)), - Some(PublicKeyTemplateAttribute::unnamed(pub_key_attrs)), + Some(CommonTemplateAttribute::new(common_attrs)), + Some(PrivateKeyTemplateAttribute::new(priv_key_attrs)), + Some(PublicKeyTemplateAttribute::new(pub_key_attrs)), ); // Execute the request and capture the response @@ -453,7 +614,7 @@ mod tests { use crate::logging::init_logging; use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, Data, - HashingAlgorithm, PaddingMethod, UniqueIdentifier, + HashingAlgorithm, UniqueIdentifier, }; use kmip::types::request::RequestPayload; use kmip::types::response::ResponsePayload; @@ -461,7 +622,8 @@ mod tests { use std::time::SystemTime; #[test] - fn connect() { + #[ignore = "Requires running PyKMIP"] + fn pykmip_connect() { init_logging(); let mut cert_bytes = Vec::new(); let file = File::open( @@ -539,10 +701,17 @@ mod tests { init_logging(); let mut conn_settings = ConnectionSettings::default(); - conn_settings.host = "eu.smartkey.io".to_string(); - conn_settings.port = 5696; - conn_settings.username = Some(env!("FORTANIX_USER").to_string()); - conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); + // conn_settings.host = "eu.smartkey.io".to_string(); + // conn_settings.port = 5696; + // conn_settings.username = Some(env!("FORTANIX_USER").to_string()); + // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); + + conn_settings.host = "127.0.0.1".to_string(); //"eu.smartkey.io".to_string(); + conn_settings.port = 1818; //5696; + conn_settings.insecure = true; // When connecting to kmip2pkcs11 + conn_settings.connect_timeout = Some(Duration::from_secs(3)); + conn_settings.read_timeout = Some(Duration::from_secs(30)); + conn_settings.write_timeout = Some(Duration::from_secs(3)); eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( @@ -557,9 +726,9 @@ mod tests { let client = pool.get().unwrap(); eprintln!("Connected"); - let res = client.query(); - dbg!(&res); - res.unwrap(); + // let res = client.query(); + // dbg!(&res); + // res.unwrap(); let generated_key_name = format!( "{}", @@ -570,59 +739,64 @@ mod tests { ); let res = generate( generated_key_name, - // crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, - crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, pool, ); - dbg!(&res); let key = res.unwrap(); + eprintln!("Generated public key with id: {}", key.public_key_id()); + eprintln!("Generated private key with id: {}", key.private_key_id()); - let dnskey = key.dnskey(); - eprintln!("DNSKEY: {}", dnskey); - - // Fortanix: Activating the public key also activates the private key. - // Attempting to then activate the private key fails as it is already - // active. Yet signing fails with "Object is not yet active"... - client.activate_key(key.public_key_id()).unwrap(); - // client.activate_key(key.private_key_id()).unwrap(); - - // This works round the not yet active yet error. - sleep(Duration::from_secs(5)); - - let request = RequestPayload::Sign( - Some(UniqueIdentifier(key.private_key_id().to_string())), - // While the KMIP 1.2 spec says crypto parameters are optional and - // if not specified those of the key will be used, Fortanix - // complains about "No cryptographic parameters specified" if this - // is None, and "Must specicify HashingAlgorithm" if that is not - // specified. - Some( - CryptographicParameters::default() - // .with_padding_method(PaddingMethod::) - .with_hashing_algorithm(HashingAlgorithm::SHA256) - .with_cryptographic_algorithm( - // CryptographicAlgorithm::RSA, - CryptographicAlgorithm::ECDSA, - ), - ), - Data("Message for ECDSA signing".as_bytes().to_vec()), - ); - - // Execute the request and capture the response - let response = client.do_request(request).unwrap(); - - let ResponsePayload::Sign(signed) = response else { - unreachable!(); - }; - - let signature = - openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) - .unwrap(); + // sleep(Duration::from_secs(5)); - dbg!(signature.r().to_vec_padded(32)); - dbg!(signature.s().to_vec_padded(32)); + // let dnskey = key.dnskey(); + // eprintln!("DNSKEY: {}", dnskey); - // dbg!(response); + // // Fortanix: Activating the public key also activates the private key. + // // Attempting to then activate the private key fails as it is already + // // active. Yet signing fails with "Object is not yet active"... + client.activate_key(key.public_key_id()).unwrap(); + // // client.activate_key(key.private_key_id()).unwrap(); + + // // This works round the not yet active yet error. + // sleep(Duration::from_secs(5)); + + // let request = RequestPayload::Sign( + // Some(UniqueIdentifier(key.private_key_id().to_string())), + // // While the KMIP 1.2 spec says crypto parameters are optional and + // // if not specified those of the key will be used, Fortanix + // // complains about "No cryptographic parameters specified" if this + // // is None, and "Must specicify HashingAlgorithm" if that is not + // // specified. + // Some( + // CryptographicParameters::default() + // // .with_padding_method(PaddingMethod::) + // .with_hashing_algorithm(HashingAlgorithm::SHA256) + // .with_cryptographic_algorithm( + // CryptographicAlgorithm::RSA, + // //CryptographicAlgorithm::ECDSA, + // ), + // ), + // Data("Message for ECDSA signing".as_bytes().to_vec()), + // ); + + // // Execute the request and capture the response + // let res = client.do_request(request).unwrap(); + + // dbg!(&res); + + // let ResponsePayload::Sign(signed) = res else { + // unreachable!(); + // }; + + // // let signature = + // // openssl::ecdsa::EcdsaSig::from_der(&signed.signature_data) + // // .unwrap(); + + // // dbg!(signature.r().to_vec_padded(32)); + // // dbg!(signature.s().to_vec_padded(32)); + + // // dbg!(response); } } From 422be4cc11219c754cfc90cb6f84f994f3e560b3 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 23 Jun 2025 11:02:15 +0200 Subject: [PATCH 454/569] Add to_system_time. --- src/rdata/dnssec.rs | 88 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 68470c824..8b1958864 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -28,6 +28,9 @@ use octseq::parse::Parser; use octseq::serde::{DeserializeOctets, SerializeOctets}; #[cfg(feature = "std")] use std::vec::Vec; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; +use std::time::Duration; use time::{Date, Month, PrimitiveDateTime, Time}; //------------ Dnskey -------------------------------------------------------- @@ -700,6 +703,46 @@ impl Timestamp { pub fn into_int(self) -> u32 { self.0.into_int() } + + /// Return a SystemTime that has to meet two requirements: + /// 1) The SystemTime value has a duration since UNIX_EPOCH that + /// modulo 2**32 is equal to our value. + /// 2) The time difference between the SystemTime value and the + /// reference fits within an i32. + #[must_use] + pub fn to_system_time(&self, reference: SystemTime) -> SystemTime { + // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because + // the timestamp may be too far in the future. We may have to add + // n * 2**32 for some unknown value of n. + const POW_2_32: u64 = 0x1_0000_0000; + let ref_secs = reference.duration_since(UNIX_EPOCH).unwrap().as_secs(); + let k = ref_secs / POW_2_32; + let ref_secs_mod = ref_secs % POW_2_32; + let ts_secs = self.into_int() as u64; + let ts_secs = + if ts_secs < ref_secs_mod { + if ref_secs_mod - ts_secs <= POW_2_32/2 { + // Close enough, use k. + ts_secs + k*POW_2_32 + } + else { + // ts_secs is really beyond ref_secs, use k+1. + ts_secs + (k+1)*POW_2_32 + } + } else { // ts_secs >= ref_secs_mod + if ts_secs - ref_secs_mod < POW_2_32/2 { + // Close enough, use k. + ts_secs + k*POW_2_32 + } + else { + // ts_secs is really old than ref_secs. Try to use k-1 + // but only if k is not zero. + let k = if k > 0 { k-1 } else { k }; + ts_secs + k*POW_2_32 + } + }; + UNIX_EPOCH + Duration::from_secs(ts_secs) + } } /// # Parsing and Composing @@ -2976,4 +3019,49 @@ mod test { assert!(dnskey.is_secure_entry_point()); assert!(!dnskey.is_revoked()); } + + #[test] + fn timestamp_to_system_time() { + struct Params { + ts: u32, + ref_ts: u64, + res: u64 + } + let tests = vec![ + // Simple cases, ts and ref_ts mod 2**32 are within 2*31-1. + // First ts less than ref_ts mod 2**32. + Params { ts: 0x0000_0000, ref_ts: 0x1_7fff_ffff, res: 0x1_0000_0000 }, + Params { ts: 0x7fff_ffff, ref_ts: 0x1_8000_0000, res: 0x1_7fff_ffff }, + Params { ts: 0x8000_0000, ref_ts: 0x1_ffff_ffff, res: 0x1_8000_0000 }, + // Then ts larger than ref_ts mod 2**32. + Params { ts: 0x7fff_ffff, ref_ts: 0x1_0000_0000, res: 0x1_7fff_ffff }, + Params { ts: 0x8000_0000, ref_ts: 0x1_7fff_ffff, res: 0x1_8000_0000 }, + Params { ts: 0xffff_ffff, ref_ts: 0x1_8000_0000, res: 0x1_ffff_ffff }, + + // Next, cases where the difference between ts and ref_ts mod 2**32 + // are at least 2**31+1. + Params { ts: 0x0000_0000, ref_ts: 0x1_8000_0001, res: 0x2_0000_0000 }, + Params { ts: 0x7fff_fffe, ref_ts: 0x1_ffff_ffff, res: 0x2_7fff_fffe }, + Params { ts: 0x8000_0001, ref_ts: 0x1_0000_0000, res: 0x0_8000_0001 }, + Params { ts: 0xffff_ffff, ref_ts: 0x1_7fff_fffe, res: 0x0_ffff_ffff }, + // Test cases where the difference is exactly 2**31. + Params { ts: 0x0000_0000, ref_ts: 0x1_8000_0000, res: 0x1_0000_0000 }, + Params { ts: 0x7fff_ffff, ref_ts: 0x1_ffff_ffff, res: 0x1_7fff_ffff }, + Params { ts: 0x8000_0000, ref_ts: 0x1_0000_0000, res: 0x0_8000_0000 }, + Params { ts: 0xffff_ffff, ref_ts: 0x1_7fff_ffff, res: 0x0_ffff_ffff }, + // Special case: ERA 0. We don't want values before UNIX_EPOCH. + Params { ts: 0x8000_0001, ref_ts: 0x0_0000_0000, res: 0x0_8000_0001 }, + Params { ts: 0xffff_ffff, ref_ts: 0x0_7fff_fffe, res: 0x0_ffff_ffff }, + Params { ts: 0x8000_0000, ref_ts: 0x0_0000_0000, res: 0x0_8000_0000 }, + Params { ts: 0xffff_ffff, ref_ts: 0x0_7fff_ffff, res: 0x0_ffff_ffff }, + ]; + + for t in tests { + let ts = Timestamp(Serial(t.ts)); + let ref_ts = UNIX_EPOCH + Duration::from_secs(t.ref_ts); + let res = ts.to_system_time(ref_ts); + let res = res.duration_since(UNIX_EPOCH).unwrap().as_secs(); + assert_eq!(res, t.res); + } + } } From e2540cd221711f17d9523eca64135213268e9fb1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:33:53 +0200 Subject: [PATCH 455/569] Add a KMIP PublicKey type ala how it is done for OpenSSL and Ring. --- Cargo.lock | 4 +- src/crypto/kmip.rs | 358 +++++++++++++++++++++++++++------------------ 2 files changed, 216 insertions(+), 146 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 54ad77c61..d54013241 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#e87e4422d117ad298360204ef61564b3a6ade3c2" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#de0c8b2d96cf8b58ca155d727bbd1686f0a66a48" dependencies = [ "cfg-if", "enum-display-derive", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "kmip-ttlv" version = "0.4.0" -source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#680a9d139ca88765220ab98a96e6d52646b0551a" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#d626d4c549eddeeb1064a6e8213b65707072fb11" dependencies = [ "cfg-if", "hex", diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index da1ac708d..a7dd026f2 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -7,6 +7,23 @@ use core::fmt; +use std::{string::String, vec::Vec}; + +use bcder::decode::SliceSource; +use kmip::types::{ + common::{KeyMaterial, TransparentRSAPublicKey}, + response::ManagedObject, +}; +use log::error; + +use crate::{ + base::iana::SecurityAlgorithm, + crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, + rdata::Dnskey, +}; + +pub use kmip::client::ConnectionSettings; + /// An error in generating a key pair with OpenSSL. #[derive(Clone, Debug)] pub enum GenerateError { @@ -34,6 +51,176 @@ impl fmt::Display for GenerateError { impl std::error::Error for GenerateError {} +pub struct PublicKey { + algorithm: SecurityAlgorithm, + + public_key_id: String, + + conn_pool: KmipConnPool, +} + +impl PublicKey { + pub fn new( + public_key_id: String, + algorithm: SecurityAlgorithm, + conn_pool: KmipConnPool, + ) -> Self { + Self { + public_key_id, + algorithm, + conn_pool, + } + } + + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + pub fn dnskey( + &self, + flags: u16, + ) -> Result>, kmip::client::Error> { + // https://datatracker.ietf.org/doc/html/rfc5702#section-2 + // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource + // Records for DNSSEC + // 2. DNSKEY Resource Records + // "The format of the DNSKEY RR can be found in [RFC4034]. + // [RFC3110] describes the use of RSA/SHA-1 for DNSSEC + // signatures." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 + // Resource Records for the DNS Security Extensions + // 2. The DNSKEY Resource Record + // 2.1.4. The Public Key Field + // "The Public Key Field holds the public key material. The + // format depends on the algorithm of the key being stored and + // is described in separate documents." + // | + // | + // v + // https://datatracker.ietf.org/doc/html/rfc3110#section-2 + // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) + // 2. RSA Public KEY Resource Records + // "... The structure of the algorithm specific portion of the + // RDATA part of such RRs is as shown below. + // + // Field Size + // ----- ---- + // exponent length 1 or 3 octets (see text) + // exponent as specified by length field + // modulus remaining space + // + // For interoperability, the exponent and modulus are each limited + // to 4096 bits in length. The public key exponent is a variable + // length unsigned integer. Its length in octets is represented + // as one octet if it is in the range of 1 to 255 and by a zero + // octet followed by a two octet unsigned length if it is longer + // than 255 bytes. The public key modulus field is a + // multiprecision unsigned integer. The length of the modulus can + // be determined from the RDLENGTH and the preceding RDATA fields + // including the exponent. Leading zero octets are prohibited in + // the exponent and modulus. + + let client = self.conn_pool.get().inspect_err(|err| error!("{err}")).map_err(|err| { + kmip::client::Error::ServerError(format!( + "Error while attempting to acquire KMIP connection from pool: {err}" + )) + })?; + + // Note: OpenDNSSEC queries the public key ID, _unless_ it was + // configured not to store the public key in the HSM (by setting + // CKA_TOKEN false) in which case there is no public key and so it + // uses the private key object handle instead. + let res = client + .get_key(&self.public_key_id) + .inspect_err(|err| error!("{err}"))?; + + let ManagedObject::PublicKey(public_key) = res.cryptographic_object + else { + return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object))); + }; + + // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 + // "“Raw” key format is intended to be applied to symmetric keys + // and not asymmetric keys" + // + // As we deal in assymetric keys (RSA, ECDSA), not symmetric keys, + // we should not encounter public_key.key_block.key_format_type == + // KeyFormatType::Raw. However, Fortanix DSM returns + // KeyFormatType::Raw when fetching key data for an ECDSA public key. + + // TODO: SAFETY + // TODO: We don't know that these lengths are correct, consult cryptographic_length() too? + let algorithm = + match public_key.key_block.cryptographic_algorithm.unwrap() { + kmip::types::common::CryptographicAlgorithm::RSA => { + SecurityAlgorithm::RSASHA256 + } + kmip::types::common::CryptographicAlgorithm::ECDSA => { + SecurityAlgorithm::ECDSAP256SHA256 + } + alg => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported cryptographic algorithm type: {alg}"))), + }; + + let octets = match public_key.key_block.key_value.key_material { + KeyMaterial::Bytes(bytes) => { + // This is what we get with PyKMIP using RSASHA256 and + // Fortanix using ECDSAP256SHA256. With Fortanix it + // appears to be a DER encoded SubjectPublicKeyInfo + // data structure of the form: + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + let source = SliceSource::new(&bytes); + let public_key = + rpki::crypto::PublicKey::decode(source).unwrap(); + let bits = public_key.bits().to_vec(); + + // For RSA, the bits are also DER encoded of the form: + // RSAPrivateKey SEQUENCE (2 elem) + // version Version INTEGER (1024 bit) 140670898145304244147145320460151523064481569650486421654946000437850… + // modulus INTEGER 65537 + // + // or is it really: + // RSAPrivateKey SEQUENCE (2 elem) + // modulus INTEGER + // publicExponent INTEGER + + // if public_key.algorithm() == PublicKeyFormat::Rsa { + // let source = SliceSource::new(&bits); + // let mut modulus = vec![]; + // let mut public_exponent = vec![]; + // bcder::Mode::Der.decode(source, |cons| { + // cons.take_sequence(|cons| { + // modulus = bcder::string::BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); + // public_exponent = BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); + // Ok(()) + // }) + // }).unwrap(); + // rsa_encode(&public_exponent, &modulus) + // } else { + bits + // } + } + + KeyMaterial::TransparentRSAPublicKey( + TransparentRSAPublicKey { + modulus, + public_exponent, + }, + ) => rsa_encode(&public_exponent, &modulus), + + mat => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported key material type: {mat}"))), + }; + + Ok(Dnskey::new(flags, 3, algorithm, octets).unwrap()) + } +} + #[cfg(feature = "unstable-crypto-sign")] pub mod sign { use std::boxed::Box; @@ -44,27 +231,24 @@ pub mod sign { use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, DigitalSignatureAlgorithm, - HashingAlgorithm, KeyMaterial, ObjectType, TransparentRSAPublicKey, - UniqueIdentifier, + HashingAlgorithm, UniqueIdentifier, }; use kmip::types::request::{ self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, PublicKeyTemplateAttribute, RequestPayload, }; - use kmip::types::response::{ManagedObject, ResponsePayload}; + use kmip::types::response::ResponsePayload; use log::error; - use rpki::dep::bcder::decode::SliceSource; use crate::base::iana::SecurityAlgorithm; + use crate::crypto::common::{DigestBuilder, DigestType}; + use crate::crypto::kmip::PublicKey; use crate::crypto::kmip_pool::KmipConnPool; use crate::crypto::sign::{ GenerateError, GenerateParams, SignError, SignRaw, Signature, }; use crate::rdata::Dnskey; - use crate::crypto::common::{rsa_encode, DigestBuilder, DigestType}; - pub use kmip::client::ConnectionSettings; - #[derive(Clone, Debug)] pub struct KeyPair { /// The algorithm used by the key. @@ -103,6 +287,14 @@ pub mod sign { pub fn public_key_id(&self) -> &str { &self.public_key_id } + + pub fn public_key(&self) -> PublicKey { + PublicKey::new( + self.public_key_id.clone(), + self.algorithm, + self.conn_pool.clone(), + ) + } } impl SignRaw for KeyPair { @@ -111,133 +303,14 @@ pub mod sign { } fn dnskey(&self) -> Dnskey> { - // https://datatracker.ietf.org/doc/html/rfc5702#section-2 - // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource - // Records for DNSSEC - // 2. DNSKEY Resource Records - // "The format of the DNSKEY RR can be found in [RFC4034]. - // [RFC3110] describes the use of RSA/SHA-1 for DNSSEC - // signatures." - // | - // | - // v - // https://datatracker.ietf.org/doc/html/rfc4034#section-2.1.4 - // Resource Records for the DNS Security Extensions - // 2. The DNSKEY Resource Record - // 2.1.4. The Public Key Field - // "The Public Key Field holds the public key material. The - // format depends on the algorithm of the key being stored and - // is described in separate documents." - // | - // | - // v - // https://datatracker.ietf.org/doc/html/rfc3110#section-2 - // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) - // 2. RSA Public KEY Resource Records - // "... The structure of the algorithm specific portion of the - // RDATA part of such RRs is as shown below. - // - // Field Size - // ----- ---- - // exponent length 1 or 3 octets (see text) - // exponent as specified by length field - // modulus remaining space - // - // For interoperability, the exponent and modulus are each limited - // to 4096 bits in length. The public key exponent is a variable - // length unsigned integer. Its length in octets is represented - // as one octet if it is in the range of 1 to 255 and by a zero - // octet followed by a two octet unsigned length if it is longer - // than 255 bytes. The public key modulus field is a - // multiprecision unsigned integer. The length of the modulus can - // be determined from the RDLENGTH and the preceding RDATA fields - // including the exponent. Leading zero octets are prohibited in - // the exponent and modulus. - - let client = self.conn_pool.get().unwrap(); - - // Note: OpenDNSSEC queries the public key ID, _unless_ it was - // configured not to store the public key in the HSM (by setting - // CKA_TOKEN false) in which case there is no public key and so it - // uses the private key object handle instead. - let res = client - .get_key(&self.public_key_id) - .inspect_err(|err| error!("{err}")) - .unwrap(); - - dbg!(&res); - - assert_eq!(res.object_type, ObjectType::PublicKey); - - let ManagedObject::PublicKey(public_key) = - res.cryptographic_object - else { - todo!(); - }; - - // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 - // "“Raw” key format is intended to be applied to symmetric keys - // and not asymmetric keys" - // - // As we deal in assymetric keys (RSA, ECDSA), not symmetric keys, - // we should not encounter public_key.key_block.key_format_type == - // KeyFormatType::Raw. However, Fortanix DSM returns - // KeyFormatType::Raw when fetching key data for an ECDSA public key. - - let octets = match public_key.key_block.key_value.key_material { - KeyMaterial::Bytes(bytes) => { - // This is what we get with PyKMIP using RSASHA256 and - // Fortanix using ECDSAP256SHA256. With Fortanix it - // appears to be a DER encoded SubjectPublicKeyInfo - // data structure of the form: - // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) - // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) - // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type - // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve - // subjectPublicKey BIT_STRING @23+66: (520 bit) - let source = SliceSource::new(&bytes); - let public_key = - rpki::crypto::PublicKey::decode(source).unwrap(); - let bits = public_key.bits().to_vec(); - - // For RSA, the bits are also DER encoded of the form: - // RSAPrivateKey SEQUENCE (2 elem) - // version Version INTEGER (1024 bit) 140670898145304244147145320460151523064481569650486421654946000437850… - // modulus INTEGER 65537 - // - // or is it really: - // RSAPrivateKey SEQUENCE (2 elem) - // modulus INTEGER - // publicExponent INTEGER - - // if public_key.algorithm() == PublicKeyFormat::Rsa { - // let source = SliceSource::new(&bits); - // let mut modulus = vec![]; - // let mut public_exponent = vec![]; - // bcder::Mode::Der.decode(source, |cons| { - // cons.take_sequence(|cons| { - // modulus = bcder::string::BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); - // public_exponent = BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); - // Ok(()) - // }) - // }).unwrap(); - // rsa_encode(&public_exponent, &modulus) - // } else { - bits - // } - } - - KeyMaterial::TransparentRSAPublicKey( - TransparentRSAPublicKey { - modulus, - public_exponent, - }, - ) => rsa_encode(&public_exponent, &modulus), - - _ => todo!(), - }; - - Dnskey::new(self.flags, 3, self.algorithm, octets).unwrap() + // TODO: SAFETY + PublicKey::new( + self.public_key_id.clone(), + self.algorithm, + self.conn_pool.clone(), + ) + .dnskey(self.flags) + .unwrap() } fn sign_raw(&self, data: &[u8]) -> Result { @@ -268,7 +341,6 @@ pub mod sign { // We assume that the HSM signing operation implements this signing // operation according to these rules. - eprintln!("Signing1"); let (crypto_alg, hashing_alg, digest_type) = match self.algorithm { SecurityAlgorithm::RSASHA256 => ( @@ -284,7 +356,6 @@ pub mod sign { _ => return Err(SignError), }; - eprintln!("Signing2"); // TODO: For HSMs that don't support hashing do we have to do // hashing ourselves here after signing? Note: PyKMIP doesn't // support CryptographicParameters (and thus also not @@ -325,7 +396,6 @@ pub mod sign { data = &new_data; } - eprintln!("Signing3: digest len={}", data.as_ref().len()); let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some( @@ -341,15 +411,12 @@ pub mod sign { let client = self.conn_pool.get().map_err(|_| SignError)?; let res = client.do_request(request).unwrap(); - eprintln!("Signing4"); let ResponsePayload::Sign(signed) = res else { unreachable!(); }; - eprintln!("Signing5: len={}", signed.signature_data.len()); match self.algorithm { SecurityAlgorithm::RSASHA256 => { - eprintln!("Signing6"); // Ok(Signature::RsaSha256(Box::<[u8; 64]>::new( // signed // .signature_data @@ -408,7 +475,7 @@ pub mod sign { /// Generate a new secret key for the given algorithm. pub fn generate( name: String, - params: GenerateParams, + params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? flags: u16, conn_pool: KmipConnPool, ) -> Result { @@ -493,6 +560,9 @@ pub mod sign { ); } } + GenerateParams::RsaSha512 { .. } => { + todo!() + } GenerateParams::EcdsaP256Sha256 => { // PyKMIP doesn't support ECDSA: // "Operation CreateKeyPair failed: The cryptographic @@ -707,7 +777,7 @@ mod tests { // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); conn_settings.host = "127.0.0.1".to_string(); //"eu.smartkey.io".to_string(); - conn_settings.port = 1818; //5696; + conn_settings.port = 5696; conn_settings.insecure = true; // When connecting to kmip2pkcs11 conn_settings.connect_timeout = Some(Duration::from_secs(3)); conn_settings.read_timeout = Some(Duration::from_secs(30)); @@ -750,8 +820,8 @@ mod tests { // sleep(Duration::from_secs(5)); - // let dnskey = key.dnskey(); - // eprintln!("DNSKEY: {}", dnskey); + let dnskey = key.dnskey(); + eprintln!("DNSKEY: {}", dnskey); // // Fortanix: Activating the public key also activates the private key. // // Attempting to then activate the private key fails as it is already From 8d28ca55d6bbdc56ea0af0300b86b6c8ecbbc3ce Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:34:23 +0200 Subject: [PATCH 456/569] Bump kmip dependency. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index d54013241..92c515d50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#de0c8b2d96cf8b58ca155d727bbd1686f0a66a48" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#bf390a26ef65e9b62cb1a7f3bbafb8041bd4253f" dependencies = [ "cfg-if", "enum-display-derive", From ea219f6ff2d19d3e235430666743876877c37b25 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 24 Jun 2025 18:24:59 +0200 Subject: [PATCH 457/569] Retry KMIP operations on data not found errors, in case the HSM is not in sync yet. (cherry picked from commit d3793e9f009ceaf3379e676f8c0c0e9a86d7fff5) --- src/crypto/kmip.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index a7dd026f2..397cc6567 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -223,6 +223,7 @@ impl PublicKey { #[cfg(feature = "unstable-crypto-sign")] pub mod sign { + use core::time::Duration; use std::boxed::Box; use std::string::{String, ToString}; use std::time::SystemTime; @@ -238,7 +239,7 @@ pub mod sign { PublicKeyTemplateAttribute, RequestPayload, }; use kmip::types::response::ResponsePayload; - use log::error; + use log::{error, warn}; use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::{DigestBuilder, DigestType}; @@ -409,7 +410,25 @@ pub mod sign { // Execute the request and capture the response let client = self.conn_pool.get().map_err(|_| SignError)?; - let res = client.do_request(request).unwrap(); + let mut retries = 3; + let res = loop { + match client.do_request(request.clone()) { + Ok(res) => break res, + Err(kmip::client::Error::ItemNotFound(err)) + if retries > 0 => + { + warn!("KMIP item not found error, will retry: {err}"); + tokio::task::block_in_place(|| { + std::thread::sleep(Duration::from_secs(3)); + }); + retries -= 1; + } + Err(err) => { + error!("Error while sending KMIP request: {err}"); + return Err(SignError); + } + } + }; let ResponsePayload::Sign(signed) = res else { unreachable!(); From 04318adf9ee258fddce7537f247b67357d7447cd Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 25 Jun 2025 10:54:39 +0200 Subject: [PATCH 458/569] Add available flag. --- examples/keyset.rs | 3 +++ src/dnssec/sign/keys/keyset.rs | 43 +++++++++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index 35f9c4235..2ac207d0b 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -134,6 +134,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else if keytype == "zsk" { @@ -143,6 +144,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else if keytype == "csk" { @@ -152,6 +154,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else { diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 976771a2f..83653ba88 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -19,9 +19,11 @@ //! let mut ks = KeySet::new(Name::from_str("example.com").unwrap()); //! //! // Add two keys. -//! ks.add_key_ksk("first KSK.key".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); +//! ks.add_key_ksk("first KSK.key".to_string(), None, +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); //! ks.add_key_zsk("first ZSK.key".to_string(), -//! Some("first ZSK.private".to_string()), SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); +//! Some("first ZSK.private".to_string()), +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); //! //! // Save the state. //! let json = serde_json::to_string(&ks).unwrap(); @@ -109,11 +111,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Ksk(keystate), @@ -137,11 +143,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Zsk(keystate), @@ -165,11 +175,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Csk(keystate.clone(), keystate), @@ -418,6 +432,7 @@ impl KeySet { }; if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -491,6 +506,7 @@ impl KeySet { }; if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -569,6 +585,7 @@ impl KeySet { KeyType::Ksk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -586,6 +603,7 @@ impl KeySet { KeyType::Zsk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -602,6 +620,7 @@ impl KeySet { KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { if *ksk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -617,6 +636,7 @@ impl KeySet { if *zsk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -705,6 +725,7 @@ impl KeySet { | KeyType::Zsk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -722,6 +743,7 @@ impl KeySet { KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { if *ksk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -737,6 +759,7 @@ impl KeySet { if *zsk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -863,7 +886,8 @@ pub enum KeyType { /// State of a key. /// -/// The state is expressed as four booleans: +/// The state is expressed as five booleans: +/// * available. The key is available as an incoming key during key rolls. /// * old. Set if the key is on its way out. /// * signer. Set if the key either signes the DNSKEY RRset or the rest of the /// zone. @@ -871,6 +895,7 @@ pub enum KeyType { /// * at_parent. If the key has a DS record at the parent. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct KeyState { + available: bool, old: bool, signer: bool, present: bool, @@ -1993,6 +2018,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); ks.add_key_zsk( @@ -2001,6 +2027,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 1, UnixTime::now(), + true, ) .unwrap(); @@ -2084,6 +2111,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 2, UnixTime::now(), + true, ) .unwrap(); ks.add_key_zsk( @@ -2092,6 +2120,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 3, UnixTime::now(), + true, ) .unwrap(); @@ -2217,6 +2246,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); @@ -2293,6 +2323,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 4, UnixTime::now(), + true, ) .unwrap(); From 831d983ce04a1655b7c6f27420bd718703a5e4f1 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 25 Jun 2025 11:58:14 +0200 Subject: [PATCH 459/569] Improvements to error reporting. --- src/crypto/kmip.rs | 82 ++++++++++++++++++++++++++++------------------ src/crypto/sign.rs | 9 +++-- 2 files changed, 57 insertions(+), 34 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 397cc6567..f77504259 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -16,34 +16,52 @@ use kmip::types::{ }; use log::error; +pub use kmip::client::ConnectionSettings; + use crate::{ base::iana::SecurityAlgorithm, crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, rdata::Dnskey, }; -pub use kmip::client::ConnectionSettings; - /// An error in generating a key pair with OpenSSL. #[derive(Clone, Debug)] pub enum GenerateError { - /// The requested algorithm was not supported. - UnsupportedAlgorithm, + /// The requested algorithm is not supported. + UnsupportedAlgorithm(SecurityAlgorithm), + + // The requested key size for the given algorithm is not supported. + UnsupportedKeySize { + algorithm: SecurityAlgorithm, + min: u32, + max: u32, + requested: u32, + }, - /// An implementation failure occurred. - /// - /// This includes memory allocation failures. - Implementation, + /// A problem occurred while communicating with the KMIP server. + Kmip(String), } //--- Formatting impl fmt::Display for GenerateError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Self::UnsupportedAlgorithm => "algorithm not supported", - Self::Implementation => "an internal error occurred", - }) + match self { + Self::UnsupportedAlgorithm(algorithm) => { + write!(f, "algorithm {algorithm} not supported") + } + Self::UnsupportedKeySize { + algorithm, + min, + max, + requested, + } => { + write!(f, "key size {requested} for algorithm {algorithm} must be in the range {min}..={max}") + } + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } } } @@ -239,14 +257,14 @@ pub mod sign { PublicKeyTemplateAttribute, RequestPayload, }; use kmip::types::response::ResponsePayload; - use log::{error, warn}; + use log::{debug, error}; use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::{DigestBuilder, DigestType}; - use crate::crypto::kmip::PublicKey; + use crate::crypto::kmip::{GenerateError, PublicKey}; use crate::crypto::kmip_pool::KmipConnPool; use crate::crypto::sign::{ - GenerateError, GenerateParams, SignError, SignRaw, Signature, + GenerateParams, SignError, SignRaw, Signature, }; use crate::rdata::Dnskey; @@ -417,7 +435,9 @@ pub mod sign { Err(kmip::client::Error::ItemNotFound(err)) if retries > 0 => { - warn!("KMIP item not found error, will retry: {err}"); + error!( + "KMIP item not found error, will retry: {err}" + ); tokio::task::block_in_place(|| { std::thread::sleep(Duration::from_secs(3)); }); @@ -556,7 +576,12 @@ pub mod sign { // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST // NOT be more than 4096 bits." if !(512..=4096).contains(&bits) { - return Err(GenerateError::UnsupportedAlgorithm); + return Err(GenerateError::UnsupportedKeySize { + algorithm: SecurityAlgorithm::RSASHA256, + min: 512, + max: 4096, + requested: bits, + }); } if use_cryptographic_params { @@ -580,7 +605,9 @@ pub mod sign { } } GenerateParams::RsaSha512 { .. } => { - todo!() + return Err(GenerateError::UnsupportedAlgorithm( + SecurityAlgorithm::RSASHA512, + )); } GenerateParams::EcdsaP256Sha256 => { // PyKMIP doesn't support ECDSA: @@ -659,21 +686,21 @@ pub mod sign { // Execute the request and capture the response let response = client.do_request(request).map_err(|err| { error!("KMIP request failed: {err}"); - error!( + debug!( "KMIP last request: {}", client.last_req_diag_str().unwrap_or_default() ); - error!( + debug!( "KMIP last response: {}", client.last_res_diag_str().unwrap_or_default() ); - GenerateError::Implementation + GenerateError::Kmip(err.to_string()) })?; // Process the successful response let ResponsePayload::CreateKeyPair(payload) = response else { error!("KMIP request failed: Wrong response type received!"); - return Err(GenerateError::Implementation); + return Err(GenerateError::Kmip("Unable to parse KMIP response: payload should be CreateKeyPair".to_string())); }; Ok(KeyPair { @@ -693,6 +720,7 @@ mod tests { use std::fs::File; use std::io::{BufReader, Read}; use std::string::ToString; + use std::time::SystemTime; use std::vec::Vec; use kmip::client::ConnectionSettings; @@ -701,14 +729,6 @@ mod tests { use crate::crypto::kmip_pool::ConnectionManager; use crate::crypto::sign::SignRaw; use crate::logging::init_logging; - use kmip::types::common::{ - CryptographicAlgorithm, CryptographicParameters, Data, - HashingAlgorithm, UniqueIdentifier, - }; - use kmip::types::request::RequestPayload; - use kmip::types::response::ResponsePayload; - use std::thread::sleep; - use std::time::SystemTime; #[test] #[ignore = "Requires running PyKMIP"] @@ -772,7 +792,7 @@ mod tests { pool, ); dbg!(&res); - let key = res.unwrap(); + let _key = res.unwrap(); // let dnskey = key.dnskey(); // eprintln!("DNSKEY: {}", dnskey); diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index c3b71a2e4..d79e9aeeb 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -957,10 +957,13 @@ impl From for GenerateError { impl From for GenerateError { fn from(value: kmip::GenerateError) -> Self { match value { - kmip::GenerateError::UnsupportedAlgorithm => { - Self::UnsupportedAlgorithm + kmip::GenerateError::UnsupportedAlgorithm(_) => { + GenerateError::UnsupportedAlgorithm + } + kmip::GenerateError::UnsupportedKeySize { .. } => { + GenerateError::UnsupportedAlgorithm } - kmip::GenerateError::Implementation => Self::Implementation, + kmip::GenerateError::Kmip(_) => GenerateError::Implementation, } } } From 2f619ecf47c0d58a0170474a13ccf45b945db740 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:00:58 +0200 Subject: [PATCH 460/569] Revert "Retry KMIP operations on data not found errors, in case the HSM is not in sync yet." This reverts commit ea219f6ff2d19d3e235430666743876877c37b25. --- src/crypto/kmip.rs | 33 ++++++++++----------------------- 1 file changed, 10 insertions(+), 23 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index f77504259..cc2f18c92 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -241,7 +241,6 @@ impl PublicKey { #[cfg(feature = "unstable-crypto-sign")] pub mod sign { - use core::time::Duration; use std::boxed::Box; use std::string::{String, ToString}; use std::time::SystemTime; @@ -427,28 +426,16 @@ pub mod sign { ); // Execute the request and capture the response - let client = self.conn_pool.get().map_err(|_| SignError)?; - let mut retries = 3; - let res = loop { - match client.do_request(request.clone()) { - Ok(res) => break res, - Err(kmip::client::Error::ItemNotFound(err)) - if retries > 0 => - { - error!( - "KMIP item not found error, will retry: {err}" - ); - tokio::task::block_in_place(|| { - std::thread::sleep(Duration::from_secs(3)); - }); - retries -= 1; - } - Err(err) => { - error!("Error while sending KMIP request: {err}"); - return Err(SignError); - } - } - }; + let client = self + .conn_pool + .get() + .inspect_err(|err| error!("Error while obtaining KMIP pool connection: {err}")) + .map_err(|_| SignError)?; + + let res = client + .do_request(request) + .inspect_err(|err| error!("Error while sending KMIP request: {err}")) + .map_err(|_| SignError)?; let ResponsePayload::Sign(signed) = res else { unreachable!(); From 08914c07e2fb450290c08e3b297f0dde57385e87 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 25 Jun 2025 12:01:12 +0200 Subject: [PATCH 461/569] Cargo fmt. --- src/crypto/kmip.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index cc2f18c92..3331daf29 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -429,12 +429,18 @@ pub mod sign { let client = self .conn_pool .get() - .inspect_err(|err| error!("Error while obtaining KMIP pool connection: {err}")) + .inspect_err(|err| { + error!( + "Error while obtaining KMIP pool connection: {err}" + ) + }) .map_err(|_| SignError)?; let res = client .do_request(request) - .inspect_err(|err| error!("Error while sending KMIP request: {err}")) + .inspect_err(|err| { + error!("Error while sending KMIP request: {err}") + }) .map_err(|_| SignError)?; let ResponsePayload::Sign(signed) = res else { From d0e8548ce75b85a8a6a0855346e60bd1967a8c13 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 25 Jun 2025 13:16:26 +0200 Subject: [PATCH 462/569] Add changes from the crypto-and-keyset-fixes-plus-patches-for-nameshed-prototype branch. --- src/rdata/dnssec.rs | 105 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 82 insertions(+), 23 deletions(-) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 68470c824..e63538b7c 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -4,21 +4,14 @@ //! //! [RFC 4034]: https://tools.ietf.org/html/rfc4034 -use crate::base::cmp::CanonicalOrd; -use crate::base::iana::{DigestAlgorithm, Rtype, SecurityAlgorithm}; -use crate::base::name::{FlattenInto, ParsedName, ToName}; -use crate::base::rdata::{ - ComposeRecordData, LongRecordData, ParseRecordData, RecordData, -}; -use crate::base::scan::{Scan, Scanner, ScannerError}; -use crate::base::serial::Serial; -use crate::base::wire::{Compose, Composer, FormError, Parse, ParseError}; -use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; -use crate::base::Ttl; -use crate::utils::{base16, base64}; use core::cmp::Ordering; use core::convert::TryInto; +use core::time::Duration; use core::{cmp, fmt, hash, str}; + +use std::time::{SystemTime, UNIX_EPOCH}; +use std::vec::Vec; + use octseq::builder::{ EmptyBuilder, FreezeBuilder, FromBuilder, OctetsBuilder, Truncate, }; @@ -27,9 +20,21 @@ use octseq::parse::Parser; #[cfg(feature = "serde")] use octseq::serde::{DeserializeOctets, SerializeOctets}; #[cfg(feature = "std")] -use std::vec::Vec; use time::{Date, Month, PrimitiveDateTime, Time}; +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{DigestAlgorithm, Rtype, SecurityAlgorithm}; +use crate::base::name::{FlattenInto, ParsedName, ToName}; +use crate::base::rdata::{ + ComposeRecordData, LongRecordData, ParseRecordData, RecordData, +}; +use crate::base::scan::{Scan, Scanner, ScannerError}; +use crate::base::serial::Serial; +use crate::base::wire::{Compose, Composer, FormError, Parse, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::Ttl; +use crate::utils::{base16, base64}; + //------------ Dnskey -------------------------------------------------------- #[derive(Clone)] @@ -75,7 +80,9 @@ impl Dnskey { { LongRecordData::check_len( usize::from( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, + u16::COMPOSE_LEN + + u8::COMPOSE_LEN + + SecurityAlgorithm::COMPOSE_LEN, ) .checked_add(public_key.as_ref().len()) .expect("long key"), @@ -386,7 +393,9 @@ impl> ComposeRecordData for Dnskey { u16::try_from(self.public_key.as_ref().len()) .expect("long key") .checked_add( - u16::COMPOSE_LEN + u8::COMPOSE_LEN + SecurityAlgorithm::COMPOSE_LEN, + u16::COMPOSE_LEN + + u8::COMPOSE_LEN + + SecurityAlgorithm::COMPOSE_LEN, ) .expect("long key"), ) @@ -700,6 +709,45 @@ impl Timestamp { pub fn into_int(self) -> u32 { self.0.into_int() } + + /// Return a SystemTime that has to meet two requirements: + /// 1) The SystemTime value has a duration since UNIX_EPOCH that + /// modulo 2**32 is equal to our value. + /// 2) The time difference between the SystemTime value and the + /// reference fits within an i32. + #[must_use] + pub fn to_system_time(&self, reference: SystemTime) -> SystemTime { + // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because + // the timestamp may be too far in the future. We may have to add + // n * 2**32 for some unknown value of n. + const POW_2_32: u64 = 0x1_0000_0000; + let ref_secs = + reference.duration_since(UNIX_EPOCH).unwrap().as_secs(); + let k = ref_secs / POW_2_32; + let ref_secs_mod = ref_secs % POW_2_32; + let ts_secs = self.into_int() as u64; + let ts_secs = if ts_secs < ref_secs_mod { + if ref_secs_mod - ts_secs <= POW_2_32 / 2 { + // Close enough, use k. + ts_secs + k * POW_2_32 + } else { + // ts_secs is really beyond ref_secs, use k+1. + ts_secs + (k + 1) * POW_2_32 + } + } else { + // ts_secs >= ref_secs_mod + if ts_secs - ref_secs_mod < POW_2_32 / 2 { + // Close enough, use k. + ts_secs + k * POW_2_32 + } else { + // ts_secs is really old than ref_secs. Try to use k-1 + // but only if k is not zero. + let k = if k > 0 { k - 1 } else { k }; + ts_secs + k * POW_2_32 + } + }; + UNIX_EPOCH + Duration::from_secs(ts_secs) + } } /// # Parsing and Composing @@ -2637,8 +2685,9 @@ impl Iterator for RtypeBitmapIter<'_> { if self.data.is_empty() { return None; } - let res = - Rtype::from_int(self.block | ((self.octet as u16) << 3) | self.bit); + let res = Rtype::from_int( + self.block | ((self.octet as u16) << 3) | self.bit, + ); self.advance(); Some(res) } @@ -2752,7 +2801,8 @@ mod test { #[test] #[allow(clippy::redundant_closure)] // lifetimes ... fn dnskey_compose_parse_scan() { - let rdata = Dnskey::new(10, 11, SecurityAlgorithm::RSASHA1, b"key0").unwrap(); + let rdata = + Dnskey::new(10, 11, SecurityAlgorithm::RSASHA1, b"key0").unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Dnskey::parse(parser)); test_scan(&["10", "11", "5", "a2V5MA=="], Dnskey::scan, &rdata); @@ -2823,8 +2873,13 @@ mod test { #[test] #[allow(clippy::redundant_closure)] // lifetimes ... fn ds_compose_parse_scan() { - let rdata = - Ds::new(10, SecurityAlgorithm::RSASHA1, DigestAlgorithm::SHA256, b"key").unwrap(); + let rdata = Ds::new( + 10, + SecurityAlgorithm::RSASHA1, + DigestAlgorithm::SHA256, + b"key", + ) + .unwrap(); test_rdlen(&rdata); test_compose_parse(&rdata, |parser| Ds::parse(parser)); test_scan(&["10", "5", "2", "6b6579"], Ds::scan, &rdata); @@ -2969,9 +3024,13 @@ mod test { #[test] fn dnskey_flags() { - let dnskey = - Dnskey::new(257, 3, SecurityAlgorithm::RSASHA256, bytes::Bytes::new()) - .unwrap(); + let dnskey = Dnskey::new( + 257, + 3, + SecurityAlgorithm::RSASHA256, + bytes::Bytes::new(), + ) + .unwrap(); assert!(dnskey.is_zone_key()); assert!(dnskey.is_secure_entry_point()); assert!(!dnskey.is_revoked()); From a145689cd59a7d090423a7a052dc5f1f7d42a4e9 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 25 Jun 2025 15:21:29 +0200 Subject: [PATCH 463/569] Add Double-DS KSK Roll --- examples/keyset.rs | 5 +- src/dnssec/sign/keys/keyset.rs | 343 ++++++++++++++++++++++++++++++++- 2 files changed, 338 insertions(+), 10 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index 2ac207d0b..5d54a9225 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -200,7 +200,7 @@ fn do_start(filename: &str, args: &[String]) { .keys() .iter() .filter_map(|(pr, k)| match rolltype { - RollType::KskRoll => { + RollType::KskRoll | RollType::KskDoubleDsRoll => { if let KeyType::Ksk(keystate) = k.keytype() { Some((keystate.clone(), pr)) } else { @@ -555,6 +555,9 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { Action::ReportDsPropagated => println!( "\tReport that the DS RRset has propagated at the parent" ), + Action::WaitDsPropagated => { + println!("\tWait until DS RRset has propagated at the parent") + } } } } diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 83653ba88..9d6fc8bfe 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -469,6 +469,76 @@ impl KeySet { Ok(()) } + fn update_ksk_double_ds( + &mut self, + mode: Mode, + old: &[&str], + new: &[&str], + ) -> Result<(), Error> { + let mut tmpkeys = self.keys.clone(); + let keys: &mut HashMap = match mode { + Mode::DryRun => &mut tmpkeys, + Mode::ForReal => &mut self.keys, + }; + let mut algs_old = HashSet::new(); + for k in old { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Ksk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + + // Set old for any key we find. + keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); + } + let mut algs_new = HashSet::new(); + for k in new { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Ksk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + if *keystate + != (KeyState { + available: true, + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + keystate.at_parent = true; + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); + } + + // Make sure we have at least one key in the right state. + if !keys.iter().any(|(_, k)| { + if let KeyType::Ksk(keystate) = &k.keytype { + !keystate.old && keystate.at_parent + } else { + false + } + }) { + return Err(Error::NoSuitableKeyPresent); + } + Ok(()) + } + fn update_zsk( &mut self, mode: Mode, @@ -1166,6 +1236,11 @@ pub enum Action { /// the DS records. ReportDsPropagated, + /// Wait for the update FS records to have propagated to all + /// secondaries that serve the parent zone. Waiting is necessary to + /// avoid removing the CDS and CDNSKEY records too soon. + WaitDsPropagated, + /// Report whether updated RRSIG records have propagated to all /// secondaries that the serve the zone. For propagation it is /// sufficient to track the signatures on the SOA record. Report the @@ -1182,12 +1257,22 @@ pub enum Action { /// The type of key roll to perform. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum RollType { - /// A KSK roll. + /// A KSK roll. This implements the Double-Signature ZSK Roll as described + /// in Section 4.1.2 of RFC 6781. KskRoll, - /// A ZSK roll. + /// An alternative KSK roll. This implements the Double-DS KSK Roll as + /// described in Section 4.1.2 of RFC 6781. + KskDoubleDsRoll, + + /// A ZSK roll. This implements the Pre-Publish ZSK Roll as described + /// in Section 4.1.1.1. of RFC 6781. ZskRoll, + /// An alternative ZSK roll. This implements the Double-Signature ZSK + /// Roll as described in Section 4.1.1.2. of RFC 6781. + //ZskDoubleSignatureRoll, + /// A CSK roll. CskRoll, @@ -1199,6 +1284,7 @@ impl RollType { fn rollfn(&self) -> fn(RollOp, &mut KeySet) -> Result<(), Error> { match self { RollType::KskRoll => ksk_roll, + RollType::KskDoubleDsRoll => ksk_double_ds_roll, RollType::ZskRoll => zsk_roll, RollType::CskRoll => csk_roll, RollType::AlgorithmRoll => algorithm_roll, @@ -1207,6 +1293,7 @@ impl RollType { fn roll_actions_fn(&self) -> fn(RollState) -> Vec { match self { RollType::KskRoll => ksk_roll_actions, + RollType::KskDoubleDsRoll => ksk_double_ds_roll_actions, RollType::ZskRoll => zsk_roll_actions, RollType::CskRoll => csk_roll_actions, RollType::AlgorithmRoll => algorithm_roll_actions, @@ -1414,6 +1501,130 @@ fn ksk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { Ok(()) } +fn ksk_double_ds_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current KSK-roll state is idle. We need to + // check all conflicting key rolls as well. The way we check is + // to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = + ks.rollstates.keys().find(|k| **k != RollType::ZskRoll) + { + if *rolltype == RollType::KskDoubleDsRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } + // Check if we can move the states of the keys + ks.update_ksk_double_ds(Mode::DryRun, old, new)?; + // Move the states of the keys + ks.update_ksk_double_ds(Mode::ForReal, old, new) + .expect("Should have been checked by DryRun"); + } + RollOp::Propagation1 => { + // Set the ds_visible time of new KSKs to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Ksk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.at_parent { + continue; + } + + k.timestamps.ds_visible = Some(now.clone()); + } + } + RollOp::CacheExpire1(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Ksk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + let ds_visible = k + .timestamps + .ds_visible + .as_ref() + .expect("Should have been set in Propagation1"); + let elapsed = ds_visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Old keys are no longer present and signing, new keys will + // be present and signing. + for k in &mut ks.keys.values_mut() { + if let KeyType::Ksk(ref mut keystate) = k.keytype { + if keystate.old && keystate.present { + keystate.present = false; + keystate.signer = false; + } + + if !keystate.old && keystate.at_parent { + keystate.present = true; + keystate.signer = true; + } + } + } + } + RollOp::Propagation2 => { + // Set the visible time of new keys to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Ksk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + } + } + RollOp::CacheExpire2(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Ksk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + let visible = k + .timestamps + .ds_visible + .as_ref() + .expect("Should have been set in Propagation2"); + let elapsed = visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move old DS records out + for k in ks.keys.values_mut() { + let KeyType::Ksk(ref mut keystate) = k.keytype else { + continue; + }; + if keystate.old && keystate.at_parent { + keystate.at_parent = false; + k.timestamps.withdrawn = Some(UnixTime::now()); + } + } + } + RollOp::Done => (), + } + Ok(()) +} + fn ksk_roll_actions(rollstate: RollState) -> Vec { let mut actions = Vec::new(); match rollstate { @@ -1436,6 +1647,30 @@ fn ksk_roll_actions(rollstate: RollState) -> Vec { actions } +fn ksk_double_ds_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::ReportDsPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::RemoveCdsRrset); + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::ReportDnskeyPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => { + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::WaitDsPropagated); + } // Missing: RemoveCdsRrset, This would require one more state, + } + actions +} + fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { @@ -2240,6 +2475,96 @@ mod tests { assert_eq!(actions, []); ks.delete_key("first KSK").unwrap(); + ks.add_key_ksk( + "third KSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 4, + UnixTime::now(), + true, + ) + .unwrap(); + + let actions = ks + .start_roll( + RollType::KskDoubleDsRoll, + &["second KSK"], + &["third KSK"], + ) + .unwrap(); + assert_eq!( + actions, + [ + Action::CreateCdsRrset, + Action::UpdateDsRrset, + Action::ReportDsPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["second KSK", "second ZSK"]); + let mut dks = dnskey_sigs(&ks); + dks.sort(); + assert_eq!(dks, ["second KSK"]); + assert_eq!(zone_sigs(&ks), ["second ZSK"]); + let mut dsks = ds_keys(&ks); + dsks.sort(); + assert_eq!(dsks, ["second KSK", "third KSK"]); + + let actions = ks + .propagation1_complete(RollType::KskDoubleDsRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = ks.cache_expired1(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!( + actions, + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::ReportDnskeyPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["second ZSK", "third KSK"]); + let mut dks = dnskey_sigs(&ks); + dks.sort(); + assert_eq!(dks, ["third KSK"]); + assert_eq!(zone_sigs(&ks), ["second ZSK"]); + let mut dsks = ds_keys(&ks); + dsks.sort(); + assert_eq!(dsks, ["second KSK", "third KSK"]); + + let actions = ks + .propagation2_complete(RollType::KskDoubleDsRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = ks.cache_expired2(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!( + actions, + [ + Action::CreateCdsRrset, + Action::UpdateDsRrset, + Action::WaitDsPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["second ZSK", "third KSK"]); + assert_eq!(dnskey_sigs(&ks), ["third KSK"]); + assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(ds_keys(&ks), ["third KSK"]); + + let actions = ks.roll_done(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!(actions, []); + ks.delete_key("second KSK").unwrap(); + ks.add_key_csk( "first CSK".to_string(), None, @@ -2253,7 +2578,7 @@ mod tests { let actions = ks .start_roll( RollType::CskRoll, - &["second KSK", "second ZSK"], + &["third KSK", "second ZSK"], &["first CSK"], ) .unwrap(); @@ -2263,12 +2588,12 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first CSK", "second ZSK", "third KSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); - assert_eq!(dks, ["first CSK", "second KSK"]); + assert_eq!(dks, ["first CSK", "third KSK"]); assert_eq!(zone_sigs(&ks), ["second ZSK"]); - assert_eq!(ds_keys(&ks), ["second KSK"]); + assert_eq!(ds_keys(&ks), ["third KSK"]); let actions = ks.propagation1_complete(RollType::CskRoll, 3600).unwrap(); @@ -2289,10 +2614,10 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first CSK", "second ZSK", "third KSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); - assert_eq!(dks, ["first CSK", "second KSK"]); + assert_eq!(dks, ["first CSK", "third KSK"]); assert_eq!(zone_sigs(&ks), ["first CSK"]); assert_eq!(ds_keys(&ks), ["first CSK"]); @@ -2314,7 +2639,7 @@ mod tests { let actions = ks.roll_done(RollType::CskRoll).unwrap(); assert_eq!(actions, []); - ks.delete_key("second KSK").unwrap(); + ks.delete_key("third KSK").unwrap(); ks.delete_key("second ZSK").unwrap(); ks.add_key_csk( From 27fadbc80b71a63d8885a7422fff43dbbc3d32cb Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 25 Jun 2025 16:19:01 +0200 Subject: [PATCH 464/569] Add Double-Signature ZSK Roll. --- src/dnssec/sign/keys/keyset.rs | 350 ++++++++++++++++++++++++++++++--- 1 file changed, 322 insertions(+), 28 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 9d6fc8bfe..2ab1e469f 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -612,6 +612,80 @@ impl KeySet { Ok(()) } + fn update_zsk_double_signature( + &mut self, + mode: Mode, + old: &[&str], + new: &[&str], + ) -> Result<(), Error> { + let mut tmpkeys = self.keys.clone(); + let keys: &mut HashMap = match mode { + Mode::DryRun => &mut tmpkeys, + Mode::ForReal => &mut self.keys, + }; + let mut algs_old = HashSet::new(); + for k in old { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Zsk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + + // Set old for any key we find. + keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); + } + let now = UnixTime::now(); + let mut algs_new = HashSet::new(); + for k in new { + let Some(key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Zsk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + if *keystate + != (KeyState { + available: true, + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + // Move key state to Incoming. + keystate.present = true; + keystate.signer = true; + key.timestamps.published = Some(now.clone()); + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); + } + + // Make sure we have at least one key in incoming state. + if !keys.iter().any(|(_, k)| { + if let KeyType::Zsk(keystate) = &k.keytype { + !keystate.old || keystate.present + } else { + false + } + }) { + return Err(Error::NoSuitableKeyPresent); + } + Ok(()) + } + fn update_csk( &mut self, mode: Mode, @@ -1271,7 +1345,7 @@ pub enum RollType { /// An alternative ZSK roll. This implements the Double-Signature ZSK /// Roll as described in Section 4.1.1.2. of RFC 6781. - //ZskDoubleSignatureRoll, + ZskDoubleSignatureRoll, /// A CSK roll. CskRoll, @@ -1286,6 +1360,7 @@ impl RollType { RollType::KskRoll => ksk_roll, RollType::KskDoubleDsRoll => ksk_double_ds_roll, RollType::ZskRoll => zsk_roll, + RollType::ZskDoubleSignatureRoll => zsk_double_signature_roll, RollType::CskRoll => csk_roll, RollType::AlgorithmRoll => algorithm_roll, } @@ -1295,6 +1370,9 @@ impl RollType { RollType::KskRoll => ksk_roll_actions, RollType::KskDoubleDsRoll => ksk_double_ds_roll_actions, RollType::ZskRoll => zsk_roll_actions, + RollType::ZskDoubleSignatureRoll => { + zsk_double_signature_roll_actions + } RollType::CskRoll => csk_roll_actions, RollType::AlgorithmRoll => algorithm_roll_actions, } @@ -1387,9 +1465,10 @@ fn ksk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { // check all conflicting key rolls as well. The way we check is // to allow specified non-conflicting rolls and consider // everything else as a conflict. - if let Some(rolltype) = - ks.rollstates.keys().find(|k| **k != RollType::ZskRoll) - { + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::ZskRoll + && **k != RollType::ZskDoubleSignatureRoll + }) { if *rolltype == RollType::KskRoll { return Err(Error::WrongStateForRollOperation); } else { @@ -1508,9 +1587,10 @@ fn ksk_double_ds_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { // check all conflicting key rolls as well. The way we check is // to allow specified non-conflicting rolls and consider // everything else as a conflict. - if let Some(rolltype) = - ks.rollstates.keys().find(|k| **k != RollType::ZskRoll) - { + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::ZskRoll + && **k != RollType::ZskDoubleSignatureRoll + }) { if *rolltype == RollType::KskDoubleDsRoll { return Err(Error::WrongStateForRollOperation); } else { @@ -1678,9 +1758,9 @@ fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { // to check all conflicting key rolls as well. The way we check // is to allow specified non-conflicting rolls and consider // everything else as a conflict. - if let Some(rolltype) = - ks.rollstates.keys().find(|k| **k != RollType::KskRoll) - { + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::KskRoll && **k != RollType::KskDoubleDsRoll + }) { if *rolltype == RollType::ZskRoll { return Err(Error::WrongStateForRollOperation); } else { @@ -1793,6 +1873,117 @@ fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { Ok(()) } +fn zsk_double_signature_roll( + rollop: RollOp, + ks: &mut KeySet, +) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current ZSK-roll state is idle. We need + // to check all conflicting key rolls as well. The way we check + // is to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::KskRoll && **k != RollType::KskDoubleDsRoll + }) { + if *rolltype == RollType::ZskDoubleSignatureRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } + // Check if we can move the states of the keys + ks.update_zsk_double_signature(Mode::DryRun, old, new)?; + // Move the states of the keys + ks.update_zsk_double_signature(Mode::ForReal, old, new) + .expect("Should have been checked with DryRun"); + } + RollOp::Propagation1 => { + // Set the visiable time of new ZSKs to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + k.timestamps.rrsig_visible = Some(now.clone()); + } + } + RollOp::CacheExpire1(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + let visible = k + .timestamps + .visible + .as_ref() + .expect("Should have been set in Propagation1"); + let elapsed = visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move the Leaving keys to Retired. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref mut keystate) = k.keytype else { + continue; + }; + if keystate.old { + keystate.present = false; + keystate.signer = false; + k.timestamps.withdrawn = Some(now.clone()); + } + } + } + RollOp::Propagation2 => { + // Set the published time of new RRSIG records to the current time. + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + } + } + RollOp::CacheExpire2(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + + let rrsig_visible = k + .timestamps + .rrsig_visible + .as_ref() + .expect("Should have been set in Propagation2"); + let elapsed = rrsig_visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + } + RollOp::Done => (), + } + Ok(()) +} + fn zsk_roll_actions(rollstate: RollState) -> Vec { let mut actions = Vec::new(); match rollstate { @@ -1813,6 +2004,28 @@ fn zsk_roll_actions(rollstate: RollState) -> Vec { actions } +fn zsk_double_signature_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportDnskeyPropagated); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportDnskeyPropagated); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => (), + } + actions +} + fn csk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { @@ -2413,6 +2626,87 @@ mod tests { assert_eq!(actions, []); ks.delete_key("first ZSK").unwrap(); + ks.add_key_zsk( + "third ZSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 4, + UnixTime::now(), + true, + ) + .unwrap(); + + let actions = ks + .start_roll( + RollType::ZskDoubleSignatureRoll, + &["second ZSK"], + &["third ZSK"], + ) + .unwrap(); + assert_eq!( + actions, + [ + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::ReportDnskeyPropagated, + Action::ReportRrsigPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "second ZSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + let mut zs = zone_sigs(&ks); + zs.sort(); + assert_eq!(zs, ["second ZSK", "third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks + .propagation1_complete(RollType::ZskDoubleSignatureRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = + ks.cache_expired1(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!( + actions, + [ + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::ReportDnskeyPropagated, + Action::ReportRrsigPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks + .propagation2_complete(RollType::ZskDoubleSignatureRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = + ks.cache_expired2(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!(actions, []); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks.roll_done(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!(actions, []); + ks.delete_key("second ZSK").unwrap(); + let actions = ks .start_roll(RollType::KskRoll, &["first KSK"], &["second KSK"]) .unwrap(); @@ -2422,11 +2716,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first KSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first KSK", "second KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first KSK", "second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["first KSK"]); let actions = @@ -2446,11 +2740,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first KSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first KSK", "second KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first KSK", "second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["second KSK"]); let actions = @@ -2466,9 +2760,9 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["second KSK", "second ZSK"]); + assert_eq!(dk, ["second KSK", "third ZSK"]); assert_eq!(dnskey_sigs(&ks), ["second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["second KSK"]); let actions = ks.roll_done(RollType::KskRoll).unwrap(); @@ -2479,7 +2773,7 @@ mod tests { "third KSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, - 4, + 5, UnixTime::now(), true, ) @@ -2502,11 +2796,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["second KSK", "second ZSK"]); + assert_eq!(dk, ["second KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); let mut dsks = ds_keys(&ks); dsks.sort(); assert_eq!(dsks, ["second KSK", "third KSK"]); @@ -2529,11 +2823,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["second ZSK", "third KSK"]); + assert_eq!(dk, ["third KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["third KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); let mut dsks = ds_keys(&ks); dsks.sort(); assert_eq!(dsks, ["second KSK", "third KSK"]); @@ -2556,9 +2850,9 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["second ZSK", "third KSK"]); + assert_eq!(dk, ["third KSK", "third ZSK"]); assert_eq!(dnskey_sigs(&ks), ["third KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["third KSK"]); let actions = ks.roll_done(RollType::KskDoubleDsRoll).unwrap(); @@ -2578,7 +2872,7 @@ mod tests { let actions = ks .start_roll( RollType::CskRoll, - &["third KSK", "second ZSK"], + &["third KSK", "third ZSK"], &["first CSK"], ) .unwrap(); @@ -2588,11 +2882,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second ZSK", "third KSK"]); + assert_eq!(dk, ["first CSK", "third KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first CSK", "third KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["third KSK"]); let actions = @@ -2614,7 +2908,7 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second ZSK", "third KSK"]); + assert_eq!(dk, ["first CSK", "third KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first CSK", "third KSK"]); @@ -2640,7 +2934,7 @@ mod tests { let actions = ks.roll_done(RollType::CskRoll).unwrap(); assert_eq!(actions, []); ks.delete_key("third KSK").unwrap(); - ks.delete_key("second ZSK").unwrap(); + ks.delete_key("third ZSK").unwrap(); ks.add_key_csk( "second CSK".to_string(), From 8529cd829fc574adcc2818328372cc3fd396c7a5 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 25 Jun 2025 16:30:47 +0200 Subject: [PATCH 465/569] Add some notes about CSK and algorithm rolls. --- src/dnssec/sign/keys/keyset.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 2ab1e469f..996526ed1 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -1347,10 +1347,12 @@ pub enum RollType { /// Roll as described in Section 4.1.1.2. of RFC 6781. ZskDoubleSignatureRoll, - /// A CSK roll. + /// A CSK roll. This implements neither of the two algorithms in + /// Section 4.1.3. of RFC 6781. CskRoll, - /// An algorithm roll. + /// An algorithm roll. This implements the 'liberal approach' as + /// described in Section 4.1.4 of RFC 6781. AlgorithmRoll, } From 708d5b4c4545515aed78013ade78a8cd648ea79a Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 27 Jun 2025 14:19:27 +0200 Subject: [PATCH 466/569] A bit of cleanup. --- examples/keyset.rs | 2 +- src/dnssec/sign/keys/keyset.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index 013877782..1152bc16d 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -207,7 +207,7 @@ fn do_start(filename: &str, args: &[String]) { None } } - RollType::ZskRoll => { + RollType::ZskRoll | RollType::ZskDoubleSignatureRoll => { if let KeyType::Zsk(keystate) = k.keytype() { Some((keystate.clone(), pr)) } else { diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index f3b411453..5fbe52930 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -1582,7 +1582,10 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { Ok(()) } -fn ksk_double_ds_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { +fn ksk_double_ds_roll( + rollop: RollOp<'_>, + ks: &mut KeySet, +) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { // First check if the current KSK-roll state is idle. We need to @@ -1876,7 +1879,7 @@ fn zsk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } fn zsk_double_signature_roll( - rollop: RollOp, + rollop: RollOp<'_>, ks: &mut KeySet, ) -> Result<(), Error> { match rollop { @@ -2251,7 +2254,7 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { // An algorithm roll is similar to a CSK roll. The main difference is that // to zone is signed with all keys before introducing the DS records for // the new KSKs or CSKs. -fn algorithm_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { +fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { // First check if the current algorithm-roll state is idle. We need From 0fa094bb9812b0ebcf8a2651e4f46d5923505d8e Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 27 Jun 2025 14:22:29 +0200 Subject: [PATCH 467/569] A bit more cleanup. --- src/rdata/dnssec.rs | 226 +++++++++++++++++++++++++++++--------------- 1 file changed, 148 insertions(+), 78 deletions(-) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 8fa57a552..d30cd60eb 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -26,11 +26,9 @@ use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::Parser; #[cfg(feature = "serde")] use octseq::serde::{DeserializeOctets, SerializeOctets}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[cfg(feature = "std")] use std::vec::Vec; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; -use std::time::Duration; use time::{Date, Month, PrimitiveDateTime, Time}; //------------ Dnskey -------------------------------------------------------- @@ -708,44 +706,45 @@ impl Timestamp { self.0.into_int() } - /// Return a SystemTime that has to meet two requirements: + /// Return a SystemTime that meets two requirements: /// 1) The SystemTime value has a duration since UNIX_EPOCH that - /// modulo 2**32 is equal to our value. + /// modulo 2**32 is equal to our Timestamp value. /// 2) The time difference between the SystemTime value and the - /// reference fits within an i32. + /// reference time fits in an i32. + /// + /// This can be used to sort Timestamp values. #[must_use] pub fn to_system_time(&self, reference: SystemTime) -> SystemTime { - // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because - // the timestamp may be too far in the future. We may have to add - // n * 2**32 for some unknown value of n. - const POW_2_32: u64 = 0x1_0000_0000; - let ref_secs = reference.duration_since(UNIX_EPOCH).unwrap().as_secs(); - let k = ref_secs / POW_2_32; - let ref_secs_mod = ref_secs % POW_2_32; - let ts_secs = self.into_int() as u64; - let ts_secs = - if ts_secs < ref_secs_mod { - if ref_secs_mod - ts_secs <= POW_2_32/2 { - // Close enough, use k. - ts_secs + k*POW_2_32 - } - else { - // ts_secs is really beyond ref_secs, use k+1. - ts_secs + (k+1)*POW_2_32 - } - } else { // ts_secs >= ref_secs_mod - if ts_secs - ref_secs_mod < POW_2_32/2 { - // Close enough, use k. - ts_secs + k*POW_2_32 - } - else { - // ts_secs is really old than ref_secs. Try to use k-1 - // but only if k is not zero. - let k = if k > 0 { k-1 } else { k }; - ts_secs + k*POW_2_32 - } - }; - UNIX_EPOCH + Duration::from_secs(ts_secs) + // Timestamp is a 32-bit value. We cannot just add UNIX_EPOCH because + // the timestamp may be too far in the future. We may have to add + // n * 2**32 for some unknown value of n. + const POW_2_32: u64 = 0x1_0000_0000; + let ref_secs = + reference.duration_since(UNIX_EPOCH).unwrap().as_secs(); + let k = ref_secs / POW_2_32; + let ref_secs_mod = ref_secs % POW_2_32; + let ts_secs = self.into_int() as u64; + let ts_secs = if ts_secs < ref_secs_mod { + if ref_secs_mod - ts_secs <= POW_2_32 / 2 { + // Close enough, use k. + ts_secs + k * POW_2_32 + } else { + // ts_secs is really beyond ref_secs, use k+1. + ts_secs + (k + 1) * POW_2_32 + } + } else { + // ts_secs >= ref_secs_mod + if ts_secs - ref_secs_mod < POW_2_32 / 2 { + // Close enough, use k. + ts_secs + k * POW_2_32 + } else { + // ts_secs is really old than ref_secs. Try to use k-1 + // but only if k is not zero. + let k = if k > 0 { k - 1 } else { k }; + ts_secs + k * POW_2_32 + } + }; + UNIX_EPOCH + Duration::from_secs(ts_secs) } } @@ -3037,46 +3036,117 @@ mod test { #[test] fn timestamp_to_system_time() { - struct Params { - ts: u32, - ref_ts: u64, - res: u64 - } - let tests = vec![ - // Simple cases, ts and ref_ts mod 2**32 are within 2*31-1. - // First ts less than ref_ts mod 2**32. - Params { ts: 0x0000_0000, ref_ts: 0x1_7fff_ffff, res: 0x1_0000_0000 }, - Params { ts: 0x7fff_ffff, ref_ts: 0x1_8000_0000, res: 0x1_7fff_ffff }, - Params { ts: 0x8000_0000, ref_ts: 0x1_ffff_ffff, res: 0x1_8000_0000 }, - // Then ts larger than ref_ts mod 2**32. - Params { ts: 0x7fff_ffff, ref_ts: 0x1_0000_0000, res: 0x1_7fff_ffff }, - Params { ts: 0x8000_0000, ref_ts: 0x1_7fff_ffff, res: 0x1_8000_0000 }, - Params { ts: 0xffff_ffff, ref_ts: 0x1_8000_0000, res: 0x1_ffff_ffff }, - - // Next, cases where the difference between ts and ref_ts mod 2**32 - // are at least 2**31+1. - Params { ts: 0x0000_0000, ref_ts: 0x1_8000_0001, res: 0x2_0000_0000 }, - Params { ts: 0x7fff_fffe, ref_ts: 0x1_ffff_ffff, res: 0x2_7fff_fffe }, - Params { ts: 0x8000_0001, ref_ts: 0x1_0000_0000, res: 0x0_8000_0001 }, - Params { ts: 0xffff_ffff, ref_ts: 0x1_7fff_fffe, res: 0x0_ffff_ffff }, - // Test cases where the difference is exactly 2**31. - Params { ts: 0x0000_0000, ref_ts: 0x1_8000_0000, res: 0x1_0000_0000 }, - Params { ts: 0x7fff_ffff, ref_ts: 0x1_ffff_ffff, res: 0x1_7fff_ffff }, - Params { ts: 0x8000_0000, ref_ts: 0x1_0000_0000, res: 0x0_8000_0000 }, - Params { ts: 0xffff_ffff, ref_ts: 0x1_7fff_ffff, res: 0x0_ffff_ffff }, - // Special case: ERA 0. We don't want values before UNIX_EPOCH. - Params { ts: 0x8000_0001, ref_ts: 0x0_0000_0000, res: 0x0_8000_0001 }, - Params { ts: 0xffff_ffff, ref_ts: 0x0_7fff_fffe, res: 0x0_ffff_ffff }, - Params { ts: 0x8000_0000, ref_ts: 0x0_0000_0000, res: 0x0_8000_0000 }, - Params { ts: 0xffff_ffff, ref_ts: 0x0_7fff_ffff, res: 0x0_ffff_ffff }, - ]; - - for t in tests { - let ts = Timestamp(Serial(t.ts)); - let ref_ts = UNIX_EPOCH + Duration::from_secs(t.ref_ts); - let res = ts.to_system_time(ref_ts); - let res = res.duration_since(UNIX_EPOCH).unwrap().as_secs(); - assert_eq!(res, t.res); - } + struct Params { + ts: u32, + ref_ts: u64, + res: u64, + } + let tests = vec![ + // Simple cases, ts and ref_ts mod 2**32 are within 2*31-1. + // First ts less than ref_ts mod 2**32. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_7fff_ffff, + res: 0x1_0000_0000, + }, + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_8000_0000, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_ffff_ffff, + res: 0x1_8000_0000, + }, + // Then ts larger than ref_ts mod 2**32. + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_0000_0000, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_7fff_ffff, + res: 0x1_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_8000_0000, + res: 0x1_ffff_ffff, + }, + // Next, cases where the difference between ts and ref_ts mod 2**32 + // are at least 2**31+1. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_8000_0001, + res: 0x2_0000_0000, + }, + Params { + ts: 0x7fff_fffe, + ref_ts: 0x1_ffff_ffff, + res: 0x2_7fff_fffe, + }, + Params { + ts: 0x8000_0001, + ref_ts: 0x1_0000_0000, + res: 0x0_8000_0001, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_7fff_fffe, + res: 0x0_ffff_ffff, + }, + // Test cases where the difference is exactly 2**31. + Params { + ts: 0x0000_0000, + ref_ts: 0x1_8000_0000, + res: 0x1_0000_0000, + }, + Params { + ts: 0x7fff_ffff, + ref_ts: 0x1_ffff_ffff, + res: 0x1_7fff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x1_0000_0000, + res: 0x0_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x1_7fff_ffff, + res: 0x0_ffff_ffff, + }, + // Special case: ERA 0. We don't want values before UNIX_EPOCH. + Params { + ts: 0x8000_0001, + ref_ts: 0x0_0000_0000, + res: 0x0_8000_0001, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x0_7fff_fffe, + res: 0x0_ffff_ffff, + }, + Params { + ts: 0x8000_0000, + ref_ts: 0x0_0000_0000, + res: 0x0_8000_0000, + }, + Params { + ts: 0xffff_ffff, + ref_ts: 0x0_7fff_ffff, + res: 0x0_ffff_ffff, + }, + ]; + + for t in tests { + let ts = Timestamp(Serial(t.ts)); + let ref_ts = UNIX_EPOCH + Duration::from_secs(t.ref_ts); + let res = ts.to_system_time(ref_ts); + let res = res.duration_since(UNIX_EPOCH).unwrap().as_secs(); + assert_eq!(res, t.res); + } } } From 0c81a154c808492b00d95814c727412d2573f6e3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:34:36 +0200 Subject: [PATCH 468/569] KMIP HSMs do the hashing and OID prefixing themselves. --- src/crypto/kmip.rs | 236 ++++++++++++++++++++++++--------------------- 1 file changed, 127 insertions(+), 109 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 3331daf29..3f6f6ec85 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -10,8 +10,9 @@ use core::fmt; use std::{string::String, vec::Vec}; use bcder::decode::SliceSource; +use bytes::BufMut; use kmip::types::{ - common::{KeyMaterial, TransparentRSAPublicKey}, + common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, response::ManagedObject, }; use log::error; @@ -154,7 +155,7 @@ impl PublicKey { let res = client .get_key(&self.public_key_id) .inspect_err(|err| error!("{err}"))?; - + dbg!(&res); let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object))); @@ -184,48 +185,71 @@ impl PublicKey { let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { - // This is what we get with PyKMIP using RSASHA256 and - // Fortanix using ECDSAP256SHA256. With Fortanix it - // appears to be a DER encoded SubjectPublicKeyInfo - // data structure of the form: - // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) - // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) - // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type - // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve - // subjectPublicKey BIT_STRING @23+66: (520 bit) - let source = SliceSource::new(&bytes); - let public_key = - rpki::crypto::PublicKey::decode(source).unwrap(); - let bits = public_key.bits().to_vec(); - - // For RSA, the bits are also DER encoded of the form: - // RSAPrivateKey SEQUENCE (2 elem) - // version Version INTEGER (1024 bit) 140670898145304244147145320460151523064481569650486421654946000437850… - // modulus INTEGER 65537 - // - // or is it really: - // RSAPrivateKey SEQUENCE (2 elem) - // modulus INTEGER - // publicExponent INTEGER - - // if public_key.algorithm() == PublicKeyFormat::Rsa { - // let source = SliceSource::new(&bits); - // let mut modulus = vec![]; - // let mut public_exponent = vec![]; - // bcder::Mode::Der.decode(source, |cons| { - // cons.take_sequence(|cons| { - // modulus = bcder::string::BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); - // public_exponent = BitString::take_from(cons)?.octet_slice().unwrap().to_vec(); - // Ok(()) - // }) - // }).unwrap(); - // rsa_encode(&public_exponent, &modulus) - // } else { - bits - // } + // Note: With Fortanix the ECDSAP256SHA256 key data appears to + // be a DER encoded SubjectPublicKeyInfo data structure of the + // form: + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + + // Handle key format type PKCS1 + match public_key.key_block.key_format_type { + KeyFormatType::PKCS1 => { + // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public + // key data like so: + // RSAPublicKey::=SEQUENCE{ + // modulus INTEGER, -- n + // publicExponent INTEGER -- e } + // + // Parse it an encode the modulus and exponent as + let source = SliceSource::new(&bytes); + bcder::Mode::Der.decode(source, |cons| { + cons.take_sequence(|cons| { + fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] { + bytes + .iter() + .position(|&v| v != 0) + .map(|idx| &bytes[idx..]) + .unwrap_or_default() + } + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + let m = modulus.as_slice(); + let m = trim_leading_zeroes(m); + let e = public_exponent.as_slice(); + let e = trim_leading_zeroes(e); + let e_len = e.len(); + let mut res = vec![]; + assert!(e_len >= 1 && e_len <= (4096/8)); + if e_len < 255 { + res.put_u8(e_len as u8); + } else { + res.put_u8(0); + res.extend_from_slice(&(e_len as u16).to_be_bytes()); + } + res.extend_from_slice(e); + res.extend_from_slice(m); + Ok(res) + }) + }).unwrap() + }, + + KeyFormatType::Raw => { + // Fortanix DSM + let source = SliceSource::new(&bytes); + let public_key = + rpki::crypto::PublicKey::decode(source).unwrap(); + public_key.bits().to_vec() + } + + _ => todo!(), + } } KeyMaterial::TransparentRSAPublicKey( + // Nameshed-HSM-Relay TransparentRSAPublicKey { modulus, public_exponent, @@ -249,17 +273,19 @@ pub mod sign { use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, DigitalSignatureAlgorithm, - HashingAlgorithm, UniqueIdentifier, + HashingAlgorithm, PaddingMethod, UniqueIdentifier, }; use kmip::types::request::{ self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, PublicKeyTemplateAttribute, RequestPayload, }; - use kmip::types::response::ResponsePayload; + use kmip::types::response::{ + CreateKeyPairResponsePayload, ResponsePayload, + }; use log::{debug, error}; use crate::base::iana::SecurityAlgorithm; - use crate::crypto::common::{DigestBuilder, DigestType}; + use crate::crypto::common::DigestType; use crate::crypto::kmip::{GenerateError, PublicKey}; use crate::crypto::kmip_pool::KmipConnPool; use crate::crypto::sign::{ @@ -331,7 +357,10 @@ pub mod sign { .unwrap() } - fn sign_raw(&self, data: &[u8]) -> Result { + fn sign_raw( + &self, + data: &[u8], + ) -> Result { // https://www.rfc-editor.org/rfc/rfc5702.html#section-3 // 3. RRSIG Resource Records // "The value of the signature field in the RRSIG RR follows the @@ -356,9 +385,8 @@ pub mod sign { // // hex 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20" // - // We assume that the HSM signing operation implements this signing - // operation according to these rules. - + // KMIP HSMs implement the Sign opration operation according to + // these rules. let (crypto_alg, hashing_alg, digest_type) = match self.algorithm { SecurityAlgorithm::RSASHA256 => ( @@ -374,51 +402,14 @@ pub mod sign { _ => return Err(SignError), }; - // TODO: For HSMs that don't support hashing do we have to do - // hashing ourselves here after signing? Note: PyKMIP doesn't - // support CryptographicParameters (and thus also not - // HashingFunction) nor does it support the Hash operation. - // Maybe via crypto::common::DigestBuilder? - // - // TODO: Where do we find out what the HSM supports? Trying an - // operation then falling back each time it fails is inefficient. - // We can presumably instead discover this on first use of the - // HSM, ala how Krill does HSM probing. We would need to know the - // result of such probing, which features are supported, here. We - // only have access to the KMIP connection pool here, so I guess - // that has to be able to tell us what we want to know. - // - // Note: OpenDNSSEC does its own hashing. Trying to do SHA256 - // hashing ourselves and then not passing a hashing algorithm to - // the Sign operation below results (with Fortanix at least) in - // error "Must specify HashingAlgorithm". OpenDNSSEC code comments - // say this is done because "some HSMs don't really handle - // CKM_SHA1_RSA_PKCS well". - let mut ctx = DigestBuilder::new(digest_type); - ctx.update(data); - let digest = ctx.finish(); - let mut data = digest.as_ref(); - - // OpenDNSSEC says that for RSA the prefix must be added to the - // buffer manually first as "CKM_RSA_PKCS does the padding, but - // cannot know the identifier prefix, so we need to add that - // ourselves." - let mut new_data; - if matches!(self.algorithm, SecurityAlgorithm::RSASHA256) { - new_data = vec![ - 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48, - 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, - 0x20, - ]; - new_data.extend_from_slice(data); - data = &new_data; - } - let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some( CryptographicParameters::default() - // .with_padding_method(PaddingMethod::) + // PyKMIP requires that the padding method be + // specified otherwise it complains with: "For + // signing, a padding method must be specified." + .with_padding_method(PaddingMethod::PKCS1_v1_5) .with_hashing_algorithm(hashing_alg) .with_cryptographic_algorithm(crypto_alg), ), @@ -449,13 +440,6 @@ pub mod sign { match self.algorithm { SecurityAlgorithm::RSASHA256 => { - // Ok(Signature::RsaSha256(Box::<[u8; 64]>::new( - // signed - // .signature_data - // .into_boxed_slice() - // .inspect_err(|err| eprintln!("Signing7: Error")) - // .map_err(|_| SignError)?, - // ))) Ok(Signature::RsaSha256( signed.signature_data.into_boxed_slice(), )) @@ -696,13 +680,46 @@ pub mod sign { return Err(GenerateError::Kmip("Unable to parse KMIP response: payload should be CreateKeyPair".to_string())); }; - Ok(KeyPair { + let CreateKeyPairResponsePayload { + private_key_unique_identifier, + public_key_unique_identifier, + } = payload; + + let key_pair = KeyPair { algorithm, - private_key_id: payload.private_key_unique_identifier.to_string(), - public_key_id: payload.public_key_unique_identifier.to_string(), + private_key_id: private_key_unique_identifier.to_string(), + public_key_id: public_key_unique_identifier.to_string(), conn_pool, flags, - }) + }; + + // Activate the key if not already, otherwise it cannot be used for signing. + if !activate_on_create { + let request = + RequestPayload::Activate(Some(private_key_unique_identifier)); + + // Execute the request and capture the response + let response = client.do_request(request).map_err(|err| { + eprintln!("KMIP activate private key request failed: {err}"); + eprintln!( + "KMIP last request: {}", + client.last_req_diag_str().unwrap_or_default() + ); + eprintln!( + "KMIP last response: {}", + client.last_res_diag_str().unwrap_or_default() + ); + GenerateError::Kmip(err.to_string()) + })?; + + // Process the successful response + let ResponsePayload::Activate(_) = response else { + error!("KMIP request failed: Wrong response type received!"); + return Err(GenerateError::Kmip("Unable to parse KMIP response: payload should be Activate".to_string())); + }; + } + + Ok(key_pair) } } @@ -779,16 +796,16 @@ mod tests { ); let res = generate( generated_key_name, - //crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, - crate::crypto::sign::GenerateParams::EcdsaP256Sha256, + crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, + // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, pool, ); dbg!(&res); - let _key = res.unwrap(); + let key = res.unwrap(); - // let dnskey = key.dnskey(); - // eprintln!("DNSKEY: {}", dnskey); + let dnskey = key.dnskey(); + eprintln!("DNSKEY: {}", dnskey); } #[test] @@ -855,11 +872,12 @@ mod tests { let dnskey = key.dnskey(); eprintln!("DNSKEY: {}", dnskey); - // // Fortanix: Activating the public key also activates the private key. - // // Attempting to then activate the private key fails as it is already - // // active. Yet signing fails with "Object is not yet active"... client.activate_key(key.public_key_id()).unwrap(); - // // client.activate_key(key.private_key_id()).unwrap(); + + // Fortanix: Activating the public key also activates the private key. + // Attempting to then activate the private key fails as it is already + // active. Yet signing fails with "Object is not yet active"... + // client.activate_key(key.private_key_id()).unwrap(); // // This works round the not yet active yet error. // sleep(Duration::from_secs(5)); From be1ce2356811983ca95cb55ef9bb82531015d2ca Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:34:46 +0200 Subject: [PATCH 469/569] Added some comments. --- src/crypto/kmip_pool.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 7ecf7adbc..017da996f 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -13,10 +13,26 @@ use std::net::TcpStream; use std::{sync::Arc, time::Duration}; use kmip::client::{Client, ConnectionSettings}; + +// TODO: Remove the hard-coded use of OpenSSL? use openssl::ssl::SslStream; +/// A KMIP client used to send KMIP requests and receive KMIP responses. +/// +/// Note: Currently depends on OpenSSL because our KMIP implementation +/// supports elements of KMIP 1.2 [1] but not KMIP 1.3 [2], but prior to KMIP +/// 1.3 it is required for servers to support TLS 1.0, and RustLS doesn't +/// support TLS < 1.2. +/// +/// [1]: https://docs.oasis-open.org/kmip/profiles/v1.2/os/kmip-profiles-v1.2-os.html#_Toc409613167 +/// [2]: https://docs.oasis-open.org/kmip/profiles/v1.3/os/kmip-profiles-v1.3-os.html#_Toc473103053 pub type KmipTlsClient = Client>; +/// A pool of already connected KMIP clients. +/// +/// This pool can be used to acquire a KMIP client without first having to +/// wait for it to connect at the TCP/TLS level, and without unnecessarily +/// closing the connection when finished. pub type KmipConnPool = r2d2::Pool; /// Manages KMIP TCP + TLS connection creation. From 012f243506658d95334b30335e451276cf077c4a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:46:41 +0200 Subject: [PATCH 470/569] Cargo fmt. --- src/crypto/kmip.rs | 13 ++++--------- src/crypto/kmip_pool.rs | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 3f6f6ec85..73a76aaba 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -357,10 +357,7 @@ pub mod sign { .unwrap() } - fn sign_raw( - &self, - data: &[u8], - ) -> Result { + fn sign_raw(&self, data: &[u8]) -> Result { // https://www.rfc-editor.org/rfc/rfc5702.html#section-3 // 3. RRSIG Resource Records // "The value of the signature field in the RRSIG RR follows the @@ -439,11 +436,9 @@ pub mod sign { }; match self.algorithm { - SecurityAlgorithm::RSASHA256 => { - Ok(Signature::RsaSha256( - signed.signature_data.into_boxed_slice(), - )) - } + SecurityAlgorithm::RSASHA256 => Ok(Signature::RsaSha256( + signed.signature_data.into_boxed_slice(), + )), SecurityAlgorithm::ECDSAP256SHA256 => { let signature = openssl::ecdsa::EcdsaSig::from_der( &signed.signature_data, diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 017da996f..07e65a1ae 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -23,13 +23,13 @@ use openssl::ssl::SslStream; /// supports elements of KMIP 1.2 [1] but not KMIP 1.3 [2], but prior to KMIP /// 1.3 it is required for servers to support TLS 1.0, and RustLS doesn't /// support TLS < 1.2. -/// +/// /// [1]: https://docs.oasis-open.org/kmip/profiles/v1.2/os/kmip-profiles-v1.2-os.html#_Toc409613167 /// [2]: https://docs.oasis-open.org/kmip/profiles/v1.3/os/kmip-profiles-v1.3-os.html#_Toc473103053 pub type KmipTlsClient = Client>; /// A pool of already connected KMIP clients. -/// +/// /// This pool can be used to acquire a KMIP client without first having to /// wait for it to connect at the TCP/TLS level, and without unnecessarily /// closing the connection when finished. From 4e35125e99bc014ffe5014e511eeb74af2195092 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 10:58:36 +0200 Subject: [PATCH 471/569] Bump dependencies. --- Cargo.lock | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 92c515d50..4681e96f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -92,7 +92,7 @@ checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -140,9 +140,9 @@ checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "bumpalo" -version = "3.18.1" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] name = "byteorder" @@ -242,7 +242,7 @@ checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -309,7 +309,7 @@ version = "0.11.1-dev" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -438,7 +438,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -592,9 +592,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.9.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" dependencies = [ "equivalent", "hashbrown 0.15.4", @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#bf390a26ef65e9b62cb1a7f3bbafb8041bd4253f" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#a85e5f83e6a1217dbd06c9aa9ce4201a24c5eefa" dependencies = [ "cfg-if", "enum-display-derive", @@ -648,7 +648,7 @@ dependencies = [ [[package]] name = "kmip-ttlv" version = "0.4.0" -source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#d626d4c549eddeeb1064a6e8213b65707072fb11" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#38e72487d218f57649b5173483296be8946ef4d1" dependencies = [ "cfg-if", "hex", @@ -717,7 +717,7 @@ checksum = "5cf92c10c7e361d6b99666ec1c6f9805b0bea2c3bd8c78dc6fe98ac5bd78db11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -849,7 +849,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -926,7 +926,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1167,7 +1167,7 @@ dependencies = [ "regex", "relative-path", "rustc_version", - "syn 2.0.103", + "syn 2.0.104", "unicode-ident", ] @@ -1304,7 +1304,7 @@ checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1409,9 +1409,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.103" +version = "2.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" dependencies = [ "proc-macro2", "quote", @@ -1441,7 +1441,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1508,7 +1508,7 @@ checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1599,7 +1599,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1730,7 +1730,7 @@ dependencies = [ "log", "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-shared", ] @@ -1752,7 +1752,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1860,7 +1860,7 @@ checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -1871,7 +1871,7 @@ checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] @@ -2106,7 +2106,7 @@ checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" dependencies = [ "proc-macro2", "quote", - "syn 2.0.103", + "syn 2.0.104", ] [[package]] From 91f260e0a908d81ce893327cfd33539e232ac3d2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 13:34:37 +0200 Subject: [PATCH 472/569] Remove dbg and fix typo in comment. --- src/crypto/kmip.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 73a76aaba..8b5426f43 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -155,7 +155,6 @@ impl PublicKey { let res = client .get_key(&self.public_key_id) .inspect_err(|err| error!("{err}"))?; - dbg!(&res); let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object))); @@ -165,7 +164,7 @@ impl PublicKey { // "“Raw” key format is intended to be applied to symmetric keys // and not asymmetric keys" // - // As we deal in assymetric keys (RSA, ECDSA), not symmetric keys, + // As we deal in asymmetric keys (RSA, ECDSA), not symmetric keys, // we should not encounter public_key.key_block.key_format_type == // KeyFormatType::Raw. However, Fortanix DSM returns // KeyFormatType::Raw when fetching key data for an ECDSA public key. From ee793ca251938bb181496bb7d04b78d22ec53c82 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:29:35 +0200 Subject: [PATCH 473/569] Handle conversion of RSA public key raw bytes to DNSKEY RDATA format. --- src/crypto/kmip.rs | 68 ++++++++++++++++++++++++---------------------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 8b5426f43..2461390ab 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -10,12 +10,11 @@ use core::fmt; use std::{string::String, vec::Vec}; use bcder::decode::SliceSource; -use bytes::BufMut; use kmip::types::{ common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, response::ManagedObject, }; -use log::error; +use log::{debug, error}; pub use kmip::client::ConnectionSettings; @@ -23,6 +22,7 @@ use crate::{ base::iana::SecurityAlgorithm, crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, rdata::Dnskey, + utils::base16, }; /// An error in generating a key pair with OpenSSL. @@ -184,6 +184,8 @@ impl PublicKey { let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { + debug!("Key Format Type: {:?}", public_key.key_block.key_format_type); + debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); // Note: With Fortanix the ECDSAP256SHA256 key data appears to // be a DER encoded SubjectPublicKeyInfo data structure of the // form: @@ -204,43 +206,24 @@ impl PublicKey { // // Parse it an encode the modulus and exponent as let source = SliceSource::new(&bytes); - bcder::Mode::Der.decode(source, |cons| { - cons.take_sequence(|cons| { - fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] { - bytes - .iter() - .position(|&v| v != 0) - .map(|idx| &bytes[idx..]) - .unwrap_or_default() - } - let modulus = bcder::Unsigned::take_from(cons)?; - let public_exponent = bcder::Unsigned::take_from(cons)?; - let m = modulus.as_slice(); - let m = trim_leading_zeroes(m); - let e = public_exponent.as_slice(); - let e = trim_leading_zeroes(e); - let e_len = e.len(); - let mut res = vec![]; - assert!(e_len >= 1 && e_len <= (4096/8)); - if e_len < 255 { - res.put_u8(e_len as u8); - } else { - res.put_u8(0); - res.extend_from_slice(&(e_len as u16).to_be_bytes()); - } - res.extend_from_slice(e); - res.extend_from_slice(m); - Ok(res) - }) - }).unwrap() + encode_asn1_rsa_key_as_dnskey_rdata(source) }, KeyFormatType::Raw => { - // Fortanix DSM + // For an RSA key Fortanix DSM supplies: + // SubjectPublicKeyInfo SEQUENCE (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE (2 elem) + // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) + // parameter ANY NULL + // subjectPublicKey BIT STRING (2160 bit) 001100001000001000000001000010100000001010000010000000010000000100000… + // SEQUENCE (2 elem) + // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… + // INTEGER 65537 let source = SliceSource::new(&bytes); let public_key = rpki::crypto::PublicKey::decode(source).unwrap(); - public_key.bits().to_vec() + let source = SliceSource::new(public_key.bits()); + encode_asn1_rsa_key_as_dnskey_rdata(source) } _ => todo!(), @@ -262,6 +245,25 @@ impl PublicKey { } } +// RSAPublicKey::=SEQUENCE{ +// modulus INTEGER, -- n +// publicExponent INTEGER -- e } +fn encode_asn1_rsa_key_as_dnskey_rdata( + source: SliceSource<'_>, +) -> Vec { + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + let modulus = bcder::Unsigned::take_from(cons)?; + let public_exponent = bcder::Unsigned::take_from(cons)?; + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + Ok(crate::crypto::common::rsa_encode(e, n)) + }) + }) + .unwrap() +} + #[cfg(feature = "unstable-crypto-sign")] pub mod sign { use std::boxed::Box; From b7b9e6e958286b43c64cb4f9ab1f1dfd86d33879 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:29:51 +0200 Subject: [PATCH 474/569] Upgrade to latest kmip-protocol. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 4681e96f1..2f126a29c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -628,7 +628,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#a85e5f83e6a1217dbd06c9aa9ce4201a24c5eefa" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#8b9bef377247046e8d6094114890fdc5e0827c60" dependencies = [ "cfg-if", "enum-display-derive", From fd6960404e963ac4be30523707dfa3cac60b585a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 30 Jun 2025 13:16:47 +0200 Subject: [PATCH 475/569] Make 'ring' keypairs thread-safe 'SecureRandom' is only implemented by ring's 'SystemRandom' type, which is thread-safe. However, this property was lost when the trait object was used. Re-introducing these bounds makes 'KeyPair' thread-safe. (cherry picked from commit 6a9982f6c4e803b572ed4dd4f95924980c818cd1) --- src/crypto/ring.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index 6ce4f3b18..d0c818700 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -341,7 +341,7 @@ pub mod sign { flags: u16, /// Random number generator. - rng: Arc, + rng: Arc, }, /// An ECDSA P-256/SHA-256 keypair. @@ -353,7 +353,7 @@ pub mod sign { flags: u16, /// Random number generator. - rng: Arc, + rng: Arc, }, /// An ECDSA P-384/SHA-384 keypair. @@ -365,7 +365,7 @@ pub mod sign { flags: u16, /// Random number generator. - rng: Arc, + rng: Arc, }, /// An Ed25519 keypair. From e3d600c4c0f8806a85d5b79ba26879891c9b53ea Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:13:17 +0200 Subject: [PATCH 476/569] Re-export ClientCertificate from the kmip-protocol crate to prevent clients needing to depend on kmip-protocol themselves for just this one type. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 2461390ab..b7a0dde76 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -16,7 +16,7 @@ use kmip::types::{ }; use log::{debug, error}; -pub use kmip::client::ConnectionSettings; +pub use kmip::client::{ClientCertificate, ConnectionSettings}; use crate::{ base::iana::SecurityAlgorithm, From becc840a088a3c6b28e32107f235ab1fe0bc1d78 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:13:37 +0200 Subject: [PATCH 477/569] FIX: Make logs appear in dnst. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index b7a0dde76..fcf50f9ef 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -14,7 +14,7 @@ use kmip::types::{ common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, response::ManagedObject, }; -use log::{debug, error}; +use tracing::{debug, error}; pub use kmip::client::{ClientCertificate, ConnectionSettings}; From 951890e06133d5911d94ea66998ff4fec32ac696 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 2 Jul 2025 23:14:09 +0200 Subject: [PATCH 478/569] FIX: Make logs appear in dnst. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index fcf50f9ef..5d0e6f792 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -283,7 +283,7 @@ pub mod sign { use kmip::types::response::{ CreateKeyPairResponsePayload, ResponsePayload, }; - use log::{debug, error}; + use tracing::{debug, error}; use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; From 8638ac4d2b3e962a12ebc70376badc3781430929 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 3 Jul 2025 16:54:18 +0200 Subject: [PATCH 479/569] Fix KMIP ECDSA support. Tested with Fortanix DSM. --- src/crypto/kmip.rs | 245 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 194 insertions(+), 51 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 5d0e6f792..9361bef17 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -9,7 +9,7 @@ use core::fmt; use std::{string::String, vec::Vec}; -use bcder::decode::SliceSource; +use bcder::{decode::SliceSource, BitString, Oid}; use kmip::types::{ common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, response::ManagedObject, @@ -184,33 +184,45 @@ impl PublicKey { let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { + debug!("Cryptographic Algorithm: {:?}", public_key.key_block.cryptographic_algorithm); debug!("Key Format Type: {:?}", public_key.key_block.key_format_type); debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); - // Note: With Fortanix the ECDSAP256SHA256 key data appears to - // be a DER encoded SubjectPublicKeyInfo data structure of the - // form: - // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) - // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) - // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type - // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve - // subjectPublicKey BIT_STRING @23+66: (520 bit) // Handle key format type PKCS1 - match public_key.key_block.key_format_type { - KeyFormatType::PKCS1 => { + match (algorithm, public_key.key_block.key_format_type) { + (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) => { // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public // key data like so: // RSAPublicKey::=SEQUENCE{ // modulus INTEGER, -- n // publicExponent INTEGER -- e } - // - // Parse it an encode the modulus and exponent as let source = SliceSource::new(&bytes); - encode_asn1_rsa_key_as_dnskey_rdata(source) + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: missing modulus".into())); + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: missing public exponent".into())); + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) }, - KeyFormatType::Raw => { - // For an RSA key Fortanix DSM supplies: + (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) => { + // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) // SubjectPublicKeyInfo SEQUENCE (2 elem) // algorithm AlgorithmIdentifier SEQUENCE (2 elem) // algorithm OBJECT IDENTIFIER 1.2.840.113549.1.1.1 rsaEncryption (PKCS #1) @@ -220,10 +232,119 @@ impl PublicKey { // INTEGER (2048 bit) 229677698057230630160769379936346719377896297586216888467726484346678… // INTEGER 65537 let source = SliceSource::new(&bytes); - let public_key = - rpki::crypto::PublicKey::decode(source).unwrap(); - let source = SliceSource::new(public_key.bits()); - encode_asn1_rsa_key_as_dnskey_rdata(source) + let mut modulus = None; + let mut public_exponent = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != rpki::oid::RSA_ENCRYPTION { + return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); + } + // Ignore the parameters. + Ok(()) + })?; + cons.take_sequence(|cons| { + modulus = Some(bcder::Unsigned::take_from(cons)?); + public_exponent = Some(bcder::Unsigned::take_from(cons)?); + Ok(()) + }) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(modulus) = modulus else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into())); + }; + + let Some(public_exponent) = public_exponent else { + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into())); + }; + + let n = modulus.as_slice(); + let e = public_exponent.as_slice(); + crate::crypto::common::rsa_encode(e, n) + } + + (SecurityAlgorithm::ECDSAP256SHA256, KeyFormatType::Raw) => { + // For an ECDSA key Fortanix DSM supplies: (from https://asn1js.eu/) + // SubjectPublicKeyInfo SEQUENCE @0+89 (constructed): (2 elem) + // algorithm AlgorithmIdentifier SEQUENCE @2+19 (constructed): (2 elem) + // algorithm OBJECT_IDENTIFIER @4+7: 1.2.840.10045.2.1|ecPublicKey|ANSI X9.62 public key type + // parameters ANY OBJECT_IDENTIFIER @13+8: 1.2.840.10045.3.1.7|prime256v1|ANSI X9.62 named elliptic curve + // subjectPublicKey BIT_STRING @23+66: (520 bit) + // + // From: https://www.rfc-editor.org/rfc/rfc5480.html#section-2.1.1 + // The parameter for id-ecPublicKey is as follows and MUST always be + // present: + // + // ECParameters ::= CHOICE { + // namedCurve OBJECT IDENTIFIER + // -- implicitCurve NULL + // -- specifiedCurve SpecifiedECDomain + // } + // -- implicitCurve and specifiedCurve MUST NOT be used in PKIX. + // -- Details for SpecifiedECDomain can be found in [X9.62]. + // -- Any future additions to this CHOICE should be coordinated + // -- with ANSI X9. + let source = SliceSource::new(&bytes); + tracing::info!("SPKI bytes: {}", base16::encode_display(&bytes)); + let mut bits = None; + bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + cons.take_sequence(|cons| { + let algorithm = Oid::take_from(cons)?; + if algorithm != rpki::oid::EC_PUBLIC_KEY { + Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) + } else { + let named_curve = Oid::take_from(cons)?; + if named_curve != rpki::oid::SECP256R1 { + return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); + } + Ok(()) + } + })?; + bits = Some(BitString::take_from(cons)?); + Ok(()) + }) + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; + + let Some(bits) = bits else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into())); + }; + + // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 + // "The subjectPublicKey from SubjectPublicKeyInfo is the ECC public key. + // ECC public keys have the following syntax: + // + // ECPoint ::= OCTET STRING + // ... + // The first octet of the OCTET STRING indicates whether the key is + // compressed or uncompressed. The uncompressed form is indicated + // by 0x04 and the compressed form is indicated by either 0x02 or + // 0x03 (see 2.3.3 in [SEC1]). The public key MUST be rejected if + // any other value is included in the first octet." + let Some(octets) = bits.octet_slice() else { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into())); + }; + + if octets.len() != 65 { + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [0x04, <32-byte X value>, <32-byte Y value>]".into())); + } + + // Note: OpenDNSSEC doesn't support the compressed form either. + let compression_flag = octets[0]; + if compression_flag != 0x04 { + return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}")))?; + } + + // Expect octet string to be X | Y (| denotes concatenation) where + // X and Y are each 32 bytes (because P-256 uses 256 bit values and + // 256 bits are 32 bytes). + + // Skip the compression flag. + octets[1..].to_vec() } _ => todo!(), @@ -245,25 +366,6 @@ impl PublicKey { } } -// RSAPublicKey::=SEQUENCE{ -// modulus INTEGER, -- n -// publicExponent INTEGER -- e } -fn encode_asn1_rsa_key_as_dnskey_rdata( - source: SliceSource<'_>, -) -> Vec { - bcder::Mode::Der - .decode(source, |cons| { - cons.take_sequence(|cons| { - let modulus = bcder::Unsigned::take_from(cons)?; - let public_exponent = bcder::Unsigned::take_from(cons)?; - let n = modulus.as_slice(); - let e = public_exponent.as_slice(); - Ok(crate::crypto::common::rsa_encode(e, n)) - }) - }) - .unwrap() -} - #[cfg(feature = "unstable-crypto-sign")] pub mod sign { use std::boxed::Box; @@ -293,6 +395,7 @@ pub mod sign { GenerateParams, SignError, SignRaw, Signature, }; use crate::rdata::Dnskey; + use crate::utils::base16; #[derive(Clone, Debug)] pub struct KeyPair { @@ -400,17 +503,22 @@ pub mod sign { _ => return Err(SignError), }; + // PyKMIP requires that the padding method be specified otherwise + // it complains with: "For signing, a padding method must be + // specified." + let mut cryptographic_parameters = + CryptographicParameters::default() + .with_hashing_algorithm(hashing_alg) + .with_cryptographic_algorithm(crypto_alg); + + if self.algorithm == SecurityAlgorithm::RSASHA256 { + cryptographic_parameters = cryptographic_parameters + .with_padding_method(PaddingMethod::PKCS1_v1_5); + } + let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), - Some( - CryptographicParameters::default() - // PyKMIP requires that the padding method be - // specified otherwise it complains with: "For - // signing, a padding method must be specified." - .with_padding_method(PaddingMethod::PKCS1_v1_5) - .with_hashing_algorithm(hashing_alg) - .with_cryptographic_algorithm(crypto_alg), - ), + Some(cryptographic_parameters), Data(data.as_ref().to_vec()), ); @@ -436,21 +544,53 @@ pub mod sign { unreachable!(); }; + debug!("Algorithm: {}", self.algorithm); + debug!( + "Signature Data: {}", + base16::encode_display(&signed.signature_data) + ); match self.algorithm { SecurityAlgorithm::RSASHA256 => Ok(Signature::RsaSha256( signed.signature_data.into_boxed_slice(), )), SecurityAlgorithm::ECDSAP256SHA256 => { + // ECDSA signature received from Fortanix DSM, decoded + // using this command: + // + // $ echo '' | xxd -r -p | dumpasn1 - + // 0 69: SEQUENCE { + // 2 33: INTEGER + // : 00 C6 A7 D1 2E A1 0C B4 96 BD D9 A5 48 2C 9B F4 + // : 0C EC 9F FC EF 1A 0D 59 BB B9 24 F3 FE DA DC F8 + // : 9E + // 37 32: INTEGER + // : 4B A7 22 69 F2 F8 65 88 63 D0 25 D3 A9 D5 92 4F + // : A2 21 BD 59 CD 27 60 6D 16 C3 79 EF B4 0A CA 33 + // : } + // + // Where the two integer values are known as 'r' and 's'. + // let source = SliceSource::new(&signed.signature_data); + // let (r, s) = bcder::Mode::Der + // .decode(source, |cons| { + // cons.take_sequence(|cons| { + // let r = bcder::Unsigned::take_from(cons)?; + // let s = bcder::Unsigned::take_from(cons)?; + // Ok((r, s)) + // }) + // }) + // .unwrap(); + + // 3046022100e4e87c417196c6e5cd63f93e94929ccda6d04fc0a7446922baf3070e854ec4f4022100a1ecd098008329de9bc93fb2ded6aaceecc921f7183d6b3cfc673b3ef8af219e let signature = openssl::ecdsa::EcdsaSig::from_der( &signed.signature_data, ) .unwrap(); - let mut r = signature.r().to_vec_padded(32).unwrap(); + let mut sig = signature.r().to_vec_padded(32).unwrap(); let mut s = signature.s().to_vec_padded(32).unwrap(); - r.append(&mut s); + sig.append(&mut s); Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( - r.try_into().map_err(|_| SignError)?, + sig.try_into().map_err(|_| SignError)?, ))) } SecurityAlgorithm::ECDSAP384SHA384 => { @@ -729,12 +869,15 @@ mod tests { use std::time::SystemTime; use std::vec::Vec; + use bcder::decode::SliceSource; use kmip::client::ConnectionSettings; + use openssl::bn::BigNum; use crate::crypto::kmip::sign::generate; use crate::crypto::kmip_pool::ConnectionManager; use crate::crypto::sign::SignRaw; use crate::logging::init_logging; + use crate::utils::base16; #[test] #[ignore = "Requires running PyKMIP"] From b236ccc65e8b020fd52e3a86bf2d7cc07c5cc36a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:33:25 +0200 Subject: [PATCH 480/569] Remove dependence on the rpki crate. --- Cargo.lock | 26 -------------------------- Cargo.toml | 3 +-- src/crypto/kmip.rs | 23 ++++++++++++++++++++--- 3 files changed, 21 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f126a29c..66a219f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,12 +116,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - [[package]] name = "bcder" version = "0.7.5" @@ -179,10 +173,7 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", - "js-sys", "num-traits", - "serde", - "wasm-bindgen", "windows-link", ] @@ -280,7 +271,6 @@ dependencies = [ "r2d2", "rand", "ring", - "rpki", "rstest", "rustls-pemfile", "rustversion", @@ -1125,22 +1115,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rpki" -version = "0.18.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a043d99463db58c05283f5ae5d9ced858cc3483011747264e21f50b9201cdd" -dependencies = [ - "base64", - "bcder", - "bytes", - "chrono", - "log", - "ring", - "untrusted", - "uuid", -] - [[package]] name = "rstest" version = "0.23.0" diff --git a/Cargo.toml b/Cargo.toml index 412cd3dba..d2da71cee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } bcder = { version = "0.7", optional = true } -rpki = { version = "0.18", optional = true, features = ["crypto"] } rustversion = { version = "1", optional = true } secrecy = { version = "0.10", optional = true } serde = { version = "1.0.130", optional = true, features = ["derive"] } @@ -71,7 +70,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:rpki", "dep:bcder"] +kmip = ["dep:kmip", "dep:r2d2", "dep:bcder"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 9361bef17..bb462f29e 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -70,6 +70,23 @@ impl fmt::Display for GenerateError { impl std::error::Error for GenerateError {} +/// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` +/// +/// Identifies an RSA public key with no limitation to either RSASSA-PSS or +/// RSAES-OEAP. +pub const RSA_ENCRYPTION_OID: ConstOid + = Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. +/// +/// Identifies public keys for elliptic curve cryptography. +pub const EC_PUBLIC_KEY_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 2, 1]); + +/// [RFC 5480](https://tools.ietf.org/html/rfc5480) `secp256r1`. +/// +/// Identifies the P-256 curve for elliptic curve cryptography. +pub const SECP256R1_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 3, 1, 7]); + pub struct PublicKey { algorithm: SecurityAlgorithm, @@ -239,7 +256,7 @@ impl PublicKey { cons.take_sequence(|cons| { cons.take_sequence(|cons| { let algorithm = Oid::take_from(cons)?; - if algorithm != rpki::oid::RSA_ENCRYPTION { + if algorithm != RSA_ENCRYPTION_OID { return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); } // Ignore the parameters. @@ -295,11 +312,11 @@ impl PublicKey { cons.take_sequence(|cons| { cons.take_sequence(|cons| { let algorithm = Oid::take_from(cons)?; - if algorithm != rpki::oid::EC_PUBLIC_KEY { + if algorithm != EC_PUBLIC_KEY_OID { Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm id-ecPublicKey is supported")) } else { let named_curve = Oid::take_from(cons)?; - if named_curve != rpki::oid::SECP256R1 { + if named_curve != SECP256R1_OID { return Err(cons.content_err("Only SubjectPublicKeyInfo with namedCurve secp256r1 is supported")); } Ok(()) From 53fd6ca803907023c9f15af6a34a6caa71ba773d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Thu, 3 Jul 2025 17:34:09 +0200 Subject: [PATCH 481/569] Add missing import. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index bb462f29e..14f479046 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -9,7 +9,7 @@ use core::fmt; use std::{string::String, vec::Vec}; -use bcder::{decode::SliceSource, BitString, Oid}; +use bcder::{decode::SliceSource, BitString, ConstOid, Oid}; use kmip::types::{ common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, response::ManagedObject, From aec3c7927a2a93b349302187159e328281ed59e5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:05:52 +0200 Subject: [PATCH 482/569] - Make the trim_leading_zeroes() fn more widely usable. - Don't panic on KMIP pool creation failure. - Don't panic on DNSKEY lookup failure. - Make connection pool timers optional. - Remove redundant log message. --- src/crypto/common.rs | 16 +++--- src/crypto/kmip.rs | 78 ++++++++++++++-------------- src/crypto/kmip_pool.rs | 28 +++++++--- src/crypto/openssl.rs | 12 ++--- src/crypto/ring.rs | 12 ++--- src/crypto/sign.rs | 11 ++-- src/dnssec/sign/keys/signingkey.rs | 4 +- src/dnssec/sign/signatures/rrsigs.rs | 31 ++++++----- 8 files changed, 107 insertions(+), 85 deletions(-) diff --git a/src/crypto/common.rs b/src/crypto/common.rs index bcfe27fff..aab22cf6c 100644 --- a/src/crypto/common.rs +++ b/src/crypto/common.rs @@ -211,14 +211,6 @@ pub fn rsa_exponent_modulus( /// Encode the RSA exponent and modulus components in DNSKEY record data /// format. Leading zeroes will be ignored per RFC 3110 section 2. pub fn rsa_encode(mut e: &[u8], mut n: &[u8]) -> Vec { - fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] { - bytes - .iter() - .position(|&v| v != 0) - .map(|idx| &bytes[idx..]) - .unwrap_or_default() - } - let mut key = Vec::new(); // Trim leading zeroes. @@ -243,6 +235,14 @@ pub fn rsa_encode(mut e: &[u8], mut n: &[u8]) -> Vec { key } +pub fn trim_leading_zeroes(bytes: &[u8]) -> &[u8] { + bytes + .iter() + .position(|&v| v != 0) + .map(|idx| &bytes[idx..]) + .unwrap_or_default() +} + //------------ AlgorithmError ------------------------------------------------ /// An algorithm error during verification. diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 14f479046..f9f47df2e 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -20,7 +20,7 @@ pub use kmip::client::{ClientCertificate, ConnectionSettings}; use crate::{ base::iana::SecurityAlgorithm, - crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, + crypto::{common::{rsa_encode, trim_leading_zeroes}, kmip_pool::KmipConnPool}, rdata::Dnskey, utils::base16, }; @@ -305,7 +305,6 @@ impl PublicKey { // -- Any future additions to this CHOICE should be coordinated // -- with ANSI X9. let source = SliceSource::new(&bytes); - tracing::info!("SPKI bytes: {}", base16::encode_display(&bytes)); let mut bits = None; bcder::Mode::Der .decode(source, |cons| { @@ -346,8 +345,9 @@ impl PublicKey { return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into())); }; + // Expect octet string to be [, <32-byte X value>, <32-byte Y value>]. if octets.len() != 65 { - return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [0x04, <32-byte X value>, <32-byte Y value>]".into())); + return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", base16::encode_display(octets), octets.len()))); } // Note: OpenDNSSEC doesn't support the compressed form either. @@ -359,7 +359,6 @@ impl PublicKey { // Expect octet string to be X | Y (| denotes concatenation) where // X and Y are each 32 bytes (because P-256 uses 256 bit values and // 256 bits are 32 bytes). - // Skip the compression flag. octets[1..].to_vec() } @@ -405,7 +404,7 @@ pub mod sign { use tracing::{debug, error}; use crate::base::iana::SecurityAlgorithm; - use crate::crypto::common::DigestType; + use crate::crypto::common::{trim_leading_zeroes, DigestType}; use crate::crypto::kmip::{GenerateError, PublicKey}; use crate::crypto::kmip_pool::KmipConnPool; use crate::crypto::sign::{ @@ -413,6 +412,14 @@ pub mod sign { }; use crate::rdata::Dnskey; use crate::utils::base16; + use bcder::decode::SliceSource; + + impl From for SignError { + fn from(err: kmip::client::Error) -> Self { + error!("KMIP error: {err}"); + SignError + } + } #[derive(Clone, Debug)] pub struct KeyPair { @@ -467,15 +474,13 @@ pub mod sign { self.algorithm } - fn dnskey(&self) -> Dnskey> { - // TODO: SAFETY - PublicKey::new( + fn dnskey(&self) -> Result>, SignError> { + Ok(PublicKey::new( self.public_key_id.clone(), self.algorithm, self.conn_pool.clone(), ) - .dnskey(self.flags) - .unwrap() + .dnskey(self.flags)?) } fn sign_raw(&self, data: &[u8]) -> Result { @@ -505,7 +510,7 @@ pub mod sign { // // KMIP HSMs implement the Sign opration operation according to // these rules. - let (crypto_alg, hashing_alg, digest_type) = match self.algorithm + let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm { SecurityAlgorithm::RSASHA256 => ( CryptographicAlgorithm::RSA, @@ -586,25 +591,23 @@ pub mod sign { // : } // // Where the two integer values are known as 'r' and 's'. - // let source = SliceSource::new(&signed.signature_data); - // let (r, s) = bcder::Mode::Der - // .decode(source, |cons| { - // cons.take_sequence(|cons| { - // let r = bcder::Unsigned::take_from(cons)?; - // let s = bcder::Unsigned::take_from(cons)?; - // Ok((r, s)) - // }) - // }) - // .unwrap(); - - // 3046022100e4e87c417196c6e5cd63f93e94929ccda6d04fc0a7446922baf3070e854ec4f4022100a1ecd098008329de9bc93fb2ded6aaceecc921f7183d6b3cfc673b3ef8af219e - let signature = openssl::ecdsa::EcdsaSig::from_der( - &signed.signature_data, - ) - .unwrap(); - let mut sig = signature.r().to_vec_padded(32).unwrap(); - let mut s = signature.s().to_vec_padded(32).unwrap(); - sig.append(&mut s); + tracing::info!("Parsing received signature: {}", base16::encode_display(&signed.signature_data)); + let source = SliceSource::new(&signed.signature_data); + let (r, s) = bcder::Mode::Der + .decode(source, |cons| { + cons.take_sequence(|cons| { + let r = bcder::Unsigned::take_from(cons)?; + let s = bcder::Unsigned::take_from(cons)?; + Ok((r, s)) + }) + }) + .map_err(|err| { + error!("Failed to parse ECDSAP256SHA256 signature as ASN.1 DER encoded (r, s) sequence: {err} ({})", base16::encode_display(&signed.signature_data)); + SignError + })?; + + let mut sig = trim_leading_zeroes(r.as_slice()).to_vec(); + sig.extend_from_slice(trim_leading_zeroes(s.as_slice())); Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( sig.try_into().map_err(|_| SignError)?, @@ -886,15 +889,12 @@ mod tests { use std::time::SystemTime; use std::vec::Vec; - use bcder::decode::SliceSource; use kmip::client::ConnectionSettings; - use openssl::bn::BigNum; use crate::crypto::kmip::sign::generate; use crate::crypto::kmip_pool::ConnectionManager; use crate::crypto::sign::SignRaw; use crate::logging::init_logging; - use crate::utils::base16; #[test] #[ignore = "Requires running PyKMIP"] @@ -930,8 +930,8 @@ mod tests { let pool = ConnectionManager::create_connection_pool( conn_settings.into(), 16384, - Duration::from_secs(60), - Duration::from_secs(60), + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), ) .unwrap(); @@ -960,7 +960,7 @@ mod tests { dbg!(&res); let key = res.unwrap(); - let dnskey = key.dnskey(); + let dnskey = key.dnskey().unwrap(); eprintln!("DNSKEY: {}", dnskey); } @@ -992,8 +992,8 @@ mod tests { let pool = ConnectionManager::create_connection_pool( conn_settings.into(), 16384, - Duration::from_secs(60), - Duration::from_secs(60), + Some(Duration::from_secs(60)), + Some(Duration::from_secs(60)), ) .unwrap(); @@ -1025,7 +1025,7 @@ mod tests { // sleep(Duration::from_secs(5)); - let dnskey = key.dnskey(); + let dnskey = key.dnskey().unwrap(); eprintln!("DNSKEY: {}", dnskey); client.activate_key(key.public_key_id()).unwrap(); diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 07e65a1ae..2516404ea 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -9,7 +9,9 @@ //! - Handle loss of connectivity by re-creating the connection when an //! existing connection is considered to be "broken" at the network //! level. +use core::fmt::Display; use std::net::TcpStream; +use std::string::String; use std::{sync::Arc, time::Duration}; use kmip::client::{Client, ConnectionSettings}; @@ -35,6 +37,21 @@ pub type KmipTlsClient = Client>; /// closing the connection when finished. pub type KmipConnPool = r2d2::Pool; +#[derive(Clone, Debug)] +pub struct KmipConnError(String); + +impl From for KmipConnError { + fn from(err: r2d2::Error) -> Self { + Self(format!("{err}")) + } +} + +impl Display for KmipConnError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + /// Manages KMIP TCP + TLS connection creation. /// /// Uses the [r2d2] crate to manage a pool of connections. @@ -51,12 +68,9 @@ impl ConnectionManager { pub fn create_connection_pool( conn_settings: Arc, max_conncurrent_connections: u32, - max_life_time: Duration, - max_idle_time: Duration, - ) -> Result { - let max_life_time = Some(max_life_time); - let max_idle_time = Some(max_idle_time); - + max_life_time: Option, + max_idle_time: Option, + ) -> Result { let pool = r2d2::Pool::builder() // Don't pre-create idle connections to the KMIP server .min_idle(Some(0)) @@ -84,7 +98,7 @@ impl ConnectionManager { .connection_timeout(conn_settings.connect_timeout.unwrap_or(Duration::from_secs(30))) // Use our connection manager to create connections in the pool and to verify their health - .build(ConnectionManager { conn_settings }).unwrap(); + .build(ConnectionManager { conn_settings })?; Ok(pool) } diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 907c2bb69..60fb817e7 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -685,7 +685,7 @@ pub mod sign { self.algorithm } - fn dnskey(&self) -> Dnskey> { + fn dnskey(&self) -> Result>, SignError> { match self.algorithm { SecurityAlgorithm::RSASHA256 => { let key = self.pkey.rsa().expect("should not fail"); @@ -699,7 +699,7 @@ pub mod sign { key, self.flags, ); - public.dnskey() + Ok(public.dnskey()) } SecurityAlgorithm::ECDSAP256SHA256 | SecurityAlgorithm::ECDSAP384SHA384 => { @@ -725,7 +725,7 @@ pub mod sign { public_key, self.flags, ); - public.dnskey() + Ok(public.dnskey()) } SecurityAlgorithm::ED25519 | SecurityAlgorithm::ED448 => { let id = match self.algorithm { @@ -739,7 +739,7 @@ pub mod sign { let key = PKey::public_key_from_raw_bytes(&key, id) .expect("shoul not fail"); let public = PublicKey::NoDigest(key, self.flags); - public.dnskey() + Ok(public.dnskey()) } _ => unreachable!(), } @@ -875,7 +875,7 @@ pub mod sign { let key = super::generate(params, 256).unwrap(); let gen_key = key.to_bytes(); - let pub_key = key.dnskey(); + let pub_key = key.dnskey().unwrap(); let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } @@ -922,7 +922,7 @@ pub mod sign { let key = KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); - assert_eq!(key.dnskey(), *pub_key.data()); + assert_eq!(key.dnskey().unwrap(), *pub_key.data()); } } diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index d0c818700..e484b287d 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -502,7 +502,7 @@ pub mod sign { } } - fn dnskey(&self) -> Dnskey> { + fn dnskey(&self) -> Result>, SignError> { match self { Self::RsaSha256 { key, flags, rng: _ } => { let components: ring::rsa::PublicKeyComponents> = @@ -512,7 +512,7 @@ pub mod sign { let public_key = signature::RsaPublicKeyComponents { n, e }; let public = PublicKey::Rsa(&signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, public_key); - public.dnskey(*flags) + Ok(public.dnskey(*flags)) } Self::EcdsaP256Sha256 { key, flags, rng: _ } @@ -544,7 +544,7 @@ pub mod sign { key.to_vec(), ), ); - public.dnskey(*flags) + Ok(public.dnskey(*flags)) } Self::Ed25519(key, flags) => { let (algorithm, sec_alg) = match self { @@ -561,7 +561,7 @@ pub mod sign { key.to_vec(), ), ); - public.dnskey(*flags) + Ok(public.dnskey(*flags)) } } } @@ -716,7 +716,7 @@ pub mod sign { crate::crypto::sign::generate(params.clone(), 256) .unwrap(); let key = KeyPair::from_bytes(&sk, &pk).unwrap(); - assert_eq!(key.dnskey(), pk); + assert_eq!(key.dnskey().unwrap(), pk); } } @@ -737,7 +737,7 @@ pub mod sign { let key = KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); - assert_eq!(key.dnskey(), *pub_key.data()); + assert_eq!(key.dnskey().unwrap(), *pub_key.data()); } } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index d79e9aeeb..83d5b3da5 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -181,7 +181,7 @@ pub trait SignRaw { /// algorithm as returned by [`algorithm()`]. /// /// [`algorithm()`]: Self::algorithm() - fn dnskey(&self) -> Dnskey>; + fn dnskey(&self) -> Result>, SignError>; /// Sign the given bytes. /// @@ -316,7 +316,7 @@ pub enum KeyPair { /// A key backed by a KMIP capable HSM. #[cfg(feature = "kmip")] - Kmip(kmip::sign::KeyPair), + Kmip(self::kmip::sign::KeyPair), } //--- Conversion to and from bytes @@ -378,7 +378,7 @@ impl SignRaw for KeyPair { } } - fn dnskey(&self) -> Dnskey> { + fn dnskey(&self) -> Result>, SignError> { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.dnskey(), @@ -424,7 +424,10 @@ pub fn generate( #[cfg(feature = "openssl")] { let key = openssl::sign::generate(params, flags)?; - return Ok((key.to_bytes(), key.dnskey())); + return Ok(( + key.to_bytes(), + key.dnskey().map_err(|_| GenerateError::Implementation)?, + )); } // Otherwise fail. diff --git a/src/dnssec/sign/keys/signingkey.rs b/src/dnssec/sign/keys/signingkey.rs index dd27fdc96..152bd5869 100644 --- a/src/dnssec/sign/keys/signingkey.rs +++ b/src/dnssec/sign/keys/signingkey.rs @@ -1,6 +1,6 @@ use crate::base::iana::SecurityAlgorithm; use crate::base::Name; -use crate::crypto::sign::SignRaw; +use crate::crypto::sign::{SignError, SignRaw}; use crate::rdata::Dnskey; use std::fmt::Debug; use std::vec::Vec; @@ -122,7 +122,7 @@ where self.inner.algorithm() } - pub fn dnskey(&self) -> Dnskey> { + pub fn dnskey(&self) -> Result>, SignError> { self.inner.dnskey() } } diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 27aeb986f..0afce6618 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -202,7 +202,7 @@ where "Signed {} RRSET at {} with keytag {}", rrset.rtype(), rrset.owner(), - key.dnskey().key_tag() + key.dnskey()?.key_tag() ); } } @@ -310,7 +310,7 @@ where rrset.ttl(), expiration, inception, - key.dnskey().key_tag(), + key.dnskey()?.key_tag(), // The fns provided by `ToName` state in their RustDoc that they // "Converts the name into a single, uncompressed name" which matches // the RFC 4034 section 3.1.7 requirement that "A sender MUST NOT use @@ -476,7 +476,7 @@ mod tests { let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let (inception, expiration) = (Timestamp::from(0), Timestamp::from(0)); - let dnskey = key.dnskey().convert(); + let dnskey = key.dnskey().unwrap().convert(); let mut records = SortedRecords::::default(); @@ -665,7 +665,7 @@ mod tests { // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().convert(); + let dnskey = keys[0].dnskey().unwrap().convert(); // Generate RRSIGs. Use the default signing config and thus also the // DefaultSigningKeyUsageStrategy which will honour the purpose of the @@ -710,7 +710,7 @@ mod tests { // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().convert(); + let dnskey = keys[0].dnskey().unwrap().convert(); let generated_records = sign_sorted_zone_records( &apex, @@ -754,7 +754,7 @@ mod tests { // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().convert(); + let dnskey = keys[0].dnskey().unwrap().convert(); let generated_records = sign_sorted_zone_records( &apex, @@ -829,7 +829,7 @@ mod tests { let dnskeys = keys .iter() - .map(|k| k.dnskey().convert()) + .map(|k| k.dnskey().unwrap().convert()) .collect::>(); let zsk = &dnskeys[zsk_idx]; @@ -1016,8 +1016,8 @@ mod tests { let keys = [&mk_dnssec_signing_key(false), &mk_dnssec_signing_key(false)]; - let zsk1 = keys[0].dnskey().convert(); - let zsk2 = keys[1].dnskey().convert(); + let zsk1 = keys[0].dnskey().unwrap().convert(); + let zsk2 = keys[1].dnskey().unwrap().convert(); let generated_records = sign_sorted_zone_records( &apex_owner, @@ -1067,7 +1067,7 @@ mod tests { fn generate_rrsigs_for_already_signed_zone() { let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().convert(); + let dnskey = keys[0].dnskey().unwrap().convert(); let apex = Name::from_str("example.").unwrap(); let mut records = SortedRecords::default(); @@ -1218,10 +1218,15 @@ mod tests { SecurityAlgorithm::ED25519 } - fn dnskey(&self) -> Dnskey> { + fn dnskey(&self) -> Result>, SignError> { let flags = 0; - Dnskey::new(flags, 3, SecurityAlgorithm::ED25519, self.0.to_vec()) - .unwrap() + Ok(Dnskey::new( + flags, + 3, + SecurityAlgorithm::ED25519, + self.0.to_vec(), + ) + .unwrap()) } fn sign_raw(&self, _data: &[u8]) -> Result { From 2fde2cf498c2813d09cf10fa2d84985ad2598daa Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:40:24 +0200 Subject: [PATCH 483/569] Lower level of log statement. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index f9f47df2e..95cd5eff0 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -591,7 +591,7 @@ pub mod sign { // : } // // Where the two integer values are known as 'r' and 's'. - tracing::info!("Parsing received signature: {}", base16::encode_display(&signed.signature_data)); + debug!("Parsing received signature: {}", base16::encode_display(&signed.signature_data)); let source = SliceSource::new(&signed.signature_data); let (r, s) = bcder::Mode::Der .decode(source, |cons| { From b508a4a58ae3881c75f040121d3bd65b09b8c593 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:49:37 +0200 Subject: [PATCH 484/569] Determine the DNSKEY RR once, not every time that dnskey() is invoked. --- src/crypto/kmip.rs | 49 +++++++++++++++++++++++++++++++++------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 95cd5eff0..a9b4285da 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -432,6 +432,8 @@ pub mod sign { conn_pool: KmipConnPool, + dnskey: Dnskey>, + flags: u16, } @@ -442,14 +444,26 @@ pub mod sign { private_key_id: &str, public_key_id: &str, conn_pool: KmipConnPool, - ) -> Self { - Self { + ) -> Result { + let dnskey = PublicKey::new( + public_key_id.to_string(), + algorithm, + conn_pool.clone(), + ) + .dnskey(flags)?; + + Ok(Self { algorithm, private_key_id: private_key_id.to_string(), public_key_id: public_key_id.to_string(), conn_pool, flags, - } + dnskey, + }) + } + + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm } pub fn private_key_id(&self) -> &str { @@ -467,6 +481,10 @@ pub mod sign { self.conn_pool.clone(), ) } + + pub fn flags(&self) -> u16 { + self.flags + } } impl SignRaw for KeyPair { @@ -475,12 +493,7 @@ pub mod sign { } fn dnskey(&self) -> Result>, SignError> { - Ok(PublicKey::new( - self.public_key_id.clone(), - self.algorithm, - self.conn_pool.clone(), - ) - .dnskey(self.flags)?) + Ok(self.dnskey.clone()) } fn sign_raw(&self, data: &[u8]) -> Result { @@ -830,6 +843,11 @@ pub mod sign { GenerateError::Kmip(err.to_string()) })?; + // Drop the KMIP client so that it will be returned to the pool and + // thus be available below when KeyPair::new() is invoked and tries + // to fetch the details needed to determine the DNSKEY RR. + drop(client); + // Process the successful response let ResponsePayload::CreateKeyPair(payload) = response else { error!("KMIP request failed: Wrong response type received!"); @@ -841,16 +859,19 @@ pub mod sign { public_key_unique_identifier, } = payload; - let key_pair = KeyPair { + let key_pair = KeyPair::new( algorithm, - private_key_id: private_key_unique_identifier.to_string(), - public_key_id: public_key_unique_identifier.to_string(), - conn_pool, flags, - }; + private_key_unique_identifier.as_str(), + public_key_unique_identifier.as_str(), + conn_pool.clone()) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; // Activate the key if not already, otherwise it cannot be used for signing. if !activate_on_create { + let client = conn_pool + .get() + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server: {err}")))?; let request = RequestPayload::Activate(Some(private_key_unique_identifier)); From 5913003cb4171c66860616160540f3d2ae705980 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:51:01 +0200 Subject: [PATCH 485/569] Better error reporting. --- src/crypto/kmip.rs | 67 +++++++++++++++++++++------------------- src/crypto/openssl.rs | 14 +++++---- src/crypto/ring.rs | 22 ++++++++----- src/crypto/sign.rs | 19 ++++++++++-- src/dnssec/sign/error.rs | 2 +- 5 files changed, 75 insertions(+), 49 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index a9b4285da..2f25afbbb 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -20,7 +20,7 @@ pub use kmip::client::{ClientCertificate, ConnectionSettings}; use crate::{ base::iana::SecurityAlgorithm, - crypto::{common::{rsa_encode, trim_leading_zeroes}, kmip_pool::KmipConnPool}, + crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, rdata::Dnskey, utils::base16, }; @@ -166,7 +166,7 @@ impl PublicKey { })?; // Note: OpenDNSSEC queries the public key ID, _unless_ it was - // configured not to store the public key in the HSM (by setting + // configured not the public key in the HSM (by setting // CKA_TOKEN false) in which case there is no public key and so it // uses the private key object handle instead. let res = client @@ -416,8 +416,7 @@ pub mod sign { impl From for SignError { fn from(err: kmip::client::Error) -> Self { - error!("KMIP error: {err}"); - SignError + err.to_string().into() } } @@ -535,7 +534,7 @@ pub mod sign { HashingAlgorithm::SHA256, DigestType::Sha256, ), - _ => return Err(SignError), + alg => return Err(format!("Algorithm not supported for KMIP signing: {alg}").into()), }; // PyKMIP requires that the padding method be specified otherwise @@ -561,20 +560,13 @@ pub mod sign { let client = self .conn_pool .get() - .inspect_err(|err| { - error!( - "Error while obtaining KMIP pool connection: {err}" - ) - }) - .map_err(|_| SignError)?; + .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; let res = client .do_request(request) - .inspect_err(|err| { - error!("Error while sending KMIP request: {err}") - }) - .map_err(|_| SignError)?; + .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + tracing::trace!("Checking sign payload"); let ResponsePayload::Sign(signed) = res else { unreachable!(); }; @@ -584,11 +576,13 @@ pub mod sign { "Signature Data: {}", base16::encode_display(&signed.signature_data) ); - match self.algorithm { - SecurityAlgorithm::RSASHA256 => Ok(Signature::RsaSha256( + + match (self.algorithm, signed.signature_data.len()) { + (SecurityAlgorithm::RSASHA256, _) => Ok(Signature::RsaSha256( signed.signature_data.into_boxed_slice(), )), - SecurityAlgorithm::ECDSAP256SHA256 => { + + (SecurityAlgorithm::ECDSAP256SHA256, _) => { // ECDSA signature received from Fortanix DSM, decoded // using this command: // @@ -615,42 +609,46 @@ pub mod sign { }) }) .map_err(|err| { - error!("Failed to parse ECDSAP256SHA256 signature as ASN.1 DER encoded (r, s) sequence: {err} ({})", base16::encode_display(&signed.signature_data)); - SignError + format!("Failed to parse KMIP generated ECDSAP256SHA256 signature as ASN.1 DER encoded (r, s) sequence: {err} (0x{})", base16::encode_display(&signed.signature_data)) })?; - let mut sig = trim_leading_zeroes(r.as_slice()).to_vec(); - sig.extend_from_slice(trim_leading_zeroes(s.as_slice())); Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( - sig.try_into().map_err(|_| SignError)?, + sig.try_into().map_err(|_| format!("Incorrect KMIP ECDSAP256SHA256 signature length: {sig_len} != 64 bytes (r={}, s={})", base16::encode_display(r.as_slice()), base16::encode_display(s.as_slice())))?, ))) } - SecurityAlgorithm::ECDSAP384SHA384 => { + + (SecurityAlgorithm::ECDSAP384SHA384, 96) => { Ok(Signature::EcdsaP384Sha384(Box::<[u8; 96]>::new( signed .signature_data .try_into() - .map_err(|_| SignError)?, + .unwrap() ))) } - SecurityAlgorithm::ED25519 => { + (SecurityAlgorithm::ED25519, 64) => { Ok(Signature::Ed25519(Box::<[u8; 64]>::new( signed .signature_data .try_into() - .map_err(|_| SignError)?, + .unwrap() ))) } - SecurityAlgorithm::ED448 => { + + (SecurityAlgorithm::ED448, 114) => { Ok(Signature::Ed448(Box::<[u8; 114]>::new( signed .signature_data .try_into() - .map_err(|_| SignError)?, + .unwrap() ))) } - _ => Err(SignError)?, + + (alg, sig_len) => { + Err(format!("KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", + base16::encode_display(&signed.signature_data + )))? + } } } } @@ -666,7 +664,9 @@ pub mod sign { ) -> Result { let algorithm = params.algorithm(); - let client = conn_pool.get().map_err(|_| SignError).unwrap(); + let client = conn_pool + .get() + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server: {err}")))?; // TODO: Determine this on first use of the HSM? // PyKMIP doesn't support ActivationDate. @@ -842,6 +842,7 @@ pub mod sign { ); GenerateError::Kmip(err.to_string()) })?; + tracing::trace!("Key generation operation complete"); // Drop the KMIP client so that it will be returned to the pool and // thus be available below when KeyPair::new() is invoked and tries @@ -859,6 +860,8 @@ pub mod sign { public_key_unique_identifier, } = payload; + tracing::trace!("Creating KeyPair with DNSKEY"); + let key_pair = KeyPair::new( algorithm, flags, @@ -876,6 +879,7 @@ pub mod sign { RequestPayload::Activate(Some(private_key_unique_identifier)); // Execute the request and capture the response + tracing::trace!("Activating KMIP key..."); let response = client.do_request(request).map_err(|err| { eprintln!("KMIP activate private key request failed: {err}"); eprintln!( @@ -888,6 +892,7 @@ pub mod sign { ); GenerateError::Kmip(err.to_string()) })?; + tracing::trace!("Activate operation complete"); // Process the successful response let ResponsePayload::Activate(_) = response else { diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 60fb817e7..1794624a1 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -749,7 +749,7 @@ pub mod sign { let signature = self .sign(data) .map(Vec::into_boxed_slice) - .map_err(|_| SignError)?; + .map_err(|err| format!("OpenSSL signing failed: {err}"))?; match self.algorithm { SecurityAlgorithm::RSASHA256 => { @@ -759,22 +759,24 @@ pub mod sign { SecurityAlgorithm::ECDSAP256SHA256 => signature .try_into() .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError), + .map_err(|_| "OpenSSL ECDSAP256SHA256 signature too large".into()), + SecurityAlgorithm::ECDSAP384SHA384 => signature .try_into() .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError), + .map_err(|_| "OpenSSL ECDSAP384SHA384 signature too large".into()), SecurityAlgorithm::ED25519 => signature .try_into() .map(Signature::Ed25519) - .map_err(|_| SignError), + .map_err(|_| "OpenSSL ED25519 signature too large".into()), + SecurityAlgorithm::ED448 => signature .try_into() .map(Signature::Ed448) - .map_err(|_| SignError), + .map_err(|_| "OpenSSL ED448 signature too large".into()), - _ => unreachable!(), + alg => Err(format!("OpenSSL signature algorithm not supported: {alg}").into()), } } } diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index e484b287d..8402c2fc3 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -575,35 +575,41 @@ pub mod sign { .map(|()| { Signature::RsaSha256(buf.into_boxed_slice()) }) - .map_err(|_| SignError) + .map_err(|_| "Ring RSASHA256 signing failed".into()) } Self::EcdsaP256Sha256 { key, flags: _, rng } => key .sign(&**rng, data) .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) + .map_err(|_| "Ring ECDSAP256SHA256 signing failed".into()) .and_then(|buf| { buf.try_into() .map(Signature::EcdsaP256Sha256) - .map_err(|_| SignError) + .map_err(|_| { + "Ring ECDSAP256SHA256 signature too large" + .into() + }) }), Self::EcdsaP384Sha384 { key, flags: _, rng } => key .sign(&**rng, data) .map(|sig| Box::<[u8]>::from(sig.as_ref())) - .map_err(|_| SignError) + .map_err(|_| "Ring ECDSAP384SHA384 signing failed".into()) .and_then(|buf| { buf.try_into() .map(Signature::EcdsaP384Sha384) - .map_err(|_| SignError) + .map_err(|_| { + "Ring ECDSAP384SHA384 signature too large" + .into() + }) }), Self::Ed25519(key, _) => { let sig = key.sign(data); let buf: Box<[u8]> = sig.as_ref().into(); - buf.try_into() - .map(Signature::Ed25519) - .map_err(|_| SignError) + buf.try_into().map(Signature::Ed25519).map_err(|_| { + "Ring ED25519 signature too large".into() + }) } } } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 83d5b3da5..8a619eeb0 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -81,6 +81,7 @@ use std::boxed::Box; use std::fmt; +use std::string::{String, ToString}; use std::vec::Vec; use secrecy::{ExposeSecret, SecretBox}; @@ -1047,17 +1048,29 @@ impl std::error::Error for GenerateError {} /// is an optional step, or where crashing is prohibited, may wish to recover /// from such an error differently (e.g. by foregoing signatures or informing /// an operator). -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct SignError; +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SignError(String); impl fmt::Display for SignError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("could not create a cryptographic signature") + write!(f, "could not create a cryptographic signature: {}", self.0) } } impl std::error::Error for SignError {} +impl From for SignError { + fn from(err: String) -> Self { + Self(err) + } +} + +impl From<&'static str> for SignError { + fn from(err: &'static str) -> Self { + Self(err.to_string()) + } +} + //----------- BindFormatError ------------------------------------------------ /// An error in loading a [`SecretKeyBytes`] from the conventional DNS format. diff --git a/src/dnssec/sign/error.rs b/src/dnssec/sign/error.rs index 1a0791000..7dc2fc11a 100644 --- a/src/dnssec/sign/error.rs +++ b/src/dnssec/sign/error.rs @@ -7,7 +7,7 @@ use crate::rdata::dnssec::Timestamp; //------------ SigningError -------------------------------------------------- -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug)] pub enum SigningError { /// One or more keys does not have a signature validity period defined. NoSignatureValidityPeriodProvided, From 10c936eed80d3443f37b630661d563d92bfe7d80 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:51:39 +0200 Subject: [PATCH 486/569] Trim leading zeros and then pad with leading zeros. --- src/crypto/kmip.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 2f25afbbb..78e914226 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -612,6 +612,20 @@ pub mod sign { format!("Failed to parse KMIP generated ECDSAP256SHA256 signature as ASN.1 DER encoded (r, s) sequence: {err} (0x{})", base16::encode_display(&signed.signature_data)) })?; + let r_trimmed = trim_leading_zeroes(r.as_slice()); + if r_trimmed.len() > 32 { + return Err(format!("Incorrect KMIP ECDSAP256SHA256 signature length: r > 32 bytes, r={})", base16::encode_display(r.as_slice())).into()); + } + let mut sig = r_trimmed.to_vec(); + sig.resize(32, 0); + + let s_trimmed = trim_leading_zeroes(s.as_slice()); + if s_trimmed.len() > 32 { + return Err(format!("Incorrect KMIP ECDSAP256SHA256 signature length: s > 32 bytes, s={})", base16::encode_display(s.as_slice())).into()); + } + sig.extend_from_slice(s_trimmed); + sig.resize(64, 0); + let sig_len = sig.len(); Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( sig.try_into().map_err(|_| format!("Incorrect KMIP ECDSAP256SHA256 signature length: {sig_len} != 64 bytes (r={}, s={})", base16::encode_display(r.as_slice()), base16::encode_display(s.as_slice())))?, From 3d01aaf49d53f92bd9d40ab7ff1f287dc5a002d3 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:52:15 +0200 Subject: [PATCH 487/569] Note a possible performance improvement idea for later if needed. --- src/crypto/kmip.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 78e914226..42add0da5 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -550,6 +550,10 @@ pub mod sign { .with_padding_method(PaddingMethod::PKCS1_v1_5); } + // TODO: We could optionally add a KMIP Message Extension to the + // request via which we signal support for domain format response + // data, so that the Nameshed HSM Relay doesn't have to convert + // from PKCS#11 format to the format needed by domain. let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some(cryptographic_parameters), From 7cfc9548fbbb11e33f5d7a8f9ddc276c3788066c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 7 Jul 2025 14:52:52 +0200 Subject: [PATCH 488/569] Bump kmip-protocol. --- Cargo.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 66a219f39..465fa8105 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,7 +618,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#8b9bef377247046e8d6094114890fdc5e0827c60" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#e7dc73910e91f2e0053a2b9c413cfbf2c4a36d51" dependencies = [ "cfg-if", "enum-display-derive", @@ -632,6 +632,7 @@ dependencies = [ "serde", "serde_bytes", "serde_derive", + "tracing", "trait-set", ] From f817628e08ebee9245522cdb1a47c1a58c3f3647 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 8 Jul 2025 18:21:58 +0200 Subject: [PATCH 489/569] FIX ECDSAP256SHA256 signing issue with larger zones having invalid signatures (presumably something to do with DER zero trimming/padding). --- src/crypto/kmip.rs | 39 +++++++-------------------------------- 1 file changed, 7 insertions(+), 32 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 42add0da5..508f3b04e 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -401,10 +401,11 @@ pub mod sign { use kmip::types::response::{ CreateKeyPairResponsePayload, ResponsePayload, }; + use openssl::ecdsa::EcdsaSig; use tracing::{debug, error}; use crate::base::iana::SecurityAlgorithm; - use crate::crypto::common::{trim_leading_zeroes, DigestType}; + use crate::crypto::common::DigestType; use crate::crypto::kmip::{GenerateError, PublicKey}; use crate::crypto::kmip_pool::KmipConnPool; use crate::crypto::sign::{ @@ -412,7 +413,6 @@ pub mod sign { }; use crate::rdata::Dnskey; use crate::utils::base16; - use bcder::decode::SliceSource; impl From for SignError { fn from(err: kmip::client::Error) -> Self { @@ -602,37 +602,12 @@ pub mod sign { // : } // // Where the two integer values are known as 'r' and 's'. - debug!("Parsing received signature: {}", base16::encode_display(&signed.signature_data)); - let source = SliceSource::new(&signed.signature_data); - let (r, s) = bcder::Mode::Der - .decode(source, |cons| { - cons.take_sequence(|cons| { - let r = bcder::Unsigned::take_from(cons)?; - let s = bcder::Unsigned::take_from(cons)?; - Ok((r, s)) - }) - }) - .map_err(|err| { - format!("Failed to parse KMIP generated ECDSAP256SHA256 signature as ASN.1 DER encoded (r, s) sequence: {err} (0x{})", base16::encode_display(&signed.signature_data)) - })?; - - let r_trimmed = trim_leading_zeroes(r.as_slice()); - if r_trimmed.len() > 32 { - return Err(format!("Incorrect KMIP ECDSAP256SHA256 signature length: r > 32 bytes, r={})", base16::encode_display(r.as_slice())).into()); - } - let mut sig = r_trimmed.to_vec(); - sig.resize(32, 0); - - let s_trimmed = trim_leading_zeroes(s.as_slice()); - if s_trimmed.len() > 32 { - return Err(format!("Incorrect KMIP ECDSAP256SHA256 signature length: s > 32 bytes, s={})", base16::encode_display(s.as_slice())).into()); - } - sig.extend_from_slice(s_trimmed); - sig.resize(64, 0); - let sig_len = sig.len(); - + let signature = EcdsaSig::from_der(&signed.signature_data).unwrap(); + let mut r = signature.r().to_vec_padded(32).unwrap(); + let mut s = signature.s().to_vec_padded(32).unwrap(); + r.append(&mut s); Ok(Signature::EcdsaP256Sha256(Box::<[u8; 64]>::new( - sig.try_into().map_err(|_| format!("Incorrect KMIP ECDSAP256SHA256 signature length: {sig_len} != 64 bytes (r={}, s={})", base16::encode_display(r.as_slice()), base16::encode_display(s.as_slice())))?, + r.try_into().unwrap() ))) } From b6751b4ff40253de2904135c9ee60651e2219ed9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Sat, 12 Jul 2025 00:58:44 +0200 Subject: [PATCH 490/569] Bump KMIP dependencies. --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 465fa8105..b62f21d6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -618,7 +618,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#e7dc73910e91f2e0053a2b9c413cfbf2c4a36d51" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#2454c6bd29ff3c7e711724e67d50f374ec0c249b" dependencies = [ "cfg-if", "enum-display-derive", @@ -639,7 +639,7 @@ dependencies = [ [[package]] name = "kmip-ttlv" version = "0.4.0" -source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#38e72487d218f57649b5173483296be8946ef4d1" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#ffe005638e150db0c1f1435a607f292d82b634b1" dependencies = [ "cfg-if", "hex", From 1fc6a8c592e6e447f26590cdd686843246b760db Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:40:25 +0200 Subject: [PATCH 491/569] - Add support for KMIP key URL construction and parsing. - Don't leak the internal r2d2 pool and connection type details via the KMIP crypto interface. - Extend SignRaw to expoee flags() as all underlying implementations have the flags value immediately available, instead of going via fn dnskey() which could be a costly operation involving the KMIP server. - Extend the KMIP pool with the concept of a server_id separate to the host:port etc which simply identifies the target server. --- Cargo.lock | 271 ++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 3 +- src/crypto/kmip.rs | 250 +++++++++++++++++++++++++++++++++++- src/crypto/kmip_pool.rs | 161 ++++++++++++++++++------ src/crypto/openssl.rs | 4 + src/crypto/ring.rs | 9 ++ src/crypto/sign.rs | 15 ++- 7 files changed, 666 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b62f21d6a..656beeed5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -242,6 +242,17 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "domain" version = "0.11.1-dev" @@ -290,6 +301,7 @@ dependencies = [ "tokio-tfo", "tracing", "tracing-subscriber", + "url", "webpki-roots 0.26.11", ] @@ -372,6 +384,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures" version = "0.3.31" @@ -580,6 +601,113 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.10.0" @@ -662,6 +790,12 @@ version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -900,6 +1034,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project" version = "1.1.10" @@ -944,6 +1084,15 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1393,6 +1542,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -1459,6 +1619,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tokio" version = "1.45.1" @@ -1645,6 +1815,23 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.17.0" @@ -2058,12 +2245,42 @@ dependencies = [ "bitflags", ] +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.8.26" @@ -2084,8 +2301,62 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] diff --git a/Cargo.toml b/Cargo.toml index d2da71cee..f8130c148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,6 +53,7 @@ tokio-rustls = { version = "0.26", optional = true, default-features = false } tokio-stream = { version = "0.1.1", optional = true } tracing = { version = "0.1.40", optional = true, features = ["log"] } tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-filter"] } +url = { version = "2.5.4", optional = true } [features] default = ["std", "rand"] @@ -70,7 +71,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:bcder"] +kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 508f3b04e..3bc1502fc 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -384,6 +384,7 @@ impl PublicKey { #[cfg(feature = "unstable-crypto-sign")] pub mod sign { + use core::str::FromStr; use std::boxed::Box; use std::string::{String, ToString}; use std::time::SystemTime; @@ -403,6 +404,7 @@ pub mod sign { }; use openssl::ecdsa::EcdsaSig; use tracing::{debug, error}; + use url::Url; use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; @@ -443,13 +445,13 @@ pub mod sign { private_key_id: &str, public_key_id: &str, conn_pool: KmipConnPool, - ) -> Result { + ) -> Result { let dnskey = PublicKey::new( public_key_id.to_string(), algorithm, conn_pool.clone(), ) - .dnskey(flags)?; + .dnskey(flags).map_err(|err| GenerateError::Kmip(err.to_string()))?; Ok(Self { algorithm, @@ -461,6 +463,25 @@ pub mod sign { }) } + pub fn new_from_urls( + priv_key_url: KeyUrl, + pub_key_url: KeyUrl, + conn_pool: KmipConnPool, + ) -> Result { + if priv_key_url.algorithm() != pub_key_url.algorithm() { + return Err(GenerateError::Kmip(format!("Private and public key URLs have different algorithms: {} vs {}", priv_key_url.algorithm(), pub_key_url.algorithm()).into())); + } else if priv_key_url.flags() != pub_key_url.flags() { + return Err(GenerateError::Kmip(format!("Private and public key URLs have different flags: {} vs {}", priv_key_url.flags(), pub_key_url.flags()).into())); + } else if priv_key_url.server_id() != pub_key_url.server_id() { + return Err(GenerateError::Kmip(format!("Private and public key URLs have different server IDs: {} vs {}", priv_key_url.server_id(), pub_key_url.server_id()).into())); + } else if priv_key_url.server_id() != conn_pool.server_id() { + return Err(GenerateError::Kmip(format!("Key URLs have different server ID to the KMIP connection pool: {} vs {}", priv_key_url.server_id(), conn_pool.server_id()).into())); + } else { + Self::new(priv_key_url.algorithm(), priv_key_url.flags(), priv_key_url.key_id(), pub_key_url.key_id(), conn_pool) + } + + } + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } @@ -484,12 +505,50 @@ pub mod sign { pub fn flags(&self) -> u16 { self.flags } + + pub fn public_key_url(&self) -> Result { + self.mk_key_url(&self.public_key_id) + } + + pub fn private_key_url(&self) -> Result { + self.mk_key_url(&self.private_key_id) + } + + fn mk_key_url(&self, key_id: &str) -> Result { + // We have to store the algorithm in the URL because the DNSSEC + // algorithm (e.g. 5 and 7) don't necessarily correspond to the + // cryptographic algorithm of the key known to the HSM. And we + // have to store the flags in the URL because these are not known + // to the HSM, they say someting about the use to which the key + // will be put of which the HSM is unaware. + let url = format!( + "kmip://{}/keys/{}?algorithm={}&flags={}", + self.conn_pool.server_id(), + key_id, + self.algorithm, + self.flags + ); + + let url = Url::parse(&url).map_err::(|err| { + format!("unable to parse {url} as URL: {err}").into() + })?; + + Ok(url) + } + + pub fn conn_pool(&self) -> &KmipConnPool { + &self.conn_pool + } } impl SignRaw for KeyPair { fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } + + fn flags(&self) -> u16 { + self.flags + } fn dnskey(&self) -> Result>, SignError> { Ok(self.dnskey.clone()) @@ -646,6 +705,183 @@ pub mod sign { } } + /// A URL that represents a key stored in a KMIP compatible HSM. + /// + /// The URL structure is: + /// + /// kmip:///keys/?algorithm=&flags= + /// + /// The algorithm and flags must be stored in the URL because they are + /// DNSSEC specific and not properties of the key itself and thus not + /// known to or stored by the HSM. + /// + /// While algorithm may seem to be something known to and stored by the + /// HSM, DNSSEC complicates that by aliasing multiple algorithm numbers to + /// the same cryptographic algorithm, and we need to know when using the + /// key which _DNSSEC_ algorithm number to use. + /// + /// The server_id could be the actual address of the target, but does not + /// have to be. There are multiple for this: + /// + /// - In a highly available clustered deployment across multiple subnets + /// it could be that the clustered HSM is available to the clustered + /// application via different names/IP addresses in different subnets + /// of the deployment. Using an abstract server_id which is mapped via + /// local configuration in the subnet to the correct hostname/FQDN/IP + /// address for that subnet allows the correct target address to be + /// determined at the point of access. + /// - Using the actual hostname/FQDN/IP address may make it confusing + /// for an operator trying to understand where the key is actually + /// stored. This can happen for example if the product name for the + /// HSM is say Fortanix DSM, while the domain name used to access the + /// HSM might be eu.smartkey.io, which having no mention of the name + /// Fortanix in the FQDN is not immediately obvious that it has any + /// relationship with Fortanix. + /// - If the same HSM is used for different use cases via use of HSM + /// partitions, referring to the HSM by its address may not make it + /// clear which partition is being used, so using a more meaningful + /// name like 'testing' or such could make it clearer where the key is + /// actually being stored. + /// - Storing the username and password in the key URL will cause many + /// copies of those credentials to be stored, one per key, which is + /// harder to secure than if they are only in a single location and + /// looked up on actual access. + /// - Storing the username and password in the key URL would cause the + /// URL to become unusable if the credentials were rotated even though + /// the location at which the key is stored has not changed. + /// - Even if the FQDN, port number, username and password are all + /// correct, there may need to be more settings specified in order to + /// connect to the HSM some of which would not fit easily into a URL + /// such as TLS client certficate details and whether or not to + /// require the server TLS certificate to be valid (which can be + /// inconvenient in test setups using self-signed certificates). + /// + /// Thus an abstract server_id is stored in the key URL and it is the + /// responsibility of the user of the key URL to map the server id to the + /// full set of settings required to successfully connect to the HSM to + /// make use of the key. + pub struct KeyUrl { + url: Url, + server_id: String, + key_id: String, + algorithm: SecurityAlgorithm, + flags: u16, + } + + impl KeyUrl { + fn new( + url: Url, + server_id: String, + key_id: String, + algorithm: SecurityAlgorithm, + flags: u16 + ) -> Self { + Self { url, server_id, key_id, algorithm, flags } + } + + pub fn new_public_key_url(key_pair: &KeyPair) -> Result { + Ok(Self { + url: Self::mk_url(key_pair, &key_pair.public_key_id)?, + server_id: key_pair.conn_pool.server_id().to_string(), + key_id: key_pair.public_key_id.clone(), + algorithm: key_pair.algorithm, + flags: key_pair.flags, + }) + } + + pub fn new_private_key_url(key_pair: &KeyPair) -> Result { + Ok(Self { + url: Self::mk_url(key_pair, &key_pair.private_key_id)?, + server_id: key_pair.conn_pool.server_id().to_string(), + key_id: key_pair.private_key_id.clone(), + algorithm: key_pair.algorithm, + flags: key_pair.flags, + }) + } + + pub fn url(&self) -> &str { + self.url.as_ref() + } + + pub fn server_id(&self) -> &str { + &self.server_id + } + + pub fn key_id(&self) -> &str { + &self.key_id + } + + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + pub fn flags(&self) -> u16 { + self.flags + } + + pub fn into_url(self) -> Url { + self.url + } + } + + impl KeyUrl { + fn mk_url(key_pair: &KeyPair, key_id: &str) -> Result { + // We have to store the algorithm in the URL because the DNSSEC + // algorithm (e.g. 5 and 7) don't necessarily correspond to the + // cryptographic algorithm of the key known to the HSM. And we + // have to store the flags in the URL because these are not known + // to the HSM, they say someting about the use to which the key + // will be put of which the HSM is unaware. + let url = format!( + "kmip://{}/keys/{}?algorithm={}&flags={}", + key_pair.conn_pool().server_id(), + key_id, + key_pair.algorithm, + key_pair.flags + ); + + let url = Url::parse(&url).map_err::(|err| { + format!("unable to parse {url} as URL: {err}").into() + })?; + + Ok(url) + } + } + + impl TryFrom for KeyUrl { + type Error = SignError; + + fn try_from(url: Url) -> Result { + let server_id = url.host_str().ok_or(format!("Key lacks hostname component: {url}"))?.to_string(); + + let url_path = url.path().to_string(); + let key_id = url_path + .strip_prefix("/keys/") + .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + + let key_id = key_id.to_string(); + let mut flags = None; + let mut algorithm = None; + for (k, v) in url.query_pairs() { + match &*k { + "flags" => flags = Some(v.parse::().map_err(|err| format!("Key URL flags value is invalid: {err}"))?), + "algorithm" => algorithm = Some(SecurityAlgorithm::from_str(&v).map_err(|err| format!("Key URL algorithm value is invalid: {err}"))?), + unknown => Err(format!("Key URL contains unknown query parameter: {unknown}"))?, + } + } + let algorithm = algorithm.ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; + let flags = flags.ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + + Ok(Self { + url, + server_id, + key_id, + algorithm, + flags, + }) + } + } + //----------- generate() ------------------------------------------------- /// Generate a new secret key for the given algorithm. @@ -659,7 +895,7 @@ pub mod sign { let client = conn_pool .get() - .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server: {err}")))?; + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; // TODO: Determine this on first use of the HSM? // PyKMIP doesn't support ActivationDate. @@ -867,7 +1103,7 @@ pub mod sign { if !activate_on_create { let client = conn_pool .get() - .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server: {err}")))?; + .map_err(|err| GenerateError::Kmip(format!("Key generation failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; let request = RequestPayload::Activate(Some(private_key_unique_identifier)); @@ -896,6 +1132,10 @@ pub mod sign { Ok(key_pair) } + + //----------- TODO: destroy() -------------------------------------------- + + // TODO } #[cfg(test)] @@ -947,6 +1187,7 @@ mod tests { eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), conn_settings.into(), 16384, Some(Duration::from_secs(60)), @@ -1009,6 +1250,7 @@ mod tests { eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( + "Test server".to_string(), conn_settings.into(), 16384, Some(Duration::from_secs(60)), diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 2516404ea..eb5846e95 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -10,32 +10,17 @@ //! existing connection is considered to be "broken" at the network //! level. use core::fmt::Display; +use core::ops::Deref; use std::net::TcpStream; use std::string::String; use std::{sync::Arc, time::Duration}; use kmip::client::{Client, ConnectionSettings}; - // TODO: Remove the hard-coded use of OpenSSL? use openssl::ssl::SslStream; +use r2d2::PooledConnection; -/// A KMIP client used to send KMIP requests and receive KMIP responses. -/// -/// Note: Currently depends on OpenSSL because our KMIP implementation -/// supports elements of KMIP 1.2 [1] but not KMIP 1.3 [2], but prior to KMIP -/// 1.3 it is required for servers to support TLS 1.0, and RustLS doesn't -/// support TLS < 1.2. -/// -/// [1]: https://docs.oasis-open.org/kmip/profiles/v1.2/os/kmip-profiles-v1.2-os.html#_Toc409613167 -/// [2]: https://docs.oasis-open.org/kmip/profiles/v1.3/os/kmip-profiles-v1.3-os.html#_Toc473103053 -pub type KmipTlsClient = Client>; - -/// A pool of already connected KMIP clients. -/// -/// This pool can be used to acquire a KMIP client without first having to -/// wait for it to connect at the TCP/TLS level, and without unnecessarily -/// closing the connection when finished. -pub type KmipConnPool = r2d2::Pool; +//------------ KmipConnError ------------------------------------------------- #[derive(Clone, Debug)] pub struct KmipConnError(String); @@ -52,20 +37,53 @@ impl Display for KmipConnError { } } -/// Manages KMIP TCP + TLS connection creation. +//------------ KmipConn ------------------------------------------------------ + +/// A KMIP connection pool connection. +pub struct KmipConn { + conn: PooledConnection, +} + +impl KmipConn { + fn new(conn: PooledConnection) -> Self { + Self { conn } + } +} + +impl Deref for KmipConn { + type Target = KmipTlsClient; + + fn deref(&self) -> &Self::Target { + self.conn.deref() + } +} + +/// A KMIP client used to send KMIP requests and receive KMIP responses. /// -/// Uses the [r2d2] crate to manage a pool of connections. +/// Note: Currently depends on OpenSSL because our KMIP implementation +/// supports elements of KMIP 1.2 [1] but not KMIP 1.3 [2], but prior to KMIP +/// 1.3 it is required for servers to support TLS 1.0, and RustLS doesn't +/// support TLS < 1.2. /// -/// [r2d2]: https://crates.io/crates/r2d2/ -#[derive(Debug)] -pub struct ConnectionManager { +/// [1]: https://docs.oasis-open.org/kmip/profiles/v1.2/os/kmip-profiles-v1.2-os.html#_Toc409613167 +/// [2]: https://docs.oasis-open.org/kmip/profiles/v1.3/os/kmip-profiles-v1.3-os.html#_Toc473103053 +pub type KmipTlsClient = Client>; + +/// A pool of already connected KMIP clients. +/// +/// This pool can be used to acquire a KMIP client without first having to +/// wait for it to connect at the TCP/TLS level, and without unnecessarily +/// closing the connection when finished. +#[derive(Clone, Debug)] +pub struct KmipConnPool { + server_id: String, conn_settings: Arc, + pool: r2d2::Pool, } -impl ConnectionManager { - /// Create a pool of up-to N TCP + TLS connections to the KMIP server. - #[rustfmt::skip] - pub fn create_connection_pool( +impl KmipConnPool { + pub fn new( + server_id: String, conn_settings: Arc, max_conncurrent_connections: u32, max_life_time: Option, @@ -75,32 +93,93 @@ impl ConnectionManager { // Don't pre-create idle connections to the KMIP server .min_idle(Some(0)) - // Create at most this many concurrent connections to the KMIP server + // Create at most this many concurrent connections to the KMIP + // server .max_size(max_conncurrent_connections) - // Don't verify that a connection is usable when fetching it from the pool (as doing so requires sending a - // request to the server and we might as well just try the actual request that we want the connection for) + // Don't verify that a connection is usable when fetching it from + // the pool (as doing so requires sending a request to the server + // and we might as well just try the actual request that we want + // the connection for) .test_on_check_out(false) - // Don't use the default logging behaviour as `[ERROR] [r2d2] Server error: ...` is a bit confusing for end - // users who shouldn't know or care that we use the r2d2 crate. + // Don't use the default logging behaviour as `[ERROR] [r2d2] + // Server error: ...` is a bit confusing for end users who + // shouldn't know or care that we use the r2d2 crate. // .error_handler(Box::new(ErrorLoggingHandler)) - // Don't keep using the same connection for longer than around N minutes (unless in use in which case it - // will wait until the connection is returned to the pool before closing it) - maybe long held connections - // would run into problems with some firewalls. + // Don't keep using the same connection for longer than around N + // minutes (unless in use in which case it will wait until the + // connection is returned to the pool before closing it) - maybe + // long held connections would run into problems with some + // firewalls. .max_lifetime(max_life_time) - // Don't keep connections open that were not used in the last N minutes. + // Don't keep connections open that were not used in the last N + // minutes. .idle_timeout(max_idle_time) - // Don't wait longer than N seconds for a new connection to be established, instead try again to connect. - .connection_timeout(conn_settings.connect_timeout.unwrap_or(Duration::from_secs(30))) + // Don't wait longer than N seconds for a new connection to be + // established, instead try again to connect. + .connection_timeout( + conn_settings + .connect_timeout + .unwrap_or(Duration::from_secs(30)), + ) + + // Use our connection manager to create connections in the pool + // and to verify their health + .build(ConnectionManager { + conn_settings: conn_settings.clone(), + })?; + + Ok(Self { + server_id, + conn_settings, + pool, + }) + } + + pub fn server_id(&self) -> &str { + &self.server_id + } + + pub fn conn_settings(&self) -> &ConnectionSettings { + &self.conn_settings + } - // Use our connection manager to create connections in the pool and to verify their health - .build(ConnectionManager { conn_settings })?; + pub fn get(&self) -> Result { + Ok(KmipConn::new(self.pool.get()?)) + } +} - Ok(pool) +/// Manages KMIP TCP + TLS connection creation. +/// +/// Uses the [r2d2] crate to manage a pool of connections. +/// +/// [r2d2]: https://crates.io/crates/r2d2/ +#[derive(Debug)] +pub struct ConnectionManager { + conn_settings: Arc, +} + +impl ConnectionManager { + /// Create a pool of up-to N TCP + TLS connections to the KMIP server. + #[rustfmt::skip] + pub fn create_connection_pool( + server_id: String, + conn_settings: Arc, + max_conncurrent_connections: u32, + max_life_time: Option, + max_idle_time: Option, + ) -> Result { + Ok(KmipConnPool::new( + server_id, + conn_settings, + max_conncurrent_connections, + max_life_time, + max_idle_time + )?) } /// Connect using the given connection settings to a KMIP server. diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 1794624a1..1cc8a0ecc 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -685,6 +685,10 @@ pub mod sign { self.algorithm } + fn flags(&self) -> u16 { + self.flags + } + fn dnskey(&self) -> Result>, SignError> { match self.algorithm { SecurityAlgorithm::RSASHA256 => { diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index 8402c2fc3..1782363c5 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -502,6 +502,15 @@ pub mod sign { } } + fn flags(&self) -> u16 { + match *self { + KeyPair::RsaSha256 { flags, .. } => flags, + KeyPair::EcdsaP256Sha256 { flags, .. } => flags, + KeyPair::EcdsaP384Sha384 { flags, .. } => flags, + KeyPair::Ed25519(_, flags) => flags, + } + } + fn dnskey(&self) -> Result>, SignError> { match self { Self::RsaSha256 { key, flags, rng: _ } => { diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 8a619eeb0..5ed6fe17e 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -36,7 +36,7 @@ //! //! // Check that the owner, algorithm, and key tag matched expectations. //! assert_eq!(key_pair.algorithm(), SecurityAlgorithm::ED25519); -//! assert_eq!(key_pair.dnskey().key_tag(), 56037); +//! assert_eq!(key_pair.dnskey().unwrap().key_tag(), 56037); //! ``` //! //! # Generating keys @@ -176,6 +176,8 @@ pub trait SignRaw { /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 fn algorithm(&self) -> SecurityAlgorithm; + fn flags(&self) -> u16; + /// The public key. /// /// This can be used to verify produced signatures. It must use the same @@ -379,6 +381,17 @@ impl SignRaw for KeyPair { } } + fn flags(&self) -> u16 { + match self { + #[cfg(feature = "ring")] + Self::Ring(key) => key.flags(), + #[cfg(feature = "openssl")] + Self::OpenSSL(key) => key.flags(), + #[cfg(feature = "kmip")] + Self::Kmip(key) => key.flags(), + } + } + fn dnskey(&self) -> Result>, SignError> { match self { #[cfg(feature = "ring")] From 83de250741bf8b32060e44548e39d6e8bd3cfef8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:40:58 +0200 Subject: [PATCH 492/569] Cargo fmt. --- src/crypto/kmip.rs | 138 ++++++++++++++++++++++++++-------------- src/crypto/kmip_pool.rs | 7 -- src/crypto/openssl.rs | 22 +++++-- 3 files changed, 107 insertions(+), 60 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 3bc1502fc..85ef74851 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -74,8 +74,8 @@ impl std::error::Error for GenerateError {} /// /// Identifies an RSA public key with no limitation to either RSASSA-PSS or /// RSAES-OEAP. -pub const RSA_ENCRYPTION_OID: ConstOid - = Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); +pub const RSA_ENCRYPTION_OID: ConstOid = + Oid(&[42, 134, 72, 134, 247, 13, 1, 1, 1]); /// [RFC 5480](https://tools.ietf.org/html/rfc5480) `ecPublicKey`. /// @@ -258,7 +258,7 @@ impl PublicKey { let algorithm = Oid::take_from(cons)?; if algorithm != RSA_ENCRYPTION_OID { return Err(cons.content_err("Only SubjectPublicKeyInfo with algorithm rsaEncryption is supported")); - } + } // Ignore the parameters. Ok(()) })?; @@ -451,7 +451,8 @@ pub mod sign { algorithm, conn_pool.clone(), ) - .dnskey(flags).map_err(|err| GenerateError::Kmip(err.to_string()))?; + .dnskey(flags) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; Ok(Self { algorithm, @@ -477,9 +478,14 @@ pub mod sign { } else if priv_key_url.server_id() != conn_pool.server_id() { return Err(GenerateError::Kmip(format!("Key URLs have different server ID to the KMIP connection pool: {} vs {}", priv_key_url.server_id(), conn_pool.server_id()).into())); } else { - Self::new(priv_key_url.algorithm(), priv_key_url.flags(), priv_key_url.key_id(), pub_key_url.key_id(), conn_pool) + Self::new( + priv_key_url.algorithm(), + priv_key_url.flags(), + priv_key_url.key_id(), + pub_key_url.key_id(), + conn_pool, + ) } - } pub fn algorithm(&self) -> SecurityAlgorithm { @@ -528,14 +534,14 @@ pub mod sign { self.algorithm, self.flags ); - + let url = Url::parse(&url).map_err::(|err| { format!("unable to parse {url} as URL: {err}").into() })?; - + Ok(url) } - + pub fn conn_pool(&self) -> &KmipConnPool { &self.conn_pool } @@ -545,7 +551,7 @@ pub mod sign { fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } - + fn flags(&self) -> u16 { self.flags } @@ -593,7 +599,12 @@ pub mod sign { HashingAlgorithm::SHA256, DigestType::Sha256, ), - alg => return Err(format!("Algorithm not supported for KMIP signing: {alg}").into()), + alg => { + return Err(format!( + "Algorithm not supported for KMIP signing: {alg}" + ) + .into()) + } }; // PyKMIP requires that the padding method be specified otherwise @@ -620,14 +631,13 @@ pub mod sign { ); // Execute the request and capture the response - let client = self - .conn_pool - .get() - .map_err(|err| format!("Error while obtaining KMIP pool connection: {err}"))?; + let client = self.conn_pool.get().map_err(|err| { + format!("Error while obtaining KMIP pool connection: {err}") + })?; - let res = client - .do_request(request) - .map_err(|err| format!("Error while sending KMIP request: {err}"))?; + let res = client.do_request(request).map_err(|err| { + format!("Error while sending KMIP request: {err}") + })?; tracing::trace!("Checking sign payload"); let ResponsePayload::Sign(signed) = res else { @@ -706,23 +716,23 @@ pub mod sign { } /// A URL that represents a key stored in a KMIP compatible HSM. - /// + /// /// The URL structure is: - /// + /// /// kmip:///keys/?algorithm=&flags= - /// + /// /// The algorithm and flags must be stored in the URL because they are /// DNSSEC specific and not properties of the key itself and thus not /// known to or stored by the HSM. - /// + /// /// While algorithm may seem to be something known to and stored by the /// HSM, DNSSEC complicates that by aliasing multiple algorithm numbers to /// the same cryptographic algorithm, and we need to know when using the /// key which _DNSSEC_ algorithm number to use. - /// + /// /// The server_id could be the actual address of the target, but does not /// have to be. There are multiple for this: - /// + /// /// - In a highly available clustered deployment across multiple subnets /// it could be that the clustered HSM is available to the clustered /// application via different names/IP addresses in different subnets @@ -755,7 +765,7 @@ pub mod sign { /// such as TLS client certficate details and whether or not to /// require the server TLS certificate to be valid (which can be /// inconvenient in test setups using self-signed certificates). - /// + /// /// Thus an abstract server_id is stored in the key URL and it is the /// responsibility of the user of the key URL to map the server id to the /// full set of settings required to successfully connect to the HSM to @@ -774,12 +784,20 @@ pub mod sign { server_id: String, key_id: String, algorithm: SecurityAlgorithm, - flags: u16 + flags: u16, ) -> Self { - Self { url, server_id, key_id, algorithm, flags } + Self { + url, + server_id, + key_id, + algorithm, + flags, + } } - - pub fn new_public_key_url(key_pair: &KeyPair) -> Result { + + pub fn new_public_key_url( + key_pair: &KeyPair, + ) -> Result { Ok(Self { url: Self::mk_url(key_pair, &key_pair.public_key_id)?, server_id: key_pair.conn_pool.server_id().to_string(), @@ -789,7 +807,9 @@ pub mod sign { }) } - pub fn new_private_key_url(key_pair: &KeyPair) -> Result { + pub fn new_private_key_url( + key_pair: &KeyPair, + ) -> Result { Ok(Self { url: Self::mk_url(key_pair, &key_pair.private_key_id)?, server_id: key_pair.conn_pool.server_id().to_string(), @@ -802,7 +822,7 @@ pub mod sign { pub fn url(&self) -> &str { self.url.as_ref() } - + pub fn server_id(&self) -> &str { &self.server_id } @@ -810,11 +830,11 @@ pub mod sign { pub fn key_id(&self) -> &str { &self.key_id } - + pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } - + pub fn flags(&self) -> u16 { self.flags } @@ -825,7 +845,10 @@ pub mod sign { } impl KeyUrl { - fn mk_url(key_pair: &KeyPair, key_id: &str) -> Result { + fn mk_url( + key_pair: &KeyPair, + key_id: &str, + ) -> Result { // We have to store the algorithm in the URL because the DNSSEC // algorithm (e.g. 5 and 7) don't necessarily correspond to the // cryptographic algorithm of the key known to the HSM. And we @@ -850,27 +873,49 @@ pub mod sign { impl TryFrom for KeyUrl { type Error = SignError; - + fn try_from(url: Url) -> Result { - let server_id = url.host_str().ok_or(format!("Key lacks hostname component: {url}"))?.to_string(); + let server_id = url + .host_str() + .ok_or(format!("Key lacks hostname component: {url}"))? + .to_string(); let url_path = url.path().to_string(); - let key_id = url_path - .strip_prefix("/keys/") - .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + let key_id = url_path.strip_prefix("/keys/").ok_or(format!( + "Key URL lacks /keys/ path component: {url}" + ))?; let key_id = key_id.to_string(); let mut flags = None; let mut algorithm = None; for (k, v) in url.query_pairs() { match &*k { - "flags" => flags = Some(v.parse::().map_err(|err| format!("Key URL flags value is invalid: {err}"))?), - "algorithm" => algorithm = Some(SecurityAlgorithm::from_str(&v).map_err(|err| format!("Key URL algorithm value is invalid: {err}"))?), - unknown => Err(format!("Key URL contains unknown query parameter: {unknown}"))?, + "flags" => { + flags = Some(v.parse::().map_err(|err| { + format!("Key URL flags value is invalid: {err}") + })?) + } + "algorithm" => { + algorithm = + Some(SecurityAlgorithm::from_str(&v).map_err( + |err| { + format!( + "Key URL algorithm value is invalid: {err}" + ) + }, + )?) + } + unknown => Err(format!( + "Key URL contains unknown query parameter: {unknown}" + ))?, } } - let algorithm = algorithm.ok_or(format!("Key URL lacks algorithm query parameter: {url}"))?; - let flags = flags.ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + let algorithm = algorithm.ok_or(format!( + "Key URL lacks algorithm query parameter: {url}" + ))?; + let flags = flags.ok_or(format!( + "Key URL lacks flags query parameter: {url}" + ))?; Ok(Self { url, @@ -1096,8 +1141,9 @@ pub mod sign { flags, private_key_unique_identifier.as_str(), public_key_unique_identifier.as_str(), - conn_pool.clone()) - .map_err(|err| GenerateError::Kmip(err.to_string()))?; + conn_pool.clone(), + ) + .map_err(|err| GenerateError::Kmip(err.to_string()))?; // Activate the key if not already, otherwise it cannot be used for signing. if !activate_on_create { diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index eb5846e95..eb1322c34 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -92,33 +92,27 @@ impl KmipConnPool { let pool = r2d2::Pool::builder() // Don't pre-create idle connections to the KMIP server .min_idle(Some(0)) - // Create at most this many concurrent connections to the KMIP // server .max_size(max_conncurrent_connections) - // Don't verify that a connection is usable when fetching it from // the pool (as doing so requires sending a request to the server // and we might as well just try the actual request that we want // the connection for) .test_on_check_out(false) - // Don't use the default logging behaviour as `[ERROR] [r2d2] // Server error: ...` is a bit confusing for end users who // shouldn't know or care that we use the r2d2 crate. // .error_handler(Box::new(ErrorLoggingHandler)) - // Don't keep using the same connection for longer than around N // minutes (unless in use in which case it will wait until the // connection is returned to the pool before closing it) - maybe // long held connections would run into problems with some // firewalls. .max_lifetime(max_life_time) - // Don't keep connections open that were not used in the last N // minutes. .idle_timeout(max_idle_time) - // Don't wait longer than N seconds for a new connection to be // established, instead try again to connect. .connection_timeout( @@ -126,7 +120,6 @@ impl KmipConnPool { .connect_timeout .unwrap_or(Duration::from_secs(30)), ) - // Use our connection manager to create connections in the pool // and to verify their health .build(ConnectionManager { diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 1cc8a0ecc..84a6397aa 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -763,24 +763,32 @@ pub mod sign { SecurityAlgorithm::ECDSAP256SHA256 => signature .try_into() .map(Signature::EcdsaP256Sha256) - .map_err(|_| "OpenSSL ECDSAP256SHA256 signature too large".into()), + .map_err(|_| { + "OpenSSL ECDSAP256SHA256 signature too large".into() + }), SecurityAlgorithm::ECDSAP384SHA384 => signature .try_into() .map(Signature::EcdsaP384Sha384) - .map_err(|_| "OpenSSL ECDSAP384SHA384 signature too large".into()), + .map_err(|_| { + "OpenSSL ECDSAP384SHA384 signature too large".into() + }), - SecurityAlgorithm::ED25519 => signature - .try_into() - .map(Signature::Ed25519) - .map_err(|_| "OpenSSL ED25519 signature too large".into()), + SecurityAlgorithm::ED25519 => { + signature.try_into().map(Signature::Ed25519).map_err( + |_| "OpenSSL ED25519 signature too large".into(), + ) + } SecurityAlgorithm::ED448 => signature .try_into() .map(Signature::Ed448) .map_err(|_| "OpenSSL ED448 signature too large".into()), - alg => Err(format!("OpenSSL signature algorithm not supported: {alg}").into()), + alg => Err(format!( + "OpenSSL signature algorithm not supported: {alg}" + ) + .into()), } } } From b0e970fba782d0a17528b7220a52f08b95f74e30 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:41:55 +0200 Subject: [PATCH 493/569] Clippy. --- src/crypto/kmip_pool.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index eb1322c34..9758f6c80 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -166,13 +166,13 @@ impl ConnectionManager { max_life_time: Option, max_idle_time: Option, ) -> Result { - Ok(KmipConnPool::new( + KmipConnPool::new( server_id, conn_settings, max_conncurrent_connections, max_life_time, max_idle_time - )?) + ) } /// Connect using the given connection settings to a KMIP server. From 847e2e1bdbb806c14a4c01ab47b48e7dd6b93cdb Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Jul 2025 10:18:29 +0200 Subject: [PATCH 494/569] Minor correct to error message. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 85ef74851..4f4c1b44d 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -877,7 +877,7 @@ pub mod sign { fn try_from(url: Url) -> Result { let server_id = url .host_str() - .ok_or(format!("Key lacks hostname component: {url}"))? + .ok_or(format!("Key URL lacks hostname component: {url}"))? .to_string(); let url_path = url.path().to_string(); From 7e28e85a313e3534d781d910558a3380f50a8e06 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 15 Jul 2025 11:52:32 +0200 Subject: [PATCH 495/569] Merge latest changes from domain branch keyset-improvements. --- examples/keyset.rs | 28 +- src/dnssec/sign/keys/keyset.rs | 883 ++++++++++++++++++++++++++++----- 2 files changed, 786 insertions(+), 125 deletions(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index 35f9c4235..ee0b396d0 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -134,6 +134,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else if keytype == "zsk" { @@ -143,6 +144,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else if keytype == "csk" { @@ -152,6 +154,7 @@ fn do_addkey(filename: &str, args: &[String]) { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); } else { @@ -197,14 +200,14 @@ fn do_start(filename: &str, args: &[String]) { .keys() .iter() .filter_map(|(pr, k)| match rolltype { - RollType::KskRoll => { + RollType::KskRoll | RollType::KskDoubleDsRoll => { if let KeyType::Ksk(keystate) = k.keytype() { Some((keystate.clone(), pr)) } else { None } } - RollType::ZskRoll => { + RollType::ZskRoll | RollType::ZskDoubleSignatureRoll => { if let KeyType::Zsk(keystate) = k.keytype() { Some((keystate.clone(), pr)) } else { @@ -370,7 +373,7 @@ fn do_status(filename: &str, args: &[String]) { pubref, key.privref().unwrap_or_default(), ); - println!("\t\tState: {}", keystate); + println!("\t\tState: {keystate}"); } KeyType::Csk(keystate_ksk, keystate_zsk) => { println!( @@ -378,8 +381,8 @@ fn do_status(filename: &str, args: &[String]) { pubref, key.privref().unwrap_or_default(), ); - println!("\t\tKSK role state: {}", keystate_ksk,); - println!("\t\tZSK role state: {}", keystate_zsk,); + println!("\t\tKSK role state: {keystate_ksk}"); + println!("\t\tZSK role state: {keystate_zsk}"); } } let ts = key.timestamps(); @@ -473,7 +476,7 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { | KeyType::Include(keystate) => keystate, }; if status.present() { - println!("\t\t\t{}", pubref); + println!("\t\t\t{pubref}"); } } println!("\t\tKeys signing the DNSKEY RRset:"); @@ -482,7 +485,7 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { if keystate.signer() { - println!("\t\t\t{}", pubref); + println!("\t\t\t{pubref}"); } } KeyType::Zsk(_) | KeyType::Include(_) => (), @@ -497,7 +500,7 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { KeyType::Zsk(keystate) | KeyType::Csk(_, keystate) => { if keystate.signer() { - println!("\t\t{}", pubref); + println!("\t\t{pubref}"); } } KeyType::Ksk(_) | KeyType::Include(_) => (), @@ -515,7 +518,7 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { | KeyType::Include(keystate) => keystate, }; if status.at_parent() { - println!("\t\t{}", pubref); + println!("\t\t{pubref}"); } } } @@ -530,7 +533,7 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { | KeyType::Include(keystate) => keystate, }; if status.at_parent() { - println!("\t\t{}", pubref); + println!("\t\t{pubref}"); } } } @@ -552,6 +555,9 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { Action::ReportDsPropagated => println!( "\tReport that the DS RRset has propagated at the parent" ), + Action::WaitDsPropagated => { + println!("\tWait until DS RRset has propagated at the parent") + } } } -} +} \ No newline at end of file diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 976771a2f..8ea928759 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -19,9 +19,11 @@ //! let mut ks = KeySet::new(Name::from_str("example.com").unwrap()); //! //! // Add two keys. -//! ks.add_key_ksk("first KSK.key".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); +//! ks.add_key_ksk("first KSK.key".to_string(), None, +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); //! ks.add_key_zsk("first ZSK.key".to_string(), -//! Some("first ZSK.private".to_string()), SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now()); +//! Some("first ZSK.private".to_string()), +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); //! //! // Save the state. //! let json = serde_json::to_string(&ks).unwrap(); @@ -109,11 +111,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Ksk(keystate), @@ -137,11 +143,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Zsk(keystate), @@ -165,11 +175,15 @@ impl KeySet { algorithm: SecurityAlgorithm, key_tag: u16, creation_ts: UnixTime, + available: bool, ) -> Result<(), Error> { if !self.unique_key_tag(key_tag) { return Err(Error::DuplicateKeyTag); } - let keystate: KeyState = Default::default(); + let keystate = KeyState { + available, + ..Default::default() + }; let key = Key::new( privref, KeyType::Csk(keystate.clone(), keystate), @@ -418,6 +432,7 @@ impl KeySet { }; if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -454,6 +469,76 @@ impl KeySet { Ok(()) } + fn update_ksk_double_ds( + &mut self, + mode: Mode, + old: &[&str], + new: &[&str], + ) -> Result<(), Error> { + let mut tmpkeys = self.keys.clone(); + let keys: &mut HashMap = match mode { + Mode::DryRun => &mut tmpkeys, + Mode::ForReal => &mut self.keys, + }; + let mut algs_old = HashSet::new(); + for k in old { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Ksk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + + // Set old for any key we find. + keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); + } + let mut algs_new = HashSet::new(); + for k in new { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Ksk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + if *keystate + != (KeyState { + available: true, + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + keystate.at_parent = true; + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); + } + + // Make sure we have at least one key in the right state. + if !keys.iter().any(|(_, k)| { + if let KeyType::Ksk(keystate) = &k.keytype { + !keystate.old && keystate.at_parent + } else { + false + } + }) { + return Err(Error::NoSuitableKeyPresent); + } + Ok(()) + } + fn update_zsk( &mut self, mode: Mode, @@ -491,6 +576,80 @@ impl KeySet { }; if *keystate != (KeyState { + available: true, + old: false, + signer: false, + present: false, + at_parent: false, + }) + { + return Err(Error::WrongKeyState); + } + + // Move key state to Incoming. + keystate.present = true; + key.timestamps.published = Some(now.clone()); + + // Add algorithm + algs_new.insert(key.algorithm); + } + + // Make sure the sets of algorithms are the same. + if algs_old != algs_new { + return Err(Error::AlgorithmSetsMismatch); + } + + // Make sure we have at least one key in incoming state. + if !keys.iter().any(|(_, k)| { + if let KeyType::Zsk(keystate) = &k.keytype { + !keystate.old || keystate.present + } else { + false + } + }) { + return Err(Error::NoSuitableKeyPresent); + } + Ok(()) + } + + fn update_zsk_double_signature( + &mut self, + mode: Mode, + old: &[&str], + new: &[&str], + ) -> Result<(), Error> { + let mut tmpkeys = self.keys.clone(); + let keys: &mut HashMap = match mode { + Mode::DryRun => &mut tmpkeys, + Mode::ForReal => &mut self.keys, + }; + let mut algs_old = HashSet::new(); + for k in old { + let Some(ref mut key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Zsk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + + // Set old for any key we find. + keystate.old = true; + + // Add algorithm + algs_old.insert(key.algorithm); + } + let now = UnixTime::now(); + let mut algs_new = HashSet::new(); + for k in new { + let Some(key) = keys.get_mut(&(*k).to_string()) else { + return Err(Error::KeyNotFound); + }; + let KeyType::Zsk(ref mut keystate) = key.keytype else { + return Err(Error::WrongKeyType); + }; + if *keystate + != (KeyState { + available: true, old: false, signer: false, present: false, @@ -502,6 +661,7 @@ impl KeySet { // Move key state to Incoming. keystate.present = true; + keystate.signer = true; key.timestamps.published = Some(now.clone()); // Add algorithm @@ -569,6 +729,7 @@ impl KeySet { KeyType::Ksk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -586,6 +747,7 @@ impl KeySet { KeyType::Zsk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -602,6 +764,7 @@ impl KeySet { KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { if *ksk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -617,6 +780,7 @@ impl KeySet { if *zsk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -705,6 +869,7 @@ impl KeySet { | KeyType::Zsk(ref mut keystate) => { if *keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -722,6 +887,7 @@ impl KeySet { KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { if *ksk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -737,6 +903,7 @@ impl KeySet { if *zsk_keystate != (KeyState { + available: true, old: false, signer: false, present: false, @@ -863,7 +1030,8 @@ pub enum KeyType { /// State of a key. /// -/// The state is expressed as four booleans: +/// The state is expressed as five booleans: +/// * available. The key is available as an incoming key during key rolls. /// * old. Set if the key is on its way out. /// * signer. Set if the key either signes the DNSKEY RRset or the rest of the /// zone. @@ -871,6 +1039,7 @@ pub enum KeyType { /// * at_parent. If the key has a DS record at the parent. #[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct KeyState { + available: bool, old: bool, signer: bool, present: bool, @@ -1141,6 +1310,11 @@ pub enum Action { /// the DS records. ReportDsPropagated, + /// Wait for the update FS records to have propagated to all + /// secondaries that serve the parent zone. Waiting is necessary to + /// avoid removing the CDS and CDNSKEY records too soon. + WaitDsPropagated, + /// Report whether updated RRSIG records have propagated to all /// secondaries that the serve the zone. For propagation it is /// sufficient to track the signatures on the SOA record. Report the @@ -1157,24 +1331,38 @@ pub enum Action { /// The type of key roll to perform. #[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum RollType { - /// A KSK roll. + /// A KSK roll. This implements the Double-Signature ZSK Roll as described + /// in Section 4.1.2 of RFC 6781. KskRoll, - /// A ZSK roll. + /// An alternative KSK roll. This implements the Double-DS KSK Roll as + /// described in Section 4.1.2 of RFC 6781. + KskDoubleDsRoll, + + /// A ZSK roll. This implements the Pre-Publish ZSK Roll as described + /// in Section 4.1.1.1. of RFC 6781. ZskRoll, - /// A CSK roll. + /// An alternative ZSK roll. This implements the Double-Signature ZSK + /// Roll as described in Section 4.1.1.2. of RFC 6781. + ZskDoubleSignatureRoll, + + /// A CSK roll. This implements neither of the two algorithms in + /// Section 4.1.3. of RFC 6781. CskRoll, - /// An algorithm roll. + /// An algorithm roll. This implements the 'liberal approach' as + /// described in Section 4.1.4 of RFC 6781. AlgorithmRoll, } impl RollType { - fn rollfn(&self) -> fn(RollOp, &mut KeySet) -> Result<(), Error> { + fn rollfn(&self) -> fn(RollOp<'_>, &mut KeySet) -> Result<(), Error> { match self { RollType::KskRoll => ksk_roll, + RollType::KskDoubleDsRoll => ksk_double_ds_roll, RollType::ZskRoll => zsk_roll, + RollType::ZskDoubleSignatureRoll => zsk_double_signature_roll, RollType::CskRoll => csk_roll, RollType::AlgorithmRoll => algorithm_roll, } @@ -1182,7 +1370,11 @@ impl RollType { fn roll_actions_fn(&self) -> fn(RollState) -> Vec { match self { RollType::KskRoll => ksk_roll_actions, + RollType::KskDoubleDsRoll => ksk_double_ds_roll_actions, RollType::ZskRoll => zsk_roll_actions, + RollType::ZskDoubleSignatureRoll => { + zsk_double_signature_roll_actions + } RollType::CskRoll => csk_roll_actions, RollType::AlgorithmRoll => algorithm_roll_actions, } @@ -1268,16 +1460,17 @@ impl fmt::Display for Error { } } -fn ksk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { +fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { // First check if the current KSK-roll state is idle. We need to // check all conflicting key rolls as well. The way we check is // to allow specified non-conflicting rolls and consider // everything else as a conflict. - if let Some(rolltype) = - ks.rollstates.keys().find(|k| **k != RollType::ZskRoll) - { + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::ZskRoll + && **k != RollType::ZskDoubleSignatureRoll + }) { if *rolltype == RollType::KskRoll { return Err(Error::WrongStateForRollOperation); } else { @@ -1389,141 +1582,125 @@ fn ksk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { Ok(()) } -fn ksk_roll_actions(rollstate: RollState) -> Vec { - let mut actions = Vec::new(); - match rollstate { - RollState::Propagation1 => { - actions.push(Action::UpdateDnskeyRrset); - actions.push(Action::ReportDnskeyPropagated); - } - RollState::CacheExpire1(_) => (), - RollState::Propagation2 => { - actions.push(Action::CreateCdsRrset); - actions.push(Action::UpdateDsRrset); - actions.push(Action::ReportDsPropagated); - } - RollState::CacheExpire2(_) => (), - RollState::Done => { - actions.push(Action::RemoveCdsRrset); - actions.push(Action::UpdateDnskeyRrset); - } - } - actions -} - -fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { +fn ksk_double_ds_roll( + rollop: RollOp<'_>, + ks: &mut KeySet, +) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { - // First check if the current ZSK-roll state is idle. We need - // to check all conflicting key rolls as well. The way we check - // is to allow specified non-conflicting rolls and consider + // First check if the current KSK-roll state is idle. We need to + // check all conflicting key rolls as well. The way we check is + // to allow specified non-conflicting rolls and consider // everything else as a conflict. - if let Some(rolltype) = - ks.rollstates.keys().find(|k| **k != RollType::KskRoll) - { - if *rolltype == RollType::ZskRoll { + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::ZskRoll + && **k != RollType::ZskDoubleSignatureRoll + }) { + if *rolltype == RollType::KskDoubleDsRoll { return Err(Error::WrongStateForRollOperation); } else { return Err(Error::ConflictingRollInProgress); } } // Check if we can move the states of the keys - ks.update_zsk(Mode::DryRun, old, new)?; + ks.update_ksk_double_ds(Mode::DryRun, old, new)?; // Move the states of the keys - ks.update_zsk(Mode::ForReal, old, new) - .expect("Should have been checked with DryRun"); + ks.update_ksk_double_ds(Mode::ForReal, old, new) + .expect("Should have been checked by DryRun"); } RollOp::Propagation1 => { - // Set the visiable time of new ZSKs to the current time. + // Set the ds_visible time of new KSKs to the current time. let now = UnixTime::now(); for k in ks.keys.values_mut() { - let KeyType::Zsk(ref keystate) = k.keytype else { + let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.present { + if keystate.old || !keystate.at_parent { continue; } - k.timestamps.visible = Some(now.clone()); + k.timestamps.ds_visible = Some(now.clone()); } } RollOp::CacheExpire1(ttl) => { for k in ks.keys.values_mut() { - let KeyType::Zsk(ref keystate) = k.keytype else { + let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; if keystate.old || !keystate.present { continue; } - let visible = k + let ds_visible = k .timestamps - .visible + .ds_visible .as_ref() .expect("Should have been set in Propagation1"); - let elapsed = visible.elapsed(); + let elapsed = ds_visible.elapsed(); let ttl = Duration::from_secs(ttl.into()); if elapsed < ttl { return Err(Error::Wait(ttl - elapsed)); } } - // Move the Incoming keys to Active. Move the Leaving keys to - // Retired. - for k in ks.keys.values_mut() { - let KeyType::Zsk(ref mut keystate) = k.keytype else { - continue; - }; - if !keystate.old && keystate.present { - keystate.signer = true; - } - if keystate.old { - keystate.signer = false; + // Old keys are no longer present and signing, new keys will + // be present and signing. + for k in &mut ks.keys.values_mut() { + if let KeyType::Ksk(ref mut keystate) = k.keytype { + if keystate.old && keystate.present { + keystate.present = false; + keystate.signer = false; + } + + if !keystate.old && keystate.at_parent { + keystate.present = true; + keystate.signer = true; + } } } } RollOp::Propagation2 => { - // Set the published time of new RRSIG records to the current time. + // Set the visible time of new keys to the current time. let now = UnixTime::now(); for k in ks.keys.values_mut() { - let KeyType::Zsk(ref keystate) = k.keytype else { + let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.signer { + if keystate.old || !keystate.present { continue; } - k.timestamps.rrsig_visible = Some(now.clone()); + k.timestamps.visible = Some(now.clone()); } } RollOp::CacheExpire2(ttl) => { for k in ks.keys.values_mut() { - let KeyType::Zsk(ref keystate) = k.keytype else { + let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.signer { + if keystate.old || !keystate.present { continue; } - let rrsig_visible = k + let visible = k .timestamps - .rrsig_visible + .ds_visible .as_ref() .expect("Should have been set in Propagation2"); - let elapsed = rrsig_visible.elapsed(); + let elapsed = visible.elapsed(); let ttl = Duration::from_secs(ttl.into()); if elapsed < ttl { return Err(Error::Wait(ttl - elapsed)); } } - // Move old keys out + // Move old DS records out for k in ks.keys.values_mut() { - let KeyType::Zsk(ref mut keystate) = k.keytype else { + let KeyType::Ksk(ref mut keystate) = k.keytype else { continue; }; - if keystate.old && !keystate.signer { - keystate.present = false; + if keystate.old && keystate.at_parent { + keystate.at_parent = false; k.timestamps.withdrawn = Some(UnixTime::now()); } } @@ -1533,7 +1710,7 @@ fn zsk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { Ok(()) } -fn zsk_roll_actions(rollstate: RollState) -> Vec { +fn ksk_roll_actions(rollstate: RollState) -> Vec { let mut actions = Vec::new(); match rollstate { RollState::Propagation1 => { @@ -1542,31 +1719,332 @@ fn zsk_roll_actions(rollstate: RollState) -> Vec { } RollState::CacheExpire1(_) => (), RollState::Propagation2 => { - actions.push(Action::UpdateRrsig); - actions.push(Action::ReportRrsigPropagated); + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::ReportDsPropagated); } RollState::CacheExpire2(_) => (), RollState::Done => { + actions.push(Action::RemoveCdsRrset); actions.push(Action::UpdateDnskeyRrset); } } actions } -fn csk_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { - match rollop { - RollOp::Start(old, new) => { - // First check if the current CSK-roll state is idle. We need - // to check all conflicting key rolls as well. The way we check - // is to allow specified non-conflicting rolls and consider - // everything else as a conflict. - if let Some(rolltype) = ks.rollstates.keys().next() { - if *rolltype == RollType::CskRoll { - return Err(Error::WrongStateForRollOperation); - } else { - return Err(Error::ConflictingRollInProgress); - } - } +fn ksk_double_ds_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::ReportDsPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::RemoveCdsRrset); + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::ReportDnskeyPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => { + actions.push(Action::CreateCdsRrset); + actions.push(Action::UpdateDsRrset); + actions.push(Action::WaitDsPropagated); + } // Missing: RemoveCdsRrset, This would require one more state, + } + actions +} + +fn zsk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current ZSK-roll state is idle. We need + // to check all conflicting key rolls as well. The way we check + // is to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::KskRoll && **k != RollType::KskDoubleDsRoll + }) { + if *rolltype == RollType::ZskRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } + // Check if we can move the states of the keys + ks.update_zsk(Mode::DryRun, old, new)?; + // Move the states of the keys + ks.update_zsk(Mode::ForReal, old, new) + .expect("Should have been checked with DryRun"); + } + RollOp::Propagation1 => { + // Set the visiable time of new ZSKs to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + } + } + RollOp::CacheExpire1(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + let visible = k + .timestamps + .visible + .as_ref() + .expect("Should have been set in Propagation1"); + let elapsed = visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move the Incoming keys to Active. Move the Leaving keys to + // Retired. + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref mut keystate) = k.keytype else { + continue; + }; + if !keystate.old && keystate.present { + keystate.signer = true; + } + if keystate.old { + keystate.signer = false; + } + } + } + RollOp::Propagation2 => { + // Set the published time of new RRSIG records to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + + k.timestamps.rrsig_visible = Some(now.clone()); + } + } + RollOp::CacheExpire2(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + + let rrsig_visible = k + .timestamps + .rrsig_visible + .as_ref() + .expect("Should have been set in Propagation2"); + let elapsed = rrsig_visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move old keys out + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref mut keystate) = k.keytype else { + continue; + }; + if keystate.old && !keystate.signer { + keystate.present = false; + k.timestamps.withdrawn = Some(UnixTime::now()); + } + } + } + RollOp::Done => (), + } + Ok(()) +} + +fn zsk_double_signature_roll( + rollop: RollOp<'_>, + ks: &mut KeySet, +) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current ZSK-roll state is idle. We need + // to check all conflicting key rolls as well. The way we check + // is to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = ks.rollstates.keys().find(|k| { + **k != RollType::KskRoll && **k != RollType::KskDoubleDsRoll + }) { + if *rolltype == RollType::ZskDoubleSignatureRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } + // Check if we can move the states of the keys + ks.update_zsk_double_signature(Mode::DryRun, old, new)?; + // Move the states of the keys + ks.update_zsk_double_signature(Mode::ForReal, old, new) + .expect("Should have been checked with DryRun"); + } + RollOp::Propagation1 => { + // Set the visiable time of new ZSKs to the current time. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + k.timestamps.visible = Some(now.clone()); + k.timestamps.rrsig_visible = Some(now.clone()); + } + } + RollOp::CacheExpire1(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.present { + continue; + } + + let visible = k + .timestamps + .visible + .as_ref() + .expect("Should have been set in Propagation1"); + let elapsed = visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + + // Move the Leaving keys to Retired. + let now = UnixTime::now(); + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref mut keystate) = k.keytype else { + continue; + }; + if keystate.old { + keystate.present = false; + keystate.signer = false; + k.timestamps.withdrawn = Some(now.clone()); + } + } + } + RollOp::Propagation2 => { + // Set the published time of new RRSIG records to the current time. + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + } + } + RollOp::CacheExpire2(ttl) => { + for k in ks.keys.values_mut() { + let KeyType::Zsk(ref keystate) = k.keytype else { + continue; + }; + if keystate.old || !keystate.signer { + continue; + } + + let rrsig_visible = k + .timestamps + .rrsig_visible + .as_ref() + .expect("Should have been set in Propagation2"); + let elapsed = rrsig_visible.elapsed(); + let ttl = Duration::from_secs(ttl.into()); + if elapsed < ttl { + return Err(Error::Wait(ttl - elapsed)); + } + } + } + RollOp::Done => (), + } + Ok(()) +} + +fn zsk_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::ReportDnskeyPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => { + actions.push(Action::UpdateDnskeyRrset); + } + } + actions +} + +fn zsk_double_signature_roll_actions(rollstate: RollState) -> Vec { + let mut actions = Vec::new(); + match rollstate { + RollState::Propagation1 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportDnskeyPropagated); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire1(_) => (), + RollState::Propagation2 => { + actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::UpdateRrsig); + actions.push(Action::ReportDnskeyPropagated); + actions.push(Action::ReportRrsigPropagated); + } + RollState::CacheExpire2(_) => (), + RollState::Done => (), + } + actions +} + +fn csk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { + match rollop { + RollOp::Start(old, new) => { + // First check if the current CSK-roll state is idle. We need + // to check all conflicting key rolls as well. The way we check + // is to allow specified non-conflicting rolls and consider + // everything else as a conflict. + if let Some(rolltype) = ks.rollstates.keys().next() { + if *rolltype == RollType::CskRoll { + return Err(Error::WrongStateForRollOperation); + } else { + return Err(Error::ConflictingRollInProgress); + } + } // Check if we can move the states of the keys ks.update_csk(Mode::DryRun, old, new)?; // Move the states of the keys @@ -1776,7 +2254,7 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { // An algorithm roll is similar to a CSK roll. The main difference is that // to zone is signed with all keys before introducing the DS records for // the new KSKs or CSKs. -fn algorithm_roll(rollop: RollOp, ks: &mut KeySet) -> Result<(), Error> { +fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { match rollop { RollOp::Start(old, new) => { // First check if the current algorithm-roll state is idle. We need @@ -1993,6 +2471,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); ks.add_key_zsk( @@ -2001,6 +2480,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 1, UnixTime::now(), + true, ) .unwrap(); @@ -2084,6 +2564,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 2, UnixTime::now(), + true, ) .unwrap(); ks.add_key_zsk( @@ -2092,6 +2573,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 3, UnixTime::now(), + true, ) .unwrap(); @@ -2149,6 +2631,87 @@ mod tests { assert_eq!(actions, []); ks.delete_key("first ZSK").unwrap(); + ks.add_key_zsk( + "third ZSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 4, + UnixTime::now(), + true, + ) + .unwrap(); + + let actions = ks + .start_roll( + RollType::ZskDoubleSignatureRoll, + &["second ZSK"], + &["third ZSK"], + ) + .unwrap(); + assert_eq!( + actions, + [ + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::ReportDnskeyPropagated, + Action::ReportRrsigPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "second ZSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + let mut zs = zone_sigs(&ks); + zs.sort(); + assert_eq!(zs, ["second ZSK", "third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks + .propagation1_complete(RollType::ZskDoubleSignatureRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = + ks.cache_expired1(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!( + actions, + [ + Action::UpdateDnskeyRrset, + Action::UpdateRrsig, + Action::ReportDnskeyPropagated, + Action::ReportRrsigPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks + .propagation2_complete(RollType::ZskDoubleSignatureRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = + ks.cache_expired2(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!(actions, []); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["first KSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["first KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["first KSK"]); + + let actions = ks.roll_done(RollType::ZskDoubleSignatureRoll).unwrap(); + assert_eq!(actions, []); + ks.delete_key("second ZSK").unwrap(); + let actions = ks .start_roll(RollType::KskRoll, &["first KSK"], &["second KSK"]) .unwrap(); @@ -2158,11 +2721,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first KSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first KSK", "second KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first KSK", "second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["first KSK"]); let actions = @@ -2182,11 +2745,11 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first KSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first KSK", "second KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); assert_eq!(dks, ["first KSK", "second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["second KSK"]); let actions = @@ -2202,28 +2765,119 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["second KSK", "second ZSK"]); + assert_eq!(dk, ["second KSK", "third ZSK"]); assert_eq!(dnskey_sigs(&ks), ["second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); assert_eq!(ds_keys(&ks), ["second KSK"]); let actions = ks.roll_done(RollType::KskRoll).unwrap(); assert_eq!(actions, []); ks.delete_key("first KSK").unwrap(); + ks.add_key_ksk( + "third KSK".to_string(), + None, + SecurityAlgorithm::ECDSAP256SHA256, + 5, + UnixTime::now(), + true, + ) + .unwrap(); + + let actions = ks + .start_roll( + RollType::KskDoubleDsRoll, + &["second KSK"], + &["third KSK"], + ) + .unwrap(); + assert_eq!( + actions, + [ + Action::CreateCdsRrset, + Action::UpdateDsRrset, + Action::ReportDsPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["second KSK", "third ZSK"]); + let mut dks = dnskey_sigs(&ks); + dks.sort(); + assert_eq!(dks, ["second KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + let mut dsks = ds_keys(&ks); + dsks.sort(); + assert_eq!(dsks, ["second KSK", "third KSK"]); + + let actions = ks + .propagation1_complete(RollType::KskDoubleDsRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = ks.cache_expired1(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!( + actions, + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::ReportDnskeyPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["third KSK", "third ZSK"]); + let mut dks = dnskey_sigs(&ks); + dks.sort(); + assert_eq!(dks, ["third KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + let mut dsks = ds_keys(&ks); + dsks.sort(); + assert_eq!(dsks, ["second KSK", "third KSK"]); + + let actions = ks + .propagation2_complete(RollType::KskDoubleDsRoll, 3600) + .unwrap(); + assert_eq!(actions, []); + + MockClock::advance_system_time(Duration::from_secs(3600)); + + let actions = ks.cache_expired2(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!( + actions, + [ + Action::CreateCdsRrset, + Action::UpdateDsRrset, + Action::WaitDsPropagated + ] + ); + let mut dk = dnskey(&ks); + dk.sort(); + assert_eq!(dk, ["third KSK", "third ZSK"]); + assert_eq!(dnskey_sigs(&ks), ["third KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["third KSK"]); + + let actions = ks.roll_done(RollType::KskDoubleDsRoll).unwrap(); + assert_eq!(actions, []); + ks.delete_key("second KSK").unwrap(); + ks.add_key_csk( "first CSK".to_string(), None, SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), + true, ) .unwrap(); let actions = ks .start_roll( RollType::CskRoll, - &["second KSK", "second ZSK"], + &["third KSK", "third ZSK"], &["first CSK"], ) .unwrap(); @@ -2233,12 +2887,12 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first CSK", "third KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); - assert_eq!(dks, ["first CSK", "second KSK"]); - assert_eq!(zone_sigs(&ks), ["second ZSK"]); - assert_eq!(ds_keys(&ks), ["second KSK"]); + assert_eq!(dks, ["first CSK", "third KSK"]); + assert_eq!(zone_sigs(&ks), ["third ZSK"]); + assert_eq!(ds_keys(&ks), ["third KSK"]); let actions = ks.propagation1_complete(RollType::CskRoll, 3600).unwrap(); @@ -2259,10 +2913,10 @@ mod tests { ); let mut dk = dnskey(&ks); dk.sort(); - assert_eq!(dk, ["first CSK", "second KSK", "second ZSK"]); + assert_eq!(dk, ["first CSK", "third KSK", "third ZSK"]); let mut dks = dnskey_sigs(&ks); dks.sort(); - assert_eq!(dks, ["first CSK", "second KSK"]); + assert_eq!(dks, ["first CSK", "third KSK"]); assert_eq!(zone_sigs(&ks), ["first CSK"]); assert_eq!(ds_keys(&ks), ["first CSK"]); @@ -2284,8 +2938,8 @@ mod tests { let actions = ks.roll_done(RollType::CskRoll).unwrap(); assert_eq!(actions, []); - ks.delete_key("second KSK").unwrap(); - ks.delete_key("second ZSK").unwrap(); + ks.delete_key("third KSK").unwrap(); + ks.delete_key("third ZSK").unwrap(); ks.add_key_csk( "second CSK".to_string(), @@ -2293,6 +2947,7 @@ mod tests { SecurityAlgorithm::ECDSAP256SHA256, 4, UnixTime::now(), + true, ) .unwrap(); @@ -2422,4 +3077,4 @@ mod tests { } vec } -} +} \ No newline at end of file From e1aa2d4a6f011d75ea0957a081cdf3de2155aaa8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:11:23 +0200 Subject: [PATCH 496/569] Move some log statements from debug level to trace level. --- src/crypto/kmip.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 4f4c1b44d..94da70a9b 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -402,6 +402,7 @@ pub mod sign { use kmip::types::response::{ CreateKeyPairResponsePayload, ResponsePayload, }; + use log::trace; use openssl::ecdsa::EcdsaSig; use tracing::{debug, error}; use url::Url; @@ -644,12 +645,11 @@ pub mod sign { unreachable!(); }; - debug!("Algorithm: {}", self.algorithm); - debug!( - "Signature Data: {}", + trace!( + "Algorithm: {}, Signature Data: {}", + self.algorithm, base16::encode_display(&signed.signature_data) ); - match (self.algorithm, signed.signature_data.len()) { (SecurityAlgorithm::RSASHA256, _) => Ok(Signature::RsaSha256( signed.signature_data.into_boxed_slice(), From 5a644e3f372e7adef25584ba298724f24ad6fd1c Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:34:21 +0200 Subject: [PATCH 497/569] Fix test that fails to compile. --- src/dnssec/sign/signatures/rrsigs.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 0afce6618..79dbd45d1 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -1232,6 +1232,10 @@ mod tests { fn sign_raw(&self, _data: &[u8]) -> Result { Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) } + + fn flags(&self) -> u16 { + todo!() + } } impl Default for TestKey { From f549b77d4297dff678cf82c3f2d984e167610df9 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:38:02 +0200 Subject: [PATCH 498/569] Rename KmipConnPool to remove redundant Kmip and to make it clear this is a sync implementation, not async. --- src/crypto/kmip.rs | 18 +++++++++--------- src/crypto/kmip_pool.rs | 12 +++++++----- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 94da70a9b..c13621aa3 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -20,7 +20,7 @@ pub use kmip::client::{ClientCertificate, ConnectionSettings}; use crate::{ base::iana::SecurityAlgorithm, - crypto::{common::rsa_encode, kmip_pool::KmipConnPool}, + crypto::{common::rsa_encode, kmip_pool::SyncConnPool}, rdata::Dnskey, utils::base16, }; @@ -92,14 +92,14 @@ pub struct PublicKey { public_key_id: String, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, } impl PublicKey { pub fn new( public_key_id: String, algorithm: SecurityAlgorithm, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, ) -> Self { Self { public_key_id, @@ -410,7 +410,7 @@ pub mod sign { use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; use crate::crypto::kmip::{GenerateError, PublicKey}; - use crate::crypto::kmip_pool::KmipConnPool; + use crate::crypto::kmip_pool::SyncConnPool; use crate::crypto::sign::{ GenerateParams, SignError, SignRaw, Signature, }; @@ -432,7 +432,7 @@ pub mod sign { public_key_id: String, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, dnskey: Dnskey>, @@ -445,7 +445,7 @@ pub mod sign { flags: u16, private_key_id: &str, public_key_id: &str, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, ) -> Result { let dnskey = PublicKey::new( public_key_id.to_string(), @@ -468,7 +468,7 @@ pub mod sign { pub fn new_from_urls( priv_key_url: KeyUrl, pub_key_url: KeyUrl, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, ) -> Result { if priv_key_url.algorithm() != pub_key_url.algorithm() { return Err(GenerateError::Kmip(format!("Private and public key URLs have different algorithms: {} vs {}", priv_key_url.algorithm(), pub_key_url.algorithm()).into())); @@ -543,7 +543,7 @@ pub mod sign { Ok(url) } - pub fn conn_pool(&self) -> &KmipConnPool { + pub fn conn_pool(&self) -> &SyncConnPool { &self.conn_pool } } @@ -934,7 +934,7 @@ pub mod sign { name: String, params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? flags: u16, - conn_pool: KmipConnPool, + conn_pool: SyncConnPool, ) -> Result { let algorithm = params.algorithm(); diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 9758f6c80..7fe67d1fc 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -74,21 +74,23 @@ pub type KmipTlsClient = Client>; /// This pool can be used to acquire a KMIP client without first having to /// wait for it to connect at the TCP/TLS level, and without unnecessarily /// closing the connection when finished. +// TODO: Move this to the kmip-protocol crate and add an AsyncConnPool variant +// implemented using the bb8 crate instead of the r2d2 crate. #[derive(Clone, Debug)] -pub struct KmipConnPool { +pub struct SyncConnPool { server_id: String, conn_settings: Arc, pool: r2d2::Pool, } -impl KmipConnPool { +impl SyncConnPool { pub fn new( server_id: String, conn_settings: Arc, max_conncurrent_connections: u32, max_life_time: Option, max_idle_time: Option, - ) -> Result { + ) -> Result { let pool = r2d2::Pool::builder() // Don't pre-create idle connections to the KMIP server .min_idle(Some(0)) @@ -165,8 +167,8 @@ impl ConnectionManager { max_conncurrent_connections: u32, max_life_time: Option, max_idle_time: Option, - ) -> Result { - KmipConnPool::new( + ) -> Result { + SyncConnPool::new( server_id, conn_settings, max_conncurrent_connections, From 578129a1fd961ae8d82e2f3719a57b210dbfecf5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:24:14 +0200 Subject: [PATCH 499/569] Add KMIP batching support. Modifies the signing APIs to allow preparation of the bytes to sign to be done separately to incorporating the generated signature bytes into R1RSIG records. Without this there is no way to batch KMIP signing requests as calls to SignRaw::sign_raw() cannot continue without the resulting signature bytes which will not be available until later when the rest of the batch has been collected and submitted for signing. --- Cargo.lock | 3 +- Cargo.toml | 3 +- src/crypto/kmip.rs | 196 ++++++++++++++++---------- src/crypto/kmip_pool.rs | 2 +- src/crypto/sign.rs | 4 +- src/dnssec/sign/keys/keyset.rs | 2 +- src/dnssec/sign/signatures/rrsigs.rs | 201 ++++++++++++++++++++------- 7 files changed, 285 insertions(+), 126 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 656beeed5..d8404ecd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,7 @@ dependencies = [ "tracing", "tracing-subscriber", "url", + "uuid", "webpki-roots 0.26.11", ] @@ -746,7 +747,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#2454c6bd29ff3c7e711724e67d50f374ec0c249b" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#d3c323caf199c982218a573ee7f5ab43075d0c27" dependencies = [ "cfg-if", "enum-display-derive", diff --git a/Cargo.toml b/Cargo.toml index f8130c148..8d650fbb2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ tokio-stream = { version = "0.1.1", optional = true } tracing = { version = "0.1.40", optional = true, features = ["log"] } tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-filter"] } url = { version = "2.5.4", optional = true } +uuid = { version = "1.17.0", optional = true, features = ["v4"] } [features] default = ["std", "rand"] @@ -71,7 +72,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url"] +kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url", "dep:uuid"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index c13621aa3..7fb7161ae 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -1,10 +1,6 @@ #![cfg(feature = "kmip")] #![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] -//============ Error Types =================================================== - -//----------- GenerateError -------------------------------------------------- - use core::fmt; use std::{string::String, vec::Vec}; @@ -16,8 +12,6 @@ use kmip::types::{ }; use tracing::{debug, error}; -pub use kmip::client::{ClientCertificate, ConnectionSettings}; - use crate::{ base::iana::SecurityAlgorithm, crypto::{common::rsa_encode, kmip_pool::SyncConnPool}, @@ -25,6 +19,12 @@ use crate::{ utils::base16, }; +pub use kmip::client::{ClientCertificate, ConnectionSettings}; + +//============ Error Types =================================================== + +//----------- GenerateError -------------------------------------------------- + /// An error in generating a key pair with OpenSSL. #[derive(Clone, Debug)] pub enum GenerateError { @@ -393,11 +393,12 @@ pub mod sign { use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, DigitalSignatureAlgorithm, - HashingAlgorithm, PaddingMethod, UniqueIdentifier, + HashingAlgorithm, PaddingMethod, UniqueBatchItemID, UniqueIdentifier, }; use kmip::types::request::{ - self, CommonTemplateAttribute, PrivateKeyTemplateAttribute, - PublicKeyTemplateAttribute, RequestPayload, + self, BatchItem, CommonTemplateAttribute, + PrivateKeyTemplateAttribute, PublicKeyTemplateAttribute, + RequestPayload, }; use kmip::types::response::{ CreateKeyPairResponsePayload, ResponsePayload, @@ -406,6 +407,7 @@ pub mod sign { use openssl::ecdsa::EcdsaSig; use tracing::{debug, error}; use url::Url; + use uuid::Uuid; use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; @@ -439,6 +441,8 @@ pub mod sign { flags: u16, } + //--- Constructors + impl KeyPair { pub fn new( algorithm: SecurityAlgorithm, @@ -488,7 +492,11 @@ pub mod sign { ) } } + } + + //--- Accessors + impl KeyPair { pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } @@ -521,6 +529,65 @@ pub mod sign { self.mk_key_url(&self.private_key_id) } + pub fn conn_pool(&self) -> &SyncConnPool { + &self.conn_pool + } + } + + //--- Operations + + impl KeyPair { + pub fn sign_raw_enqueue( + &self, + queue: &mut SignQueue, + data: &[u8], + ) -> Result, SignError> { + let request = self.sign_pre(data)?; + let operation = request.operation(); + let batch_item_id = + UniqueBatchItemID(Uuid::new_v4().into_bytes().to_vec()); + let batch_item = + BatchItem(operation, Some(batch_item_id), request); + queue.0.push(batch_item); + Ok(None) + } + + pub fn sign_raw_submit_queue( + &self, + queue: &mut SignQueue, + ) -> Result, SignError> { + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + format!("Error while obtaining KMIP pool connection: {err}") + })?; + + // Drain the queue. + let q_size = queue.0.capacity(); + let mut empty = Vec::with_capacity(q_size); + std::mem::swap(&mut queue.0, &mut empty); + let queue = empty; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_requests(queue).map_err(|err| { + format!("Error while sending KMIP request: {err}") + })?; + + let mut sigs = Vec::with_capacity(q_size); + for res in res { + let res = res?; + let sig = self.sign_post(res.payload.unwrap())?; + sigs.push(sig); + } + + Ok(sigs) + } + } + + //--- Internal details + + impl KeyPair { fn mk_key_url(&self, key_id: &str) -> Result { // We have to store the algorithm in the URL because the DNSSEC // algorithm (e.g. 5 and 7) don't necessarily correspond to the @@ -543,51 +610,7 @@ pub mod sign { Ok(url) } - pub fn conn_pool(&self) -> &SyncConnPool { - &self.conn_pool - } - } - - impl SignRaw for KeyPair { - fn algorithm(&self) -> SecurityAlgorithm { - self.algorithm - } - - fn flags(&self) -> u16 { - self.flags - } - - fn dnskey(&self) -> Result>, SignError> { - Ok(self.dnskey.clone()) - } - - fn sign_raw(&self, data: &[u8]) -> Result { - // https://www.rfc-editor.org/rfc/rfc5702.html#section-3 - // 3. RRSIG Resource Records - // "The value of the signature field in the RRSIG RR follows the - // RSASSA- PKCS1-v1_5 signature scheme and is calculated as - // follows." - // ... - // hash = SHA-XXX(data) - // - // Here XXX is either 256 or 512, depending on the algorithm used, as - // specified in FIPS PUB 180-3; "data" is the wire format data of the - // resource record set that is signed, as specified in [RFC4034]. - // - // signature = ( 00 | 01 | FF* | 00 | prefix | hash ) ** e (mod n)" - // ... - // - // 3.1. RSA/SHA-256 RRSIG Resource Records - // "RSA/SHA-256 signatures are stored in the DNS using RRSIG resource - // records (RRs) with algorithm number 8. - // - // The prefix is the ASN.1 DER SHA-256 algorithm designator prefix, as - // specified in PKCS #1 v2.1 [RFC3447]: - // - // hex 30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20" - // - // KMIP HSMs implement the Sign opration operation according to - // these rules. + fn sign_pre(&self, data: &[u8]) -> Result { let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm { SecurityAlgorithm::RSASHA256 => ( @@ -607,39 +630,26 @@ pub mod sign { .into()) } }; - - // PyKMIP requires that the padding method be specified otherwise - // it complains with: "For signing, a padding method must be - // specified." let mut cryptographic_parameters = CryptographicParameters::default() .with_hashing_algorithm(hashing_alg) .with_cryptographic_algorithm(crypto_alg); - if self.algorithm == SecurityAlgorithm::RSASHA256 { cryptographic_parameters = cryptographic_parameters .with_padding_method(PaddingMethod::PKCS1_v1_5); } - - // TODO: We could optionally add a KMIP Message Extension to the - // request via which we signal support for domain format response - // data, so that the Nameshed HSM Relay doesn't have to convert - // from PKCS#11 format to the format needed by domain. let request = RequestPayload::Sign( Some(UniqueIdentifier(self.private_key_id.clone())), Some(cryptographic_parameters), Data(data.as_ref().to_vec()), ); + Ok(request) + } - // Execute the request and capture the response - let client = self.conn_pool.get().map_err(|err| { - format!("Error while obtaining KMIP pool connection: {err}") - })?; - - let res = client.do_request(request).map_err(|err| { - format!("Error while sending KMIP request: {err}") - })?; - + fn sign_post( + &self, + res: ResponsePayload, + ) -> Result { tracing::trace!("Checking sign payload"); let ResponsePayload::Sign(signed) = res else { unreachable!(); @@ -715,6 +725,46 @@ pub mod sign { } } + pub struct SignQueue(Vec); + + impl SignQueue { + pub fn new() -> Self { + Self(vec![]) + } + } + + impl SignRaw for KeyPair { + fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + fn flags(&self) -> u16 { + self.flags + } + + fn dnskey(&self) -> Result>, SignError> { + Ok(self.dnskey.clone()) + } + + fn sign_raw(&self, data: &[u8]) -> Result { + let request = self.sign_pre(data)?; + + // Execute the request and capture the response. + let client = self.conn_pool.get().map_err(|err| { + format!("Error while obtaining KMIP pool connection: {err}") + })?; + + // This will block which could be problematic if executed from an + // async task handler thread as it will block execution of other + // tasks while waiting for the remote KMIP server to respond. + let res = client.do_request(request).map_err(|err| { + format!("Error while sending KMIP request: {err}") + })?; + + self.sign_post(res) + } + } + /// A URL that represents a key stored in a KMIP compatible HSM. /// /// The URL structure is: diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs index 7fe67d1fc..aef209975 100644 --- a/src/crypto/kmip_pool.rs +++ b/src/crypto/kmip_pool.rs @@ -173,7 +173,7 @@ impl ConnectionManager { conn_settings, max_conncurrent_connections, max_life_time, - max_idle_time + max_idle_time, ) } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 5ed6fe17e..4796fd338 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -1074,13 +1074,13 @@ impl std::error::Error for SignError {} impl From for SignError { fn from(err: String) -> Self { - Self(err) + SignError(err) } } impl From<&'static str> for SignError { fn from(err: &'static str) -> Self { - Self(err.to_string()) + SignError(err.to_string()) } } diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 8ea928759..5fbe52930 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -3077,4 +3077,4 @@ mod tests { } vec } -} \ No newline at end of file +} diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 79dbd45d1..fa9813be6 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -3,6 +3,7 @@ use core::convert::{AsRef, From}; use core::fmt::Display; use core::marker::Send; +use core::slice; use std::boxed::Box; use std::cmp::Ordering; use std::fmt::Debug; @@ -13,17 +14,17 @@ use octseq::{OctetsFrom, OctetsInto}; use tracing::debug; use crate::base::cmp::CanonicalOrd; -use crate::base::iana::Rtype; +use crate::base::iana::{Class, Rtype}; use crate::base::name::ToName; use crate::base::rdata::{ComposeRecordData, RecordData}; use crate::base::record::Record; -use crate::base::Name; -use crate::crypto::sign::SignRaw; +use crate::base::{Name, Ttl}; +use crate::crypto::sign::{SignRaw, Signature}; use crate::dnssec::sign::error::SigningError; use crate::dnssec::sign::keys::signingkey::SigningKey; use crate::dnssec::sign::records::{RecordsIter, Rrset}; use crate::rdata::dnssec::{ProtoRrsig, Timestamp}; -use crate::rdata::{Rrsig, ZoneRecordData}; +use crate::rdata::Rrsig; //------------ GenerateRrsigConfig ------------------------------------------- @@ -95,9 +96,9 @@ impl GenerateRrsigConfig { /// [RFC 9364]: https://www.rfc-editor.org/rfc/rfc9364 // TODO: Add mutable iterator based variant. #[allow(clippy::type_complexity)] -pub fn sign_sorted_zone_records( +pub fn sign_sorted_zone_records( apex_owner: &N, - mut records: RecordsIter<'_, N, ZoneRecordData>, + records: RecordsIter<'_, N, D>, keys: &[&SigningKey], config: &GenerateRrsigConfig, ) -> Result>>, SigningError> @@ -119,6 +120,55 @@ where + Clone + FromBuilder + From<&'static [u8]>, + D: CanonicalOrd + ComposeRecordData, +{ + sign_sorted_zone_records_with( + apex_owner, + records, + keys, + config, + sign_sorted_rrset_in::, + ) +} + +#[allow(clippy::type_complexity)] +pub fn sign_sorted_zone_records_with<'a, 'b, N, Octs, D, Inner, O, F>( + apex_owner: &N, + mut records: RecordsIter<'b, N, D>, + keys: &[&'a SigningKey], + config: &GenerateRrsigConfig, + signer_fn: F, +) -> Result, SigningError> +where + Inner: Debug + SignRaw, + N: ToName + + PartialEq + + Clone + + Debug + + Display + + Send + + CanonicalOrd + + From>, + Octs: AsRef<[u8]> + + Debug + + From> + + Send + + OctetsFrom> + + Clone + + FromBuilder + + From<&'static [u8]>, + D: RecordData, + F: Fn( + &'a SigningKey, + Rtype, + Class, + N, + Ttl, + slice::Iter<'b, Record>, + Timestamp, + Timestamp, + &mut Vec, + ) -> Result, { // The generated collection of RRSIG RRs that will be returned to the // caller. @@ -190,9 +240,13 @@ where for key in keys { let inception = config.inception; let expiration = config.expiration; - let rrsig_rr = sign_sorted_rrset_in( + let rrsig_rr = signer_fn( key, - &rrset, + rrset.rtype(), + rrset.class(), + rrset.owner().clone(), + rrset.ttl(), + rrset.iter(), inception, expiration, &mut reusable_scratch, @@ -225,17 +279,17 @@ where /// and re-use it across multiple calls. /// /// This function will sort the RRset in canonical ordering prior to signing. -pub fn sign_rrset( +pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, inception: Timestamp, expiration: Timestamp, ) -> Result>, SigningError> where - N: ToName + Debug + Clone + From>, - D: Clone + Debug + RecordData + ComposeRecordData + CanonicalOrd, + N: ToName + Debug + Clone + From> + CanonicalOrd, Inner: Debug + SignRaw, Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData + Clone, { let mut records = rrset.as_slice().to_vec(); records @@ -243,7 +297,17 @@ where let rrset = Rrset::new(&records) .expect("records is not empty so new should not fail"); - sign_sorted_rrset_in(key, &rrset, inception, expiration, &mut vec![]) + sign_sorted_rrset_in( + key, + rrset.rtype(), + rrset.class(), + rrset.owner().clone(), + rrset.ttl(), + rrset.iter(), + inception, + expiration, + &mut vec![], + ) } /// Generate `RRSIG` records for a given RRset. @@ -269,24 +333,88 @@ where /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 /// [RFC 6840 section 5.11]: /// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 -pub fn sign_sorted_rrset_in( - key: &SigningKey, - rrset: &Rrset<'_, N, D>, +pub fn sign_sorted_rrset_in<'a, 'b, N, Octs, D, Inner>( + key: &'a SigningKey, + rrset_rtype: Rtype, + rrset_class: Class, + rrset_owner: N, + rrset_ttl: Ttl, + rrset_iter: slice::Iter<'b, Record>, inception: Timestamp, expiration: Timestamp, scratch: &mut Vec, ) -> Result>, SigningError> where N: ToName + Clone + Debug + From>, - D: RecordData + Debug + ComposeRecordData + CanonicalOrd, Inner: Debug + SignRaw, Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData, +{ + let rrsig = sign_sorted_rrset_in_pre( + key, + rrset_rtype, + rrset_owner.rrsig_label_count(), + rrset_ttl, + rrset_iter, + inception, + expiration, + scratch, + )?; + let signature = key.raw_secret_key().sign_raw(&*scratch)?; + signature_to_record(signature, rrsig, rrset_owner, rrset_class, rrset_ttl) +} + +pub fn signature_to_record( + signature: Signature, + rrsig: ProtoRrsig, + rrset_owner: N, + rrset_class: Class, + rrset_ttl: Ttl, +) -> Result>, SigningError> +where + N: ToName + Clone + Debug + From>, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, +{ + let signature = signature.as_ref().to_vec(); + let Ok(signature) = signature.try_octets_into() else { + return Err(SigningError::OutOfMemory); + }; + + let rrsig = rrsig.into_rrsig(signature).expect("long signature"); + + // RFC 4034 + // 3.1.3. The Labels Field + // ... + // "The value of the Labels field MUST be less than or equal to the + // number of labels in the RRSIG owner name." + debug_assert!( + (rrsig.labels() as usize) < rrset_owner.iter_labels().count() + ); + + Ok(Record::new(rrset_owner, rrset_class, rrset_ttl, rrsig)) +} + +pub fn sign_sorted_rrset_in_pre( + key: &SigningKey, + rrset_rtype: Rtype, + rrset_owner_rrsig_label_count: u8, + rrset_ttl: Ttl, + rrset_iter: slice::Iter>, + inception: Timestamp, + expiration: Timestamp, + scratch: &mut Vec, +) -> Result, SigningError> +where + N: ToName + Clone + Debug + From>, + Inner: Debug + SignRaw, + Octs: AsRef<[u8]> + Clone + Debug + OctetsFrom>, + D: CanonicalOrd + ComposeRecordData, { // RFC 4035 // 2.2. Including RRSIG RRs in a Zone // ... // "An RRSIG RR itself MUST NOT be signed" - if rrset.rtype() == Rtype::RRSIG { + if rrset_rtype == Rtype::RRSIG { return Err(SigningError::RrsigRrsMustNotBeSigned); } @@ -304,10 +432,10 @@ where // the same owner name will have different TTL values if the RRsets // they cover have different TTL values." let rrsig = ProtoRrsig::new( - rrset.rtype(), + rrset_rtype, key.algorithm(), - rrset.owner().rrsig_label_count(), - rrset.ttl(), + rrset_owner_rrsig_label_count, + rrset_ttl, expiration, inception, key.dnskey()?.key_tag(), @@ -329,32 +457,11 @@ where scratch.clear(); rrsig.compose_canonical(scratch).unwrap(); - for record in rrset.iter() { + for record in rrset_iter { record.compose_canonical(scratch).unwrap(); } - let signature = key.raw_secret_key().sign_raw(&*scratch)?; - let signature = signature.as_ref().to_vec(); - let Ok(signature) = signature.try_octets_into() else { - return Err(SigningError::OutOfMemory); - }; - - let rrsig = rrsig.into_rrsig(signature).expect("long signature"); - - // RFC 4034 - // 3.1.3. The Labels Field - // ... - // "The value of the Labels field MUST be less than or equal to the - // number of labels in the RRSIG owner name." - debug_assert!( - (rrsig.labels() as usize) < rrset.owner().iter_labels().count() - ); - Ok(Record::new( - rrset.owner().clone(), - rrset.class(), - rrset.ttl(), - rrsig, - )) + Ok(rrsig) } #[cfg(test)] @@ -1218,6 +1325,10 @@ mod tests { SecurityAlgorithm::ED25519 } + fn flags(&self) -> u16 { + todo!() + } + fn dnskey(&self) -> Result>, SignError> { let flags = 0; Ok(Dnskey::new( @@ -1232,10 +1343,6 @@ mod tests { fn sign_raw(&self, _data: &[u8]) -> Result { Ok(Signature::Ed25519(TEST_SIGNATURE_RAW.into())) } - - fn flags(&self) -> u16 { - todo!() - } } impl Default for TestKey { From c6aeafdc15f8bce7a08e2a94ee6a0af0e46ba911 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 25 Jul 2025 11:40:10 +0200 Subject: [PATCH 500/569] Undo accidental re-ordering of sign_rrset() generics. --- src/dnssec/sign/signatures/rrsigs.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index fa9813be6..717d33d8b 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -279,7 +279,7 @@ where /// and re-use it across multiple calls. /// /// This function will sort the RRset in canonical ordering prior to signing. -pub fn sign_rrset( +pub fn sign_rrset( key: &SigningKey, rrset: &Rrset<'_, N, D>, inception: Timestamp, From b500eebaba7f8a119f13de0bcc37044fe2f3b9a3 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Tue, 29 Jul 2025 11:27:44 +0200 Subject: [PATCH 501/569] Derive clone for KeySet, add 'Z' to UnixTime display, derive Copy for RollType, replace values_mut with values in many places. Set published in a roll, add WaitDnskeyPropagated to a roll. --- src/dnssec/sign/keys/keyset.rs | 45 ++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 5fbe52930..727992eca 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -85,7 +85,7 @@ use std::time::{SystemTime, SystemTimeError, UNIX_EPOCH}; /// /// The state of this type can be serialized and deserialized. The state /// includes the state of any key rollovers going on. -#[derive(Deserialize, Serialize)] +#[derive(Clone, Deserialize, Serialize)] pub struct KeySet { name: Name>, keys: HashMap, @@ -273,7 +273,7 @@ impl KeySet { let next_state = RollState::Propagation1; rolltype.rollfn()(RollOp::Start(old, new), self)?; - self.rollstates.insert(rolltype.clone(), next_state.clone()); + self.rollstates.insert(rolltype, next_state.clone()); Ok(rolltype.roll_actions_fn()(next_state)) } @@ -297,7 +297,7 @@ impl KeySet { let next_state = RollState::CacheExpire1(ttl); rolltype.rollfn()(RollOp::Propagation1, self)?; - self.rollstates.insert(rolltype.clone(), next_state.clone()); + self.rollstates.insert(rolltype, next_state.clone()); Ok(rolltype.roll_actions_fn()(next_state)) } @@ -320,7 +320,7 @@ impl KeySet { }; let next_state = RollState::Propagation2; rolltype.rollfn()(RollOp::CacheExpire1(*ttl), self)?; - self.rollstates.insert(rolltype.clone(), next_state.clone()); + self.rollstates.insert(rolltype, next_state.clone()); Ok(rolltype.roll_actions_fn()(next_state)) } @@ -343,7 +343,7 @@ impl KeySet { }; let next_state = RollState::CacheExpire2(ttl); rolltype.rollfn()(RollOp::Propagation2, self)?; - self.rollstates.insert(rolltype.clone(), next_state.clone()); + self.rollstates.insert(rolltype, next_state.clone()); Ok(rolltype.roll_actions_fn()(next_state)) } @@ -365,7 +365,7 @@ impl KeySet { }; let next_state = RollState::Done; rolltype.rollfn()(RollOp::CacheExpire2(*ttl), self)?; - self.rollstates.insert(rolltype.clone(), next_state.clone()); + self.rollstates.insert(rolltype, next_state.clone()); Ok(rolltype.roll_actions_fn()(next_state)) } @@ -1218,7 +1218,7 @@ impl Display for UnixTime { ) .expect("bad time value"); let format = format_description::parse( - "[year]-[month]-[day]T[hour]:[minute]:[second]", + "[year]-[month]-[day]T[hour]:[minute]:[second]Z", ) .expect(""); write!(f, "{}", dt.format(&format).expect("")) @@ -1329,7 +1329,7 @@ pub enum Action { } /// The type of key roll to perform. -#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum RollType { /// A KSK roll. This implements the Double-Signature ZSK Roll as described /// in Section 4.1.2 of RFC 6781. @@ -1498,7 +1498,7 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; @@ -1545,7 +1545,7 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; @@ -1623,7 +1623,7 @@ fn ksk_double_ds_roll( } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; @@ -1644,7 +1644,8 @@ fn ksk_double_ds_roll( } // Old keys are no longer present and signing, new keys will - // be present and signing. + // be present and signing. Set published. + let now = UnixTime::now(); for k in &mut ks.keys.values_mut() { if let KeyType::Ksk(ref mut keystate) = k.keytype { if keystate.old && keystate.present { @@ -1655,6 +1656,7 @@ fn ksk_double_ds_roll( if !keystate.old && keystate.at_parent { keystate.present = true; keystate.signer = true; + k.timestamps.published = Some(now.clone()); } } } @@ -1674,7 +1676,7 @@ fn ksk_double_ds_roll( } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; @@ -1727,6 +1729,7 @@ fn ksk_roll_actions(rollstate: RollState) -> Vec { RollState::Done => { actions.push(Action::RemoveCdsRrset); actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::WaitDnskeyPropagated); } } actions @@ -1793,7 +1796,7 @@ fn zsk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Zsk(ref keystate) = k.keytype else { continue; }; @@ -1842,7 +1845,7 @@ fn zsk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Zsk(ref keystate) = k.keytype else { continue; }; @@ -1919,7 +1922,7 @@ fn zsk_double_signature_roll( } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Zsk(ref keystate) = k.keytype else { continue; }; @@ -1964,7 +1967,7 @@ fn zsk_double_signature_roll( } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let KeyType::Zsk(ref keystate) = k.keytype else { continue; }; @@ -2071,7 +2074,7 @@ fn csk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Ksk(keystate) | KeyType::Zsk(keystate) @@ -2172,7 +2175,7 @@ fn csk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Zsk(keystate) | KeyType::Csk(_, keystate) => { keystate @@ -2301,7 +2304,7 @@ fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Ksk(keystate) | KeyType::Zsk(keystate) @@ -2358,7 +2361,7 @@ fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { keystate From 89cd2570de0717170d4df2da0a05ef901ff1df1a Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 30 Jul 2025 14:54:22 +0200 Subject: [PATCH 502/569] Add WaitDnskeyPropagated to CskRoll. --- src/dnssec/sign/keys/keyset.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 727992eca..9901273ac 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -2249,6 +2249,7 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { RollState::Done => { actions.push(Action::RemoveCdsRrset); actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::WaitDnskeyPropagated); } } actions From ab3017cf73d523649c620a016985e204c6091757 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Thu, 31 Jul 2025 17:12:08 +0200 Subject: [PATCH 503/569] Add stale function. Fix tests (WaitDnskeyPropagated) was missing. --- src/dnssec/sign/keys/keyset.rs | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 9901273ac..4354efb06 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -1067,6 +1067,11 @@ impl KeyState { pub fn at_parent(&self) -> bool { self.at_parent } + + /// Return whether this key is no long in use. + pub fn stale(&self) -> bool { + self.old && !self.signer && !self.present && !self.at_parent + } } impl Display for KeyState { @@ -2765,7 +2770,11 @@ mod tests { let actions = ks.cache_expired2(RollType::KskRoll).unwrap(); assert_eq!( actions, - [Action::RemoveCdsRrset, Action::UpdateDnskeyRrset] + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::WaitDnskeyPropagated + ] ); let mut dk = dnskey(&ks); dk.sort(); @@ -2933,7 +2942,11 @@ mod tests { let actions = ks.cache_expired2(RollType::CskRoll).unwrap(); assert_eq!( actions, - [Action::RemoveCdsRrset, Action::UpdateDnskeyRrset] + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::WaitDnskeyPropagated + ] ); assert_eq!(dnskey(&ks), ["first CSK"]); assert_eq!(dnskey_sigs(&ks), ["first CSK"]); @@ -3007,7 +3020,11 @@ mod tests { let actions = ks.cache_expired2(RollType::CskRoll).unwrap(); assert_eq!( actions, - [Action::RemoveCdsRrset, Action::UpdateDnskeyRrset] + [ + Action::RemoveCdsRrset, + Action::UpdateDnskeyRrset, + Action::WaitDnskeyPropagated + ] ); assert_eq!(dnskey(&ks), ["second CSK"]); assert_eq!(dnskey_sigs(&ks), ["second CSK"]); From 0302857bf4a6f96a2fe645c32ce26d2a9c49cc71 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 1 Aug 2025 16:28:24 +0200 Subject: [PATCH 504/569] Use stale() and small fixes. --- src/dnssec/sign/keys/keyset.rs | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 4354efb06..1ea8f5012 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -211,27 +211,15 @@ impl KeySet { KeyType::Ksk(keystate) | KeyType::Zsk(keystate) | KeyType::Include(keystate) => { - if !keystate.old - || keystate.signer - || keystate.present - || keystate.at_parent - { + if !keystate.stale() { return Err(Error::KeyNotOld); } } KeyType::Csk(ksk_keystate, zsk_keystate) => { - if !ksk_keystate.old - || ksk_keystate.signer - || ksk_keystate.present - || ksk_keystate.at_parent - { + if !ksk_keystate.stale() { return Err(Error::KeyNotOld); } - if !zsk_keystate.old - || zsk_keystate.signer - || zsk_keystate.present - || zsk_keystate.at_parent - { + if !zsk_keystate.stale() { return Err(Error::KeyNotOld); } } @@ -1098,7 +1086,7 @@ impl Display for KeyState { (false, true, true) => write!(f, " (Active)")?, (true, true, true) => write!(f, " (Leaving)")?, (true, false, true) => write!(f, " (Retired)")?, - (true, false, false) => write!(f, " (Old)")?, + (true, false, false) => write!(f, " (Stale)")?, (_, _, _) => (), } Ok(()) @@ -1386,6 +1374,7 @@ impl RollType { } } +#[derive(Debug)] enum RollOp<'a> { Start(&'a [&'a str], &'a [&'a str]), Propagation1, @@ -1507,7 +1496,7 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.present { + if keystate.stale() { continue; } @@ -1542,7 +1531,7 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.present { + if keystate.old || !keystate.at_parent { continue; } @@ -1554,7 +1543,7 @@ fn ksk_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.present { + if keystate.stale() { continue; } @@ -1632,7 +1621,7 @@ fn ksk_double_ds_roll( let KeyType::Ksk(ref keystate) = k.keytype else { continue; }; - if keystate.old || !keystate.present { + if keystate.old || !keystate.at_parent { continue; } From 77a5cf8492d80ade6499fd535a94a79acd24d3c5 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Aug 2025 09:10:56 +0200 Subject: [PATCH 505/569] Add missing dependending on OpenSSL currently needed by the KMIP support. --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8d650fbb2..c3d449f10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -72,7 +72,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url", "dep:uuid"] +kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url", "dep:uuid", "dep:openssl"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] From 48b9b6b1f0b4278fb92c9b6f840bdba10eab134a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 6 Aug 2025 23:23:05 +0200 Subject: [PATCH 506/569] Review feedback: Move public key lookup into crypto::kmip::PublicKey::new() to allow dnskey() to be infallible. --- src/crypto/kmip.rs | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 7fb7161ae..da6ffcfa9 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -93,6 +93,8 @@ pub struct PublicKey { public_key_id: String, conn_pool: SyncConnPool, + + public_key: Vec, } impl PublicKey { @@ -100,22 +102,32 @@ impl PublicKey { public_key_id: String, algorithm: SecurityAlgorithm, conn_pool: SyncConnPool, - ) -> Self { - Self { + ) -> Result { + let public_key = Self::fetch_public_key(&public_key_id, &conn_pool)?; + + Ok(Self { public_key_id, algorithm, conn_pool, - } + public_key, + }) } pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } - pub fn dnskey( - &self, - flags: u16, - ) -> Result>, kmip::client::Error> { + pub fn dnskey(&self, flags: u16) -> Dnskey> { + Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()) + .unwrap() + } +} + +impl PublicKey { + fn fetch_public_key( + public_key_id: &str, + conn_pool: &SyncConnPool, + ) -> Result, kmip::client::Error> { // https://datatracker.ietf.org/doc/html/rfc5702#section-2 // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource // Records for DNSSEC @@ -159,7 +171,7 @@ impl PublicKey { // including the exponent. Leading zero octets are prohibited in // the exponent and modulus. - let client = self.conn_pool.get().inspect_err(|err| error!("{err}")).map_err(|err| { + let client = conn_pool.get().inspect_err(|err| error!("{err}")).map_err(|err| { kmip::client::Error::ServerError(format!( "Error while attempting to acquire KMIP connection from pool: {err}" )) @@ -170,7 +182,7 @@ impl PublicKey { // CKA_TOKEN false) in which case there is no public key and so it // uses the private key object handle instead. let res = client - .get_key(&self.public_key_id) + .get_key(public_key_id) .inspect_err(|err| error!("{err}"))?; let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { @@ -378,7 +390,7 @@ impl PublicKey { mat => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported key material type: {mat}"))), }; - Ok(Dnskey::new(flags, 3, algorithm, octets).unwrap()) + Ok(octets) } } @@ -456,8 +468,8 @@ pub mod sign { algorithm, conn_pool.clone(), ) - .dnskey(flags) - .map_err(|err| GenerateError::Kmip(err.to_string()))?; + .map_err(|err| GenerateError::Kmip(err.to_string()))? + .dnskey(flags); Ok(Self { algorithm, @@ -509,14 +521,6 @@ pub mod sign { &self.public_key_id } - pub fn public_key(&self) -> PublicKey { - PublicKey::new( - self.public_key_id.clone(), - self.algorithm, - self.conn_pool.clone(), - ) - } - pub fn flags(&self) -> u16 { self.flags } From 8c58b3602171b41ecd528ee378e0debb5ceb834a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:34:10 +0200 Subject: [PATCH 507/569] Fix regression in examples/serve-zone.rs where the TSIG middleware was wrongly moved higher in the middleware chain while it MUST be the last middleware as the TSIG OPT record MUST be the last OPT record in the response. This was accidentally changed in commit 45ade90 to this branch, it is correct in main. --- examples/serve-zone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index 109676e24..d7de67fbc 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -124,10 +124,10 @@ async fn main() { 1, ); let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); - let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); let svc = EdnsMiddlewareSvc::, _, _>::new(svc); let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); + let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); From 96426e6c5b0ef264a7d83792f2871aee28a4d1af Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:49:13 +0200 Subject: [PATCH 508/569] Apply fixes from PR #564. --- examples/serve-zone.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index d7de67fbc..f1a7b8c70 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -126,8 +126,8 @@ async fn main() { let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); let svc = EdnsMiddlewareSvc::, _, _>::new(svc); - let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); + let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); @@ -342,7 +342,7 @@ impl XfrDataProvider> for ZoneTreeWithDiffs { Octs: Octets + Send + Sync, { if req.metadata().is_none() { - eprintln!("Rejecting"); + eprintln!("Rejecting request due to missing TSIG key"); return Box::pin(ready(Err(XfrDataProviderError::Refused))); } let res = req From a5934515edff5fa81b9b7fff3b223d3909a3af99 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:51:17 +0200 Subject: [PATCH 509/569] Upgrade to latest kmip crate version. To gain connection pool support. --- Cargo.lock | 5 +- src/crypto/kmip_pool.rs | 253 ---------------------------------------- 2 files changed, 3 insertions(+), 255 deletions(-) delete mode 100644 src/crypto/kmip_pool.rs diff --git a/Cargo.lock b/Cargo.lock index d8404ecd2..c450e291b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -747,7 +747,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#d3c323caf199c982218a573ee7f5ab43075d0c27" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#cde038ea7dd8947309af7f733e8c09ec62fa655f" dependencies = [ "cfg-if", "enum-display-derive", @@ -757,6 +757,7 @@ dependencies = [ "log", "maybe-async", "openssl", + "r2d2", "rustc_version", "serde", "serde_bytes", @@ -768,7 +769,7 @@ dependencies = [ [[package]] name = "kmip-ttlv" version = "0.4.0" -source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#ffe005638e150db0c1f1435a607f292d82b634b1" +source = "git+https://github.com/NLnetLabs/kmip-ttlv?branch=next#4ca144e19e69375a6ccd63cf40b0e61f89462f97" dependencies = [ "cfg-if", "hex", diff --git a/src/crypto/kmip_pool.rs b/src/crypto/kmip_pool.rs deleted file mode 100644 index aef209975..000000000 --- a/src/crypto/kmip_pool.rs +++ /dev/null @@ -1,253 +0,0 @@ -#![cfg(feature = "kmip")] -#![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] - -//! KMIP TLS connection pool -//! -//! Used to: -//! - Avoid repeated TCP connection setup and TLS session establishment -//! for mutiple KMIP requests made close together in time. -//! - Handle loss of connectivity by re-creating the connection when an -//! existing connection is considered to be "broken" at the network -//! level. -use core::fmt::Display; -use core::ops::Deref; -use std::net::TcpStream; -use std::string::String; -use std::{sync::Arc, time::Duration}; - -use kmip::client::{Client, ConnectionSettings}; -// TODO: Remove the hard-coded use of OpenSSL? -use openssl::ssl::SslStream; -use r2d2::PooledConnection; - -//------------ KmipConnError ------------------------------------------------- - -#[derive(Clone, Debug)] -pub struct KmipConnError(String); - -impl From for KmipConnError { - fn from(err: r2d2::Error) -> Self { - Self(format!("{err}")) - } -} - -impl Display for KmipConnError { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - write!(f, "{}", self.0) - } -} - -//------------ KmipConn ------------------------------------------------------ - -/// A KMIP connection pool connection. -pub struct KmipConn { - conn: PooledConnection, -} - -impl KmipConn { - fn new(conn: PooledConnection) -> Self { - Self { conn } - } -} - -impl Deref for KmipConn { - type Target = KmipTlsClient; - - fn deref(&self) -> &Self::Target { - self.conn.deref() - } -} - -/// A KMIP client used to send KMIP requests and receive KMIP responses. -/// -/// Note: Currently depends on OpenSSL because our KMIP implementation -/// supports elements of KMIP 1.2 [1] but not KMIP 1.3 [2], but prior to KMIP -/// 1.3 it is required for servers to support TLS 1.0, and RustLS doesn't -/// support TLS < 1.2. -/// -/// [1]: https://docs.oasis-open.org/kmip/profiles/v1.2/os/kmip-profiles-v1.2-os.html#_Toc409613167 -/// [2]: https://docs.oasis-open.org/kmip/profiles/v1.3/os/kmip-profiles-v1.3-os.html#_Toc473103053 -pub type KmipTlsClient = Client>; - -/// A pool of already connected KMIP clients. -/// -/// This pool can be used to acquire a KMIP client without first having to -/// wait for it to connect at the TCP/TLS level, and without unnecessarily -/// closing the connection when finished. -// TODO: Move this to the kmip-protocol crate and add an AsyncConnPool variant -// implemented using the bb8 crate instead of the r2d2 crate. -#[derive(Clone, Debug)] -pub struct SyncConnPool { - server_id: String, - conn_settings: Arc, - pool: r2d2::Pool, -} - -impl SyncConnPool { - pub fn new( - server_id: String, - conn_settings: Arc, - max_conncurrent_connections: u32, - max_life_time: Option, - max_idle_time: Option, - ) -> Result { - let pool = r2d2::Pool::builder() - // Don't pre-create idle connections to the KMIP server - .min_idle(Some(0)) - // Create at most this many concurrent connections to the KMIP - // server - .max_size(max_conncurrent_connections) - // Don't verify that a connection is usable when fetching it from - // the pool (as doing so requires sending a request to the server - // and we might as well just try the actual request that we want - // the connection for) - .test_on_check_out(false) - // Don't use the default logging behaviour as `[ERROR] [r2d2] - // Server error: ...` is a bit confusing for end users who - // shouldn't know or care that we use the r2d2 crate. - // .error_handler(Box::new(ErrorLoggingHandler)) - // Don't keep using the same connection for longer than around N - // minutes (unless in use in which case it will wait until the - // connection is returned to the pool before closing it) - maybe - // long held connections would run into problems with some - // firewalls. - .max_lifetime(max_life_time) - // Don't keep connections open that were not used in the last N - // minutes. - .idle_timeout(max_idle_time) - // Don't wait longer than N seconds for a new connection to be - // established, instead try again to connect. - .connection_timeout( - conn_settings - .connect_timeout - .unwrap_or(Duration::from_secs(30)), - ) - // Use our connection manager to create connections in the pool - // and to verify their health - .build(ConnectionManager { - conn_settings: conn_settings.clone(), - })?; - - Ok(Self { - server_id, - conn_settings, - pool, - }) - } - - pub fn server_id(&self) -> &str { - &self.server_id - } - - pub fn conn_settings(&self) -> &ConnectionSettings { - &self.conn_settings - } - - pub fn get(&self) -> Result { - Ok(KmipConn::new(self.pool.get()?)) - } -} - -/// Manages KMIP TCP + TLS connection creation. -/// -/// Uses the [r2d2] crate to manage a pool of connections. -/// -/// [r2d2]: https://crates.io/crates/r2d2/ -#[derive(Debug)] -pub struct ConnectionManager { - conn_settings: Arc, -} - -impl ConnectionManager { - /// Create a pool of up-to N TCP + TLS connections to the KMIP server. - #[rustfmt::skip] - pub fn create_connection_pool( - server_id: String, - conn_settings: Arc, - max_conncurrent_connections: u32, - max_life_time: Option, - max_idle_time: Option, - ) -> Result { - SyncConnPool::new( - server_id, - conn_settings, - max_conncurrent_connections, - max_life_time, - max_idle_time, - ) - } - - /// Connect using the given connection settings to a KMIP server. - /// - /// This function creates a new connection to the server. The connection - /// is NOT taken from the connection pool. - pub fn connect_one_off( - settings: &ConnectionSettings, - ) -> kmip::client::Result { - let conn = kmip::client::tls::openssl::connect(settings)?; - Ok(conn) - } -} - -impl r2d2::ManageConnection for ConnectionManager { - type Connection = KmipTlsClient; - - type Error = kmip::client::Error; - - /// Establishes a KMIP server connection which will be added to the - /// connection pool. - fn connect(&self) -> Result { - Self::connect_one_off(&self.conn_settings) - } - - /// This function is never used because the [r2d2] `test_on_check_out` - /// flag is set to false when the connection pool is created. - /// - /// [r2d2]: https://crates.io/crates/r2d2/ - fn is_valid( - &self, - _conn: &mut Self::Connection, - ) -> Result<(), Self::Error> { - unreachable!() - } - - /// Quickly verify if an existing connection is broken. - /// - /// Used to discard and re-create connections that encounter multiple - /// connection related errors. - fn has_broken(&self, conn: &mut Self::Connection) -> bool { - conn.connection_error_count() > 1 - } -} - -// /// A Krill specific [r2d2] error logging handler. -// /// -// /// Logs connection pool related connection error messages using the format -// /// `"[] Pool error: ..."` instead of -// /// the default [r2d2] `"[ERROR] [r2d2] Server error: ..."` format. Assumes -// /// that the logging framework will include the logging module context in the -// /// logged message, i.e. `xxx::kmip::xxx` and thus we don't need to mention -// /// KMIP in the logged message content. -// /// -// /// Rationale: -// /// - The use of the [r2d2] crate is an internal detail which of no use to -// /// end users consulting the logs and which we may change at any time. -// /// - Krill should be the one to determine the appropriate level to log a -// /// connection issue at, not [r2d2]. -// #[derive(Debug)] -// struct ErrorLoggingHandler; - -// impl r2d2::HandleError for ErrorLoggingHandler -// where -// E: std::fmt::Display, -// { -// fn handle_error(&self, err: E) { -// warn!("Pool error: {}", err) -// } -// } - -// impl From for SignerError { -// fn from(err: r2d2::Error) -> Self { -// SignerError::KmipError(format!("{}", err)) -// } -// } From c1124de1c7cb6ad75e6d1e4e87ad0e3e1782f715 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:52:15 +0200 Subject: [PATCH 510/569] Re-export kmip dependency. To make life easier for library users who also need to use kmip crate features directly, beyond those that we use ourselves. --- src/dep.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/dep.rs b/src/dep.rs index db6c635b1..c5bc76cd6 100644 --- a/src/dep.rs +++ b/src/dep.rs @@ -1,3 +1,6 @@ //! Re-exports of dependencies pub use octseq; + +#[cfg(feature = "kmip")] +pub use kmip; From b9a89f3ada34efb8a4ea079a7c2b73ace63190e2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:52:39 +0200 Subject: [PATCH 511/569] Remove own KMIP connection pool, use the one from the kmip crate instead. --- src/crypto/kmip.rs | 19 ++++++++++--------- src/crypto/mod.rs | 1 - 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index da6ffcfa9..ccd191b3d 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -1,4 +1,4 @@ -#![cfg(feature = "kmip")] +#![cfg(all(feature = "kmip", any(feature = "ring", feature = "openssl")))] #![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] use core::fmt; @@ -6,16 +6,17 @@ use core::fmt; use std::{string::String, vec::Vec}; use bcder::{decode::SliceSource, BitString, ConstOid, Oid}; -use kmip::types::{ - common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, - response::ManagedObject, +use kmip::{ + client::pool::SyncConnPool, + types::{ + common::{KeyFormatType, KeyMaterial, TransparentRSAPublicKey}, + response::ManagedObject, + }, }; use tracing::{debug, error}; use crate::{ - base::iana::SecurityAlgorithm, - crypto::{common::rsa_encode, kmip_pool::SyncConnPool}, - rdata::Dnskey, + base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, utils::base16, }; @@ -402,6 +403,7 @@ pub mod sign { use std::time::SystemTime; use std::vec::Vec; + use kmip::client::pool::SyncConnPool; use kmip::types::common::{ CryptographicAlgorithm, CryptographicParameters, CryptographicUsageMask, Data, DigitalSignatureAlgorithm, @@ -424,7 +426,6 @@ pub mod sign { use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; use crate::crypto::kmip::{GenerateError, PublicKey}; - use crate::crypto::kmip_pool::SyncConnPool; use crate::crypto::sign::{ GenerateParams, SignError, SignRaw, Signature, }; @@ -1248,10 +1249,10 @@ mod tests { use std::time::SystemTime; use std::vec::Vec; + use kmip::client::pool::ConnectionManager; use kmip::client::ConnectionSettings; use crate::crypto::kmip::sign::generate; - use crate::crypto::kmip_pool::ConnectionManager; use crate::crypto::sign::SignRaw; use crate::logging::init_logging; diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index aa39e070f..0eee46174 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -138,7 +138,6 @@ pub mod common; pub mod kmip; -pub mod kmip_pool; pub mod openssl; pub mod ring; pub mod sign; From 01aa449eae5fb33ed1e9241718870811e038c8c2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 12:55:02 +0200 Subject: [PATCH 512/569] Remove no-longer needed direct dependency on the r2d2 crate. --- Cargo.lock | 1 - Cargo.toml | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c450e291b..e187f487a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -279,7 +279,6 @@ dependencies = [ "parking_lot", "pretty_assertions", "proc-macro2", - "r2d2", "rand", "ring", "rstest", diff --git a/Cargo.toml b/Cargo.toml index c3d449f10..fa2a8e348 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,6 @@ parking_lot = { version = "0.12", optional = true } moka = { version = "0.12.3", optional = true, features = ["future"] } openssl = { version = "0.10.72", optional = true } # 0.10.70 upgrades to 'bitflags' 2.x proc-macro2 = { version = "1.0.69", optional = true } # Force proc-macro2 to at least 1.0.69 for minimal-version build -r2d2 = { version = "0.8.9", optional = true } ring = { version = "0.17.2", optional = true } bcder = { version = "0.7", optional = true } rustversion = { version = "1", optional = true } @@ -72,7 +71,7 @@ tracing = ["dep:log", "dep:tracing"] # Cryptographic backends ring = ["dep:ring"] openssl = ["dep:openssl"] -kmip = ["dep:kmip", "dep:r2d2", "dep:bcder", "dep:url", "dep:uuid", "dep:openssl"] +kmip = ["dep:kmip", "dep:bcder", "dep:url", "dep:uuid", "dep:openssl"] # Crate features net = ["bytes", "futures-util", "rand", "std", "tokio"] From 5ca30222e3890b8dbb2111ba91b588147870b79e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 13:37:07 +0200 Subject: [PATCH 513/569] Bump kmip-protocol crate. --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index e187f487a..0790d6543 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -746,7 +746,7 @@ dependencies = [ [[package]] name = "kmip-protocol" version = "0.5.0" -source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#cde038ea7dd8947309af7f733e8c09ec62fa655f" +source = "git+https://github.com/NLnetLabs/kmip-protocol?branch=next#36960114c4e4170c219add8c861c69610219c60d" dependencies = [ "cfg-if", "enum-display-derive", From 9d95a7297b4281db90a901f0d6b4be6c882a488e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:03:19 +0200 Subject: [PATCH 514/569] Rename of signature_to_record() to sign_sorted_rrset_in_post(). --- src/dnssec/sign/signatures/rrsigs.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 717d33d8b..5984609c4 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -361,10 +361,16 @@ where scratch, )?; let signature = key.raw_secret_key().sign_raw(&*scratch)?; - signature_to_record(signature, rrsig, rrset_owner, rrset_class, rrset_ttl) + sign_sorted_rrset_in_post( + signature, + rrsig, + rrset_owner, + rrset_class, + rrset_ttl, + ) } -pub fn signature_to_record( +pub fn sign_sorted_rrset_in_post( signature: Signature, rrsig: ProtoRrsig, rrset_owner: N, From 61aef920ddea4f59d5e1d77524adf6d81b299721 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 15 Aug 2025 11:23:49 +0200 Subject: [PATCH 515/569] Sync with changes made to branch poc-kmip-crypto-impl. --- src/crypto/kmip.rs | 985 +++++++++++++++------------ src/crypto/mod.rs | 24 +- src/crypto/openssl.rs | 12 +- src/crypto/ring.rs | 14 +- src/crypto/sign.rs | 29 +- src/dnssec/sign/keys/signingkey.rs | 4 +- src/dnssec/sign/signatures/rrsigs.rs | 57 +- src/dnssec/sign/test_util/mod.rs | 8 +- 8 files changed, 616 insertions(+), 517 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index ccd191b3d..19858030b 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -1,9 +1,13 @@ +//! DNSSEC signing using OASIS KMIP (Key Management Interoperability Protocol). #![cfg(all(feature = "kmip", any(feature = "ring", feature = "openssl")))] #![cfg_attr(docsrs, doc(cfg(feature = "kmip")))] -use core::fmt; +use core::{fmt, str::FromStr}; -use std::{string::String, vec::Vec}; +use std::{ + string::{String, ToString}, + vec::Vec, +}; use bcder::{decode::SliceSource, BitString, ConstOid, Oid}; use kmip::{ @@ -14,62 +18,18 @@ use kmip::{ }, }; use tracing::{debug, error}; +use url::Url; use crate::{ - base::iana::SecurityAlgorithm, crypto::common::rsa_encode, rdata::Dnskey, + base::iana::SecurityAlgorithm, + crypto::{common::rsa_encode, sign::SignError}, + rdata::Dnskey, utils::base16, }; pub use kmip::client::{ClientCertificate, ConnectionSettings}; -//============ Error Types =================================================== - -//----------- GenerateError -------------------------------------------------- - -/// An error in generating a key pair with OpenSSL. -#[derive(Clone, Debug)] -pub enum GenerateError { - /// The requested algorithm is not supported. - UnsupportedAlgorithm(SecurityAlgorithm), - - // The requested key size for the given algorithm is not supported. - UnsupportedKeySize { - algorithm: SecurityAlgorithm, - min: u32, - max: u32, - requested: u32, - }, - - /// A problem occurred while communicating with the KMIP server. - Kmip(String), -} - -//--- Formatting - -impl fmt::Display for GenerateError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::UnsupportedAlgorithm(algorithm) => { - write!(f, "algorithm {algorithm} not supported") - } - Self::UnsupportedKeySize { - algorithm, - min, - max, - requested, - } => { - write!(f, "key size {requested} for algorithm {algorithm} must be in the range {min}..={max}") - } - Self::Kmip(err) => { - write!(f, "a problem occurred while communicating with the KMIP server: {err}") - } - } - } -} - -//--- Error - -impl std::error::Error for GenerateError {} +//------------ Constants ----------------------------------------------------- /// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` /// @@ -88,54 +48,270 @@ pub const EC_PUBLIC_KEY_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 2, 1]); /// Identifies the P-256 curve for elliptic curve cryptography. pub const SECP256R1_OID: ConstOid = Oid(&[42, 134, 72, 206, 61, 3, 1, 7]); -pub struct PublicKey { +//------------ KeyUrl -------------------------------------------------------- + +/// A URL that represents a key stored in a KMIP compatible HSM. +/// +/// The URL structure is: +/// +/// kmip:///keys/?algorithm=&flags= +/// +/// The algorithm and flags must be stored in the URL because they are DNSSEC +/// specific and not properties of the key itself and thus not known to or +/// stored by the HSM. +/// +/// While algorithm may seem to be something known to and stored by the HSM, +/// DNSSEC complicates that by aliasing multiple algorithm numbers to the +/// same cryptographic algorithm, and we need to know when using the key which +/// _DNSSEC_ algorithm number to use. +/// +/// The server_id could be the actual address of the target, but does not have +/// to be. There are multiple for this: +/// +/// - In a highly available clustered deployment across multiple subnets +/// it could be that the clustered HSM is available to the clustered +/// application via different names/IP addresses in different subnets of +/// the deployment. Using an abstract server_id which is mapped via local +/// configuration in the subnet to the correct hostname/FQDN/IP address +/// for that subnet allows the correct target address to be determined at +/// the point of access. +/// - Using the actual hostname/FQDN/IP address may make it confusing for +/// an operator trying to understand where the key is actually stored. +/// This can happen for example if the product name for the HSM is say +/// Fortanix DSM, while the domain name used to access the HSM might be +/// eu.smartkey.io, which having no mention of the name Fortanix in the +/// FQDN is not immediately obvious that it has any relationship with +/// Fortanix. +/// - If the same HSM is used for different use cases via use of HSM +/// partitions, referring to the HSM by its address may not make it clear +/// which partition is being used, so using a more meaningful name like +/// 'testing' or such could make it clearer where the key is actually +/// being stored. +/// - Storing the username and password in the key URL will cause many +/// copies of those credentials to be stored, one per key, which is harder +/// to secure than if they are only in a single location and looked up on +/// actual access. +/// - Storing the username and password in the key URL would cause the URL +/// to become unusable if the credentials were rotated even though the +/// location at which the key is stored has not changed. +/// - Even if the FQDN, port number, username and password are all correct, +/// there may need to be more settings specified in order to connect to +/// the HSM some of which would not fit easily into a URL such as TLS +/// client certficate details and whether or not to require the server +/// TLS certificate to be valid (which can be inconvenient in test setups +/// using self-signed certificates). +/// +/// Thus an abstract server_id is stored in the key URL and it is the +/// responsibility of the user of the key URL to map the server id to the full +/// set of settings required to successfully connect to the HSM to make use of +/// the key. +pub struct KeyUrl { + /// The original URL from which this KeyUrl was parsed. + url: Url, + + /// The KMIP server ID. Produced by the application. + server_id: String, + + /// The KMIP key ID. Produced by the KMIP server. + key_id: String, + + /// The DNSSEC algorithm this key is to be used for. algorithm: SecurityAlgorithm, - public_key_id: String, + /// The DNSSEC flags that apply to this key. + flags: u16, +} - conn_pool: SyncConnPool, +//--- Accessors +impl KeyUrl { + /// The KMIP server ID. + pub fn server_id(&self) -> &str { + &self.server_id + } + + /// The KMIP key ID. + pub fn key_id(&self) -> &str { + &self.key_id + } + + /// The DNSSEC algorithm identifier for the key. + pub fn algorithm(&self) -> SecurityAlgorithm { + self.algorithm + } + + /// The DNSSEC flags for the key. + pub fn flags(&self) -> u16 { + self.flags + } +} + +//--- impl Into + +// Disablow the Clippy lint as it is safe to go from a KeyURL to a URL but +// not vice-versa, so we implement Into but not From. +#[allow(clippy::from_over_into)] +impl Into for KeyUrl { + fn into(self) -> Url { + self.url + } +} + +//--- impl Deref + +impl std::ops::Deref for KeyUrl { + type Target = Url; + + fn deref(&self) -> &Self::Target { + &self.url + } +} + +//--- Conversions + +impl TryFrom for KeyUrl { + type Error = String; + + fn try_from(url: Url) -> Result { + let server_id = url + .host_str() + .ok_or(format!("Key URL lacks hostname component: {url}"))? + .to_string(); + + let url_path = url.path().to_string(); + let key_id = url_path + .strip_prefix("/keys/") + .ok_or(format!("Key URL lacks /keys/ path component: {url}"))?; + + let key_id = key_id.to_string(); + let mut flags = None; + let mut algorithm = None; + for (k, v) in url.query_pairs() { + match &*k { + "flags" => { + flags = Some(v.parse::().map_err(|err| { + format!("Key URL flags value is invalid: {err}") + })?) + } + "algorithm" => { + algorithm = Some( + SecurityAlgorithm::from_str(&v).map_err(|err| { + format!( + "Key URL algorithm value is invalid: {err}" + ) + })?, + ) + } + unknown => Err(format!( + "Key URL contains unknown query parameter: {unknown}" + ))?, + } + } + let algorithm = algorithm.ok_or(format!( + "Key URL lacks algorithm query parameter: {url}" + ))?; + let flags = flags + .ok_or(format!("Key URL lacks flags query parameter: {url}"))?; + + Ok(Self { + url, + server_id, + key_id, + algorithm, + flags, + }) + } +} + +//------------ PublicKey ----------------------------------------------------- + +/// A public key for verifying a signature. +pub struct PublicKey { + /// The DNSSEC algorithm for use with this public key. + algorithm: SecurityAlgorithm, + + /// The public key octets. public_key: Vec, } impl PublicKey { - pub fn new( - public_key_id: String, + /// Create a public key from a key stored on a KMIP server. + /// + /// The public key details will be retrieved from the KMIP server. + /// + /// The DNSSEC algorithm is needed in order for [`Self::dnskey()`] to + /// generate a [`Dnskey`] and must match the cryptographic algorithm of + /// the key stored on the KMIP server. + /// + /// Note: This function will block while awaiting the response from the + /// KMIP server. + /// + /// If the KMIP operation fails an error or the response cannot be parsed + /// an error will be returned. + /// + /// If the cryptographic algorithm of the retrieved key does not match + /// the given DNSSEC algorithm an error will be returned. + pub fn for_key_id_and_dnssec_algorithm( + public_key_id: &str, algorithm: SecurityAlgorithm, conn_pool: SyncConnPool, - ) -> Result { - let public_key = Self::fetch_public_key(&public_key_id, &conn_pool)?; + ) -> Result { + let public_key = + Self::fetch_public_key(public_key_id, algorithm, &conn_pool)?; Ok(Self { - public_key_id, algorithm, - conn_pool, public_key, }) } + /// Create a public key from a key stored on a KMIP server. + /// + /// This is a thin wrapper around + /// [`Self::for_key_id_and_dnssec_algorithm`]. + pub fn for_key_url( + public_key_url: KeyUrl, + conn_pool: SyncConnPool, + ) -> Result { + Self::for_key_id_and_dnssec_algorithm( + public_key_url.key_id(), + public_key_url.algorithm(), + conn_pool, + ) + } + + /// The DNSSEC algorithm of the key. pub fn algorithm(&self) -> SecurityAlgorithm { self.algorithm } + /// Generate a DNSKEY RR or this public key. pub fn dnskey(&self, flags: u16) -> Dnskey> { + // SAFETY: The key came from a KMIP server and was validated to have + // the expected length when the KMIP server response was parsed by + // fetch_public_key(). Dnskey::new(flags, 3, self.algorithm, self.public_key.clone()) .unwrap() } } impl PublicKey { + /// Query the KMIP server for the bytes of the specified public key. + /// + /// Verifies that the cryptographic algorithm of the key is compatible + /// with the specified DNSSEC algorithm. fn fetch_public_key( public_key_id: &str, + expected_algorithm: SecurityAlgorithm, conn_pool: &SyncConnPool, - ) -> Result, kmip::client::Error> { + ) -> Result, PublicKeyError> { // https://datatracker.ietf.org/doc/html/rfc5702#section-2 // Use of SHA-2 Algorithms with RSA in DNSKEY and RRSIG Resource // Records for DNSSEC + // // 2. DNSKEY Resource Records - // "The format of the DNSKEY RR can be found in [RFC4034]. - // [RFC3110] describes the use of RSA/SHA-1 for DNSSEC - // signatures." + // "The format of the DNSKEY RR can be found in [RFC4034]. [RFC3110] + // describes the use of RSA/SHA-1 for DNSSEC signatures." // | // | // v @@ -144,16 +320,16 @@ impl PublicKey { // 2. The DNSKEY Resource Record // 2.1.4. The Public Key Field // "The Public Key Field holds the public key material. The - // format depends on the algorithm of the key being stored and - // is described in separate documents." + // format depends on the algorithm of the key being stored and is + // described in separate documents." // | // | // v // https://datatracker.ietf.org/doc/html/rfc3110#section-2 // RSA/SHA-1 SIGs and RSA KEYs in the Domain Name System (DNS) // 2. RSA Public KEY Resource Records - // "... The structure of the algorithm specific portion of the - // RDATA part of such RRs is as shown below. + // "... The structure of the algorithm specific portion of the RDATA + // part of such RRs is as shown below. // // Field Size // ----- ---- @@ -161,16 +337,15 @@ impl PublicKey { // exponent as specified by length field // modulus remaining space // - // For interoperability, the exponent and modulus are each limited - // to 4096 bits in length. The public key exponent is a variable - // length unsigned integer. Its length in octets is represented - // as one octet if it is in the range of 1 to 255 and by a zero - // octet followed by a two octet unsigned length if it is longer - // than 255 bytes. The public key modulus field is a - // multiprecision unsigned integer. The length of the modulus can - // be determined from the RDLENGTH and the preceding RDATA fields - // including the exponent. Leading zero octets are prohibited in - // the exponent and modulus. + // For interoperability, the exponent and modulus are each limited to + // 4096 bits in length. The public key exponent is a variable length + // unsigned integer. Its length in octets is represented as one octet + // if it is in the range of 1 to 255 and by a zero octet followed by + // a two octet unsigned length if it is longer than 255 bytes. The + // public key modulus field is a multiprecision unsigned integer. The + // length of the modulus can be determined from the RDLENGTH and the + // preceding RDATA fields including the exponent. Leading zero octets + // are prohibited in the exponent and modulus. let client = conn_pool.get().inspect_err(|err| error!("{err}")).map_err(|err| { kmip::client::Error::ServerError(format!( @@ -179,15 +354,15 @@ impl PublicKey { })?; // Note: OpenDNSSEC queries the public key ID, _unless_ it was - // configured not the public key in the HSM (by setting - // CKA_TOKEN false) in which case there is no public key and so it - // uses the private key object handle instead. + // configured not the public key in the HSM (by setting CKA_TOKEN + // false) in which case there is no public key and so it uses the + // private key object handle instead. let res = client .get_key(public_key_id) .inspect_err(|err| error!("{err}"))?; let ManagedObject::PublicKey(public_key) = res.cryptographic_object else { - return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object))); + return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object was expected to be a PublicKey but was instead: {}", res.cryptographic_object)))?; }; // https://docs.oasis-open.org/kmip/ug/v1.2/cn01/kmip-ug-v1.2-cn01.html#_Toc407027125 @@ -195,32 +370,23 @@ impl PublicKey { // and not asymmetric keys" // // As we deal in asymmetric keys (RSA, ECDSA), not symmetric keys, - // we should not encounter public_key.key_block.key_format_type == - // KeyFormatType::Raw. However, Fortanix DSM returns + // we should not encounter public_key.key_block.key_format_type + // == KeyFormatType::Raw. However, Fortanix DSM returns // KeyFormatType::Raw when fetching key data for an ECDSA public key. - // TODO: SAFETY - // TODO: We don't know that these lengths are correct, consult cryptographic_length() too? - let algorithm = - match public_key.key_block.cryptographic_algorithm.unwrap() { - kmip::types::common::CryptographicAlgorithm::RSA => { - SecurityAlgorithm::RSASHA256 - } - kmip::types::common::CryptographicAlgorithm::ECDSA => { - SecurityAlgorithm::ECDSAP256SHA256 - } - alg => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported cryptographic algorithm type: {alg}"))), - }; - let octets = match public_key.key_block.key_value.key_material { KeyMaterial::Bytes(bytes) => { debug!("Cryptographic Algorithm: {:?}", public_key.key_block.cryptographic_algorithm); + debug!("Cryptographic Length: {:?}", public_key.key_block.cryptographic_length); debug!("Key Format Type: {:?}", public_key.key_block.key_format_type); + debug!("Key Compression Type: {:?}", public_key.key_block.key_compression_type); debug!("Key bytes as hex: {}", base16::encode_display(&bytes)); - // Handle key format type PKCS1 - match (algorithm, public_key.key_block.key_format_type) { - (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) => { + match (expected_algorithm, public_key.key_block.key_format_type) { + (SecurityAlgorithm::RSASHA1, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA256, KeyFormatType::PKCS1) | + (SecurityAlgorithm::RSASHA512, KeyFormatType::PKCS1) => { // PyKMIP outputs PKCS#1 ASN.1 DER encoded RSA public // key data like so: // RSAPublicKey::=SEQUENCE{ @@ -236,14 +402,14 @@ impl PublicKey { public_exponent = Some(bcder::Unsigned::take_from(cons)?); Ok(()) }) - }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: {err}")))?; + }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse DER encoded PKCS#1 RSAPublicKey: {err}")))?; let Some(modulus) = modulus else { - return Err(kmip::client::Error::DeserializeError("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: missing modulus".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing modulus".into()))?; }; let Some(public_exponent) = public_exponent else { - return Err(kmip::client::Error::DeserializeError("Unable to parse PKCS#1 RSASHA256 SubjectPublicKeyInfo: missing public exponent".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse DER encoded PKCS#1 RSAPublicKey: missing public exponent".into()))?; }; let n = modulus.as_slice(); @@ -251,7 +417,10 @@ impl PublicKey { crate::crypto::common::rsa_encode(e, n) }, - (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) => { + (SecurityAlgorithm::RSASHA1, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA1_NSEC3_SHA1, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA256, KeyFormatType::Raw) | + (SecurityAlgorithm::RSASHA512, KeyFormatType::Raw) => { // For an RSA key Fortanix DSM supplies: (from https://asn1js.eu/) // SubjectPublicKeyInfo SEQUENCE (2 elem) // algorithm AlgorithmIdentifier SEQUENCE (2 elem) @@ -284,11 +453,11 @@ impl PublicKey { }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: {err}")))?; let Some(modulus) = modulus else { - return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing modulus".into()))?; }; let Some(public_exponent) = public_exponent else { - return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse raw RSASHA256 SubjectPublicKeyInfo: missing public exponent".into()))?; }; let n = modulus.as_slice(); @@ -340,43 +509,53 @@ impl PublicKey { }).map_err(|err| kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo: {err}")))?; let Some(bits) = bits else { - return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; }; // https://www.rfc-editor.org/rfc/rfc5480#section-2.2 - // "The subjectPublicKey from SubjectPublicKeyInfo is the ECC public key. - // ECC public keys have the following syntax: + // "The subjectPublicKey from SubjectPublicKeyInfo + // is the ECC public key. ECC public keys have the + // following syntax: // // ECPoint ::= OCTET STRING // ... - // The first octet of the OCTET STRING indicates whether the key is - // compressed or uncompressed. The uncompressed form is indicated - // by 0x04 and the compressed form is indicated by either 0x02 or - // 0x03 (see 2.3.3 in [SEC1]). The public key MUST be rejected if - // any other value is included in the first octet." + // The first octet of the OCTET STRING indicates + // whether the key is compressed or uncompressed. + // The uncompressed form is indicated by 0x04 and + // the compressed form is indicated by either 0x02 + // or 0x03 (see 2.3.3 in [SEC1]). The public key + // MUST be rejected if any other value is included + // in the first octet." let Some(octets) = bits.octet_slice() else { - return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into())); + return Err(kmip::client::Error::DeserializeError("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: missing octets".into()))?; }; - // Expect octet string to be [, <32-byte X value>, <32-byte Y value>]. + // Expect octet string to be [, + // <32-byte X value>, <32-byte Y value>]. if octets.len() != 65 { - return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", base16::encode_display(octets), octets.len()))); + return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: expected [, <32-byte X value>, <32-byte Y value>]: {} ({} bytes)", base16::encode_display(octets), octets.len())))?; } - // Note: OpenDNSSEC doesn't support the compressed form either. + // Note: OpenDNSSEC doesn't support the compressed + // form either. let compression_flag = octets[0]; if compression_flag != 0x04 { return Err(kmip::client::Error::DeserializeError(format!("Unable to parse ECDSAP256SHA256 SubjectPublicKeyInfo bit string: unknown compression flag {compression_flag:?}")))?; } - // Expect octet string to be X | Y (| denotes concatenation) where - // X and Y are each 32 bytes (because P-256 uses 256 bit values and - // 256 bits are 32 bytes). - // Skip the compression flag. + // Expect octet string to be X | Y (| denotes + // concatenation) where X and Y are each 32 bytes + // (because P-256 uses 256 bit values and 256 bits are + // 32 bytes). Skip the compression flag. octets[1..].to_vec() } - _ => todo!(), + (expected, key_format_type) => { + let alg = public_key.key_block.cryptographic_algorithm.map(|a| a.to_string()).unwrap_or("unknown algorithm".to_string()); + let len = public_key.key_block.cryptographic_length.map(|l| l.to_string()).unwrap_or("unknown length".to_string()); + let actual = format!("{alg} ({len}) as {key_format_type}"); + return Err(PublicKeyError::AlgorithmMismatch { expected, actual }); + } } } @@ -388,16 +567,18 @@ impl PublicKey { }, ) => rsa_encode(&public_exponent, &modulus), - mat => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported key material type: {mat}"))), + mat => return Err(kmip::client::Error::DeserializeError(format!("Fetched KMIP object has unsupported key material type: {mat}")))?, }; Ok(octets) } } +//============ sign ========================================================== + #[cfg(feature = "unstable-crypto-sign")] +/// Submodule for private keys and signing. pub mod sign { - use core::str::FromStr; use std::boxed::Box; use std::string::{String, ToString}; use std::time::SystemTime; @@ -425,47 +606,58 @@ pub mod sign { use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; - use crate::crypto::kmip::{GenerateError, PublicKey}; + use crate::crypto::kmip::{ + GenerateError, KeyUrl, KeyUrlParseError, PublicKey, + }; use crate::crypto::sign::{ GenerateParams, SignError, SignRaw, Signature, }; use crate::rdata::Dnskey; use crate::utils::base16; - impl From for SignError { - fn from(err: kmip::client::Error) -> Self { - err.to_string().into() - } - } + //----------- KeyPair ---------------------------------------------------- + /// A reference to a key pair stored in an [OASIS KMIP] compliant HSM + /// server. + /// + /// [OASIS KMIP]: https://www.oasis-open.org/committees/tc_home.php?wg_abbrev=kmip #[derive(Clone, Debug)] pub struct KeyPair { /// The algorithm used by the key. algorithm: SecurityAlgorithm, + /// The KMIP ID of the private key. private_key_id: String, + /// The KMIP ID of the public key. public_key_id: String, + /// The connection pool for connecting to the KMIP server. + // TODO: Should this be T that impl's a Connection trait, why should + // it know that it's a pool rather than a single connection? conn_pool: SyncConnPool, + /// Cached DNSKEY RR for the public key. dnskey: Dnskey>, + /// Flags from [`Dnskey`]. flags: u16, } //--- Constructors impl KeyPair { - pub fn new( + /// Construct a reference to a KMIP HSM held key pair using key + /// metadata. + pub fn from_metadata( algorithm: SecurityAlgorithm, flags: u16, private_key_id: &str, public_key_id: &str, conn_pool: SyncConnPool, ) -> Result { - let dnskey = PublicKey::new( - public_key_id.to_string(), + let dnskey = PublicKey::for_key_id_and_dnssec_algorithm( + public_key_id, algorithm, conn_pool.clone(), ) @@ -482,21 +674,22 @@ pub mod sign { }) } - pub fn new_from_urls( + /// Construct a reference to a KMIP HSM held key pair using key URLs. + pub fn from_urls( priv_key_url: KeyUrl, pub_key_url: KeyUrl, conn_pool: SyncConnPool, ) -> Result { if priv_key_url.algorithm() != pub_key_url.algorithm() { - return Err(GenerateError::Kmip(format!("Private and public key URLs have different algorithms: {} vs {}", priv_key_url.algorithm(), pub_key_url.algorithm()).into())); + Err(GenerateError::Kmip(format!("Private and public key URLs have different algorithms: {} vs {}", priv_key_url.algorithm(), pub_key_url.algorithm()))) } else if priv_key_url.flags() != pub_key_url.flags() { - return Err(GenerateError::Kmip(format!("Private and public key URLs have different flags: {} vs {}", priv_key_url.flags(), pub_key_url.flags()).into())); + Err(GenerateError::Kmip(format!("Private and public key URLs have different flags: {} vs {}", priv_key_url.flags(), pub_key_url.flags()))) } else if priv_key_url.server_id() != pub_key_url.server_id() { - return Err(GenerateError::Kmip(format!("Private and public key URLs have different server IDs: {} vs {}", priv_key_url.server_id(), pub_key_url.server_id()).into())); + Err(GenerateError::Kmip(format!("Private and public key URLs have different server IDs: {} vs {}", priv_key_url.server_id(), pub_key_url.server_id()))) } else if priv_key_url.server_id() != conn_pool.server_id() { - return Err(GenerateError::Kmip(format!("Key URLs have different server ID to the KMIP connection pool: {} vs {}", priv_key_url.server_id(), conn_pool.server_id()).into())); + Err(GenerateError::Kmip(format!("Key URLs have different server ID to the KMIP connection pool: {} vs {}", priv_key_url.server_id(), conn_pool.server_id()))) } else { - Self::new( + Self::from_metadata( priv_key_url.algorithm(), priv_key_url.flags(), priv_key_url.key_id(), @@ -510,30 +703,28 @@ pub mod sign { //--- Accessors impl KeyPair { - pub fn algorithm(&self) -> SecurityAlgorithm { - self.algorithm - } - + /// Get the KMIP HSM ID for the private half of this key pair. pub fn private_key_id(&self) -> &str { &self.private_key_id } + /// Get the KMIP HSM ID for the public half of this key pair. pub fn public_key_id(&self) -> &str { &self.public_key_id } - pub fn flags(&self) -> u16 { - self.flags - } - - pub fn public_key_url(&self) -> Result { - self.mk_key_url(&self.public_key_id) + /// Get a KMIP URL for the private half of this key pair. + pub fn private_key_url(&self) -> Url { + // + self.mk_key_url(&self.private_key_id).unwrap() } - pub fn private_key_url(&self) -> Result { - self.mk_key_url(&self.private_key_id) + /// Get a KMIP URL for the public half of this key pair. + pub fn public_key_url(&self) -> Url { + self.mk_key_url(&self.public_key_id).unwrap() } + /// Get a reference to the KMIP HSM connection pool for this key pair. pub fn conn_pool(&self) -> &SyncConnPool { &self.conn_pool } @@ -542,6 +733,11 @@ pub mod sign { //--- Operations impl KeyPair { + /// Enqueue a KMIP signing operation using this key pair on the given + /// data. + /// + /// Like [`SignRaw::sign_raw()`] but deferred until + /// [`KeyPair::sign_raw_submit_queue()`] is called. pub fn sign_raw_enqueue( &self, queue: &mut SignQueue, @@ -557,6 +753,13 @@ pub mod sign { Ok(None) } + /// Submit the given signing queue as a batch to the KMIP HSM. + // + // TODO: Should the queue store the KMIP connection pool reference and + // should submit() be a method on the queue? + // TODO: What happens if the same queue is used with + // sign_raw_enqueue() but with keys that are held by different KMIP + // HSMs and thus have different KMIP connection pools? pub fn sign_raw_submit_queue( &self, queue: &mut SignQueue, @@ -593,7 +796,8 @@ pub mod sign { //--- Internal details impl KeyPair { - fn mk_key_url(&self, key_id: &str) -> Result { + /// Make a KMIP URL for this key using the given KMIP ID. + fn mk_key_url(&self, key_id: &str) -> Result { // We have to store the algorithm in the URL because the DNSSEC // algorithm (e.g. 5 and 7) don't necessarily correspond to the // cryptographic algorithm of the key known to the HSM. And we @@ -608,13 +812,17 @@ pub mod sign { self.flags ); - let url = Url::parse(&url).map_err::(|err| { - format!("unable to parse {url} as URL: {err}").into() + let url = Url::parse(&url).map_err(|err| { + KeyUrlParseError(format!( + "unable to parse {url} as URL: {err}" + )) })?; Ok(url) } + /// Prepare a KMIP signing operation request to sign the given data + /// using this key pair. fn sign_pre(&self, data: &[u8]) -> Result { let (crypto_alg, hashing_alg, _digest_type) = match self.algorithm { @@ -651,6 +859,7 @@ pub mod sign { Ok(request) } + /// Process a KMIP HSM signing operation response for this key pair. fn sign_post( &self, res: ResponsePayload, @@ -695,31 +904,10 @@ pub mod sign { ))) } - (SecurityAlgorithm::ECDSAP384SHA384, 96) => { - Ok(Signature::EcdsaP384Sha384(Box::<[u8; 96]>::new( - signed - .signature_data - .try_into() - .unwrap() - ))) - } - (SecurityAlgorithm::ED25519, 64) => { - Ok(Signature::Ed25519(Box::<[u8; 64]>::new( - signed - .signature_data - .try_into() - .unwrap() - ))) - } - - (SecurityAlgorithm::ED448, 114) => { - Ok(Signature::Ed448(Box::<[u8; 114]>::new( - signed - .signature_data - .try_into() - .unwrap() - ))) - } + // TODO + //(SecurityAlgorithm::ECDSAP384SHA384, 96) => {}, + //(SecurityAlgorithm::ED25519, 64) => {}, + //(SecurityAlgorithm::ED448, 114) => {}, (alg, sig_len) => { Err(format!("KMIP signature algorithm not supported or signature length incorrect: {sig_len} byte {alg} signature (0x{})", @@ -730,9 +918,14 @@ pub mod sign { } } + //----------- SignQueue -------------------------------------------------- + + /// A queue of KMIP signing operations pending batch submission. + #[derive(Debug, Default)] pub struct SignQueue(Vec); impl SignQueue { + /// TODO pub fn new() -> Self { Self(vec![]) } @@ -747,8 +940,8 @@ pub mod sign { self.flags } - fn dnskey(&self) -> Result>, SignError> { - Ok(self.dnskey.clone()) + fn dnskey(&self) -> Dnskey> { + self.dnskey.clone() } fn sign_raw(&self, data: &[u8]) -> Result { @@ -770,224 +963,15 @@ pub mod sign { } } - /// A URL that represents a key stored in a KMIP compatible HSM. - /// - /// The URL structure is: - /// - /// kmip:///keys/?algorithm=&flags= - /// - /// The algorithm and flags must be stored in the URL because they are - /// DNSSEC specific and not properties of the key itself and thus not - /// known to or stored by the HSM. - /// - /// While algorithm may seem to be something known to and stored by the - /// HSM, DNSSEC complicates that by aliasing multiple algorithm numbers to - /// the same cryptographic algorithm, and we need to know when using the - /// key which _DNSSEC_ algorithm number to use. - /// - /// The server_id could be the actual address of the target, but does not - /// have to be. There are multiple for this: - /// - /// - In a highly available clustered deployment across multiple subnets - /// it could be that the clustered HSM is available to the clustered - /// application via different names/IP addresses in different subnets - /// of the deployment. Using an abstract server_id which is mapped via - /// local configuration in the subnet to the correct hostname/FQDN/IP - /// address for that subnet allows the correct target address to be - /// determined at the point of access. - /// - Using the actual hostname/FQDN/IP address may make it confusing - /// for an operator trying to understand where the key is actually - /// stored. This can happen for example if the product name for the - /// HSM is say Fortanix DSM, while the domain name used to access the - /// HSM might be eu.smartkey.io, which having no mention of the name - /// Fortanix in the FQDN is not immediately obvious that it has any - /// relationship with Fortanix. - /// - If the same HSM is used for different use cases via use of HSM - /// partitions, referring to the HSM by its address may not make it - /// clear which partition is being used, so using a more meaningful - /// name like 'testing' or such could make it clearer where the key is - /// actually being stored. - /// - Storing the username and password in the key URL will cause many - /// copies of those credentials to be stored, one per key, which is - /// harder to secure than if they are only in a single location and - /// looked up on actual access. - /// - Storing the username and password in the key URL would cause the - /// URL to become unusable if the credentials were rotated even though - /// the location at which the key is stored has not changed. - /// - Even if the FQDN, port number, username and password are all - /// correct, there may need to be more settings specified in order to - /// connect to the HSM some of which would not fit easily into a URL - /// such as TLS client certficate details and whether or not to - /// require the server TLS certificate to be valid (which can be - /// inconvenient in test setups using self-signed certificates). - /// - /// Thus an abstract server_id is stored in the key URL and it is the - /// responsibility of the user of the key URL to map the server id to the - /// full set of settings required to successfully connect to the HSM to - /// make use of the key. - pub struct KeyUrl { - url: Url, - server_id: String, - key_id: String, - algorithm: SecurityAlgorithm, - flags: u16, - } - - impl KeyUrl { - fn new( - url: Url, - server_id: String, - key_id: String, - algorithm: SecurityAlgorithm, - flags: u16, - ) -> Self { - Self { - url, - server_id, - key_id, - algorithm, - flags, - } - } - - pub fn new_public_key_url( - key_pair: &KeyPair, - ) -> Result { - Ok(Self { - url: Self::mk_url(key_pair, &key_pair.public_key_id)?, - server_id: key_pair.conn_pool.server_id().to_string(), - key_id: key_pair.public_key_id.clone(), - algorithm: key_pair.algorithm, - flags: key_pair.flags, - }) - } - - pub fn new_private_key_url( - key_pair: &KeyPair, - ) -> Result { - Ok(Self { - url: Self::mk_url(key_pair, &key_pair.private_key_id)?, - server_id: key_pair.conn_pool.server_id().to_string(), - key_id: key_pair.private_key_id.clone(), - algorithm: key_pair.algorithm, - flags: key_pair.flags, - }) - } - - pub fn url(&self) -> &str { - self.url.as_ref() - } - - pub fn server_id(&self) -> &str { - &self.server_id - } - - pub fn key_id(&self) -> &str { - &self.key_id - } - - pub fn algorithm(&self) -> SecurityAlgorithm { - self.algorithm - } - - pub fn flags(&self) -> u16 { - self.flags - } - - pub fn into_url(self) -> Url { - self.url - } - } - - impl KeyUrl { - fn mk_url( - key_pair: &KeyPair, - key_id: &str, - ) -> Result { - // We have to store the algorithm in the URL because the DNSSEC - // algorithm (e.g. 5 and 7) don't necessarily correspond to the - // cryptographic algorithm of the key known to the HSM. And we - // have to store the flags in the URL because these are not known - // to the HSM, they say someting about the use to which the key - // will be put of which the HSM is unaware. - let url = format!( - "kmip://{}/keys/{}?algorithm={}&flags={}", - key_pair.conn_pool().server_id(), - key_id, - key_pair.algorithm, - key_pair.flags - ); - - let url = Url::parse(&url).map_err::(|err| { - format!("unable to parse {url} as URL: {err}").into() - })?; - - Ok(url) - } - } - - impl TryFrom for KeyUrl { - type Error = SignError; - - fn try_from(url: Url) -> Result { - let server_id = url - .host_str() - .ok_or(format!("Key URL lacks hostname component: {url}"))? - .to_string(); - - let url_path = url.path().to_string(); - let key_id = url_path.strip_prefix("/keys/").ok_or(format!( - "Key URL lacks /keys/ path component: {url}" - ))?; - - let key_id = key_id.to_string(); - let mut flags = None; - let mut algorithm = None; - for (k, v) in url.query_pairs() { - match &*k { - "flags" => { - flags = Some(v.parse::().map_err(|err| { - format!("Key URL flags value is invalid: {err}") - })?) - } - "algorithm" => { - algorithm = - Some(SecurityAlgorithm::from_str(&v).map_err( - |err| { - format!( - "Key URL algorithm value is invalid: {err}" - ) - }, - )?) - } - unknown => Err(format!( - "Key URL contains unknown query parameter: {unknown}" - ))?, - } - } - let algorithm = algorithm.ok_or(format!( - "Key URL lacks algorithm query parameter: {url}" - ))?; - let flags = flags.ok_or(format!( - "Key URL lacks flags query parameter: {url}" - ))?; - - Ok(Self { - url, - server_id, - key_id, - algorithm, - flags, - }) - } - } - //----------- generate() ------------------------------------------------- - /// Generate a new secret key for the given algorithm. + /// Generate a new key pair for a given algorithm using a specified HSM. pub fn generate( name: String, - params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? + // TODO: Is this enough? Or do we need to take SecurityAlgorithm + // as input instead of GenerateParams to ensure we don't lose + // distinctions like 5 vs 7 which are both RSASHA1? + params: GenerateParams, flags: u16, conn_pool: SyncConnPool, ) -> Result { @@ -999,8 +983,10 @@ pub mod sign { // TODO: Determine this on first use of the HSM? // PyKMIP doesn't support ActivationDate. - // Fortanix DSM does support it and creates the key in an activated state but still returns a (harmless?) error: - // Server error: Operation CreateKeyPair failed: Input field `state` is not coherent with provided activation/deactivation dates + // Fortanix DSM does support it and creates the key in an activated + // state but still returns a (harmless?) error: + // Server error: Operation CreateKeyPair failed: Input field `state` + // is not coherent with provided activation/deactivation dates let activate_on_create = false; let use_cryptographic_params = false; @@ -1008,7 +994,8 @@ pub mod sign { let mut common_attrs = vec![]; let priv_key_attrs = vec![ // Krill supplies a name at creation time. Do we need to? - // Note: Fortanix DSM requires a name for at least the private key. + // Note: Fortanix DSM requires a name for at least the private + // key. request::Attribute::Name(format!("{name}_priv")), request::Attribute::CryptographicUsageMask( CryptographicUsageMask::Sign, @@ -1016,7 +1003,8 @@ pub mod sign { ]; let pub_key_attrs = vec![ // Krill supplies a name at creation time. Do we need to? - // Note: Fortanix DSM requires a name for at least the private key. + // Note: Fortanix DSM requires a name for at least the private + // key. request::Attribute::Name(format!("{name}_pub")), // Krill does verification, do we need to? ODS doesn't. // Note: PyKMIP requires a Cryptographic Usage Mask for the public @@ -1027,12 +1015,13 @@ pub mod sign { ]; // PyKMIP doesn't support CryptographicParameters so we cannot supply - // HashingAlgorithm. It also doesn't support the Hash operation. How - // do we specify SHA256 hashing? Do we have to do it ourselves + // HashingAlgorithm. It also doesn't support the Hash operation. + // How do we specify SHA256 hashing? Do we have to do it ourselves // post-signing? Can we just specify the hashing to do when invoking // the Sign operation? // Fortanix DSM also doesn't support Cryptographic Parameters: - // Server error: Operation CreateKeyPair failed: Don't have handling for attribute Cryptographic Parameters + // Server error: Operation CreateKeyPair failed: Don't have handling + // for attribute Cryptographic Parameters // PyKMIP doesn't support Attribute::ActivationDate. For HSMs that // don't support it we have to do a separate Activate operation after @@ -1043,7 +1032,8 @@ pub mod sign { GenerateParams::RsaSha256 { bits } => { // RFC 8624 3.1 DNSSEC Signing: MUST // https://docs.oasis-open.org/kmip/spec/v1.2/os/kmip-spec-v1.2-os.html#_Toc395776503 - // "For RSA, Cryptographic Length corresponds to the bit length of the Modulus" + // "For RSA, Cryptographic Length corresponds to the bit + // length of the Modulus" // https://www.rfc-editor.org/rfc/rfc5702.html#section-2.1 // 2.1. RSA/SHA-256 DNSKEY Resource Records @@ -1051,12 +1041,7 @@ pub mod sign { // RSA/SHA-256 keys MUST NOT be less than 512 bits and MUST // NOT be more than 4096 bits." if !(512..=4096).contains(&bits) { - return Err(GenerateError::UnsupportedKeySize { - algorithm: SecurityAlgorithm::RSASHA256, - min: 512, - max: 4096, - requested: bits, - }); + return Err(GenerateError::UnsupportedAlgorithm); } if use_cryptographic_params { @@ -1080,9 +1065,7 @@ pub mod sign { } } GenerateParams::RsaSha512 { .. } => { - return Err(GenerateError::UnsupportedAlgorithm( - SecurityAlgorithm::RSASHA512, - )); + return Err(GenerateError::UnsupportedAlgorithm); } GenerateParams::EcdsaP256Sha256 => { // PyKMIP doesn't support ECDSA: @@ -1116,8 +1099,8 @@ pub mod sign { // Note: PyKMIP requires a length: use 256 from P-256? // Note: Fortanix also requires a length and gives error // "missing required field `elliptic_curve` in request - // body" if cryptographic length is not specified, and a - // value of 256 works fine while a value of 255 causes + // body" if cryptographic length is not specified, and + // a value of 256 works fine while a value of 255 causes // error "Unsupported length for ECC key". When using 256 // the Fortanix UI shows the key as type EC with curve // NistP256 so that seems good. @@ -1127,15 +1110,18 @@ pub mod sign { } GenerateParams::EcdsaP384Sha384 => { // RFC 8624 3.1 DNSSEC Signing: MAY - todo!() + // TODO + return Err(GenerateError::UnsupportedAlgorithm); } GenerateParams::Ed25519 => { // RFC 8624 3.1 DNSSEC Signing: RECOMMENDED - todo!() + // TODO + return Err(GenerateError::UnsupportedAlgorithm); } GenerateParams::Ed448 => { // RFC 8624 3.1 DNSSEC Signing: MAY - todo!() + // TODO + return Err(GenerateError::UnsupportedAlgorithm); } }; @@ -1174,8 +1160,8 @@ pub mod sign { tracing::trace!("Key generation operation complete"); // Drop the KMIP client so that it will be returned to the pool and - // thus be available below when KeyPair::new() is invoked and tries - // to fetch the details needed to determine the DNSKEY RR. + // thus be available below when KeyPair::new() is invoked and tries to + // fetch the details needed to determine the DNSKEY RR. drop(client); // Process the successful response @@ -1191,7 +1177,7 @@ pub mod sign { tracing::trace!("Creating KeyPair with DNSKEY"); - let key_pair = KeyPair::new( + let key_pair = KeyPair::from_metadata( algorithm, flags, private_key_unique_identifier.as_str(), @@ -1200,7 +1186,8 @@ pub mod sign { ) .map_err(|err| GenerateError::Kmip(err.to_string()))?; - // Activate the key if not already, otherwise it cannot be used for signing. + // Activate the key if not already, otherwise it cannot be used for + // signing. if !activate_on_create { let client = conn_pool .get() @@ -1234,11 +1221,126 @@ pub mod sign { Ok(key_pair) } - //----------- TODO: destroy() -------------------------------------------- + //----------- destroy() -------------------------------------------------- // TODO } +//============ Error Types =================================================== + +//--- Conversion + +impl From for SignError { + fn from(err: kmip::client::Error) -> Self { + err.to_string().into() + } +} + +//----------- GenerateError -------------------------------------------------- + +/// An error occurred while generating a key pair with a KMIP server. +#[derive(Clone, Debug)] +pub enum GenerateError { + /// The requested algorithm is not supported. + UnsupportedAlgorithm, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for GenerateError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedAlgorithm => { + write!(f, "algorithm not supported") + } + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- impl Error + +impl std::error::Error for GenerateError {} + +//------------ KeyUrlError --------------------------------------------------- + +/// An error occurred while parsing a KMIP key URL. +#[derive(Clone, Debug)] +pub struct KeyUrlParseError(String); + +//--- Formatting + +impl fmt::Display for KeyUrlParseError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "invalid key URL: {}", self.0) + } +} + +//--- impl Error + +impl std::error::Error for KeyUrlParseError {} + +//--- Conversions + +impl From for KeyUrlParseError { + fn from(err: String) -> Self { + KeyUrlParseError(err) + } +} + +//------------ PublicKeyError ------------------------------------------------ + +/// An error occurred while retrieving a KMIP public key. +#[derive(Clone, Debug)] +pub enum PublicKeyError { + /// The cryptographic algorithm of the KMIP key does not match the + /// specified DNSSEC algorithm. + AlgorithmMismatch { + /// The DNSSEC algorithm that was expected. + expected: SecurityAlgorithm, + + /// The type of key data received from the KMIP server. + actual: String, + }, + + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for PublicKeyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AlgorithmMismatch { expected, actual } => { + write!(f, "algorithm mismatch: expected {expected} but found {actual}") + } + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- impl Error + +impl std::error::Error for PublicKeyError {} + +//--- Conversions + +impl From for PublicKeyError { + fn from(err: kmip::client::Error) -> Self { + PublicKeyError::Kmip(err.to_string()) + } +} + +//============ Testing ======================================================= + #[cfg(test)] mod tests { use core::time::Duration; @@ -1276,15 +1378,16 @@ mod tests { let mut reader = BufReader::new(file); reader.read_to_end(&mut key_bytes).unwrap(); - let mut conn_settings = ConnectionSettings::default(); - conn_settings.host = "localhost".to_string(); - conn_settings.port = 5696; - conn_settings.insecure = true; - conn_settings.client_cert = - Some(kmip::client::ClientCertificate::SeparatePem { + let conn_settings = ConnectionSettings { + host: "localhost".to_string(), + port: 5696, + insecure: true, + client_cert: Some(kmip::client::ClientCertificate::SeparatePem { cert_bytes, key_bytes: Some(key_bytes), - }); + }), + ..Default::default() + }; eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( @@ -1321,8 +1424,7 @@ mod tests { dbg!(&res); let key = res.unwrap(); - let dnskey = key.dnskey().unwrap(); - eprintln!("DNSKEY: {}", dnskey); + eprintln!("DNSKEY: {}", key.dnskey()); } #[test] @@ -1336,18 +1438,20 @@ mod tests { init_logging(); - let mut conn_settings = ConnectionSettings::default(); // conn_settings.host = "eu.smartkey.io".to_string(); // conn_settings.port = 5696; // conn_settings.username = Some(env!("FORTANIX_USER").to_string()); // conn_settings.password = Some(env!("FORTANIX_PASS").to_string()); - conn_settings.host = "127.0.0.1".to_string(); //"eu.smartkey.io".to_string(); - conn_settings.port = 5696; - conn_settings.insecure = true; // When connecting to kmip2pkcs11 - conn_settings.connect_timeout = Some(Duration::from_secs(3)); - conn_settings.read_timeout = Some(Duration::from_secs(30)); - conn_settings.write_timeout = Some(Duration::from_secs(3)); + let conn_settings = ConnectionSettings { + host: "127.0.0.1".to_string(), + port: 5696, + insecure: true, + connect_timeout: Some(Duration::from_secs(3)), + read_timeout: Some(Duration::from_secs(30)), + write_timeout: Some(Duration::from_secs(3)), + ..Default::default() + }; eprintln!("Creating pool..."); let pool = ConnectionManager::create_connection_pool( @@ -1387,8 +1491,7 @@ mod tests { // sleep(Duration::from_secs(5)); - let dnskey = key.dnskey().unwrap(); - eprintln!("DNSKEY: {}", dnskey); + eprintln!("DNSKEY: {}", key.dnskey()); client.activate_key(key.public_key_id()).unwrap(); diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 0eee46174..d3842fb37 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -5,15 +5,25 @@ //! private key operations such as generation and signing. All features of //! this module are enabled with the `unstable-crypto-sign` feature flag. //! -//! 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 (and, for RSA keys, supports -//! weaker key sizes). A +//! This crate supports OpenSSL and Ring backends for local in-memory +//! cryptography, and also an [OASIS KMIP 1.2] backend for generating, and +//! signing with, remote key pairs using compliant HSMs. +//! +//! These cryptographic backends are gated on the `openssl`, `ring` and +//! `kmip` features, respectively. They offer mostly equivalent functionality +//! but OpenSSL supports a larger set of signing algorithms than Ring (and, +//! or RSA keys, supports weaker key sizes). +//! +//! An implementation agnostic #![cfg_attr(feature = "unstable-crypto-sign", doc = "[`sign`]")] #![cfg_attr(not(feature = "unstable-crypto-sign"), doc = "`sign`")] -//! backend is provided for users that wish -//! to use either or both backends at runtime. +//! backend is provided for users that wish to use either or both of the +//! OpenSSL or Ring backends at runtime. The KMIP backend must be used +//! explicitly as it requires details of the KMIP HSM server to connect to +//! and so has a slightly different interface to that of the OpenSSL and +//! Ring backends. +//! +//! [OASIS KMIP 1.2]: https://docs.oasis-open.org/kmip/spec/v1.2/kmip-spec-v1.2.html //! //! Each backend module ( #![cfg_attr( diff --git a/src/crypto/openssl.rs b/src/crypto/openssl.rs index 84a6397aa..6251c6b5e 100644 --- a/src/crypto/openssl.rs +++ b/src/crypto/openssl.rs @@ -689,7 +689,7 @@ pub mod sign { self.flags } - fn dnskey(&self) -> Result>, SignError> { + fn dnskey(&self) -> Dnskey> { match self.algorithm { SecurityAlgorithm::RSASHA256 => { let key = self.pkey.rsa().expect("should not fail"); @@ -703,7 +703,7 @@ pub mod sign { key, self.flags, ); - Ok(public.dnskey()) + public.dnskey() } SecurityAlgorithm::ECDSAP256SHA256 | SecurityAlgorithm::ECDSAP384SHA384 => { @@ -729,7 +729,7 @@ pub mod sign { public_key, self.flags, ); - Ok(public.dnskey()) + public.dnskey() } SecurityAlgorithm::ED25519 | SecurityAlgorithm::ED448 => { let id = match self.algorithm { @@ -743,7 +743,7 @@ pub mod sign { let key = PKey::public_key_from_raw_bytes(&key, id) .expect("shoul not fail"); let public = PublicKey::NoDigest(key, self.flags); - Ok(public.dnskey()) + public.dnskey() } _ => unreachable!(), } @@ -889,7 +889,7 @@ pub mod sign { let key = super::generate(params, 256).unwrap(); let gen_key = key.to_bytes(); - let pub_key = key.dnskey().unwrap(); + let pub_key = key.dnskey(); let equiv = KeyPair::from_bytes(&gen_key, &pub_key).unwrap(); assert!(key.pkey.public_eq(&equiv.pkey)); } @@ -936,7 +936,7 @@ pub mod sign { let key = KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); - assert_eq!(key.dnskey().unwrap(), *pub_key.data()); + assert_eq!(key.dnskey(), *pub_key.data()); } } diff --git a/src/crypto/ring.rs b/src/crypto/ring.rs index 1782363c5..2e5156644 100644 --- a/src/crypto/ring.rs +++ b/src/crypto/ring.rs @@ -375,7 +375,7 @@ pub mod sign { //--- Conversion from bytes impl KeyPair { - /// Import a key pair from bytes into OpenSSL. + /// Import a key pair from bytes into Ring. pub fn from_bytes( secret: &SecretKeyBytes, public: &Dnskey, @@ -511,7 +511,7 @@ pub mod sign { } } - fn dnskey(&self) -> Result>, SignError> { + fn dnskey(&self) -> Dnskey> { match self { Self::RsaSha256 { key, flags, rng: _ } => { let components: ring::rsa::PublicKeyComponents> = @@ -521,7 +521,7 @@ pub mod sign { let public_key = signature::RsaPublicKeyComponents { n, e }; let public = PublicKey::Rsa(&signature::RSA_PKCS1_1024_8192_SHA256_FOR_LEGACY_USE_ONLY, public_key); - Ok(public.dnskey(*flags)) + public.dnskey(*flags) } Self::EcdsaP256Sha256 { key, flags, rng: _ } @@ -553,7 +553,7 @@ pub mod sign { key.to_vec(), ), ); - Ok(public.dnskey(*flags)) + public.dnskey(*flags) } Self::Ed25519(key, flags) => { let (algorithm, sec_alg) = match self { @@ -570,7 +570,7 @@ pub mod sign { key.to_vec(), ), ); - Ok(public.dnskey(*flags)) + public.dnskey(*flags) } } } @@ -731,7 +731,7 @@ pub mod sign { crate::crypto::sign::generate(params.clone(), 256) .unwrap(); let key = KeyPair::from_bytes(&sk, &pk).unwrap(); - assert_eq!(key.dnskey().unwrap(), pk); + assert_eq!(key.dnskey(), pk); } } @@ -752,7 +752,7 @@ pub mod sign { let key = KeyPair::from_bytes(&gen_key, pub_key.data()).unwrap(); - assert_eq!(key.dnskey().unwrap(), *pub_key.data()); + assert_eq!(key.dnskey(), *pub_key.data()); } } diff --git a/src/crypto/sign.rs b/src/crypto/sign.rs index 4796fd338..8ba573854 100644 --- a/src/crypto/sign.rs +++ b/src/crypto/sign.rs @@ -36,7 +36,7 @@ //! //! // Check that the owner, algorithm, and key tag matched expectations. //! assert_eq!(key_pair.algorithm(), SecurityAlgorithm::ED25519); -//! assert_eq!(key_pair.dnskey().unwrap().key_tag(), 56037); +//! assert_eq!(key_pair.dnskey().key_tag(), 56037); //! ``` //! //! # Generating keys @@ -176,6 +176,7 @@ pub trait SignRaw { /// [RFC 8624, section 3.1]: https://datatracker.ietf.org/doc/html/rfc8624#section-3.1 fn algorithm(&self) -> SecurityAlgorithm; + /// TODO fn flags(&self) -> u16; /// The public key. @@ -184,7 +185,7 @@ pub trait SignRaw { /// algorithm as returned by [`algorithm()`]. /// /// [`algorithm()`]: Self::algorithm() - fn dnskey(&self) -> Result>, SignError>; + fn dnskey(&self) -> Dnskey>; /// Sign the given bytes. /// @@ -319,7 +320,7 @@ pub enum KeyPair { /// A key backed by a KMIP capable HSM. #[cfg(feature = "kmip")] - Kmip(self::kmip::sign::KeyPair), + Kmip(kmip::sign::KeyPair), } //--- Conversion to and from bytes @@ -392,7 +393,7 @@ impl SignRaw for KeyPair { } } - fn dnskey(&self) -> Result>, SignError> { + fn dnskey(&self) -> Dnskey> { match self { #[cfg(feature = "ring")] Self::Ring(key) => key.dnskey(), @@ -438,10 +439,7 @@ pub fn generate( #[cfg(feature = "openssl")] { let key = openssl::sign::generate(params, flags)?; - return Ok(( - key.to_bytes(), - key.dnskey().map_err(|_| GenerateError::Implementation)?, - )); + return Ok((key.to_bytes(), key.dnskey())); } // Otherwise fail. @@ -970,21 +968,6 @@ impl From for GenerateError { } } -#[cfg(feature = "kmip")] -impl From for GenerateError { - fn from(value: kmip::GenerateError) -> Self { - match value { - kmip::GenerateError::UnsupportedAlgorithm(_) => { - GenerateError::UnsupportedAlgorithm - } - kmip::GenerateError::UnsupportedKeySize { .. } => { - GenerateError::UnsupportedAlgorithm - } - kmip::GenerateError::Kmip(_) => GenerateError::Implementation, - } - } -} - //--- Formatting impl fmt::Display for GenerateError { diff --git a/src/dnssec/sign/keys/signingkey.rs b/src/dnssec/sign/keys/signingkey.rs index 152bd5869..dd27fdc96 100644 --- a/src/dnssec/sign/keys/signingkey.rs +++ b/src/dnssec/sign/keys/signingkey.rs @@ -1,6 +1,6 @@ use crate::base::iana::SecurityAlgorithm; use crate::base::Name; -use crate::crypto::sign::{SignError, SignRaw}; +use crate::crypto::sign::SignRaw; use crate::rdata::Dnskey; use std::fmt::Debug; use std::vec::Vec; @@ -122,7 +122,7 @@ where self.inner.algorithm() } - pub fn dnskey(&self) -> Result>, SignError> { + pub fn dnskey(&self) -> Dnskey> { self.inner.dnskey() } } diff --git a/src/dnssec/sign/signatures/rrsigs.rs b/src/dnssec/sign/signatures/rrsigs.rs index 5984609c4..281d907ac 100644 --- a/src/dnssec/sign/signatures/rrsigs.rs +++ b/src/dnssec/sign/signatures/rrsigs.rs @@ -256,7 +256,7 @@ where "Signed {} RRSET at {} with keytag {}", rrset.rtype(), rrset.owner(), - key.dnskey()?.key_tag() + key.dnskey().key_tag() ); } } @@ -333,6 +333,7 @@ where /// https://www.rfc-editor.org/rfc/rfc4035.html#section-2.2 /// [RFC 6840 section 5.11]: /// https://www.rfc-editor.org/rfc/rfc6840.html#section-5.11 +#[allow(clippy::too_many_arguments)] pub fn sign_sorted_rrset_in<'a, 'b, N, Octs, D, Inner>( key: &'a SigningKey, rrset_rtype: Rtype, @@ -400,12 +401,13 @@ where Ok(Record::new(rrset_owner, rrset_class, rrset_ttl, rrsig)) } +#[allow(clippy::too_many_arguments)] pub fn sign_sorted_rrset_in_pre( key: &SigningKey, rrset_rtype: Rtype, rrset_owner_rrsig_label_count: u8, rrset_ttl: Ttl, - rrset_iter: slice::Iter>, + rrset_iter: slice::Iter<'_, Record>, inception: Timestamp, expiration: Timestamp, scratch: &mut Vec, @@ -444,7 +446,7 @@ where rrset_ttl, expiration, inception, - key.dnskey()?.key_tag(), + key.dnskey().key_tag(), // The fns provided by `ToName` state in their RustDoc that they // "Converts the name into a single, uncompressed name" which matches // the RFC 4034 section 3.1.7 requirement that "A sender MUST NOT use @@ -483,7 +485,7 @@ mod tests { use crate::dnssec::sign::test_util; use crate::dnssec::sign::test_util::*; use crate::rdata::dnssec::Timestamp; - use crate::rdata::Dnskey; + use crate::rdata::{Dnskey, ZoneRecordData}; use crate::zonetree::StoredName; use super::*; @@ -589,7 +591,7 @@ mod tests { let key = SigningKey::new(apex_owner.clone(), 0, TestKey::default()); let (inception, expiration) = (Timestamp::from(0), Timestamp::from(0)); - let dnskey = key.dnskey().unwrap().convert(); + let dnskey = key.dnskey().convert(); let mut records = SortedRecords::::default(); @@ -740,7 +742,8 @@ mod tests { #[test] fn generate_rrsigs_without_keys_generates_no_rrsigs() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.insert(mk_a_rr("example.")).unwrap(); let no_keys: [&SigningKey; 0] = []; @@ -773,12 +776,13 @@ mod tests { // full zone, in this case just for an A record. This test // deliberately does not include a SOA record as the zone is partial. let apex = Name::from_str(zone_apex).unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.insert(mk_a_rr(record_owner)).unwrap(); // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().unwrap().convert(); + let dnskey = keys[0].dnskey().convert(); // Generate RRSIGs. Use the default signing config and thus also the // DefaultSigningKeyUsageStrategy which will honour the purpose of the @@ -814,7 +818,8 @@ mod tests { #[test] fn generate_rrsigs_ignores_records_outside_the_zone() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_a_rr("in_zone.example."), @@ -823,7 +828,7 @@ mod tests { // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().unwrap().convert(); + let dnskey = keys[0].dnskey().convert(); let generated_records = sign_sorted_zone_records( &apex, @@ -855,7 +860,8 @@ mod tests { #[test] fn generate_rrsigs_ignores_glue_records() { let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr("example.", "mname.", "rname."), mk_ns_rr("example.", "early_sorting_glue."), @@ -867,7 +873,7 @@ mod tests { // Prepare a zone signing key and a key signing key. let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().unwrap().convert(); + let dnskey = keys[0].dnskey().convert(); let generated_records = sign_sorted_zone_records( &apex, @@ -942,7 +948,7 @@ mod tests { let dnskeys = keys .iter() - .map(|k| k.dnskey().unwrap().convert()) + .map(|k| k.dnskey().convert()) .collect::>(); let zsk = &dnskeys[zsk_idx]; @@ -1119,7 +1125,8 @@ mod tests { let apex = "example."; let apex_owner = Name::from_str(apex).unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ mk_soa_rr(apex, "some.mname.", "some.rname."), mk_ns_rr(apex, "ns.example."), @@ -1129,8 +1136,8 @@ mod tests { let keys = [&mk_dnssec_signing_key(false), &mk_dnssec_signing_key(false)]; - let zsk1 = keys[0].dnskey().unwrap().convert(); - let zsk2 = keys[1].dnskey().unwrap().convert(); + let zsk1 = keys[0].dnskey().convert(); + let zsk2 = keys[1].dnskey().convert(); let generated_records = sign_sorted_zone_records( &apex_owner, @@ -1180,10 +1187,11 @@ mod tests { fn generate_rrsigs_for_already_signed_zone() { let keys = [&mk_dnssec_signing_key(true)]; - let dnskey = keys[0].dnskey().unwrap().convert(); + let dnskey = keys[0].dnskey().convert(); let apex = Name::from_str("example.").unwrap(); - let mut records = SortedRecords::default(); + let mut records = + SortedRecords::<_, ZoneRecordData>::new(); records.extend([ // -- example. mk_soa_rr("example.", "some.mname.", "some.rname."), @@ -1303,7 +1311,7 @@ mod tests { dnskey: &Dnskey, ) -> Record where - R: From>, + R: From> + Send, { test_util::mk_rrsig_rr( name, @@ -1335,15 +1343,10 @@ mod tests { todo!() } - fn dnskey(&self) -> Result>, SignError> { + fn dnskey(&self) -> Dnskey> { let flags = 0; - Ok(Dnskey::new( - flags, - 3, - SecurityAlgorithm::ED25519, - self.0.to_vec(), - ) - .unwrap()) + Dnskey::new(flags, 3, SecurityAlgorithm::ED25519, self.0.to_vec()) + .unwrap() } fn sign_raw(&self, _data: &[u8]) -> Result { diff --git a/src/dnssec/sign/test_util/mod.rs b/src/dnssec/sign/test_util/mod.rs index b36c39d4c..17d0008ee 100644 --- a/src/dnssec/sign/test_util/mod.rs +++ b/src/dnssec/sign/test_util/mod.rs @@ -50,7 +50,7 @@ pub(crate) fn mk_record(owner: &str, data: D) -> Record { pub(crate) fn mk_a_rr(owner: &str) -> Record where - R: From, + R: From + Send, { mk_record(owner, A::from_str("1.2.3.4").unwrap().into()) } @@ -214,7 +214,7 @@ pub(crate) fn mk_rrsig_rr( signature: Bytes, ) -> Record where - R: From>, + R: From> + Send, { let signer_name = mk_name(signer_name); let expiration = Timestamp::from(expiration); @@ -243,7 +243,7 @@ pub(crate) fn mk_soa_rr( rname: &str, ) -> Record where - R: From>, + R: From> + Send, { let soa = Soa::new( mk_name(mname), @@ -254,7 +254,7 @@ where TEST_TTL, TEST_TTL, ); - mk_record(owner, soa.into()) + mk_record::(owner, soa.into()) } #[allow(clippy::type_complexity)] From c792eaf84a1f65af2440b743d694fb16b2dfd8b2 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:20:33 +0200 Subject: [PATCH 516/569] Add kmip::destroy(). --- src/crypto/kmip.rs | 53 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index ccd191b3d..179f59d0d 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -26,7 +26,7 @@ pub use kmip::client::{ClientCertificate, ConnectionSettings}; //----------- GenerateError -------------------------------------------------- -/// An error in generating a key pair with OpenSSL. +/// An error while generating a key pair using KMIP. #[derive(Clone, Debug)] pub enum GenerateError { /// The requested algorithm is not supported. @@ -71,6 +71,31 @@ impl fmt::Display for GenerateError { impl std::error::Error for GenerateError {} +//------------ DestroyError -------------------------------------------------- + +/// An error while destroying a key using KMIP. +#[derive(Clone, Debug)] +pub enum DestroyError { + /// A problem occurred while communicating with the KMIP server. + Kmip(String), +} + +//--- Formatting + +impl fmt::Display for DestroyError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Kmip(err) => { + write!(f, "a problem occurred while communicating with the KMIP server: {err}") + } + } + } +} + +//--- Error + +impl std::error::Error for DestroyError {} + /// [RFC 4055](https://tools.ietf.org/html/rfc4055) `rsaEncryption` /// /// Identifies an RSA public key with no limitation to either RSASSA-PSS or @@ -425,7 +450,7 @@ pub mod sign { use crate::base::iana::SecurityAlgorithm; use crate::crypto::common::DigestType; - use crate::crypto::kmip::{GenerateError, PublicKey}; + use crate::crypto::kmip::{DestroyError, GenerateError, PublicKey}; use crate::crypto::sign::{ GenerateParams, SignError, SignRaw, Signature, }; @@ -986,7 +1011,7 @@ pub mod sign { /// Generate a new secret key for the given algorithm. pub fn generate( - name: String, + name: String, // TODO: Should we restrict names to a compatible set? What is that set? params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? flags: u16, conn_pool: SyncConnPool, @@ -1234,9 +1259,27 @@ pub mod sign { Ok(key_pair) } - //----------- TODO: destroy() -------------------------------------------- + //----------- destroy() -------------------------------------------------- - // TODO + /// Destroy a KMIP key by ID. + /// + /// Note: A KMIP key cannot be destroyed if it is active. To deactivate + /// the key we must first "revoke" it. + fn destroy( + key_id: &str, + conn_pool: SyncConnPool, + ) -> Result<(), DestroyError> { + let client = conn_pool + .get() + .map_err(|err| DestroyError::Kmip(format!("Key destruction failed: Cannot connect to KMIP server {}: {err}", conn_pool.server_id())))?; + + client + .revoke_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string()))?; + client + .destroy_key(key_id) + .map_err(|err| DestroyError::Kmip(err.to_string())) + } } #[cfg(test)] From 418e787f275eacb62011f2cc672a551df0a72d9a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:36:00 +0200 Subject: [PATCH 517/569] Added impl Display for kmip::KeyUrl. --- src/crypto/kmip.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 52cb9798f..1d5a7a7e3 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -223,6 +223,14 @@ impl TryFrom for KeyUrl { } } +//--- impl Display + +impl std::fmt::Display for KeyUrl { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.url.fmt(f) + } +} + //------------ PublicKey ----------------------------------------------------- /// A public key for verifying a signature. From afec5d24072be1cfc39dba3b65afc7880aa22265 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 22 Aug 2025 22:16:25 +0200 Subject: [PATCH 518/569] Don't generate private and public KMIP names as that prevents final label length calculation in the caller. --- src/crypto/kmip.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 1d5a7a7e3..30f2ffd4f 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -975,7 +975,8 @@ pub mod sign { /// Generate a new key pair for a given algorithm using a specified HSM. pub fn generate( - name: String, // TODO: Should we restrict names to a compatible set? What is that set? + public_key_name: String, + private_key_name: String, params: GenerateParams, // TODO: Is this enough? Or do we need to take SecurityAlgorithm as input instead of GenerateParams to ensure we don't lose distinctions like 5 vs 7 which are both RSASHA1? flags: u16, conn_pool: SyncConnPool, @@ -996,12 +997,25 @@ pub mod sign { let use_cryptographic_params = false; + // Note: Strictly speaking KMIP requires that each key, including + // public and private "halves" of the same key "pair", have a unique + // name within the HSM namespace. We don't enforce that here, e.g. + // maybe you know that your backend is actually a KMIP to PKCS#11 + // gateway and PKCS#11 doesn't have the same restriction and you + // want keys to be named as you are used to with your PKCS#11 HSM. We + // also don't intefere with names by making them unique as that would + // change any max name length calculations performed by the caller + // to avoid known issues with backend name limitations for their + // particular HSM (the PKCS#11 and KMIP specifications are silent on + // name limits but implementations definitely have limits, and not all + // the same). + let mut common_attrs = vec![]; let priv_key_attrs = vec![ // Krill supplies a name at creation time. Do we need to? // Note: Fortanix DSM requires a name for at least the private // key. - request::Attribute::Name(format!("{name}_priv")), + request::Attribute::Name(private_key_name), request::Attribute::CryptographicUsageMask( CryptographicUsageMask::Sign, ), @@ -1010,7 +1024,7 @@ pub mod sign { // Krill supplies a name at creation time. Do we need to? // Note: Fortanix DSM requires a name for at least the private // key. - request::Attribute::Name(format!("{name}_pub")), + request::Attribute::Name(public_key_name), // Krill does verification, do we need to? ODS doesn't. // Note: PyKMIP requires a Cryptographic Usage Mask for the public // key. From 17d9f57993fb8a3c29b3987195713f872eae2732 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Aug 2025 02:15:05 +0200 Subject: [PATCH 519/569] Use RustLS for TLS >= 1.2 with secure ciphers. The kmip-protocol crate constructs KMIP requests with upto KMIP version 1.2 advertized in the request. The KMIP 1.2 specification says that servers MAY use TLS 1.0, but should use TLS 1.2. Use RustLS which is (a) Rust and so has fewer safety issues than OpenSSL and less build dependencies/time, and (b) lacks support for TLS < 1.2 and insecure ciphers so cannot accidentally do TLS < 1.2 and likely does more secure TLS 1.2 than OpenSSL if OpenSSL were not used securely. --- Cargo.lock | 113 ++++++++++++++++++++++++++++++++++++++++++++--------- Cargo.toml | 2 +- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0790d6543..61f638b11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -116,6 +116,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + [[package]] name = "bcder" version = "0.7.5" @@ -280,9 +286,9 @@ dependencies = [ "pretty_assertions", "proc-macro2", "rand", - "ring", + "ring 0.17.14", "rstest", - "rustls-pemfile", + "rustls-pemfile 2.2.0", "rustversion", "secrecy", "serde", @@ -755,14 +761,16 @@ dependencies = [ "kmip-ttlv", "log", "maybe-async", - "openssl", "r2d2", "rustc_version", + "rustls 0.19.1", + "rustls-pemfile 0.2.1", "serde", "serde_bytes", "serde_derive", "tracing", "trait-set", + "webpki", ] [[package]] @@ -978,15 +986,6 @@ dependencies = [ "syn 2.0.104", ] -[[package]] -name = "openssl-src" -version = "300.5.0+3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.109" @@ -995,7 +994,6 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -1252,6 +1250,21 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[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 0.7.1", + "web-sys", + "winapi", +] + [[package]] name = "ring" version = "0.17.14" @@ -1262,7 +1275,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted", + "untrusted 0.9.0", "windows-sys 0.52.0", ] @@ -1311,6 +1324,19 @@ dependencies = [ "semver", ] +[[package]] +name = "rustls" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" +dependencies = [ + "base64", + "log", + "ring 0.16.20", + "sct", + "webpki", +] + [[package]] name = "rustls" version = "0.23.28" @@ -1319,13 +1345,22 @@ checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" dependencies = [ "log", "once_cell", - "ring", + "ring 0.17.14", "rustls-pki-types", "rustls-webpki", "subtle", "zeroize", ] +[[package]] +name = "rustls-pemfile" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eebeaeb360c87bfb72e84abdb3447159c0eaececf1bef2aecd65a8be949d1c9" +dependencies = [ + "base64", +] + [[package]] name = "rustls-pemfile" version = "2.2.0" @@ -1350,9 +1385,9 @@ version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ - "ring", + "ring 0.17.14", "rustls-pki-types", - "untrusted", + "untrusted 0.9.0", ] [[package]] @@ -1388,6 +1423,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "secrecy" version = "0.10.3" @@ -1509,6 +1554,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -1663,7 +1714,7 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.28", "tokio", ] @@ -1810,6 +1861,12 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "untrusted" version = "0.9.0" @@ -1929,6 +1986,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring 0.16.20", + "untrusted 0.7.1", +] + [[package]] name = "webpki-roots" version = "0.26.11" diff --git a/Cargo.toml b/Cargo.toml index fa2a8e348..1157f530e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,7 +33,7 @@ chrono = { version = "0.4.35", optional = true, default-features = false futures-util = { version = "0.3", optional = true } hashbrown = { version = "0.14.2", optional = true, default-features = false, features = ["allocator-api2", "inline-more"] } # 0.14.2 introduces explicit hashing heapless = { version = "0.8", optional = true } -kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-openssl-vendored"] } # TODO: use the async feature instead +kmip = { git = "https://github.com/NLnetLabs/kmip-protocol", branch = "next", package = "kmip-protocol", version = "0.5.0", optional = true, features = ["tls-with-rustls"] } libc = { version = "0.2.153", default-features = false, optional = true } # 0.2.79 is the first version that has IP_PMTUDISC_OMIT log = { version = "0.4.22", optional = true } parking_lot = { version = "0.12", optional = true } From ab61e4c9c51d38d3574c232040849bd0ef0d2923 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Mon, 25 Aug 2025 02:15:11 +0200 Subject: [PATCH 520/569] Fix broken tests. --- src/crypto/kmip.rs | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 30f2ffd4f..884869013 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -1469,7 +1469,14 @@ mod tests { dbg!(&res); res.unwrap(); - let generated_key_name = format!( + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( "{}", SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -1477,7 +1484,8 @@ mod tests { .as_secs() ); let res = generate( - generated_key_name, + pub_key_name, + pri_key_name, crate::crypto::sign::GenerateParams::RsaSha256 { bits: 2048 }, // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, @@ -1533,7 +1541,14 @@ mod tests { // dbg!(&res); // res.unwrap(); - let generated_key_name = format!( + let pub_key_name = format!( + "{}", + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + ); + let pri_key_name = format!( "{}", SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) @@ -1541,7 +1556,8 @@ mod tests { .as_secs() ); let res = generate( - generated_key_name, + pub_key_name, + pri_key_name, crate::crypto::sign::GenerateParams::RsaSha256 { bits: 1024 }, // crate::crypto::sign::GenerateParams::EcdsaP256Sha256, 256, From 708727708b7bbf3e138ec1e59165041a8a3f8a5e Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 27 Aug 2025 08:59:45 +0200 Subject: [PATCH 521/569] Add missing WaitDnskeyPropagated. --- src/dnssec/sign/keys/keyset.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 1ea8f5012..03baed8b8 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -2001,6 +2001,7 @@ fn zsk_roll_actions(rollstate: RollState) -> Vec { RollState::CacheExpire2(_) => (), RollState::Done => { actions.push(Action::UpdateDnskeyRrset); + actions.push(Action::WaitDnskeyPropagated); } } actions From 7268eb36b850959b67f52d94940462181f4b3fd7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 29 Aug 2025 10:28:46 +0200 Subject: [PATCH 522/569] Don't discard the NOTIFY SOA serial, if one is received. (#562) Extends NotifyMiddlewareSvc and Notifiable to also extract the optional SOA serial that a DNS NOTIFY message can contain. --- examples/serve-zone.rs | 3 +- src/net/server/middleware/notify.rs | 43 ++++++++++++++++++++++++----- src/net/server/tests/integration.rs | 4 ++- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index f1a7b8c70..50c3e8d34 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -273,11 +273,12 @@ impl Notifiable for DemoNotifyTarget { &self, class: Class, apex_name: &StoredName, + serial: Option, source: IpAddr, ) -> Pin< Box> + Sync + Send + '_>, > { - eprintln!("Notify received from {source} of change to zone {apex_name} in class {class}"); + eprintln!("Notify received from {source} of change to zone {apex_name} in class {class} with serial {serial:?}"); let res = match apex_name.to_string().to_lowercase().as_str() { "example.com" => Ok(()), diff --git a/src/net/server/middleware/notify.rs b/src/net/server/middleware/notify.rs index fa0e465f9..a34d30f69 100644 --- a/src/net/server/middleware/notify.rs +++ b/src/net/server/middleware/notify.rs @@ -62,18 +62,21 @@ use crate::base::message_builder::AdditionalBuilder; use crate::base::name::Name; use crate::base::net::IpAddr; use crate::base::{ - Message, ParsedName, Question, Rtype, StreamTarget, ToName, + Message, ParsedName, Question, Rtype, Serial, StreamTarget, ToName, }; use crate::net::server::message::Request; use crate::net::server::middleware::stream::MiddlewareStream; use crate::net::server::service::{CallResult, Service}; use crate::net::server::util::{mk_builder_for_target, mk_error_response}; -use crate::rdata::AllRecordData; +use crate::rdata::{AllRecordData, ZoneRecordData}; /// A DNS NOTIFY middleware service. /// /// [NotifyMiddlewareSvc] implements an [RFC 1996] compliant recipient of DNS -/// NOTIFY messages. +/// NOTIFY messages with QTYPE SOA. +/// +/// NOTIFY messages with other QTYPEs will pass through this middleware +/// unchanged and unhandled. /// /// See the [module documentation][super] for more information. /// @@ -150,6 +153,29 @@ where let apex_name = q.qname().to_name(); let source = req.client_addr().ip(); + // https://datatracker.ietf.org/doc/html/rfc1996#section-3 + // "3.7. A NOTIFY request has QDCOUNT>0, ANCOUNT>=0, AUCOUNT>=0, + // ADCOUNT>=0. If ANCOUNT>0, then the answer section represents an + // unsecure hint at the new RRset for this ." + // + // "3.11. The only defined NOTIFY event at this time is that the SOA + // RR has changed." + // + // Check if the ANSWER section contains a SOA RR. If so, extract the + // SOA serial to pass to notify_target. + let mut serial = None; + if msg.header_counts().ancount() > 0 { + if let Ok(mut answer) = msg.answer() { + if let Some(Ok(record)) = answer.next() { + if let Ok(Some(record)) = record.to_record() { + if let ZoneRecordData::Soa(soa) = record.data() { + serial = Some(soa.serial()); + } + } + } + } + } + // https://datatracker.ietf.org/doc/html/rfc1996#section-3 // "3.1. When a master has updated one or more RRs in which slave // servers may be interested, the master may send the changed RR's @@ -160,9 +186,10 @@ where // So, we have received a notification from a server that an RR // changed that we may be interested in. info!( - "NOTIFY received from {} for zone '{}'", + "NOTIFY received from {} for zone '{}' with serial {:?}", req.client_addr(), - q.qname() + q.qname(), + serial, ); // https://datatracker.ietf.org/doc/html/rfc1996#section-3 @@ -194,7 +221,7 @@ where // // Announce this notification for processing. match notify_target - .notify_zone_changed(class, &apex_name, source) + .notify_zone_changed(class, &apex_name, serial, source) .await { Err(NotifyError::NotAuthForZone) => { @@ -404,6 +431,7 @@ pub trait Notifiable { &self, class: Class, apex_name: &Name, + serial: Option, source: IpAddr, ) -> Pin< Box> + Sync + Send + '_>, @@ -417,10 +445,11 @@ impl Notifiable for Arc { &self, class: Class, apex_name: &Name, + serial: Option, source: IpAddr, ) -> Pin< Box> + Sync + Send + '_>, > { - (**self).notify_zone_changed(class, apex_name, source) + (**self).notify_zone_changed(class, apex_name, serial, source) } } diff --git a/src/net/server/tests/integration.rs b/src/net/server/tests/integration.rs index 71959527a..3ac3ed482 100644 --- a/src/net/server/tests/integration.rs +++ b/src/net/server/tests/integration.rs @@ -24,6 +24,7 @@ use crate::base::name::ToName; use crate::base::net::IpAddr; use crate::base::Name; use crate::base::Rtype; +use crate::base::Serial; use crate::logging::init_logging; use crate::net::client::request::{RequestMessage, RequestMessageMulti}; use crate::net::client::{dgram, stream, tsig}; @@ -562,11 +563,12 @@ impl Notifiable for TestNotifyTarget { &self, class: Class, apex_name: &StoredName, + serial: Option, source: IpAddr, ) -> Pin< Box> + Sync + Send + '_>, > { - trace!("Notify received from {source} of change to zone {apex_name} in class {class}"); + trace!("Notify received from {source} of change to zone {apex_name} in class {class} with serial {serial:?}"); let res = match apex_name.to_string().to_lowercase().as_str() { "example.com" => Ok(()), From 3c6b7f7bd4a7ae3074e41f15c5144d5cfe200bdf Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 1 Sep 2025 14:25:38 +0200 Subject: [PATCH 523/569] Add add_public_key, set_present, set_signer, set_at_parent, set_visible, set_ds_visible, set_rrsig_visible. --- src/dnssec/sign/keys/keyset.rs | 167 +++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 03baed8b8..4cdfcddd2 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -199,6 +199,173 @@ impl KeySet { } } + /// Add a public key. + pub fn add_public_key( + &mut self, + pubref: String, + algorithm: SecurityAlgorithm, + key_tag: u16, + creation_ts: UnixTime, + available: bool, + ) -> Result<(), Error> { + if !self.unique_key_tag(key_tag) { + return Err(Error::DuplicateKeyTag); + } + let keystate = KeyState { + available, + ..Default::default() + }; + let key = Key::new( + None, + KeyType::Include(keystate), + algorithm, + key_tag, + creation_ts, + ); + if let hash_map::Entry::Vacant(e) = self.keys.entry(pubref) { + e.insert(key); + Ok(()) + } else { + Err(Error::KeyExists) + } + } + + /// Set the present flag of a key. + /// + /// For CSK set the present in both key states. + pub fn set_present( + &mut self, + pubref: &str, + value: bool, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + match &mut key.keytype { + KeyType::Ksk(keystate) + | KeyType::Zsk(keystate) + | KeyType::Include(keystate) => { + keystate.present = value; + } + KeyType::Csk(ksk_keystate, zsk_keystate) => { + ksk_keystate.present = value; + zsk_keystate.present = value; + } + }; + if value && key.timestamps.published.is_none() { + key.timestamps.published = Some(UnixTime::now()); + } + } + } + Ok(()) + } + + /// Set the signer flag of a key. + /// + /// For CSK set the signer in both key states. Return an error if the + /// key is Include. + pub fn set_signer( + &mut self, + pubref: &str, + value: bool, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + match &mut key.keytype { + KeyType::Ksk(keystate) | KeyType::Zsk(keystate) => { + keystate.signer = value; + } + KeyType::Csk(ksk_keystate, zsk_keystate) => { + ksk_keystate.signer = value; + zsk_keystate.signer = value; + } + KeyType::Include(_) => return Err(Error::WrongKeyType), + }; + } + } + Ok(()) + } + + /// Set the at_parent flag of a key. + /// + /// For CSK set the signer the KSK state. Return an error if the key is + /// Include or a ZSK. + pub fn set_at_parent( + &mut self, + pubref: &str, + value: bool, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + match &mut key.keytype { + KeyType::Ksk(keystate) => { + keystate.at_parent = value; + } + KeyType::Csk(ksk_keystate, _) => { + ksk_keystate.at_parent = value; + } + KeyType::Zsk(_) | KeyType::Include(_) => { + return Err(Error::WrongKeyType) + } + }; + } + } + Ok(()) + } + + /// Set the visible time of a key. + pub fn set_visible( + &mut self, + pubref: &str, + time: UnixTime, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + key.timestamps.visible = Some(time); + } + } + Ok(()) + } + + /// Set the ds_visible time of a key. + /// + /// Note: there is no consistency check. The ds_visible time can be + /// set even if at_parent is false. + pub fn set_ds_visible( + &mut self, + pubref: &str, + time: UnixTime, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + key.timestamps.ds_visible = Some(time); + } + } + Ok(()) + } + + /// Set the rrsig_visible time of a key. + /// + /// Note: there is no consistency check. The rrsig_visible time can be + /// set even if signer is false or the key is not signing the zone. + pub fn set_rrsig_visible( + &mut self, + pubref: &str, + time: UnixTime, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + key.timestamps.rrsig_visible = Some(time); + } + } + Ok(()) + } + fn unique_key_tag(&self, key_tag: u16) -> bool { !self.keys.iter().any(|(_, k)| k.key_tag == key_tag) } From c4dc41dbbd314f19437b9c30ac6e5c793b97f186 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 3 Sep 2025 10:28:27 +0200 Subject: [PATCH 524/569] Add decoupled flag. --- src/dnssec/sign/keys/keyset.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 4cdfcddd2..b321ae679 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -230,6 +230,19 @@ impl KeySet { } } + /// Set the decoupled flag of a key. + pub fn set_decoupled( + &mut self, + pubref: &str, + value: bool, + ) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => key.decoupled = value, + } + Ok(()) + } + /// Set the present flag of a key. /// /// For CSK set the present in both key states. @@ -1111,6 +1124,11 @@ impl KeySet { #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Key { privref: Option, + + // XXX - remove the following directive before merging. + #[serde(default)] + decoupled: bool, + keytype: KeyType, algorithm: SecurityAlgorithm, key_tag: u16, @@ -1123,6 +1141,12 @@ impl Key { self.privref.as_deref() } + /// Return whether the key is decoupled from the underlying key storage + /// or not. + pub fn decoupled(&self) -> bool { + self.decoupled + } + /// Return the key type (which includes the state of the key). pub fn keytype(&self) -> KeyType { self.keytype.clone() @@ -1156,6 +1180,7 @@ impl Key { }; Self { privref, + decoupled: false, keytype, algorithm, key_tag, From e1f74daa0ccec2e4f111881e97354ec0fa72ff6f Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 3 Sep 2025 15:17:36 +0200 Subject: [PATCH 525/569] Add set_stale. --- src/dnssec/sign/keys/keyset.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index b321ae679..d478a4c70 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -328,6 +328,38 @@ impl KeySet { Ok(()) } + /// Make a key stale. + /// + /// Set old and clear present, signer and at_parent. + pub fn set_stale(&mut self, pubref: &str) -> Result<(), Error> { + match self.keys.get_mut(pubref) { + None => return Err(Error::KeyNotFound), + Some(key) => { + match &mut key.keytype { + KeyType::Ksk(keystate) + | KeyType::Zsk(keystate) + | KeyType::Include(keystate) => { + keystate.old = true; + keystate.present = false; + keystate.signer = false; + keystate.at_parent = false; + } + KeyType::Csk(ksk_keystate, zsk_keystate) => { + ksk_keystate.old = true; + ksk_keystate.present = false; + ksk_keystate.signer = false; + ksk_keystate.at_parent = false; + zsk_keystate.old = true; + zsk_keystate.present = false; + zsk_keystate.signer = false; + zsk_keystate.at_parent = false; + } + }; + } + } + Ok(()) + } + /// Set the visible time of a key. pub fn set_visible( &mut self, From 3ee93e7741d0b9c6b3004bbc5906322535ccc714 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 4 Sep 2025 16:10:47 +0200 Subject: [PATCH 526/569] Implement support for SSHFP records --- src/base/iana/mod.rs | 2 + src/base/iana/sshfp.rs | 73 +++++++++ src/rdata/mod.rs | 6 + src/rdata/sshfp.rs | 349 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 430 insertions(+) create mode 100644 src/base/iana/sshfp.rs create mode 100644 src/rdata/sshfp.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 1eee9dc8f..5f8155057 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -34,6 +34,7 @@ pub use self::opt::OptionCode; pub use self::rcode::{OptRcode, Rcode, TsigRcode}; pub use self::rtype::Rtype; pub use self::secalg::SecurityAlgorithm; +pub use self::sshfp::{SshfpAlgorithm, SshfpType}; pub use self::svcb::SvcParamKey; pub use self::zonemd::{ZonemdAlgorithm, ZonemdScheme}; @@ -49,5 +50,6 @@ pub mod opt; pub mod rcode; pub mod rtype; pub mod secalg; +pub mod sshfp; pub mod svcb; pub mod zonemd; diff --git a/src/base/iana/sshfp.rs b/src/base/iana/sshfp.rs new file mode 100644 index 000000000..28e3a70f0 --- /dev/null +++ b/src/base/iana/sshfp.rs @@ -0,0 +1,73 @@ +//! SSHFP IANA parameters. +//! +//! [RFC 4255]: https://tools.ietf.org/html/rfc4255 +//! [RFC 6594]: https://tools.ietf.org/html/rfc6594 +//! [RFC 7479]: https://tools.ietf.org/html/rfc7479 +//! [RFC 8709]: https://tools.ietf.org/html/rfc8709 + +//------------ SshfpType ----------------------------------------------------- + +// FIXME: These types don't actually have a mnemonic, only a description. +int_enum! { + /// SSHFP fingerprint type. + /// + /// This type selects the digest algorithm used for the fingerprint in the + /// [SSHFP] record. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [SSHFP]: ../../../rdata/sshfp/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-2 + => + SshfpType, u8; + + (RESERVED => 0, "Reserved") + + /// Specified that the SHA-1 algorithm is used. [RFC4255] + (SHA1 => 1, "SHA-1") + + /// Specified that the SHA-256 algorithm is used. [RFC6594] + (SHA256 => 2, "SHA-256") + +} + +int_enum_str_decimal!(SshfpType, u8); +int_enum_zonefile_fmt_decimal!(SshfpType, "fingerprint type"); + +//------------ SshfpAlgorithm ------------------------------------------------ + +int_enum! { + /// SSHFP public key algorithms. + /// + /// This type selects the algorithm of the public key associated with the [SSHFP]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [SSHFP]: ../../../rdata/sshfp/index.html + /// [IANA registration]: https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml#dns-sshfp-rr-parameters-1 + => + SshfpAlgorithm, u8; + + /// Specified that the Reserved algorithm is used. [RFC4255] + (RESERVED => 0, "Reserved") + + /// Specified that the RSA algorithm is used. [RFC4255] + (RSA => 1, "RSA") + + /// Specified that the DSA algorithm is used. [RFC4255] + (DSA => 2, "DSA") + + /// Specified that the ECDSA algorithm is used. [RFC6594] + (ECDSA => 3, "ECDSA") + + /// Specified that the Ed25519 algorithm is used. [RFC7479] + (ED25519 => 4, "Ed25519") + + /// Specified that the Ed448 algorithm is used. [RFC8709] + (ED448 => 6, "Ed448") +} + +int_enum_str_decimal!(SshfpAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(SshfpAlgorithm, "public key algorithm"); diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 1d7de2b5a..96bd6192c 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -53,6 +53,7 @@ pub mod naptr; pub mod nsec3; pub mod rfc1035; pub mod srv; +pub mod sshfp; pub mod svcb; pub mod tsig; pub mod zonemd; @@ -133,6 +134,11 @@ rdata_types! { Srv, } } + sshfp::{ + zone { + Sshfp, + } + } svcb::{ pseudo { Svcb, diff --git a/src/rdata/sshfp.rs b/src/rdata/sshfp.rs new file mode 100644 index 000000000..4dc76254a --- /dev/null +++ b/src/rdata/sshfp.rs @@ -0,0 +1,349 @@ +//! Record data from [RFC 4255]: SSHFP records. +//! +//! This RFC defines the SSHFP record type and is updated by [RFC 6594], +//! [RFC 7479], and [RFC 8709]. +//! +//! [RFC 4255]: https://tools.ietf.org/html/rfc4255 +//! [RFC 6594]: https://tools.ietf.org/html/rfc6594 +//! [RFC 7479]: https://tools.ietf.org/html/rfc7479 +//! [RFC 8709]: https://tools.ietf.org/html/rfc8709 + +// Currently a false positive on Sshfp. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{SshfpAlgorithm, SshfpType}; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::Rtype; +use crate::utils::base16; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +//------------ Sshfp --------------------------------------------------------- + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Sshfp { + algorithm: SshfpAlgorithm, + fingerprint_type: SshfpType, + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + fingerprint: Octs, +} + +impl Sshfp<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::SSHFP; +} + +impl Sshfp { + pub fn new( + algorithm: SshfpAlgorithm, + fingerprint_type: SshfpType, + fingerprint: Octs, + ) -> Self { + Sshfp { + algorithm, + fingerprint_type, + fingerprint, + } + } + + /// Get the algorithm field. + pub fn algorithm(&self) -> SshfpAlgorithm { + self.algorithm + } + + /// Get the fingerprint type field. + pub fn fingerprint_type(&self) -> SshfpType { + self.fingerprint_type + } + + /// Get the fingerprint field. + pub fn fingerprint(&self) -> &Octs { + &self.fingerprint + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let algorithm = SshfpAlgorithm::parse(parser)?; + let fingerprint_type = SshfpType::parse(parser)?; + let len = parser.remaining(); + let fingerprint = parser.parse_octets(len)?; + Ok(Self { + algorithm, + fingerprint_type, + fingerprint, + }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let algorithm = SshfpAlgorithm::scan(scanner)?; + let fingerprint_type = SshfpType::scan(scanner)?; + let fingerprint = + scanner.convert_entry(base16::SymbolConverter::new())?; + + Ok(Self { + algorithm, + fingerprint_type, + fingerprint, + }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Sshfp { + algorithm, + fingerprint_type, + fingerprint, + } = self; + + Ok(Sshfp { + algorithm, + fingerprint_type, + fingerprint: fingerprint.try_octets_into()?, + }) + } +} + +impl RecordData for Sshfp { + fn rtype(&self) -> Rtype { + Sshfp::RTYPE + } +} + +impl> ComposeRecordData for Sshfp { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // algorithm + fingerprint_type + fingerprint_len + u16::try_from(1 + 1 + self.fingerprint.as_ref().len()) + .expect("long SSHFP rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.algorithm.into()])?; + target.append_slice(&[self.fingerprint_type.into()])?; + target.append_slice(self.fingerprint.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Sshfp { + fn hash(&self, state: &mut H) { + self.algorithm.hash(state); + self.fingerprint_type.hash(state); + self.fingerprint.as_ref().hash(state); + } +} + +impl PartialEq> for Sshfp +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Sshfp) -> bool { + self.algorithm.eq(&other.algorithm) + && self.fingerprint_type.eq(&other.fingerprint_type) + && self.fingerprint.as_ref().eq(other.fingerprint.as_ref()) + } +} + +impl + ?Sized> Eq for Sshfp {} + +impl> fmt::Display for Sshfp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} ( ", + u8::from(self.algorithm), + u8::from(self.fingerprint_type) + )?; + base16::display(&self.fingerprint, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Sshfp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Sshfp(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Sshfp { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.algorithm)?; + p.write_show(self.fingerprint_type)?; + p.write_token(base16::encode_display(&self.fingerprint)) + }) + } +} + +impl PartialOrd> for Sshfp +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Sshfp) -> Option { + match self.algorithm.partial_cmp(&other.algorithm) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.fingerprint_type.partial_cmp(&other.fingerprint_type) { + Some(Ordering::Equal) => {} + other => return other, + } + self.fingerprint + .as_ref() + .partial_cmp(other.fingerprint.as_ref()) + } +} + +impl CanonicalOrd> for Sshfp +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Sshfp) -> Ordering { + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.fingerprint_type.cmp(&other.fingerprint_type) { + Ordering::Equal => {} + other => return other, + } + self.fingerprint.as_ref().cmp(other.fingerprint.as_ref()) + } +} + +impl> Ord for Sshfp { + fn cmp(&self, other: &Self) -> Ordering { + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.fingerprint_type.cmp(&other.fingerprint_type) { + Ordering::Equal => {} + other => return other, + } + self.fingerprint.as_ref().cmp(other.fingerprint.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base16::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + fn sshfp_compose_parse_scan() { + let algorithm = 1.into(); + let fingerprint_type = 1.into(); + let fingerprint_str = "73d3fa022a121062580431316bfe5d56653b91c2"; + let fingerprint: Vec = decode(fingerprint_str).unwrap(); + let rdata = Sshfp::new(algorithm, fingerprint_type, fingerprint); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Sshfp::parse(parser)); + test_scan( + &[ + &u8::from(algorithm).to_string(), + &u8::from(fingerprint_type).to_string(), + fingerprint_str, + ], + Sshfp::scan, + &rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn sshfp_parse_zonefile() { + use crate::base::iana::{SshfpAlgorithm, SshfpType}; + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN SSHFP 1 1 ( + 73d3fa022a121062 + 580431316bfe5d56 + 653b91c2 ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::SSHFP { + continue; + } + match record.into_data() { + ZoneRecordData::Sshfp(rd) => { + assert_eq!(SshfpAlgorithm::RSA, rd.algorithm()); + assert_eq!( + SshfpType::SHA1, + rd.fingerprint_type() + ); + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} From 4f9f54cd0d667788f3419f6f87ad820174b2695e Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 4 Sep 2025 16:46:57 +0200 Subject: [PATCH 527/569] Implement support for OPENPGPKEY records --- src/rdata/mod.rs | 6 + src/rdata/openpgpkey.rs | 262 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+) create mode 100644 src/rdata/openpgpkey.rs diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 96bd6192c..b5b9be9a2 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -51,6 +51,7 @@ pub mod dname; pub mod dnssec; pub mod naptr; pub mod nsec3; +pub mod openpgpkey; pub mod rfc1035; pub mod srv; pub mod sshfp; @@ -129,6 +130,11 @@ rdata_types! { Nsec3param, } } + openpgpkey::{ + zone { + Openpgpkey, + } + } srv::{ zone { Srv, diff --git a/src/rdata/openpgpkey.rs b/src/rdata/openpgpkey.rs new file mode 100644 index 000000000..070d3b11c --- /dev/null +++ b/src/rdata/openpgpkey.rs @@ -0,0 +1,262 @@ +//! OPENPGPKEY record data. +//! +//! The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key. +//! +//! [RFC 7929]: https://tools.ietf.org/html/rfc7929 + +// Currently a false positive on Openpgpkey. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::Rtype; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::utils::base64; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +/// The OPENPGPKEY Resource Record carries a single OpenPGP Transferable Public Key. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Openpgpkey { + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + key: Octs, +} + +impl Openpgpkey<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::OPENPGPKEY; +} + +impl Openpgpkey { + /// Create a Openpgpkey record data from provided parameters. + pub fn new(key: Octs) -> Self { + Self { key } + } + + /// Get the key field. + pub fn key(&self) -> &Octs { + &self.key + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let len = parser.remaining(); + let key = parser.parse_octets(len)?; + Ok(Self { key }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let key = scanner.convert_entry(base64::SymbolConverter::new())?; + + Ok(Self { key }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Openpgpkey { key } = self; + + Ok(Openpgpkey { + key: key.try_octets_into()?, + }) + } +} + +impl RecordData for Openpgpkey { + fn rtype(&self) -> Rtype { + Openpgpkey::RTYPE + } +} + +impl> ComposeRecordData for Openpgpkey { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // key_len + u16::try_from(self.key.as_ref().len()) + .expect("long OPENPGPKEY rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(self.key.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Openpgpkey { + fn hash(&self, state: &mut H) { + self.key.as_ref().hash(state); + } +} + +impl PartialEq> for Openpgpkey +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Openpgpkey) -> bool { + self.key.as_ref().eq(other.key.as_ref()) + } +} + +impl + ?Sized> Eq for Openpgpkey {} + +impl> fmt::Display for Openpgpkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "( ",)?; + base64::display(&self.key, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Openpgpkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Openpgpkey(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Openpgpkey { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| p.write_token(base64::encode_display(&self.key))) + } +} + +impl PartialOrd> for Openpgpkey +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Openpgpkey) -> Option { + self.key.as_ref().partial_cmp(other.key.as_ref()) + } +} + +impl CanonicalOrd> for Openpgpkey +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Openpgpkey) -> Ordering { + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +impl> Ord for Openpgpkey { + fn cmp(&self, other: &Self) -> Ordering { + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base64::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + fn openpgpkey_compose_parse_scan() { + let key_str = "mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6PfPJsT8to5dksKP1JsCmR0DqOTmVYLOv7mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBubG5ldGxhYnMubmw+iJYEExYKAD4WIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbAwUJBaOagAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAxXU3+oVEKemSgAP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzGwfpixgRI4sqKfwD9FxJhsj43vGEbOLdsWwf/lQvkajRov5FpofS1IFy/dgi4OARouaNyEgorBgEEAZdVAQUBAQdAUQr9riJNCFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJBaOagAAKCRAxXU3+oVEKeuX3APkB5piWOSbOPLvtiElIVTHT6gWlu1wSpVVzZEmgtnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWwHvIw4JxHS1a/piLwkwM="; + let key: Vec = decode(key_str).unwrap(); + let rdata = Openpgpkey::new(key); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Openpgpkey::parse(parser)); + test_scan(&[key_str], Openpgpkey::scan, &rdata); + } + + #[cfg(feature = "zonefile")] + #[test] + fn openpgpkey_parse_zonefile() { + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN OPENPGPKEY ( + mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6P + fPJsT8to5dksKP1JsCmR0DqOTmVYLOv7 + mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBu + bG5ldGxhYnMubmw+iJYEExYKAD4WIQT/ + B5WhrMOftpJIwIkxXU3+oVEKegUCaLmj + cgIbAwUJBaOagAULCQgHAgYVCgkICwIE + FgIDAQIeAQIXgAAKCRAxXU3+oVEKemSg + AP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzG + wfpixgRI4sqKfwD9FxJhsj43vGEbOLds + Wwf/lQvkajRov5FpofS1IFy/dgi4OARo + uaNyEgorBgEEAZdVAQUBAQdAUQr9riJN + CFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7 + mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOf + tpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJ + BaOagAAKCRAxXU3+oVEKeuX3APkB5piW + OSbOPLvtiElIVTHT6gWlu1wSpVVzZEmg + tnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWw + HvIw4JxHS1a/piLwkwM= ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::OPENPGPKEY { + continue; + } + match record.into_data() { + ZoneRecordData::Openpgpkey(_) => {} + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} From f3ab60712e4e26c80d0e438ce40b41c8cd2ff310 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 4 Sep 2025 17:54:09 +0200 Subject: [PATCH 528/569] Implement support for TLSA records --- src/base/iana/mod.rs | 2 + src/base/iana/tlsa.rs | 99 +++++++++++ src/rdata/mod.rs | 6 + src/rdata/tlsa.rs | 384 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 491 insertions(+) create mode 100644 src/base/iana/tlsa.rs create mode 100644 src/rdata/tlsa.rs diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 5f8155057..5eca06ec0 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -36,6 +36,7 @@ pub use self::rtype::Rtype; pub use self::secalg::SecurityAlgorithm; pub use self::sshfp::{SshfpAlgorithm, SshfpType}; pub use self::svcb::SvcParamKey; +pub use self::tlsa::{TlsaCertificateUsage, TlsaMatchingType, TlsaSelector}; pub use self::zonemd::{ZonemdAlgorithm, ZonemdScheme}; #[macro_use] @@ -52,4 +53,5 @@ pub mod rtype; pub mod secalg; pub mod sshfp; pub mod svcb; +pub mod tlsa; pub mod zonemd; diff --git a/src/base/iana/tlsa.rs b/src/base/iana/tlsa.rs new file mode 100644 index 000000000..b320e4c2d --- /dev/null +++ b/src/base/iana/tlsa.rs @@ -0,0 +1,99 @@ +//! TLSA IANA parameters. + +//------------ TlsaCertificateUsage ------------------------------------------ + +int_enum! { + /// TLSA Certificate Usage type. + /// + /// This type specifies the provided association that will be used to match the certificate + /// presented in the TLS handshake + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#certificate-usages + => + TlsaCertificateUsage, u8; + + /// CA constraint + (PKIX_TA => 0, "PKIX-TA") + + /// Service certificate constraint + (PKIX_EE => 1, "PKIX-EE") + + /// Trust anchor assertion + (DANE_TA => 2, "DANE-TA") + + /// Domain-issued certificate + (DANE_EE => 3, "DANE-EE") + + /// Reserved for Private Use + (PRIVCERT => 255, "PrivCert") +} + +int_enum_str_decimal!(TlsaCertificateUsage, u8); +int_enum_zonefile_fmt_decimal!( + TlsaCertificateUsage, + "certificate usage type" +); + +//------------ TlsaSelector -------------------------------------------------- + +int_enum! { + /// TLSA Selector type. + /// + /// This type specifies which part of the TLS certificate presented by the server will be + /// matched against the association data + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#selectors + => + TlsaSelector, u8; + + /// Full certificate + (CERT => 0, "Cert") + + /// SubjectPublicKeyInfo + (SPKI => 1, "SPKI") + + /// Reserved for Private Use + (PRIVSEL => 255, "PrivSel") +} + +int_enum_str_decimal!(TlsaSelector, u8); +int_enum_zonefile_fmt_decimal!(TlsaSelector, "selector"); + +//------------ TlsaMatchingType ---------------------------------------------- + +int_enum! { + /// TLSA Matching Type type. + /// + /// This type specifies how the certificate association is presented. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-04. + /// + /// [TLSA]: ../../../rdata/tlsa/index.html + /// [IANA registration]: https://www.iana.org/assignments/dane-parameters/dane-parameters.xhtml#matching-types + => + TlsaMatchingType, u8; + + /// No hash used + (FULL => 0, "Full") + + /// 256 bit hash by SHA2 + (SHA2_256 => 1, "SHA2-256") + + /// 512 bit hash by SHA2 + (SHA2_512 => 2, "SHA2-512") + + /// Reserved for Private Use + (PRIVMATCH => 255, "PrivMatch") +} + +int_enum_str_decimal!(TlsaMatchingType, u8); +int_enum_zonefile_fmt_decimal!(TlsaMatchingType, "matching type"); diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index b5b9be9a2..77280f76d 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -56,6 +56,7 @@ pub mod rfc1035; pub mod srv; pub mod sshfp; pub mod svcb; +pub mod tlsa; pub mod tsig; pub mod zonemd; @@ -151,6 +152,11 @@ rdata_types! { Https, } } + tlsa::{ + zone { + Tlsa, + } + } tsig::{ pseudo { Tsig, diff --git a/src/rdata/tlsa.rs b/src/rdata/tlsa.rs new file mode 100644 index 000000000..21d9376d1 --- /dev/null +++ b/src/rdata/tlsa.rs @@ -0,0 +1,384 @@ +//! TLSA record data. +//! +//! The TLSA Resource Record is used to associate a TLS server certificate or +//! public key with the domain name of the RR +//! +//! [RFC 6698]: https://tools.ietf.org/html/rfc6698 + +// Currently a false positive on Tlsa. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{ + Rtype, TlsaCertificateUsage, TlsaMatchingType, TlsaSelector, +}; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::Scanner; +use crate::base::wire::{Composer, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::utils::base16; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +/// The TLSA Resource Record is used to associate a TLS server certificate or +/// public key with the domain name of the RR +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Tlsa { + usage: TlsaCertificateUsage, + selector: TlsaSelector, + matching_type: TlsaMatchingType, + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + /// Certificate Association Data + data: Octs, +} + +impl Tlsa<()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::TLSA; +} + +impl Tlsa { + /// Create a Tlsa record data from provided parameters. + pub fn new( + usage: TlsaCertificateUsage, + selector: TlsaSelector, + matching_type: TlsaMatchingType, + data: Octs, + ) -> Self { + Self { + usage, + selector, + matching_type, + data, + } + } + + /// Get the usage field. + pub fn usage(&self) -> TlsaCertificateUsage { + self.usage + } + + /// Get the selector field. + pub fn selector(&self) -> TlsaSelector { + self.selector + } + + /// Get the hash matching_type field. + pub fn matching_type(&self) -> TlsaMatchingType { + self.matching_type + } + + /// Get the certificate association data field. + pub fn data(&self) -> &Octs { + &self.data + } + + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let usage = TlsaCertificateUsage::parse(parser)?; + let selector = TlsaSelector::parse(parser)?; + let matching_type = TlsaMatchingType::parse(parser)?; + let len = parser.remaining(); + let data = parser.parse_octets(len)?; + Ok(Self { + usage, + selector, + matching_type, + data, + }) + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let usage = TlsaCertificateUsage::scan(scanner)?; + let selector = TlsaSelector::scan(scanner)?; + let matching_type = TlsaMatchingType::scan(scanner)?; + let data = scanner.convert_entry(base16::SymbolConverter::new())?; + + Ok(Self { + usage, + selector, + matching_type, + data, + }) + } + + pub(super) fn flatten>( + self, + ) -> Result, Target::Error> { + self.convert_octets() + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + let Tlsa { + usage, + selector, + matching_type, + data, + } = self; + + Ok(Tlsa { + usage, + selector, + matching_type, + data: data.try_octets_into()?, + }) + } +} + +impl RecordData for Tlsa { + fn rtype(&self) -> Rtype { + Tlsa::RTYPE + } +} + +impl> ComposeRecordData for Tlsa { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // usage + selector + matching_type + data_len + u16::try_from(1 + 1 + 1 + self.data.as_ref().len()) + .expect("long TLSA rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.usage.into()])?; + target.append_slice(&[self.selector.into()])?; + target.append_slice(&[self.matching_type.into()])?; + target.append_slice(self.data.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl> hash::Hash for Tlsa { + fn hash(&self, state: &mut H) { + self.usage.hash(state); + self.selector.hash(state); + self.matching_type.hash(state); + self.data.as_ref().hash(state); + } +} + +impl PartialEq> for Tlsa +where + Octs: AsRef<[u8]> + ?Sized, + Other: AsRef<[u8]> + ?Sized, +{ + fn eq(&self, other: &Tlsa) -> bool { + self.usage.eq(&other.usage) + && self.selector.eq(&other.selector) + && self.matching_type.eq(&other.matching_type) + && self.data.as_ref().eq(other.data.as_ref()) + } +} + +impl + ?Sized> Eq for Tlsa {} + +impl> fmt::Display for Tlsa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} {} ( ", + u8::from(self.usage), + u8::from(self.selector), + u8::from(self.matching_type) + )?; + base16::display(&self.data, f)?; + write!(f, " )") + } +} + +impl> fmt::Debug for Tlsa { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("Tlsa(")?; + fmt::Display::fmt(self, f)?; + f.write_str(")") + } +} + +impl> ZonefileFmt for Tlsa { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.usage)?; + p.write_show(self.selector)?; + p.write_show(self.matching_type)?; + p.write_token(base16::encode_display(&self.data)) + }) + } +} + +impl PartialOrd> for Tlsa +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn partial_cmp(&self, other: &Tlsa) -> Option { + match self.usage.partial_cmp(&other.usage) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.selector.partial_cmp(&other.selector) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.matching_type.partial_cmp(&other.matching_type) { + Some(Ordering::Equal) => {} + other => return other, + } + self.data.as_ref().partial_cmp(other.data.as_ref()) + } +} + +impl CanonicalOrd> for Tlsa +where + Octs: AsRef<[u8]>, + Other: AsRef<[u8]>, +{ + fn canonical_cmp(&self, other: &Tlsa) -> Ordering { + match self.usage.cmp(&other.usage) { + Ordering::Equal => {} + other => return other, + } + match self.selector.cmp(&other.selector) { + Ordering::Equal => {} + other => return other, + } + match self.matching_type.cmp(&other.matching_type) { + Ordering::Equal => {} + other => return other, + } + self.data.as_ref().cmp(other.data.as_ref()) + } +} + +impl> Ord for Tlsa { + fn cmp(&self, other: &Self) -> Ordering { + match self.usage.cmp(&other.usage) { + Ordering::Equal => {} + other => return other, + } + match self.selector.cmp(&other.selector) { + Ordering::Equal => {} + other => return other, + } + match self.matching_type.cmp(&other.matching_type) { + Ordering::Equal => {} + other => return other, + } + self.data.as_ref().cmp(other.data.as_ref()) + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base16::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + fn tlsa_compose_parse_scan() { + let usage = 0.into(); + let selector = 0.into(); + let matching_type = 1.into(); + let data_str = "d2abde240d7cd3ee6b4b28c54df034b97983a1d16e8a410e4561cb106618e971"; + let data: Vec = decode(data_str).unwrap(); + let rdata = Tlsa::new(usage, selector, matching_type, data); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Tlsa::parse(parser)); + test_scan( + &[ + &u8::from(usage).to_string(), + &u8::from(selector).to_string(), + &u8::from(matching_type).to_string(), + data_str, + ], + Tlsa::scan, + &rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn tlsa_parse_zonefile() { + use crate::base::iana::{ + TlsaCertificateUsage, TlsaMatchingType, TlsaSelector, + }; + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN TLSA 0 0 1 ( + d2abde240d7cd3ee6b4b28c54df034b9 + 7983a1d16e8a410e4561cb106618e971 ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::TLSA { + continue; + } + match record.into_data() { + ZoneRecordData::Tlsa(rd) => { + assert_eq!( + TlsaCertificateUsage::PKIX_TA, + rd.usage() + ); + assert_eq!(TlsaSelector::CERT, rd.selector()); + assert_eq!( + TlsaMatchingType::SHA2_256, + rd.matching_type() + ); + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} From e4329f18095f468fe80000a1dbce81febc0bf78e Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Fri, 5 Sep 2025 10:07:36 +0200 Subject: [PATCH 529/569] Clippy --- src/rdata/openpgpkey.rs | 4 +++- src/rdata/sshfp.rs | 3 +++ src/rdata/tlsa.rs | 3 +++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/rdata/openpgpkey.rs b/src/rdata/openpgpkey.rs index 070d3b11c..a54535d63 100644 --- a/src/rdata/openpgpkey.rs +++ b/src/rdata/openpgpkey.rs @@ -193,10 +193,12 @@ mod test { test_compose_parse, test_rdlen, test_scan, }; use crate::utils::base64::decode; - use std::string::ToString; use std::vec::Vec; #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Openpgpkey::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] fn openpgpkey_compose_parse_scan() { let key_str = "mDMEaLmjchYJKwYBBAHaRw8BAQdAaO6PfPJsT8to5dksKP1JsCmR0DqOTmVYLOv7mFeQPC+0HVRlc3QgVXNlciA8dGVzdEBubG5ldGxhYnMubmw+iJYEExYKAD4WIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbAwUJBaOagAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRAxXU3+oVEKemSgAP97Zvz+PWEJC9vhlSN4gVRPR9VZYhzGwfpixgRI4sqKfwD9FxJhsj43vGEbOLdsWwf/lQvkajRov5FpofS1IFy/dgi4OARouaNyEgorBgEEAZdVAQUBAQdAUQr9riJNCFWRzQ6q70B/H/o+uwvL6nGJRhWSg1v7mRkDAQgHiH4EGBYKACYWIQT/B5WhrMOftpJIwIkxXU3+oVEKegUCaLmjcgIbDAUJBaOagAAKCRAxXU3+oVEKeuX3APkB5piWOSbOPLvtiElIVTHT6gWlu1wSpVVzZEmgtnOpiQD+Kk/IFjHpT0RbgsIvI3qhnXWwHvIw4JxHS1a/piLwkwM="; let key: Vec = decode(key_str).unwrap(); diff --git a/src/rdata/sshfp.rs b/src/rdata/sshfp.rs index 4dc76254a..ab647bc1f 100644 --- a/src/rdata/sshfp.rs +++ b/src/rdata/sshfp.rs @@ -282,6 +282,9 @@ mod test { use std::vec::Vec; #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Sshfp::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] fn sshfp_compose_parse_scan() { let algorithm = 1.into(); let fingerprint_type = 1.into(); diff --git a/src/rdata/tlsa.rs b/src/rdata/tlsa.rs index 21d9376d1..56eb950cf 100644 --- a/src/rdata/tlsa.rs +++ b/src/rdata/tlsa.rs @@ -310,6 +310,9 @@ mod test { use std::vec::Vec; #[test] + // allow redundant_closure because because of lifetime shenanigans + // in test_compose_parse(Tlsa::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] fn tlsa_compose_parse_scan() { let usage = 0.into(); let selector = 0.into(); From 99a0a73c2190fc4e910ee83c2b845da3ba3b2f82 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 5 Sep 2025 12:08:37 +0200 Subject: [PATCH 530/569] Fix test. --- src/dnssec/sign/keys/keyset.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index d478a4c70..c9b22754d 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -2842,7 +2842,10 @@ mod tests { MockClock::advance_system_time(Duration::from_secs(3600)); let actions = ks.cache_expired2(RollType::ZskRoll).unwrap(); - assert_eq!(actions, [Action::UpdateDnskeyRrset]); + assert_eq!( + actions, + [Action::UpdateDnskeyRrset, Action::WaitDnskeyPropagated] + ); let mut dk = dnskey(&ks); dk.sort(); assert_eq!(dk, ["first KSK", "second ZSK"]); From 007ccc7eef5d8949caa447b7c65642be31e87784 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Tue, 9 Sep 2025 14:47:52 +0200 Subject: [PATCH 531/569] Resolve FIXME for SSHFP --- src/base/iana/sshfp.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/base/iana/sshfp.rs b/src/base/iana/sshfp.rs index 28e3a70f0..22675a7fd 100644 --- a/src/base/iana/sshfp.rs +++ b/src/base/iana/sshfp.rs @@ -4,10 +4,12 @@ //! [RFC 6594]: https://tools.ietf.org/html/rfc6594 //! [RFC 7479]: https://tools.ietf.org/html/rfc7479 //! [RFC 8709]: https://tools.ietf.org/html/rfc8709 +//! +//! The values of these types don't officially have an IANA assigned mnemonic. +//! For ease of use, we define them here anyway. //------------ SshfpType ----------------------------------------------------- -// FIXME: These types don't actually have a mnemonic, only a description. int_enum! { /// SSHFP fingerprint type. /// From 64e673bca0527afe9bd0d4c98f85bb1e58854aa8 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 10:54:29 +0200 Subject: [PATCH 532/569] Implement support for IPSECKEY records --- src/base/iana/ipseckey.rs | 71 ++++ src/base/iana/mod.rs | 2 + src/rdata/ipseckey.rs | 725 ++++++++++++++++++++++++++++++++++++++ src/rdata/mod.rs | 6 + 4 files changed, 804 insertions(+) create mode 100644 src/base/iana/ipseckey.rs create mode 100644 src/rdata/ipseckey.rs diff --git a/src/base/iana/ipseckey.rs b/src/base/iana/ipseckey.rs new file mode 100644 index 000000000..0dc85c84c --- /dev/null +++ b/src/base/iana/ipseckey.rs @@ -0,0 +1,71 @@ +//! IPSECKEY IANA parameters. +//! +//! The values of these types don't officially have an IANA assigned name and +//! mnemonic. For ease of use, we define them here anyway. + +//------------ IpseckeyAlgorithm --------------------------------------------- + +int_enum! { + /// IPSECKEY Algorithms. + /// + /// This type identifies the public key's cryptographic algorithm of the + /// [IPSECKEY]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-09. + /// + /// [IPSECKEY]: ../../../rdata/ipseckey/index.html + /// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-1 + => + IpseckeyAlgorithm, u8; + + /// Specified that no Public key is present. + (NONE => 0, "NONE") + + /// Specified that a DSA Public Key is used. + (DSA => 1, "DSA") + + /// Specified that an RSA Public Key is used. + (RSA => 2, "RSA") + + /// Specified that an ECDSA Public Key is used. + (ECDSA => 3, "ECDSA") + + /// Specified that an EdDSA Public Key is used. + (EDDSA => 4, "EdDSA") +} + +int_enum_str_decimal!(IpseckeyAlgorithm, u8); +int_enum_zonefile_fmt_decimal!(IpseckeyAlgorithm, "ipseckey algorithm"); + +//------------ IpseckeyGateway ----------------------------------------------- + +int_enum! { + /// IPSECKEY Gateway Types. + /// + /// This type indicates the format of the information that is stored in + /// the gateway field of the [IPSECKEY]. + /// + /// For the currently registered values see the [IANA registration]. This + /// type is complete as of 2025-09-09. + /// + /// [IPSECKEY]: ../../../rdata/ipseckey/index.html + /// [IANA registration]: https://www.iana.org/assignments/ipseckey-rr-parameters/ipseckey-rr-parameters.xhtml#ipseckey-rr-parameters-2 + => + IpseckeyGatewayType, u8; + + /// Specified that No gateway is present. + (NONE => 0, "NONE") + + /// Specified that A 4-byte IPv4 address is present. + (IPV4 => 1, "IPV4") + + /// Specified that A 16-byte IPv6 address is present. + (IPV6 => 2, "IPV6") + + /// Specified that A wire-encoded domain name is present. + (NAME => 3, "NAME") +} + +int_enum_str_decimal!(IpseckeyGatewayType, u8); +int_enum_zonefile_fmt_decimal!(IpseckeyGatewayType, "ipseckey gateway type"); diff --git a/src/base/iana/mod.rs b/src/base/iana/mod.rs index 5eca06ec0..1c734ccee 100644 --- a/src/base/iana/mod.rs +++ b/src/base/iana/mod.rs @@ -28,6 +28,7 @@ pub use self::class::Class; pub use self::digestalg::DigestAlgorithm; pub use self::exterr::ExtendedErrorCode; +pub use self::ipseckey::{IpseckeyAlgorithm, IpseckeyGatewayType}; pub use self::nsec3::Nsec3HashAlgorithm; pub use self::opcode::Opcode; pub use self::opt::OptionCode; @@ -45,6 +46,7 @@ mod macros; pub mod class; pub mod digestalg; pub mod exterr; +pub mod ipseckey; pub mod nsec3; pub mod opcode; pub mod opt; diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs new file mode 100644 index 000000000..aea2f5ca1 --- /dev/null +++ b/src/rdata/ipseckey.rs @@ -0,0 +1,725 @@ +//! IPSECKEY record data. +//! +//! The IPSECKEY Resource Record is used to publish a public key that is to be +//! associated with a domain name for use with the IPsec protocol suite. +//! +//! [RFC 4025]: https://tools.ietf.org/html/rfc4025 + +// Currently a false positive on Ipseckey. We cannot apply it there because +// the allow attribute doesn't get copied to the code generated by serde. +#![allow(clippy::needless_maybe_sized)] + +use crate::base::cmp::CanonicalOrd; +use crate::base::iana::{IpseckeyAlgorithm, IpseckeyGatewayType, Rtype}; +use crate::base::name::FlattenInto; +use crate::base::rdata::{ComposeRecordData, RecordData}; +use crate::base::scan::{Scan, Scanner, ScannerError}; +use crate::base::wire::{Composer, FormError, ParseError}; +use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; +use crate::base::{ParsedName, ToName}; +use crate::utils::base64; +use core::cmp::Ordering; +use core::{fmt, hash}; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + +use super::{Aaaa, A}; + +/// The IPSECKEY Resource Record is used to publish a public key that is to be +/// associated with a domain name for use with the IPsec protocol suite. +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct Ipseckey { + precedence: u8, + gateway_type: IpseckeyGatewayType, + algorithm: IpseckeyAlgorithm, + + /// "The gateway to which an IPsec tunnel may be created" + /// + /// There are three formats: + /// + /// - IPv4 address: This is a 32-bit number in network byte order. + /// - IPv6 address: This is a 128-bit number in network byte order. + /// - A normal wire-encoded domain name, always uncompressed. + /// + /// May be empty if the gateway type is IpseckeyGatewayType::NONE. + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + gateway: IpseckeyGateway, + + // May be zero bytes long + #[cfg_attr( + feature = "serde", + serde( + serialize_with = "octseq::serde::SerializeOctets::serialize_octets", + deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", + bound( + serialize = "Octs: octseq::serde::SerializeOctets", + deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", + ) + ) + )] + key: Octs, +} + +//------------ Ipseckey ------------------------------------------------------ + +impl Ipseckey<(), ()> { + /// The rtype of this record data type. + pub(crate) const RTYPE: Rtype = Rtype::IPSECKEY; +} + +impl Ipseckey { + /// Create a Ipseckey record data from provided parameters. + pub fn new( + precedence: u8, + gateway_type: IpseckeyGatewayType, + algorithm: IpseckeyAlgorithm, + gateway: IpseckeyGateway, + key: Octs, + ) -> Self { + Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + } + } + + /// Get the precedence field. + pub fn precedence(&self) -> u8 { + self.precedence + } + + /// Get the gateway type field. + pub fn gateway_type(&self) -> IpseckeyGatewayType { + self.gateway_type + } + + /// Get the public key algorithm field. + pub fn algorithm(&self) -> IpseckeyAlgorithm { + self.algorithm + } + + /// Get the gateway field. + pub fn gateway(&self) -> &IpseckeyGateway { + &self.gateway + } + + /// Get the public key field. + pub fn key(&self) -> &Octs { + &self.key + } + + /// Parse the record data from zonefile format. + pub fn scan>( + scanner: &mut S, + ) -> Result { + let precedence = u8::scan(scanner)?; + // Using u8::scan instead of Ipseckey{GatewayType,Algorithm}::scan to + // restrict the allowed input to integers and disallow mnemonics. + let gateway_type = u8::scan(scanner)?.into(); + let algorithm = u8::scan(scanner)?.into(); + let gateway = IpseckeyGateway::scan(scanner, gateway_type)?; + let key = scanner.convert_entry(base64::SymbolConverter::new())?; + + Ok(Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + }) + } + + pub(super) fn flatten( + self, + ) -> Result, N::AppendError> + where + TargetOcts: OctetsFrom, + N: FlattenInto, + { + let Ipseckey { + precedence, + gateway_type, + algorithm, + gateway, + key, + } = self; + + Ok(Ipseckey { + precedence, + gateway_type, + algorithm, + gateway: gateway.flatten()?, + key: key.try_octets_into()?, + }) + } + + pub(super) fn convert_octets( + self, + ) -> Result, TargetOcts::Error> + where + TargetOcts: OctetsFrom, + TargetName: OctetsFrom, + { + let Ipseckey { + precedence, + gateway_type, + algorithm, + gateway, + key, + } = self; + + Ok(Ipseckey { + precedence, + gateway_type, + algorithm, + gateway: gateway.convert_octets()?, + key: key.try_octets_into()?, + }) + } + +} + +impl Ipseckey> { + /// Parse the record data from wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + let precedence = parser.parse_u8()?; + let gateway_type = IpseckeyGatewayType::parse(parser)?; + let algorithm = IpseckeyAlgorithm::parse(parser)?; + let gateway = IpseckeyGateway::parse(parser, gateway_type)?; + let len_key = parser.remaining(); + let key = parser.parse_octets(len_key)?; + Ok(Self { + precedence, + gateway_type, + algorithm, + gateway, + key, + }) + } +} + +impl RecordData for Ipseckey { + fn rtype(&self) -> Rtype { + Ipseckey::RTYPE + } +} + +impl, N: ToName> ComposeRecordData for Ipseckey { + fn rdlen(&self, _compress: bool) -> Option { + Some( + // precedence=1 + gateway_type=1 + algorithm=1 + gateway + key + u16::try_from( + 1 + 1 + + 1 + + self.gateway.len() as usize + + self.key.as_ref().len(), + ) + .expect("long IPSECKEY rdata"), + ) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + target.append_slice(&[self.precedence.into()])?; + target.append_slice(&[self.gateway_type.into()])?; + target.append_slice(&[self.algorithm.into()])?; + self.gateway.compose_rdata(target)?; + target.append_slice(self.key.as_ref()) + } + + fn compose_canonical_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> { + self.compose_rdata(target) + } +} + +impl, N: hash::Hash> hash::Hash for Ipseckey { + fn hash(&self, state: &mut H) { + self.precedence.hash(state); + self.gateway_type.hash(state); + self.algorithm.hash(state); + self.gateway.hash(state); + self.key.as_ref().hash(state); + } +} + +impl PartialEq> + for Ipseckey +where + Octs: AsRef<[u8]> + ?Sized, + OtherOcts: AsRef<[u8]> + ?Sized, + N: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &Ipseckey) -> bool { + self.precedence.eq(&other.precedence) + && self.gateway_type.eq(&other.gateway_type) + && self.algorithm.eq(&other.algorithm) + && self.gateway.eq(&other.gateway) + && self.key.as_ref().eq(other.key.as_ref()) + } +} + +impl + ?Sized, N: ToName> Eq for Ipseckey {} + +impl, N: fmt::Display> fmt::Display for Ipseckey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} {} {} {} ( ", + self.precedence, + u8::from(self.gateway_type), + u8::from(self.algorithm), + self.gateway, + )?; + base64::display(&self.key, f)?; + write!(f, " )") + } +} + +impl, N: fmt::Debug> fmt::Debug for Ipseckey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // f.write_str("Ipseckey(")?; + // fmt::Display::fmt(self, f)?; + // f.write_str(")") + f.debug_struct("Ipseckey") + .field("precedence", &self.precedence) + .field("gateway_type", &self.gateway_type) + .field("algorithm", &self.algorithm) + .field("gateway", &self.gateway) + .field("key", &base64::encode_string(&self.key)) + .finish() + } +} + +impl, N: ToName> ZonefileFmt for Ipseckey { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + p.block(|p| { + p.write_token(self.precedence)?; + p.write_comment("precedence")?; + p.write_show(self.gateway_type)?; + p.write_comment("gateway type")?; + p.write_show(self.algorithm)?; + p.write_comment("algorithm")?; + p.write_show(&self.gateway)?; + p.write_comment("gateway")?; + p.write_token(base64::encode_display(&self.key)) + }) + } +} + +impl PartialOrd> + for Ipseckey +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + N: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &Ipseckey, + ) -> Option { + match self.precedence.partial_cmp(&other.precedence) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.gateway_type.partial_cmp(&other.gateway_type) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.algorithm.partial_cmp(&other.algorithm) { + Some(Ordering::Equal) => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + other => return other, + } + self.key.as_ref().partial_cmp(other.key.as_ref()) + } +} + +impl + CanonicalOrd> for Ipseckey +where + Octs: AsRef<[u8]>, + OtherOcts: AsRef<[u8]>, + N: ToName, + OtherName: ToName, +{ + fn canonical_cmp( + &self, + other: &Ipseckey, + ) -> Ordering { + match self.precedence.cmp(&other.precedence) { + Ordering::Equal => {} + other => return other, + } + match self.gateway_type.cmp(&other.gateway_type) { + Ordering::Equal => {} + other => return other, + } + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + Some(other) => return other, + None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"), + } + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +impl, N: ToName> Ord for Ipseckey { + fn cmp(&self, other: &Self) -> Ordering { + match self.precedence.cmp(&other.precedence) { + Ordering::Equal => {} + other => return other, + } + match self.gateway_type.cmp(&other.gateway_type) { + Ordering::Equal => {} + other => return other, + } + match self.algorithm.cmp(&other.algorithm) { + Ordering::Equal => {} + other => return other, + } + match self.gateway.partial_cmp(&other.gateway) { + Some(Ordering::Equal) => {} + Some(other) => return other, + None => unreachable!("The gateway will be the same variant and therefore have an ordering, because the gateway_type above was Equal"), + } + self.key.as_ref().cmp(other.key.as_ref()) + } +} + +//------------ IpseckeyGateway ----------------------------------------------- + +#[derive(Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub enum IpseckeyGateway { + None, + Ipv4(A), + Ipv6(Aaaa), + Name(N), +} + +impl IpseckeyGateway { + pub fn len(&self) -> u16 + where + N: ToName, + { + match self { + IpseckeyGateway::None => 0, + IpseckeyGateway::Ipv4(_) => 4, + IpseckeyGateway::Ipv6(_) => 16, + IpseckeyGateway::Name(n) => n.compose_len(), + } + } + + pub fn scan>( + scanner: &mut S, + gateway_type: IpseckeyGatewayType, + ) -> Result { + Ok(match gateway_type { + IpseckeyGatewayType::NONE => Self::None, + IpseckeyGatewayType::IPV4 => Self::Ipv4(A::scan(scanner)?), + IpseckeyGatewayType::IPV6 => Self::Ipv6(Aaaa::scan(scanner)?), + IpseckeyGatewayType::NAME => Self::Name(scanner.scan_name()?), + _ => { + return Err(ScannerError::custom( + "Unknown IPSECKEY gateway type", + )) + } + }) + } + + pub(super) fn flatten( + self, + ) -> Result, N::AppendError> + where + N: FlattenInto, + { + Ok(match self { + IpseckeyGateway::None => IpseckeyGateway::None, + IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a), + IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa), + IpseckeyGateway::Name(n) => { + IpseckeyGateway::Name(n.try_flatten_into()?) + } + }) + } + + pub(super) fn convert_octets>( + self, + ) -> Result, Target::Error> { + Ok(match self { + IpseckeyGateway::None => IpseckeyGateway::None, + IpseckeyGateway::Ipv4(a) => IpseckeyGateway::Ipv4(a), + IpseckeyGateway::Ipv6(aaaa) => IpseckeyGateway::Ipv6(aaaa), + IpseckeyGateway::Name(n) => { + IpseckeyGateway::Name(n.try_octets_into()?) + } + }) + } + + fn compose_rdata( + &self, + target: &mut Target, + ) -> Result<(), Target::AppendError> + where + N: ToName, + { + Ok(match self { + IpseckeyGateway::None => (), + IpseckeyGateway::Ipv4(a) => a.compose_rdata(target)?, + IpseckeyGateway::Ipv6(aaaa) => aaaa.compose_rdata(target)?, + IpseckeyGateway::Name(n) => n.compose(target)?, + }) + } +} + +impl hash::Hash for IpseckeyGateway { + fn hash(&self, state: &mut H) { + match self { + IpseckeyGateway::None => todo!(), + IpseckeyGateway::Ipv4(a) => a.hash(state), + IpseckeyGateway::Ipv6(aaaa) => aaaa.hash(state), + IpseckeyGateway::Name(n) => n.hash(state), + } + } +} + +impl PartialEq> + for IpseckeyGateway +where + N: ToName, + OtherName: ToName, +{ + fn eq(&self, other: &IpseckeyGateway) -> bool { + match (self, other) { + (IpseckeyGateway::None, IpseckeyGateway::None) => true, + (IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => a.eq(o), + (IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => { + aaaa.eq(o) + } + (IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => { + n.name_eq(o) + } + _ => false, + } + } +} + +impl PartialOrd> + for IpseckeyGateway +where + N: ToName, + OtherName: ToName, +{ + fn partial_cmp( + &self, + other: &IpseckeyGateway, + ) -> Option { + match (self, other) { + (IpseckeyGateway::None, IpseckeyGateway::None) => { + Some(Ordering::Equal) + } + (IpseckeyGateway::Ipv4(a), IpseckeyGateway::Ipv4(o)) => { + a.partial_cmp(o) + } + (IpseckeyGateway::Ipv6(aaaa), IpseckeyGateway::Ipv6(o)) => { + aaaa.partial_cmp(o) + } + (IpseckeyGateway::Name(n), IpseckeyGateway::Name(o)) => { + Some(n.name_cmp(o)) + } + _ => None, + } + } +} + +impl IpseckeyGateway> { + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + gateway_type: IpseckeyGatewayType, + ) -> Result { + let len_gateway = match gateway_type { + IpseckeyGatewayType::NONE => Some(0), + IpseckeyGatewayType::IPV4 => Some(4), + IpseckeyGatewayType::IPV6 => Some(16), + IpseckeyGatewayType::NAME => None, + _ => { + return Err(ParseError::Form(FormError::new( + "Unknown IPSECKEY gateway type", + ))) + } + }; + let remaining = parser.remaining(); + let gateway = if let Some(len_gateway) = len_gateway { + if remaining < len_gateway { + return Err(ParseError::ShortInput); + } + match gateway_type { + IpseckeyGatewayType::NONE => IpseckeyGateway::None, + IpseckeyGatewayType::IPV4 => { + IpseckeyGateway::Ipv4(A::parse(parser)?) + } + IpseckeyGatewayType::IPV6 => { + IpseckeyGateway::Ipv6(Aaaa::parse(parser)?) + } + _ => unreachable!(), + } + } else { + // Minimal length unknown, it contains a domain name + let name = ParsedName::parse(parser)?; + if name.is_compressed() { + return Err(ParseError::Form(FormError::new( + "IPSECKEY gateway contains compressed name", + ))); + } + IpseckeyGateway::Name(name) + }; + Ok(gateway) + } +} + +impl ZonefileFmt for IpseckeyGateway { + fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { + Ok(match self { + IpseckeyGateway::None => (), + IpseckeyGateway::Ipv4(a) => p.write_show(a)?, + IpseckeyGateway::Ipv6(aaaa) => p.write_show(aaaa)?, + IpseckeyGateway::Name(n) => p.write_token(n.fmt_with_dot())?, + }) + } +} + +impl fmt::Display for IpseckeyGateway { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpseckeyGateway::None => Ok(()), + IpseckeyGateway::Ipv4(a) => write!(f, "{a}"), + IpseckeyGateway::Ipv6(aaaa) => write!(f, "{aaaa}"), + IpseckeyGateway::Name(n) => write!(f, "{n}"), + } + } +} + +impl fmt::Debug for IpseckeyGateway { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + IpseckeyGateway::None => write!(f, "IpseckeyGateway::None"), + IpseckeyGateway::Ipv4(a) => write!(f, "IpseckeyGateway::Ipv4({a:?})"), + IpseckeyGateway::Ipv6(aaaa) => write!(f, "IpseckeyGateway::Ipv6({aaaa:?})"), + IpseckeyGateway::Name(n) => write!(f, "IpseckeyGateway::Name({n:?})"), + } + } +} + +#[cfg(test)] +#[cfg(all(feature = "std", feature = "bytes"))] +mod test { + use super::*; + use crate::base::rdata::test::{ + test_compose_parse, test_rdlen, test_scan, + }; + use crate::utils::base16::decode; + use std::string::ToString; + use std::vec::Vec; + + #[test] + #[allow(clippy::redundant_closure)] // lifetimes ... + fn ipseckey_compose_parse_scan() { + let serial = 2023092203; + let scheme = 1.into(); + let algo = 241.into(); + let digest_str = "CDBE0DED9484490493580583BF868A3E95F89FC3515BF26ADBD230A6C23987F36BC6E504EFC83606F9445476D4E57FFB"; + let digest: Vec = decode(digest_str).unwrap(); + let rdata = Ipseckey::new(serial.into(), scheme, algo, digest); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + test_scan( + &[ + &serial.to_string(), + &u8::from(scheme).to_string(), + &u8::from(algo).to_string(), + digest_str, + ], + Ipseckey::scan, + &rdata, + ); + } + + #[cfg(feature = "zonefile")] + #[test] + fn ipseckey_parse_zonefile() { + use crate::base::iana::IpseckeyAlgorithm; + use crate::base::Name; + use crate::rdata::ZoneRecordData; + use crate::zonefile::inplace::{Entry, Zonefile}; + + // section A.1 + let content = r#" +example. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 + 86400 IN IPSECKEY 2018031900 1 1 ( + c68090d90a7aed71 + 6bc459f9340e3d7c + 1370d4d24b7e2fc3 + a1ddc0b9a87153b9 + a9713b3c9ae5cc27 + 777f98b8e730044c ) +ns1 3600 IN A 203.0.113.63 +ns2 3600 IN AAAA 2001:db8::63 +"#; + + let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); + zone.set_origin(Name::root()); + while let Some(entry) = zone.next_entry().unwrap() { + match entry { + Entry::Record(record) => { + if record.rtype() != Rtype::IPSECKEY { + continue; + } + match record.into_data() { + ZoneRecordData::Ipseckey(rd) => { + assert_eq!(2018031900, rd.serial().into_int()); + assert_eq!(IpseckeyScheme::SIMPLE, rd.scheme()); + assert_eq!( + IpseckeyAlgorithm::SHA384, + rd.algorithm() + ); + } + _ => panic!(), + } + } + _ => panic!(), + } + } + } +} diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 77280f76d..86c2fd56d 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -49,6 +49,7 @@ pub mod aaaa; pub mod cds; pub mod dname; pub mod dnssec; +pub mod ipseckey; pub mod naptr; pub mod nsec3; pub mod openpgpkey; @@ -120,6 +121,11 @@ rdata_types! { Ds, } } + ipseckey::{ + zone { + Ipseckey, + } + } naptr::{ zone { Naptr, From 96d1b7c9cbc64b535ea9088aae17e9dc986d9a3c Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 12:15:49 +0200 Subject: [PATCH 533/569] Cargo clippy and fmt --- src/rdata/ipseckey.rs | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index aea2f5ca1..01fee0650 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -168,7 +168,7 @@ impl Ipseckey { pub(super) fn convert_octets( self, - ) -> Result, TargetOcts::Error> + ) -> Result, TargetOcts::Error> where TargetOcts: OctetsFrom, TargetName: OctetsFrom, @@ -189,7 +189,6 @@ impl Ipseckey { key: key.try_octets_into()?, }) } - } impl Ipseckey> { @@ -226,7 +225,7 @@ impl, N: ToName> ComposeRecordData for Ipseckey { u16::try_from( 1 + 1 + 1 - + self.gateway.len() as usize + + self.gateway.rdlen() as usize + self.key.as_ref().len(), ) .expect("long IPSECKEY rdata"), @@ -237,7 +236,7 @@ impl, N: ToName> ComposeRecordData for Ipseckey { &self, target: &mut Target, ) -> Result<(), Target::AppendError> { - target.append_slice(&[self.precedence.into()])?; + target.append_slice(&[self.precedence])?; target.append_slice(&[self.gateway_type.into()])?; target.append_slice(&[self.algorithm.into()])?; self.gateway.compose_rdata(target)?; @@ -427,7 +426,7 @@ pub enum IpseckeyGateway { } impl IpseckeyGateway { - pub fn len(&self) -> u16 + pub fn rdlen(&self) -> u16 where N: ToName, { @@ -492,12 +491,13 @@ impl IpseckeyGateway { where N: ToName, { - Ok(match self { + match self { IpseckeyGateway::None => (), IpseckeyGateway::Ipv4(a) => a.compose_rdata(target)?, IpseckeyGateway::Ipv6(aaaa) => aaaa.compose_rdata(target)?, IpseckeyGateway::Name(n) => n.compose(target)?, - }) + }; + Ok(()) } } @@ -608,12 +608,13 @@ impl IpseckeyGateway> { impl ZonefileFmt for IpseckeyGateway { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { - Ok(match self { + match self { IpseckeyGateway::None => (), IpseckeyGateway::Ipv4(a) => p.write_show(a)?, IpseckeyGateway::Ipv6(aaaa) => p.write_show(aaaa)?, IpseckeyGateway::Name(n) => p.write_token(n.fmt_with_dot())?, - }) + }; + Ok(()) } } @@ -632,9 +633,15 @@ impl fmt::Debug for IpseckeyGateway { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { IpseckeyGateway::None => write!(f, "IpseckeyGateway::None"), - IpseckeyGateway::Ipv4(a) => write!(f, "IpseckeyGateway::Ipv4({a:?})"), - IpseckeyGateway::Ipv6(aaaa) => write!(f, "IpseckeyGateway::Ipv6({aaaa:?})"), - IpseckeyGateway::Name(n) => write!(f, "IpseckeyGateway::Name({n:?})"), + IpseckeyGateway::Ipv4(a) => { + write!(f, "IpseckeyGateway::Ipv4({a:?})") + } + IpseckeyGateway::Ipv6(aaaa) => { + write!(f, "IpseckeyGateway::Ipv6({aaaa:?})") + } + IpseckeyGateway::Name(n) => { + write!(f, "IpseckeyGateway::Name({n:?})") + } } } } From 7bc8d9adcbe416c506bd106b2df6ba87804e5450 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 12:16:26 +0200 Subject: [PATCH 534/569] Add tests for IPSECKEY --- src/rdata/ipseckey.rs | 279 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 227 insertions(+), 52 deletions(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index 01fee0650..cb927fd56 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -29,6 +29,19 @@ use super::{Aaaa, A}; /// associated with a domain name for use with the IPsec protocol suite. #[derive(Clone)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "serde", + serde(bound( + serialize = " + N: serde::Serialize, + Octs: octseq::serde::SerializeOctets + ", + deserialize = " + N: serde::Deserialize<'de>, + Octs: octseq::serde::DeserializeOctets<'de> + ", + )) +)] pub struct Ipseckey { precedence: u8, gateway_type: IpseckeyGatewayType, @@ -43,17 +56,6 @@ pub struct Ipseckey { /// - A normal wire-encoded domain name, always uncompressed. /// /// May be empty if the gateway type is IpseckeyGatewayType::NONE. - #[cfg_attr( - feature = "serde", - serde( - serialize_with = "octseq::serde::SerializeOctets::serialize_octets", - deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", - bound( - serialize = "Octs: octseq::serde::SerializeOctets", - deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", - ) - ) - )] gateway: IpseckeyGateway, // May be zero bytes long @@ -62,10 +64,6 @@ pub struct Ipseckey { serde( serialize_with = "octseq::serde::SerializeOctets::serialize_octets", deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets", - bound( - serialize = "Octs: octseq::serde::SerializeOctets", - deserialize = "Octs: octseq::serde::DeserializeOctets<'de>", - ) ) )] key: Octs, @@ -87,6 +85,7 @@ impl Ipseckey { gateway: IpseckeyGateway, key: Octs, ) -> Self { + // TODO: Check if gateway_type and gateway variant are compatible? Self { precedence, gateway_type, @@ -438,12 +437,30 @@ impl IpseckeyGateway { } } + pub fn is_correct_gateway_type(&self, gwt: IpseckeyGatewayType) -> bool { + match (self, gwt) { + (IpseckeyGateway::None, IpseckeyGatewayType::NONE) => true, + (IpseckeyGateway::Ipv4(_), IpseckeyGatewayType::IPV4) => true, + (IpseckeyGateway::Ipv6(_), IpseckeyGatewayType::IPV6) => true, + (IpseckeyGateway::Name(_), IpseckeyGatewayType::NAME) => true, + _ => false, + } + } + pub fn scan>( scanner: &mut S, gateway_type: IpseckeyGatewayType, ) -> Result { Ok(match gateway_type { - IpseckeyGatewayType::NONE => Self::None, + IpseckeyGatewayType::NONE => { + scanner.scan_ascii_str(|s| { + if s == "." { + return Ok(Self::None) + } else { + return Err(ScannerError::custom("Invalid IPSECKEY gateway. As the gateway type is specified as 0 (None), the gateway MUST be set to '.'")) + } + })? + }, IpseckeyGatewayType::IPV4 => Self::Ipv4(A::scan(scanner)?), IpseckeyGatewayType::IPV6 => Self::Ipv6(Aaaa::scan(scanner)?), IpseckeyGatewayType::NAME => Self::Name(scanner.scan_name()?), @@ -621,7 +638,7 @@ impl ZonefileFmt for IpseckeyGateway { impl fmt::Display for IpseckeyGateway { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - IpseckeyGateway::None => Ok(()), + IpseckeyGateway::None => write!(f, "."), IpseckeyGateway::Ipv4(a) => write!(f, "{a}"), IpseckeyGateway::Ipv6(aaaa) => write!(f, "{aaaa}"), IpseckeyGateway::Name(n) => write!(f, "{n}"), @@ -653,60 +670,222 @@ mod test { use crate::base::rdata::test::{ test_compose_parse, test_rdlen, test_scan, }; - use crate::utils::base16::decode; + use crate::base::Name; + use crate::utils::base64::decode; + use core::str::FromStr; + use std::net::{Ipv4Addr, Ipv6Addr}; use std::string::ToString; use std::vec::Vec; #[test] - #[allow(clippy::redundant_closure)] // lifetimes ... + // allow redundant_closure because of lifetime shenanigans + // in test_compose_parse(...::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] fn ipseckey_compose_parse_scan() { - let serial = 2023092203; - let scheme = 1.into(); - let algo = 241.into(); - let digest_str = "CDBE0DED9484490493580583BF868A3E95F89FC3515BF26ADBD230A6C23987F36BC6E504EFC83606F9445476D4E57FFB"; - let digest: Vec = decode(digest_str).unwrap(); - let rdata = Ipseckey::new(serial.into(), scheme, algo, digest); - test_rdlen(&rdata); - test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + // From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2 + // IPSECKEY ( 10 1 2 192.0.2.38 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 0 2 . AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 1 2 192.0.2.3 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 3 2 mygateway.example.com. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + // IPSECKEY ( 10 2 2 2001:0DB8:0:8002::2000:1 AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + for (precedence, gateway_type, algorithm, gateway_str, gateway) in [ + ( + 10, + 1.into(), + 2.into(), + "192.0.2.38", + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 38).into(), + ), + ), + ( + 10, + 0.into(), + 2.into(), + ".", + IpseckeyGateway::>>::None, + ), + ( + 10, + 1.into(), + 2.into(), + "192.0.2.3", + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 3).into(), + ), + ), + ( + 10, + 3.into(), + 2.into(), + "mygateway.example.com.", + IpseckeyGateway::>>::Name( + Name::from_str("mygateway.example.com.").unwrap(), + ), + ), + ( + 10, + 2.into(), + 2.into(), + "2001:0DB8:0:8002::2000:1", + IpseckeyGateway::>>::Ipv6( + Ipv6Addr::new( + 0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1, + ) + .into(), + ), + ), + ] { + let rdata = Ipseckey::new( + precedence.into(), + gateway_type, + algorithm, + gateway, + &key, + ); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + test_scan( + &[ + &precedence.to_string(), + &u8::from(gateway_type).to_string(), + &u8::from(algorithm).to_string(), + gateway_str, + key_str, + ], + Ipseckey::scan, + &rdata, + ); + } + } + + #[test] + #[should_panic] + // allow redundant_closure because of lifetime shenanigans + // in test_compose_parse(...::parse), "FnOnce is not general enough" + #[allow(clippy::redundant_closure)] + fn ipseckey_scan_wrong_gateway() { + // IPSECKEY ( 10 0 2 this.should.be.just.dot. AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + let precedence = 10; + let gateway_type = 0.into(); + let algorithm = 2.into(); + let wrong_gateway_str = "this.should.be.just.dot."; + // let wrong_gateway: IpseckeyGateway>> = + // IpseckeyGateway::Name(Name::from_str(wrong_gateway_str).unwrap()); + let correct_gateway = IpseckeyGateway::>>::None; + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + let correct_rdata = Ipseckey::new( + precedence.into(), + gateway_type, + algorithm, + correct_gateway, + key, + ); + // This should panic in the unwrap within test_scan test_scan( &[ - &serial.to_string(), - &u8::from(scheme).to_string(), - &u8::from(algo).to_string(), - digest_str, + &precedence.to_string(), + &u8::from(gateway_type).to_string(), + &u8::from(algorithm).to_string(), + wrong_gateway_str, + key_str, ], Ipseckey::scan, - &rdata, + &correct_rdata, ); } #[cfg(feature = "zonefile")] #[test] fn ipseckey_parse_zonefile() { - use crate::base::iana::IpseckeyAlgorithm; - use crate::base::Name; use crate::rdata::ZoneRecordData; use crate::zonefile::inplace::{Entry, Zonefile}; - // section A.1 + // From https://www.rfc-editor.org/rfc/rfc4025.html#section-3.2 let content = r#" -example. 86400 IN SOA ns1 admin 2018031900 ( - 1800 900 604800 86400 ) - 86400 IN NS ns1 - 86400 IN NS ns2 - 86400 IN IPSECKEY 2018031900 1 1 ( - c68090d90a7aed71 - 6bc459f9340e3d7c - 1370d4d24b7e2fc3 - a1ddc0b9a87153b9 - a9713b3c9ae5cc27 - 777f98b8e730044c ) +arpa. 86400 IN SOA ns1 admin 2018031900 ( + 1800 900 604800 86400 ) + 86400 IN NS ns1 + 86400 IN NS ns2 ns1 3600 IN A 203.0.113.63 ns2 3600 IN AAAA 2001:db8::63 + +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2 192.0.2.38 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 0 2 . + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +38.2.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 1 2 + 192.0.2.3 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +38.1.0.192.in-addr.arpa. 7200 IN IPSECKEY ( 10 3 2 + mygateway.example.com. + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) + +$ORIGIN 1.0.0.0.0.0.2.8.B.D.0.1.0.0.2.ip6.arpa. +0.d.4.0.3.0.e.f.f.f.3.f.0.1.2.0 7200 IN IPSECKEY ( 10 2 2 + 2001:0DB8:0:8002::2000:1 + AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ== ) "#; let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap(); zone.set_origin(Name::root()); + let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; + let key: Vec = decode(key_str).unwrap(); + let expected_ipseckeys = vec![ + Ipseckey::new( + 10, + 1.into(), + 2.into(), + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 38).into(), + ), + &key, + ), + Ipseckey::new( + 10, + 0.into(), + 2.into(), + IpseckeyGateway::>>::None, + &key, + ), + Ipseckey::new( + 10, + 1.into(), + 2.into(), + IpseckeyGateway::>>::Ipv4( + Ipv4Addr::new(192, 0, 2, 3).into(), + ), + &key, + ), + Ipseckey::new( + 10, + 3.into(), + 2.into(), + IpseckeyGateway::>>::Name( + Name::from_str("mygateway.example.com.").unwrap(), + ), + &key, + ), + Ipseckey::new( + 10, + 2.into(), + 2.into(), + IpseckeyGateway::>>::Ipv6( + Ipv6Addr::new( + 0x2001, 0x0DB8, 0x0, 0x8002, 0x0, 0x0, 0x2000, 0x1, + ) + .into(), + ), + &key, + ), + ]; + let mut expected_idx = 0; while let Some(entry) = zone.next_entry().unwrap() { match entry { Entry::Record(record) => { @@ -715,12 +894,8 @@ ns2 3600 IN AAAA 2001:db8::63 } match record.into_data() { ZoneRecordData::Ipseckey(rd) => { - assert_eq!(2018031900, rd.serial().into_int()); - assert_eq!(IpseckeyScheme::SIMPLE, rd.scheme()); - assert_eq!( - IpseckeyAlgorithm::SHA384, - rd.algorithm() - ); + assert_eq!(expected_ipseckeys[expected_idx], rd); + expected_idx += 1; } _ => panic!(), } From d3f66e27424aefd67951e8a768ed3e4a744843c8 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 12:34:32 +0200 Subject: [PATCH 535/569] Add key length checks for IPSECKEY --- src/rdata/ipseckey.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index cb927fd56..d4e821afe 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -123,7 +123,10 @@ impl Ipseckey { /// Parse the record data from zonefile format. pub fn scan>( scanner: &mut S, - ) -> Result { + ) -> Result + where + Octs: AsRef<[u8]>, + { let precedence = u8::scan(scanner)?; // Using u8::scan instead of Ipseckey{GatewayType,Algorithm}::scan to // restrict the allowed input to integers and disallow mnemonics. @@ -131,6 +134,9 @@ impl Ipseckey { let algorithm = u8::scan(scanner)?.into(); let gateway = IpseckeyGateway::scan(scanner, gateway_type)?; let key = scanner.convert_entry(base64::SymbolConverter::new())?; + if key.as_ref().is_empty() && algorithm != IpseckeyAlgorithm::NONE { + return Err(ScannerError::custom("Missing IPSECKEY public key field. The public key field may only be omitted when the algorithm is specified as 0")); + } Ok(Self { precedence, @@ -200,6 +206,9 @@ impl Ipseckey> { let algorithm = IpseckeyAlgorithm::parse(parser)?; let gateway = IpseckeyGateway::parse(parser, gateway_type)?; let len_key = parser.remaining(); + if len_key == 0 && algorithm != IpseckeyAlgorithm::NONE { + return Err(ParseError::ShortInput); + } let key = parser.parse_octets(len_key)?; Ok(Self { precedence, @@ -760,6 +769,22 @@ mod test { &rdata, ); } + + // IPSECKEY ( 10 0 0 . ) + let rdata = Ipseckey::new( + 10, + 0.into(), + 0.into(), + IpseckeyGateway::>>::None, + &[], + ); + test_rdlen(&rdata); + test_compose_parse(&rdata, |parser| Ipseckey::parse(parser)); + test_scan( + &[&10.to_string(), &0.to_string(), &0.to_string(), "."], + Ipseckey::scan, + &rdata, + ); } #[test] From 1404cd61e352c964a2797b3e18773531629117af Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 12:34:47 +0200 Subject: [PATCH 536/569] Update comments and clippy --- src/rdata/ipseckey.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index d4e821afe..28c64a2cc 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -54,8 +54,8 @@ pub struct Ipseckey { /// - IPv4 address: This is a 32-bit number in network byte order. /// - IPv6 address: This is a 128-bit number in network byte order. /// - A normal wire-encoded domain name, always uncompressed. - /// - /// May be empty if the gateway type is IpseckeyGatewayType::NONE. + /// - The domain MUST equal '.' if the gateway type is + /// IpseckeyGatewayType::NONE gateway: IpseckeyGateway, // May be zero bytes long @@ -447,13 +447,13 @@ impl IpseckeyGateway { } pub fn is_correct_gateway_type(&self, gwt: IpseckeyGatewayType) -> bool { - match (self, gwt) { - (IpseckeyGateway::None, IpseckeyGatewayType::NONE) => true, - (IpseckeyGateway::Ipv4(_), IpseckeyGatewayType::IPV4) => true, - (IpseckeyGateway::Ipv6(_), IpseckeyGatewayType::IPV6) => true, - (IpseckeyGateway::Name(_), IpseckeyGatewayType::NAME) => true, - _ => false, - } + matches!( + (self, gwt), + (IpseckeyGateway::None, IpseckeyGatewayType::NONE) + | (IpseckeyGateway::Ipv4(_), IpseckeyGatewayType::IPV4) + | (IpseckeyGateway::Ipv6(_), IpseckeyGatewayType::IPV6) + | (IpseckeyGateway::Name(_), IpseckeyGatewayType::NAME) + ) } pub fn scan>( @@ -464,9 +464,9 @@ impl IpseckeyGateway { IpseckeyGatewayType::NONE => { scanner.scan_ascii_str(|s| { if s == "." { - return Ok(Self::None) + Ok(Self::None) } else { - return Err(ScannerError::custom("Invalid IPSECKEY gateway. As the gateway type is specified as 0 (None), the gateway MUST be set to '.'")) + Err(ScannerError::custom("Invalid IPSECKEY gateway. As the gateway type is specified as 0 (None), the gateway MUST be set to '.'")) } })? }, @@ -749,7 +749,7 @@ mod test { ), ] { let rdata = Ipseckey::new( - precedence.into(), + precedence, gateway_type, algorithm, gateway, From 2b1d961c03baf0ee742f6d241c1ea1e67e54656d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:26:04 +0200 Subject: [PATCH 537/569] Minor code re-ordering to make TCP and UDP handling the same. --- src/net/server/connection.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index 772fa183d..e90a42a87 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -645,13 +645,13 @@ where Ok(buf) => { let received_at = Instant::now(); + self.metrics.inc_num_received_requests(); + if log_enabled!(Level::Trace) { let pcap_text = to_pcap_text(&buf, buf.as_ref().len()); trace!(addr = %self.addr, pcap_text, "Received message"); } - self.metrics.inc_num_received_requests(); - // Message received, reset the DNS idle timer self.idle_timer.full_msg_received(); @@ -696,6 +696,7 @@ where request.message().header().id() ); tokio::spawn(async move { + trace!("Task spawned to handle message"); let request_id = request.message().header().id(); trace!( "Calling service for request id {request_id}" From 8c0198acd7e7f69d94ba8fcd0806b493931dec19 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:30:00 +0200 Subject: [PATCH 538/569] Add equivalent logging in the Dgram server as in the Stream server connection handler. And add a task started log which helps to detect sockets which have not been put in the non-blocking state. --- src/net/server/connection.rs | 8 ++++++++ src/net/server/dgram.rs | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index e90a42a87..6bf2bb364 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -696,7 +696,15 @@ where request.message().header().id() ); tokio::spawn(async move { + // If we don't see the next log message it may be + // because the stream listener was originally an + // std listener, not a Tokio listener, and it was + // not properly put in non-blocking mode before + // being passed to us. This then causes .recv() + // above to block, preventing this task from + // running if it is scheduled on the same thread. trace!("Task spawned to handle message"); + let request_id = request.message().header().id(); trace!( "Calling service for request id {request_id}" diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index bab33cda1..9344cd0b4 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -451,6 +451,7 @@ where let mut command_rx = self.command_rx.clone(); loop { + trace!("Dgram server loop"); tokio::select! { // Poll futures in match arm order, not randomly. biased; @@ -482,7 +483,17 @@ where let cloned_sock = self.sock.clone(); let write_timeout = self.config.load().write_timeout; + trace!("Spawning task to handle new message"); tokio::spawn(async move { + // If we don't see the next log message it may be + // because the stream listener was originally an std + // listener, not a Tokio listener, and it was not + // properly put in non-blocking mode before being + // passed to us. This then causes .recv() above to + // block, preventing this task from running if it is + // scheduled on the same thread. + trace!("Task spawned to handle message"); + match Message::from_octets(buf) { Err(err) => { // TO DO: Count this event? @@ -500,11 +511,21 @@ where } Ok(msg) => { + trace!("Message decoded"); let ctx = UdpTransportContext::new(cfg.load().max_response_size); let ctx = TransportSpecificContext::Udp(ctx); let request = Request::new(addr, received_at, msg, ctx, ()); + + let request_id = request.message().header().id(); + trace!( + "Calling service for request id {request_id}" + ); + let mut stream = svc.call(request).await; + + trace!("Awaiting service call results for request id {request_id}"); while let Some(Ok(call_result)) = stream.next().await { + trace!("Processing service call result for request id {request_id}"); let (response, feedback) = call_result.into_inner(); if let Some(feedback) = feedback { @@ -552,6 +573,7 @@ where metrics.inc_num_sent_responses(); } } + trace!("Finished processing service call results for request id {request_id}"); } } }); From acf3936448af51d6ffc960e74795c41fa8c3d0b0 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 10 Sep 2025 13:50:26 +0200 Subject: [PATCH 539/569] Remove left behind diagnostic logging. --- src/net/server/dgram.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 9344cd0b4..5c0de428e 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -451,7 +451,6 @@ where let mut command_rx = self.command_rx.clone(); loop { - trace!("Dgram server loop"); tokio::select! { // Poll futures in match arm order, not randomly. biased; From 0887c20de3ae675d7bbef20f2cbb77ebde52804a Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 13:59:16 +0200 Subject: [PATCH 540/569] More clippy --- src/rdata/ipseckey.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index 28c64a2cc..ec1432284 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -804,7 +804,7 @@ mod test { let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; let key: Vec = decode(key_str).unwrap(); let correct_rdata = Ipseckey::new( - precedence.into(), + precedence, gateway_type, algorithm, correct_gateway, @@ -862,7 +862,7 @@ $ORIGIN 1.0.0.0.0.0.2.8.B.D.0.1.0.0.2.ip6.arpa. zone.set_origin(Name::root()); let key_str = "AQNRU3mG7TVTO2BkR47usntb102uFJtugbo6BSGvgqt4AQ=="; let key: Vec = decode(key_str).unwrap(); - let expected_ipseckeys = vec![ + let expected_ipseckeys = [ Ipseckey::new( 10, 1.into(), From 3998272f18805fd459fbe1703c6bf1d63dcf5be2 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 10 Sep 2025 14:05:14 +0200 Subject: [PATCH 541/569] Fix Debug of base64 key without std feature --- src/rdata/ipseckey.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/rdata/ipseckey.rs b/src/rdata/ipseckey.rs index ec1432284..ec6bd78b2 100644 --- a/src/rdata/ipseckey.rs +++ b/src/rdata/ipseckey.rs @@ -313,7 +313,10 @@ impl, N: fmt::Debug> fmt::Debug for Ipseckey { .field("gateway_type", &self.gateway_type) .field("algorithm", &self.algorithm) .field("gateway", &self.gateway) - .field("key", &base64::encode_string(&self.key)) + .field( + "key", + &format_args!("{}", base64::encode_display(&self.key)), + ) .finish() } } From e9b086bc1cb4fe487b6f955ba317706e5769d4a8 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:53:17 +0200 Subject: [PATCH 542/569] FIX: XfrMiddlewareService should always support at least one concurrent XFR. --- src/net/server/middleware/xfr/service.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/net/server/middleware/xfr/service.rs b/src/net/server/middleware/xfr/service.rs index 8f330eaba..83a8f262a 100644 --- a/src/net/server/middleware/xfr/service.rs +++ b/src/net/server/middleware/xfr/service.rs @@ -111,6 +111,9 @@ where xfr_data_provider: XDP, max_concurrency: usize, ) -> Self { + let max_concurrency = (max_concurrency > 0) + .then_some(max_concurrency) + .unwrap_or(1); let zone_walking_semaphore = Arc::new(Semaphore::new(max_concurrency)); let batcher_semaphore = Arc::new(Semaphore::new(max_concurrency)); From 289f2c1130b01e427635bc0dfd53e06eeb81839e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Wed, 10 Sep 2025 22:53:42 +0200 Subject: [PATCH 543/569] More trace level logging. --- src/net/server/connection.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index 6bf2bb364..c712601cd 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -716,10 +716,10 @@ where while let Some(Ok(call_result)) = stream.next().await { - trace!("Processing service call result for request id {request_id}"); let (response, feedback) = call_result.into_inner(); + trace!("Processing service call result for request id {request_id}: response? {} feedback? {feedback:?}", response.is_some()); if let Some(feedback) = feedback { match feedback { ServiceFeedback::Reconfigure { @@ -753,6 +753,7 @@ where if let Some(mut response) = response { loop { + trace!("Sending response"); match result_q_tx.try_send(response) { Ok(()) => { let pending_writes = @@ -790,6 +791,7 @@ where } } } + trace!("Finished processing service call result for request id {request_id}"); } trace!("Finished processing service call results for request id {request_id}"); }); From 3e132d188852ac98141cc49203976b9c3ce3e5bc Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 8 Sep 2025 16:06:33 +0200 Subject: [PATCH 544/569] Allow an optional TTL in public key files. --- src/dnssec/common.rs | 46 +++++++++++++++++-- .../dnssec-keys/Ktest-ttl.+008+60616.key | 1 + 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 test-data/dnssec-keys/Ktest-ttl.+008+60616.key diff --git a/src/dnssec/common.rs b/src/dnssec/common.rs index 3e4661d37..61c0d730e 100644 --- a/src/dnssec/common.rs +++ b/src/dnssec/common.rs @@ -6,7 +6,7 @@ #![warn(clippy::missing_docs_in_private_items)] use crate::base::iana::{Class, Nsec3HashAlgorithm}; -use crate::base::scan::{IterScanner, Scanner}; +use crate::base::scan::{IterScanner, Scanner, ScannerError}; use crate::base::wire::Composer; use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt}; use crate::base::{Name, Record, Rtype, ToName, Ttl}; @@ -19,6 +19,7 @@ use crate::rdata::{Dnskey, Nsec3param}; use std::error; use std::fmt; +use std::str::FromStr; //------------ Nsec3HashError ------------------------------------------------- @@ -203,7 +204,27 @@ where let name = scanner.scan_name().map_err(|_| ParseDnskeyTextError)?; - let _ = Class::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?; + // We can have an optional TTL here. Try to scan either a TTL or a + // Class. Return Some(ttl) if we found a TTL. Return None if we + // Successfully scanned a class. Otherwise return an error. + let opt_ttl = scanner + .scan_ascii_str(|s| { + if let Ok(ttl) = u32::from_str(s) { + Ok(Some(Ttl::from_secs(ttl))) + } else if Class::from_str(s).is_ok() { + Ok(None) + } else { + Err(ScannerError::custom("TTL or Class expected")) + } + }) + .map_err(|_| ParseDnskeyTextError)?; + + if opt_ttl.is_some() { + // The previous token was a TTL. If opt_ttl is None then the previous + // token was a class. + let _ = + Class::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?; + } if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) { return Err(ParseDnskeyTextError); @@ -212,7 +233,12 @@ where let data = Dnskey::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?; - Ok(Record::new(name, Class::IN, Ttl::ZERO, data)) + Ok(Record::new( + name, + Class::IN, + opt_ttl.unwrap_or(Ttl::ZERO), + data, + )) } //------------ format_as_bind ------------------------------------------------ @@ -332,6 +358,20 @@ mod test { } } + #[test] + fn test_parse_from_bind_ttl() { + for &(algorithm, key_tag, _) in + &[(SecurityAlgorithm::RSASHA256, 60616, 2048)] + { + let name = + format!("test-ttl.+{:03}+{:05}", algorithm.to_int(), key_tag); + + let path = format!("test-data/dnssec-keys/K{}.key", name); + let data = std::fs::read_to_string(path).unwrap(); + let _ = parse_from_bind::>(&data).unwrap(); + } + } + #[test] fn key_tag() { for &(algorithm, key_tag, _) in KEYS { diff --git a/test-data/dnssec-keys/Ktest-ttl.+008+60616.key b/test-data/dnssec-keys/Ktest-ttl.+008+60616.key new file mode 100644 index 000000000..e2ef7c9ba --- /dev/null +++ b/test-data/dnssec-keys/Ktest-ttl.+008+60616.key @@ -0,0 +1 @@ +test. 3600 IN DNSKEY 257 3 8 AwEAAdaxEmT1eAAnXMGDjYfivh6ax6BOESlNZY85BlVWkCOYV6jf5GcSgweqcCowFW2HtHKiE/FACwG5Wfq/xCDhLHYg4PQIvd5UcrDzj+WBEFe7pVhUjZrMsMRAVy2W4jliat6IrJv+CdycErp4cLxmqfNECIP7i9vI8onruvBe1YWebJN38TxdGCteg5waI27DNaQsXldxZoCfSY7Fkhj7BJ4XxHDeWzE876LmSMkkYFWqEQwesD280piL+4tmySMPxhVC1EUguQyn/Lc9FbEd3h1RyaO8hg8ub/70espLVElE9ImOibaY+gj9jK7HFD/mqdxYdFfr3yiQsGOt2ui4jGM= ;{id = 60616 (ksk), size = 2048b} From ece5029920898fbd19ca5ac39903d868da85d099 Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 17 Sep 2025 16:19:05 +0200 Subject: [PATCH 545/569] Implement support for SVCB and HTTPS records in zonefiles --- src/base/iana/svcb.rs | 6 +- src/base/scan.rs | 12 + src/rdata/mod.rs | 2 +- src/rdata/svcb/mod.rs | 4 +- src/rdata/svcb/params.rs | 176 +++++++++- src/rdata/svcb/rdata.rs | 52 ++- src/rdata/svcb/value.rs | 735 ++++++++++++++++++++++++++++++++++++++- src/zonefile/inplace.rs | 57 +++ 8 files changed, 1031 insertions(+), 13 deletions(-) diff --git a/src/base/iana/svcb.rs b/src/base/iana/svcb.rs index 9dd31fbe7..58e651d22 100644 --- a/src/base/iana/svcb.rs +++ b/src/base/iana/svcb.rs @@ -12,8 +12,12 @@ int_enum! { // https://datatracker.ietf.org/doc/draft-ietf-tls-esni/ (ECH => 5, "ech") (IPV6HINT => 6, "ipv6hint") - // https://datatracker.ietf.org/doc/draft-ietf-add-svcb-dns/ + // https://datatracker.ietf.org/doc/rfc9461/ (DOHPATH => 7, "dohpath") + (OHTTP => 8, "ohttp") + // https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction/ + (TLS_SUPPORTED_GROUPS => 9, "tls-supported-groups") + // TODO: docpath https://datatracker.ietf.org/doc/draft-ietf-core-dns-over-coap/ } int_enum_str_with_prefix!(SvcParamKey, "key", b"key", u16, "unknown key"); diff --git a/src/base/scan.rs b/src/base/scan.rs index df2420827..f7ae29fd9 100644 --- a/src/base/scan.rs +++ b/src/base/scan.rs @@ -211,6 +211,18 @@ pub trait Scanner { /// It can be of any length. fn scan_octets(&mut self) -> Result; + /// Scans a token into an octets sequence combining tokens that are not + /// separated by whitespace into a single token (used for SVCB quoted + /// SvcParamValues). + /// + /// The returned sequence has all symbols converted into their octets. + /// It can be of any length. + fn scan_svcb_octets(&mut self) -> Result { + Err(Self::Error::custom( + "Scanning SVCB octets is only implemented by some Scanners", + )) + } + /// Scans a token as a borrowed ASCII string. /// /// If the next token contains non-ascii characters, returns an error. diff --git a/src/rdata/mod.rs b/src/rdata/mod.rs index 86c2fd56d..d4f211483 100644 --- a/src/rdata/mod.rs +++ b/src/rdata/mod.rs @@ -153,7 +153,7 @@ rdata_types! { } } svcb::{ - pseudo { + zone { Svcb, Https, } diff --git a/src/rdata/svcb/mod.rs b/src/rdata/svcb/mod.rs index c0afb8590..83142d1e9 100644 --- a/src/rdata/svcb/mod.rs +++ b/src/rdata/svcb/mod.rs @@ -29,8 +29,8 @@ //! pub use self::params::{ ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError, - SvcParamValue, SvcParams, SvcParamsBuilder, SvcParamsError, - UnknownSvcParam, ValueIter, + ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder, + SvcParamsError, UnknownSvcParam, ValueIter, }; pub use self::rdata::{Https, HttpsVariant, Svcb, SvcbRdata, SvcbVariant}; diff --git a/src/rdata/svcb/params.rs b/src/rdata/svcb/params.rs index d4f1a6eb8..282527a03 100644 --- a/src/rdata/svcb/params.rs +++ b/src/rdata/svcb/params.rs @@ -7,18 +7,26 @@ // the allow attribute doesn't get copied to the code generated by serde. #![allow(clippy::needless_maybe_sized)] +#[cfg(feature = "std")] +use std::collections::BTreeMap; +#[cfg(feature = "std")] +use std::string::String; + use super::value::AllValues; use crate::base::cmp::CanonicalOrd; use crate::base::iana::SvcParamKey; -use crate::base::scan::Symbol; +use crate::base::scan::{Scanner, ScannerError, Symbol}; use crate::base::wire::{Compose, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use core::cmp::Ordering; use core::marker::PhantomData; +#[cfg(feature = "std")] +use core::str::FromStr; use core::{cmp, fmt, hash, mem}; use octseq::builder::{EmptyBuilder, FromBuilder, OctetsBuilder, ShortBuf}; use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::{Parser, ShortInput}; +use octseq::FreezeBuilder; //------------ SvcParams ----------------------------------------------------- @@ -186,6 +194,130 @@ impl> SvcParams { } } +#[cfg(feature = "std")] +impl> SvcParams { + pub fn scan>( + scanner: &mut S, + ) -> Result { + // SvcParams in presentation format MAY appear in any order, but keys + // MUST NOT be repeated. + + // SvcParam = SvcParamKey ["=" SvcParamValue] + // SvcParamValue = char-string ; See Appendix A. + // value = *OCTET ; Value before key-specific parsing + + // alpha-lc = %x61-7A ; a-z + // SvcParamKey = 1*63(alpha-lc / DIGIT / "-") + fn allowed_key_charset(ch: u8) -> bool { + (0x61..0x7A).contains(&ch) + || (0x30..0x39).contains(&ch) + || 0x2D == ch + } + + let mut builder = scanner.octets_builder()?; + + // Loop over tokens. SvcbParams might be split across multiple tokens + // if the SvcParamValue is quoted, therefore using custom + // scan_svcb_octets scanner function. + let mut key_map = BTreeMap::>::new(); + while scanner.continues() { + let mut is_key = true; + let mut key_end = None; + let mut value_start = None; + let octs = scanner.scan_svcb_octets()?; + if octs.as_ref().is_empty() { + // If someone provides an empty params token, e.g.: SVCB 10 . "" + return Err(S::Error::custom("SvcParams cannot be empty")); + } + + for (i, &ch) in octs.as_ref().iter().enumerate() { + if is_key { + if !allowed_key_charset(ch) { + if ch == b'=' { + key_end = Some(i); + is_key = false; + } else { + return Err(ScannerError::custom( + "invalid SvcParamKey", + )); + } + } + } else if value_start.is_none() { + value_start = Some(i); + } + } + + let key_end = + key_end.expect("unexpectedly not read any SVCB keys"); + let param_key = SvcParamKey::from_str(&String::from_utf8_lossy( + &octs.as_ref()[0..key_end], + )) + .map_err(|_| ScannerError::custom("unknown SvcParamKey"))?; + + let param_value = if let Some(value_start) = value_start { + let value = &octs.as_ref()[value_start..]; + AllValues::::value_from_scan_octets( + scanner, param_key, value, + )? + .ok_or(S::Error::custom("could not parse SvcParamValue"))? + } else { + AllValues::::value_from_scan_octets( + scanner, + param_key, + &[], + )? + .ok_or(S::Error::custom( + "could not parse SvcParamValue from empty value", + ))? + }; + + if key_map.insert(param_key, param_value).is_some() { + return Err(S::Error::custom("duplicate SvcParamKey")); + } + } + + for (param_key, param_value) in &key_map { + // https://www.rfc-editor.org/rfc/rfc9460.html#name-rdata-wire-format + // When the list of SvcParams is non-empty, it contains a series + // of SvcParamKey=SvcParamValue pairs, represented as: + // - a 2-octet field containing the SvcParamKey as an integer in + // network byte order. + param_key + .compose(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // - a 2-octet field containing the length of the SvcParamValue as + // an integer between 0 and 65535 in network byte order. + let value_len = param_value.compose_len(); + value_len + .compose(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // - an octet string of this length whose contents are the + // SvcParamValue in a format determined by the SvcParamKey. + param_value + .compose_value(&mut builder) + .map_err(|_| S::Error::short_buf())?; + + // TODO: [...] Other automatically mandatory keys SHOULD NOT + // appear in the list either. (Including them wastes space and + // otherwise has no effect.) + if let AllValues::Mandatory(m) = ¶m_value { + for req in m.iter() { + if !key_map.contains_key(&req) { + return Err(S::Error::custom("all SvcParamKeys listed in mandatory MUST appear in SvcParams")); + } + } + } + } + + SvcParams::from_octets(builder.freeze()).map_err(|e| { + println!("error in params::from_octets = {}", &e.0); + S::Error::custom("invalid SvcParams") + }) + } +} + impl SvcParams { /// Returns a reference to the underlying octets sequence. pub fn as_octets(&self) -> &Octs { @@ -334,7 +466,7 @@ where //--- Display and Debug -impl fmt::Display for SvcParams { +impl + ?Sized> fmt::Display for SvcParams { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut parser = Parser::from_ref(self.as_slice()); let mut first = true; @@ -361,7 +493,7 @@ impl fmt::Display for SvcParams { } } -impl fmt::Debug for SvcParams { +impl + ?Sized> fmt::Debug for SvcParams { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("SvcParams") .field(&format_args!("{}", self)) @@ -371,7 +503,7 @@ impl fmt::Debug for SvcParams { //--- ZonefileFmt -impl ZonefileFmt for SvcParams { +impl + ?Sized> ZonefileFmt for SvcParams { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { p.block(|p| { let mut parser = Parser::from_ref(self.as_slice()); @@ -476,6 +608,23 @@ pub trait ParseSvcParamValue<'a, Octs: ?Sized>: ) -> Result, ParseError>; } +/// A service binding parameter value that can be parse from wire format. +pub trait ScanSvcParamValue< + SrcOcts: AsRef<[u8]> + ?Sized, + Octs: AsRef<[u8]> + ?Sized, +>: SvcParamValue + Sized +{ + /// Scan a parameter value from octets from presentation format. + /// + /// The method should return `Ok(None)` if the type cannot parse values + /// with `key`. It should return an error if parsing fails. + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error>; +} + /// A service binding parameter value that can be composed into wire format. /// /// All value types need to be able to calculate the length of their @@ -658,6 +807,25 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> } } +impl ScanSvcParamValue for UnknownSvcParam +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + let mut tmp = scanner.octets_builder()?; + tmp.append_slice(octs.as_ref()) + .map_err(|_| S::Error::short_buf())?; + Self::new(key, tmp.freeze()).map(Some).map_err(|_| { + S::Error::custom("SvcParamValue for unknown param too long") + }) + } +} + impl> ComposeSvcParamValue for UnknownSvcParam { fn compose_len(&self) -> u16 { u16::try_from(self.as_slice().len()).expect("long value") diff --git a/src/rdata/svcb/rdata.rs b/src/rdata/svcb/rdata.rs index a0d26714d..f44663b38 100644 --- a/src/rdata/svcb/rdata.rs +++ b/src/rdata/svcb/rdata.rs @@ -9,6 +9,9 @@ use crate::base::name::{FlattenInto, ParsedName, ToName}; use crate::base::rdata::{ ComposeRecordData, LongRecordData, ParseRecordData, RecordData, }; +#[cfg(feature = "std")] +use crate::base::scan::Scan; +use crate::base::scan::{Scanner, ScannerError}; use crate::base::wire::{Compose, Composer, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use core::marker::PhantomData; @@ -179,6 +182,49 @@ impl> SvcbRdata> { } } +impl, Name: ToName> SvcbRdata { + pub fn scan>( + scanner: &mut S, + ) -> Result { + #[cfg(feature = "std")] + { + let priority = u16::scan(scanner)?; + let target = scanner.scan_name()?; + let params = SvcParams::scan(scanner)?; + + Self::new(priority, target, params) + .map_err(|_| S::Error::custom("SVCB record too long")) + } + #[cfg(not(feature = "std"))] + { + let _ = scanner; + Err(S::Error::custom("zonefile parsing of SVCB RRs is not implemented without the domain std feature")) + } + } +} + +impl, Name: ToName> SvcbRdata { + pub fn scan>( + scanner: &mut S, + ) -> Result { + #[cfg(feature = "std")] + { + let priority = u16::scan(scanner)?; + let target = scanner.scan_name()?; + // TODO: The "automatically mandatory" keys (Section 8) are "port" and "no-default-alpn". + let params = SvcParams::scan(scanner)?; + + Self::new(priority, target, params) + .map_err(|_| S::Error::custom("HTTPS record too long")) + } + #[cfg(not(feature = "std"))] + { + let _ = scanner; + Err(S::Error::custom("zonefile parsing of HTTPS RRs is not implemented without the domain std feature")) + } + } +} + impl SvcbRdata { /// Returns the priority. pub fn priority(&self) -> u16 { @@ -467,7 +513,7 @@ where impl fmt::Display for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: fmt::Display, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -477,7 +523,7 @@ where impl fmt::Debug for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: fmt::Debug, { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -493,7 +539,7 @@ where impl ZonefileFmt for SvcbRdata where - Octs: Octets, + Octs: AsRef<[u8]>, Name: ToName, { fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result { diff --git a/src/rdata/svcb/value.rs b/src/rdata/svcb/value.rs index 8cd7c2e1f..2bb9138dc 100644 --- a/src/rdata/svcb/value.rs +++ b/src/rdata/svcb/value.rs @@ -1,9 +1,18 @@ +#[cfg(feature = "std")] +use std::collections::BTreeSet; +#[cfg(feature = "std")] +use std::collections::HashSet; + use super::{ ComposeSvcParamValue, LongSvcParam, ParseSvcParamValue, PushError, - SvcParamValue, SvcParams, SvcParamsBuilder, UnknownSvcParam, + ScanSvcParamValue, SvcParamValue, SvcParams, SvcParamsBuilder, + UnknownSvcParam, }; use crate::base::iana::SvcParamKey; use crate::base::net::{Ipv4Addr, Ipv6Addr}; +use crate::base::scan::{ + ConvertSymbols, EntrySymbol, Scanner, ScannerError, Symbol, +}; use crate::base::wire::{Compose, Parse, ParseError}; use crate::utils::base64; use core::fmt::Write as _; @@ -131,6 +140,37 @@ macro_rules! values_enum { } } + #[cfg(feature = "std")] + impl ScanSvcParamValue + for AllValues + where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, + { + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + match key { + $( + $type::KEY => { + $type::value_from_scan_octets( + scanner, + key, + octs + ).map(|res| Some(Self::$type(res.unwrap()))) + } + )+ + _ => { + UnknownSvcParam::value_from_scan_octets( + scanner, key, octs + ).map(|res| res.map(Self::Unknown)) + } + } + } + } + impl> ComposeSvcParamValue for AllValues { fn compose_len(&self) -> u16 { match self { @@ -215,6 +255,8 @@ values_enum! { Ipv4Hint, Ipv6Hint, DohPath, + Ohttp, + TlsSupportedGroups, } //============ Individual Value Types ======================================== @@ -474,9 +516,56 @@ impl> Mandatory { } } +#[cfg(feature = "std")] +impl ScanSvcParamValue for Mandatory +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Mandatory::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + // keys must be in order, therefore BTreeSet instead of HashSet + let mut keys = BTreeSet::::new(); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in mandatory") + })? { + let k = + SvcParamKey::from_bytes(item).ok_or(S::Error::custom( + "invalid key listed in SvcParamKey mandatory", + ))?; + if k == SvcParamKey::MANDATORY { + return Err(S::Error::custom( + // https://www.rfc-editor.org/rfc/rfc9460.html#section-8-8 + "the key 'mandatory' MUST NOT appear in the mandatory keys list", + )); + } + if !keys.insert(k) { + return Err(S::Error::custom( + "mandatory contains duplicate keys", + )); + } + } + for k in keys { + k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?; + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for mandatory") + })?)) + } else { + Ok(None) + } + } +} + //--- Iterator -impl Iterator for MandatoryIter<'_, Octs> { +impl + ?Sized> Iterator for MandatoryIter<'_, Octs> { type Item = SvcParamKey; fn next(&mut self) -> Option { @@ -608,6 +697,45 @@ impl> Alpn { } } +impl ScanSvcParamValue for Alpn +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Alpn::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + let mut at_least_one = false; + while let Some(item) = iter.next() { + at_least_one = true; + let len: u8 = item.len().try_into().map_err(|_| { + S::Error::custom("SvcParamValue is too long") + })?; + tmp.append_slice(&[len]) + .map_err(|_| S::Error::short_buf())?; + tmp.append_slice(item).map_err(|_| S::Error::short_buf())?; + } + + if !at_least_one { + return Err(S::Error::custom( + "expected at least one alpn-id value", + )); + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for alpn") + })?)) + } else { + // TODO: why is it ok none if the key is wrong? (stolen from parse_value) + Ok(None) + } + } +} + //--- Iterator impl<'a, Octs: Octets + ?Sized> Iterator for AlpnIter<'a, Octs> { @@ -798,6 +926,29 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> } } +impl ScanSvcParamValue for NoDefaultAlpn +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if !octs.as_ref().is_empty() { + return Err(S::Error::custom( + "no-default-alpn takes no values", + )); + } + Ok(Some(Self)) + } else { + Ok(None) + } + } +} + impl ComposeSvcParamValue for NoDefaultAlpn { fn compose_len(&self) -> u16 { 0 @@ -890,6 +1041,35 @@ impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Port { } } +impl ScanSvcParamValue for Port +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if octs.as_ref().is_empty() { + return Err(S::Error::custom("port requires a value")); + } + let s = str::from_utf8(octs.as_ref()).map_err(|_| { + S::Error::custom("port value must be valid utf-8") + })?; + let port = s.parse::().map_err(|_| { + S::Error::custom( + "port value must be a 16-bit unsigned decimal number", + ) + })?; + Ok(Some(Self::new(port))) + } else { + Ok(None) + } + } +} + impl ComposeSvcParamValue for Port { fn compose_len(&self) -> u16 { u16::COMPOSE_LEN @@ -981,6 +1161,53 @@ impl> Ech { } } +impl ScanSvcParamValue for Ech +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ech::KEY { + if octs.as_ref().is_empty() { + return Err(S::Error::custom("ech requires as value")); + } + let mut builder = scanner.octets_builder()?; + let mut convert = base64::SymbolConverter::new(); + for ch in octs.as_ref() { + if let Some(data) = convert.process_symbol( + EntrySymbol::from(Symbol::from_octet(*ch)), + )? { + builder + .append_slice(data) + .map_err(|_| S::Error::short_buf())?; + } + } + + // if let Some(data) = convert.process_tail()? { + if let Some(data) = ::Error, + >>::process_tail(&mut convert)? + { + builder + .append_slice(data) + .map_err(|_| S::Error::short_buf())?; + } + let dec = builder.freeze(); + + Ok(Some(Self::from_octets(dec).map_err(|_| { + S::Error::custom("invalid ech param value") + })?)) + } else { + Ok(None) + } + } +} + //--- Display impl + ?Sized> fmt::Display for Ech { @@ -1099,6 +1326,41 @@ impl> Ipv4Hint { } } +impl ScanSvcParamValue for Ipv4Hint +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ipv4Hint::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in ipv4hint") + })? { + let ip = Ipv4Addr::from_str(str::from_utf8(item).map_err( + |_| S::Error::custom("invalid utf-8 in ipv4hint param"), + )?) + .map_err(|_| { + S::Error::custom("invalid ipv4 in ipv4hint param") + })?; + tmp.append_slice(&ip.octets()) + .map_err(|_| S::Error::short_buf())?; + } + // tmp.append_slice(); + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for ipv4hint") + })?)) + } else { + Ok(None) + } + } +} + impl Iterator for Ipv4HintIter<'_, Octs> { type Item = Ipv4Addr; @@ -1247,6 +1509,41 @@ impl> Ipv6Hint { } } +impl ScanSvcParamValue for Ipv6Hint +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Ipv6Hint::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom("no escape sequences allowed in ipv6hint") + })? { + let ip = Ipv6Addr::from_str(str::from_utf8(item).map_err( + |_| S::Error::custom("invalid utf-8 in ipv6hint param"), + )?) + .map_err(|_| { + S::Error::custom("invalid ipv6 in ipv6hint param") + })?; + tmp.append_slice(&ip.octets()) + .map_err(|_| S::Error::short_buf())?; + } + // tmp.append_slice(); + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for ipv6hint") + })?)) + } else { + Ok(None) + } + } +} + //--- Iterator impl Iterator for Ipv6HintIter<'_, Octs> { @@ -1368,6 +1665,32 @@ impl> DohPath { } } +impl ScanSvcParamValue for DohPath +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == DohPath::KEY { + let _ = str::from_utf8(octs.as_ref()).map_err(|_| { + S::Error::custom("dohpath must be valid UTF-8") + })?; + let mut tmp = scanner.octets_builder()?; + tmp.append_slice(octs.as_ref()) + .map_err(|_| S::Error::short_buf())?; + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom("invalid svc param value for dohpath") + })?)) + } else { + Ok(None) + } + } +} + //--- TryFrom and FromStr impl> TryFrom> for DohPath { @@ -1456,6 +1779,414 @@ impl + AsMut<[u8]>> SvcParamsBuilder { } } +//------------ Ohttp ------------------------------------------------- + +/// A signal that Oblivious HTTP is supported. +/// +/// The "ohttp" SvcParamKey is used to indicate that a service described in +/// a SVCB RR can be accessed as a target using an associated gateway. +/// +/// This value is always empty. +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Ohttp; + +impl Ohttp { + /// The key for this type. + const KEY: SvcParamKey = SvcParamKey::OHTTP; +} + +impl Ohttp { + /// Parses a ohttp value from its wire-format. + pub fn parse( + _parser: &mut Parser<'_, Src>, + ) -> Result { + Ok(Self) + } +} + +//--- SvcParamValue et al. + +impl SvcParamValue for Ohttp { + fn key(&self) -> SvcParamKey { + Self::KEY + } +} + +impl<'a, Octs: Octets + ?Sized> ParseSvcParamValue<'a, Octs> for Ohttp { + fn parse_value( + key: SvcParamKey, + parser: &mut Parser<'a, Octs>, + ) -> Result, ParseError> { + if key == Self::KEY { + Self::parse(parser).map(Some) + } else { + Ok(None) + } + } +} + +impl ScanSvcParamValue for Ohttp +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + _scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == Self::KEY { + if !octs.as_ref().is_empty() { + return Err(S::Error::custom("ohttp takes no values")); + } + Ok(Some(Self)) + } else { + Ok(None) + } + } +} + +impl ComposeSvcParamValue for Ohttp { + fn compose_len(&self) -> u16 { + 0 + } + + fn compose_value( + &self, + _target: &mut Target, + ) -> Result<(), Target::AppendError> { + Ok(()) + } +} + +//--- Display + +impl fmt::Display for Ohttp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ohttp") + } +} + +//--- Extend SvcParams and SvcParamsBuilder + +impl SvcParams { + /// Returns whether the [`Ohttp`] value is present. + pub fn ohttp(&self) -> bool { + self.first::().is_some() + } +} + +impl + AsMut<[u8]>> SvcParamsBuilder { + /// Adds the [`Ohttp`] value. + pub fn ohttp(&mut self) -> Result<(), PushError> { + self.push(&Ohttp) + } +} + +//------------ TlsSupportedGroups ------------------------------------------------- + +octets_wrapper!( + /// The ‘tls-supported-groups’ service parameter value. + /// + /// This value is used to specify the endpoint's TLS supported group preferences. + /// + /// This value type is described as part of the specification for + /// TLS Key Share Prediction, currently + /// [draft-ietf-tls-key-share-prediction](https://datatracker.ietf.org/doc/draft-ietf-tls-key-share-prediction). + /// + /// A value of this type wraps an octets sequence that contains the + /// integer values of the TLS supported groups preferences in network byte + /// order. You can create a value of this type by providing an iterator + /// over the keys to be included to the [`from_keys`][Self::from_keys] + /// function. You can get an iterator over the keys in an existing value + /// through the [`iter`][Self::iter] method. + TlsSupportedGroups => TLS_SUPPORTED_GROUPS, + TlsSupportedGroupsIter +); + +impl> TlsSupportedGroups { + /// Creates a new tls-supported-groups value from an octets sequence. + /// + /// The function checks that the octets sequence contains a properly + /// encoded value of at most 65,535 octets. It does not check whether + /// there are any duplicates in the data. + pub fn from_octets(octets: Octs) -> Result { + TlsSupportedGroups::check_slice(octets.as_ref())?; + Ok(unsafe { Self::from_octets_unchecked(octets) }) + } +} + +impl TlsSupportedGroups<[u8]> { + /// Creates a new tls-supported-groups value from an octets slice. + /// + /// The function checks that the octets slice contains a properly + /// encoded value of at most 65,535 octets. It does not check whether + /// there are any duplicates in the data. + pub fn from_slice(slice: &[u8]) -> Result<&Self, ParseError> { + Self::check_slice(slice)?; + Ok(unsafe { Self::from_slice_unchecked(slice) }) + } + + /// Checks that a slice contains a properly encoded tls-supported-groups value. + fn check_slice(slice: &[u8]) -> Result<(), ParseError> { + LongSvcParam::check_len(slice.len())?; + if slice.len() == 0 + || slice.len() % usize::from(u16::COMPOSE_LEN) != 0 + { + return Err(ParseError::form_error( + "invalid tls-supported-groups parameter", + )); + } + Ok(()) + } +} + +impl> TlsSupportedGroups { + /// Creates a new value from a list of keys. + /// + /// The created value will contain all the keys returned by the iterator + /// in the order provided. The function does not check for duplicates. + /// + /// Returns an error if the octets builder runs out of space or the + /// resulting value would be longer than 65,535 octets. + pub fn from_keys( + keys: impl Iterator, + ) -> Result + where + Octs: FromBuilder, + ::Builder: EmptyBuilder, + { + let mut octets = EmptyBuilder::empty(); + for item in keys { + item.compose(&mut octets)?; + } + let octets = Octs::from_builder(octets); + if LongSvcParam::check_len(octets.as_ref().len()).is_err() { + return Err(BuildValueError::LongSvcParam); + } + Ok(unsafe { Self::from_octets_unchecked(octets) }) + } +} + +impl> TlsSupportedGroups { + /// Parses a tls-supported-groups value from its wire format. + pub fn parse<'a, Src: Octets = Octs> + ?Sized>( + parser: &mut Parser<'a, Src>, + ) -> Result { + Self::from_octets(parser.parse_octets(parser.remaining())?) + } +} + +#[cfg(feature = "std")] +impl ScanSvcParamValue + for TlsSupportedGroups +where + Octs: AsRef<[u8]>, + SrcOcts: AsRef<[u8]> + ?Sized, +{ + fn value_from_scan_octets>( + scanner: &mut S, + key: SvcParamKey, + octs: &SrcOcts, + ) -> Result, S::Error> { + if key == TlsSupportedGroups::KEY { + let mut tmp = scanner.octets_builder()?; + let mut iter = SvcParamValueScanIter::from_slice(octs.as_ref()); + // keys must not be duplicated + let mut keys = HashSet::::new(); + while let Some(item) = iter.next_no_escapes().map_err(|_| { + S::Error::custom( + "no escape sequences allowed in tls-supported-groups", + ) + })? { + let k = str::from_utf8(item) + .map_err(|_| { + S::Error::custom( + "invalid key listed in SvcParamKey tls-supported-groups, must contain UTF-8 encoded numbers", + ) + })? + .parse::() + .map_err(|_| { + S::Error::custom( + "invalid key listed in SvcParamKey tls-supported-groups, must contain positive 16-bit integers", + ) + })?; + if !keys.insert(k) { + return Err(S::Error::custom( + "tls-supported-groups contains duplicate values", + )); + } + k.compose(&mut tmp).map_err(|_| S::Error::short_buf())?; + } + Ok(Some(Self::from_octets(tmp.freeze()).map_err(|_| { + S::Error::custom( + "invalid svc param value for tls-supported-groups", + ) + })?)) + } else { + Ok(None) + } + } +} + +//--- Iterator + +impl + ?Sized> Iterator + for TlsSupportedGroupsIter<'_, Octs> +{ + type Item = u16; + + fn next(&mut self) -> Option { + if self.parser.remaining() == 0 { + return None; + } + Some( + u16::parse(&mut self.parser) + .expect("invalid tls-supported-groups parameter"), + ) + } +} + +//--- Display + +impl fmt::Display for TlsSupportedGroups { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for (i, v) in self.iter().enumerate() { + if i == 0 { + write!(f, "tls-supported-groups={}", v)?; + } else { + write!(f, ",{}", v)?; + } + } + Ok(()) + } +} + +//--- Extend SvcParams and SvcParamsBuilder + +impl SvcParams { + /// Returns the content of the ‘tls-supported-groups’ value if present. + pub fn tls_supported_groups( + &self, + ) -> Option>> { + self.first() + } +} + +impl + AsMut<[u8]>> SvcParamsBuilder { + /// Adds a ‘tls-supported-groups’ value with the given keys. + /// + /// Returns an error if there already is a ‘tls-supported-groups’ value, `keys` + /// contains more values than fit into a service binding parameter value, + /// or the underlying octets builder runs out of space. + pub fn tls_supported_groups( + &mut self, + keys: impl AsRef<[SvcParamKey]>, + ) -> Result<(), PushValueError> { + self.push_raw( + TlsSupportedGroups::KEY, + u16::try_from( + keys.as_ref().len() * usize::from(SvcParamKey::COMPOSE_LEN), + ) + .map_err(|_| PushValueError::LongSvcParam)?, + |octs| { + keys.as_ref().iter().try_for_each(|item| item.compose(octs)) + }, + ) + .map_err(Into::into) + } +} + +//------------ SvcParamValueScanIter ----------------------------------------- + +/// An iterator over the items of a comma separated list of octets, as used +/// in SvcParamValue [RFC9460 A.1.] with escape sequences allowed +/// +/// [RFC9460 A.1.]: https://www.rfc-editor.org/rfc/rfc9460.html#name-decoding-a-comma-separated- +struct SvcParamValueScanIter<'a> { + data: &'a [u8], + start_next: usize, + end: usize, +} + +impl<'a> SvcParamValueScanIter<'a> { + pub fn from_slice(octs: &'a [u8]) -> Self { + Self { + data: octs, + start_next: 0, + end: 0, + } + } + + pub fn next(&mut self) -> Option<&[u8]> { + if self.start_next >= self.data.len() { + None + } else { + let mut is_escaped = false; + let start = self.start_next; + self.end = self.start_next; + + loop { + if self.end < self.data.len() { + // Still data to read + if !is_escaped && self.data[self.end] == b',' { + // End of value. Return item + break; + } else { + // Value not yet ended. Read more + if is_escaped { + is_escaped = false; + } else if self.data[self.end] == b'\\' { + is_escaped = true; + } + + self.end += 1; + } + } else { + // End of data. Return item + break; + } + } + + self.start_next = self.end + 1; + Some(&self.data[start..self.end]) + } + } + + /// An iterator but error on escape sequences + pub fn next_no_escapes(&mut self) -> Result, ()> { + if self.start_next >= self.data.len() { + Ok(None) + } else { + let start = self.start_next; + self.end = self.start_next; + + loop { + if self.end < self.data.len() { + // Still data to read + if self.data[self.end] == b',' { + // End of value. Return item + break; + } else { + // Value not yet ended. Read more + if self.data[self.end] == b'\\' { + // Escaping not allowed + return Err(()); + } + self.end += 1; + } + } else { + // End of data. Return item + break; + } + } + + self.start_next = self.end + 1; + Ok(Some(&self.data[start..self.end])) + } + } +} + //============ BuildValueError =============================================== //------------ BuildValueError ----------------------------------------------- diff --git a/src/zonefile/inplace.rs b/src/zonefile/inplace.rs index 7c48421a4..433ac6efa 100644 --- a/src/zonefile/inplace.rs +++ b/src/zonefile/inplace.rs @@ -658,6 +658,63 @@ impl Scanner for EntryScanner<'_> { Ok(self.zonefile.buf.split_to(write).freeze()) } + /// SVCB's SvcParams format can contain quoted SvcParamValues, therefore we need to concatenate + /// multiple tokens into a single octet sequence if they appear without whitespace, e.g.: `SVCB + /// 10 . key1="quoted value"` would normally parsed into the tokens `SVCB` `10` `.` `key1=` and + /// `quoted value`, we need the last token to be `key1=quoted value`. + fn scan_svcb_octets(&mut self) -> Result { + self.zonefile.buf.require_token()?; + + // The result will never be longer than the encoded form, so we can + // trim off everything to the left already. + self.zonefile.buf.trim_to(self.zonefile.buf.start); + + let mut write; + // Remember if we are inside a quoted value. If so the opening quote + // has already been skipped over, it is not part of the value. + let is_quoted = self.zonefile.buf.cat == ItemCat::Quoted; + + // Skip over symbols that don’t need converting at the beginning. + while self.zonefile.buf.next_ascii_symbol()?.is_some() {} + + if self.zonefile.buf.cat == ItemCat::None { + // The item has ended. Remove the double quote. + let write = if is_quoted { + self.zonefile.buf.start - 1 + } else { + self.zonefile.buf.start + }; + self.zonefile.buf.next_item()?; + return Ok(self.zonefile.buf.split_to(write).freeze()); + } + + // If we aren’t done yet, we have escaped characters to replace. + write = self.zonefile.buf.start; + + while let Some(sym) = self.zonefile.buf.next_symbol()? { + self.zonefile.buf.buf[write] = sym.into_octet()?; + write += 1; + } + + // Done. `write` marks the end. + self.zonefile.buf.next_item()?; + + // If the next token exists (i.e. is not None or LineFeed) and + // directly follows this token without whitespace in between, it is + // part of the current token/octet-string and we read further + if !self.has_space() && self.zonefile.buf.cat == ItemCat::Quoted { + while let Some(sym) = self.zonefile.buf.next_symbol()? { + self.zonefile.buf.buf[write] = sym.into_octet()?; + write += 1; + } + // Done. `write` marks the end. + self.zonefile.buf.next_item()?; + } + + let x = self.zonefile.buf.split_to(write).freeze(); + Ok(x) + } + fn scan_ascii_str(&mut self, op: F) -> Result where F: FnOnce(&str) -> Result, From 8d47354eb28b569821e4f99a43ac19b65df4a00c Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Wed, 17 Sep 2025 16:27:44 +0200 Subject: [PATCH 546/569] Cargo clippy --- src/rdata/svcb/value.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rdata/svcb/value.rs b/src/rdata/svcb/value.rs index 2bb9138dc..c5d30b3f2 100644 --- a/src/rdata/svcb/value.rs +++ b/src/rdata/svcb/value.rs @@ -1930,7 +1930,7 @@ impl TlsSupportedGroups<[u8]> { /// Checks that a slice contains a properly encoded tls-supported-groups value. fn check_slice(slice: &[u8]) -> Result<(), ParseError> { LongSvcParam::check_len(slice.len())?; - if slice.len() == 0 + if slice.is_empty() || slice.len() % usize::from(u16::COMPOSE_LEN) != 0 { return Err(ParseError::form_error( From d94004268051c37b9a15de4b5af151f35579730e Mon Sep 17 00:00:00 2001 From: Jannik Peters Date: Thu, 18 Sep 2025 10:18:42 +0200 Subject: [PATCH 547/569] Fix reading SvcParamKey without value --- src/rdata/svcb/params.rs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/rdata/svcb/params.rs b/src/rdata/svcb/params.rs index 282527a03..adbd4acbc 100644 --- a/src/rdata/svcb/params.rs +++ b/src/rdata/svcb/params.rs @@ -247,11 +247,13 @@ impl> SvcParams { } } - let key_end = - key_end.expect("unexpectedly not read any SVCB keys"); - let param_key = SvcParamKey::from_str(&String::from_utf8_lossy( - &octs.as_ref()[0..key_end], - )) + let param_key = if let Some(key_end) = key_end { + SvcParamKey::from_str(&String::from_utf8_lossy( + &octs.as_ref()[0..key_end], + )) + } else { + SvcParamKey::from_str(&String::from_utf8_lossy(octs.as_ref())) + } .map_err(|_| ScannerError::custom("unknown SvcParamKey"))?; let param_value = if let Some(value_start) = value_start { From bbb0a4c90ff2cda00c249459ce35c736c6ea246d Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:01:17 +0100 Subject: [PATCH 548/569] Fix test compilation failure. --- src/crypto/kmip.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/crypto/kmip.rs b/src/crypto/kmip.rs index 884869013..f498a8b90 100644 --- a/src/crypto/kmip.rs +++ b/src/crypto/kmip.rs @@ -1446,7 +1446,7 @@ mod tests { insecure: true, client_cert: Some(kmip::client::ClientCertificate::SeparatePem { cert_bytes, - key_bytes: Some(key_bytes), + key_bytes, }), ..Default::default() }; From b0e140ca590cd65351393c41969ce595adf51271 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:07:21 +0100 Subject: [PATCH 549/569] Remove unnecessary type specifications in example. --- examples/query-routing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/query-routing.rs b/examples/query-routing.rs index fc1b2b2da..e0378d278 100644 --- a/examples/query-routing.rs +++ b/examples/query-routing.rs @@ -57,7 +57,7 @@ async fn main() { let conn_service = ClientTransportToSingleService::new(redun); qr.add(Name::>::from_str("nl").unwrap(), conn_service); - let srv = SingleServiceToService::<_, _, _, _>::new(qr); + let srv = SingleServiceToService::new(qr); let srv = MandatoryMiddlewareSvc::new(srv); let my_svc = Arc::new(srv); From 6f1232a3c5f96f98a40cc19c4d67825d7838605e Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:09:02 +0100 Subject: [PATCH 550/569] Remove unnecessary type specifications in example. --- examples/serve-zone.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index 50c3e8d34..d94c3af64 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -124,10 +124,10 @@ async fn main() { 1, ); let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); - let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); - let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); + let svc = CookiesMiddlewareSvc::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::new(svc); + let svc = TsigMiddlewareSvc::new(svc, key_store); + let svc = MandatoryMiddlewareSvc::new(svc); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); From 6f4739f9c7a7629384be2e9535644569677bfd6a Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:09:46 +0100 Subject: [PATCH 551/569] Remove unnecessary type specifications in example. --- examples/serve-zone.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index d94c3af64..1a2c4c426 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -146,7 +146,7 @@ async fn main() { } let sock = TcpListener::bind(addr).await.unwrap(); - let tcp_srv = StreamServer::<_, _, _>::new(sock, VecBufSource, svc); + let tcp_srv = StreamServer::new(sock, VecBufSource, svc); let tcp_metrics = tcp_srv.metrics(); tokio::spawn(async move { tcp_srv.run().await }); From 0868aa47828c11d69d4a8c8c8bd7d8564f92a105 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 5 Nov 2025 13:59:14 +0100 Subject: [PATCH 552/569] Remove duplicate import of Duration, SystemTime, and UNIX_EPOCH. --- src/rdata/dnssec.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 7e2e2e3ba..7091fd955 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -26,7 +26,6 @@ use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::Parser; #[cfg(feature = "serde")] use octseq::serde::{DeserializeOctets, SerializeOctets}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[cfg(feature = "std")] use std::time::{Duration, SystemTime, UNIX_EPOCH}; #[cfg(feature = "std")] From aca9f1d9a638d42c3d9a687e468b327488d7340f Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 5 Nov 2025 14:11:03 +0100 Subject: [PATCH 553/569] Remove fall-out from merging. --- src/rdata/dnssec.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index d6d736a46..6b311c6b1 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -35,21 +35,6 @@ use crate::base::wire::{Compose, Composer, FormError, Parse, ParseError}; use crate::base::zonefile_fmt::{self, Formatter, ZonefileFmt}; use crate::base::Ttl; use crate::utils::{base16, base64}; -use core::cmp::Ordering; -use core::convert::TryInto; -use core::{cmp, fmt, hash, str}; -use octseq::builder::{ - EmptyBuilder, FreezeBuilder, FromBuilder, OctetsBuilder, Truncate, -}; -use octseq::octets::{Octets, OctetsFrom, OctetsInto}; -use octseq::parse::Parser; -#[cfg(feature = "serde")] -use octseq::serde::{DeserializeOctets, SerializeOctets}; -#[cfg(feature = "std")] -use std::time::{Duration, SystemTime, UNIX_EPOCH}; -#[cfg(feature = "std")] -use std::vec::Vec; -use time::{Date, Month, PrimitiveDateTime, Time}; //------------ Dnskey -------------------------------------------------------- From 5325c2914c2c4f177ffcdb20e1d95fd2b6473346 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Wed, 5 Nov 2025 14:21:12 +0100 Subject: [PATCH 554/569] Fmt. --- examples/keyset.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/keyset.rs b/examples/keyset.rs index ee0b396d0..1152bc16d 100644 --- a/examples/keyset.rs +++ b/examples/keyset.rs @@ -560,4 +560,4 @@ fn report_actions(actions: Result, Error>, ks: &KeySet) { } } } -} \ No newline at end of file +} From f72c1fd98e0b1b67ca56ca20305dedd3cb9b73c0 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Mon, 10 Nov 2025 15:18:05 +0100 Subject: [PATCH 555/569] Fix merge issues. --- src/dnssec/sign/keys/keyset.rs | 352 +-------------------------------- 1 file changed, 5 insertions(+), 347 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 78fffa413..a6d6aaad4 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -412,7 +412,7 @@ impl KeySet { } fn unique_key_tag(&self, key_tag: u16) -> bool { - !self.keys.iter().any(|(_, k)| k.key_tag == key_tag) + self.keys.iter().all(|(_, k)| k.key_tag != key_tag) } /// Delete a key. @@ -898,149 +898,6 @@ impl KeySet { Mode::ForReal => &mut self.keys, }; let mut algs_old = HashSet::new(); - for k in old { - let Some(key) = keys.get_mut(&(*k).to_string()) else { - return Err(Error::KeyNotFound); - }; - match key.keytype { - KeyType::Ksk(ref mut keystate) - | KeyType::Zsk(ref mut keystate) => { - keystate.old = true; - } - KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { - ksk_keystate.old = true; - zsk_keystate.old = true; - } - KeyType::Include(_) => { - return Err(Error::WrongKeyType); - } - } - - // Add algorithm - algs_old.insert(key.algorithm); - } - let now = UnixTime::now(); - let mut algs_new = HashSet::new(); - for k in new { - let Some(key) = keys.get_mut(&(*k).to_string()) else { - return Err(Error::KeyNotFound); - }; - match key.keytype { - KeyType::Ksk(ref mut keystate) => { - if *keystate - != (KeyState { - available: true, - old: false, - signer: false, - present: false, - at_parent: false, - }) - { - return Err(Error::WrongKeyState); - } - - // Move key state to Active. - keystate.present = true; - keystate.signer = true; - key.timestamps.published = Some(now.clone()); - } - KeyType::Zsk(ref mut keystate) => { - if *keystate - != (KeyState { - available: true, - old: false, - signer: false, - present: false, - at_parent: false, - }) - { - return Err(Error::WrongKeyState); - } - - // Move key state to Incoming. - keystate.present = true; - key.timestamps.published = Some(now.clone()); - } - KeyType::Csk(ref mut ksk_keystate, ref mut zsk_keystate) => { - if *ksk_keystate - != (KeyState { - available: true, - old: false, - signer: false, - present: false, - at_parent: false, - }) - { - return Err(Error::WrongKeyState); - } - - // Move key state to Active. - ksk_keystate.present = true; - ksk_keystate.signer = true; - - if *zsk_keystate - != (KeyState { - available: true, - old: false, - signer: false, - present: false, - at_parent: false, - }) - { - return Err(Error::WrongKeyState); - } - - // Move key state to Incoming. - zsk_keystate.present = true; - - key.timestamps.published = Some(now.clone()); - } - _ => { - return Err(Error::WrongKeyType); - } - } - - // Add algorithm - algs_new.insert(key.algorithm); - } - - // Make sure the sets of algorithms are the same. - if algs_old != algs_new { - return Err(Error::AlgorithmSetsMismatch); - } - - // Make sure we have at least one KSK key in incoming state. - if !keys.iter().any(|(_, k)| match &k.keytype { - KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { - !keystate.old && keystate.present - } - _ => false, - }) { - return Err(Error::NoSuitableKeyPresent); - } - // Make sure we have at least one ZSK key in incoming state. - if !keys.iter().any(|(_, k)| match &k.keytype { - KeyType::Zsk(keystate) | KeyType::Csk(_, keystate) => { - !keystate.old && keystate.present - } - _ => false, - }) { - return Err(Error::NoSuitableKeyPresent); - } - Ok(()) - } - - fn update_algorithm( - &mut self, - mode: Mode, - old: &[&str], - new: &[&str], - ) -> Result<(), Error> { - let mut tmpkeys = self.keys.clone(); - let keys: &mut HashMap = match mode { - Mode::DryRun => &mut tmpkeys, - Mode::ForReal => &mut self.keys, - }; for k in old { let Some(key) = keys.get_mut(&(k.to_string())) else { return Err(Error::KeyNotFound); @@ -1300,8 +1157,6 @@ impl KeySet { pub struct Key { privref: Option, - // XXX - remove the following directive before merging. - #[serde(default)] decoupled: bool, keytype: KeyType, @@ -2291,7 +2146,7 @@ fn zsk_double_signature_roll( .expect("Should have been checked with DryRun"); } RollOp::Propagation1 => { - // Set the visiable time of new ZSKs to the current time. + // Set the visible time of new ZSKs to the current time. let now = UnixTime::now(); for k in ks.keys.values_mut() { let KeyType::Zsk(ref keystate) = k.keytype else { @@ -2340,15 +2195,7 @@ fn zsk_double_signature_roll( } } RollOp::Propagation2 => { - // Set the published time of new RRSIG records to the current time. - for k in ks.keys.values_mut() { - let KeyType::Zsk(ref keystate) = k.keytype else { - continue; - }; - if keystate.old || !keystate.signer { - continue; - } - } + // No need to do anything here. } RollOp::CacheExpire2(ttl) => { for k in ks.keys.values() { @@ -2647,195 +2494,6 @@ fn csk_roll_actions(rollstate: RollState) -> Vec { actions } -// An algorithm roll is similar to a CSK roll. The main difference is that -// to zone is signed with all keys before introducing the DS records for -// the new KSKs or CSKs. -fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { - match rollop { - RollOp::Start(old, new) => { - // First check if the current algorithm-roll state is idle. We need - // to check all conflicting key rolls as well. The way we check - // is to allow specified non-conflicting rolls and consider - // everything else as a conflict. - if let Some(rolltype) = ks.rollstates.keys().next() { - if *rolltype == RollType::AlgorithmRoll { - return Err(Error::WrongStateForRollOperation); - } else { - return Err(Error::ConflictingRollInProgress); - } - } - // Check if we can move the states of the keys - ks.update_algorithm(Mode::DryRun, old, new)?; - // Move the states of the keys - ks.update_algorithm(Mode::ForReal, old, new) - .expect("Should have been check with DryRun"); - } - RollOp::Propagation1 => { - // Set the visible time of new KSKs, ZSKs and CSKs to the current - // time. Set signer and for new KSKs, ZSKs and CSKs. - // Set RRSIG visible for new ZSKs and CSKs. - let now = UnixTime::now(); - for k in ks.keys.values_mut() { - match &mut k.keytype { - KeyType::Ksk(keystate) => { - if keystate.old || !keystate.present { - continue; - } - - k.timestamps.visible = Some(now.clone()); - } - KeyType::Zsk(keystate) | KeyType::Csk(keystate, _) => { - if keystate.old || !keystate.present { - continue; - } - - k.timestamps.visible = Some(now.clone()); - k.timestamps.rrsig_visible = Some(now.clone()); - } - KeyType::Include(_) => (), - } - } - } - RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values() { - let keystate = match &k.keytype { - KeyType::Ksk(keystate) - | KeyType::Zsk(keystate) - | KeyType::Csk(keystate, _) => keystate, - KeyType::Include(_) => continue, - }; - if keystate.old || !keystate.present { - continue; - } - - let visible = k - .timestamps - .visible - .as_ref() - .expect("Should have been set in Propagation1"); - let elapsed = visible.elapsed(); - let ttl = Duration::from_secs(ttl.into()); - if elapsed < ttl { - return Err(Error::Wait(ttl - elapsed)); - } - } - - for k in ks.keys.values_mut() { - match k.keytype { - KeyType::Ksk(ref mut keystate) - | KeyType::Csk(ref mut keystate, _) => { - if keystate.old && keystate.present { - keystate.at_parent = false; - } - - // Put Active keys at parent. - if !keystate.old && keystate.present { - keystate.at_parent = true; - } - } - KeyType::Zsk(_) | KeyType::Include(_) => (), - } - } - } - RollOp::Propagation2 => { - // Set the published time of new DS records to the current time. - let now = UnixTime::now(); - for k in ks.keys.values_mut() { - match &k.keytype { - KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { - if keystate.old || !keystate.present { - continue; - } - - k.timestamps.ds_visible = Some(now.clone()); - } - KeyType::Zsk(_) | KeyType::Include(_) => (), - } - } - } - RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values() { - let keystate = match &k.keytype { - KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { - keystate - } - KeyType::Zsk(_) | KeyType::Include(_) => continue, - }; - if keystate.old || !keystate.signer { - continue; - } - - let ds_visible = k - .timestamps - .ds_visible - .as_ref() - .expect("Should have been set in Propagation2"); - let elapsed = ds_visible.elapsed(); - let ttl = Duration::from_secs(ttl.into()); - if elapsed < ttl { - return Err(Error::Wait(ttl - elapsed)); - } - } - - // Move old keys out - for k in ks.keys.values_mut() { - match k.keytype { - KeyType::Ksk(ref mut keystate) - | KeyType::Zsk(ref mut keystate) => { - if keystate.old && keystate.present { - keystate.signer = false; - keystate.present = false; - k.timestamps.withdrawn = Some(UnixTime::now()); - } - } - KeyType::Csk( - ref mut ksk_keystate, - ref mut zsk_keystate, - ) => { - if ksk_keystate.old && ksk_keystate.present { - ksk_keystate.signer = false; - ksk_keystate.present = false; - zsk_keystate.signer = false; - zsk_keystate.present = false; - k.timestamps.withdrawn = Some(UnixTime::now()); - } - } - KeyType::Include(_) => (), - } - } - } - RollOp::Done => (), - } - Ok(()) -} - -fn algorithm_roll_actions(rollstate: RollState) -> Vec { - let mut actions = Vec::new(); - match rollstate { - RollState::Propagation1 => { - actions.push(Action::UpdateDnskeyRrset); - actions.push(Action::UpdateRrsig); - actions.push(Action::ReportDnskeyPropagated); - actions.push(Action::ReportRrsigPropagated); - } - RollState::CacheExpire1(_) => (), - RollState::Propagation2 => { - actions.push(Action::CreateCdsRrset); - actions.push(Action::UpdateDsRrset); - actions.push(Action::ReportDsPropagated); - } - RollState::CacheExpire2(_) => (), - RollState::Done => { - actions.push(Action::RemoveCdsRrset); - actions.push(Action::UpdateDnskeyRrset); - actions.push(Action::UpdateRrsig); - actions.push(Action::WaitDnskeyPropagated); - actions.push(Action::WaitRrsigPropagated); - } - } - actions -} - // An algorithm roll is similar to a CSK roll. The main difference is that // the zone is signed with all keys before introducing the DS records for // the new KSKs or CSKs. @@ -2885,7 +2543,7 @@ fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire1(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Ksk(keystate) | KeyType::Zsk(keystate) @@ -2942,7 +2600,7 @@ fn algorithm_roll(rollop: RollOp<'_>, ks: &mut KeySet) -> Result<(), Error> { } } RollOp::CacheExpire2(ttl) => { - for k in ks.keys.values_mut() { + for k in ks.keys.values() { let keystate = match &k.keytype { KeyType::Ksk(keystate) | KeyType::Csk(keystate, _) => { keystate From 5ad4486bce0b7c0469cafdd074f26920fcade661 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:29:32 +0100 Subject: [PATCH 556/569] Cargo fmt. --- src/net/server/connection.rs | 9 ++++++--- src/net/server/dgram.rs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index 62c430971..f6926f0f0 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -1037,7 +1037,8 @@ where status: InvokerStatus, } -impl ServiceResponseHandler +impl + ServiceResponseHandler where RequestOctets: AsRef<[u8]> + Send + Sync, RequestMeta: Clone + Default, @@ -1109,7 +1110,8 @@ where //--- Clone -impl Clone for ServiceResponseHandler +impl Clone + for ServiceResponseHandler where RequestOctets: AsRef<[u8]> + Send + Sync, RequestMeta: Clone + Default, @@ -1127,7 +1129,8 @@ where //--- ServiceInvoker -impl ServiceInvoker +impl + ServiceInvoker for ServiceResponseHandler where RequestOctets: Octets + Send + Sync + 'static, diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 0a0e71304..296b8f107 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -744,7 +744,8 @@ impl Clone for ServiceResponseHandler { //--- ServiceInvoker -impl ServiceInvoker +impl + ServiceInvoker for ServiceResponseHandler where RequestOctets: Octets + Send + Sync + 'static, From 5be58ea6c519abd92508496798c839175568e6ab Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 12:35:27 +0100 Subject: [PATCH 557/569] Remove unnecessary default type specifications. --- examples/query-routing.rs | 2 +- examples/serve-zone.rs | 23 ++++++++--------------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/examples/query-routing.rs b/examples/query-routing.rs index fc1b2b2da..e0378d278 100644 --- a/examples/query-routing.rs +++ b/examples/query-routing.rs @@ -57,7 +57,7 @@ async fn main() { let conn_service = ClientTransportToSingleService::new(redun); qr.add(Name::>::from_str("nl").unwrap(), conn_service); - let srv = SingleServiceToService::<_, _, _, _>::new(qr); + let srv = SingleServiceToService::new(qr); let srv = MandatoryMiddlewareSvc::new(srv); let my_svc = Arc::new(srv); diff --git a/examples/serve-zone.rs b/examples/serve-zone.rs index 50c3e8d34..9c22e2da0 100644 --- a/examples/serve-zone.rs +++ b/examples/serve-zone.rs @@ -118,16 +118,12 @@ async fn main() { let svc = service_fn(my_service, zones.clone()); #[cfg(feature = "siphasher")] - let svc = XfrMiddlewareSvc::, _, _, _>::new( - svc, - zones_and_diffs.clone(), - 1, - ); + let svc = XfrMiddlewareSvc::new(svc, zones_and_diffs.clone(), 1); let svc = NotifyMiddlewareSvc::new(svc, DemoNotifyTarget); - let svc = CookiesMiddlewareSvc::, _, _>::with_random_secret(svc); - let svc = EdnsMiddlewareSvc::, _, _>::new(svc); - let svc = TsigMiddlewareSvc::<_, _, _, ()>::new(svc, key_store); - let svc = MandatoryMiddlewareSvc::, _, _>::new(svc); + let svc = CookiesMiddlewareSvc::with_random_secret(svc); + let svc = EdnsMiddlewareSvc::new(svc); + let svc = TsigMiddlewareSvc::new(svc, key_store); + let svc = MandatoryMiddlewareSvc::new(svc); let svc = Arc::new(svc); let sock = UdpSocket::bind(&addr).await.unwrap(); @@ -135,18 +131,15 @@ async fn main() { let mut udp_metrics = vec![]; let num_cores = std::thread::available_parallelism().unwrap().get(); for _i in 0..num_cores { - let udp_srv = DgramServer::<_, _, _>::new( - sock.clone(), - VecBufSource, - svc.clone(), - ); + let udp_srv = + DgramServer::new(sock.clone(), VecBufSource, svc.clone()); let metrics = udp_srv.metrics(); udp_metrics.push(metrics); tokio::spawn(async move { udp_srv.run().await }); } let sock = TcpListener::bind(addr).await.unwrap(); - let tcp_srv = StreamServer::<_, _, _>::new(sock, VecBufSource, svc); + let tcp_srv = StreamServer::new(sock, VecBufSource, svc); let tcp_metrics = tcp_srv.metrics(); tokio::spawn(async move { tcp_srv.run().await }); From ad00ee1c9d17d1190e87bb2e9d1da65ca33434a7 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:11:44 +0100 Subject: [PATCH 558/569] Fix compilation failure with --no-default-features. --- src/rdata/dnssec.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/rdata/dnssec.rs b/src/rdata/dnssec.rs index 6b311c6b1..da94e9a54 100644 --- a/src/rdata/dnssec.rs +++ b/src/rdata/dnssec.rs @@ -20,7 +20,6 @@ use octseq::octets::{Octets, OctetsFrom, OctetsInto}; use octseq::parse::Parser; #[cfg(feature = "serde")] use octseq::serde::{DeserializeOctets, SerializeOctets}; -#[cfg(feature = "std")] use time::{Date, Month, PrimitiveDateTime, Time}; use crate::base::cmp::CanonicalOrd; From 0159a6aaa75e282227ab51eb34dfb7ce732c7067 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:25:44 +0100 Subject: [PATCH 559/569] Clippy and cargo fmt. --- src/net/server/connection.rs | 9 ++++++--- src/net/server/dgram.rs | 3 ++- src/net/server/middleware/xfr/service.rs | 8 +++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/net/server/connection.rs b/src/net/server/connection.rs index 62c430971..f6926f0f0 100644 --- a/src/net/server/connection.rs +++ b/src/net/server/connection.rs @@ -1037,7 +1037,8 @@ where status: InvokerStatus, } -impl ServiceResponseHandler +impl + ServiceResponseHandler where RequestOctets: AsRef<[u8]> + Send + Sync, RequestMeta: Clone + Default, @@ -1109,7 +1110,8 @@ where //--- Clone -impl Clone for ServiceResponseHandler +impl Clone + for ServiceResponseHandler where RequestOctets: AsRef<[u8]> + Send + Sync, RequestMeta: Clone + Default, @@ -1127,7 +1129,8 @@ where //--- ServiceInvoker -impl ServiceInvoker +impl + ServiceInvoker for ServiceResponseHandler where RequestOctets: Octets + Send + Sync + 'static, diff --git a/src/net/server/dgram.rs b/src/net/server/dgram.rs index 0a0e71304..296b8f107 100644 --- a/src/net/server/dgram.rs +++ b/src/net/server/dgram.rs @@ -744,7 +744,8 @@ impl Clone for ServiceResponseHandler { //--- ServiceInvoker -impl ServiceInvoker +impl + ServiceInvoker for ServiceResponseHandler where RequestOctets: Octets + Send + Sync + 'static, diff --git a/src/net/server/middleware/xfr/service.rs b/src/net/server/middleware/xfr/service.rs index 83a8f262a..cb4f33f51 100644 --- a/src/net/server/middleware/xfr/service.rs +++ b/src/net/server/middleware/xfr/service.rs @@ -111,9 +111,11 @@ where xfr_data_provider: XDP, max_concurrency: usize, ) -> Self { - let max_concurrency = (max_concurrency > 0) - .then_some(max_concurrency) - .unwrap_or(1); + let max_concurrency = if max_concurrency > 0 { + max_concurrency + } else { + 1 + }; let zone_walking_semaphore = Arc::new(Semaphore::new(max_concurrency)); let batcher_semaphore = Arc::new(Semaphore::new(max_concurrency)); From 6a94e5b62bb5510d7e776cad0390aaa13fc55c08 Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 21 Nov 2025 13:32:00 +0100 Subject: [PATCH 560/569] Add run: cargo test --doc to test doc explicitly. --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a6a4d4ae2..c0918835b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -295,6 +295,7 @@ jobs: # Build and run the test suite. - name: Test run: cargo test --all-targets $DOMAIN_FEATURES + run: cargo test --doc $DOMAIN_FEATURES # Build Cache # ----------- From 7913fd36fbaa893c7320b3f2fccfad5316e2be9d Mon Sep 17 00:00:00 2001 From: Philip Homburg Date: Fri, 21 Nov 2025 13:35:58 +0100 Subject: [PATCH 561/569] Try to fix doc test. --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0918835b..69bf678ab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -295,6 +295,9 @@ jobs: # Build and run the test suite. - name: Test run: cargo test --all-targets $DOMAIN_FEATURES + + # Test docs. + - name: Test docs run: cargo test --doc $DOMAIN_FEATURES # Build Cache From 87770b5fe3ae0161ce442a6b2f21469ca53d5681 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:36:18 +0100 Subject: [PATCH 562/569] Remove unnecessary extra import. --- examples/server-transports.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/server-transports.rs b/examples/server-transports.rs index 159d6a691..94f57ba8b 100644 --- a/examples/server-transports.rs +++ b/examples/server-transports.rs @@ -7,7 +7,6 @@ use core::time::Duration; use std::fs::File; use std::io; use std::io::BufReader; -use std::marker::Unpin; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; From 42cd5a0529f17f7df7605947b6a24aec44222118 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:36:18 +0100 Subject: [PATCH 563/569] Remove unnecessary extra import. --- examples/server-transports.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/server-transports.rs b/examples/server-transports.rs index 159d6a691..94f57ba8b 100644 --- a/examples/server-transports.rs +++ b/examples/server-transports.rs @@ -7,7 +7,6 @@ use core::time::Duration; use std::fs::File; use std::io; use std::io::BufReader; -use std::marker::Unpin; use std::net::SocketAddr; use std::pin::Pin; use std::sync::Arc; From 756a9bcf78b6473cee4c82bda1c471b360fde583 Mon Sep 17 00:00:00 2001 From: Ximon Eighteen <3304436+ximon18@users.noreply.github.com> Date: Fri, 21 Nov 2025 13:40:49 +0100 Subject: [PATCH 564/569] Fix failing doc test. --- src/dnssec/sign/keys/keyset.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/dnssec/sign/keys/keyset.rs b/src/dnssec/sign/keys/keyset.rs index 1fac58dbf..f2e5b0da9 100644 --- a/src/dnssec/sign/keys/keyset.rs +++ b/src/dnssec/sign/keys/keyset.rs @@ -8,7 +8,7 @@ //! ```no_run //! use domain::base::iana::SecurityAlgorithm; //! use domain::base::Name; -//! use domain::dnssec::sign::keys::keyset::{KeySet, RollType, UnixTime}; +//! use domain::dnssec::sign::keys::keyset::{Available, KeySet, RollType, UnixTime}; //! use std::fs::File; //! use std::io::Write; //! use std::str::FromStr; @@ -20,10 +20,10 @@ //! //! // Add two keys. //! ks.add_key_ksk("first KSK.key".to_string(), None, -//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), Available::Available); //! ks.add_key_zsk("first ZSK.key".to_string(), //! Some("first ZSK.private".to_string()), -//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), true); +//! SecurityAlgorithm::ECDSAP256SHA256, 0, UnixTime::now(), Available::Available); //! //! // Save the state. //! let json = serde_json::to_string(&ks).unwrap(); From 14f57d75e9c8a185131f7b10b62d12ac18b85529 Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Thu, 4 Dec 2025 17:27:40 +0100 Subject: [PATCH 565/569] Allow Clippy false positive. (#602) --- src/resolv/lookup/srv.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/resolv/lookup/srv.rs b/src/resolv/lookup/srv.rs index f4a79f292..eec9248f1 100644 --- a/src/resolv/lookup/srv.rs +++ b/src/resolv/lookup/srv.rs @@ -144,6 +144,9 @@ impl FoundSrvs { if self.items.is_err() { let one = mem::replace(&mut self.items, Ok(Vec::new())).unwrap_err(); + + // False positive. -- XXX This whole thing should be re-written. + #[allow(clippy::panicking_unwrap)] self.items.as_mut().unwrap().push(one); } match self.items { From 0b78cec0c0dd687808339e417a8104b50dd351f5 Mon Sep 17 00:00:00 2001 From: Ross MacArthur Date: Mon, 8 Dec 2025 14:49:06 +0200 Subject: [PATCH 566/569] Implement `FreezeBuilder` for compressor targets (#601) This change implements `FreezeBuilder` for `TreeCompressor` and `HashCompressor`, allowing methods like `into_message()` to be used when building messages with these compressors. --- src/base/message_builder.rs | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/base/message_builder.rs b/src/base/message_builder.rs index 21ff700ce..32566c11a 100644 --- a/src/base/message_builder.rs +++ b/src/base/message_builder.rs @@ -2373,6 +2373,15 @@ impl Truncate for TreeCompressor { } } +#[cfg(feature = "std")] +impl FreezeBuilder for TreeCompressor { + type Octets = Target::Octets; + + fn freeze(self) -> Self::Octets { + self.target.freeze() + } +} + //------------ HashCompressor ------------------------------------------------ /// A domain name compressor that uses a hash table. @@ -2639,6 +2648,15 @@ impl Truncate for HashCompressor { } } +#[cfg(feature = "std")] +impl FreezeBuilder for HashCompressor { + type Octets = Target::Octets; + + fn freeze(self) -> Self::Octets { + self.target.freeze() + } +} + //============ Errors ======================================================== /// An error occurred when attempting to add data to a message. @@ -2880,6 +2898,22 @@ mod test { assert_eq!(&expect[..], msg.as_ref()); } + // just check into_message compiles for all compressors + #[test] + fn compressor_into_message() { + let target = StaticCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + + let target = TreeCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + + let target = HashCompressor::new(Vec::new()); + let _msg = + MessageBuilder::from_target(target).unwrap().into_message(); + } + #[test] fn compress_positive_response() { // An example positive response to `A example.com.` that is compressed From 5ca89e76298815dfe9321bd875cabf3ccec49baf Mon Sep 17 00:00:00 2001 From: Martin Hoffmann Date: Mon, 8 Dec 2025 13:51:00 +0100 Subject: [PATCH 567/569] Update changelog. --- Changelog.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Changelog.md b/Changelog.md index 57c5d2328..1711eaf2a 100644 --- a/Changelog.md +++ b/Changelog.md @@ -16,6 +16,8 @@ New record types and added presentation format support for the `SVCB`/`HTTPS` record types. ([#569]) * Add support for the `CAA` record type. ([#434] by [@weilence]) +* Added `FreezeBuilder` to the message compressors. ([#601] by + [@rossmacarthur]) Improvements @@ -76,9 +78,11 @@ Other changes [#570]: https://github.com/NLnetLabs/domain/pull/570 [#593]: https://github.com/NLnetLabs/domain/pull/593 [#594]: https://github.com/NLnetLabs/domain/pull/594 +[#601]: https://github.com/NLnetLabs/domain/pull/601 [@rossmacarthur]: https://github.com/rossmacarthur [@weilence]: https://github.com/weilence [@WhyNotHugo]: https://github.com/WhyNotHugo +[@rossmacarthur]: https://github.com/rossmacarthur ## 0.11.1 From 0ad143becff1f101d70ea4d12da6fdfdfc947c2d Mon Sep 17 00:00:00 2001 From: Philip-NLnetLabs Date: Mon, 15 Dec 2025 12:26:15 +0100 Subject: [PATCH 568/569] Update tokio-stream to 0.1.17 because 0.1.1 results in an error in CI. (#604) --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 76e39ec2e..6ce8e8f07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,7 +48,7 @@ siphasher = { version = "1", optional = true } smallvec = { version = "1.3", optional = true } tokio = { version = "1.33", optional = true, features = ["io-util", "macros", "net", "time", "sync", "rt-multi-thread" ] } tokio-rustls = { version = "0.26", optional = true, default-features = false } -tokio-stream = { version = "0.1.1", optional = true } +tokio-stream = { version = "0.1.17", optional = true } tracing = { version = "0.1.40", optional = true, features = ["log"] } tracing-subscriber = { version = "0.3.18", optional = true, features = ["env-filter"] } From 1e1d3fb4360f03a1bc8bcb949f5f41cb0fe8223c Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Mon, 15 Dec 2025 12:41:10 +0100 Subject: [PATCH 569/569] make tsig::RequestMessage implement Clone (#603) --- src/net/client/tsig.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/net/client/tsig.rs b/src/net/client/tsig.rs index 277690dce..2fbb5b70e 100644 --- a/src/net/client/tsig.rs +++ b/src/net/client/tsig.rs @@ -510,7 +510,7 @@ enum RequestState { /// the request prior to sending it, e.g. to assign a message ID or to add /// EDNS options, and signing **MUST** be the last modification made to the /// message prior to sending. -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct RequestMessage where CR: Send + Sync,