Skip to main content

dnsbox/rdata/
rrsig.rs

1//! RRSIG (RFC 4034 §3) and SIG (RFC 2535 §4.1, RFC 2931 §3) record data,
2//! which share one wire format.
3
4use core::fmt;
5
6use super::{ComposeRdata, ParseRdata, ParseRdataText};
7use crate::dnssec::{Algorithm, Timestamp};
8use crate::name::Name;
9use crate::text::Base64;
10use crate::wire::{Composer, NameEncoding, OutBuf, WireReader};
11use crate::zone::Scanner;
12use crate::{Result, Rtype};
13
14/// Defines an RRSIG-shaped record-data view.
15macro_rules! rrsig_like {
16    ($(#[$doc:meta])* $ty:ident, $rt:ident, $read:ident) => {
17        $(#[$doc])*
18        #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
19        pub struct $ty<'a> {
20            /// The type of the RRset covered by the signature (RFC 4034 §3.1.1).
21            pub type_covered: Rtype,
22            /// The signing algorithm (RFC 4034 §3.1.2).
23            pub algorithm: Algorithm,
24            /// The number of labels in the original owner name, not counting
25            /// the root or a leading wildcard label (RFC 4034 §3.1.3).
26            pub labels: u8,
27            /// The TTL of the covered RRset in the zone (RFC 4034 §3.1.4).
28            pub original_ttl: u32,
29            /// End of the validity period, seconds since 1970 modulo 2^32
30            /// (RFC 4034 §3.1.5).
31            pub expiration: u32,
32            /// Start of the validity period, seconds since 1970 modulo 2^32
33            /// (RFC 4034 §3.1.5).
34            pub inception: u32,
35            /// Key tag of the signing DNSKEY (RFC 4034 §3.1.6).
36            pub key_tag: u16,
37            /// The owner name of the signing DNSKEY: the zone apex
38            /// (RFC 4034 §3.1.7).
39            pub signer_name: Name<'a>,
40            /// The signature (RFC 4034 §3.1.8).
41            pub signature: &'a [u8],
42        }
43
44        impl ParseRdataText for $ty<'_> {
45            /// `<type covered> <algorithm> <labels> <original TTL>
46            /// <expiration> <inception> <key tag> <signer's name>
47            /// <signature>` (RFC 4034 §3.2, RFC 2535 §7.2): the type as a
48            /// mnemonic (or `TYPEnnn`), the algorithm as a number or a
49            /// mnemonic, the original TTL with optional units (as BIND
50            /// accepts), the times as `YYYYMMDDHHmmSS` or seconds since
51            /// the epoch, the signature in base64, possibly split into
52            /// several tokens.
53            fn parse_text<B: OutBuf + ?Sized>(s: &mut Scanner<'_>, out: &mut B) -> Result<()> {
54                out.put_u16(s.parse::<Rtype>()?.get())?;
55                out.put_u8(s.parse::<Algorithm>()?.get())?;
56                out.put_u8(s.u8()?)?;
57                out.put_u32(s.ttl()?)?;
58                out.put_u32(s.timestamp()?)?;
59                out.put_u32(s.timestamp()?)?;
60                out.put_u16(s.u16()?)?;
61                s.name_into(out, NameEncoding::Lowercase)?;
62                s.base64_rest_into(out)?;
63                Ok(())
64            }
65        }
66
67        impl<'a> ParseRdata<'a> for $ty<'a> {
68            const RTYPE: Rtype = Rtype::$rt;
69
70            fn parse_rdata(rdata: &mut WireReader<'a>) -> Result<Self> {
71                Ok($ty {
72                    type_covered: Rtype::new(rdata.read_u16()?),
73                    algorithm: Algorithm::new(rdata.read_u8()?),
74                    labels: rdata.read_u8()?,
75                    original_ttl: rdata.read_u32()?,
76                    expiration: rdata.read_u32()?,
77                    inception: rdata.read_u32()?,
78                    key_tag: rdata.read_u16()?,
79                    signer_name: rdata.$read()?,
80                    signature: rdata.read_rest(),
81                })
82            }
83        }
84
85        impl<'a> $ty<'a> {
86            /// Writes the RDATA without the signature field — the prefix of
87            /// the signed data (RFC 4034 §3.1.8.1, RFC 2931 §3.1). Wrap `c`
88            /// in [`Canonical`](crate::wire::Canonical) to lowercase the
89            /// signer's name as the signature requires.
90            pub fn compose_unsigned<C: Composer + ?Sized>(&self, c: &mut C) -> Result<()> {
91                c.put_u16(self.type_covered.get())?;
92                c.put_u8(self.algorithm.get())?;
93                c.put_u8(self.labels)?;
94                c.put_u32(self.original_ttl)?;
95                c.put_u32(self.expiration)?;
96                c.put_u32(self.inception)?;
97                c.put_u16(self.key_tag)?;
98                // Never compressed (RFC 4034 §3.1.7), lowercased in canonical
99                // form (RFC 4034 §6.2, RFC 6840 §5.1).
100                c.put_name(self.signer_name, NameEncoding::Lowercase)
101            }
102
103            /// The same record data with another signature (e.g. a template
104            /// from [`ZoneKey::rrsig_template`](crate::dnssec::ZoneKey::rrsig_template)
105            /// completed after signing).
106            #[inline]
107            #[must_use]
108            pub const fn with_signature<'s>(&self, signature: &'s [u8]) -> $ty<'s>
109            where
110                'a: 's,
111            {
112                $ty {
113                    type_covered: self.type_covered,
114                    algorithm: self.algorithm,
115                    labels: self.labels,
116                    original_ttl: self.original_ttl,
117                    expiration: self.expiration,
118                    inception: self.inception,
119                    key_tag: self.key_tag,
120                    signer_name: self.signer_name,
121                    signature,
122                }
123            }
124        }
125
126        impl ComposeRdata for $ty<'_> {
127            fn rtype(&self) -> Rtype {
128                Rtype::$rt
129            }
130
131            fn compose_rdata<C: Composer + ?Sized>(&self, c: &mut C) -> Result<()> {
132                self.compose_unsigned(c)?;
133                c.put_bytes(self.signature)
134            }
135        }
136
137        impl fmt::Display for $ty<'_> {
138            /// `type algorithm labels ttl expiration inception key-tag
139            /// signer signature` (RFC 4034 §3.2), with `YYYYMMDDHHmmSS`
140            /// times and a numeric algorithm.
141            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142                write!(
143                    f,
144                    "{} {} {} {} {} {} {} {}",
145                    self.type_covered,
146                    self.algorithm.get(),
147                    self.labels,
148                    self.original_ttl,
149                    Timestamp::new(self.expiration),
150                    Timestamp::new(self.inception),
151                    self.key_tag,
152                    self.signer_name,
153                )?;
154                if !self.signature.is_empty() {
155                    write!(f, " {}", Base64(self.signature))?;
156                }
157                Ok(())
158            }
159        }
160    };
161}
162
163rrsig_like! {
164    /// `RRSIG` record data: a DNSSEC signature over an RRset (RFC 4034 §3).
165    ///
166    /// The signer's name is never compressed (RFC 4034 §3.1.7). See
167    /// [`crate::dnssec`] for signed-data construction and validation.
168    Rrsig, RRSIG, read_name_uncompressed
169}
170
171rrsig_like! {
172    /// `SIG` record data: the RFC 2535 signature, used today for SIG(0)
173    /// transaction signatures (RFC 2931). Its signer's name may be
174    /// compressed on receipt (RFC 3597 §4).
175    Sig, SIG, read_name
176}
177
178impl<'a> From<Rrsig<'a>> for Sig<'a> {
179    fn from(s: Rrsig<'a>) -> Self {
180        let Rrsig {
181            type_covered,
182            algorithm,
183            labels,
184            original_ttl,
185            expiration,
186            inception,
187            key_tag,
188            signer_name,
189            signature,
190        } = s;
191        Sig {
192            type_covered,
193            algorithm,
194            labels,
195            original_ttl,
196            expiration,
197            inception,
198            key_tag,
199            signer_name,
200            signature,
201        }
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208    use crate::rdata::RData;
209    use crate::rdata::tests::{parse, round_trip, text_error, text_parse, text_round_trip};
210    use crate::testutil::hex;
211    use crate::wire::{Canonical, WireWriter};
212    use crate::{Class, Error};
213    use std::string::ToString;
214
215    /// RFC 4034 §3.3: the example RRSIG over host.example.com. A.
216    pub(crate) fn rfc4034_rrsig() -> std::vec::Vec<u8> {
217        let mut w = hex("0001 05 03 00015180 3e7c9dd7 3e5510d7 0a52");
218        w.extend(b"\x07example\x03com\x00");
219        w.extend(hex(
220            "a090755ba58d1affa576f4375831b4310920e481218d18a9f164eb3d81afd3b8
221             75d3c75428631e0cf2a28d50875f70c329d7dbfafea807dc1fba1dc34c95d401
222             f23f334ce63bfcf3f1b5b44739e5f0eded18d6b33f040a911376d173d757a9f0
223             c1fa1798941bb0b36b2df9062790fa7f0166f2737eea907378341fb12dc0a77a",
224        ));
225        w
226    }
227
228    #[test]
229    fn rfc4034_example() {
230        let wire = rfc4034_rrsig();
231        let text = round_trip(
232            Rtype::RRSIG,
233            &wire,
234            "A 5 3 86400 20030322173103 20030220173103 2642 example.com. \
235             oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6o\
236             B9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkG\
237             J5D6fwFm8nN+6pBzeDQfsS3Ap3o=",
238        );
239        round_trip(Rtype::SIG, &wire, &text);
240        let RData::Rrsig(r) = parse(Rtype::RRSIG, Class::IN, &wire).unwrap() else {
241            panic!()
242        };
243        assert_eq!(r.type_covered, Rtype::A);
244        assert_eq!(r.algorithm, Algorithm::RSASHA1);
245        assert_eq!((r.labels, r.original_ttl, r.key_tag), (3, 86400, 2642));
246        let sig: Sig<'_> = r.into();
247        assert_eq!(sig.signature, r.signature);
248
249        // compose_unsigned + Canonical lowercases the signer's name.
250        let upper = crate::NameBuf::from_text(b"EXAMPLE.com").unwrap();
251        let r2 = Rrsig {
252            signer_name: upper.as_name(),
253            ..r
254        };
255        let mut buf = [0u8; 64];
256        let mut w = WireWriter::new(&mut buf);
257        r2.compose_unsigned(&mut Canonical::new(&mut w)).unwrap();
258        assert_eq!(w.as_bytes(), &wire[..31]);
259        let empty = r.with_signature(&[]);
260        assert_eq!(empty.signature, b"");
261        assert!(empty.to_string().ends_with(" example.com."));
262    }
263
264    /// RFC 4034 §3.3's RRSIG in the presentation form `Display` gives.
265    const RFC4034_RRSIG_TEXT: &str = "A 5 3 86400 20030322173103 20030220173103 2642 example.com. \
266        oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6o\
267        B9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3tGNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkG\
268        J5D6fwFm8nN+6pBzeDQfsS3Ap3o=";
269
270    #[test]
271    fn text() {
272        let wire = rfc4034_rrsig();
273        // RFC 4034 §3.3, as printed there.
274        let rfc = "A 5 3 86400 20030322173103 (\n\
275                   20030220173103 2642 example.com.\n\
276                   oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTr\n\
277                   PYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6o\n\
278                   B9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3t\n\
279                   GNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkG\n\
280                   J5D6fwFm8nN+6pBzeDQfsS3Ap3o= )";
281        for t in [Rtype::RRSIG, Rtype::SIG] {
282            text_round_trip(t, rfc, &wire, RFC4034_RRSIG_TEXT);
283            // The generic type, an algorithm mnemonic, a TTL with units,
284            // times in seconds (RFC 4034 §3.2), a relative signer name
285            // (origin `example.`) and a different base64 split.
286            let alt = "TYPE1 RSASHA1 3 1D 1048354263 1045762263 2642 example.com \
287                oJB1W6WNGv+ldvQ3WDG0MQkg5IEhjRip8WTrPYGv07h108dUKGMeDPKijVCHX3DDKdfb+v6o\
288                B9wfuh3DTJXUAfI/M0zmO/zz8bW0Rznl8O3t GNazPwQKkRN20XPXV6nwwfoXmJQbsLNrLfkG\
289                J5D6fwFm8nN+6pBzeDQfsS3Ap3o=";
290            let mut want = wire.clone();
291            want.splice(18..31, b"\x07example\x03com\x07example\x00".iter().copied());
292            assert_eq!(text_parse(t, alt).as_deref(), Ok(&want[..]));
293        }
294        // The SIG(0) capture of `sig0_capture_round_trip`: type 0, labels
295        // and TTL 0.
296        text_round_trip(Rtype::SIG, SIG0_TEXT, &hex(ED25519_SIG0), SIG0_TEXT);
297        // No signature (a template), a timestamp past 2038.
298        let mut tmpl = hex("0030 0f 02 00000e10 ffffffff 00000000 0001");
299        tmpl.push(0);
300        text_round_trip(
301            Rtype::RRSIG,
302            "DNSKEY ED25519 2 1h 21060207062815 19700101000000 1 .",
303            &tmpl,
304            "DNSKEY 15 2 3600 21060207062815 19700101000000 1 .",
305        );
306    }
307
308    #[test]
309    fn text_malformed() {
310        for t in [Rtype::RRSIG, Rtype::SIG] {
311            let base = "A 5 3 86400 20030322173103 20030220173103 2642 example.com.";
312            assert!(text_parse(t, base).is_ok());
313            let short = "A 5 3 86400 20030322173103 20030220173103 2642";
314            assert_eq!(text_error(t, short), Error::UnexpectedEof);
315            assert_eq!(text_error(t, "A 5"), Error::UnexpectedEof);
316            for (from, to, err) in [
317                ("A ", "NOSUCHTYPE ", Error::UnknownMnemonic),
318                ("A ", "TYPE65536 ", Error::InvalidText),
319                (" 5 ", " 256 ", Error::InvalidText),
320                (" 3 ", " 256 ", Error::InvalidText),
321                (" 86400 ", " 1x ", Error::InvalidText),
322                (" 86400 ", " 4294967296 ", Error::InvalidText),
323                // Not a date (month 13), 13 and 15 digits (too large a
324                // number), a sign.
325                (" 20030322173103 ", " 20031322173103 ", Error::InvalidText),
326                (" 20030322173103 ", " 2003032217310 ", Error::InvalidText),
327                (" 20030220173103 ", " 200302201731030 ", Error::InvalidText),
328                (" 20030220173103 ", " +1 ", Error::InvalidText),
329                (" 2642 ", " 65536 ", Error::InvalidText),
330                (" example.com.", " example..com.", Error::EmptyLabel),
331            ] {
332                let bad = base.replacen(from, to, 1);
333                assert_eq!(text_error(t, &bad), err, "{bad}");
334            }
335            assert_eq!(text_error(t, &std::format!("{base} AQ*=")), Error::InvalidText);
336            assert_eq!(text_error(t, &std::format!("{base} AQ")), Error::InvalidText);
337        }
338    }
339
340    #[test]
341    fn names() {
342        // RRSIG's signer name must not be compressed; SIG's may be.
343        let mut wire = hex("0001 05 03 00015180 3e7c9dd7 3e5510d7 0a52");
344        wire.extend(b"\xc0\x00\x01");
345        assert_eq!(
346            parse(Rtype::RRSIG, Class::IN, &wire),
347            Err(Error::UnexpectedPointer)
348        );
349        // A backward pointer (here to the zero octet at offset 0, the root).
350        let RData::Sig(sig) = parse(Rtype::SIG, Class::IN, &wire).unwrap() else {
351            panic!()
352        };
353        assert!(sig.signer_name.is_root());
354        // A forward pointer.
355        wire[19] = 0x7f;
356        assert_eq!(parse(Rtype::SIG, Class::IN, &wire), Err(Error::BadPointer));
357        assert_eq!(
358            parse(Rtype::RRSIG, Class::IN, &wire[..17]),
359            Err(Error::UnexpectedEof)
360        );
361    }
362    // SIG(0) RDATA from an `nsupdate -k` (Ed25519) request captured from
363    // BIND 9.18 (tests/data/named/sig0-ed25519.query.bin).
364    const ED25519_SIG0: &str = "00000f00000000006ac216736ac2141ba898\
365        0473696730076578616d706c6503636f6d00\
366        a613d9eefcc566c0cd1d342d1a536cc338588d6b6fe01b120608a67f4b3b50b9\
367        56b380b1ddfd71202b83cac241ae907342407af499359896fc6c8bfa6bad9c05";
368
369    const SIG0_TEXT: &str = "TYPE0 15 0 0 20261004090347 20261004085347 43160 sig0.example.com. \
370        phPZ7vzFZsDNHTQtGlNswzhYjWtv4BsSBgimf0s7ULlWs4Cx3f1xICuDysJBrpBzQkB69Jk1mJb8bIv6a62cBQ==";
371
372    #[test]
373    fn sig0_capture_round_trip() {
374        let wire = hex(ED25519_SIG0);
375        round_trip(Rtype::SIG, &wire, SIG0_TEXT);
376    }
377
378    #[test]
379    fn sig0_unsigned_fields_are_canonical() {
380        // RFC 2931 §3.1: the signed SIG RDATA has the signer name
381        // uncompressed and lowercased.
382        let signer = crate::NameBuf::from_text(b"SIG0.Example.COM").unwrap();
383        let sig = Sig {
384            type_covered: Rtype::new(0),
385            algorithm: Algorithm::ED25519,
386            labels: 0,
387            original_ttl: 0,
388            expiration: 2,
389            inception: 1,
390            key_tag: 7,
391            signer_name: signer.as_name(),
392            signature: &[1, 2, 3],
393        };
394        let mut buf = [0u8; 64];
395        let mut w = WireWriter::new(&mut buf);
396        sig.compose_unsigned(&mut Canonical::new(&mut w)).unwrap();
397        assert_eq!(
398            w.as_bytes(),
399            b"\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x07\
400              \x04sig0\x07example\x03com\x00"
401        );
402    }
403
404    #[test]
405    fn sig_minimal() {
406        assert_eq!(
407            parse(Rtype::SIG, Class::IN, &[0; 17]),
408            Err(Error::UnexpectedEof)
409        );
410        // Minimal: root signer, empty signature.
411        let d = parse(Rtype::SIG, Class::IN, &[0; 19]).unwrap();
412        assert!(matches!(d, RData::Sig(s) if s.signature.is_empty()));
413    }
414}