domain/rdata/
zonemd.rs

1//! ZONEMD record data.
2//!
3//! The ZONEMD Resource Record conveys the digest data in the zone itself.
4//!
5//! [RFC 8976]: https://tools.ietf.org/html/rfc8976
6
7use crate::base::cmp::CanonicalOrd;
8use crate::base::iana::Rtype;
9use crate::base::rdata::{ComposeRecordData, RecordData};
10use crate::base::scan::{Scan, Scanner};
11use crate::base::serial::Serial;
12use crate::base::wire::{Composer, ParseError};
13use crate::utils::base16;
14use core::cmp::Ordering;
15use core::{fmt, hash};
16use octseq::octets::{Octets, OctetsFrom, OctetsInto};
17use octseq::parse::Parser;
18
19// section 2.2.4
20const DIGEST_MIN_LEN: usize = 12;
21
22/// The ZONEMD Resource Record conveys the digest data in the zone itself.
23#[derive(Clone)]
24#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
25pub struct Zonemd<Octs: ?Sized> {
26    serial: Serial,
27    scheme: Scheme,
28    algo: Algorithm,
29    #[cfg_attr(
30        feature = "serde",
31        serde(
32            serialize_with = "octseq::serde::SerializeOctets::serialize_octets",
33            deserialize_with = "octseq::serde::DeserializeOctets::deserialize_octets",
34            bound(
35                serialize = "Octs: octseq::serde::SerializeOctets",
36                deserialize = "Octs: octseq::serde::DeserializeOctets<'de>",
37            )
38        )
39    )]
40    digest: Octs,
41}
42
43impl<Octs> Zonemd<Octs> {
44    /// Create a Zonemd record data from provided parameters.
45    pub fn new(
46        serial: Serial,
47        scheme: Scheme,
48        algo: Algorithm,
49        digest: Octs,
50    ) -> Self {
51        Self {
52            serial,
53            scheme,
54            algo,
55            digest,
56        }
57    }
58
59    /// Get the serial field.
60    pub fn serial(&self) -> Serial {
61        self.serial
62    }
63
64    /// Get the scheme field.
65    pub fn scheme(&self) -> Scheme {
66        self.scheme
67    }
68
69    /// Get the hash algorithm field.
70    pub fn algorithm(&self) -> Algorithm {
71        self.algo
72    }
73
74    /// Get the digest field.
75    pub fn digest(&self) -> &Octs {
76        &self.digest
77    }
78
79    /// Parse the record data from wire format.
80    pub fn parse<'a, Src: Octets<Range<'a> = Octs> + ?Sized>(
81        parser: &mut Parser<'a, Src>,
82    ) -> Result<Self, ParseError> {
83        let serial = Serial::parse(parser)?;
84        let scheme = parser.parse_u8()?.into();
85        let algo = parser.parse_u8()?.into();
86        let len = parser.remaining();
87        if len < DIGEST_MIN_LEN {
88            return Err(ParseError::ShortInput);
89        }
90        let digest = parser.parse_octets(len)?;
91        Ok(Self {
92            serial,
93            scheme,
94            algo,
95            digest,
96        })
97    }
98
99    /// Parse the record data from zonefile format.
100    pub fn scan<S: Scanner<Octets = Octs>>(
101        scanner: &mut S,
102    ) -> Result<Self, S::Error> {
103        let serial = Serial::scan(scanner)?;
104        let scheme = u8::scan(scanner)?.into();
105        let algo = u8::scan(scanner)?.into();
106        let digest = scanner.convert_entry(base16::SymbolConverter::new())?;
107
108        Ok(Self {
109            serial,
110            scheme,
111            algo,
112            digest,
113        })
114    }
115
116    pub(super) fn flatten<Target: OctetsFrom<Octs>>(
117        self
118    ) -> Result<Zonemd<Target>, Target::Error> {
119        self.convert_octets()
120    }
121
122    pub(super) fn convert_octets<Target: OctetsFrom<Octs>>(
123        self,
124    ) -> Result<Zonemd<Target>, Target::Error> {
125        let Zonemd {
126            serial,
127            scheme,
128            algo,
129            digest,
130        } = self;
131
132        Ok(Zonemd {
133            serial,
134            scheme,
135            algo,
136            digest: digest.try_octets_into()?,
137        })
138    }
139}
140
141impl<Octs> RecordData for Zonemd<Octs> {
142    fn rtype(&self) -> Rtype {
143        Rtype::Zonemd
144    }
145}
146
147impl<Octs: AsRef<[u8]>> ComposeRecordData for Zonemd<Octs> {
148    fn rdlen(&self, _compress: bool) -> Option<u16> {
149        Some(
150            // serial + scheme + algorithm + digest_len
151            u16::try_from(4 + 1 + 1 + self.digest.as_ref().len())
152                .expect("long ZONEMD rdata"),
153        )
154    }
155
156    fn compose_rdata<Target: Composer + ?Sized>(
157        &self,
158        target: &mut Target,
159    ) -> Result<(), Target::AppendError> {
160        target.append_slice(&self.serial.into_int().to_be_bytes())?;
161        target.append_slice(&[self.scheme.into()])?;
162        target.append_slice(&[self.algo.into()])?;
163        target.append_slice(self.digest.as_ref())
164    }
165
166    fn compose_canonical_rdata<Target: Composer + ?Sized>(
167        &self,
168        target: &mut Target,
169    ) -> Result<(), Target::AppendError> {
170        self.compose_rdata(target)
171    }
172}
173
174impl<Octs: AsRef<[u8]>> hash::Hash for Zonemd<Octs> {
175    fn hash<H: hash::Hasher>(&self, state: &mut H) {
176        self.serial.hash(state);
177        self.scheme.hash(state);
178        self.algo.hash(state);
179        self.digest.as_ref().hash(state);
180    }
181}
182
183impl<Octs, Other> PartialEq<Zonemd<Other>> for Zonemd<Octs>
184where
185    Octs: AsRef<[u8]> + ?Sized,
186    Other: AsRef<[u8]> + ?Sized,
187{
188    fn eq(&self, other: &Zonemd<Other>) -> bool {
189        self.serial.eq(&other.serial)
190            && self.scheme.eq(&other.scheme)
191            && self.algo.eq(&other.algo)
192            && self.digest.as_ref().eq(other.digest.as_ref())
193    }
194}
195
196impl<Octs: AsRef<[u8]> + ?Sized> Eq for Zonemd<Octs> {}
197
198// section 2.4
199impl<Octs: AsRef<[u8]>> fmt::Display for Zonemd<Octs> {
200    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
201        write!(
202            f,
203            "{} {} {} ( ",
204            self.serial,
205            u8::from(self.scheme),
206            u8::from(self.algo)
207        )?;
208        base16::display(&self.digest, f)?;
209        write!(f, " )")
210    }
211}
212
213impl<Octs: AsRef<[u8]>> fmt::Debug for Zonemd<Octs> {
214    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
215        f.write_str("Zonemd(")?;
216        fmt::Display::fmt(self, f)?;
217        f.write_str(")")
218    }
219}
220
221impl<Octs, Other> PartialOrd<Zonemd<Other>> for Zonemd<Octs>
222where
223    Octs: AsRef<[u8]>,
224    Other: AsRef<[u8]>,
225{
226    fn partial_cmp(&self, other: &Zonemd<Other>) -> Option<Ordering> {
227        match self.serial.partial_cmp(&other.serial) {
228            Some(Ordering::Equal) => {}
229            other => return other,
230        }
231        match self.scheme.partial_cmp(&other.scheme) {
232            Some(Ordering::Equal) => {}
233            other => return other,
234        }
235        match self.algo.partial_cmp(&other.algo) {
236            Some(Ordering::Equal) => {}
237            other => return other,
238        }
239        self.digest.as_ref().partial_cmp(other.digest.as_ref())
240    }
241}
242
243impl<Octs, Other> CanonicalOrd<Zonemd<Other>> for Zonemd<Octs>
244where
245    Octs: AsRef<[u8]>,
246    Other: AsRef<[u8]>,
247{
248    fn canonical_cmp(&self, other: &Zonemd<Other>) -> Ordering {
249        match self.serial.into_int().cmp(&other.serial.into_int()) {
250            Ordering::Equal => {}
251            other => return other,
252        }
253        match self.scheme.cmp(&other.scheme) {
254            Ordering::Equal => {}
255            other => return other,
256        }
257        match self.algo.cmp(&other.algo) {
258            Ordering::Equal => {}
259            other => return other,
260        }
261        self.digest.as_ref().cmp(other.digest.as_ref())
262    }
263}
264
265impl<Octs: AsRef<[u8]>> Ord for Zonemd<Octs> {
266    fn cmp(&self, other: &Self) -> Ordering {
267        match self.serial.into_int().cmp(&other.serial.into_int()) {
268            Ordering::Equal => {}
269            other => return other,
270        }
271        match self.scheme.cmp(&other.scheme) {
272            Ordering::Equal => {}
273            other => return other,
274        }
275        match self.algo.cmp(&other.algo) {
276            Ordering::Equal => {}
277            other => return other,
278        }
279        self.digest.as_ref().cmp(other.digest.as_ref())
280    }
281}
282
283/// The data collation scheme.
284///
285/// This enumeration wraps an 8-bit unsigned integer that identifies the
286/// methods by which data is collated and presented as input to the
287/// hashing function.
288#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
289#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
290pub enum Scheme {
291    Reserved,
292    Simple,
293    Unassigned(u8),
294    Private(u8),
295}
296
297impl From<Scheme> for u8 {
298    fn from(s: Scheme) -> u8 {
299        match s {
300            Scheme::Reserved => 0,
301            Scheme::Simple => 1,
302            Scheme::Unassigned(n) => n,
303            Scheme::Private(n) => n,
304        }
305    }
306}
307
308impl From<u8> for Scheme {
309    fn from(n: u8) -> Self {
310        match n {
311            0 | 255 => Self::Reserved,
312            1 => Self::Simple,
313            2..=239 => Self::Unassigned(n),
314            240..=254 => Self::Private(n),
315        }
316    }
317}
318
319/// The Hash Algorithm used to construct the digest.
320///
321/// This enumeration wraps an 8-bit unsigned integer that identifies
322/// the cryptographic hash algorithm.
323#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
324#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
325pub enum Algorithm {
326    Reserved,
327    Sha384,
328    Sha512,
329    Unassigned(u8),
330    Private(u8),
331}
332
333impl From<Algorithm> for u8 {
334    fn from(algo: Algorithm) -> u8 {
335        match algo {
336            Algorithm::Reserved => 0,
337            Algorithm::Sha384 => 1,
338            Algorithm::Sha512 => 2,
339            Algorithm::Unassigned(n) => n,
340            Algorithm::Private(n) => n,
341        }
342    }
343}
344
345impl From<u8> for Algorithm {
346    fn from(n: u8) -> Self {
347        match n {
348            0 | 255 => Self::Reserved,
349            1 => Self::Sha384,
350            2 => Self::Sha512,
351            3..=239 => Self::Unassigned(n),
352            240..=254 => Self::Private(n),
353        }
354    }
355}
356
357#[cfg(test)]
358#[cfg(all(feature = "std", feature = "bytes"))]
359mod test {
360    use super::*;
361    use crate::base::rdata::test::{
362        test_compose_parse, test_rdlen, test_scan,
363    };
364    use crate::base::Dname;
365    use crate::rdata::ZoneRecordData;
366    use crate::utils::base16::decode;
367    use std::string::ToString;
368    use std::vec::Vec;
369
370    #[test]
371    #[allow(clippy::redundant_closure)] // lifetimes ...
372    fn zonemd_compose_parse_scan() {
373        let serial = 2023092203;
374        let scheme = 1.into();
375        let algo = 241.into();
376        let digest_str = "CDBE0DED9484490493580583BF868A3E95F89FC3515BF26ADBD230A6C23987F36BC6E504EFC83606F9445476D4E57FFB";
377        let digest: Vec<u8> = decode(digest_str).unwrap();
378        let rdata = Zonemd::new(serial.into(), scheme, algo, digest);
379        test_rdlen(&rdata);
380        test_compose_parse(&rdata, |parser| Zonemd::parse(parser));
381        test_scan(
382            &[
383                &serial.to_string(),
384                &u8::from(scheme).to_string(),
385                &u8::from(algo).to_string(),
386                digest_str,
387            ],
388            Zonemd::scan,
389            &rdata,
390        );
391    }
392
393    #[cfg(feature = "zonefile")]
394    #[test]
395    fn zonemd_parse_zonefile() {
396        use crate::zonefile::inplace::{Entry, Zonefile};
397
398        // section A.1
399        let content = r#"
400example.      86400  IN  SOA     ns1 admin 2018031900 (
401                                 1800 900 604800 86400 )
402              86400  IN  NS      ns1
403              86400  IN  NS      ns2
404              86400  IN  ZONEMD  2018031900 1 1 (
405                                 c68090d90a7aed71
406                                 6bc459f9340e3d7c
407                                 1370d4d24b7e2fc3
408                                 a1ddc0b9a87153b9
409                                 a9713b3c9ae5cc27
410                                 777f98b8e730044c )
411ns1           3600   IN  A       203.0.113.63
412ns2           3600   IN  AAAA    2001:db8::63
413"#;
414
415        let mut zone = Zonefile::load(&mut content.as_bytes()).unwrap();
416        zone.set_origin(Dname::root());
417        while let Some(entry) = zone.next_entry().unwrap() {
418            match entry {
419                Entry::Record(record) => {
420                    if record.rtype() != Rtype::Zonemd {
421                        continue;
422                    }
423                    match record.into_data() {
424                        ZoneRecordData::Zonemd(rd) => {
425                            assert_eq!(2018031900, rd.serial().into_int());
426                            assert_eq!(Scheme::Simple, rd.scheme());
427                            assert_eq!(Algorithm::Sha384, rd.algorithm());
428                        }
429                        _ => panic!(),
430                    }
431                }
432                _ => panic!(),
433            }
434        }
435    }
436}