1use 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
30pub const ALGORITHM: Algorithm = Algorithm::ECDSAP256SHA256;
32
33pub const KEY_TTL: u32 = 3600;
35
36pub const INCEPTION_SKEW_SECS: u32 = 3600;
38
39pub const VALIDITY_SECS: u32 = 7 * 24 * 3600;
41
42pub struct ZoneSigner {
44 zone: Name,
45 key: KeyPair<Private>,
46 dnskey: DNSKEY,
47 key_tag: u16,
48 soa: SOA,
50}
51
52impl ZoneSigner {
53 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 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 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 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 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 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 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 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
250pub 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
258pub const CLASS: DNSClass = DNSClass::IN;