Skip to main content

dnsbox/rdata/
dhcid.rs

1//! DHCID record data (RFC 4701).
2
3use core::fmt;
4
5use super::{ComposeRdata, ParseRdata, ParseRdataText};
6use crate::wire::{Composer, OutBuf, WireReader};
7use crate::zone::Scanner;
8use crate::{Error, Result, Rtype};
9
10/// `DHCID` record data: the DHCP client identity associated with a name
11/// (RFC 4701 §3).
12///
13/// The RDATA is an identifier-type code, a digest-type code and a digest
14/// (RFC 4701 §3.3); it must hold at least the two codes, i.e. 3 bytes.
15#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
16pub struct Dhcid<'a> {
17    data: &'a [u8],
18}
19
20impl<'a> Dhcid<'a> {
21    /// Identifier type: the client's `htype` and `chaddr` from a DHCPv4
22    /// message (RFC 4701 §3.3).
23    pub const ID_CHADDR: u16 = 0x0000;
24    /// Identifier type: the DHCPv4 client identifier option (RFC 4701
25    /// §3.3).
26    pub const ID_CLIENT_ID: u16 = 0x0001;
27    /// Identifier type: the DHCPv6 client DUID (RFC 4701 §3.3).
28    pub const ID_DUID: u16 = 0x0002;
29    /// Digest type: SHA-256 (RFC 4701 §3.4).
30    pub const DIGEST_SHA256: u8 = 1;
31
32    /// Wraps the RDATA bytes, which must hold at least the identifier-type
33    /// and digest-type codes (RFC 4701 §3.3), else
34    /// [`Error::InvalidRdata`].
35    #[inline]
36    pub const fn from_wire(data: &'a [u8]) -> Result<Self> {
37        if data.len() < 3 {
38            return Err(Error::InvalidRdata);
39        }
40        Ok(Dhcid { data })
41    }
42
43    /// The whole RDATA.
44    #[inline]
45    #[must_use]
46    pub const fn as_wire(&self) -> &'a [u8] {
47        self.data
48    }
49
50    /// The identifier-type code (RFC 4701 §3.3).
51    #[inline]
52    #[must_use]
53    pub const fn identifier_type(&self) -> u16 {
54        match self.data {
55            [a, b, ..] => u16::from_be_bytes([*a, *b]),
56            _ => 0,
57        }
58    }
59
60    /// The digest-type code (RFC 4701 §3.4).
61    #[inline]
62    #[must_use]
63    pub const fn digest_type(&self) -> u8 {
64        match self.data {
65            [_, _, d, ..] => *d,
66            _ => 0,
67        }
68    }
69
70    /// The digest: `digest(identifier || FQDN)` (RFC 4701 §3.3, §3.5).
71    #[inline]
72    #[must_use]
73    pub const fn digest(&self) -> &'a [u8] {
74        match self.data {
75            [_, _, _, rest @ ..] => rest,
76            _ => &[],
77        }
78    }
79}
80
81impl ParseRdataText for Dhcid<'_> {
82    /// The whole RDATA in base64 (RFC 4701 §3.1), which may be split
83    /// across blanks and lines; it must decode to at least the two type
84    /// codes (3 octets).
85    fn parse_text<B: OutBuf + ?Sized>(s: &mut Scanner<'_>, out: &mut B) -> Result<()> {
86        if s.base64_rest_into(out)? == 0 {
87            return Err(Error::UnexpectedEof);
88        }
89        Ok(())
90    }
91}
92
93impl<'a> ParseRdata<'a> for Dhcid<'a> {
94    const RTYPE: Rtype = Rtype::DHCID;
95
96    fn parse_rdata(rdata: &mut WireReader<'a>) -> Result<Self> {
97        let d = Dhcid::from_wire(rdata.peek_rest())?;
98        rdata.read_rest();
99        Ok(d)
100    }
101}
102
103impl ComposeRdata for Dhcid<'_> {
104    fn rtype(&self) -> Rtype {
105        Rtype::DHCID
106    }
107
108    fn compose_rdata<C: Composer + ?Sized>(&self, c: &mut C) -> Result<()> {
109        c.put_bytes(self.data)
110    }
111}
112
113impl fmt::Display for Dhcid<'_> {
114    /// The RDATA in base64 (RFC 4701 §3.1).
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        fmt::Display::fmt(&crate::text::Base64(self.data), f)
117    }
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::rdata::tests::{parse, round_trip, text_error, text_round_trip};
124    use crate::testutil::hex;
125    use crate::{Class, RData};
126
127    #[test]
128    fn rfc4701_examples() {
129        // RFC 4701 §3.6, examples 1–3 (identifier types DUID, client-id
130        // option, chaddr). The digests were checked against
131        // SHA-256(identifier || FQDN) when writing this test.
132        for (id_type, wire, text) in [
133            (
134                Dhcid::ID_DUID,
135                "000201636fc0b8271c82825bb1ac5c41cf5351aa69b4febd94e8f17cdb95000da48c40",
136                "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=",
137            ),
138            (
139                Dhcid::ID_CLIENT_ID,
140                "0001013920fe5d1dceb3fd0ba3379756a70d73b17009f41d58bddbfcd6a2503956d8da",
141                "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=",
142            ),
143            (
144                Dhcid::ID_CHADDR,
145                "000001c4b9a5b249651343158dde7bcc77169841f7a4243a572b5c283fffedeb3f75e6",
146                "AAABxLmlskllE0MVjd57zHcWmEH3pCQ6VytcKD//7es/deY=",
147            ),
148        ] {
149            let wire = hex(wire);
150            round_trip(Rtype::DHCID, &wire, text);
151            let Ok(RData::Dhcid(d)) = parse(Rtype::DHCID, Class::IN, &wire) else {
152                panic!("not DHCID")
153            };
154            assert_eq!(d.identifier_type(), id_type);
155            assert_eq!(d.digest_type(), Dhcid::DIGEST_SHA256);
156            assert_eq!(d.digest().len(), 32);
157            assert_eq!(d.as_wire(), &wire[..]);
158        }
159    }
160
161    #[test]
162    fn too_short() {
163        for bad in [&b""[..], b"\x00", b"\x00\x02"] {
164            assert_eq!(Dhcid::from_wire(bad), Err(Error::InvalidRdata));
165            assert_eq!(
166                parse(Rtype::DHCID, Class::IN, bad),
167                Err(Error::InvalidRdata)
168            );
169        }
170        let d = Dhcid::from_wire(b"\x00\x02\x01").unwrap();
171        assert!(d.digest().is_empty());
172        round_trip(Rtype::DHCID, b"\x00\x02\x01", "AAIB");
173    }
174
175    #[test]
176    fn text() {
177        // RFC 4701 §3.6, exactly as presented there: the base64 is split
178        // mid-quantum across lines.
179        for (text, wire, shown) in [
180            (
181                "( AAIBY2/AuCccgoJbsaxcQc9TUapptP69l\n  OjxfNuVAA2kjEA= )",
182                "000201636fc0b8271c82825bb1ac5c41cf5351aa69b4febd94e8f17cdb95000da48c40",
183                "AAIBY2/AuCccgoJbsaxcQc9TUapptP69lOjxfNuVAA2kjEA=",
184            ),
185            (
186                "( AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdW\n  L3b/NaiUDlW2No= )",
187                "0001013920fe5d1dceb3fd0ba3379756a70d73b17009f41d58bddbfcd6a2503956d8da",
188                "AAEBOSD+XR3Os/0LozeXVqcNc7FwCfQdWL3b/NaiUDlW2No=",
189            ),
190            (
191                "( AAABxLmlskllE0MVjd57zHcWmEH3pCQ6V\n  ytcKD//7es/deY= )",
192                "000001c4b9a5b249651343158dde7bcc77169841f7a4243a572b5c283fffedeb3f75e6",
193                "AAABxLmlskllE0MVjd57zHcWmEH3pCQ6VytcKD//7es/deY=",
194            ),
195        ] {
196            text_round_trip(Rtype::DHCID, text, &hex(wire), shown);
197        }
198        text_round_trip(Rtype::DHCID, "AAIB", b"\x00\x02\x01", "AAIB");
199    }
200
201    #[test]
202    fn text_malformed() {
203        for (text, err) in [
204            ("", Error::UnexpectedEof),
205            // Shorter than the two type codes (RFC 4701 §3.3).
206            ("AAI=", Error::InvalidRdata),
207            ("AA==", Error::InvalidRdata),
208            ("AAIB=", Error::InvalidText),
209            ("AAI", Error::InvalidText),
210            ("AA!B", Error::InvalidText),
211            ("\"AAIB\"", Error::InvalidText),
212        ] {
213            assert_eq!(text_error(Rtype::DHCID, text), err, "{text:?}");
214        }
215    }
216}