Skip to main content

netget/server/dns/
dnssec.rs

1//! DNSSEC (RFC 4033-4035) for one zone: NetGet signs what the model answers.
2//!
3//! The model never sees a key, a signature or an NSEC record. It answers as it always does,
4//! and when the operator names a zone with the `dnssec_zone` startup parameter the server signs
5//! every answer under that zone for a client that sets the DO bit, and every zone transfer of
6//! it. The key is generated at startup — one ECDSA P-256 key (algorithm 13) acting as both KSK
7//! and ZSK, flags 257 — and lives only as long as the server; a validator is given it as a trust
8//! anchor by querying the zone's DNSKEY, which the server answers itself.
9//!
10//! Denial of existence:
11//!
12//! - **In an answer**, compact denial ("black lies", RFC 9824 without the NXNAME type): a name
13//!   the model says does not exist is answered NOERROR with one NSEC at the queried name whose
14//!   next name is `\000.<qname>` and whose bitmap holds only RRSIG and NSEC. It proves no type
15//!   exists there, needs no knowledge of the rest of the zone (the model has none to give), and
16//!   cannot be walked. A validator accepts it as NODATA; NXDOMAIN is not reported as such,
17//!   which is the documented cost of the technique.
18//! - **In a zone transfer**, a real NSEC chain over every owner name in the zone, because a
19//!   transferred zone is a zone file and a verifier checks the whole chain.
20//!
21//! Signatures are valid from an hour before signing to seven days after.
22
23use anyhow::{bail, Context, Result};
24use hickory_proto::rr::dnssec::rdata::{DNSSECRData, DNSKEY, NSEC, RRSIG};
25use hickory_proto::rr::dnssec::{tbs, Algorithm, KeyFormat, KeyPair, Private};
26use hickory_proto::rr::rdata::SOA;
27use hickory_proto::rr::{DNSClass, Name, RData, Record, RecordType};
28use std::collections::{BTreeMap, BTreeSet};
29
30/// The one algorithm: ECDSA P-256 with SHA-256 (13), the algorithm RFC 8624 recommends.
31pub const ALGORITHM: Algorithm = Algorithm::ECDSAP256SHA256;
32
33/// TTL of the DNSKEY RRset and of a synthesized SOA.
34pub const KEY_TTL: u32 = 3600;
35
36/// Signatures start this long before signing, for clock skew.
37pub const INCEPTION_SKEW_SECS: u32 = 3600;
38
39/// And end this long after.
40pub const VALIDITY_SECS: u32 = 7 * 24 * 3600;
41
42/// The zone's signing key and what is derived from it.
43pub struct ZoneSigner {
44    zone: Name,
45    key: KeyPair<Private>,
46    dnskey: DNSKEY,
47    key_tag: u16,
48    /// The SOA used when the model's answer carries none and a negative answer needs one.
49    soa: SOA,
50}
51
52impl ZoneSigner {
53    /// Generate a key for `zone`.
54    pub fn generate(zone: Name, now: u32) -> Result<ZoneSigner> {
55        let mut zone = zone;
56        zone.set_fqdn(true);
57        let pkcs8 = KeyPair::<Private>::generate_pkcs8(ALGORITHM)
58            .map_err(|e| anyhow::anyhow!("cannot generate a DNSSEC key: {e}"))?;
59        let key = KeyFormat::Pkcs8
60            .decode_key(&pkcs8, None, ALGORITHM)
61            .map_err(|e| anyhow::anyhow!("cannot load the generated DNSSEC key: {e}"))?;
62        let public = key
63            .to_public_bytes()
64            .map_err(|e| anyhow::anyhow!("cannot derive the DNSSEC public key: {e}"))?;
65        // Zone key, secure entry point: one key signing everything, flags 257.
66        let dnskey = DNSKEY::new(true, true, false, ALGORITHM, public);
67        let key_tag = dnskey
68            .calculate_key_tag()
69            .map_err(|e| anyhow::anyhow!("cannot compute the key tag: {e}"))?;
70        let host = |label: &str| -> Result<Name> {
71            Name::from_str_relaxed(label)?
72                .append_domain(&zone)
73                .context("zone name too long")
74        };
75        let soa = SOA::new(
76            host("ns1")?,
77            host("hostmaster")?,
78            now,
79            3600,
80            600,
81            604_800,
82            300,
83        );
84        Ok(ZoneSigner {
85            zone,
86            key,
87            dnskey,
88            key_tag,
89            soa,
90        })
91    }
92
93    pub fn zone(&self) -> &Name {
94        &self.zone
95    }
96
97    pub fn key_tag(&self) -> u16 {
98        self.key_tag
99    }
100
101    /// True when `name` is the zone or below it.
102    pub fn covers(&self, name: &Name) -> bool {
103        self.zone.zone_of(name)
104    }
105
106    pub fn dnskey_record(&self) -> Record {
107        let mut r = Record::with(self.zone.clone(), RecordType::DNSKEY, KEY_TTL);
108        r.set_data(Some(RData::DNSSEC(DNSSECRData::DNSKEY(
109            self.dnskey.clone(),
110        ))));
111        r
112    }
113
114    /// The SOA a negative answer carries when the model gave none.
115    pub fn soa_record(&self) -> Record {
116        let mut r = Record::with(self.zone.clone(), RecordType::SOA, KEY_TTL);
117        r.set_data(Some(RData::SOA(self.soa.clone())));
118        r
119    }
120
121    /// The RRSIG over one RRset. Every record must share owner, class and type; their TTLs are
122    /// made equal (the smallest), since an RRset's TTL is signed as one value.
123    pub fn sign_rrset(&self, rrset: &mut [Record], now: u32) -> Result<Record> {
124        let first = rrset.first().context("cannot sign an empty RRset")?;
125        let name = first.name().clone();
126        let rtype = first.record_type();
127        let class = first.dns_class();
128        if rrset
129            .iter()
130            .any(|r| r.name() != &name || r.record_type() != rtype || r.dns_class() != class)
131        {
132            bail!("an RRset must share owner, class and type");
133        }
134        let ttl = rrset.iter().map(|r| r.ttl()).min().unwrap_or(KEY_TTL);
135        for r in rrset.iter_mut() {
136            r.set_ttl(ttl);
137        }
138        let inception = now.saturating_sub(INCEPTION_SKEW_SECS);
139        let expiration = now.saturating_add(VALIDITY_SECS);
140        let labels = name.num_labels();
141        let to_sign = tbs::rrset_tbs(
142            &name,
143            class,
144            labels,
145            rtype,
146            ALGORITHM,
147            ttl,
148            expiration,
149            inception,
150            self.key_tag,
151            &self.zone,
152            rrset,
153        )
154        .map_err(|e| anyhow::anyhow!("cannot serialise {name} {rtype} for signing: {e}"))?;
155        let signature = self
156            .key
157            .sign(ALGORITHM, &to_sign)
158            .map_err(|e| anyhow::anyhow!("cannot sign {name} {rtype}: {e}"))?;
159        let sig = RRSIG::new(
160            rtype,
161            ALGORITHM,
162            labels,
163            ttl,
164            expiration,
165            inception,
166            self.key_tag,
167            self.zone.clone(),
168            signature,
169        );
170        let mut r = Record::with(name, RecordType::RRSIG, ttl);
171        r.set_dns_class(class);
172        r.set_data(Some(RData::DNSSEC(DNSSECRData::RRSIG(sig))));
173        Ok(r)
174    }
175
176    /// RRSIGs for every in-zone RRset in `records`, which are left in place (TTLs equalised).
177    /// RRSIGs already present are not signed again.
178    pub fn signatures_for(&self, records: &mut [Record], now: u32) -> Result<Vec<Record>> {
179        let mut sets: BTreeMap<(Name, u16), Vec<usize>> = BTreeMap::new();
180        for (i, r) in records.iter().enumerate() {
181            if r.record_type() == RecordType::RRSIG
182                || r.record_type() == RecordType::OPT
183                || !self.covers(r.name())
184            {
185                continue;
186            }
187            sets.entry((r.name().clone(), u16::from(r.record_type())))
188                .or_default()
189                .push(i);
190        }
191        let mut sigs = Vec::new();
192        for indexes in sets.values() {
193            let mut rrset: Vec<Record> = indexes.iter().map(|i| records[*i].clone()).collect();
194            sigs.push(self.sign_rrset(&mut rrset, now)?);
195            for (i, signed) in indexes.iter().zip(rrset) {
196                records[*i] = signed;
197            }
198        }
199        Ok(sigs)
200    }
201
202    /// The compact-denial NSEC for `qname`: next name `\000.<qname>`, types RRSIG and NSEC.
203    pub fn compact_denial(&self, qname: &Name, ttl: u32) -> Result<Record> {
204        let next = Name::from_labels(std::iter::once(&[0u8][..]).chain(qname.iter()))
205            .map_err(|e| anyhow::anyhow!("cannot build the NSEC next name for {qname}: {e}"))?;
206        let mut next = next;
207        next.set_fqdn(true);
208        let mut r = Record::with(qname.clone(), RecordType::NSEC, ttl);
209        r.set_data(Some(RData::DNSSEC(DNSSECRData::NSEC(NSEC::new(
210            next,
211            vec![RecordType::RRSIG, RecordType::NSEC],
212        )))));
213        Ok(r)
214    }
215
216    /// The full NSEC chain over `zone_records` (which must all be in the zone), in canonical
217    /// order, closing back to the apex. Each name's bitmap lists its types plus RRSIG and NSEC.
218    pub fn nsec_chain(&self, zone_records: &[Record], ttl: u32) -> Result<Vec<Record>> {
219        let mut types: BTreeMap<Name, BTreeSet<u16>> = BTreeMap::new();
220        types.entry(self.zone.clone()).or_default();
221        for r in zone_records {
222            if r.record_type() == RecordType::RRSIG || r.record_type() == RecordType::NSEC {
223                continue;
224            }
225            types
226                .entry(r.name().to_lowercase())
227                .or_default()
228                .insert(u16::from(r.record_type()));
229        }
230        let names: Vec<Name> = types.keys().cloned().collect();
231        let mut chain = Vec::with_capacity(names.len());
232        for (i, name) in names.iter().enumerate() {
233            let next = names.get(i + 1).unwrap_or(&names[0]).clone();
234            let mut bitmap: Vec<RecordType> =
235                types[name].iter().map(|t| RecordType::from(*t)).collect();
236            bitmap.push(RecordType::RRSIG);
237            bitmap.push(RecordType::NSEC);
238            bitmap.sort_by_key(|t| u16::from(*t));
239            bitmap.dedup();
240            let mut r = Record::with(name.clone(), RecordType::NSEC, ttl);
241            r.set_data(Some(RData::DNSSEC(DNSSECRData::NSEC(NSEC::new(
242                next, bitmap,
243            )))));
244            chain.push(r);
245        }
246        Ok(chain)
247    }
248}
249
250/// The negative-caching TTL a zone's SOA gives (RFC 9077: the smaller of its TTL and MINIMUM).
251pub fn negative_ttl(soa: &Record) -> u32 {
252    match soa.data() {
253        Some(RData::SOA(s)) => soa.ttl().min(s.minimum()),
254        _ => soa.ttl(),
255    }
256}
257
258/// The class every record here uses.
259pub const CLASS: DNSClass = DNSClass::IN;