Files
NLnetLabs-dnst/src/commands/signzone.rs
T

2819 lines
124 KiB
Rust

use core::cmp::Ordering;
use core::fmt::Write;
use core::ops::Add;
use core::str::FromStr;
use std::cmp::min;
use std::collections::{HashMap, HashSet};
use std::ffi::OsString;
use std::fmt::{self, Display};
use std::fs::File;
use std::hash::RandomState;
use std::io::{self, BufWriter};
use std::path::{Path, PathBuf};
use bytes::{BufMut, Bytes, BytesMut};
use clap::builder::ValueParser;
use domain::base::iana::nsec3::Nsec3HashAlg;
use domain::base::iana::zonemd::{ZonemdAlg, ZonemdScheme};
use domain::base::name::FlattenInto;
use domain::base::zonefile_fmt::{self, Formatter, ZonefileFmt};
use domain::base::{
CanonicalOrd, Name, NameBuilder, Record, RecordData, Rtype, Serial, ToName, Ttl,
};
use domain::rdata::dnssec::Timestamp;
use domain::rdata::nsec3::Nsec3Salt;
use domain::rdata::{Dnskey, Nsec3, Nsec3param, Rrsig, Soa, ZoneRecordData, Zonemd};
use domain::sign::common::{FromBytesError, KeyPair};
use domain::sign::records::{
DefaultSigningKeyUsageStrategy, DnssecSigningKey, Family, FamilyName, IntendedKeyPurpose,
Nsec3OptOut, Nsec3Records, RecordsIter, Signer, SigningKeyUsageStrategy, SortedRecords, Sorter,
};
use domain::sign::{SecretKeyBytes, SigningKey};
use domain::utils::base64;
use domain::validate::Key;
use domain::zonefile::inplace::{self, Entry};
use domain::zonetree::types::StoredRecordData;
use domain::zonetree::{StoredName, StoredRecord};
use lexopt::Arg;
use octseq::builder::with_infallible;
use ring::digest;
use crate::env::{Env, Stream};
use crate::error::{Context, Error};
use crate::{Args, DISPLAY_KIND};
use super::nsec3hash::Nsec3Hash;
use super::{parse_os, parse_os_with, Command, LdnsCommand};
//------------ Constants -----------------------------------------------------
const FOUR_WEEKS: u32 = 2419200;
//------------ SignZone ------------------------------------------------------
#[derive(Clone, Debug, clap::Args, PartialEq)]
#[clap(
after_help = "Keys must be specified by their base name (usually K<name>+<alg>+<id>), i.e. WITHOUT the .private or .key extension.
If the public part of the key is not present in the zone, the DNSKEY RR will be read from the file called <base name>.key.
A date can be a timestamp (seconds since the epoch), or of the form <YYYYMMdd[hhmmss]>
"
)]
pub struct SignZone {
// -----------------------------------------------------------------------
// Original ldns-signzone options in ldns-signzone -h order:
// -----------------------------------------------------------------------
/// Use layout in signed zone and print comments on DNSSEC records
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
short = 'b',
default_value_t = false
)]
extra_comments: bool,
/// Used keys are not added to the zone
#[arg(short = 'd', default_value_t = false)]
do_not_add_keys_to_zone: bool,
/// Expiration date [default: 4 weeks from now]
// Default is not documented in ldns-signzone -h or man ldns-signzone but
// in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses
// now + 4 weeks if no expiration timestamp is specified.
#[arg(
short = 'e',
value_name = "date",
default_value_t = Timestamp::now().into_int().add(FOUR_WEEKS).into(),
hide_default_value = true,
value_parser = ValueParser::new(SignZone::parse_timestamp),
)]
expiration: Timestamp,
/// Output zone to file [default: <zonefile>.signed]
///
/// Use '-f -' to output to stdout.
#[arg(short = 'f', value_name = "file")]
out_file: Option<PathBuf>,
/// Inception date [default: now]
// Default is not documented in ldns-signzone -h or man ldns-signzone but
// in code (see ldns/dnssec_sign.c::ldns_create_empty_rrsig()) LDNS uses
// now if no inception timestamp is specified.
#[arg(
short = 'i',
value_name = "date",
default_value_t = Timestamp::now(),
hide_default_value = true,
value_parser = ValueParser::new(SignZone::parse_timestamp),
)]
inception: Timestamp,
/// Origin for the zone (for zonefiles with relative names and no $ORIGIN)
#[arg(short = 'o', value_name = "domain")]
origin: Option<Name<Bytes>>,
/// Set SOA serial to the number of seconds since Jan 1st 1970
///
/// If this would NOT result in the SOA serial increasing it will be
/// incremented instead.
#[arg(short = 'u', default_value_t = false)]
set_soa_serial_to_epoch_time: bool,
// SKIPPED: -v
// This should be handled at the dnst top level, not per subcommand.
/// Add a ZONEMD resource record
///
/// <hash> currently supports "SHA384" (1) or "SHA512" (2).
/// <scheme> currently only supports "SIMPLE" (1).
///
/// Can occur more than once, but only one per unique scheme and hash
/// tuple will be added.
#[arg(
short = 'z',
value_name = "[scheme:]hash",
value_parser = Self::parse_zonemd_tuple,
action = clap::ArgAction::Append
)]
// Clap doesn't support HashSet (without complex workarounds), therefore
// the uniqueness of the tuples need to be checked at runtime.
zonemd: Vec<ZonemdTuple>,
/// Allow ZONEMDs to be added without signing
#[arg(short = 'Z', requires = "zonemd")]
allow_zonemd_without_signing: bool,
/// Sign DNSKEYs with all keys instead of minimal
#[arg(short = 'A', default_value_t = false)]
sign_dnskeys_with_all_keys: bool,
/// Sign with every unique algorithm in the provided keys
#[arg(short = 'U', default_value_t = false)]
sign_with_every_unique_algorithm: bool,
/// Use NSEC3 instead of NSEC
#[arg(short = 'n', default_value_t = false, group = "nsec3")]
use_nsec3: bool,
/// Hashing algorithm
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 'a',
value_name = "algorithm",
default_value = "SHA-1",
value_parser = ValueParser::new(Nsec3Hash::parse_nsec3_alg),
requires = "nsec3"
)]
algorithm: Nsec3HashAlg,
/// Number of hash iterations
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 't',
value_name = "number",
default_value_t = 0,
requires = "nsec3"
)]
iterations: u16,
/// Salt
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 's',
value_name = "string",
default_value_t = Nsec3Salt::empty(),
requires = "nsec3"
)]
salt: Nsec3Salt<Bytes>,
/// Set the opt-out flag on all NSEC3 RRs
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 'p',
default_value_t = false,
requires = "nsec3",
conflicts_with = "nsec3_opt_out"
)]
nsec3_opt_out_flags_only: bool,
// -----------------------------------------------------------------------
// Extra options not supported by the original ldns-signzone:
// -----------------------------------------------------------------------
/// Set the opt-out flag on all NSEC3 RRs and skip unsigned delegations
#[arg(
help_heading = Some("NSEC3 (when using '-n')"),
short = 'P',
default_value_t = false,
requires = "nsec3",
conflicts_with = "nsec3_opt_out_flags_only"
)]
nsec3_opt_out: bool,
/// Hash only, don't sign
#[arg(short = 'H', default_value_t = false)]
hash_only: bool,
/// Do not require that key names match the apex.
#[arg(short = 'M', default_value_t = false)]
no_require_keys_match_apex: bool,
/// Output YYYYMMDDHHmmSS RRSIG timestamps instead of seconds since epoch.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
short = 'T',
default_value_t = false
)]
use_yyyymmddhhmmss_rrsig_format: bool,
/// Preceed the zone output by a list that contains the NSEC3 hashes of the
/// original ownernames.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
short = 'L',
default_value_t = false,
requires = "nsec3"
)]
preceed_zone_with_hash_list: bool,
/// Order RRSIG RRs by the record type that they cover.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
short = 'R',
default_value_ifs([
("extra_comments", "false", Some("false")),
("extra_comments", "true", Some("true"))
]),
)]
order_rrsigs_after_the_rtype_they_cover: bool,
/// Order NSEC3 RRs by unhashed owner name.
#[arg(
help_heading = Some("OUTPUT FORMATTING"),
short = 'O',
default_value_t = false,
requires = "nsec3",
default_value_ifs([
("extra_comments", "false", Some("false")),
("extra_comments", "true", Some("true"))
]),
)]
order_nsec3_rrs_by_unhashed_owner_name: bool,
// -----------------------------------------------------------------------
// Original ldns-signzone positional arguments in position order:
// -----------------------------------------------------------------------
/// The zonefile to sign
#[arg(value_name = "zonefile")]
zonefile_path: PathBuf,
/// The keys to sign the zone with
// May be omitted if -Z or -H are given
#[arg(value_name = "key", required_unless_present_any = ["allow_zonemd_without_signing", "hash_only"])]
key_paths: Vec<PathBuf>,
// -----------------------------------------------------------------------
// Non-command line argument fields:
// -----------------------------------------------------------------------
/// Whether or not we were invoked as `ldns-signzone`.
#[arg(skip)]
invoked_as_ldns: bool,
}
const LDNS_HELP: &str = r###"ldns-signzone [OPTIONS] zonefile key [key [key]]
signs the zone with the given key(s)
-b use layout in signed zone and print comments DNSSEC records
-d used keys are not added to the zone
-e <date> expiration date
-f <file> output zone to file (default <name>.signed)
-i <date> inception date
-o <domain> origin for the zone
-u set SOA serial to the number of seconds since 1-1-1970
-v print version and exit
-z <[scheme:]hash> Add ZONEMD resource record
<scheme> should be "simple" (or 1)
<hash> should be "sha384" or "sha512" (or 1 or 2)
this option can be given more than once
-Z Allow ZONEMDs to be added without signing
-A sign DNSKEY with all keys instead of minimal
-U Sign with every unique algorithm in the provided keys
-n use NSEC3 instead of NSEC.
If you use NSEC3, you can specify the following extra options:
-a [algorithm] hashing algorithm
-t [number] number of hash iterations
-s [string] salt
-p set the opt-out flag on all nsec3 rrs
-L Preceed the zone output by a list of NSEC3 owners and hashes.
keys must be specified by their base name (usually K<name>+<alg>+<id>),
i.e. WITHOUT the .private extension.
If the public part of the key is not present in the zone, the DNSKEY RR
will be read from the file called <base name>.key.
A date can be a timestamp (seconds since the epoch), or of
the form <YYYYMMdd[hhmmss]>
"###;
impl LdnsCommand for SignZone {
const NAME: &'static str = "signzone";
const HELP: &'static str = LDNS_HELP;
const COMPATIBLE_VERSION: &'static str = "1.8.4";
fn parse_ldns<I: IntoIterator<Item = OsString>>(args: I) -> Result<Args, Error> {
let mut extra_comments = false;
let mut do_not_add_keys_to_zone = false;
let mut expiration = Timestamp::now().into_int().add(FOUR_WEEKS).into();
let mut out_file = Option::<PathBuf>::None;
let mut inception = Timestamp::now();
let mut origin = Option::<Name<Bytes>>::None;
let mut set_soa_serial_to_epoch_time = false;
let mut zonemd = Vec::new();
let mut allow_zonemd_without_signing = false;
let mut sign_dnskeys_with_all_keys = false;
let mut sign_with_every_unique_algorithm = false;
let mut use_nsec3 = false;
let mut algorithm = Nsec3HashAlg::SHA1;
let mut iterations = 1u16;
let mut salt = Nsec3Salt::<Bytes>::empty();
let mut nsec3_opt_out_flags_only = false;
let mut preceed_zone_with_hash_list = false;
let mut key_paths = Vec::<PathBuf>::new();
let mut zonefile = Option::<PathBuf>::None;
let mut parser = lexopt::Parser::from_args(args);
while let Some(arg) = parser.next()? {
match arg {
Arg::Short('b') => {
extra_comments = true;
}
Arg::Short('d') => {
do_not_add_keys_to_zone = true;
}
Arg::Short('e') => {
let val = parser.value()?;
// LDNS treats 0 as unset.
let val_as_num = usize::from_str(val.to_str().unwrap_or_default());
if val_as_num.is_err() || val_as_num.unwrap() > 0 {
expiration = parse_os_with("-e", &val, SignZone::parse_timestamp)?;
}
}
Arg::Short('f') => {
let val = parser.value()?;
out_file = Some(parse_os("-f", &val)?);
}
Arg::Short('i') => {
let val = parser.value()?;
// LDNS treats 0 as unset.
let val_as_num = usize::from_str(val.to_str().unwrap_or_default());
if val_as_num.is_err() || val_as_num.unwrap() > 0 {
inception = parse_os_with("-e", &val, SignZone::parse_timestamp)?;
}
}
Arg::Short('o') => {
let val = parser.value()?;
origin = Some(parse_os("-o", &val)?);
}
Arg::Short('u') => {
set_soa_serial_to_epoch_time = true;
}
Arg::Short('z') => {
let val = parser.value()?;
zonemd.push(parse_os_with(
"-z",
&val,
SignZone::parse_zonemd_tuple_ldns,
)?);
}
Arg::Short('Z') => {
allow_zonemd_without_signing = true;
}
Arg::Short('A') => {
sign_dnskeys_with_all_keys = true;
}
Arg::Short('U') => {
sign_with_every_unique_algorithm = true;
}
Arg::Short('v') => {
return Ok(Self::report_version());
}
Arg::Short('n') => {
use_nsec3 = true;
}
Arg::Short('a') => {
let val = parser.value()?;
algorithm = parse_os_with("-a", &val, Nsec3Hash::parse_nsec3_alg)?;
}
Arg::Short('t') => {
let val = parser.value()?;
iterations = parse_os("-t", &val)?;
}
Arg::Short('s') => {
let val = parser.value()?;
salt = parse_os("-s", &val)?;
}
Arg::Short('p') => {
nsec3_opt_out_flags_only = true;
}
Arg::Short('L') => {
preceed_zone_with_hash_list = true;
}
Arg::Value(val) => {
if zonefile.is_none() {
zonefile = Some(parse_os("zonefile", &val)?);
} else {
key_paths.push(parse_os("key", &val)?);
}
}
Arg::Short(x) => return Err(format!("Invalid short option: -{x}").into()),
Arg::Long(x) => {
return Err(format!("Long options are not supported, but `--{x}` given").into())
}
}
}
let Some(zonefile_path) = zonefile else {
return Err("Missing zonefile argument".into());
};
if let Some(out_file) = &out_file {
if out_file.as_os_str() == "-" {
extra_comments = false;
}
}
// Logically this should also check that zonemd flags are given, but
// ldns-signzone just copies the unsigned zone (without comments) when
// using only -Z (without -z).
if key_paths.is_empty() && !allow_zonemd_without_signing {
return Err("Missing key argument".into());
};
preceed_zone_with_hash_list &= extra_comments && use_nsec3;
Ok(Args::from(Command::SignZone(Self {
extra_comments,
do_not_add_keys_to_zone,
expiration,
out_file,
inception,
origin,
set_soa_serial_to_epoch_time,
zonemd,
allow_zonemd_without_signing,
sign_dnskeys_with_all_keys,
sign_with_every_unique_algorithm,
use_nsec3,
algorithm,
iterations,
salt,
nsec3_opt_out_flags_only,
nsec3_opt_out: false,
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list,
order_rrsigs_after_the_rtype_they_cover: extra_comments,
order_nsec3_rrs_by_unhashed_owner_name: extra_comments,
zonefile_path,
key_paths,
no_require_keys_match_apex: false,
invoked_as_ldns: true,
})))
}
}
impl SignZone {
fn parse_zonemd_tuple(arg: &str) -> Result<ZonemdTuple, Error> {
let scheme;
let hash_alg;
if let Some((s, h)) = arg.split_once(':') {
scheme = if let Ok(num) = s.parse() {
Self::num_to_zonemd_scheme(num)
} else {
ZonemdScheme::from_mnemonic(s.as_bytes()).ok_or("unknown ZONEMD scheme mnemonic")
}?;
hash_alg = h;
} else {
scheme = ZonemdScheme::SIMPLE;
hash_alg = arg
};
let hash_alg = if let Ok(num) = hash_alg.parse() {
Self::num_to_zonemd_alg(num)
} else {
ZonemdAlg::from_mnemonic(hash_alg.as_bytes()).ok_or("unknown ZONEMD algorithm mnemonic")
}?;
Ok(ZonemdTuple(scheme, hash_alg))
}
pub fn num_to_zonemd_alg(num: u8) -> Result<ZonemdAlg, &'static str> {
let alg = ZonemdAlg::from_int(num);
match alg.to_mnemonic() {
Some(_) => Ok(alg),
None => Err("unknown ZONEMD algorithm number"),
}
}
pub fn num_to_zonemd_scheme(num: u8) -> Result<ZonemdScheme, &'static str> {
let alg = ZonemdScheme::from_int(num);
match alg.to_mnemonic() {
Some(_) => Ok(alg),
None => Err("unknown ZONEMD scheme number"),
}
}
fn parse_zonemd_tuple_ldns(arg: &str) -> Result<ZonemdTuple, Error> {
let scheme;
let hash_alg;
fn parse_zonemd_scheme_ldns(s: &str) -> Result<ZonemdScheme, Error> {
match s.to_lowercase().as_str() {
"simple" | "1" => Ok(ZonemdScheme::SIMPLE),
_ => Err("unknown ZONEMD scheme name or number".into()),
}
}
fn parse_zonemd_hash_alg_ldns(h: &str) -> Result<ZonemdAlg, Error> {
match h.to_lowercase().as_str() {
"sha384" | "1" => Ok(ZonemdAlg::SHA384),
"sha512" | "2" => Ok(ZonemdAlg::SHA512),
_ => Err("unknown ZONEMD algorithm name or number".into()),
}
}
if let Some((s, h)) = arg.split_once(':') {
scheme = parse_zonemd_scheme_ldns(s)?;
hash_alg = parse_zonemd_hash_alg_ldns(h)?;
} else {
scheme = ZonemdScheme::SIMPLE;
hash_alg = parse_zonemd_hash_alg_ldns(arg)?;
};
Ok(ZonemdTuple(scheme, hash_alg))
}
pub fn parse_timestamp(arg: &str) -> Result<Timestamp, Error> {
// We can't just use Timestamp::from_str from the domain crate because
// ldns-signzone treats YYYYMMDD as a special case and domain does
// not. For invalid values this YYYYMMDDD prevents use of valid Unix
// timestamps that have the same value, e.g. ldns-signzone complains
// that for 99999999 "The month must be in the range 1 to 12". There's
// also no checking that an expiration timestamp is in the future of
// an inception timestamp (which for serial numbers is hard to say for
// sure but for YYYYMMDD or YYYYMMDDHHmmSS we could check).
let res = if arg.len() == 8 && arg.parse::<u32>().is_ok() {
// This can give strange errors, e.g. 99999999 warns about illegal
// signature time, but the alternative would be to add a
// dependency on chrono and parse the value ourselves in order to
// produce a better error message. Given that this only happens
// for very old or far future Unix timestamps we don't attempt to
// do better than this for now.
Timestamp::from_str(&format!("{arg}000000"))
} else {
Timestamp::from_str(arg)
};
res.map_err(|err| Error::from(format!("Invalid timestamp: {err}")))
}
pub fn execute(self, env: impl Env) -> Result<(), Error> {
eprintln!("-L = {}", self.preceed_zone_with_hash_list);
// Post-process arguments.
// TODO: Can Clap do this for us?
let opt_out = if self.nsec3_opt_out {
Nsec3OptOut::OptOut
} else if self.nsec3_opt_out_flags_only {
Nsec3OptOut::OptOutFlagsOnly
} else {
Nsec3OptOut::NoOptOut
};
let signing_mode = if self.hash_only {
SigningMode::HashOnly
} else if self.key_paths.is_empty() {
if self.allow_zonemd_without_signing {
SigningMode::None
} else {
return Err("Missing key argument".into());
}
} else {
SigningMode::HashAndSign
};
let out_file = if let Some(out_file) = &self.out_file {
out_file.clone()
} else {
let out_file = format!("{}.signed", self.zonefile_path.display());
PathBuf::from_str(&out_file)
.map_err(|err| format!("Cannot write to {out_file}: {err}"))?
};
let writer = if out_file.as_os_str() == "-" {
FileOrStdout::Stdout(env.stdout())
} else {
let file = File::create(env.in_cwd(&out_file))?;
let file = BufWriter::new(file);
FileOrStdout::File(file)
};
// ldns-signzone only shows these warnings if verbosity < 1 but offers
// no way to configure the verbosity level. I assume the intent was to
// add support for a -q (--quiet) option or similar but that was never
// done.
match self.iterations {
500.. => Self::write_extreme_iterations_warning(&env),
100.. if self.invoked_as_ldns => Self::write_large_iterations_warning(&env),
1.. if !self.invoked_as_ldns => Self::write_non_zero_iterations_warning(&env),
_ => { /* Good, nothing to warn about */ }
}
// Read the zone file.
let records = self.load_zone(&env.in_cwd(&self.zonefile_path))?;
// Extract the SOA RR from the loaded zone.
let Some(soa_rr) = records.find_soa() else {
return Err(format!(
"Zone file '{}' does not contain a SOA record",
self.zonefile_path.display()
)
.into());
};
let ZoneRecordData::Soa(_) = soa_rr.first().data() else {
return Err(format!(
"Zone file '{}' contains an invalid SOA record",
self.zonefile_path.display()
)
.into());
};
// Extract and validate the DNSKEY RRs from the loaded zone.
let mut found_public_keys = vec![];
for rr in records.iter() {
if let ZoneRecordData::Dnskey(dnskey) = rr.data() {
// Create a public key object from the found DNSKEY RR.
let public_key =
Key::from_dnskey(rr.owner().clone(), dnskey.clone()).map_err(|err| {
Error::from(format!(
"Zone file '{}' DNSKEY record '{dnskey}' is invalid: {err}",
self.zonefile_path.display()
))
})?;
found_public_keys.push(public_key);
}
}
// Load the specified private keys, match them against the found
// public keys, failing that load a DNSKEY RR from the corresponding
// public key file and validate that its owner matches that of the
// zone apex. Unlike ldns-signzone we don't use a generated public key
// if these attempts fail.
let mut signing_keys: Vec<DnssecSigningKey<Bytes, KeyPair>> = vec![];
'next_key_path: for key_path in &self.key_paths {
let key_path = env.in_cwd(key_path).into_owned();
// Load the private key.
let private_key_path = Self::mk_private_key_path(&key_path);
let private_key = Self::load_private_key(&env.in_cwd(&private_key_path))?;
// Note: Our behaviour differs to that of the original
// ldns-signzone because we are unable at the time of writing to
// generate a public key from a private key. As such we cannot
// compare the key tag of any found DNSKEY RRs to that of the
// public key generated from the private key. Instead we attempt
// to construct a key pair from the found public key and each
// private key which tests that they match.
for public_key in &found_public_keys {
// Attempt to create a key pair from this public key and every
// private key that we have.
if let Ok(signing_key) = self.mk_signing_key(&private_key, public_key.clone()) {
// Match found, keep the created signing key.
// TODO: Log here.
// TODO: Check the key tag against the key tag in the key file name?
// println!(
// "DNSKEY RR with key tag {} matches loaded private key '{}'",
// public_key.key_tag(),
// private_key_path.display()
// );
signing_keys.push(DnssecSigningKey::inferred(signing_key));
continue 'next_key_path;
}
}
// No matching public key found, try to load the public key
// instead.
let public_key_path = Self::mk_public_key_path(&key_path);
let public_key = Self::load_public_key(&env.in_cwd(&public_key_path))?;
// Verify that the owner of the public key matches the apex of the
// zone.
if public_key.owner() != soa_rr.owner() {
return Err(format!(
"Zone apex ({}) does not match the expected apex ({})",
soa_rr.owner(),
public_key.owner()
)
.into());
}
// Attempt to crate a key pair from the loaded private and public
// keys.
let signing_key = self
.mk_signing_key(&private_key, public_key.clone())
.map_err(|err| {
format!(
"Unable to create key pair from '{}' and '{}': {}",
public_key_path.display(),
private_key_path.display(),
err
)
})?;
// Store the created signing key.
signing_keys.push(DnssecSigningKey::inferred(signing_key));
// TODO: Log
// println!(
// "Loaded public key with key tag {} from '{}' for private key '{}'",
// public_key.key_tag(),
// public_key_path.display(),
// private_key_path.display()
// );
}
if self.sign_dnskeys_with_all_keys {
let signer = Signer::<Bytes, KeyPair, AllKeyStrat>::new();
self.go_further(
signer,
records,
signing_mode,
opt_out,
&signing_keys,
writer,
)
} else if self.sign_with_every_unique_algorithm {
let signer = Signer::<Bytes, KeyPair, AllUniqStrat>::new();
self.go_further(
signer,
records,
signing_mode,
opt_out,
&signing_keys,
writer,
)
} else {
let signer = Signer::<Bytes, KeyPair, FallbackStrat>::new();
self.go_further(
signer,
records,
signing_mode,
opt_out,
&signing_keys,
writer,
)
}
}
fn go_further<Strat: SigningKeyUsageStrategy<Bytes, KeyPair>>(
&self,
signer: Signer<Bytes, KeyPair, Strat>,
mut records: SortedRecords<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
signing_mode: SigningMode,
opt_out: Nsec3OptOut,
signing_keys: &[DnssecSigningKey<Bytes, KeyPair>],
mut writer: FileOrStdout<BufWriter<File>, impl Write>,
) -> Result<(), Error> {
// Make sure, zonemd arguments are unique
let zonemd: HashSet<ZonemdTuple> = HashSet::from_iter(self.zonemd.clone());
// Change the SOA serial.
if self.set_soa_serial_to_epoch_time {
Self::bump_soa_serial(&mut records)?;
}
// Find the apex.
let (apex, ttl, soa_serial) = Self::find_apex(&records).unwrap();
if !zonemd.is_empty() {
Self::replace_apex_zonemd_with_placeholder(&mut records, &apex, soa_serial, ttl);
}
// Hash the zone with NSEC or NSEC3, unless only ZONEMD is done.
let hashes = if matches!(
signing_mode,
SigningMode::HashOnly | SigningMode::HashAndSign
) {
if self.use_nsec3 {
let params = Nsec3param::new(self.algorithm, 0, self.iterations, self.salt.clone());
let Nsec3Records {
recs,
param,
hashes,
} = records
.nsec3s::<_, BytesMut>(
&apex,
ttl,
params,
opt_out,
!self.do_not_add_keys_to_zone,
self.extra_comments || self.preceed_zone_with_hash_list,
)
.unwrap();
records.extend(recs.into_iter().map(Record::from_record));
records.insert(Record::from_record(param)).unwrap();
hashes
} else {
let nsecs = records.nsecs::<Bytes>(&apex, ttl, !self.do_not_add_keys_to_zone);
records.extend(nsecs.into_iter().map(Record::from_record));
None
}
} else {
None
};
// Sign the zone unless disabled.
if signing_mode == SigningMode::HashAndSign {
let extra_records = signer
.sign(
&apex,
records.families(),
signing_keys,
!self.do_not_add_keys_to_zone,
)
.map_err(|_| "Signing failed")?;
records.extend(extra_records.into_iter().map(Record::from_record));
}
if !zonemd.is_empty() {
// Remove existing ZONEMD RRs at apex (the placeholder is no longer needed)
let _ = records.remove_first_by_name_class_rtype(
apex.owner().clone(),
None,
Some(Rtype::ZONEMD),
);
let zonemd_rrs =
Self::create_zonemd_digest_and_records(&records, &apex, &zonemd, soa_serial, ttl)?;
// Add ZONEMD RRs to output records
records.extend(zonemd_rrs.clone().into_iter().map(Record::from_record));
if signing_mode == SigningMode::HashAndSign {
self.update_zonemd_rrsig(&signer, &mut records, &apex, signing_keys, zonemd_rrs);
}
}
// The signed RRs are in DNSSEC canonical order by owner name. For
// compatibility with ldns-signzone, re-order them to be in canonical
// order by unhashed owner name and so that hashed names come after
// equivalent unhashed names.
//
// INCOMAPATIBILITY WARNING: Unlike ldns-signzone, we only apply this
// ordering if `-b` is specified.
//
// Note: Family refers to the underlying record data, so while we are
// creating a new Vec, it only contains references to the original
// data so it's indiividual are not the records themselves.
let mut families;
let family_iter: AnyFamiliesIter =
if self.order_nsec3_rrs_by_unhashed_owner_name && hashes.is_some() {
families = records.families().collect::<Vec<_>>();
let Some(hashes) = hashes.as_ref() else {
unreachable!();
};
families.sort_unstable_by(|a, b| {
let mut hashed_count = 0;
let unhashed_a = if let Some(unhashed_owner) = hashes.get(a.owner()) {
hashed_count += 1;
unhashed_owner
} else {
a.owner()
};
let unhashed_b = if let Some(unhashed_owner) = hashes.get(b.owner()) {
hashed_count += 2;
unhashed_owner
} else {
b.owner()
};
match unhashed_a.cmp(unhashed_b) {
Ordering::Less => Ordering::Less,
Ordering::Equal => match hashed_count {
0 | 3 => Ordering::Equal,
1 => Ordering::Greater,
2 => Ordering::Less,
_ => unreachable!(),
},
Ordering::Greater => Ordering::Greater,
}
});
families.iter().into()
} else {
records.families().into()
};
// Output the resulting zone, with comments if enabled.
if self.extra_comments {
writer.write_fmt(format_args!(
";; Zone: {}\n;\n",
apex.owner().fmt_with_dot()
))?;
}
if self.preceed_zone_with_hash_list {
if let Some(hashes) = hashes.as_ref() {
let mut owner_sorted_hashes = hashes.iter().collect::<Vec<_>>();
owner_sorted_hashes
.sort_by(|(_, owner_a), (_, owner_b)| owner_a.canonical_cmp(owner_b));
for (hash, owner) in owner_sorted_hashes {
writer.write_fmt(format_args!("; H({owner}) = {hash}\n"))?;
}
}
}
if let Some(record) = records.iter().find(|r| r.rtype() == Rtype::SOA) {
self.writeln_rr(&mut writer, record)?;
if self.order_rrsigs_after_the_rtype_they_cover {
if let Some(record) = records.iter().find(|r| {
if let ZoneRecordData::Rrsig(rrsig) = r.data() {
rrsig.type_covered() == Rtype::SOA
} else {
false
}
}) {
self.writeln_rr(&mut writer, record)?;
}
if self.extra_comments {
writer.write_str(";\n")?;
}
}
}
let nsec3_cs = Nsec3CommentState {
hashes: hashes.as_ref(),
apex: &apex,
};
for family in family_iter {
if self.extra_comments {
if let Some(hashes) = hashes.as_ref() {
// If this is family contains an NSEC3 RR and the number
// of RRs in the RRSET of the unhashed owner name is zero,
// then the NSEC3 was generated for an empty non-terminal.
if family.rrsets().any(|rrset| rrset.rtype() == Rtype::NSEC3) {
if let Some(unhashed_name) = hashes.get(family.owner()) {
if !records
.families()
.any(|family| family.owner() == unhashed_name)
{
writer.write_fmt(format_args!(
";; Empty nonterminal: {unhashed_name}\n"
))?;
}
} else {
// ??? Every hashed name must correspond to an
// unhashed name?
unreachable!();
}
}
}
}
// The SOA is output separately above as the very first RRset so
// we skip that, and we skip RRSIGs as they are output only after
// the RRset that they cover.
if self.order_rrsigs_after_the_rtype_they_cover {
for rrset in family
.rrsets()
.filter(|rrset| !matches!(rrset.rtype(), Rtype::SOA | Rtype::RRSIG))
{
for rr in rrset.iter() {
self.write_rr(&mut writer, rr)?;
match rr.data() {
ZoneRecordData::Nsec3(nsec3) if self.extra_comments => {
nsec3.comment(&mut writer, rr, nsec3_cs)?
}
ZoneRecordData::Dnskey(dnskey) => {
dnskey.comment(&mut writer, rr, ())?
}
_ => {
// Nothing to do. We do not support Bubble Babble
// output for DS records.
//
// See:
// https://bohwaz.net/archives/web/Bubble_Babble.html
}
}
writer.write_str("\n")?;
}
// Now attempt to print the RRSIGs that covers the RTYPE of this RRSET.
for covering_rrsigs in family
.rrsets()
.filter(|this_rrset| this_rrset.rtype() == Rtype::RRSIG)
.map(|this_rrset| this_rrset.iter().filter(|rr| matches!(rr.data(), ZoneRecordData::Rrsig(rrsig) if rrsig.type_covered() == rrset.rtype())))
{
for covering_rrsig_rr in covering_rrsigs {
self.writeln_rr(&mut writer, covering_rrsig_rr)?;
}
}
}
if self.extra_comments {
writer.write_str(";\n")?;
}
} else {
for rrset in family.rrsets().filter(|rrset| rrset.rtype() != Rtype::SOA) {
for rr in rrset.iter() {
// Only output the key tag comment if running as LDNS.
// When running as DNST we assume without `-b` that speed
// is wanted, not human readable comments.
self.write_rr(&mut writer, rr)?;
if self.invoked_as_ldns {
if let ZoneRecordData::Dnskey(dnskey) = rr.data() {
dnskey.comment(&mut writer, rr, ())?
}
}
writer.write_char('\n')?;
}
}
}
}
Ok(())
}
fn write_rr<W, N, O: AsRef<[u8]>>(
&self,
writer: &mut W,
rr: &Record<N, ZoneRecordData<O, N>>,
) -> std::fmt::Result
where
N: ToName,
W: Write,
ZoneRecordData<O, N>: ZonefileFmt,
{
if self.use_yyyymmddhhmmss_rrsig_format {
if let ZoneRecordData::Rrsig(rrsig) = rr.data() {
let rr = Record::new(rr.owner(), rr.class(), rr.ttl(), YyyyMmDdHhMMSsRrsig(rrsig));
return writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)));
}
}
writer.write_fmt(format_args!("{}", rr.display_zonefile(DISPLAY_KIND)))
}
fn writeln_rr<W, N, O: AsRef<[u8]>>(
&self,
writer: &mut W,
rr: &Record<N, ZoneRecordData<O, N>>,
) -> std::fmt::Result
where
N: ToName,
W: Write,
ZoneRecordData<O, N>: ZonefileFmt,
{
self.write_rr(writer, rr)?;
writer.write_char('\n')
}
fn load_zone(
&self,
zonefile_path: &Path,
) -> Result<SortedRecords<StoredName, StoredRecordData>, Error> {
// Don't use Zonefile::load() as it knows nothing about the size of
// the original file so uses default allocation which allocates more
// bytes than are needed. Instead control the allocation size based on
// our knowledge of the file size.
let mut zone_file = File::open(zonefile_path)
.map_err(Error::from)
.context(&format!(
"loading zone file from path '{}'",
zonefile_path.display(),
))?;
let zone_file_len = zone_file.metadata()?.len();
let mut buf = inplace::Zonefile::with_capacity(zone_file_len as usize).writer();
std::io::copy(&mut zone_file, &mut buf)?;
let mut reader = buf.into_inner();
let mut records = SortedRecords::new();
if let Some(origin) = &self.origin {
reader.set_origin(origin.clone());
}
for entry in reader {
let entry = entry.map_err(|err| format!("Invalid zone file: {err}"))?;
match entry {
Entry::Record(record) => {
let record: StoredRecord = record.flatten_into();
// Ignore any existing NSEC(3) and RRSIG RRs from the
// loaded zone as we only support signing an unsigned
// zone. We do not ignore DNSKEY RRs as we match given
// keys against those.
//
// TODO: RFC 5155 DNS Security (DNSSEC) Hashed
// Authenticated Denial of Existence says in section 10
// says that to safely transition between NSEC <-> NSEC3
// one must be able to have both RR types in the zone at
// once, while our current implementation only supports
// having either NSEC or NSEC3 in the zone at any one
// time.
//
// TODO: NSEC3PARAM and ZONEMD should only be ignored at
// the apex (the only place RFCs define them to be valid).
if !matches!(
record.rtype(),
Rtype::NSEC
| Rtype::NSEC3
| Rtype::NSEC3PARAM
| Rtype::RRSIG
| Rtype::ZONEMD
) {
let _ = records.insert(record);
}
}
Entry::Include { .. } => {
return Err(Error::from(
"Invalid zone file: $INCLUDE directive is not supported",
));
}
}
}
Ok(records)
}
fn find_apex(
records: &SortedRecords<StoredName, StoredRecordData>,
) -> Result<(FamilyName<Name<Bytes>>, Ttl, Serial), Error> {
let soa = match records.find_soa() {
Some(soa) => soa,
None => {
return Err(Error::from("Invalid zone file: Cannot find SOA record"));
}
};
let (ttl, serial) = match *soa.first().data() {
ZoneRecordData::Soa(ref soa_data) => {
// RFC 9077 updated RFC 4034 (NSEC) and RFC 5155 (NSEC3) to
// say that the "TTL of the NSEC(3) RR that is returned MUST be
// the lesser of the MINIMUM field of the SOA record and the
// TTL of the SOA itself".
(min(soa_data.minimum(), soa.ttl()), soa_data.serial())
}
_ => unreachable!(),
};
Ok((soa.family_name().cloned(), ttl, serial))
}
fn bump_soa_serial(
records: &mut SortedRecords<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
) -> Result<(), Error> {
// SAFETY: Already checked before this point.
let old_soa_rr = records.find_soa().unwrap();
let ZoneRecordData::Soa(old_soa) = old_soa_rr.first().data() else {
unreachable!();
};
// Undocumented behaviour in ldns-signzone: it doesn't just set the
// SOA serial to the current unix timestamp as is documented for '-u'
// but rather only does that if the resulting value would be larger
// than the current unix timestamp, otherwise it increments it. I
// assume it does that to ensure that the SOA serial advances on zone
// change per expectations defined in RFC 1034, though it is assuming
// that the SOA serial can be interpreted as a unix timestamp which
// may not be the intention of the zone owner.
let now = Serial::now();
let new_serial = if now > old_soa.serial() {
now
} else {
old_soa.serial().add(1)
};
let new_soa = Soa::new(
old_soa.mname().clone(),
old_soa.rname().clone(),
new_serial,
old_soa.refresh(),
old_soa.retry(),
old_soa.expire(),
old_soa.minimum(),
);
records.replace_soa(new_soa);
Ok(())
}
fn load_private_key(key_path: &Path) -> Result<SecretKeyBytes, Error> {
let private_data = std::fs::read_to_string(key_path)
.map_err(Error::from)
.context(&format!(
"loading private key from file '{}'",
key_path.display(),
))?;
// Note: Compared to the original ldns-signzone there is a minor
// regression here because at the time of writing the error returned
// from parsing indicates broadly the type of parsing failure but does
// note indicate the line number at which parsing failed.
let secret_key = SecretKeyBytes::parse_from_bind(&private_data).map_err(|err| {
format!(
"Unable to parse BIND formatted private key file '{}': {}",
key_path.display(),
err
)
})?;
Ok(secret_key)
}
fn load_public_key(key_path: &Path) -> Result<Key<Bytes>, Error> {
let public_data = std::fs::read_to_string(key_path)
.map_err(Error::from)
.context(&format!(
"loading public key from file '{}'",
key_path.display(),
))?;
// Note: Compared to the original ldns-signzone there is a minor
// regression here because at the time of writing the error returned
// from parsing indicates broadly the type of parsing failure but does
// note indicate the line number at which parsing failed.
let public_key_info = Key::parse_from_bind(&public_data).map_err(|err| {
format!(
"Unable to parse BIND formatted public key file '{}': {}",
key_path.display(),
err
)
})?;
Ok(public_key_info)
}
fn mk_public_key_path(key_path: &Path) -> PathBuf {
if key_path.extension().and_then(|ext| ext.to_str()) == Some("key") {
key_path.to_path_buf()
} else {
PathBuf::from(format!("{}.key", key_path.display()))
}
}
fn mk_private_key_path(key_path: &Path) -> PathBuf {
if key_path.extension().and_then(|ext| ext.to_str()) == Some("private") {
key_path.to_path_buf()
} else {
PathBuf::from(format!("{}.private", key_path.display()))
}
}
fn mk_signing_key(
&self,
private_key: &SecretKeyBytes,
public_key: Key<Bytes>,
) -> Result<SigningKey<Bytes, KeyPair>, FromBytesError> {
let key_pair = KeyPair::from_bytes(private_key, public_key.raw_public_key())?;
let signing_key = SigningKey::new(public_key.owner().clone(), public_key.flags(), key_pair)
.with_validity(self.inception, self.expiration);
Ok(signing_key)
}
fn write_extreme_iterations_warning(env: &impl Env) {
Self::write_iterations_warning(
env,
"NSEC3 iterations larger than 500 may cause validating resolvers to return SERVFAIL!",
);
}
fn write_large_iterations_warning(env: &impl Env) {
Self::write_iterations_warning(env, "NSEC3 iterations larger than 100 may cause validating resolvers to return insecure responses!");
}
fn write_non_zero_iterations_warning(env: &impl Env) {
Self::write_iterations_warning(env, "NSEC3 iterations larger than 0 increases performance cost while providing only moderate protection!");
}
fn write_iterations_warning(env: &impl Env, text: &str) {
Error::write_warning(&mut env.stderr(), text);
writeln!(
env.stderr(),
"See: https://www.rfc-editor.org/rfc/rfc9276.html"
);
}
/// Create the ZONEMD digest for the SIMPLE scheme.
/// The records need to be in DNSSEC canonical ordering,
/// with same owner RRs sorted numerically by RTYPE.
///
/// [RFC 8976] Section 3.3.1. The SIMPLE Scheme
/// ```text
/// 3.3.1. The SIMPLE Scheme
///
/// For the SIMPLE scheme, the digest is calculated over the zone as a
/// whole. This means that a change to a single RR in the zone requires
/// iterating over all RRs in the zone to recalculate the digest. SIMPLE
/// is a good choice for zones that are small and/or stable, but it is
/// probably not good for zones that are large and/or dynamic.
///
/// Calculation of a zone digest requires RRs to be processed in a
/// consistent format and ordering. This specification uses DNSSEC's
/// canonical on-the-wire RR format (without name compression) and
/// ordering as specified in Sections 6.1, 6.2, and 6.3 of [RFC4034] with
/// the additional provision that RRsets having the same owner name MUST
/// be numerically ordered, in ascending order, by their numeric RR TYPE.
///
/// 3.3.1.1. SIMPLE Scheme Inclusion/Exclusion Rules
///
/// When iterating over records in the zone, the following inclusion/
/// exclusion rules apply:
///
/// * All records in the zone, including glue records, MUST be included
/// unless excluded by a subsequent rule.
///
/// * Occluded data ([RFC5936], Section 3.5) MUST be included.
///
/// * If there are duplicate RRs with equal owner, class, type, and
/// RDATA, only one instance is included ([RFC4034], Section 6.3) and
/// the duplicates MUST be omitted.
///
/// * The placeholder apex ZONEMD RR(s) MUST NOT be included.
///
/// * If the zone is signed, DNSSEC RRs MUST be included, except:
///
/// * The RRSIG covering the apex ZONEMD RRset MUST NOT be included
/// because the RRSIG will be updated after all digests have been
/// calculated.
///
/// 3.3.1.2. SIMPLE Scheme Digest Calculation
///
/// A zone digest using the SIMPLE scheme is calculated by concatenating
/// all RRs in the zone, in the format and order described in
/// Section 3.3.1 subject to the inclusion/exclusion rules described in
/// Section 3.3.1.1, and then applying the chosen hash algorithm:
///
/// digest = hash( RR(1) | RR(2) | RR(3) | ... )
///
/// where "|" denotes concatenation.
/// ```
///
/// [RFC 8976]: https://www.rfc-editor.org/rfc/rfc8976.html
/// [RFC 4034]: https://www.rfc-editor.org/rfc/rfc4034.html
fn create_zonemd_digest_simple(
apex: &FamilyName<Name<Bytes>>,
records: &SortedRecords<StoredName, StoredRecordData>,
algorithm: ZonemdAlg,
) -> Result<digest::Digest, Error> {
// TODO: optimize by using multiple digest'ers at once, instead of
// looping over the whole zone per digest algorithm.
let mut buf: Vec<u8> = Vec::new();
let mut ctx = match algorithm {
ZonemdAlg::SHA384 => digest::Context::new(&digest::SHA384),
ZonemdAlg::SHA512 => digest::Context::new(&digest::SHA512),
_ => {
// This should be caught by the argument parsing, but in case...
return Err("unsupported zonemd hash algorithm".into());
}
};
for family in records.families() {
if !family.is_in_zone(apex) {
continue;
}
// From RFC 8976:
// ```text
// * All records in the zone, including glue records, MUST be included
// unless excluded by a subsequent rule.
// * Occluded data ([RFC5936], Section 3.5) MUST be included.
// * If there are duplicate RRs with equal owner, class, type, and
// RDATA, only one instance is included ([RFC4034], Section 6.3) and
// the duplicates MUST be omitted.
// * The placeholder apex ZONEMD RR(s) MUST NOT be included.
// * If the zone is signed, DNSSEC RRs MUST be included, except:
// * The RRSIG covering the apex ZONEMD RRset MUST NOT be included
// because the RRSIG will be updated after all digests have been
// calculated.
// ```
// The first three rules are currently implemented by the SortedRecords type.
for record in family.records() {
buf.clear();
if record.rtype() == Rtype::ZONEMD && record.owner() == apex.owner() {
// Skip placeholder ZONEMD at apex
continue;
} else if record.rtype() == Rtype::RRSIG && record.owner() == apex.owner() {
// Skip RRSIG for ZONEMD at apex
if let ZoneRecordData::Rrsig(rrsig) = record.data() {
if rrsig.type_covered() == Rtype::ZONEMD {
continue;
}
};
}
with_infallible(|| record.compose_canonical(&mut buf));
ctx.update(&buf);
}
}
Ok(ctx.finish())
}
fn replace_apex_zonemd_with_placeholder(
records: &mut SortedRecords<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
apex: &FamilyName<Name<Bytes>>,
soa_serial: Serial,
ttl: Ttl,
) {
// Remove existing ZONEMD RRs at apex for any class (it's class independent).
let _ =
records.remove_all_by_name_class_rtype(apex.owner().clone(), None, Some(Rtype::ZONEMD));
// Insert placeholder ZONEMD at apex for
// correct NSEC(3) bitmap (will be replaced later).
let placeholder_zonemd = ZoneRecordData::Zonemd(Zonemd::new(
soa_serial,
ZonemdScheme::from_int(0),
ZonemdAlg::from_int(0),
Bytes::default(),
));
let _ = records.insert(Record::new(
apex.owner().clone(),
apex.class(),
ttl,
placeholder_zonemd,
));
}
fn create_zonemd_digest_and_records(
records: &SortedRecords<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
apex: &FamilyName<Name<Bytes>>,
zonemd: &HashSet<ZonemdTuple>,
soa_serial: Serial,
ttl: Ttl,
) -> Result<Vec<Record<StoredName, StoredRecordData>>, Error> {
let mut zonemd_rrs = Vec::new();
for z in zonemd {
// For now, only the SIMPLE scheme for ZONEMD is defined
if z.0 != ZonemdScheme::SIMPLE {
return Err("unsupported zonemd scheme (only SIMPLE is supported)".into());
}
let digest = Self::create_zonemd_digest_simple(apex, records, z.1)?;
// Create actual ZONEMD RR
let tmp_zrr = ZoneRecordData::Zonemd(Zonemd::new(
soa_serial,
z.0,
z.1,
Bytes::copy_from_slice(digest.as_ref()),
));
zonemd_rrs.push(Record::new(
apex.owner().clone(),
apex.class(),
ttl,
tmp_zrr,
));
}
Ok(zonemd_rrs)
}
fn update_zonemd_rrsig<KeyStrat, Sort>(
&self,
signer: &Signer<Bytes, KeyPair, KeyStrat, Sort>,
records: &mut SortedRecords<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>, Sort>,
apex: &FamilyName<Name<Bytes>>,
keys: &[DnssecSigningKey<Bytes, KeyPair>],
zonemd_rrs: Vec<Record<StoredName, StoredRecordData>>,
) where
KeyStrat: SigningKeyUsageStrategy<Bytes, KeyPair>,
Sort: Sorter,
{
// Sign only ZONEMD RRs
let zonemd_rrs: SortedRecords<StoredName, StoredRecordData> =
SortedRecords::from(zonemd_rrs);
let mut zonemd_rrsig = signer
.sign(apex, zonemd_rrs.families(), keys, false)
.unwrap();
// Replace original ZONEMD RRSIG with newly generated one
if let Some(rrsig) = zonemd_rrsig.pop() {
if let ZoneRecordData::Rrsig(rrsig) = rrsig.data() {
records.replace_rrsig_for_apex_zonemd(rrsig.clone(), apex);
}
}
}
}
fn next_owner_hash_to_name(
next_owner_hash_hex: &str,
apex: &FamilyName<Name<Bytes>>,
) -> Result<Name<Bytes>, ()> {
let mut builder = NameBuilder::new_bytes();
builder
.append_chars(next_owner_hash_hex.chars())
.map_err(|_| ())?;
let next_owner_name = builder.append_origin(apex.owner()).map_err(|_| ())?;
Ok(next_owner_name)
}
//------------ SigningMode ---------------------------------------------------
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
enum SigningMode {
/// Both hash (NSEC/NSEC3) and sign zone records.
#[default]
HashAndSign,
/// Only hash (NSEC/NSEC3) zone records, don't sign them.
HashOnly,
// /// Only sign zone records, assume they are already hashed.
// SignOnly,
/// Neither hash or sign zone records (e.g. when just using ZONEMD).
None,
}
//------------ ZonemdTuple ---------------------------------------------------
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
struct ZonemdTuple(ZonemdScheme, ZonemdAlg);
//------------ FileOrStdout --------------------------------------------------
enum FileOrStdout<T: io::Write, U: fmt::Write> {
File(T),
Stdout(Stream<U>),
}
impl<T: io::Write, U: fmt::Write> fmt::Write for FileOrStdout<T, U> {
fn write_str(&mut self, s: &str) -> std::fmt::Result {
match self {
FileOrStdout::File(f) => f.write_all(s.as_bytes()).map_err(|_| fmt::Error),
FileOrStdout::Stdout(o) => {
o.write_str(s);
Ok(())
}
}
}
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
match self {
FileOrStdout::File(f) => f.write_fmt(args).map_err(|_| fmt::Error),
FileOrStdout::Stdout(o) => {
o.write_fmt(args);
Ok(())
}
}
}
}
//------------ Commented -----------------------------------------------------
/// Support for RTYPE specific zonefile comment generation.
///
/// Intended to be used to enable behaviour to be matched to that of the LDNS
/// `ldns_rr2buffer_str_fmt()` function.
trait Commented<T> {
fn comment<W: fmt::Write>(
&self,
writer: &mut W,
record: &Record<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
metadata: T,
) -> Result<(), fmt::Error>;
}
impl Commented<()> for Dnskey<Bytes> {
fn comment<W: fmt::Write>(
&self,
writer: &mut W,
record: &Record<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
_metadata: (),
) -> Result<(), fmt::Error> {
writer.write_fmt(format_args!(" ;{{id = {}", self.key_tag()))?;
if self.is_secure_entry_point() {
writer.write_str(" (ksk)")?;
} else if self.is_zone_key() {
writer.write_str(" (zsk)")?;
}
let owner = record.owner().clone();
let key = domain::validate::Key::from_dnskey(owner, self.clone()).unwrap();
let key_size = key.key_size();
writer.write_fmt(format_args!(", size = {key_size}b}}"))
}
}
#[derive(Copy, Clone)]
struct Nsec3CommentState<'a> {
hashes: Option<&'a HashMap<Name<Bytes>, Name<Bytes>, RandomState>>,
apex: &'a FamilyName<Name<Bytes>>,
}
impl<'b, O: AsRef<[u8]>> Commented<Nsec3CommentState<'b>> for Nsec3<O> {
fn comment<'a, W: fmt::Write>(
&self,
writer: &mut W,
record: &'a Record<Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>,
state: Nsec3CommentState<'b>,
) -> Result<(), fmt::Error> {
if let Some(hashes) = state.hashes {
// TODO: For ldns-signzone backward compatibilty we output
// " ;{... <domain>.}" but I find the spacing ugly and
// would prefer for dnst to output " ; {... <domain>. }"
// instead.
writer.write_str(" ;{ flags: ")?;
if self.opt_out() {
writer.write_str("optout")?;
} else {
writer.write_str("-")?;
}
let next_owner_hash_hex = format!("{}", self.next_owner());
let next_owner_name = next_owner_hash_to_name(&next_owner_hash_hex, state.apex);
let from = hashes
.get(record.owner())
.map(|n| format!("{}", n.fmt_with_dot()))
.unwrap_or_default();
let to = if let Ok(next_owner_name) = next_owner_name {
hashes
.get(&next_owner_name)
.map(|n| format!("{}", n.fmt_with_dot()))
.unwrap_or_else(|| format!("<unknown hash: {next_owner_hash_hex}>"))
} else {
format!("<invalid name: {next_owner_hash_hex}>")
};
writer.write_fmt(format_args!(", from: {from} to: {to}}}"))?;
}
Ok(())
}
}
//------------ AnyFamiliesIter -----------------------------------------------
type FamilyIterByValue<'a> =
std::slice::Iter<'a, Family<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>>;
type FamilyIterByRef<'a> = RecordsIter<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>;
/// An iterator over a collection of [`Family`], whether by reference or not.
enum AnyFamiliesIter<'a> {
VecIter(FamilyIterByValue<'a>),
FamiliesIter(FamilyIterByRef<'a>),
}
impl<'a> Iterator for AnyFamiliesIter<'a>
where
Family<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>: Clone,
{
type Item = Family<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>;
fn next(&mut self) -> Option<Self::Item> {
match self {
AnyFamiliesIter::VecIter(it) => it.next().cloned(),
AnyFamiliesIter::FamiliesIter(it) => it.next(),
}
}
}
//--- From<std::slice::Iter<'a, Family<'a, N, D>>>
impl<'a> From<std::slice::Iter<'a, Family<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>>>
for AnyFamiliesIter<'a>
{
fn from(
iter: std::slice::Iter<'a, Family<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>>,
) -> Self {
Self::VecIter(iter)
}
}
//--- From<RecordsIter<'a, N, D>>
impl<'a> From<RecordsIter<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>>
for AnyFamiliesIter<'a>
{
fn from(iter: RecordsIter<'a, Name<Bytes>, ZoneRecordData<Bytes, Name<Bytes>>>) -> Self {
Self::FamiliesIter(iter)
}
}
struct FallbackStrat;
impl SigningKeyUsageStrategy<Bytes, KeyPair> for FallbackStrat {
const NAME: &'static str = "Fallback to ZSKs/KSKs if the other is empty";
fn select_signing_keys_for_rtype(
candidate_keys: &[DnssecSigningKey<Bytes, KeyPair>],
rtype: Option<Rtype>,
) -> HashSet<usize> {
match rtype {
// TODO: Do we need to treat CDS and CDNSKEY RRs like DNSKEY RRs?
Some(Rtype::DNSKEY) => {
// Use the default keys for signing DNSKEY RRs, i.e. keys
// intended to be used as KSKs.
let keys = DefaultSigningKeyUsageStrategy::select_signing_keys_for_rtype(
candidate_keys,
rtype,
);
// But if there are no such keys, fallback to using the keys
// used to sign other record types, i.e. keys intended to be
// used as ZSKs.
if keys.is_empty() {
Self::select_signing_keys_for_rtype(candidate_keys, None)
} else {
keys
}
}
_ => {
// Use the default keys for signing non-DNSKEY RRs, i.e. keys
// intended to be used as ZSKs.
let keys = DefaultSigningKeyUsageStrategy::select_signing_keys_for_rtype(
candidate_keys,
rtype,
);
// But if there are no such keys, fallback to using the keys
// used to sign DNSKEY RRs, i.e. keys intended to be used as
// KSKs.
if keys.is_empty() {
Self::select_signing_keys_for_rtype(candidate_keys, Some(Rtype::DNSKEY))
} else {
keys
}
}
}
}
}
struct AllKeyStrat;
impl SigningKeyUsageStrategy<Bytes, KeyPair> for AllKeyStrat {
const NAME: &'static str = "All keys (KSK and ZSK)";
fn select_signing_keys_for_rtype(
candidate_keys: &[DnssecSigningKey<Bytes, KeyPair>],
rtype: Option<Rtype>,
) -> HashSet<usize> {
match rtype {
Some(Rtype::DNSKEY) => {
let mut keys = DefaultSigningKeyUsageStrategy::select_signing_keys_for_rtype(
candidate_keys,
rtype,
);
keys.extend(
DefaultSigningKeyUsageStrategy::select_signing_keys_for_rtype(
candidate_keys,
None,
),
);
keys
}
_ => FallbackStrat::select_signing_keys_for_rtype(candidate_keys, rtype),
}
}
}
#[derive(Default)]
struct AllUniqStrat;
impl SigningKeyUsageStrategy<Bytes, KeyPair> for AllUniqStrat {
const NAME: &'static str = "Unique algorithms (all KSK + unique ZSK)";
fn select_signing_keys_for_rtype(
candidate_keys: &[DnssecSigningKey<Bytes, KeyPair>],
rtype: Option<Rtype>,
) -> HashSet<usize> {
match rtype {
Some(Rtype::DNSKEY) => {
let mut seen_algs = HashSet::new();
candidate_keys
.iter()
.enumerate()
.filter_map(|(i, k)| {
let new_alg = seen_algs.insert(k.key().algorithm());
match k.purpose() {
IntendedKeyPurpose::KSK | IntendedKeyPurpose::CSK => true,
IntendedKeyPurpose::ZSK => new_alg,
_ => false,
}
.then_some(i)
})
.collect::<HashSet<_>>()
}
_ => FallbackStrat::select_signing_keys_for_rtype(candidate_keys, rtype),
}
}
}
//------------ YyyyMmDdHhMMSsRrsig -------------------------------------------
/// A RFC 4034 section 3.2 YYYYMMDDHHmmSS presentable RRSIG wrapper.
///
/// This wrapper type provides an alternate implementation of [`ZonefileFmt`]
/// to the default implemented in `domain` such that RRSIG inception and
/// expiration timestamps are rendered in RFC 4034 3.2 YYYYMMDDHHmmSS format
/// instead of seconds since 1 January 1970 00:00:00 UTC format.
struct YyyyMmDdHhMMSsRrsig<'a, O, N>(&'a Rrsig<O, N>);
impl<O: AsRef<[u8]>, N: ToName> ZonefileFmt for YyyyMmDdHhMMSsRrsig<'_, O, N> {
fn fmt(&self, p: &mut impl Formatter) -> zonefile_fmt::Result {
#[allow(non_snake_case)]
fn to_YYYYMMDDHHmmSS(ts: &Timestamp) -> impl Display {
jiff::Timestamp::from_second(ts.into_int().into())
.unwrap()
.strftime("%Y%m%d%H%M%S")
}
// This block of code was copied from the `domain` crate impl of
// `Zonefilefmt` for domain::rdata::Rrsig. Ideally we wouldn't have to
// copy it like this but at the time of writing `domain` doesn't
// provide a way to override the rendering of RRSIG timestamps alone
// nor provide alternate renderings itself. For more information see
// https://github.com/NLnetLabs/domain/issues/467.
p.block(|p| {
let expiration = to_YYYYMMDDHHmmSS(&self.0.expiration());
let inception = to_YYYYMMDDHHmmSS(&self.0.inception());
p.write_show(self.0.type_covered())?;
p.write_show(self.0.algorithm())?;
p.write_token(self.0.labels())?;
p.write_comment("labels")?;
p.write_show(self.0.original_ttl())?;
p.write_comment("original ttl")?;
p.write_token(expiration)?;
p.write_comment("expiration")?;
p.write_token(inception)?;
p.write_comment("inception")?;
p.write_token(self.0.key_tag())?;
p.write_comment("key tag")?;
p.write_token(self.0.signer_name().fmt_with_dot())?;
p.write_comment("signer name")?;
p.write_token(base64::encode_display(&self.0.signature()))
})
}
}
impl<O, N> RecordData for YyyyMmDdHhMMSsRrsig<'_, O, N> {
fn rtype(&self) -> Rtype {
Rtype::RRSIG
}
}
//------------ Tests ---------------------------------------------------------
// TODO: Maybe resolve the Timestamp issue differently? When running the tests
// and the base struct get's constructed at say time "12:30:29" and the command
// parsing for an assertion get's executed at "12:30:30", then the timestamps
// don't match and the tests fails. This creates a flaky test without actual
// errors in the code. Right now it is solved by recreating the expiration and
// inception fields during the assertion. However, this means we need to
// remember adding that for every assertion.
#[cfg(test)]
mod test {
use std::fs::File;
use std::io::Write;
use std::ops::Add;
use std::path::PathBuf;
use std::str::FromStr;
use domain::base::iana::{Nsec3HashAlg, ZonemdAlg, ZonemdScheme};
use domain::base::Name;
use domain::rdata::dnssec::Timestamp;
use domain::rdata::nsec3::Nsec3Salt;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
use crate::commands::signzone::{ZonemdTuple, FOUR_WEEKS};
use crate::commands::Command;
use crate::env::fake::FakeCmd;
use super::SignZone;
#[track_caller]
fn parse(args: FakeCmd) -> SignZone {
let res = args.parse().unwrap();
let Command::SignZone(x) = res.command else {
panic!("Not a SignZone!");
};
x
}
#[test]
fn dnst_parse_failures() {
let cmd = FakeCmd::new(["dnst", "signzone"]);
cmd.parse().unwrap_err();
// Missing keys
cmd.args(["example.org.zone"]).parse().unwrap_err();
// Missing ZONEMD arguments
cmd.args(["-Z", "example.org.zone"]).parse().unwrap_err();
// Invalid ZONEMD arguments
cmd.args(["-z", "3", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
cmd.args(["-z", "0:0", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
// Invalid NSEC3 arguments
cmd.args(["-na", "MD5", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
cmd.args(["-ns", "NOBASE64", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
// Conflicting NSEC3 optout options
cmd.args(["-nPp", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
}
#[test]
fn dnst_parse_successes() {
let cmd = FakeCmd::new(["dnst", "signzone"]);
let base = SignZone {
extra_comments: false,
do_not_add_keys_to_zone: false,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
out_file: None,
inception: Timestamp::now(),
origin: None,
set_soa_serial_to_epoch_time: false,
zonemd: Vec::new(),
allow_zonemd_without_signing: false,
sign_dnskeys_with_all_keys: false,
use_nsec3: false,
sign_with_every_unique_algorithm: false,
algorithm: Nsec3HashAlg::SHA1,
iterations: 0,
salt: Nsec3Salt::empty(),
nsec3_opt_out_flags_only: false,
nsec3_opt_out: false,
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: false,
preceed_zone_with_hash_list: false,
order_rrsigs_after_the_rtype_they_cover: false,
order_nsec3_rrs_by_unhashed_owner_name: false,
no_require_keys_match_apex: false,
zonefile_path: PathBuf::from("example.org.zone"),
key_paths: Vec::from([PathBuf::from("anykey")]),
invoked_as_ldns: false,
};
// Check the defaults
assert_eq!(parse(cmd.args(["example.org.zone", "anykey"])), base);
// The switches (TODO: missing -A and -U)
assert_eq!(
parse(cmd.args(["-bdunpM", "example.org.zone", "anykey"])),
SignZone {
extra_comments: true,
do_not_add_keys_to_zone: true,
set_soa_serial_to_epoch_time: true,
use_nsec3: true,
nsec3_opt_out_flags_only: true,
no_require_keys_match_apex: true,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-H", "example.org.zone"])),
SignZone {
hash_only: true,
key_paths: Vec::new(),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// ZONEMD arguments
assert_eq!(
parse(cmd.args(["-z", "SIMPLE:SHA512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "simple:sha512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "sha512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// NSEC3 arguments
assert_eq!(
parse(cmd.args([
"-n",
"-s",
"BABABA",
"-t",
"15",
"example.org.zone",
"anykey"
])),
SignZone {
use_nsec3: true,
salt: Nsec3Salt::from_str("BABABA").unwrap(),
iterations: 15,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// Timestamps
assert_eq!(
parse(cmd.args([
"-i",
"20240101020202",
"-e",
"20240101050505",
"example.org.zone",
"anykey"
])),
SignZone {
expiration: Timestamp::from_str("20240101050505").unwrap(),
inception: Timestamp::from_str("20240101020202").unwrap(),
..base.clone()
}
);
// Output file
assert_eq!(
parse(cmd.args(["-f-", "example.org.zone", "anykey"])),
SignZone {
out_file: Some(PathBuf::from("-")),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-f", "output", "example.org.zone", "anykey"])),
SignZone {
out_file: Some(PathBuf::from("output")),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// Origin
assert_eq!(
parse(cmd.args(["-o", "origin.test", "example.org.zone", "anykey"])),
SignZone {
origin: Some(Name::from_str("origin.test.").unwrap()),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
}
#[test]
fn ldns_parse_failures() {
let cmd = FakeCmd::new(["ldns-signzone"]);
cmd.parse().unwrap_err();
// Missing keys
cmd.args(["example.org.zone"]).parse().unwrap_err();
// Invalid ZONEMD arguments
cmd.args(["-z", "3", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
cmd.args(["-z", "0:0", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
// Invalid NSEC3 arguments
cmd.args(["-na", "MD5", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
cmd.args(["-ns", "NOBASE64", "example.org.zone", "anykey"])
.parse()
.unwrap_err();
}
#[test]
fn ldns_parse_successes() {
let cmd = FakeCmd::new(["ldns-signzone"]);
let base = SignZone {
extra_comments: false,
do_not_add_keys_to_zone: false,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
out_file: None,
inception: Timestamp::now(),
origin: None,
set_soa_serial_to_epoch_time: false,
zonemd: Vec::new(),
allow_zonemd_without_signing: false,
sign_dnskeys_with_all_keys: false,
sign_with_every_unique_algorithm: false,
use_nsec3: false,
algorithm: Nsec3HashAlg::SHA1,
iterations: 1,
salt: Nsec3Salt::empty(),
nsec3_opt_out_flags_only: false,
nsec3_opt_out: false,
hash_only: false,
use_yyyymmddhhmmss_rrsig_format: true,
preceed_zone_with_hash_list: false,
order_rrsigs_after_the_rtype_they_cover: true,
order_nsec3_rrs_by_unhashed_owner_name: true,
no_require_keys_match_apex: false,
zonefile_path: PathBuf::from("example.org.zone"),
key_paths: Vec::from([PathBuf::from("anykey")]),
invoked_as_ldns: true,
};
// Check the defaults
assert_eq!(parse(cmd.args(["example.org.zone", "anykey"])), base);
// The switches (TODO: missing -A and -U)
assert_eq!(
parse(cmd.args(["-bdunp", "example.org.zone", "anykey"])),
SignZone {
extra_comments: true,
do_not_add_keys_to_zone: true,
set_soa_serial_to_epoch_time: true,
use_nsec3: true,
nsec3_opt_out_flags_only: true,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// ZONEMD arguments
assert_eq!(
parse(cmd.args(["-Z", "example.org.zone", "anykey"])),
SignZone {
allow_zonemd_without_signing: true,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "SIMPLE:SHA512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "simple:sha512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "sha512", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA512)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-z", "1", "example.org.zone", "anykey"])),
SignZone {
zonemd: Vec::from([ZonemdTuple(ZonemdScheme::SIMPLE, ZonemdAlg::SHA384)]),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// NSEC3 arguments
assert_eq!(
parse(cmd.args([
"-n",
"-s",
"BABABA",
"-t",
"15",
"example.org.zone",
"anykey"
])),
SignZone {
use_nsec3: true,
salt: Nsec3Salt::from_str("BABABA").unwrap(),
iterations: 15,
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// Timestamps
assert_eq!(
parse(cmd.args([
"example.org.zone",
"-i",
"20240101020202",
"-e",
"20240101050505",
"anykey"
])),
SignZone {
expiration: Timestamp::from_str("20240101050505").unwrap(),
inception: Timestamp::from_str("20240101020202").unwrap(),
..base.clone()
}
);
// Output file
assert_eq!(
parse(cmd.args(["-f-", "example.org.zone", "anykey"])),
SignZone {
out_file: Some(PathBuf::from("-")),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
assert_eq!(
parse(cmd.args(["-f", "output", "example.org.zone", "anykey"])),
SignZone {
out_file: Some(PathBuf::from("output")),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// Origin
assert_eq!(
parse(cmd.args(["-o", "origin.test", "example.org.zone", "anykey"])),
SignZone {
origin: Some(Name::from_str("origin.test.").unwrap()),
expiration: Timestamp::now().into_int().add(FOUR_WEEKS).into(),
inception: Timestamp::now(),
..base.clone()
}
);
// Version
assert!(matches!(
cmd.args(["-v"]).parse().unwrap().command,
Command::Report(_)
));
}
#[test]
fn zonemd_digest_and_replacing_existing_at_apex() {
let dir = run_setup();
let res1 = FakeCmd::new([
"dnst",
"signzone",
"-Z",
"-z",
"SIMPLE:SHA384",
"-f",
"-",
"zonemd1_example.org.zone",
])
.cwd(&dir)
.run();
assert_eq!(res1.exit_code, 0);
assert_eq!(
res1.stdout,
"example.org.\t240\tIN\tSOA\texample.net. hostmaster.example.net. 1234567890 28800 7200 604800 240\n\
example.org.\t240\tIN\tA\t128.140.76.106\n\
example.org.\t240\tIN\tNS\texample.net.\n\
example.org.\t240\tIN\tZONEMD\t1234567890 1 1 D2D125EE8B4DDAD944FD7EE437908A5D4D5A7DB7C2F948C5A051146FC75D124666033DF7D1BA1653CF490E89F9A454F3\n\
*.example.org.\t240\tIN\tA\t1.2.3.4\n\
deleg.example.org.\t240\tIN\tNS\texample.com.\n\
occluded.deleg.example.org.\t240\tIN\tA\t1.2.3.4\n"
);
assert_eq!(res1.stderr, "");
let res2 = FakeCmd::new([
"dnst",
"signzone",
"-Z",
"-z",
"SIMPLE:SHA384",
"-f",
"-",
"zonemd1_example.org.zone",
])
.cwd(&dir)
.run();
assert_eq!(res2.exit_code, 0);
assert_eq!(res2.stdout, res1.stdout);
assert_eq!(res2.stderr, "");
}
#[test]
fn zonemd_and_sign() {
let dir = run_setup();
let res = FakeCmd::new([
"dnst",
"signzone",
"-z",
"1:1",
"-f",
"-",
"-e",
"20241127162422",
"-i",
"20241127162422",
"zonemd1_example.org.zone",
"ksk1",
])
.cwd(&dir)
.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
res.stdout,
"example.org.\t240\tIN\tSOA\texample.net. hostmaster.example.net. 1234567890 28800 7200 604800 240\n\
example.org.\t240\tIN\tA\t128.140.76.106\n\
example.org.\t240\tIN\tNS\texample.net.\n\
example.org.\t240\tIN\tRRSIG\tA 15 2 240 1732724662 1732724662 38873 example.org. dVrR1Ay58L3cDaRIial45keWp/X8roeirciEqJqVZcqWO4AkSaILqDYIpfNRf3i9WvDzio0BLZT5K4r2krmyCA==\n\
example.org.\t240\tIN\tRRSIG\tNS 15 2 240 1732724662 1732724662 38873 example.org. JJDRuXMuv9yiJAFN+15/7DBbaBHepA20QxLruqrjSJZsgzRcPb1UTyGozlsq9BdCq3oxZm8lea5DcIi2tyGVDQ==\n\
example.org.\t240\tIN\tRRSIG\tSOA 15 2 240 1732724662 1732724662 38873 example.org. 2Jp7z/VMHlUvZoXApvsolX78ZzH9BmI8jznVHjagpmjOto/tAb1bL7AaTcOG2Ihk+uSSvDmIExaax0dbtL8CAg==\n\
example.org.\t240\tIN\tRRSIG\tNSEC 15 2 240 1732724662 1732724662 38873 example.org. bL1aldkxI/a0P9Oo3FUJfGspDchBs8B476AnKS4O5g43KZ5Oy+Xvb5UimyzFQ2f5gXL47cdt8EMmuy2iRhUpBg==\n\
example.org.\t240\tIN\tRRSIG\tDNSKEY 15 2 240 1732724662 1732724662 38873 example.org. UPk13WDbN2MLjSwgV82084DrNUdJFmS9bthBw52X0rfiBMAvrQJJhSYbq72G5j11SFp2DnUyml8stScKJyMlCQ==\n\
example.org.\t240\tIN\tRRSIG\tZONEMD 15 2 240 1732724662 1732724662 38873 example.org. f2VO/ROXqwgZdQNmTcu3Cc6zYbsFNRwiJsdYcfX1e+mdgIBt8PFsa5OOUy7VJHZnFD4/5Gq6n/6/FkWF/5iNDg==\n\
example.org.\t240\tIN\tNSEC\t*.example.org. A NS SOA RRSIG NSEC DNSKEY ZONEMD\n\
example.org.\t240\tIN\tDNSKEY\t257 3 15 6VdB0mk5qwjHWNC5TTOw1uHTzA0m3Xadg7aYVbcRn8Y=\n\
example.org.\t240\tIN\tZONEMD\t1234567890 1 1 97FCF584F87A42EA94F7C0DE25F3BA581A48D5FC4C5F1DD0FB275B9634EFE68A268606B6AB92A5D95062AB563B58196A\n\
*.example.org.\t240\tIN\tA\t1.2.3.4\n\
*.example.org.\t240\tIN\tRRSIG\tA 15 2 240 1732724662 1732724662 38873 example.org. 1eLPyREltQqUClcAuT4SkqdWXL8D4C3K0mnotLv8d1x6kh/ARcac9l99ulLwtxvmJb+61+zv4vFgX35Yqbm1BA==\n\
*.example.org.\t240\tIN\tRRSIG\tNSEC 15 2 240 1732724662 1732724662 38873 example.org. FgRwrOd36au9ijKnx3AxsyN5Ar4mwt4AALTye3/IqravMHa2pTTP8h0Z2GXgu3YPmP3RXpPTwza5960KwE8YCQ==\n\
*.example.org.\t240\tIN\tNSEC\tdeleg.example.org. A RRSIG NSEC\n\
deleg.example.org.\t240\tIN\tNS\texample.com.\n\
deleg.example.org.\t240\tIN\tRRSIG\tNSEC 15 3 240 1732724662 1732724662 38873 example.org. m/j7UOa1SvFw0rz5pBXVWS62gX328rxveNeD+Gd7husNcvbYhW2rLLYfTCG6LNvUP4fG2rJ45OhY3g3Trx2iBQ==\n\
deleg.example.org.\t240\tIN\tNSEC\texample.org. NS RRSIG NSEC\n\
occluded.deleg.example.org.\t240\tIN\tA\t1.2.3.4\n\
"
);
assert_eq!(res.stderr, "");
}
#[test]
/// Test NSEC3 optout behaviour with signing
fn ldns_nsec3_optout() {
// TODO: maybe make these strings a regex match of some kind for better flexibility with
// layout changes that don't affect the zonefile semantics?
let dir = run_setup();
// (dnst) ldns-signzone -np -f - -e 20241127162422 -i 20241127162422 nsec3_optout1_example.org.zone ksk1 | grep NSEC3
let ldns_dnst_output_stripped: &str = "\
example.org.\t240\tIN\tRRSIG\tNSEC3PARAM 15 2 240 20241127162422 20241127162422 38873 example.org. dOrhLIWhrQm2OunlTWrSsELkx1kKYo4jTkF5pEwrvZxjhUI9DBKdkloaVsTKcdrmffidC5pE9GoY9ckaoHpGCA==\n\
example.org.\t240\tIN\tNSEC3PARAM\t1 0 1 -\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. z4ceUmbSZiSnluFj8CDJ7B9fukCR2flTWgca4GE2xrw48+fiieH/04xCKhJmDRJUJTVkKtIYpB4p0Q4m60M1Cg==\n\
93u63bg57ppj6649al2n31l92iedkjd6.example.org.\t240\tIN\tNSEC3\t1 1 1 - K71KU6AICR5JPDJOE9J7CDNLK6D5C3UE A NS SOA RRSIG DNSKEY NSEC3PARAM\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. HUrf7tOm3simXqpZj1oZeKX/P3eWoTTKc3fsyqfuLD6sGssXrBfpv1/LINBR9eEBjJ9rFbQXILgweS6huBL/Ag==\n\
k71ku6aicr5jpdjoe9j7cdnlk6d5c3ue.example.org.\t240\tIN\tNSEC3\t1 1 1 - OJICMHRI4VP8PO7H2KVEJ99SKLQNJ5P2 NS\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tRRSIG\tNSEC3 15 3 240 20241127162422 20241127162422 38873 example.org. NG/8jk3UHht1ZYNEjUZ4swaEHea1amF4l3jZ893oARi95oxtPVLKoinVbBbfVuoanicOgeZxUPpKWHMBR12XDA==\n\
ojicmhri4vp8po7h2kvej99sklqnj5p2.example.org.\t240\tIN\tNSEC3\t1 1 1 - 93U63BG57PPJ6649AL2N31L92IEDKJD6 NS DS RRSIG\n\
";
let res = FakeCmd::new([
"ldns-signzone",
"-np",
"-f-",
"-e",
"20241127162422",
"-i",
"20241127162422",
"nsec3_optout1_example.org.zone",
"ksk1",
])
.cwd(&dir)
.run();
assert_eq!(res.exit_code, 0);
assert_eq!(
filter_lines_containing_all(&res.stdout, &["NSEC3"]),
ldns_dnst_output_stripped
);
assert_eq!(res.stderr, "");
}
// TODO: Currently fails due to https://github.com/NLnetLabs/domain/issues/468.
#[test]
fn rfc_4035_nsec_signed_zone_example() {
let dir = tempfile::TempDir::new().unwrap();
// Modified from the version in RFC 4035 replacing the keys used with
// ones we have the private key for and using a key algorithm that we
// support (8 instead of 5). Matches output produced by dnst signzone
// -b (not ldns-signzone -b as the -b output is suppressed by
// ldns-signzone when using -f-) in order to get the same ordering as
// both the original ldns-signzone and the example in RFC 4035.
let expected_signed_zone = r###"
example.\t3600\tIN\tSOA\tns1.example. bugs.x.w.example. 1081539377 3600 300 3600000 3600
example.\t3600\tIN\tRRSIG\tSOA 8 1 3600 20040409183619 20040509183619 38353 example. B/2RO0F3fBDDeouNWDWqt55qRBJttkU8UlQb54Qk8DnPqxmdlBRuP9DkMDBQNf2Us1OIQ7fnarokdSC3s9PO4FLx6YkFjR1w7ox6u1KXKJQUHqFHGOF1n3+lRZo74sEbP9+4DlvpYylLFV+bbxLDG0NEqCs+vDy2w+2zfSAW7y8=
example.\t3600\tIN\tNS\tns1.example.
example.\t3600\tIN\tNS\tns2.example.
example.\t3600\tIN\tRRSIG\tNS 8 1 3600 20040409183619 20040509183619 38353 example. ey6wIUC+5INUdErg7es/ANgPHSWAT5wZ7+ncgQ7oBytioha5YGp9aI/6p7KujzBPD6RiANhAV4gX4WqI5RDTc8KP/KBORN8quiN0G2ydVxwsedX/8r4dbb3OqQdP8Lh5AF8KS4dmu5EKdlRcckheV06+NFkWK2WqCaodScieF1Q=
example.\t3600\tIN\tMX\t1 xx.example.
example.\t3600\tIN\tRRSIG\tMX 8 1 3600 20040409183619 20040509183619 38353 example. t1B3Pnqcya7LAoDvtHkCq9XxmRhxgpBIdcmc+NSzkukk8yq8MoZ3ZPAUppFhnnwJADRAIQfi5Fpuk3FccFfHEHrai/gU/Ik/+0oX3vdFJm6aYUwYO/GI0AC+Nz6F0mukZx1uLAi1YqPjwZ10gn39G0kyBMsl5OnxAbeJ3x2bYgY=
example.\t3600\tIN\tNSEC\ta.example. NS SOA MX RRSIG NSEC DNSKEY
example.\t3600\tIN\tRRSIG\tNSEC 8 1 3600 20040409183619 20040509183619 38353 example. g6NRRy9bCZIwyiAkn/qkzJZvfYW0QILYGIJBNm8m+9gkkSX6zR9/uXIkOwQaAfHRJeUTWZAoCv10XYcZMlCXYuizbdRyn+n/cZ9nSALsBgMthSuQBw9B6tDiJbJ/PJ/SX3hdIZ3tEyS1QCFl4RbDJkTjKV6MxkKJdnnlwpFtMEo=
example.\t3600\tIN\tDNSKEY\t256 3 8 AwEAAbsD4Tcz8hl2Rldov4CrfYpK3ORIh/giSGDlZaDTZR4gpGxGvMBwu2jzQ3m0iX3PvqPoaybC4tznjlJi8g/qsCRHhOkqWmjtmOYOJXEuUTb+4tPBkiboJM5QchxTfKxkYbJ2AD+VAUX1S6h/0DI0ZCGx1H90QTBE2ymRgHBwUfBt ;{id = 38353 (zsk), size = 1024b}
example.\t3600\tIN\tDNSKEY\t257 3 8 AwEAAaYL5iwWI6UgSQVcDZmH7DrhQU/P6cOfi4wXYDzHypsfZ1D8znPwoAqhj54kTBVqgZDHw8QEnMcS3TWxvHBvncRTIXhCLx0BNK5/6mcTSK2IDbxl0j4vkcQrOxc77tyExuFfuXouuKVtE7rggOJiX6ga5LJW2if6Jxe/Rh8+aJv7 ;{id = 31967 (ksk), size = 1024b}
example.\t3600\tIN\tRRSIG\tDNSKEY 8 1 3600 20040409183619 20040509183619 31967 example. C8ZDPvp8Z2kNagqYLZwY0IGFutIi7OQR2VHjYZgCeC0NKCute2FdTilElZPg2WuJ86QHeGp3D5uZiW+dypiaNPdLh++6lY0qbLZNw3JNCzVLJzF/P+1NILsosuYMaQSc5QX4YVz5HygjofH1y/gel9O2UdmAE5oIzlBpTsgM+BI=
example.\t3600\tIN\tRRSIG\tDNSKEY 8 1 3600 20040409183619 20040509183619 38353 example. H0SIDL0zGHsdDtA7cW93wlUeAC0l/1KTQKeKctC72inf7qvanbT9ZcaqIeYIP3T3YlcIG23+j8qOK46B+KL9IWWA9oFFNuw8oTpyZGiy2kNEqhfwma7bUeWTDLaH8Uhf/it1xiiy9lV4NlOgEyTGE/IMB1+Xgf2RYMmgqLfak2o=
a.example.\t3600\tIN\tNS\tns1.a.example.
a.example.\t3600\tIN\tNS\tns2.a.example.
a.example.\t3600\tIN\tDS\t57855 5 1 B6DCD485719ADCA18E5F3D48A2331627FDD3636B
a.example.\t3600\tIN\tRRSIG\tDS 8 2 3600 20040409183619 20040509183619 38353 example. TwR1SK1LqiQCVMDoc3hJza+nVFSzFx33sjMeZvlWrqBD1G/sJcH3ItIgb/4uj0b95VM6ndxevga9pgd33PHmFmR1g6afS21kCMhhATvYBqCRgrUNBFzooGZpIitJier6T2CP/LSeispWULyXnB0F6C+/vmXWU6+Vhae6KIoS3EY=
a.example.\t3600\tIN\tNSEC\tai.example. NS DS RRSIG NSEC
a.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. FcUGncV5yjqhITrUeKiZwlrbsUY8G4sMnuckw7yqoWnsmU9TCLgjAwGWZGeNpjADKJ+dk9t06ofvLqIsQshu3Hrke1FZvuZT0vqtPNjhIz/03/2Dyx3nFdoxagofdinmNm92jt6mefflzxzYF1j9NqPC4YFDDgz5EjxX592e9QY=
ns1.a.example.\t3600\tIN\tA\t192.0.2.5
ns2.a.example.\t3600\tIN\tA\t192.0.2.6
ai.example.\t3600\tIN\tA\t192.0.2.9
ai.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20040409183619 20040509183619 38353 example. LWkkFmU/enigH/FAJJO4en4t3AUG+2GoGSrJvTO3qJYTDXzU2MMYAHXwl2RgSC7eTzbMap7HukEczWA7UdQkkurFCH4Xv/CxmJRKL9XcZ2fDfKNIzqNcsQj+NHnbKRR1GCituKLMV2AxBqH4ABopKYCvlsryaN8sLb01PyM9/Ro=
ai.example.\t3600\tIN\tHINFO\t"KLH-10" "ITS"
ai.example.\t3600\tIN\tRRSIG\tHINFO 8 2 3600 20040409183619 20040509183619 38353 example. b/CT77xaP9h9GrgdHMtToH3/rO6wqGVsn85IooPS7eHCUI5uEQWRy1i5usoImX6l6Z1yBgo7NPqhrPvRoFV7/oNtWNsAOfH1ClMgmpGyRjIMyNY4lK3i9yCmI5hBY85WNO5fExk3tqBRJ67gXbMuI1gbfve+pNVUvWubRCx4LsI=
ai.example.\t3600\tIN\tAAAA\t2001:db8::f00:baa9
ai.example.\t3600\tIN\tRRSIG\tAAAA 8 2 3600 20040409183619 20040509183619 38353 example. EGvDQig8uMlKodJP5OZ9ybhlyCinsInA4vzkoDBxBTHjuO7zyAx6mdxPw4rSHdn8ZdDssZKORFGpXcpEk3tGHAt0Z/kMME3JZ4dY2EiRhybueaPfiz95KNafdSZgqA7dKMxi9HGoBsGLqV/6I3grcGvMfHauItsX4Z8yV+htoM4=
ai.example.\t3600\tIN\tNSEC\tb.example. A HINFO AAAA RRSIG NSEC
ai.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. FbM7UgeKb7GAP9WMxBJlTb76IQx430tp1f/EIbIbqTUxduzseBSn4ntcf7+VwYVzBNtglnDFuw3C5LWE9jpa/rZ5Z/SmDmOCZP/aJPhausPO0AfQeiMoLjSJ0A9WavpGBzCC3gmrC7X3oqDuV6Y3fHAGU/YGQj0XNG7Ha1DE58c=
b.example.\t3600\tIN\tNS\tns1.b.example.
b.example.\t3600\tIN\tNS\tns2.b.example.
b.example.\t3600\tIN\tNSEC\tns1.example. NS RRSIG NSEC
b.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. UZeRqUD2OBo6iZckfZ9tMYlUuWcMzVDkhrITNptCs8CFVF2YCHBKg9heLMdEN92ie5V4GaMjIAd75MNlMm0EZGiDU2TzYz0k0jejcO/79XkAC/giqBHTedW9cbFYt2TqQi85ZxwhPq7ph5jSf5Iwt7rD4qKJXCp75npuGNDutEw=
ns1.b.example.\t3600\tIN\tA\t192.0.2.7
ns2.b.example.\t3600\tIN\tA\t192.0.2.8
ns1.example.\t3600\tIN\tA\t192.0.2.1
ns1.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20040409183619 20040509183619 38353 example. eFA7SiJRtUkMeb4JsJGTg2NRpfBCDpmMF4zvmm05BwndvEdYXBAOviHNMKXLX6ctjBUlzp0KxvUBhA/7ScEIMmmQHJfOnahzWnmbiZTZWwi3I8/fgCsEGZVsTJFRwPR4ddZF1M8S1lWnzPmnFja/nAIQdfA9HlP8xfsTDR195Ew=
ns1.example.\t3600\tIN\tNSEC\tns2.example. A RRSIG NSEC
ns1.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. hPXDx7JbevZRDI5z98jXVAVdtTIv89gjjEJMe1Nm+o/UIvsxon8OmWO6+b6lWLy6AEj/SiLC3wOaFbDb0wTkm/cI548Tyr2tg5D5ZIRjXtMJkx2aE0zLzRHnitrUQXYTJss/JCoPKEsGNExK9LgTIw7jKqcwfvH9dsxqxISwAC8=
ns2.example.\t3600\tIN\tA\t192.0.2.2
ns2.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20040409183619 20040509183619 38353 example. F36AOe1lWZ9QzPR+fiG94c+k6RitqgIrs99gaB/KdEqiZtQ8w3Tr+sfPogLMXbqW2+dSrEUM535IHqjLZzARY9Zd2KQWPMxWdponaCAIkVwJ4kXtWI2OSqx3IFoG4z91Wm3e/JsXahX6cJE5R+5LZaA6zO6sFLO/Q9LVa74parg=
ns2.example.\t3600\tIN\tNSEC\t*.w.example. A RRSIG NSEC
ns2.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. eu5gkpcWiXorZvkSYmbNKrd+7PGS/Er/J1IYt1wiRwOs0GfaWwhJb95prb+AKoAY5usp8eaQsmYbAYBnWl+cleXtnIKEwJ4OY+YVJhlIY9Pbl+49iL2uTL0O/hXcDbL0KHx2pfu//yDD3alybwrHA8QU7S4C6P2vI5h6WMnAD0E=
*.w.example.\t3600\tIN\tMX\t1 ai.example.
*.w.example.\t3600\tIN\tRRSIG\tMX 8 2 3600 20040409183619 20040509183619 38353 example. ZasaVvJ9n08Tx6wLVI5nat5LFcaUdCvcs4Oztowc/QbYuu6Ib/TXHCfm33JKyqyZiKBwiHIzpjEslgj+2X42SiIu9A57aGRWJqTfUkpe33b7iQik26Fas57k+Zr8OruHG+q5vCvcyj/BdQjNuvFpCPtp2186DZAI2YiXCvgvdvQ=
*.w.example.\t3600\tIN\tNSEC\tx.w.example. MX RRSIG NSEC
*.w.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. ebmpkmslOqUOJSErn7SoUCHGhxF4MVhx+xEk7Pu3pMfdz9i2GzlyExdOuZE48Z2YOUrP7i3Rlb2d+aqnBCSDuYW3pA7vAfm6GkZST+A5Hns5wy/KxQUxy6Fn5ruzqAUa1TIl5lUjsU+8b/KXZ0/BVrfg7LOUJlgjgVvbuYf0xZY=
x.w.example.\t3600\tIN\tMX\t1 xx.example.
x.w.example.\t3600\tIN\tRRSIG\tMX 8 3 3600 20040409183619 20040509183619 38353 example. uBNMY+9VbUbXg8s4hPKGGCASm4D0OCbdAKeDU71xYCDPd9+/gWvC8N+V+gbKMja5W67gh/LBjffkPYcWD5ttuom6SkORX95jwMlm1LFEBMmr0+jkU4j+oiQ6r3dcO+EHBpDzd3osMIAOoaNyUfeWoUtl31QtqnFysy4avQx9Mx4=
x.w.example.\t3600\tIN\tNSEC\tx.y.w.example. MX RRSIG NSEC
x.w.example.\t3600\tIN\tRRSIG\tNSEC 8 3 3600 20040409183619 20040509183619 38353 example. XyotdqZtaSA7xkmBwkE1rJB0WwHeZL4IhTKe498OUIbIEB81MjZ24YSz/ufDS0CPxqz4KwKu456HDwLubhTlIOJUXFzjyHZvuJ+3I/qGo2wt0VtOlzoKMMp/alN+Yh+PuOeiPTPremBZGY5BLN/HiekayLXPVqcNjoZvFZnCk9U=
x.y.w.example.\t3600\tIN\tMX\t1 xx.example.
x.y.w.example.\t3600\tIN\tRRSIG\tMX 8 4 3600 20040409183619 20040509183619 38353 example. AfUZgFVehMl1vifSWhH6D4u15sKZwT2KPCCntLBk26aQB089XR1adDQfRTm/Z5uW9kMjebnz2UN+158uQEjCgFM9Y3LaHakEbb5KVhMEWVTmh8Rg3eaRhouo1ruvdcRyET44UF3euhY3uHi7aXsZPufRaUCXCGRqF/ZO9MvOcfo=
x.y.w.example.\t3600\tIN\tNSEC\txx.example. MX RRSIG NSEC
x.y.w.example.\t3600\tIN\tRRSIG\tNSEC 8 4 3600 20040409183619 20040509183619 38353 example. eMyJm+jbEMy3HziBATVnXXpL3DJ46aZEbK0PXaqoWml4E9On8SCt2VCq1SJG+tz4Ah6ltnHu7ZTidiaXjV9K//UNfQaZz6KbxolhltPnLhwAW3lUJMNG88E1evhN97X9zzct87DmGAvMMMtAs4vDiP53DvcIwCoMCxfyurJ7Hr0=
xx.example.\t3600\tIN\tA\t192.0.2.10
xx.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20040409183619 20040509183619 38353 example. HoCNtIY8jdsGdu6nH+VwLEqmzH11vTTLyiZXUJrg1LvohZRrG8tdjAQpZvTPYdP8Z/P0Secy4xUk2wFIpqluENPFUxGinkEPsFzl3OlfT0JAyLBCcww2KKrXOQzlvUEBMAj/DFlErUEMiJqtAfPU1CfQvnW1mi0+WuqyBBN6Av8=
xx.example.\t3600\tIN\tHINFO\t"KLH-10" "TOPS-20"
xx.example.\t3600\tIN\tRRSIG\tHINFO 8 2 3600 20040409183619 20040509183619 38353 example. OlQJpzvy3y35q1Iny/YhT0H72UOv3Vr8l0UjprB9fAfkPfJ1tnmOjHqonUjG/T+oB2JzzJp9D8qAdxRVBGcVrfY+7d3/aS9oSJ0fwq+4Iv2coAa/VhRZ+6zbZwpVfdJ7Qt6ek0VxdifoStzaYGDaZP6ceovXUnOrc7fkorTiIJ4=
xx.example.\t3600\tIN\tAAAA\t2001:db8::f00:baaa
xx.example.\t3600\tIN\tRRSIG\tAAAA 8 2 3600 20040409183619 20040509183619 38353 example. gRWCKCsaDkmX1g768tsLwG0Luaf8vvnYeINOr4WDVD3jTgsj7agXjsMRlsoQif5EcJ9Xo8q4UrcVZPo2nt5/55Hc9egKroLXoq86gA0juCECbM5rYKl41GcEObe8UHN+2P3dclCwkVcZ3sLUEsNgtmAllMBdRtDuWvg5GMYUqWg=
xx.example.\t3600\tIN\tNSEC\texample. A HINFO AAAA RRSIG NSEC
xx.example.\t3600\tIN\tRRSIG\tNSEC 8 2 3600 20040409183619 20040509183619 38353 example. p50kHhYlNWKrbo2OeUwvVH6ithEZctVUqRsLeZq7PmE9lA06Wu6MWuImV5rZC4lUUsbkok48FD/y69fuXms7Jq+tYSwiQKjzNPKt9cRww4xIAY/4B+B6t3AP1QY6Yw6JLeQMNoj3xTWzBLYmq6sA3pEHRqU8S2YdCJFat44GeYo=
"###.replace("\\t", "\t");
let zone_file_path = mk_test_data_abs_path_string("test-data/example.rfc4035");
let ksk_path = mk_test_data_abs_path_string("test-data/Kexample.+008+31967");
let zsk_path = mk_test_data_abs_path_string("test-data/Kexample.+008+38353");
// Use dnst signzone instead of ldns-signzone so that -b works with -f-.
// Use -A to get the second DNSKEY RRSIG as included in RFC 4035 Appendix A.
// Use -T to output RRSIG timestmaps in YYYYMMDDHHmmSS format to match
// RFC 4035 Appendix A.
// Use -b to get similar ordering to that of RFC 4035 Appendix A.
// Use -e and -i to generate RRSIG timestamps that match RFC 4035 Appendix A.
// Use RSASHA256 (type 8) signing keys as they produce consistent
// signatures for the same input, and are supported by us unlike
// RSASHA1 (type 5) which is used by the RFC 4035 Appendix A signed
// zone but we do not support.
let res = FakeCmd::new([
"dnst",
"signzone",
"-A",
"-T",
"-R",
"-f",
"example.signed",
"-e",
"20040409183619",
"-i",
"20040509183619",
&zone_file_path,
&ksk_path,
&zsk_path,
])
.cwd(&dir)
.run();
assert_eq!(res.stdout, "");
assert_eq!(res.stderr, "");
assert_eq!(res.exit_code, 0);
let signed_zone = std::fs::read_to_string(dir.path().join("example.signed")).unwrap();
assert_eq!(signed_zone, expected_signed_zone);
}
#[test]
fn rfc_5155_nsec3_signed_zone_example() {
let dir = tempfile::TempDir::new().unwrap();
// TODO: RFC 5155 Appendix A Example Zone shows lowercase NSEC3 salt
// but we produce uppercase NSEC3 salt - does it matter? LDNS shows it
// in lowercase too.
// TODO: RFC 5155 Appendix A Example Zone shows next NSEC3 hashed
// owner in lowercase but we show it in uppercase - does it matter?
// LDNS shows it in lowercase too.
// TODO: RFC 5155 Appendix A Example Zone shows next NSEC3 hashed
// owner as the owner of the next record output but we use a different
// order - does it matter?
// TODO: RFC 5155 Appendix A Example Zone shows NSEC3 covered types
// in a different order than we do, e.g.
// NS SOA MX RRSIG DNSKEY NSEC3PARAM vs
// MX DNSKEY NS SOA NSEC3PARAM RRSIG
// Does it matter?
// TODO: RFC 5155 Appendix A Example Zone shows NSEC3 chain
// gjeqe526plbf1g8mklp59enfd789njgi -> ji6neoaepv8b5o6k4ev33abha8ht9fgc
// But we have:
// gjeqe526plbf1g8mklp59enfd789njgi -> J7HVASCS9U2V1V0K5U1KN203SJT3P34T
// Modified from the version in RFC 4035 replacing the keys used with
// ones we have the private key for and using a key algorithm that we
// support (8 instead of 5). Matches output produced by dnst signzone
// -b (not ldns-signzone -b as the -b output is suppressed by
// ldns-signzone when using -f-) in order to get the same ordering as
// both the original ldns-signzone and the example in RFC 4035.
let expected_signed_zone = r###"
; H(example) = 0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example
; H(2t7b4g4vsa5smi47k61mv5bv1a22bojr.example) = kohar7mbb8dc2ce8a9qvl8hon4k53uhi.example
; H(a.example) = 35mthgpgcu1qg68fab165klnsnk3dpvl.example
; H(ai.example) = gjeqe526plbf1g8mklp59enfd789njgi.example
; H(ns1.example) = 2t7b4g4vsa5smi47k61mv5bv1a22bojr.example
; H(ns2.example) = q04jkcevqvmu85r014c7dkba38o0ji5r.example
; H(w.example) = k8udemvp1j2f7eg6jebps17vp3n8i58h.example
; H(*.w.example) = r53bq7cc2uvmubfu5ocmm6pers9tk9en.example
; H(x.w.example) = b4um86eghhds6nea196smvmlo4ors995.example
; H(y.w.example) = ji6neoaepv8b5o6k4ev33abha8ht9fgc.example
; H(x.y.w.example) = 2vptu5timamqttgl4luu9kg21e0aor3s.example
; H(xx.example) = t644ebqk9bibcna874givr6joj62mlhv.example
example.\t3600\tIN\tSOA\tns1.example. bugs.x.w.example. 1 3600 300 3600000 3600
example.\t3600\tIN\tRRSIG\tSOA 8 1 3600 20150420235959 20051021000000 38353 example. OQmI2syAvTPgPZCKCV2cIvJyEAWyTatdMUKhg9hBdPovmZzRZ9wWaLtRzwGUuHdzeNzA7MEPOSZ1heIWYiS4JqEfemJSwZtQRLuwhOKznPMQt7UJNN4e7cjM2j0W7D8v92TsjwdB9j47Qjl64Yl0Y26zh25Sw3JRuq2dbGbbl8I=
example.\t3600\tIN\tNS\tns1.example.
example.\t3600\tIN\tNS\tns2.example.
example.\t3600\tIN\tRRSIG\tNS 8 1 3600 20150420235959 20051021000000 38353 example. YEedzYLNAJpDj/1ekisL51HQ3m9Dmcf/kj+1XxMs86P91wWTB07mhv9Jin6ziwPPwSn2erXKsJkFOT6W5XNh1W3WlgvxsQ1mAApppm0OPxmuA/pjMiv6Hr+df+N/6IZ2Wq36EtgUXxFU+QN4WVPzwebjM9rZLtNxN8kQnhSs4E4=
example.\t3600\tIN\tMX\t1 xx.example.
example.\t3600\tIN\tRRSIG\tMX 8 1 3600 20150420235959 20051021000000 38353 example. tEw3cOYajeExrCquvSlxpcjUUKNw7Myy6WjsQvboMtM4W5rs36oLF9bJiG0IuduLz3JnGPnl8o1XgpVpsmrt/xqh2ifesUD1SILxKmljw7IvJ1VDeqsaVJxmlbG0BXhNrGLRwfuiJnvUxGf3Dl8bW1g8aLOEwwm+Gz7091GJcvM=
example.\t3600\tIN\tDNSKEY\t256 3 8 AwEAAbsD4Tcz8hl2Rldov4CrfYpK3ORIh/giSGDlZaDTZR4gpGxGvMBwu2jzQ3m0iX3PvqPoaybC4tznjlJi8g/qsCRHhOkqWmjtmOYOJXEuUTb+4tPBkiboJM5QchxTfKxkYbJ2AD+VAUX1S6h/0DI0ZCGx1H90QTBE2ymRgHBwUfBt ;{id = 38353 (zsk), size = 1024b}
example.\t3600\tIN\tDNSKEY\t257 3 8 AwEAAaYL5iwWI6UgSQVcDZmH7DrhQU/P6cOfi4wXYDzHypsfZ1D8znPwoAqhj54kTBVqgZDHw8QEnMcS3TWxvHBvncRTIXhCLx0BNK5/6mcTSK2IDbxl0j4vkcQrOxc77tyExuFfuXouuKVtE7rggOJiX6ga5LJW2if6Jxe/Rh8+aJv7 ;{id = 31967 (ksk), size = 1024b}
example.\t3600\tIN\tRRSIG\tDNSKEY 8 1 3600 20150420235959 20051021000000 31967 example. neFL5wACumr7fNXVJAjNRz+5xpmkOVtsZfoW0AnOCT9Kmo8RKkArWxIMRoqCjSwL7gqAVkkDCe0hdkktfAjqwqi2cSy2SSytqgX3MBaJlfFsg/d0cTHRK32qDlhDZ4zZ511VmJCgK5rwrHPZIO5g1FTEj+hawpPVWlFqu/rWk6M=
example.\t3600\tIN\tNSEC3PARAM\t1 0 12 AABBCCDD
example.\t3600\tIN\tRRSIG\tNSEC3PARAM 8 1 3600 20150420235959 20051021000000 38353 example. jb9Dw0kO4hEMpxqo1veI6HmYQGMo3bbahItqjBwLuQ4y1eKQEhGok/Ar6VPrXpPNDQgLnPQafmA6ziI3WoMLtA+vfT7wzLx0UK3ZGqcWPQp00MGNwYQfJ/QezIJteHtVDWBwXWj2xR3f/eUxJAxhPzgj4kOPHMnYMYF4o2ZVsD0=
0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD 2T7B4G4VSA5SMI47K61MV5BV1A22BOJR NS SOA MX RRSIG DNSKEY NSEC3PARAM
0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. psCexsG2DMIfSm4WgYSGx/DeUGcYvj9pTcCihdM3QO5bKJfXMQ6f0zP+Af+VpYBst+zlRZkZaoNZ04rNdm3asOLGyXlEvXSecwM9VVwpof21LaX2IW/8uue/pvr1UQQUtxqbFt5VoOoLdUVUXyo/4B5BLw1qhv3vDTbaRnKjBXc=
2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.\t3600\tIN\tA\t192.0.2.127
2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20150420235959 20051021000000 38353 example. h7JOg0b+I3ZWI4usKYTCV8Kvik2wIOlJbbgqnQuMq/eADcNucUSKP454p+6HgrTA+11FLirv07d1CL3HcXUiNd0J/85LfII965t9jEKOWq2tWzEXj0LYhoXFqcfLDmYBSNxOXy8/VexRvYlIk1wooQ8aYqdc0VIeQKba66yNAKo=
2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD 2VPTU5TIMAMQTTGL4LUU9KG21E0AOR3S A RRSIG
2t7b4g4vsa5smi47k61mv5bv1a22bojr.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. W3ZqyTU5dpvSeNYUtjk5mGDDyLWyoNmJXBNfZmv9Hwpb7FZQ/dZLu9OhS6B8JBDxunRaatpNFQjurkdQNdaLPH3B61824V0mW4JZFWZuTJJMIVZtPDOXNYXeezejYwuIKn1CZXtkobdJOtQUEmiW3OjC0Hz3L/0IUoKTgIbLZB4=
2vptu5timamqttgl4luu9kg21e0aor3s.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD 35MTHGPGCU1QG68FAB165KLNSNK3DPVL MX RRSIG
2vptu5timamqttgl4luu9kg21e0aor3s.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. n0psta4fcHe5JvTi3KSA4O0n732l/4qYpwZhso2G8MvCTGTlVrGH/DQTPjS9rhBwkw2AWBN0kAVZ7Ry48jtfub9zC6VjLaF2aNzBScvbRRsewJi3pdNbo69qidOrlBEJUyVRo9cu3XQOA0zjT0mh+iT31oqQMNg3n3d66HnD3bs=
35mthgpgcu1qg68fab165klnsnk3dpvl.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD B4UM86EGHHDS6NEA196SMVMLO4ORS995 NS DS RRSIG
35mthgpgcu1qg68fab165klnsnk3dpvl.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. cLVHqZp0jL0MG2ZqcnVUsOHkrGajuOtSJU/W9t7u8JDr0pjhw/yhtY1sCemgHEDVz1E9cyp3WLvcVphApGOMR6tkVOHzsPbVlKHRHogILXWL5Q6BUvXCWYtTsPvRT0eukGy/yFGL+JnCI+uRHuhMqmAmfjvBfIDzvYyy8MjNF5w=
a.example.\t3600\tIN\tNS\tns1.a.example.
a.example.\t3600\tIN\tNS\tns2.a.example.
a.example.\t3600\tIN\tDS\t58470 5 1 3079F1593EBAD6DC121E202A8B766A6A4837206C
a.example.\t3600\tIN\tRRSIG\tDS 8 2 3600 20150420235959 20051021000000 38353 example. hvn/QOHcGuvuZFuBgc2w6Z6GwhIYlzz+Rc1Y0F8ewD9IURCHmU438p++lx8MRY7IlGpa9rO+TIXiGpeA4amgO0wLTNUz9PcCihZuJ7wI8CSM49VB9OyCgORDsW13WTAUkqKgKyldbH3xE4EzNlY59pmWQgt6dGdHNj1aM9WsEco=
ns1.a.example.\t3600\tIN\tA\t192.0.2.5
ns2.a.example.\t3600\tIN\tA\t192.0.2.6
ai.example.\t3600\tIN\tA\t192.0.2.9
ai.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20150420235959 20051021000000 38353 example. Y/ycwCcc4Ocm7Hmn0p7G2LqiQmm3rO9J8up3Q/rz6VhRm9IhAYj9Pae3iaGuaPd3lXwmWvSYx6aLhGvl5q8BPJXH5l220pDH1aszH48c+sYfSSgSkCe3Tjcd2OnWBX3rkbVIs8JYkAdkBct8jOQXzzjqtRIwdE4rbBav4/Azk3s=
ai.example.\t3600\tIN\tHINFO\t"KLH-10" "ITS"
ai.example.\t3600\tIN\tRRSIG\tHINFO 8 2 3600 20150420235959 20051021000000 38353 example. gt5ErLUHitivHynCgmH/uQJ9xnb/Y4Qja8LiQ2zilH2Yyqon2RBO/GRwSCVFN6uBAXB4JHvW/+Aflpa0MRX+CSvvWFUG65QTalw3z3tksEf+1OadC6r3sst6IF7CjCt3PQKkKuZfxWn9V6yRSYXH8Sp+YPsb63NAQev9RJhMYII=
ai.example.\t3600\tIN\tAAAA\t2001:db8::f00:baa9
ai.example.\t3600\tIN\tRRSIG\tAAAA 8 2 3600 20150420235959 20051021000000 38353 example. diBqPpbIyhguumnN3aqQnAKiqOZk0q1fJSANjYZcnGJjAxrTfQ1kkEjG1NAJpINnfIo2lD1dxXwHvW9TJXHRcx6KcLc5v0e+weoLtA+6eNViLQVG7JvL24amuPMHS0oJBE4bkJEMYGvtJmIitb0rNaA4MIf3j0oYWS+dhL4B8A4=
b4um86eghhds6nea196smvmlo4ors995.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD GJEQE526PLBF1G8MKLP59ENFD789NJGI MX RRSIG
b4um86eghhds6nea196smvmlo4ors995.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. q2De6iOGJZBGqKlrmdGEXvXHb2Rz0OT1P5Rnfqn+TutSupUYmLKZYlk66QSj/CXW8aLb0mDGdqyRTjm7DuDv0+su2T+w0SoS3M5t1wiDSeE/vl6VFwGuZeCZGb0Re4sfkGpuFv/LD6VmNvhCcy+O+sXrguMrMdJ3lQCvJQjhCqA=
c.example.\t3600\tIN\tNS\tns1.c.example.
c.example.\t3600\tIN\tNS\tns2.c.example.
ns1.c.example.\t3600\tIN\tA\t192.0.2.7
ns2.c.example.\t3600\tIN\tA\t192.0.2.8
gjeqe526plbf1g8mklp59enfd789njgi.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD JI6NEOAEPV8B5O6K4EV33ABHA8HT9FGC A HINFO AAAA RRSIG
gjeqe526plbf1g8mklp59enfd789njgi.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. WOV1cBmmwlbTsR4qie8996TsFxWeYh0Q9CKNvHbTRtvNX2BHFa2K8583B+5x/GBOrHdZqFgSHXqkyAkD8y1gAj0cHzCUIvZhlGwHKtOlLk3lZBK0UdQGtWzbqRJBfoEZW9ZLuyWw1R67hxCkysPS2Mq4pHsXQgbQZZt4G7O/XwM=
ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD K8UDEMVP1J2F7EG6JEBPS17VP3N8I58H
ji6neoaepv8b5o6k4ev33abha8ht9fgc.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. J0QT2D31aTMBikuGbnGDTazPPx2fHNg3R8T6BPyNW+nX2qtI74BEdgFOsPUL7C3DlXPayWDYHFREXumHQldAb65X2N4EGblZVJ5HiVVxe4mqaGipckyWhvbNXTm3ITvvuCK6G+Q0XUMsQ2INb7wF9Qo1acd1b5cLLi1UNET3NPo=
k8udemvp1j2f7eg6jebps17vp3n8i58h.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD KOHAR7MBB8DC2CE8A9QVL8HON4K53UHI
k8udemvp1j2f7eg6jebps17vp3n8i58h.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. s43tb7Gyh2lQ5wSKgxNMrP0HFJtjBuT+lzutMwoivhn4CMmJqYoOiMgtozsOg8OcG6mBZn6WqEC5y05CuHrHOirzGY55+Jp2B/I/RwVgWjWTA5qsjuqohgJjNnJDF1PpC+qVJZjdDU41+q/M63fiMvDBeJ5PAfqqdDLOxX/muGc=
kohar7mbb8dc2ce8a9qvl8hon4k53uhi.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD Q04JKCEVQVMU85R014C7DKBA38O0JI5R A RRSIG
kohar7mbb8dc2ce8a9qvl8hon4k53uhi.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. iCIqnxLw7KsQZxj7MNPlEGlbU4SvoroyygNAILtzxgEY0qJflPEsV4lyjsJMNMPMvzlyzs4zAl2StBYF+Y9WDCJf5h1t/W0tB9oddfoLwtAEqukHFW6DIcoHuERjdqTVr3+fvcIJzwGAuT+TYuOucq/2aTwmludE1lhHBgOIjJU=
ns1.example.\t3600\tIN\tA\t192.0.2.1
ns1.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20150420235959 20051021000000 38353 example. i2ljZXbHVRHFrDI00jW8Ln6Pivq0S2cBS9TNBHoiiCvMR4cxE/jijDAqt7U/TqIHyu3lSK3tmLEZhCh9rWEXOzfLuzo6RfcXvg4V7lLXuLMRhvLjTn1+LmWHGaW6xnNkvapU8/bm2Ckriy3+05cTEsbpTJ9swf2Fg6Q2yDnn8ig=
ns2.example.\t3600\tIN\tA\t192.0.2.2
ns2.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20150420235959 20051021000000 38353 example. hnBX5fSoXikZeE903WDLD6o2u+1j+9mo+u5b1YRxlCvR1FPRnhV8byCTEpV8RyQdjN6YL/tCG+wyLDysdHiVkNMEQe8SIRTzJLXFD1OvvdpIe+tNA2yTEemrMEkJIDcQeXy5BqWQwZb+DckvOxwnAIsHgCidUGNVXQrqtC0hwJc=
q04jkcevqvmu85r014c7dkba38o0ji5r.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD R53BQ7CC2UVMUBFU5OCMM6PERS9TK9EN A RRSIG
q04jkcevqvmu85r014c7dkba38o0ji5r.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. TolAxcK5GG0pkbK6DawH8immUjUF/HbrVlmD+QPB0te4JcawLHxARbigxoHQnwUNqhoU5CEj2f/ozPjWJ/F+sj3ZsLzC4dcGp4nMOE0cdP9SQ+5fxuq57/Aj26invkthydBMdk+kZSD5IDw2I4llR3Es+P1ZqA+qd4auIpcHsX4=
r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD T644EBQK9BIBCNA874GIVR6JOJ62MLHV MX RRSIG
r53bq7cc2uvmubfu5ocmm6pers9tk9en.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. CsWt2WIBFyVeGv5wE13EI3MyGa4lhoZIOBQQWphNLKeH7j5c5xKmaoeleKmsl2D1Ni1+sr8U5IwvWfHmjOqo0mo4zQdv6K/U6AcnwXd0hZ+jCWE0QNAJt4HJXC/7vBCeDcSZ1MJ95X24FxkToQRPFkboCoP/+9glOJAx6X+jnCE=
t644ebqk9bibcna874givr6joj62mlhv.example.\t3600\tIN\tNSEC3\t1 1 12 AABBCCDD 0P9MHAVEQVM6T7VBL5LOP2U3T2RP3TOM A HINFO AAAA RRSIG
t644ebqk9bibcna874givr6joj62mlhv.example.\t3600\tIN\tRRSIG\tNSEC3 8 2 3600 20150420235959 20051021000000 38353 example. AI+9pSvUUyTVQiLMX0Iz/2yyL9CdFzOYYJkbYH6sJX7/649vikFsMSCTpz3UTBp17ubKtlr1sP5Xiu++RCXu0hL8k9AOBSzy1ZmCS3T24Nj20gzuueN77ov0NsVxAh/tyBJV5LoNG1TG7+AVbepsqVKOMvON4clunFHlbTCYueM=
*.w.example.\t3600\tIN\tMX\t1 ai.example.
*.w.example.\t3600\tIN\tRRSIG\tMX 8 2 3600 20150420235959 20051021000000 38353 example. OzXlQ4NOdqgULXY+nHuXWzomMR9WAha768A/zfm24C4/Ug5OIR0vkjNZ0Is2MoXPCMv2GI2X42BkIY9S60pjlJ26IITW8pzArt+xURsWfonw9/WF/mpa6r1IxXZ3QCWmS7aIrQ/sDw1u6UnsTJIaFZbE94DvyeU+/TZ8mN8tz2k=
x.w.example.\t3600\tIN\tMX\t1 xx.example.
x.w.example.\t3600\tIN\tRRSIG\tMX 8 3 3600 20150420235959 20051021000000 38353 example. nw5Z1G1XkM3R6uJNzohynT9cXnNwCDwORheT4aqmO3EcfJrrp6k5VjtdY5Bqtxo6FlCgybcsinZVdcIV+14374aQrvezjiZmiqECdCDHzO/X4XVaxk6ei5oj+22Pl4P6D3YLt6D+KlXZbdTmfRkgo8ZwQ9JceEYwvTrlPQw3ldQ=
x.y.w.example.\t3600\tIN\tMX\t1 xx.example.
x.y.w.example.\t3600\tIN\tRRSIG\tMX 8 4 3600 20150420235959 20051021000000 38353 example. fJTea7tirPJYIy10rt0PHyV08ZbfuyJ4dyh8B4ycCxiHZkRJgnNjTS4y+/csAKkaIvToub5f/ob53/4ZMg9f6SlTby6ybbwxY4bWoZsISXIjhw3mDdVm2FsJiz4r8hPQjTOLSE6wpZtbxgfwtXa7OiJbzgAuHg9KbgGk2PNPfns=
xx.example.\t3600\tIN\tA\t192.0.2.10
xx.example.\t3600\tIN\tRRSIG\tA 8 2 3600 20150420235959 20051021000000 38353 example. ZPoxxa+U0ZI5Do7mJsq5rGC+bpUNTwRtTZJrr+tREhQn/AWKVwJGJFTitzn5akmusIk3RLGIfZPOLECMu6o+sF924qKA+M66ts98HfQP8b+duBd7kFW5I0hqtq0pcRDJm/tyFRgDRTas0puUzgNt4jud4CGFD0SM0h/MsWnxSnE=
xx.example.\t3600\tIN\tHINFO\t"KLH-10" "TOPS-20"
xx.example.\t3600\tIN\tRRSIG\tHINFO 8 2 3600 20150420235959 20051021000000 38353 example. hCbnIDg46IzRgVjOsllF/Q/VyqJQcMa3v/Ykh4wctqFQiyuJaIvwiGYm/QMlMZswqTF921ivFdvNVZ+Q/3p/6ykpTNWQriw5Bta2ba6/ALI/ZQVbUht4Znq5Xxs3El1641vg9936calXXmLzwNNs4JJwGhUbui9PF9UrRv49OoM=
xx.example.\t3600\tIN\tAAAA\t2001:db8::f00:baaa
xx.example.\t3600\tIN\tRRSIG\tAAAA 8 2 3600 20150420235959 20051021000000 38353 example. TX5v7Jnw/lo29b3jr0aSbRGUDrk/NJm/3mcdGgSXsIPObhEI82PGPLKpy6vTQDyoXVIMigG0XATN74gav/kF90aBsTRsm6ITKE09sccLR8OIg+lFaVtEjSroZBrBHRocWStD4yssaWrmhS/+g8IC3PTPEPXJDFkj46vK9Z/nlNU=
"###.replace("\\t", "\t");
let zone_file_path = mk_test_data_abs_path_string("test-data/example.rfc5155");
let ksk_path = mk_test_data_abs_path_string("test-data/Kexample.+008+31967");
let zsk_path = mk_test_data_abs_path_string("test-data/Kexample.+008+38353");
// Use `dnst signzone` mode instead of `ldns-signzone` mode to get
// more control via specific CLI arguments over the output format to
// better match that of the example in RFC 4035 Appendix A without
// also introducing extra comments that `ldns-signzone -b` adds.
// Specifically the following options are used to make the output a
// better match to that of RFC 4035 Appendix A:
//
// -T outputs RRSIG timestamps in YYYYMMDDHHmmSS format.
// -L outputs NSEC3 hash mappings.
// -R orders RRSIGs after the records they sign.
//
// We use RSASHA256 (type 8) signing keys instead of RSASHA1 (type 5)
// used by RFC 4035 Appendix A as we don't support type 5 (as it is
// NOT RECOMMENDED by RFC 8624) and because RSASHA256 signatures are
// consistent for the same input unlike ECDSAP256SHA256 for example.
let res = FakeCmd::new([
"dnst",
"signzone",
"-T",
"-L",
"-R",
"-f-",
"-e",
"20150420235959",
"-i",
"20051021000000",
"-n",
"-t12",
"-P",
"-saabbccdd",
&zone_file_path,
&ksk_path,
&zsk_path,
])
.cwd(&dir)
.run();
assert_eq!(res.stdout, expected_signed_zone);
// assert_eq!(res.stderr, ""); // Commented out due to NSEC3 iterations warning.
assert_eq!(res.exit_code, 0);
}
#[test]
fn glue_records_should_not_be_hashed_or_signed() {
// So there should not be NSEC, NSEC3 or RRSIG RRs for A/AAAA RRs at
// glue owner names.
todo!()
}
#[test]
fn ds_digest_rdata_should_be_presented_as_lowercase() {
// For compatibility with LDNS, so when invoked as LDNS, but for speed maybe not when invoked as DNST.
todo!()
}
#[test]
fn next_owner_hash_in_nsec3_rdata_should_be_lowercase_in_ldns_mode() {
// For compatibility with LDNS, so when invoked as LDNS, but for speed maybe not when invoked as DNST.
todo!()
}
//------------ Helper functions ------------------------------------------
fn create_file_with_content(dir: &TempDir, filename: &str, content: &[u8]) {
let mut file = File::create(dir.path().join(filename)).unwrap();
file.write_all(content).unwrap();
}
fn run_setup() -> TempDir {
let dir = tempfile::TempDir::new().unwrap();
create_file_with_content(&dir, "ksk1.key", b"example.org. IN DNSKEY 257 3 15 6VdB0mk5qwjHWNC5TTOw1uHTzA0m3Xadg7aYVbcRn8Y= ;{id = 38873 (ksk), size = 256b}\n");
create_file_with_content(&dir, "ksk1.ds", b"example.org. IN DS 38873 15 2 e195b1a7d31c878993ad0095d723592a1e5ea55c90b229fc35e4c549ef406f6c\n");
create_file_with_content(&dir, "ksk1.private", b"Private-key-format: v1.2\nAlgorithm: 15 (ED25519)\nPrivateKey: /e7bFDFF88sdC949PC2YoHX9KJ5eEak3bk/Tub2vIng=\n");
create_file_with_content(&dir, "zsk1.key", b"example.org. IN DNSKEY 256 3 15 fPzhX3Tq/w3ncwsWYIRsK8rHLNtkVv1O3kXYAMdBQUk= ;{id = 44471 (zsk), size = 256b}");
create_file_with_content(&dir, "zsk1.private", b"Private-key-format: v1.2\nAlgorithm: 15 (ED25519)\nPrivateKey: mc2xW8JiES5Ub6UPP2xoHT0KyD6Lvi6fnjugjnRzBJU=");
create_file_with_content(&dir, "zonemd1_example.org.zone", b"\
example.org. 240 IN SOA example.net. hostmaster.example.net. 1234567890 28800 7200 604800 240\n\
example.org. 240 IN NS example.net.\n\
; Will be replaced when using ZONEMD option\n\
example.org. 240 IN ZONEMD 1234567890 1 1 ABABABABABABABABABABABABABABABABABABABABABABABAB ABABABABABABABABABABABABABABABABABABABABABABABAB\n\
example.org. 240 IN ZONEMD 1234567890 1 2 ABABABABABABABABABABABABABABABABABABABABABABABAB ABABABABABABABABABABABABABABABABABABABABABABABAB ABABABABABABABABABABABABABABABAB\n\
example.org. 240 IN A 128.140.76.106\n\
*.example.org. 240 IN A 1.2.3.4\n\
deleg.example.org. 240 IN NS example.com.\n\
occluded.deleg.example.org. 240 IN A 1.2.3.4\n\
");
create_file_with_content(&dir, "nsec3_optout1_example.org.zone", b"\
example.org. 240 IN SOA example.net. hostmaster.example.net. 1234567890 28800 7200 604800 240\n\
example.org. 240 IN NS example.net.\n\
example.org. 240 IN A 128.140.76.106\n\
insecure-deleg.example.org. 240 IN NS example.com.\n\
occluded.insecure-deleg.example.org. 240 IN A 1.2.3.4\n\
secure-deleg.example.org. 240 IN NS example.com.\n\
secure-deleg.example.org. 240 IN DS 3120 15 2 0675d8c4a90ecd25492e4c4c6583afcef7c3b910b7a39162803058e6e7393a19\n\
");
dir
}
/// Filter a string slice for lines containing at least one of the provided patterns.
#[allow(dead_code)]
fn filter_lines_containing_any(src: &str, patterns: &[&str]) -> String {
if patterns.is_empty() {
// For consistency with str::contains() and filter_lines_containing_all()
String::from(src)
} else {
src.split_inclusive('\n')
.filter(|s| {
for p in patterns {
if s.contains(p) {
return true;
}
}
false
})
.collect()
}
}
/// Filter a string slice for lines containing all provided patterns.
fn filter_lines_containing_all(src: &str, patterns: &[&str]) -> String {
src.split_inclusive('\n')
.filter(|s| {
for p in patterns {
if !s.contains(p) {
return false;
}
}
true
})
.collect()
}
fn mk_test_data_abs_path_string(rel_path: &str) -> String {
std::env::current_dir()
.unwrap()
.join(rel_path)
.to_string_lossy()
.to_string()
}
}