hickory_proto/dnssec/
tsig.rs

1// Copyright 2015-2019 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! Trust dns implementation of Secret Key Transaction Authentication for DNS (TSIG)
9//! [RFC 8945](https://www.rfc-editor.org/rfc/rfc8945) November 2020
10//!
11//! Current deviation from RFC in implementation as of 2022-10-28
12//!
13//! - Mac checking don't support HMAC truncation with TSIG (pedantic constant time verification)
14//! - Time checking not in TSIG implementation but in caller
15
16use alloc::boxed::Box;
17use alloc::string::ToString;
18use alloc::sync::Arc;
19use alloc::vec::Vec;
20use core::ops::Range;
21
22use tracing::debug;
23
24use super::rdata::DNSSECRData;
25use super::rdata::tsig::{
26    TSIG, TsigAlgorithm, make_tsig_record, message_tbs, signed_bitmessage_to_buf,
27};
28use super::{DnsSecError, DnsSecErrorKind};
29use crate::error::{ProtoError, ProtoResult};
30use crate::op::{Message, MessageFinalizer, MessageVerifier};
31use crate::rr::{Name, RData, Record};
32use crate::xfer::DnsResponse;
33
34/// Struct to pass to a client for it to authenticate requests using TSIG.
35#[derive(Clone)]
36pub struct TSigner(Arc<TSignerInner>);
37
38struct TSignerInner {
39    key: Vec<u8>, // TODO this might want to be some sort of auto-zeroing on drop buffer, as it's cryptographic material
40    algorithm: TsigAlgorithm,
41    signer_name: Name,
42    fudge: u16,
43}
44
45impl TSigner {
46    /// Create a new Tsigner from its parts
47    ///
48    /// # Arguments
49    ///
50    /// * `key` - cryptographic key used to authenticate exchanges
51    /// * `algorithm` - algorithm used to authenticate exchanges
52    /// * `signer_name` - name of the key. Must match the name known to the server
53    /// * `fudge` - maximum difference between client and server time, in seconds, see [fudge](TSigner::fudge) for details
54    pub fn new(
55        key: Vec<u8>,
56        algorithm: TsigAlgorithm,
57        mut signer_name: Name,
58        fudge: u16,
59    ) -> Result<Self, DnsSecError> {
60        signer_name.set_fqdn(true);
61        if algorithm.supported() {
62            Ok(Self(Arc::new(TSignerInner {
63                key,
64                algorithm,
65                signer_name,
66                fudge,
67            })))
68        } else {
69            Err(DnsSecErrorKind::TsigUnsupportedMacAlgorithm(algorithm).into())
70        }
71    }
72
73    /// Return the key used for message authentication
74    pub fn key(&self) -> &[u8] {
75        &self.0.key
76    }
77
78    /// Return the algorithm used for message authentication
79    pub fn algorithm(&self) -> &TsigAlgorithm {
80        &self.0.algorithm
81    }
82
83    /// Name of the key used by this signer
84    pub fn signer_name(&self) -> &Name {
85        &self.0.signer_name
86    }
87
88    /// Maximum time difference between client time when issuing a message, and server time when
89    /// receiving it, in second. If time is out, the server will consider the request invalid.
90    /// Longer values means more room for replay by an attacker. A few minutes are usually a good
91    /// value.
92    pub fn fudge(&self) -> u16 {
93        self.0.fudge
94    }
95
96    /// Compute authentication tag for a buffer
97    pub fn sign(&self, tbs: &[u8]) -> Result<Vec<u8>, DnsSecError> {
98        self.0.algorithm.mac_data(&self.0.key, tbs)
99    }
100
101    /// Compute authentication tag for a message
102    pub fn sign_message(&self, message: &Message, pre_tsig: &TSIG) -> Result<Vec<u8>, DnsSecError> {
103        self.sign(&message_tbs(None, message, pre_tsig, &self.0.signer_name)?)
104    }
105
106    /// Verify hmac in constant time to prevent timing attacks
107    pub fn verify(&self, tbv: &[u8], tag: &[u8]) -> Result<(), DnsSecError> {
108        self.0.algorithm.verify_mac(&self.0.key, tbv, tag)
109    }
110
111    /// Verify the message is correctly signed
112    /// This does not perform time verification on its own, instead one should verify current time
113    /// lie in returned Range
114    ///
115    /// # Arguments
116    /// * `previous_hash` - Hash of the last message received before this one, or of the query for
117    ///   the first message
118    /// * `message` - byte buffer containing current message
119    /// * `first_message` - is this the first response message
120    ///
121    /// # Returns
122    /// Return Ok(_) on valid signature. Inner tuple contain the following values, in order:
123    /// * a byte buffer containing the hash of this message. Need to be passed back when
124    ///   authenticating next message
125    /// * a Range of time that is acceptable
126    /// * the time the signature was emitted. It must be greater or equal to the time of previous
127    ///   messages, if any
128    pub fn verify_message_byte(
129        &self,
130        previous_hash: Option<&[u8]>,
131        message: &[u8],
132        first_message: bool,
133    ) -> Result<(Vec<u8>, Range<u64>, u64), DnsSecError> {
134        let (tbv, record) = signed_bitmessage_to_buf(previous_hash, message, first_message)?;
135        let tsig = if let RData::DNSSEC(DNSSECRData::TSIG(tsig)) = record.data() {
136            tsig
137        } else {
138            unreachable!("tsig::signed_message_to_buff always returns a TSIG record")
139        };
140
141        // https://tools.ietf.org/html/rfc8945#section-5.2
142        // 1.  Check key
143        if record.name() != &self.0.signer_name || tsig.algorithm() != &self.0.algorithm {
144            return Err(DnsSecErrorKind::TsigWrongKey.into());
145        }
146
147        // 2.  Check MAC
148        //  note: that this verification does not allow for truncation of the HMAC, which technically the RFC suggests.
149        //    this is to be pedantic about constant time HMAC validation (prevent timing attacks) as well as any security
150        //    concerns about MAC truncation and collisions.
151        if tsig.mac().len() < tsig.algorithm().output_len()? {
152            return Err(DnsSecError::from(
153                "Please file an issue with https://github.com/hickory-dns/hickory-dns to support truncated HMACs with TSIG",
154            ));
155        }
156
157        // verify the MAC
158        let mac = tsig.mac();
159        self.verify(&tbv, mac)?;
160
161        // 3.  Check time values
162        // we don't actually have time here so we will let upper level decide
163        // this is technically in violation of the RFC, in case both time and
164        // truncation policy are bad, time should be reported and this code will report
165        // truncation issue instead
166
167        // 4.  Check truncation policy
168        //   see not above in regards to not supporting verification of truncated HMACs.
169        // if tsig.mac().len() < std::cmp::max(10, self.0.algorithm.output_len()? / 2) {
170        //     return Err(ProtoError::from(
171        //         "tsig validation error: truncated signature",
172        //     ));
173        // }
174
175        Ok((
176            tsig.mac().to_vec(),
177            Range {
178                start: tsig.time() - tsig.fudge() as u64,
179                end: tsig.time() + tsig.fudge() as u64,
180            },
181            tsig.time(),
182        ))
183    }
184}
185
186impl MessageFinalizer for TSigner {
187    fn finalize_message(
188        &self,
189        message: &Message,
190        current_time: u32,
191    ) -> ProtoResult<(Vec<Record>, Option<MessageVerifier>)> {
192        debug!("signing message: {:?}", message);
193        let current_time = current_time as u64;
194
195        let pre_tsig = TSIG::new(
196            self.0.algorithm.clone(),
197            current_time,
198            self.0.fudge,
199            Vec::new(),
200            message.id(),
201            0,
202            Vec::new(),
203        );
204        let mut signature: Vec<u8> = self
205            .sign_message(message, &pre_tsig)
206            .map_err(|err| ProtoError::from(err.to_string()))?;
207        let tsig = make_tsig_record(
208            self.0.signer_name.clone(),
209            pre_tsig.set_mac(signature.clone()),
210        );
211        let self2 = self.clone();
212        let mut remote_time = 0;
213        let verifier = move |dns_response: &[u8]| {
214            let (last_sig, range, rt) = self2
215                .verify_message_byte(Some(signature.as_ref()), dns_response, remote_time == 0)
216                .map_err(|err| ProtoError::from(err.to_string()))?;
217            if rt >= remote_time && range.contains(&current_time)
218            // this assumes a no-latency answer
219            {
220                signature = last_sig;
221                remote_time = rt;
222                DnsResponse::from_buffer(dns_response.to_vec())
223            } else {
224                Err(ProtoError::from("tsig validation error: outdated response"))
225            }
226        };
227        Ok((vec![tsig], Some(Box::new(verifier))))
228    }
229}
230
231#[cfg(test)]
232mod tests {
233    #![allow(clippy::dbg_macro, clippy::print_stdout)]
234
235    use crate::op::{Message, Query};
236    use crate::rr::Name;
237    use crate::serialize::binary::BinEncodable;
238
239    use super::*;
240    fn assert_send_and_sync<T: Send + Sync>() {}
241
242    #[test]
243    fn test_send_and_sync() {
244        assert_send_and_sync::<TSigner>();
245    }
246
247    #[test]
248    fn test_sign_and_verify_message_tsig() {
249        let time_begin = 1609459200u64;
250        let fudge = 300u64;
251        let origin: Name = Name::parse("example.com.", None).unwrap();
252        let key_name: Name = Name::from_ascii("key_name.").unwrap();
253        let mut question: Message = Message::new();
254        let mut query: Query = Query::new();
255        query.set_name(origin);
256        question.add_query(query);
257
258        let sig_key = b"some_key".to_vec();
259        let signer =
260            TSigner::new(sig_key, TsigAlgorithm::HmacSha512, key_name, fudge as u16).unwrap();
261
262        assert!(question.signature().is_empty());
263        question
264            .finalize(&signer, time_begin as u32)
265            .expect("should have signed");
266        assert!(!question.signature().is_empty());
267
268        let (_, validity_range, _) = signer
269            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
270            .unwrap();
271        assert!(validity_range.contains(&(time_begin + fudge / 2))); // slightly outdated, but still to be acceptable
272        assert!(validity_range.contains(&(time_begin - fudge / 2))); // sooner than our time, but still acceptable
273        assert!(!validity_range.contains(&(time_begin + fudge * 2))); // too late to be accepted
274        assert!(!validity_range.contains(&(time_begin - fudge * 2))); // too soon to be accepted
275    }
276
277    // make rejection tests shorter by centralizing common setup code
278    fn get_message_and_signer() -> (Message, TSigner) {
279        let time_begin = 1609459200u64;
280        let fudge = 300u64;
281        let origin: Name = Name::parse("example.com.", None).unwrap();
282        let key_name: Name = Name::from_ascii("key_name.").unwrap();
283        let mut question: Message = Message::new();
284        let mut query: Query = Query::new();
285        query.set_name(origin);
286        question.add_query(query);
287
288        let sig_key = b"some_key".to_vec();
289        let signer =
290            TSigner::new(sig_key, TsigAlgorithm::HmacSha512, key_name, fudge as u16).unwrap();
291
292        assert!(question.signature().is_empty());
293        question
294            .finalize(&signer, time_begin as u32)
295            .expect("should have signed");
296        assert!(!question.signature().is_empty());
297
298        // this should be ok, it has not been tampered with
299        assert!(
300            signer
301                .verify_message_byte(None, &question.to_bytes().unwrap(), true)
302                .is_ok()
303        );
304
305        (question, signer)
306    }
307
308    #[test]
309    fn test_sign_and_verify_message_tsig_reject_keyname() {
310        let (mut question, signer) = get_message_and_signer();
311
312        let other_name: Name = Name::from_ascii("other_name.").unwrap();
313        let mut signature = question.take_signature().remove(0);
314        signature.set_name(other_name);
315        question.add_tsig(signature);
316
317        assert!(
318            signer
319                .verify_message_byte(None, &question.to_bytes().unwrap(), true)
320                .is_err()
321        );
322    }
323
324    #[test]
325    fn test_sign_and_verify_message_tsig_reject_invalid_mac() {
326        let (mut question, signer) = get_message_and_signer();
327
328        let mut query: Query = Query::new();
329        let origin: Name = Name::parse("example.net.", None).unwrap();
330        query.set_name(origin);
331        question.add_query(query);
332
333        assert!(
334            signer
335                .verify_message_byte(None, &question.to_bytes().unwrap(), true)
336                .is_err()
337        );
338    }
339}