#![cfg(any(feature = "ring", feature = "openssl"))]
#![cfg_attr(docsrs, doc(cfg(any(feature = "ring", feature = "openssl"))))]
#![warn(missing_docs)]
#![warn(clippy::missing_docs_in_private_items)]
use crate::base::iana::{Class, Nsec3HashAlgorithm};
use crate::base::scan::{IterScanner, Scanner, ScannerError};
use crate::base::wire::Composer;
use crate::base::zonefile_fmt::{DisplayKind, ZonefileFmt};
use crate::base::{Name, Record, Rtype, ToName, Ttl};
use crate::crypto::common::{DigestBuilder, DigestType};
use crate::dep::octseq::{
EmptyBuilder, FromBuilder, OctetsBuilder, Truncate,
};
use crate::rdata::nsec3::{Nsec3Salt, OwnerHash};
use crate::rdata::{Dnskey, Nsec3param};
use std::error;
use std::fmt;
use std::str::FromStr;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Nsec3HashError {
UnsupportedAlgorithm,
AppendError,
OwnerHashError,
CollisionDetected,
MissingHash,
}
impl std::fmt::Display for Nsec3HashError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Nsec3HashError::UnsupportedAlgorithm => {
f.write_str("Unsupported algorithm")
}
Nsec3HashError::AppendError => {
f.write_str("Append error: out of memory?")
}
Nsec3HashError::OwnerHashError => {
f.write_str("Hashing produced an invalid owner hash")
}
Nsec3HashError::CollisionDetected => {
f.write_str("Hash collision detected")
}
Nsec3HashError::MissingHash => {
f.write_str("Missing hash for owner name")
}
}
}
}
pub fn nsec3_default_hash<N, HashOcts>(
owner: N,
) -> Result<OwnerHash<HashOcts>, Nsec3HashError>
where
N: ToName,
HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate,
for<'a> HashOcts: From<&'a [u8]>,
{
let params = Nsec3param::<HashOcts>::default();
nsec3_hash(
owner,
params.hash_algorithm(),
params.iterations(),
params.salt(),
)
}
pub fn nsec3_hash<N, SaltOcts, HashOcts>(
owner: N,
algorithm: Nsec3HashAlgorithm,
iterations: u16,
salt: &Nsec3Salt<SaltOcts>,
) -> Result<OwnerHash<HashOcts>, Nsec3HashError>
where
N: ToName,
SaltOcts: AsRef<[u8]>,
HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate,
for<'a> HashOcts: From<&'a [u8]>,
{
if algorithm != Nsec3HashAlgorithm::SHA1 {
return Err(Nsec3HashError::UnsupportedAlgorithm);
}
fn mk_hash<N, SaltOcts, HashOcts>(
owner: N,
iterations: u16,
salt: &Nsec3Salt<SaltOcts>,
) -> Result<HashOcts, HashOcts::AppendError>
where
N: ToName,
SaltOcts: AsRef<[u8]>,
HashOcts: AsRef<[u8]> + EmptyBuilder + OctetsBuilder + Truncate,
for<'a> HashOcts: From<&'a [u8]>,
{
let mut canonical_owner = HashOcts::empty();
owner.compose_canonical(&mut canonical_owner)?;
let mut ctx = DigestBuilder::new(DigestType::Sha1);
ctx.update(canonical_owner.as_ref());
ctx.update(salt.as_slice());
let mut h = ctx.finish();
for _ in 0..iterations {
let mut ctx = DigestBuilder::new(DigestType::Sha1);
ctx.update(h.as_ref());
ctx.update(salt.as_slice());
h = ctx.finish();
}
Ok(h.as_ref().into())
}
let hash = mk_hash(owner, iterations, salt)
.map_err(|_| Nsec3HashError::AppendError)?;
let owner_hash = OwnerHash::from_octets(hash)
.map_err(|_| Nsec3HashError::OwnerHashError)?;
Ok(owner_hash)
}
pub fn parse_from_bind<Octs>(
data: &str,
) -> Result<Record<Name<Octs>, Dnskey<Octs>>, ParseDnskeyTextError>
where
Octs: FromBuilder,
Octs::Builder: EmptyBuilder + Composer,
{
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(';') {
line = line
.split_once(';')
.map_or(line, |(line, _)| line)
.trim_end();
return Some((line, data));
}
}
None
}
let (line, rest) = next_line(data).ok_or(ParseDnskeyTextError)?;
if next_line(rest).is_some() {
return Err(ParseDnskeyTextError);
}
let mut scanner = IterScanner::new(line.split_ascii_whitespace());
let name = scanner.scan_name().map_err(|_| ParseDnskeyTextError)?;
let opt_ttl = scanner
.scan_ascii_str(|s| {
if let Ok(ttl) = u32::from_str(s) {
Ok(Some(Ttl::from_secs(ttl)))
} else if Class::from_str(s).is_ok() {
Ok(None)
} else {
Err(ScannerError::custom("TTL or Class expected"))
}
})
.map_err(|_| ParseDnskeyTextError)?;
if opt_ttl.is_some() {
let _ =
Class::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?;
}
if Rtype::scan(&mut scanner).map_or(true, |t| t != Rtype::DNSKEY) {
return Err(ParseDnskeyTextError);
}
let data =
Dnskey::scan(&mut scanner).map_err(|_| ParseDnskeyTextError)?;
Ok(Record::new(
name,
Class::IN,
opt_ttl.unwrap_or(Ttl::ZERO),
data,
))
}
fn format_as_bind<N, O>(
record: &Record<N, Dnskey<O>>,
mut w: impl fmt::Write,
) -> fmt::Result
where
N: ToName,
O: AsRef<[u8]>,
{
writeln!(
w,
"{} IN DNSKEY {}",
record.owner().fmt_with_dot(),
record.data().display_zonefile(DisplayKind::Simple),
)
}
pub fn display_as_bind<N, O>(
record: &Record<N, Dnskey<O>>,
) -> impl fmt::Display + '_
where
N: ToName,
O: AsRef<[u8]>,
{
struct Display<'a, N, O>(&'a Record<N, Dnskey<O>>);
impl<N, O> fmt::Display for Display<'_, N, O>
where
N: ToName,
O: AsRef<[u8]>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
format_as_bind(self.0, f)
}
}
Display(record)
}
#[derive(Clone, Debug)]
pub struct ParseDnskeyTextError;
impl fmt::Display for ParseDnskeyTextError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("misformatted DNSKEY record")
}
}
impl error::Error for ParseDnskeyTextError {}
#[cfg(test)]
#[cfg(feature = "std")]
mod test {
use std::string::ToString;
use std::vec::Vec;
use crate::base::iana::SecurityAlgorithm;
use crate::dnssec::common::{display_as_bind, parse_from_bind};
const KEYS: &[(SecurityAlgorithm, u16, usize)] = &[
(SecurityAlgorithm::RSASHA1, 439, 2048),
(SecurityAlgorithm::RSASHA1_NSEC3_SHA1, 22204, 2048),
(SecurityAlgorithm::RSASHA256, 60616, 2048),
(SecurityAlgorithm::ECDSAP256SHA256, 42253, 256),
(SecurityAlgorithm::ECDSAP384SHA384, 33566, 384),
(SecurityAlgorithm::ED25519, 56037, 256),
(SecurityAlgorithm::ED448, 7379, 456),
];
#[test]
fn test_parse_from_bind() {
for &(algorithm, key_tag, _) in KEYS {
let name =
format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag);
let path = format!("test-data/dnssec-keys/K{}.key", name);
let data = std::fs::read_to_string(path).unwrap();
let _ = parse_from_bind::<Vec<u8>>(&data).unwrap();
}
}
#[test]
fn test_parse_from_bind_ttl() {
{
let &(algorithm, key_tag, _) =
&(SecurityAlgorithm::RSASHA256, 60616, 2048);
let name =
format!("test-ttl.+{:03}+{:05}", algorithm.to_int(), key_tag);
let path = format!("test-data/dnssec-keys/K{}.key", name);
let data = std::fs::read_to_string(path).unwrap();
let _ = parse_from_bind::<Vec<u8>>(&data).unwrap();
}
}
#[test]
fn key_tag() {
for &(algorithm, key_tag, _) in KEYS {
let name =
format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag);
let path = format!("test-data/dnssec-keys/K{}.key", name);
let data = std::fs::read_to_string(path).unwrap();
let key = parse_from_bind::<Vec<u8>>(&data).unwrap();
assert_eq!(key.data().key_tag(), key_tag);
}
}
#[test]
fn bind_format_roundtrip() {
for &(algorithm, key_tag, _) in KEYS {
let name =
format!("test.+{:03}+{:05}", algorithm.to_int(), key_tag);
let path = format!("test-data/dnssec-keys/K{}.key", name);
let data = std::fs::read_to_string(path).unwrap();
let key = parse_from_bind::<Vec<u8>>(&data).unwrap();
let bind_fmt_key = display_as_bind(&key).to_string();
let same = parse_from_bind::<Vec<u8>>(&bind_fmt_key).unwrap();
assert_eq!(key, same);
}
}
}