Skip to main content

hickory_proto/rr/
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//! Hickory-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 deviations from RFC in implementation as of 2022-10-28
12//!
13//! - Truncated MACs are not supported.
14//! - Time checking is not performed in the TSIG implementation but by the caller.
15
16#[cfg(feature = "__dnssec")]
17use alloc::boxed::Box;
18#[cfg(feature = "__dnssec")]
19use alloc::string::ToString;
20use alloc::sync::Arc;
21use alloc::vec::Vec;
22#[cfg(feature = "__dnssec")]
23use core::ops::Range;
24
25#[cfg(feature = "__dnssec")]
26use tracing::debug;
27
28#[cfg(feature = "__dnssec")]
29use crate::dnssec::DnsSecError;
30
31use super::rdata::tsig::TsigAlgorithm;
32#[cfg(feature = "__dnssec")]
33use super::rdata::tsig::{
34    TSIG, TsigError, make_tsig_record, message_tbs, signed_bitmessage_to_buf,
35};
36#[cfg(feature = "__dnssec")]
37use crate::error::{ProtoError, ProtoResult};
38#[cfg(feature = "__dnssec")]
39use crate::op::DnsResponse;
40use crate::op::{Message, OpCode};
41#[cfg(feature = "__dnssec")]
42use crate::rr::Record;
43use crate::rr::{Name, RecordType};
44#[cfg(feature = "__dnssec")]
45use crate::serialize::binary::BinEncoder;
46
47/// Context for a TSIG response, used to construct a TSIG response signer
48pub struct TSigResponseContext {
49    #[cfg(feature = "__dnssec")]
50    request_id: u16,
51    #[cfg(feature = "__dnssec")]
52    time: u64,
53    #[cfg(feature = "__dnssec")]
54    kind: TsigResponseKind,
55}
56
57#[cfg(feature = "__dnssec")]
58impl TSigResponseContext {
59    /// Create a new TSIG response context
60    /// Create a new TSIG response context
61    pub fn new(
62        request_id: u16,
63        time: u64,
64        signer: TSigner,
65        request_mac: Vec<u8>,
66        error: Option<TsigError>,
67    ) -> Self {
68        Self {
69            request_id,
70            time,
71            kind: TsigResponseKind::Signed {
72                signer,
73                request_mac,
74                error,
75            },
76        }
77    }
78
79    /// Yield a response signer context for a bad request signature
80    pub fn bad_signature(request_id: u16, time: u64, signer: TSigner) -> Self {
81        Self {
82            request_id,
83            time,
84            kind: TsigResponseKind::BadSignature { signer },
85        }
86    }
87
88    /// Yield a response signer context for an unknown key
89    pub fn unknown_key(request_id: u16, time: u64, key_name: Name) -> Self {
90        Self {
91            request_id,
92            time,
93            kind: TsigResponseKind::UnknownKey { key_name },
94        }
95    }
96
97    /// Sign an encoded DNS response message according to the context
98    #[cfg(feature = "__dnssec")]
99    pub fn sign(self, response: &[u8]) -> Result<Box<Record<TSIG>>, ProtoError> {
100        match self.kind {
101            TsigResponseKind::Signed {
102                signer,
103                request_mac,
104                error,
105            } => {
106                // BadSig and BadKey are both spec'd to return **unsigned** TSIG RRs.
107                debug_assert!(!matches!(
108                    error,
109                    Some(TsigError::BadSig | TsigError::BadKey)
110                ));
111
112                let mut stub_tsig = TSIG::stub(self.request_id, self.time, &signer);
113                if let Some(err) = error {
114                    stub_tsig.error = Some(err);
115                }
116
117                let tbs_tsig_encoded =
118                    signer.encode_response_tbs(&request_mac, response, &stub_tsig)?;
119                let resp_tsig = stub_tsig.set_mac(
120                    signer
121                        .sign(&tbs_tsig_encoded)
122                        .map_err(|e| ProtoError::from(e.to_string()))?,
123                );
124
125                Ok(Box::new(make_tsig_record(
126                    signer.signer_name().clone(),
127                    resp_tsig,
128                )))
129            }
130            TsigResponseKind::BadSignature { signer } => {
131                let mut stub_tsig = TSIG::stub(self.request_id, self.time, &signer);
132                stub_tsig.error = Some(TsigError::BadSig);
133                Ok(Box::new(make_tsig_record(
134                    signer.signer_name().clone(),
135                    stub_tsig,
136                )))
137            }
138            TsigResponseKind::UnknownKey { key_name } => {
139                // "If a non-forwarding server does not recognize the key or algorithm used by the
140                // client (or recognizes the algorithm but does not implement it), the server MUST
141                // generate an error response with RCODE 9 (NOTAUTH) and TSIG ERROR 17 (BADKEY).
142                // This response MUST be unsigned"
143                //
144                // Note that this doesn't specify what TSIG algorithm, fudge, or key name we
145                // should use in the response since we didn't recognize the key name as one
146                // of our configured signers. We choose a stand-in algorithm and reflect the
147                // unknown key name in absence of further direction.
148                Ok(Box::new(make_tsig_record(
149                    key_name.clone(),
150                    TSIG::new(
151                        TsigAlgorithm::HmacSha256,
152                        self.time,
153                        300,
154                        Vec::new(),
155                        self.request_id,
156                        Some(TsigError::BadKey),
157                        Vec::new(),
158                    ),
159                )))
160            }
161        }
162    }
163}
164
165/// An enum describing the kind of response we may generate a response TSIG record for.
166#[cfg(feature = "__dnssec")]
167enum TsigResponseKind {
168    /// A TSIG response that has a populated MAC produced by the `signer`.
169    Signed {
170        signer: TSigner,
171        request_mac: Vec<u8>,
172        error: Option<TsigError>,
173    },
174    /// An unsigned TSIG response that populates a stub TSIG based on `signer`.
175    BadSignature { signer: TSigner },
176    /// An unsigned TSIG response where we were unable to find a `TSigner` with `key_name`.
177    UnknownKey { key_name: Name },
178}
179
180/// Struct to pass to a client for it to authenticate requests using TSIG.
181#[derive(Clone)]
182pub struct TSigner(Arc<TSignerInner>);
183
184struct TSignerInner {
185    key: Vec<u8>, // TODO this might want to be some sort of auto-zeroing on drop buffer, as it's cryptographic material
186    algorithm: TsigAlgorithm,
187    signer_name: Name,
188    fudge: u16,
189}
190
191impl TSigner {
192    /// Create a new TSigner from its parts
193    ///
194    /// # Arguments
195    ///
196    /// * `key` - cryptographic key used to authenticate exchanges
197    /// * `algorithm` - algorithm used to authenticate exchanges
198    /// * `signer_name` - name of the key. Must match the name known to the server
199    /// * `fudge` - maximum difference between client and server time, in seconds, see [fudge](TSigner::fudge) for details
200    #[cfg(feature = "__dnssec")]
201    pub fn new(
202        key: Vec<u8>,
203        algorithm: TsigAlgorithm,
204        mut signer_name: Name,
205        fudge: u16,
206    ) -> Result<Self, DnsSecError> {
207        if !algorithm.supported() {
208            return Err(DnsSecError::TsigUnsupportedMacAlgorithm(algorithm));
209        }
210
211        signer_name.set_fqdn(true);
212        Ok(Self(Arc::new(TSignerInner {
213            key,
214            algorithm,
215            signer_name,
216            fudge,
217        })))
218    }
219
220    /// Return the key used for message authentication
221    pub fn key(&self) -> &[u8] {
222        &self.0.key
223    }
224
225    /// Return the algorithm used for message authentication
226    pub fn algorithm(&self) -> &TsigAlgorithm {
227        &self.0.algorithm
228    }
229
230    /// Name of the key used by this signer
231    pub fn signer_name(&self) -> &Name {
232        &self.0.signer_name
233    }
234
235    /// Maximum time difference between client time when issuing a message, and server time when
236    /// receiving it, in second. If time is out, the server will consider the request invalid.
237    /// Longer values means more room for replay by an attacker. A few minutes are usually a good
238    /// value.
239    pub fn fudge(&self) -> u16 {
240        self.0.fudge
241    }
242
243    /// Compute authentication tag for a buffer
244    #[cfg(feature = "__dnssec")]
245    pub fn sign(&self, tbs: &[u8]) -> Result<Vec<u8>, DnsSecError> {
246        self.0.algorithm.mac_data(&self.0.key, tbs)
247    }
248
249    /// Verify hmac in constant time to prevent timing attacks
250    #[cfg(feature = "__dnssec")]
251    pub fn verify(&self, tbv: &[u8], tag: &[u8]) -> Result<(), DnsSecError> {
252        self.0.algorithm.verify_mac(&self.0.key, tbv, tag)
253    }
254
255    /// Returns true if the `TSigner` should sign the given `Message`
256    pub fn should_sign_message(&self, message: &Message) -> bool {
257        [OpCode::Update, OpCode::Notify].contains(&message.op_code)
258            || message
259                .queries
260                .iter()
261                .any(|q| [RecordType::AXFR, RecordType::IXFR].contains(&q.query_type()))
262    }
263
264    /// Verify the message is correctly signed
265    ///
266    /// This does not perform signature time verification. The caller should verify the
267    /// current time lies in the returned `Range`. See [RFC 8945 Section 5.2.3] for more information.
268    ///
269    /// # Arguments
270    /// * `message` - byte buffer containing the to-be-verified `Message`
271    /// * `previous_hash` - Hash of the last message received before this one when processing chained
272    ///   messages, or of a query for a first response message.
273    /// * `first_message` - whether `message` is the first response message
274    ///
275    /// # Returns
276    ///
277    /// Return `Ok(_)` for valid signatures. Inner tuple contain the following values, in order:
278    /// * a byte buffer containing the hash of `message`. This can be passed back when
279    ///   authenticating a later chained message.
280    /// * the time the signature was emitted. It must be greater or equal to the time of previous
281    ///   messages, if any.
282    /// * a `Range` of time that the signature is considered acceptable within based on the signer
283    ///   fudge value.
284    ///
285    /// [RFC 8945 Section 5.2.3]: https://www.rfc-editor.org/rfc/rfc8945.html#section-5.2.3
286    #[cfg(feature = "__dnssec")]
287    pub fn verify_message_byte(
288        &self,
289        message: &[u8],
290        previous_hash: Option<&[u8]>,
291        first_message: bool,
292    ) -> Result<(Vec<u8>, u64, Range<u64>), DnsSecError> {
293        let (tbv, record) = signed_bitmessage_to_buf(message, previous_hash, first_message)?;
294        let tsig = record.data;
295
296        // https://tools.ietf.org/html/rfc8945#section-5.2
297        // 1.  Check key
298        if record.name != self.0.signer_name || tsig.algorithm != self.0.algorithm {
299            return Err(DnsSecError::TsigWrongKey);
300        }
301
302        // 2.  Check MAC
303
304        // If the MAC length doesn't match the algorithm output length, then it was truncated.
305        // While the RFC supports this, we take a conservative approach and do not. Truncated
306        // MAC tags offer less security than their full-width counterparts, and the spec includes
307        // them only for backwards compatibility.
308        if tsig.mac.len() < tsig.algorithm.output_len()? {
309            return Err(DnsSecError::from(
310                "Please file an issue with https://github.com/hickory-dns/hickory-dns to support truncated HMACs with TSIG",
311            ));
312        }
313        self.verify(&tbv, &tsig.mac)?;
314
315        // 3.  Check time values
316        // Since we don't have a time source to use here we instead defer this to the caller.
317
318        // 4.  Check truncation policy
319        // We have already rejected truncated MACs so this step is not applicable.
320
321        Ok((
322            tsig.mac.to_vec(),
323            tsig.time,
324            Range {
325                start: tsig.time - tsig.fudge as u64,
326                end: tsig.time + tsig.fudge as u64,
327            },
328        ))
329    }
330
331    /// Encode the to-be-signed (TBS) bytes for an encoded response to a TSIG signed request
332    ///
333    /// The TSIG MAC of the query, the raw unsigned response bytes, and a stub TSIG
334    /// record are combined to produce the overall to-be-signed response.
335    ///
336    /// `previous_mac` contains the TSIG MAC of the query the reply is in response to.
337    /// `encoded_response` is the to-be-signed bytes of the constructed response.
338    /// `resp_id` is the ID of the response to use for the TSIG RR stub.
339    /// `now` is the timestamp to use for the TSIG RR stub.
340    #[cfg(feature = "__dnssec")]
341    pub fn encode_response_tbs(
342        &self,
343        previous_mac: &[u8],
344        encoded_response: &[u8],
345        stub_tsig: &TSIG,
346    ) -> Result<Vec<u8>, ProtoError> {
347        // the TBS buffer is sized based on the previous MAC, the overhead of its u16 len
348        // prefix, the size of the encoded response, and a rough approximation of the
349        // size of the stub TSIG RR.
350        let mut tbs_buf = Vec::with_capacity(
351            previous_mac.len() + size_of::<u16>() + encoded_response.len() + 128,
352        );
353        let mut encoder = BinEncoder::new(&mut tbs_buf);
354
355        debug_assert!(previous_mac.len() <= u16::MAX as usize); // Shouldn't happen for supported algorithms.
356        encoder.emit_u16(previous_mac.len() as u16)?;
357        encoder.emit_vec(previous_mac)?;
358        encoder.emit_vec(encoded_response)?;
359        stub_tsig.emit_tsig_for_mac(&mut encoder, self.signer_name())?;
360
361        Ok(tbs_buf)
362    }
363
364    /// Sign a `Message`
365    #[cfg(feature = "__dnssec")]
366    pub fn sign_message(
367        &self,
368        message: &Message,
369        current_time: u64,
370    ) -> ProtoResult<(Box<Record<TSIG>>, Option<TSigVerifier>)> {
371        debug!("signing message: {:?}", message);
372
373        let pre_tsig = TSIG::stub(message.id, current_time, self);
374        let signature = self
375            .sign(&message_tbs(message, &pre_tsig, &self.0.signer_name)?)
376            .map_err(|err| ProtoError::from(err.to_string()))?;
377        let tsig = make_tsig_record(
378            self.0.signer_name.clone(),
379            pre_tsig.set_mac(signature.clone()),
380        );
381
382        let verifier = TSigVerifier {
383            signer: self.clone(),
384            previous_signature: signature,
385            remote_time: 0,
386            request_time: current_time,
387        };
388
389        Ok((Box::new(tsig), Some(verifier)))
390    }
391}
392
393/// A verifier for TSIG-signed DNS responses.
394///
395/// This struct maintains the state necessary to verify a chain of TSIG-signed
396/// responses, tracking the previous MAC and response timestamps to ensure
397/// proper ordering and validation.
398#[cfg(feature = "__dnssec")]
399pub struct TSigVerifier {
400    signer: TSigner,
401    previous_signature: Vec<u8>,
402    remote_time: u64,
403    request_time: u64,
404}
405
406#[cfg(feature = "__dnssec")]
407impl TSigVerifier {
408    /// Verify a TSIG-signed DNS response.
409    ///
410    /// This method validates the TSIG signature on the response, checks that
411    /// the response timestamp is monotonically increasing (for chained responses),
412    /// and validates that the response time falls within the acceptable fudge window.
413    ///
414    /// # Arguments
415    ///
416    /// * `response_bytes` - The raw bytes of the DNS response message
417    ///
418    /// # Returns
419    ///
420    /// Returns the verified `DnsResponse` on success, or a `ProtoError` if
421    /// verification fails.
422    pub fn verify(&mut self, response_bytes: &[u8]) -> ProtoResult<DnsResponse> {
423        let (last_sig, rt, range) = self
424            .signer
425            .verify_message_byte(
426                response_bytes,
427                Some(&self.previous_signature),
428                self.remote_time == 0,
429            )
430            .map_err(|err| ProtoError::from(err.to_string()))?;
431
432        if rt >= self.remote_time && range.contains(&self.request_time) {
433            self.previous_signature = last_sig;
434            self.remote_time = rt;
435            DnsResponse::from_buffer(response_bytes.to_vec())
436        } else {
437            Err(ProtoError::from("tsig validation error: outdated response"))
438        }
439    }
440}
441
442#[cfg(test)]
443#[cfg(feature = "__dnssec")]
444mod tests {
445    #![allow(clippy::dbg_macro, clippy::print_stdout)]
446
447    use crate::op::{Message, Query};
448    use crate::rr::Name;
449    use crate::serialize::binary::BinEncodable;
450
451    use super::*;
452    fn assert_send_and_sync<T: Send + Sync>() {}
453
454    #[test]
455    fn test_send_and_sync() {
456        assert_send_and_sync::<TSigner>();
457    }
458
459    #[test]
460    fn test_sign_and_verify_message_tsig() {
461        let time_begin = 1609459200u64;
462        let fudge = 300u64;
463        let origin: Name = Name::parse("example.com.", None).unwrap();
464        let key_name: Name = Name::from_ascii("key_name.").unwrap();
465        let mut question = Message::query();
466        let mut query: Query = Query::new();
467        query.set_name(origin);
468        question.add_query(query);
469
470        let sig_key = b"some_key".to_vec();
471        let signer =
472            TSigner::new(sig_key, TsigAlgorithm::HmacSha512, key_name, fudge as u16).unwrap();
473
474        assert!(question.signature().is_none());
475        question
476            .finalize(&signer, time_begin)
477            .expect("should have signed");
478        assert!(question.signature().is_some());
479
480        let (_, _, validity_range) = signer
481            .verify_message_byte(&question.to_bytes().unwrap(), None, true)
482            .unwrap();
483        assert!(validity_range.contains(&(time_begin + fudge / 2))); // slightly outdated, but still to be acceptable
484        assert!(validity_range.contains(&(time_begin - fudge / 2))); // sooner than our time, but still acceptable
485        assert!(!validity_range.contains(&(time_begin + fudge * 2))); // too late to be accepted
486        assert!(!validity_range.contains(&(time_begin - fudge * 2))); // too soon to be accepted
487    }
488
489    // make rejection tests shorter by centralizing common setup code
490    fn get_message_and_signer() -> (Message, TSigner) {
491        let time_begin = 1609459200u64;
492        let fudge = 300u64;
493        let origin: Name = Name::parse("example.com.", None).unwrap();
494        let key_name: Name = Name::from_ascii("key_name.").unwrap();
495        let mut question = Message::query();
496        let mut query: Query = Query::new();
497        query.set_name(origin);
498        question.add_query(query);
499
500        let sig_key = b"some_key".to_vec();
501        let signer =
502            TSigner::new(sig_key, TsigAlgorithm::HmacSha512, key_name, fudge as u16).unwrap();
503
504        assert!(question.signature().is_none());
505        question
506            .finalize(&signer, time_begin)
507            .expect("should have signed");
508        assert!(question.signature().is_some());
509
510        // this should be ok, it has not been tampered with
511        assert!(
512            signer
513                .verify_message_byte(&question.to_bytes().unwrap(), None, true)
514                .is_ok()
515        );
516
517        (question, signer)
518    }
519
520    #[test]
521    fn test_sign_and_verify_message_tsig_reject_keyname() {
522        let (mut question, signer) = get_message_and_signer();
523
524        let other_name = Name::from_ascii("other_name.").unwrap();
525        let Some(mut signature) = question.take_signature() else {
526            panic!("should have TSIG signed");
527        };
528        signature.name = other_name;
529        question.set_signature(signature);
530
531        assert!(
532            signer
533                .verify_message_byte(&question.to_bytes().unwrap(), None, true)
534                .is_err()
535        );
536    }
537
538    #[test]
539    fn test_sign_and_verify_message_tsig_reject_invalid_mac() {
540        let (mut question, signer) = get_message_and_signer();
541
542        let mut query: Query = Query::new();
543        let origin: Name = Name::parse("example.net.", None).unwrap();
544        query.set_name(origin);
545        question.add_query(query);
546
547        assert!(
548            signer
549                .verify_message_byte(&question.to_bytes().unwrap(), None, true)
550                .is_err()
551        );
552    }
553}