trust_dns_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// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://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/bluejekyll/trust-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(test)]
230#[cfg(any(feature = "dnssec-ring", feature = "dnssec-openssl"))]
231
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!(signer
300            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
301            .is_ok());
302
303        (question, signer)
304    }
305
306    #[test]
307    fn test_sign_and_verify_message_tsig_reject_keyname() {
308        let (mut question, signer) = get_message_and_signer();
309
310        let other_name: Name = Name::from_ascii("other_name").unwrap();
311        let mut signature = question.take_signature().remove(0);
312        signature.set_name(other_name);
313        question.add_tsig(signature);
314
315        assert!(signer
316            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
317            .is_err());
318    }
319
320    #[test]
321    fn test_sign_and_verify_message_tsig_reject_invalid_mac() {
322        let (mut question, signer) = get_message_and_signer();
323
324        let mut query: Query = Query::new();
325        let origin: Name = Name::parse("example.net.", None).unwrap();
326        query.set_name(origin);
327        question.add_query(query);
328
329        assert!(signer
330            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
331            .is_err());
332    }
333
334    #[test]
335    #[cfg(feature = "hmac_truncation")] // not currently supported for security reasons
336    fn test_sign_and_verify_message_tsig_truncation() {
337        let (mut question, signer) = get_message_and_signer();
338
339        {
340            let mut signature = question.take_signature().remove(0);
341            if let RData::DNSSEC(DNSSECRData::TSIG(ref mut tsig)) = signature.rdata_mut() {
342                let mut mac = tsig.mac().to_vec();
343                mac.push(0); // make one longer than sha512
344                std::mem::swap(tsig, &mut tsig.clone().set_mac(mac));
345            } else {
346                panic!("should have been a TSIG");
347            }
348            question.add_tsig(signature);
349        }
350
351        // we are longer, there is a problem
352        assert!(signer
353            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
354            .is_err());
355        {
356            let mut signature = question.take_signature().remove(0);
357            if let RData::DNSSEC(DNSSECRData::TSIG(ref mut tsig)) = signature.rdata_mut() {
358                // sha512 is 512 bits, half of that is 256 bits, /8 for byte
359                let mac = tsig.mac()[..256 / 8].to_vec();
360                std::mem::swap(tsig, &mut tsig.clone().set_mac(mac));
361            } else {
362                panic!("should have been a TSIG");
363            }
364            question.add_tsig(signature);
365        }
366
367        // we are at half, it's allowed
368        assert!(signer
369            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
370            .is_ok());
371
372        {
373            let mut signature = question.take_signature().remove(0);
374            if let RData::DNSSEC(DNSSECRData::TSIG(ref mut tsig)) = signature.rdata_mut() {
375                // less than half of sha512
376                let mac = tsig.mac()[..240 / 8].to_vec();
377                std::mem::swap(tsig, &mut tsig.clone().set_mac(mac));
378            } else {
379                panic!("should have been a TSIG");
380            }
381            question.add_tsig(signature);
382        }
383
384        assert!(signer
385            .verify_message_byte(None, &question.to_bytes().unwrap(), true)
386            .is_err());
387    }
388}