From 4022c81f8cc1300e17865aa653637127bfbec553 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 001/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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/191] 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/191] [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/191] 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/191] [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/191] [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/191] [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/191] 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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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/191] 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/191] 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/191] 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/191] 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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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/191] 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/191] [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/191] 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/191] [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/191] [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/191] [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/191] 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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] [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/191] 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 48c006c0f122ff18b3e2760205755e4d5f7cfe03 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 067/191] [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 068/191] [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 069/191] [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 070/191] [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 071/191] [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 072/191] [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 073/191] [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 074/191] 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 075/191] [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 076/191] [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 077/191] [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 078/191] [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 079/191] [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 080/191] 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 081/191] 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 082/191] [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 083/191] 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 084/191] [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 085/191] [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 086/191] [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 087/191] 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 088/191] [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 089/191] [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 090/191] [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 091/191] [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 092/191] [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 093/191] [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 094/191] [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 095/191] [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 096/191] [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 097/191] 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 098/191] 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 099/191] [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 100/191] 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 101/191] [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 102/191] [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 103/191] [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 6388387c679b69fd6dcbbc2476b50909aa5b9e18 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 16 Oct 2024 11:59:41 +0200 Subject: [PATCH 104/191] [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 105/191] [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 106/191] [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 107/191] [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 5ba894083d86b93b73fe5286a67d986fa851812a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 2 Oct 2024 13:42:01 +0200 Subject: [PATCH 108/191] [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 109/191] [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 110/191] [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 111/191] [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 112/191] [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 113/191] [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 114/191] [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 115/191] 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 116/191] [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 117/191] [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 118/191] [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 119/191] [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 120/191] [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 121/191] 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 122/191] 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 123/191] [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 124/191] 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 125/191] [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 126/191] [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 127/191] [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 128/191] 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 129/191] [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 130/191] [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 131/191] [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 132/191] [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 133/191] [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 134/191] [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 135/191] [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 136/191] [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 137/191] [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 138/191] 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 139/191] 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 140/191] [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 141/191] 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 142/191] [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 143/191] [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 144/191] [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 145/191] [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 146/191] [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 147/191] [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 148/191] [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 149/191] [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 150/191] [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 151/191] [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 152/191] [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 153/191] [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 154/191] 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 155/191] [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 156/191] [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 157/191] [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 158/191] [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 159/191] [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 160/191] 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 161/191] [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 162/191] [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 a4316b5ff4334478a3dd98ee60e53c1a2a369e4a Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 13:59:46 +0100 Subject: [PATCH 163/191] [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 164/191] [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 48e178a1e1541b74ba48b5aa9780e719d5d8cc92 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Tue, 29 Oct 2024 14:18:04 +0100 Subject: [PATCH 165/191] [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 166/191] [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 167/191] [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 61bc3aa82fe45a5473cfc33055a41dd399a9eb78 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Wed, 30 Oct 2024 10:24:58 +0100 Subject: [PATCH 168/191] [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 169/191] [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 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 170/191] 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 171/191] 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 172/191] [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 173/191] 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 174/191] 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 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 175/191] 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 176/191] 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 177/191] 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 7c9ee4c668e88facf0a1829601b4ace0a73ce690 Mon Sep 17 00:00:00 2001 From: arya dradjica Date: Mon, 4 Nov 2024 09:46:08 +0100 Subject: [PATCH 178/191] [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 179/191] [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 180/191] [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 181/191] [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 182/191] 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 183/191] 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 184/191] [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 185/191] [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 186/191] [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 187/191] 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 188/191] 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 189/191] 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 190/191] 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 191/191] 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") + } } } }