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