dns_cookie/
lib.rs

1//! RFC7873 left the construction of Server Cookies to the discretion
2//! of the DNS Server (implementer) which has resulted in a gallimaufry
3//! of different implementations.  As a result, DNS Cookies are
4//! impractical to deploy on multi-vendor anycast networks, because the
5//! Server Cookie constructed by one implementation cannot be validated
6//! by another.
7//!
8//! This crate is an implementation of [draft-sury-toorop-dnsop-server-cookies] which provides precise
9//! directions for creating Server and Client Cookies to address this issue.
10//!
11//! [draft-sury-toorop-dnsop-server-cookies]: https://datatracker.ietf.org/doc/html/draft-sury-toorop-dns-cookies-algorithms-00
12
13#![cfg_attr(feature = "no-std-net", no_std)]
14#![forbid(unsafe_code)]
15
16use core::convert::TryFrom;
17use core::fmt;
18use core::hash::Hasher;
19#[cfg(feature = "no-std-net")]
20use no_std_net::IpAddr;
21use siphasher::sip::SipHasher24;
22#[cfg(not(feature = "no-std-net"))]
23use std::net::IpAddr;
24use time::ext::NumericalDuration;
25use time::{OffsetDateTime, UtcOffset};
26
27const SERVER_COOKIE_LEN: usize = 16;
28const CLIENT_COOKIE_LEN: usize = 8;
29
30/// Prescribes the structure and Hash calculation formula
31#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
32#[must_use]
33pub enum Version {
34    One = 1,
35}
36
37impl TryFrom<u8> for Version {
38    type Error = Error;
39
40    fn try_from(version: u8) -> Result<Self, Self::Error> {
41        match version {
42            v if Version::One as u8 == v => Ok(Version::One),
43            v => Err(Error::UnknownVersion(v)),
44        }
45    }
46}
47
48/// Defines what algorithm function to use for calculating the Hash
49#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
50#[must_use]
51pub enum Algorithm {
52    SipHash24 = 4,
53}
54
55impl TryFrom<u8> for Algorithm {
56    type Error = Error;
57
58    fn try_from(algorithm: u8) -> Result<Self, Self::Error> {
59        match algorithm {
60            v if Algorithm::SipHash24 as u8 == v => Ok(Algorithm::SipHash24),
61            1 => Err(Error::UnsupportedAlgorithm("FNV")),
62            2 => Err(Error::UnsupportedAlgorithm("HMAC-SHA-256-64")),
63            3 => Err(Error::UnsupportedAlgorithm("AES")),
64            v => Err(Error::UnknownAlgorithm(v)),
65        }
66    }
67}
68
69#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
70struct Data {
71    version: Version,
72    algorithm: Algorithm,
73    reserved: u16,
74    time: OffsetDateTime,
75    client_cookie: [u8; CLIENT_COOKIE_LEN],
76}
77
78impl Data {
79    fn hash(&self, server_secret: &[u8]) -> u64 {
80        match self.version {
81            Version::One => match self.algorithm {
82                Algorithm::SipHash24 => {
83                    let mut hasher = SipHasher24::new();
84                    hasher.write(&self.client_cookie);
85                    hasher.write_u8(self.version as u8);
86                    hasher.write_u8(self.algorithm as u8);
87                    hasher.write_u16(self.reserved);
88                    hasher.write_u32(self.time.unix_timestamp() as u32);
89                    hasher.write(server_secret);
90                    hasher.finish()
91                }
92            },
93        }
94    }
95}
96
97/// A 128-bit Server Cookie
98#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
99#[must_use]
100pub struct Server {
101    data: Data,
102    hash: u64,
103}
104
105impl Server {
106    /// Creates a new server cookie
107    pub fn new(
108        version: Version,
109        algorithm: Algorithm,
110        reserved: u16,
111        time: OffsetDateTime,
112        client_cookie: [u8; CLIENT_COOKIE_LEN],
113        server_secret: &[u8],
114    ) -> Self {
115        let data = Data {
116            version,
117            algorithm,
118            reserved,
119            client_cookie,
120            time: time.to_offset(UtcOffset::UTC),
121        };
122        Self {
123            data,
124            hash: data.hash(server_secret),
125        }
126    }
127
128    /// Regenerates a server cookie if the current cookie is more than 30 minutes old
129    /// as prescribed by the draft
130    pub fn regenerate(mut self, time: OffsetDateTime, server_secret: &[u8]) -> Self {
131        let time = time.to_offset(UtcOffset::UTC);
132        if self.data.time > time - 30.minutes() {
133            return self;
134        }
135        self.data.time = time;
136        self.hash = self.data.hash(server_secret);
137        self
138    }
139
140    /// Creates and validates a server cookie from bytes
141    pub fn decode(
142        mut now: OffsetDateTime,
143        client_cookie: [u8; CLIENT_COOKIE_LEN],
144        server_cookie: &[u8],
145        server_secrets: &[&[u8]],
146    ) -> Result<Self, Error> {
147        now = now.to_offset(UtcOffset::UTC);
148        let cookie_len = server_cookie.len();
149        if cookie_len != SERVER_COOKIE_LEN {
150            return Err(Error::IncorrectLength(cookie_len));
151        }
152        let version = Version::try_from(server_cookie[0])?;
153        let algorithm = Algorithm::try_from(server_cookie[1])?;
154        let reserved = u16::from_be_bytes([server_cookie[2], server_cookie[3]]);
155        let time = {
156            let timestamp = u32::from_be_bytes([
157                server_cookie[4],
158                server_cookie[5],
159                server_cookie[6],
160                server_cookie[7],
161            ]);
162            OffsetDateTime::from_unix_timestamp(timestamp as i64).map_err(Error::TimestampRange)?
163        };
164        if time < now - 1.hours() {
165            return Err(Error::Expired);
166        } else if time > now + 5.minutes() {
167            return Err(Error::TimeTravellor);
168        }
169        let hash = u64::from_be_bytes([
170            server_cookie[8],
171            server_cookie[9],
172            server_cookie[10],
173            server_cookie[11],
174            server_cookie[12],
175            server_cookie[13],
176            server_cookie[14],
177            server_cookie[15],
178        ]);
179        for secret in server_secrets {
180            let cookie = Self::new(version, algorithm, reserved, time, client_cookie, secret);
181            if cookie.hash == hash {
182                return Ok(cookie);
183            }
184        }
185        Err(Error::InvalidHash)
186    }
187
188    /// Converts a server cookie to bytes
189    #[must_use]
190    pub const fn encode(self) -> [u8; SERVER_COOKIE_LEN] {
191        let reserved = self.data.reserved.to_be_bytes();
192        let timestamp = (self.data.time.unix_timestamp() as u32).to_be_bytes();
193        let hash = self.hash.to_be_bytes();
194        [
195            self.data.version as u8,
196            self.data.algorithm as u8,
197            reserved[0],
198            reserved[1],
199            timestamp[0],
200            timestamp[1],
201            timestamp[2],
202            timestamp[3],
203            hash[0],
204            hash[1],
205            hash[2],
206            hash[3],
207            hash[4],
208            hash[5],
209            hash[6],
210            hash[7],
211        ]
212    }
213}
214
215/// A 64-bit Client Cookie
216#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
217#[must_use]
218pub struct Client {
219    hash: u64,
220}
221
222impl Client {
223    /// Creates a new client cookie
224    pub fn new(
225        version: Version,
226        algorithm: Algorithm,
227        client_ip: IpAddr,
228        server_ip: IpAddr,
229        client_secret: &[u8],
230    ) -> Self {
231        match version {
232            Version::One => match algorithm {
233                Algorithm::SipHash24 => {
234                    let mut hasher = SipHasher24::new();
235                    match client_ip {
236                        IpAddr::V4(ip) => hasher.write(&ip.octets()),
237                        IpAddr::V6(ip) => hasher.write(&ip.octets()),
238                    }
239                    match server_ip {
240                        IpAddr::V4(ip) => hasher.write(&ip.octets()),
241                        IpAddr::V6(ip) => hasher.write(&ip.octets()),
242                    }
243                    hasher.write(client_secret);
244                    Self {
245                        hash: hasher.finish(),
246                    }
247                }
248            },
249        }
250    }
251
252    /// Creates and validates a client cookie from bytes
253    pub fn decode(
254        version: Version,
255        algorithm: Algorithm,
256        client_ip: IpAddr,
257        server_ip: IpAddr,
258        client_cookie: [u8; CLIENT_COOKIE_LEN],
259        client_secrets: &[&[u8]],
260    ) -> Result<Self, Error> {
261        let hash = u64::from_be_bytes(client_cookie);
262        for secret in client_secrets {
263            let cookie = Self::new(version, algorithm, client_ip, server_ip, secret);
264            if cookie.hash == hash {
265                return Ok(cookie);
266            }
267        }
268        Err(Error::InvalidHash)
269    }
270
271    /// Converts a client cookie to bytes
272    #[must_use]
273    pub const fn encode(self) -> [u8; CLIENT_COOKIE_LEN] {
274        self.hash.to_be_bytes()
275    }
276}
277
278/// The errors returned by this crate
279#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
280#[must_use]
281pub enum Error {
282    IncorrectLength(usize),
283    TimestampRange(time::error::ComponentRange),
284    InvalidHash,
285    Expired,
286    TimeTravellor,
287    UnknownVersion(u8),
288    UnknownAlgorithm(u8),
289    UnsupportedAlgorithm(&'static str),
290}
291
292impl fmt::Display for Error {
293    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294        match self {
295            Error::IncorrectLength(len) => write!(f, "cookie has an incorrect length ({})", len),
296            Error::TimestampRange(error) => write!(f, "{}", error),
297            Error::InvalidHash => write!(f, "cookie has an invalid hash"),
298            Error::Expired => write!(f, "cookie has expired"),
299            Error::TimeTravellor => write!(f, "cookie has a timestamp from the future"),
300            Error::UnknownVersion(version) => {
301                write!(f, "cookie has an unknown version ({})", version)
302            }
303            Error::UnknownAlgorithm(algorithm) => {
304                write!(f, "cookie has an unknown algorithm ({})", algorithm)
305            }
306            Error::UnsupportedAlgorithm(algorithm) => {
307                write!(f, "cookie has an unsupported algorithm ({})", algorithm)
308            }
309        }
310    }
311}
312
313#[cfg(not(feature = "no-std-net"))]
314impl std::error::Error for Error {}