use anyhow::{bail, Context, Result};
use hickory_proto::rr::dnssec::rdata::{DNSSECRData, DNSKEY, NSEC, RRSIG};
use hickory_proto::rr::dnssec::{tbs, Algorithm, KeyFormat, KeyPair, Private};
use hickory_proto::rr::rdata::SOA;
use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
use std::collections::{BTreeMap, BTreeSet};
pub const ALGORITHM: Algorithm = Algorithm::ECDSAP256SHA256;
pub const KEY_TTL: u32 = 3600;
pub const INCEPTION_SKEW_SECS: u32 = 3600;
pub const VALIDITY_SECS: u32 = 7 * 24 * 3600;
pub struct ZoneSigner {
zone: Name,
key: KeyPair<Private>,
dnskey: DNSKEY,
key_tag: u16,
soa: SOA,
}
impl ZoneSigner {
pub fn generate(zone: Name, now: u32) -> Result<ZoneSigner> {
let mut zone = zone;
zone.set_fqdn(true);
let pkcs8 = KeyPair::<Private>::generate_pkcs8(ALGORITHM)
.map_err(|e| anyhow::anyhow!("cannot generate a DNSSEC key: {e}"))?;
let key = KeyFormat::Pkcs8
.decode_key(&pkcs8, None, ALGORITHM)
.map_err(|e| anyhow::anyhow!("cannot load the generated DNSSEC key: {e}"))?;
let public = key
.to_public_bytes()
.map_err(|e| anyhow::anyhow!("cannot derive the DNSSEC public key: {e}"))?;
let dnskey = DNSKEY::new(true, true, false, ALGORITHM, public);
let key_tag = dnskey
.calculate_key_tag()
.map_err(|e| anyhow::anyhow!("cannot compute the key tag: {e}"))?;
let host = |label: &str| -> Result<Name> {
Name::from_str_relaxed(label)?
.append_domain(&zone)
.context("zone name too long")
};
let soa = SOA::new(
host("ns1")?,
host("hostmaster")?,
now,
3600,
600,
604_800,
300,
);
Ok(ZoneSigner {
zone,
key,
dnskey,
key_tag,
soa,
})
}
pub fn zone(&self) -> &Name {
&self.zone
}
pub fn key_tag(&self) -> u16 {
self.key_tag
}
pub fn covers(&self, name: &Name) -> bool {
self.zone.zone_of(name)
}
pub fn dnskey_record(&self) -> Record {
let mut r = Record::with(self.zone.clone(), RecordType::DNSKEY, KEY_TTL);
r.set_data(Some(RData::DNSSEC(DNSSECRData::DNSKEY(
self.dnskey.clone(),
))));
r
}
pub fn soa_record(&self) -> Record {
let mut r = Record::with(self.zone.clone(), RecordType::SOA, KEY_TTL);
r.set_data(Some(RData::SOA(self.soa.clone())));
r
}
pub fn sign_rrset(&self, rrset: &mut [Record], now: u32) -> Result<Record> {
let first = rrset.first().context("cannot sign an empty RRset")?;
let name = first.name().clone();
let rtype = first.record_type();
let class = first.dns_class();
if rrset
.iter()
.any(|r| r.name() != &name || r.record_type() != rtype || r.dns_class() != class)
{
bail!("an RRset must share owner, class and type");
}
let ttl = rrset.iter().map(|r| r.ttl()).min().unwrap_or(KEY_TTL);
for r in rrset.iter_mut() {
r.set_ttl(ttl);
}
let inception = now.saturating_sub(INCEPTION_SKEW_SECS);
let expiration = now.saturating_add(VALIDITY_SECS);
let labels = name.num_labels();
let to_sign = tbs::rrset_tbs(
&name,
class,
labels,
rtype,
ALGORITHM,
ttl,
expiration,
inception,
self.key_tag,
&self.zone,
rrset,
)
.map_err(|e| anyhow::anyhow!("cannot serialise {name} {rtype} for signing: {e}"))?;
let signature = self
.key
.sign(ALGORITHM, &to_sign)
.map_err(|e| anyhow::anyhow!("cannot sign {name} {rtype}: {e}"))?;
let sig = RRSIG::new(
rtype,
ALGORITHM,
labels,
ttl,
expiration,
inception,
self.key_tag,
self.zone.clone(),
signature,
);
let mut r = Record::with(name, RecordType::RRSIG, ttl);
r.set_dns_class(class);
r.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(sig))));
Ok(r)
}
pub fn signatures_for(&self, records: &mut [Record], now: u32) -> Result<Vec<Record>> {
let mut sets: BTreeMap<(Name, u16), Vec<usize>> = BTreeMap::new();
for (i, r) in records.iter().enumerate() {
if r.record_type() == RecordType::RRSIG
|| r.record_type() == RecordType::OPT
|| !self.covers(r.name())
{
continue;
}
sets.entry((r.name().clone(), u16::from(r.record_type())))
.or_default()
.push(i);
}
let mut sigs = Vec::new();
for indexes in sets.values() {
let mut rrset: Vec<Record> = indexes.iter().map(|i| records[*i].clone()).collect();
sigs.push(self.sign_rrset(&mut rrset, now)?);
for (i, signed) in indexes.iter().zip(rrset) {
records[*i] = signed;
}
}
Ok(sigs)
}
pub fn compact_denial(&self, qname: &Name, ttl: u32) -> Result<Record> {
let next = Name::from_labels(std::iter::once(&[0u8][..]).chain(qname.iter()))
.map_err(|e| anyhow::anyhow!("cannot build the NSEC next name for {qname}: {e}"))?;
let mut next = next;
next.set_fqdn(true);
let mut r = Record::with(qname.clone(), RecordType::NSEC, ttl);
r.set_data(Some(RData::DNSSEC(DNSSECRData::NSEC(NSEC::new(
next,
vec![RecordType::RRSIG, RecordType::NSEC],
)))));
Ok(r)
}
pub fn nsec_chain(&self, zone_records: &[Record], ttl: u32) -> Result<Vec<Record>> {
let mut types: BTreeMap<Name, BTreeSet<u16>> = BTreeMap::new();
types.entry(self.zone.clone()).or_default();
for r in zone_records {
if r.record_type() == RecordType::RRSIG || r.record_type() == RecordType::NSEC {
continue;
}
types
.entry(r.name().to_lowercase())
.or_default()
.insert(u16::from(r.record_type()));
}
let names: Vec<Name> = types.keys().cloned().collect();
let mut chain = Vec::with_capacity(names.len());
for (i, name) in names.iter().enumerate() {
let next = names.get(i + 1).unwrap_or(&names[0]).clone();
let mut bitmap: Vec<RecordType> =
types[name].iter().map(|t| RecordType::from(*t)).collect();
bitmap.push(RecordType::RRSIG);
bitmap.push(RecordType::NSEC);
bitmap.sort_by_key(|t| u16::from(*t));
bitmap.dedup();
let mut r = Record::with(name.clone(), RecordType::NSEC, ttl);
r.set_data(Some(RData::DNSSEC(DNSSECRData::NSEC(NSEC::new(
next, bitmap,
)))));
chain.push(r);
}
Ok(chain)
}
}
pub fn negative_ttl(soa: &Record) -> u32 {
match soa.data() {
Some(RData::SOA(s)) => soa.ttl().min(s.minimum()),
_ => soa.ttl(),
}
}
pub const CLASS: DNSClass = DNSClass::IN;