hickory_proto/dnssec/tbs.rs
1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! hash functions for DNSSEC operations
9
10use alloc::{borrow::ToOwned, vec::Vec};
11use time::OffsetDateTime;
12
13use super::Algorithm;
14use crate::{
15 error::{ProtoError, ProtoResult},
16 rr::{DNSClass, Name, Record, RecordSet, RecordType, SerialNumber},
17 serialize::binary::{BinEncodable, BinEncoder, EncodeMode},
18};
19
20use super::{
21 SigSigner,
22 rdata::{RRSIG, SIG, sig},
23};
24
25/// Data To Be Signed.
26pub struct TBS(Vec<u8>);
27
28impl TBS {
29 /// Returns the to-be-signed serialization of the given record set using the information
30 /// provided from the RRSIG record.
31 ///
32 /// # Arguments
33 ///
34 /// * `rrsig` - SIG or RRSIG record, which was produced from the RRSet
35 /// * `records` - RRSet records to sign with the information in the `rrsig`
36 ///
37 /// # Return
38 ///
39 /// binary hash of the RRSet with the information from the RRSIG record
40 pub fn from_rrsig<'a>(
41 rrsig: &Record<RRSIG>,
42 records: impl Iterator<Item = &'a Record>,
43 ) -> ProtoResult<Self> {
44 Self::from_sig(rrsig.name(), rrsig.dns_class(), rrsig.data(), records)
45 }
46
47 /// Returns the to-be-signed serialization of the given record set using the information
48 /// provided from the SIG record.
49 ///
50 /// # Arguments
51 ///
52 /// * `name` - labels of the record to sign
53 /// * `dns_class` - DNSClass of the RRSet, i.e. IN
54 /// * `sig` - SIG or RRSIG record, which was produced from the RRSet
55 /// * `records` - RRSet records to sign with the information in the `rrsig`
56 ///
57 /// # Return
58 ///
59 /// binary hash of the RRSet with the information from the RRSIG record
60 pub fn from_sig<'a>(
61 name: &Name,
62 dns_class: DNSClass,
63 sig: &SIG,
64 records: impl Iterator<Item = &'a Record>,
65 ) -> ProtoResult<Self> {
66 Self::new(
67 name,
68 dns_class,
69 sig.num_labels(),
70 sig.type_covered(),
71 sig.algorithm(),
72 sig.original_ttl(),
73 sig.sig_expiration(),
74 sig.sig_inception(),
75 sig.key_tag(),
76 sig.signer_name(),
77 records,
78 )
79 }
80
81 /// Returns the to-be-signed serialization of the given record set.
82 ///
83 /// # Arguments
84 ///
85 /// * `rr_set` - RRSet to sign
86 /// * `zone_class` - DNSClass, i.e. IN, of the records
87 /// * `inception` - the date/time when this hashed signature will become valid
88 /// * `expiration` - the date/time when this hashed signature will expire
89 /// * `signer` - the signer to use for signing the RRSet
90 ///
91 /// # Returns
92 ///
93 /// the binary hash of the specified RRSet and associated information
94 pub fn from_rrset(
95 rr_set: &RecordSet,
96 zone_class: DNSClass,
97 inception: OffsetDateTime,
98 expiration: OffsetDateTime,
99 signer: &SigSigner,
100 ) -> ProtoResult<Self> {
101 Self::new(
102 rr_set.name(),
103 zone_class,
104 rr_set.name().num_labels(),
105 rr_set.record_type(),
106 signer.key().algorithm(),
107 rr_set.ttl(),
108 SerialNumber(expiration.unix_timestamp() as u32),
109 SerialNumber(inception.unix_timestamp() as u32),
110 signer.calculate_key_tag()?,
111 signer.signer_name(),
112 rr_set.records_without_rrsigs(),
113 )
114 }
115
116 /// Returns the to-be-signed serialization of the given record set.
117 ///
118 /// # Arguments
119 ///
120 /// * `name` - RRset record name
121 /// * `dns_class` - DNSClass, i.e. IN, of the records
122 /// * `num_labels` - number of labels in the name, needed to deal with `*.example.com`
123 /// * `type_covered` - RecordType of the RRSet being hashed
124 /// * `algorithm` - The Algorithm type used for the hashing
125 /// * `original_ttl` - Original TTL is the TTL as specified in the SOA zones RRSet associated record
126 /// * `sig_expiration` - the epoch seconds of when this hashed signature will expire
127 /// * `key_inception` - the epoch seconds of when this hashed signature will be valid
128 /// * `signer_name` - label of the entity responsible for signing this hash
129 /// * `records` - RRSet to hash
130 ///
131 /// # Returns
132 ///
133 /// the binary hash of the specified RRSet and associated information
134 // FIXME: OMG, there are a ton of asserts in here...
135 #[allow(clippy::too_many_arguments)]
136 fn new<'a>(
137 name: &Name,
138 dns_class: DNSClass,
139 num_labels: u8,
140 type_covered: RecordType,
141 algorithm: Algorithm,
142 original_ttl: u32,
143 sig_expiration: SerialNumber,
144 sig_inception: SerialNumber,
145 key_tag: u16,
146 signer_name: &Name,
147 records: impl Iterator<Item = &'a Record>,
148 ) -> ProtoResult<Self> {
149 // TODO: change this to a BTreeSet so that it's preordered, no sort necessary
150 let mut rrset: Vec<&Record> = Vec::new();
151
152 // collect only the records for this rrset
153 for record in records {
154 if dns_class == record.dns_class()
155 && type_covered == record.record_type()
156 && name == record.name()
157 {
158 rrset.push(record);
159 }
160 }
161
162 // put records in canonical order
163 rrset.sort();
164
165 let name = determine_name(name, num_labels)?;
166
167 // TODO: rather than buffering here, use the Signer/Verifier? might mean fewer allocations...
168 let mut buf: Vec<u8> = Vec::new();
169
170 {
171 let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut buf);
172 encoder.set_canonical_names(true);
173
174 // signed_data = RRSIG_RDATA | RR(1) | RR(2)... where
175 //
176 // "|" denotes concatenation
177 //
178 // RRSIG_RDATA is the wire format of the RRSIG RDATA fields
179 // with the Signature field excluded and the Signer's Name
180 // in canonical form.
181 assert!(
182 sig::emit_pre_sig(
183 &mut encoder,
184 type_covered,
185 algorithm,
186 name.num_labels(),
187 original_ttl,
188 sig_expiration,
189 sig_inception,
190 key_tag,
191 signer_name,
192 )
193 .is_ok()
194 );
195
196 // construct the rrset signing data
197 for record in rrset {
198 // RR(i) = name | type | class | OrigTTL | RDATA length | RDATA
199 //
200 // name is calculated according to the function in the RFC 4035
201 assert!(
202 name.to_lowercase()
203 .emit_as_canonical(&mut encoder, true)
204 .is_ok()
205 );
206 //
207 // type is the RRset type and all RRs in the class
208 assert!(type_covered.emit(&mut encoder).is_ok());
209 //
210 // class is the RRset's class
211 assert!(dns_class.emit(&mut encoder).is_ok());
212 //
213 // OrigTTL is the value from the RRSIG Original TTL field
214 assert!(encoder.emit_u32(original_ttl).is_ok());
215 //
216 // RDATA length
217 // TODO: add support to the encoder to set a marker to go back and write the length
218 let mut rdata_buf = Vec::new();
219 {
220 let mut rdata_encoder = BinEncoder::new(&mut rdata_buf);
221 rdata_encoder.set_canonical_names(true);
222 assert!(record.data().emit(&mut rdata_encoder).is_ok());
223 }
224 assert!(encoder.emit_u16(rdata_buf.len() as u16).is_ok());
225 //
226 // All names in the RDATA field are in canonical form (set above)
227 assert!(encoder.emit_vec(&rdata_buf).is_ok());
228 }
229 }
230
231 Ok(Self(buf))
232 }
233}
234
235impl<'a> From<&'a [u8]> for TBS {
236 fn from(slice: &'a [u8]) -> Self {
237 Self(slice.to_owned())
238 }
239}
240
241impl AsRef<[u8]> for TBS {
242 fn as_ref(&self) -> &[u8] {
243 self.0.as_ref()
244 }
245}
246
247/// Returns the to-be-signed serialization of the given message.
248pub fn message_tbs<M: BinEncodable>(message: &M, pre_sig0: &SIG) -> ProtoResult<TBS> {
249 // TODO: should perform the serialization and sign block by block to reduce the max memory
250 // usage, though at 4k max, this is probably unnecessary... For AXFR and large zones, it's
251 // more important
252 let mut buf: Vec<u8> = Vec::with_capacity(512);
253 let mut buf2: Vec<u8> = Vec::with_capacity(512);
254
255 {
256 let mut encoder: BinEncoder<'_> = BinEncoder::with_mode(&mut buf, EncodeMode::Normal);
257 assert!(
258 sig::emit_pre_sig(
259 &mut encoder,
260 pre_sig0.type_covered(),
261 pre_sig0.algorithm(),
262 pre_sig0.num_labels(),
263 pre_sig0.original_ttl(),
264 pre_sig0.sig_expiration(),
265 pre_sig0.sig_inception(),
266 pre_sig0.key_tag(),
267 pre_sig0.signer_name(),
268 )
269 .is_ok()
270 );
271 // need a separate encoder here, as the encoding references absolute positions
272 // inside the buffer. If the buffer already contains the sig0 RDATA, offsets
273 // are wrong and the signature won't match.
274 let mut encoder2: BinEncoder<'_> = BinEncoder::with_mode(&mut buf2, EncodeMode::Signing);
275 message.emit(&mut encoder2).unwrap(); // coding error if this panics (i think?)
276 }
277
278 buf.append(&mut buf2);
279
280 Ok(TBS(buf))
281}
282
283/// [RFC 4035](https://tools.ietf.org/html/rfc4035), DNSSEC Protocol Modifications, March 2005
284///
285/// ```text
286///
287/// 5.3.2. Reconstructing the Signed Data
288/// ...
289/// To calculate the name:
290/// let rrsig_labels = the value of the RRSIG Labels field
291///
292/// let fqdn = RRset's fully qualified domain name in
293/// canonical form
294///
295/// let fqdn_labels = Label count of the fqdn above.
296///
297/// if rrsig_labels = fqdn_labels,
298/// name = fqdn
299///
300/// if rrsig_labels < fqdn_labels,
301/// name = "*." | the rightmost rrsig_label labels of the
302/// fqdn
303///
304/// if rrsig_labels > fqdn_labels
305/// the RRSIG RR did not pass the necessary validation
306/// checks and MUST NOT be used to authenticate this
307/// RRset.
308///
309/// The canonical forms for names and RRsets are defined in [RFC4034].
310/// ```
311pub fn determine_name(name: &Name, num_labels: u8) -> Result<Name, ProtoError> {
312 // To calculate the name:
313 // let rrsig_labels = the value of the RRSIG Labels field
314 //
315 // let fqdn = RRset's fully qualified domain name in
316 // canonical form
317 //
318 // let fqdn_labels = Label count of the fqdn above.
319 let fqdn_labels = name.num_labels();
320 // if rrsig_labels = fqdn_labels,
321 // name = fqdn
322
323 if fqdn_labels == num_labels {
324 return Ok(name.clone());
325 }
326 // if rrsig_labels < fqdn_labels,
327 // name = "*." | the rightmost rrsig_label labels of the
328 // fqdn
329 if num_labels < fqdn_labels {
330 let mut star_name: Name = Name::from_labels(vec![b"*" as &[u8]]).unwrap();
331 let rightmost = name.trim_to(num_labels as usize);
332 if !rightmost.is_root() {
333 star_name = star_name.append_name(&rightmost)?;
334 return Ok(star_name);
335 }
336 return Ok(star_name);
337 }
338 //
339 // if rrsig_labels > fqdn_labels
340 // the RRSIG RR did not pass the necessary validation
341 // checks and MUST NOT be used to authenticate this
342 // RRset.
343
344 Err(format!("could not determine name from {name}").into())
345}