From 4022c81f8cc1300e17865aa653637127bfbec553 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 001/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] 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/415] 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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] 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/415] - 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/415] 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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] 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/415] 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/415] [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/415] [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/415] 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/415] [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/415] [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/415] [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/415] 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/415] [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/415] [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/415] 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/415] 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/415] 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/415] [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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] [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/415] [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/415] [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/415] [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/415] 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/415] 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/415] [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/415] [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/415] [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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] - 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] - 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/415] 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/415] 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/415] 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/415] 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/415] - 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] 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/415] - 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/415] - 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/415] 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/415] 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/415] 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/415] 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/415] 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 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 401/415] 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 402/415] 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 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 403/415] 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 404/415] 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 405/415] 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 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 406/415] 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 407/415] 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 408/415] - 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 409/415] 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 410/415] 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 411/415] 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 412/415] - 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 413/415] 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 414/415] 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 415/415] 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()) + } + } }