Skip to main content

dnsbox/rdata/
sshfp.rs

1//! SSHFP record data (RFC 4255, RFC 6594, RFC 7479, RFC 8709) and its
2//! algorithm and fingerprint-type registries.
3
4use core::fmt;
5
6use super::{ComposeRdata, ParseRdata, ParseRdataText};
7use crate::wire::{Composer, OutBuf, WireReader};
8use crate::zone::Scanner;
9use crate::{Error, Result, Rtype};
10
11open_enum! {
12    /// An SSHFP public key algorithm number (RFC 4255 §3.1.1, IANA "SSHFP
13    /// RR Types for public key algorithms").
14    ///
15    /// The presentation format of SSHFP uses the bare number.
16    pub struct SshfpAlgorithm(u8), generic "";
17    /// RSA (RFC 4255).
18    RSA = 1 => "RSA",
19    /// DSA (RFC 4255).
20    DSA = 2 => "DSA",
21    /// ECDSA (RFC 6594).
22    ECDSA = 3 => "ECDSA",
23    /// Ed25519 (RFC 7479).
24    ED25519 = 4 => "Ed25519",
25    /// Ed448 (RFC 8709).
26    ED448 = 6 => "Ed448",
27}
28
29open_enum! {
30    /// An SSHFP fingerprint type (RFC 4255 §3.1.2, IANA "SSHFP RR types
31    /// for fingerprint types").
32    ///
33    /// The presentation format of SSHFP uses the bare number.
34    pub struct SshfpFpType(u8), generic "";
35    /// SHA-1 (RFC 4255).
36    SHA1 = 1 => "SHA-1",
37    /// SHA-256 (RFC 6594).
38    SHA256 = 2 => "SHA-256",
39}
40
41/// `SSHFP` record data: an SSH host key fingerprint (RFC 4255 §3.1).
42#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
43pub struct Sshfp<'a> {
44    /// Algorithm of the public key (RFC 4255 §3.1.1).
45    pub algorithm: SshfpAlgorithm,
46    /// Message-digest algorithm of the fingerprint (RFC 4255 §3.1.2).
47    pub fp_type: SshfpFpType,
48    /// The fingerprint: the rest of the RDATA (RFC 4255 §3.1.3).
49    pub fingerprint: &'a [u8],
50}
51
52impl<'a> Sshfp<'a> {
53    /// Builds SSHFP data from its fields (RFC 4255 §3.1).
54    #[inline]
55    #[must_use]
56    pub const fn new(
57        algorithm: SshfpAlgorithm,
58        fp_type: SshfpFpType,
59        fingerprint: &'a [u8],
60    ) -> Self {
61        Sshfp {
62            algorithm,
63            fp_type,
64            fingerprint,
65        }
66    }
67}
68
69impl ParseRdataText for Sshfp<'_> {
70    /// `<algorithm> <fp-type> <fingerprint>` (RFC 4255 §3.2): two decimal
71    /// numbers and the fingerprint in hexadecimal, which may be split
72    /// across blanks and lines. At least one octet is required (an empty
73    /// fingerprint has only the generic form, as in BIND).
74    fn parse_text<B: OutBuf + ?Sized>(s: &mut Scanner<'_>, out: &mut B) -> Result<()> {
75        out.put_u8(s.u8()?)?;
76        out.put_u8(s.u8()?)?;
77        if s.hex_rest_into(out)? == 0 {
78            return Err(Error::UnexpectedEof);
79        }
80        Ok(())
81    }
82}
83
84impl<'a> ParseRdata<'a> for Sshfp<'a> {
85    const RTYPE: Rtype = Rtype::SSHFP;
86
87    fn parse_rdata(rdata: &mut WireReader<'a>) -> Result<Self> {
88        Ok(Sshfp {
89            algorithm: SshfpAlgorithm::new(rdata.read_u8()?),
90            fp_type: SshfpFpType::new(rdata.read_u8()?),
91            fingerprint: rdata.read_rest(),
92        })
93    }
94}
95
96impl ComposeRdata for Sshfp<'_> {
97    fn rtype(&self) -> Rtype {
98        Rtype::SSHFP
99    }
100
101    fn compose_rdata<C: Composer + ?Sized>(&self, c: &mut C) -> Result<()> {
102        c.put_u8(self.algorithm.get())?;
103        c.put_u8(self.fp_type.get())?;
104        c.put_bytes(self.fingerprint)
105    }
106}
107
108impl fmt::Display for Sshfp<'_> {
109    /// `algorithm fp-type fingerprint-hex` (RFC 4255 §3.2). An empty
110    /// fingerprint has no such form and is written in the generic RFC 3597
111    /// §5 form instead.
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        if self.fingerprint.is_empty() {
114            return crate::text::fmt_generic_rdata(
115                f,
116                &[self.algorithm.get(), self.fp_type.get()],
117            );
118        }
119        write!(
120            f,
121            "{} {} {}",
122            self.algorithm.get(),
123            self.fp_type.get(),
124            crate::text::Hex(self.fingerprint)
125        )
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::Class;
133    use crate::rdata::tests::{compose, parse, round_trip, text_error, text_round_trip};
134    use crate::testutil::hex;
135    use std::string::ToString;
136
137    #[test]
138    fn rfc4255_example() {
139        // RFC 4255 §3.3:
140        //   host.example.  SSHFP 2 1 123456789abcdef67890123456789abcdef67890
141        let mut wire = std::vec![2, 1];
142        wire.extend(hex("123456789abcdef67890123456789abcdef67890"));
143        round_trip(
144            Rtype::SSHFP,
145            &wire,
146            "2 1 123456789ABCDEF67890123456789ABCDEF67890",
147        );
148        let Ok(crate::RData::Sshfp(s)) = parse(Rtype::SSHFP, Class::IN, &wire) else {
149            panic!("not SSHFP")
150        };
151        assert_eq!(s.algorithm, SshfpAlgorithm::DSA);
152        assert_eq!(s.fp_type, SshfpFpType::SHA1);
153        assert_eq!(s.fingerprint.len(), 20);
154    }
155
156    #[test]
157    fn rfc6594_sha256() {
158        // An ECDSA key with a SHA-256 fingerprint (values from RFC 6594).
159        let fp = hex("821eb6c1c98d9cc827ab7f456304c0f14785b7008d9e8646a8519de80849afc7");
160        let s = Sshfp::new(SshfpAlgorithm::ECDSA, SshfpFpType::SHA256, &fp);
161        let wire = compose(&s);
162        assert_eq!(&wire[..2], [3, 2]);
163        round_trip(
164            Rtype::SSHFP,
165            &wire,
166            "3 2 821EB6C1C98D9CC827AB7F456304C0F14785B7008D9E8646A8519DE80849AFC7",
167        );
168    }
169
170    #[test]
171    fn registries_and_edge_cases() {
172        assert_eq!(SshfpAlgorithm::ED25519.get(), 4);
173        assert_eq!(SshfpAlgorithm::ED448.to_string(), "Ed448");
174        assert_eq!("sha-256".parse(), Ok(SshfpFpType::SHA256));
175        assert_eq!(SshfpFpType::new(9).to_string(), "9");
176        // Unknown numbers round-trip and display as numbers.
177        round_trip(Rtype::SSHFP, b"\x09\x07\xab", "9 7 AB");
178        // No fingerprint: generic form.
179        round_trip(Rtype::SSHFP, b"\x01\x01", "\\# 2 0101");
180        assert_eq!(parse(Rtype::SSHFP, Class::IN, b"\x01"), Err(Error::UnexpectedEof));
181    }
182
183    #[test]
184    fn text() {
185        // RFC 4255 §3.3:
186        //   host.example.  SSHFP 2 1 123456789abcdef67890123456789abcdef67890
187        let mut wire = std::vec![2, 1];
188        wire.extend(hex("123456789abcdef67890123456789abcdef67890"));
189        text_round_trip(
190            Rtype::SSHFP,
191            "2 1 123456789abcdef67890123456789abcdef67890",
192            &wire,
193            "2 1 123456789ABCDEF67890123456789ABCDEF67890",
194        );
195        // RFC 6594 §3: an ECDSA key with a SHA-256 fingerprint, split
196        // across lines as zone files do (RFC 4255 §3.2).
197        let mut wire = std::vec![3, 2];
198        wire.extend(hex(
199            "821eb6c1c98d9cc827ab7f456304c0f14785b7008d9e8646a8519de80849afc7",
200        ));
201        text_round_trip(
202            Rtype::SSHFP,
203            "3 2 (\n 821eb6c1c98d9cc827ab7f456304c0f1\n 4785b7008d9e8646a8519de80849afc7 )",
204            &wire,
205            "3 2 821EB6C1C98D9CC827AB7F456304C0F14785B7008D9E8646A8519DE80849AFC7",
206        );
207        // RFC 7479 §3 (Ed25519, SHA-256), split at odd digit counts.
208        let mut wire = std::vec![4, 2];
209        wire.extend(hex(
210            "a87f1b687ac0e57d2a081a2f282672334d90ed316d2b818ca9580ea384d92401",
211        ));
212        text_round_trip(
213            Rtype::SSHFP,
214            "4 2 ( a87f1b687ac0e57d2a081a2f2826723\n 34d90ed316d2b818ca9580ea384d924\n 01 )",
215            &wire,
216            "4 2 A87F1B687AC0E57D2A081A2F282672334D90ED316D2B818CA9580EA384D92401",
217        );
218        // Unassigned numbers round-trip; digits may be split anywhere.
219        text_round_trip(Rtype::SSHFP, "255 9 0 0 a B", b"\xff\x09\x00\xab", "255 9 00AB");
220    }
221
222    #[test]
223    fn text_malformed() {
224        for (text, err) in [
225            ("", Error::UnexpectedEof),
226            ("2", Error::UnexpectedEof),
227            // No fingerprint: only the generic form can express it.
228            ("2 1", Error::UnexpectedEof),
229            ("256 1 00", Error::InvalidText),
230            ("2 -1 00", Error::InvalidText),
231            // SSHFP numbers have no mnemonics in the presentation format.
232            ("RSA 1 00", Error::InvalidText),
233            ("2 1 0", Error::InvalidText),
234            ("2 1 0g", Error::InvalidText),
235            ("2 1 \"00\"", Error::InvalidText),
236        ] {
237            assert_eq!(text_error(Rtype::SSHFP, text), err, "{text:?}");
238        }
239    }
240}