diff --git a/examples/server-transports.rs b/examples/server-transports.rs index 1a4c03136..16bcff007 100644 --- a/examples/server-transports.rs +++ b/examples/server-transports.rs @@ -490,7 +490,7 @@ impl StatsMiddlewareSvc { fn postprocess( request: &Request, response: &AdditionalBuilder>, - stats: Arc>, + stats: &RwLock, ) where RequestOctets: Octets + Send + Sync + Unpin, Svc: Service, @@ -512,7 +512,7 @@ impl StatsMiddlewareSvc { fn map_stream_item( request: Request, stream_item: ServiceResult, - stats: Arc>, + stats: &mut Arc>, ) -> ServiceResult where RequestOctets: Octets + Send + Sync + Unpin, diff --git a/src/net/client/dgram.rs b/src/net/client/dgram.rs index 6695a5492..e771fc372 100644 --- a/src/net/client/dgram.rs +++ b/src/net/client/dgram.rs @@ -322,7 +322,7 @@ where S: AsyncConnect + Clone + Send + Sync + 'static, S::Connection: AsyncDgramRecv + AsyncDgramSend + Send + Sync + Unpin + 'static, - Req: ComposeRequest + Clone + Send + Sync + 'static, + Req: ComposeRequest + Send + Sync + 'static, { fn send_request( &self, diff --git a/src/net/server/message.rs b/src/net/server/message.rs index 84889a45b..1ac11e3b1 100644 --- a/src/net/server/message.rs +++ b/src/net/server/message.rs @@ -192,7 +192,7 @@ where impl Request where - Octs: AsRef<[u8]> + Send + Sync + Unpin, + Octs: AsRef<[u8]> + Send + Sync, { /// Creates a new request wrapper around a message along with its context. pub fn new( @@ -270,7 +270,7 @@ where impl Clone for Request where - Octs: AsRef<[u8]> + Send + Sync + Unpin, + Octs: AsRef<[u8]> + Send + Sync, Metadata: Clone, { fn clone(&self) -> Self { diff --git a/src/net/server/middleware/edns.rs b/src/net/server/middleware/edns.rs index fea9fba30..ea6b4dbfd 100644 --- a/src/net/server/middleware/edns.rs +++ b/src/net/server/middleware/edns.rs @@ -325,7 +325,7 @@ where fn map_stream_item( request: Request, mut stream_item: ServiceResult, - _pp_meta: (), + _pp_meta: &mut (), ) -> ServiceResult { if let Ok(cr) = &mut stream_item { if let Some(response) = cr.response_mut() { diff --git a/src/net/server/middleware/mandatory.rs b/src/net/server/middleware/mandatory.rs index aebd9dcc8..21ff82762 100644 --- a/src/net/server/middleware/mandatory.rs +++ b/src/net/server/middleware/mandatory.rs @@ -268,11 +268,11 @@ where fn map_stream_item( request: Request, mut stream_item: ServiceResult, - strict: bool, + strict: &mut bool, ) -> ServiceResult { if let Ok(cr) = &mut stream_item { if let Some(response) = cr.response_mut() { - Self::postprocess(&request, response, strict); + Self::postprocess(&request, response, *strict); } } stream_item diff --git a/src/net/server/middleware/mod.rs b/src/net/server/middleware/mod.rs index b364a83ba..52f172bcc 100644 --- a/src/net/server/middleware/mod.rs +++ b/src/net/server/middleware/mod.rs @@ -12,17 +12,13 @@ //! post-processing the resulting responses and propagating them back down //! through the layers to the server. //! -//! Currently the following middleware are available: +//! If needed middleware services can pass service specific data to upstream +//! services for consumption, via the `RequestMeta` custom data support of +//! the [`Service`] trait. An example of this can be seen in the +//! [`TsigMiddlewareSvc`][tsig::TsigMiddlewareSvc]. //! -//! - [`MandatoryMiddlewareSvc`]: Core DNS RFC standards based message -//! processing for MUST requirements. -//! - [`EdnsMiddlewareSvc`]: RFC 6891 and related EDNS message processing. -//! - [`CookiesMiddlewareSvc`]: RFC 7873 DNS Cookies related message -//! processing. +//! Currently the following middleware are available: //! -//! [`MandatoryMiddlewareSvc`]: mandatory::MandatoryMiddlewareSvc -//! [`EdnsMiddlewareSvc`]: edns::EdnsMiddlewareSvc -//! [`CookiesMiddlewareSvc`]: cookies::CookiesMiddlewareSvc //! [`Service`]: crate::net::server::service::Service #[cfg(feature = "siphasher")] @@ -30,3 +26,5 @@ pub mod cookies; pub mod edns; pub mod mandatory; pub mod stream; +#[cfg(feature = "tsig")] +pub mod tsig; diff --git a/src/net/server/middleware/stream.rs b/src/net/server/middleware/stream.rs index f16ce3cd5..2d62b1f3e 100644 --- a/src/net/server/middleware/stream.rs +++ b/src/net/server/middleware/stream.rs @@ -110,7 +110,7 @@ type PostprocessingStreamCallback< > = fn( Request, StreamItem, - PostProcessingMeta, + &mut PostProcessingMeta, ) -> StreamItem; //------------ PostprocessingStream ------------------------------------------ @@ -153,7 +153,7 @@ where pub fn new( svc_call_fut: Future, request: Request, - metadata: PostProcessingMeta, + pp_meta: PostProcessingMeta, cb: PostprocessingStreamCallback< RequestOctets, Stream::Item, @@ -165,7 +165,7 @@ where state: PostprocessingStreamState::Pending(svc_call_fut), request, cb, - pp_meta: metadata, + pp_meta, } } } @@ -187,7 +187,6 @@ where Stream: futures_util::stream::Stream + Unpin, Self: Unpin, RequestMeta: Clone, - PostProcessingMeta: Clone, { type Item = Stream::Item; @@ -206,9 +205,8 @@ where let stream_item = ready!(stream.poll_next_unpin(cx)); trace!("Stream item retrieved, mapping to downstream type"); let request = self.request.clone(); - let pp_meta = self.pp_meta.clone(); - let map = - stream_item.map(|item| (self.cb)(request, item, pp_meta)); + let map = stream_item + .map(|item| (self.cb)(request, item, &mut self.pp_meta)); Poll::Ready(map) } } diff --git a/src/net/server/middleware/tsig.rs b/src/net/server/middleware/tsig.rs new file mode 100644 index 000000000..759328965 --- /dev/null +++ b/src/net/server/middleware/tsig.rs @@ -0,0 +1,506 @@ +//! RFC 8495 TSIG message authentication middleware. +//! +//! This module provides a TSIG request validation and response signing +//! middleware service. The underlying TSIG RR processing is implemented using +//! the [`rdata::tsig`][crate::rdata::tsig] module. +//! +//! Signed requests that fail signature verification will be rejected. +//! +//! Unsigned requests and correctly signed requests will pass through this +//! middleware unchanged. +//! +//! For requests which were correctly signed the corresponding response(s) +//! will be signed using the same key as the request. +//! +//! # Determining the key that a request was signed with +//! +//! The key that signed a request is output by this middleware via the request +//! metadata in the form [`Option`], where `KS` denotes the type of +//! [`KeyStore`] that was used to construct this middleware. Upstream services +//! can choose to ignore the metadata by being generic over any kind of +//! metadata, or may offer a [`Service`] impl that specifically accepts the +//! `Option` metadata type, enabling the upstream service to use +//! the request metadata to determine the key that the request was signed +//! with. +//! +//! # Limitations +//! +//! * RFC 8945 5.2.3 Time Check and Error Handling states: _"The server SHOULD +//! also cache the most recent Time Signed value in a message generated by a +//! key and SHOULD return BADTIME if a message received later has an earlier +//! Time Signed value."_. This is not implemented. + +use core::convert::Infallible; +use core::future::{ready, Ready}; +use core::marker::PhantomData; +use core::ops::ControlFlow; + +use std::vec::Vec; + +use octseq::{Octets, OctetsFrom}; +use tracing::{error, trace, warn}; + +use crate::base::iana::Rcode; +use crate::base::message_builder::AdditionalBuilder; +use crate::base::wire::Composer; +use crate::base::{Message, StreamTarget}; +use crate::net::server::message::Request; +use crate::net::server::service::{ + CallResult, Service, ServiceError, ServiceFeedback, ServiceResult, +}; +use crate::net::server::util::mk_builder_for_target; +use crate::rdata::tsig::Time48; +use crate::tsig::{self, KeyStore, ServerSequence, ServerTransaction}; + +use super::stream::{MiddlewareStream, PostprocessingStream}; +use futures_util::stream::{once, Once}; +use futures_util::Stream; + +//------------ TsigMiddlewareSvc ---------------------------------------------- + +/// RFC 8495 TSIG message authentication middleware. +/// +/// This middleware service validates TSIG signatures on incoming requests, if +/// any, and adds TSIG signatures to responses to signed requests. +/// +/// Upstream services can detect whether a request is signed and with which +/// key by consuming the `Option` metadata output by this service. +#[derive(Clone, Debug)] +pub struct TsigMiddlewareSvc +where + KS: Clone + KeyStore, +{ + next_svc: NextSvc, + + key_store: KS, + + _phantom: PhantomData, +} + +impl TsigMiddlewareSvc +where + KS: Clone + KeyStore, +{ + /// Creates an instance of this middleware service. + /// + /// Keys in the provided [`KeyStore`] will be used to verify received signed + /// requests and to sign the corresponding responses. + #[must_use] + pub fn new(next_svc: NextSvc, key_store: KS) -> Self { + Self { + next_svc, + key_store, + _phantom: PhantomData, + } + } +} + +impl TsigMiddlewareSvc +where + RequestOctets: Octets + OctetsFrom> + Send + Sync + Unpin, + NextSvc: Service>, + NextSvc::Target: Composer + Default, + KS: Clone + KeyStore, + KS::Key: Clone, + Infallible: From<>>::Error>, +{ + #[allow(clippy::type_complexity)] + fn preprocess( + req: &Request, + key_store: &KS, + ) -> Result< + ControlFlow< + AdditionalBuilder>, + Option<( + Request>, + TsigSigner, + )>, + >, + ServiceError, + > { + let octets = req.message().as_slice().to_vec(); + let mut mut_msg = Message::from_octets(octets).unwrap(); + + match tsig::ServerTransaction::request( + key_store, + &mut mut_msg, + Time48::now(), + ) { + Ok(None) => { + // Message is not TSIG signed. + } + + Ok(Some(tsig)) => { + // Message is TSIG signed by a known key. + trace!( + "Request is signed with TSIG key '{}'", + tsig.key().name() + ); + + // Convert to RequestOctets so that the non-TSIG signed + // message case can just pass through the RequestOctets. + let source = mut_msg.into_octets(); + let octets = RequestOctets::octets_from(source); + let new_msg = Message::from_octets(octets).unwrap(); + + let mut new_req = Request::new( + req.client_addr(), + req.received_at(), + new_msg, + req.transport_ctx().clone(), + Some(tsig.wrapped_key().clone()), + ); + + let num_bytes_to_reserve = tsig.key().compose_len(); + new_req.reserve_bytes(num_bytes_to_reserve); + + return Ok(ControlFlow::Continue(Some(( + new_req, + TsigSigner::Transaction(tsig), + )))); + } + + Err(err) => { + // Message is incorrectly signed or signed with an unknown key. + warn!( + "{} from {} refused: {err}", + req.message().header().opcode(), + req.client_addr(), + ); + + let builder = mk_builder_for_target(); + + let res = match err.build_message(req.message(), builder) { + Ok(additional) => Ok(ControlFlow::Break(additional)), + Err(err) => { + error!("Unable to build TSIG error response: {err}"); + Err(ServiceError::InternalError) + } + }; + + return res; + } + } + + Ok(ControlFlow::Continue(None)) + } + + /// Sign the given response, or if necessary construct and return an + /// alternate response. + fn postprocess( + request: &Request, + response: &mut AdditionalBuilder>, + state: &mut PostprocessingState, + ) -> Result< + Option>>, + ServiceError, + > { + // Remove the limit we should have imposed during pre-processing so + // that we can use the space we reserved for the OPT RR. + response.clear_push_limit(); + + let truncation_ctx; + + let res = match &mut state.signer { + Some(TsigSigner::Transaction(_)) => { + // Extract the single response signer and consume it in the + // signing process. + let Some(TsigSigner::Transaction(signer)) = + state.signer.take() + else { + unreachable!() + }; + + trace!( + "Signing single response with TSIG key '{}'", + signer.key().name() + ); + + // We have to clone the key here in case the signer produces + // an error, otherwise we lose access to the key as the signer + // is consumed by calling answer(). The caller has control + // over the key type via KS::Key so if cloning cost is a + // problem the caller can choose to wrap the key in an Arc or + // such to reduce the cloning cost. + truncation_ctx = TruncationContext::NoSignerOnlyTheKey( + signer.key().clone(), + ); + + signer.answer(response, Time48::now()) + } + + Some(TsigSigner::Sequence(ref mut signer)) => { + // Use the multi-response signer to sign the response. + trace!( + "Signing response stream with TSIG key '{}'", + signer.key().name() + ); + + let res = signer.answer(response, Time48::now()); + + truncation_ctx = TruncationContext::HaveSigner(signer); + + res + } + + None => { + // Nothing to do as unsigned requests don't require response + // signing. + return Ok(None); + } + }; + + // Handle signing failure due to push error, i.e. there wasn't enough + // space in the response to add the TSIG RR. This shouldn't happen + // because we reserve space in preprocess() for the TSIG RR that we + // add when signing. + if res.is_err() { + // 5.3. Generation of TSIG on Answers + // "If addition of the TSIG record will cause the message to be + // truncated, the server MUST alter the response so that a TSIG + // can be included. This response contains only the question and + // a TSIG record, has the TC bit set, and has an RCODE of 0 + // (NOERROR). At this point, the client SHOULD retry the request + // using TCP (as per Section 4.2.2 of [RFC1035])." + Ok(Some(Self::mk_signed_truncated_response( + request, + truncation_ctx, + )?)) + } else { + Ok(None) + } + } + + fn mk_signed_truncated_response( + request: &Request, + truncation_ctx: TruncationContext, + ) -> Result>, ServiceError> + { + let builder = mk_builder_for_target(); + let mut new_response = builder + .start_answer(request.message(), Rcode::NOERROR) + .unwrap(); + new_response.header_mut().set_tc(true); + let mut additional = new_response.additional(); + + match truncation_ctx { + TruncationContext::HaveSigner(signer) => { + if let Err(err) = + signer.answer(&mut additional, Time48::now()) + { + error!("Unable to sign truncated TSIG response: {err}"); + Err(ServiceError::InternalError) + } else { + Ok(additional) + } + } + + TruncationContext::NoSignerOnlyTheKey(key) => { + // We can't use the TSIG signer state we just had as that was + // consumed in the failed attempt to sign the answer, so we + // have to create a new TSIG state in order to sign the + // truncated response. + let octets = request.message().as_slice().to_vec(); + let mut mut_msg = Message::from_octets(octets).unwrap(); + + match ServerTransaction::request( + &key, + &mut mut_msg, + Time48::now(), + ) { + Ok(None) => { + error!("Unable to create signer for truncated TSIG response: internal error: request is not signed but was expected to be"); + Err(ServiceError::InternalError) + } + + Err(err) => { + error!("Unable to create signer for truncated TSIG response: {err}"); + Err(ServiceError::InternalError) + } + + Ok(Some(signer)) => { + if let Err(err) = + signer.answer(&mut additional, Time48::now()) + { + error!("Unable to sign truncated TSIG response: {err}"); + Err(ServiceError::InternalError) + } else { + Ok(additional) + } + } + } + } + } + } + + fn map_stream_item( + request: Request, + stream_item: ServiceResult, + pp_config: &mut PostprocessingState, + ) -> ServiceResult { + if let Ok(mut call_res) = stream_item { + if matches!( + call_res.feedback(), + Some(ServiceFeedback::BeginTransaction) + ) { + // Does it need converting from the variant that supports + // single messages only (ServerTransaction) to the variant + // that supports signing multiple messages (ServerSequence)? + // Note: Confusingly BeginTransaction and ServerTransaction + // use the term "transaction" to mean completely the opposite + // of each other. With BeginTransaction we mean that the + // caller should expect a sequence of response messages + // instead of the usual single response message. With + // ServerTransaction the TSIG code means handling of single + // messages only and NOT sequences for which there is a + // separate ServerSequence type. Sigh. + if let Some(TsigSigner::Transaction(tsig_txn)) = + pp_config.signer.take() + { + // Do the conversion and store the result for future + // invocations of this function for subsequent items + // in the response stream. + pp_config.signer = Some(TsigSigner::Sequence( + ServerSequence::from(tsig_txn), + )); + } + } + + if let Some(response) = call_res.response_mut() { + if let Some(new_response) = + Self::postprocess(&request, response, pp_config)? + { + *response = new_response; + } + } + + Ok(call_res) + } else { + stream_item + } + } +} + +//--- Service + +/// This [`Service`] implementation specifies that the upstream service will +/// be passed metadata of type [`Option`]. The upstream service can +/// optionally use this to learn which TSIG key signed the request. +/// +/// This service does not accept downstream metadata, explicitly restricting +/// what it accepts to `()`. This is because (a) the service should be the +/// first layer above the network server, or as near as possible, such that it +/// receives unmodified requests and that the responses it generates are sent +/// over the network without prior modification, and thus it is not very +/// likely that the is a downstream layer that has metadata to supply to us, +/// and (b) because this service does not propagate the metadata it receives +/// from downstream but instead outputs [`Option`] metadata to +/// upstream services. +impl Service + for TsigMiddlewareSvc +where + RequestOctets: + Octets + OctetsFrom> + Send + Sync + 'static + Unpin, + NextSvc: Service>, + NextSvc::Future: Unpin, + NextSvc::Target: Composer + Default, + KS: Clone + KeyStore + Unpin, + KS::Key: Clone + Unpin, + Infallible: From<>>::Error>, +{ + type Target = NextSvc::Target; + type Stream = MiddlewareStream< + NextSvc::Future, + NextSvc::Stream, + PostprocessingStream< + RequestOctets, + NextSvc::Future, + NextSvc::Stream, + (), + PostprocessingState, + >, + Once>>, + ::Item, + >; + type Future = Ready; + + fn call(&self, request: Request) -> Self::Future { + match Self::preprocess(&request, &self.key_store) { + Ok(ControlFlow::Continue(Some((modified_req, signer)))) => { + let pp_config = PostprocessingState::new(signer); + + let svc_call_fut = self.next_svc.call(modified_req); + + let map = PostprocessingStream::new( + svc_call_fut, + request, + pp_config, + Self::map_stream_item, + ); + + ready(MiddlewareStream::Map(map)) + } + + Ok(ControlFlow::Continue(None)) => { + let request = request.with_new_metadata(None); + let svc_call_fut = self.next_svc.call(request); + ready(MiddlewareStream::IdentityFuture(svc_call_fut)) + } + + Ok(ControlFlow::Break(additional)) => { + ready(MiddlewareStream::Result(once(ready(Ok( + CallResult::new(additional), + ))))) + } + + Err(err) => { + ready(MiddlewareStream::Result(once(ready(Err(err))))) + } + } + } +} + +/// Data needed to do signing during response post-processing. + +pub struct PostprocessingState { + /// The signer used to verify the request. + /// + /// Needed to sign responses. + /// + /// We store it as an Option because ServerTransaction::answer() consumes + /// the signer so have to first take it out of this struct, as a reference + /// is held to the struct so it iself cannot be consumed. + signer: Option>, +} + +impl PostprocessingState { + fn new(signer: TsigSigner) -> Self { + Self { + signer: Some(signer), + } + } +} + +/// A wrapper around [`ServerTransaction`] and [`ServerSequence`]. +/// +/// This wrapper allows us to write calling code once that invokes methods on +/// the TSIG signer/validator which have the same name and purpose for single +/// response vs multiple response streams, yet have distinct Rust types and so +/// must be called on the correct type, without needing to know at the call +/// site which of the distinct types it actually is. +#[derive(Clone, Debug)] +enum TsigSigner { + /// A [`ServerTransaction`] for signing a single response. + Transaction(ServerTransaction), + + /// A [`ServerSequence`] for signing multiple responses. + Sequence(ServerSequence), +} + +//------------ TruncationContext ---------------------------------------------- + +enum TruncationContext<'a, KSeq, KTxn> { + HaveSigner(&'a mut tsig::ServerSequence), + + NoSignerOnlyTheKey(KTxn), +} diff --git a/src/net/server/service.rs b/src/net/server/service.rs index 99f9f6b48..4c7993248 100644 --- a/src/net/server/service.rs +++ b/src/net/server/service.rs @@ -153,14 +153,8 @@ pub type ServiceResult = Result, ServiceError>; /// /// For more advanced cases you may need to override these defaults. /// -/// - `RequestMeta`: If implementing a [middleware] `Service` you may need to -/// supply your own `RequestMeta` type. `RequestMeta` is intended to enable -/// middleware `Service` impls to express strongly typed support for -/// middleware specific data that can be consumed by upstream middleware, or -/// even by your application service. For example a middleware `Service` may -/// detect that the request is signed using a particular key and communicate -/// the name of the key to any upstream `Service` that needs to know the -/// name of the key used to sign the request. +/// - `RequestMeta`: Use this to pass additional custom data to your service. +/// [Middleware] services use this to pass data to the next layer. /// /// - `RequestOctets`: By specifying your own `RequestOctets` type you can use /// a type other than `Vec` to transport request bytes through your diff --git a/src/net/server/tests/integration.rs b/src/net/server/tests/integration.rs index 45106ab39..a215eb954 100644 --- a/src/net/server/tests/integration.rs +++ b/src/net/server/tests/integration.rs @@ -1,5 +1,7 @@ +use core::str::FromStr; + use std::boxed::Box; -use std::collections::VecDeque; +use std::collections::{HashMap, VecDeque}; use std::fs::File; use std::net::SocketAddr; use std::path::PathBuf; @@ -8,6 +10,7 @@ use std::sync::Arc; use std::time::Duration; use std::vec::Vec; +use ring::test::rand::FixedByteRandom; use rstest::rstest; use tracing::instrument; use tracing::{trace, warn}; @@ -18,7 +21,7 @@ use crate::base::net::IpAddr; use crate::base::wire::Composer; use crate::base::Rtype; use crate::net::client::request::{RequestMessage, RequestMessageMulti}; -use crate::net::client::{dgram, stream}; +use crate::net::client::{dgram, stream, tsig}; use crate::net::server; use crate::net::server::buf::VecBufSource; use crate::net::server::dgram::DgramServer; @@ -36,6 +39,7 @@ use crate::stelline::client::{ }; use crate::stelline::parse_stelline::{self, parse_file, Config, Matches}; use crate::stelline::simple_dgram_client; +use crate::tsig::{Algorithm, Key, KeyName, KeyStore}; use crate::utils::base16; use crate::zonefile::inplace::{Entry, ScannedRecord, Zonefile}; @@ -59,6 +63,8 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) { // Initialize tracing based logging. Override with env var RUST_LOG, e.g. // RUST_LOG=trace. DEBUG level will show the .rpl file name, Stelline step // numbers and types as they are being executed. + + use crate::net::server::middleware::tsig::TsigMiddlewareSvc; tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .with_thread_ids(true) @@ -66,11 +72,27 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) { .try_init() .ok(); + // Load the test .rpl file that determines which queries will be sent + // and which responses will be expected, and how the server that + // answers them should be configured. let file = File::open(&rpl_file).unwrap(); let stelline = parse_file(&file, rpl_file.to_str().unwrap()); let server_config = parse_server_config(&stelline.config); - // Create a service to answer queries received by the DNS servers. + // Create a TSIG key store containing a 'TESTKEY' + let mut key_store = TestKeyStore::new(); + let key_name = KeyName::from_str("TESTKEY").unwrap(); + let rng = FixedByteRandom { byte: 0u8 }; + let (key, _) = + Key::generate(Algorithm::Sha256, &rng, key_name.clone(), None, None) + .unwrap(); + key_store.insert((key_name, Algorithm::Sha256), key.into()); + let key_store = Arc::new(key_store); + + // Create a connection factory. + let dgram_server_conn = ClientServerChannel::new_dgram(); + let stream_server_conn = ClientServerChannel::new_stream(); + let zonefile = server_config.zonefile.clone(); let with_cookies = server_config.cookies.enabled @@ -105,12 +127,25 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) { // 4. Mandatory DNS behaviour (e.g. RFC 1034/35 rules). let svc = MandatoryMiddlewareSvc::new(svc); + // 5. TSIG message authentication. + let svc = TsigMiddlewareSvc::new(svc, key_store.clone()); + + // NOTE: TSIG middleware *MUST* be the first middleware in the chain per + // RFC 8945 as it has to see incoming messages prior to any modification + // in order to verify the signature, and has to sign outgoing messages in + // their final state without any modification occuring thereafter. + // Create dgram and stream servers for answering requests - let (dgram_srv, dgram_conn, stream_srv, stream_conn) = - mk_servers(svc, &server_config); + let (dgram_srv, stream_srv) = mk_servers( + svc, + &server_config, + dgram_server_conn.clone(), + stream_server_conn.clone(), + ); // Create a client factory for sending requests - let client_factory = mk_client_factory(dgram_conn, stream_conn); + let client_factory = + mk_client_factory(dgram_server_conn, stream_server_conn, key_store); // Run the Stelline test! let step_value = Arc::new(CurrStepValue::new()); @@ -132,11 +167,11 @@ async fn server_tests(#[files("test-data/server/*.rpl")] rpl_file: PathBuf) { fn mk_servers( service: Svc, server_config: &ServerConfig, + dgram_server_conn: ClientServerChannel, + stream_server_conn: ClientServerChannel, ) -> ( Arc>, - ClientServerChannel, Arc>, - ClientServerChannel, ) where Svc: Clone + Service + Send + Sync, @@ -149,8 +184,7 @@ where let (dgram_config, stream_config) = mk_server_configs(server_config); // Create a dgram server for handling UDP requests. - let dgram_server_conn = ClientServerChannel::new_dgram(); - let dgram_server = DgramServer::with_config( + let dgram_server = DgramServer::<_, _, Svc>::with_config( dgram_server_conn.clone(), VecBufSource, service.clone(), @@ -162,7 +196,6 @@ where // Create a stream server for handling TCP requests, i.e. Stelline queries // with "MATCH TCP". - let stream_server_conn = ClientServerChannel::new_stream(); let stream_server = StreamServer::with_config( stream_server_conn.clone(), VecBufSource, @@ -173,17 +206,13 @@ where let cloned_stream_server = stream_server.clone(); tokio::spawn(async move { cloned_stream_server.run().await }); - ( - dgram_server, - dgram_server_conn, - stream_server, - stream_server_conn, - ) + (dgram_server, stream_server) } fn mk_client_factory( dgram_server_conn: ClientServerChannel, stream_server_conn: ClientServerChannel, + key_store: Arc, ) -> impl ClientFactory { // Create a TCP client factory that only creates a client if (a) no // existing TCP client exists for the source address of the Stelline @@ -193,23 +222,56 @@ fn mk_client_factory( matches!(entry.matches, Some(Matches { tcp: true, .. })) }; + let tcp_key_store = key_store.clone(); let tcp_client_factory = PerClientAddressClientFactory::new( move |source_addr, entry| { let stream = stream_server_conn .connect(Some(SocketAddr::new(*source_addr, 0))); - let (conn, transport) = stream::Connection::< - RequestMessage>, - RequestMessageMulti>, - >::new(stream); - tokio::spawn(transport.run()); - if let Some(sections) = &entry.sections { - if let Some(q) = sections.question.first() { - if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { - return Client::Multi(Box::new(conn)); + + let key = entry.key_name.as_ref().and_then(|key_name| { + tcp_key_store.get_key(&key_name, Algorithm::Sha256) + }); + + if let Some(key) = key { + let (conn, transport) = stream::Connection::< + tsig::RequestMessage>, Arc>, + tsig::RequestMessage< + RequestMessageMulti>, + Arc, + >, + >::new(stream); + + tokio::spawn(transport.run()); + + let conn = Box::new(tsig::Connection::new(key, conn)); + + if let Some(sections) = &entry.sections { + if let Some(q) = sections.question.first() { + if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { + return Client::Multi(conn); + } } } + Client::Single(conn) + } else { + let (conn, transport) = stream::Connection::< + RequestMessage>, + RequestMessageMulti>, + >::new(stream); + + tokio::spawn(transport.run()); + + let conn = Box::new(conn); + + if let Some(sections) = &entry.sections { + if let Some(q) = sections.question.first() { + if matches!(q.qtype(), Rtype::AXFR | Rtype::IXFR) { + return Client::Multi(conn); + } + } + } + Client::Single(conn) } - Client::Single(Box::new(conn)) }, only_for_tcp_queries, ); @@ -224,12 +286,33 @@ fn mk_client_factory( let connect = dgram_server_conn .new_client(Some(SocketAddr::new(*source_addr, 0))); - match entry.matches.as_ref().map(|v| v.mock_client) { - Some(true) => Client::Single(Box::new( - simple_dgram_client::Connection::new(connect), - )), - _ => { - Client::Single(Box::new(dgram::Connection::new(connect))) + let key = entry.key_name.as_ref().and_then(|key_name| { + key_store.get_key(&key_name, Algorithm::Sha256) + }); + + if let Some(key) = key { + match entry.matches.as_ref().map(|v| v.mock_client) { + Some(true) => { + Client::Single(Box::new(tsig::Connection::new( + key, + simple_dgram_client::Connection::new(connect), + ))) + } + + _ => Client::Single(Box::new(tsig::Connection::new( + key, + dgram::Connection::new(connect), + ))), + } + } else { + match entry.matches.as_ref().map(|v| v.mock_client) { + Some(true) => Client::Single(Box::new( + simple_dgram_client::Connection::new(connect), + )), + + _ => Client::Single(Box::new(dgram::Connection::new( + connect, + ))), } } }, @@ -274,8 +357,8 @@ fn mk_server_configs( // - Controlling the content of the `Zonefile` passed to instances of this // `Service` impl. #[allow(clippy::type_complexity)] -fn test_service( - request: Request>, +fn test_service( + request: Request, RequestMeta>, zonefile: Zonefile, ) -> ServiceResult> { fn as_record_and_dname( @@ -324,9 +407,6 @@ fn test_service( let mut answer = mk_builder_for_target() .start_answer(request.message(), Rcode::NOERROR) .unwrap(); - // As we serve all answers from our own zones we are the - // authority for the domain in question. - answer.header_mut().set_aa(true); answer.push(record).unwrap(); answer }, @@ -440,3 +520,24 @@ fn parse_server_config(config: &Config) -> ServerConfig { parsed_config } + +//------------ TestKeyStore --------------------------------------------------- + +// KeyStore is impl'd elsewhere for HashMap<(KeyName, Algorithm), K, S>. +type TestKeyStore = HashMap<(KeyName, Algorithm), Arc>; + +impl KeyStore for Arc { + type Key = Arc; + + fn get_key( + &self, + name: &N, + algorithm: Algorithm, + ) -> Option { + if let Ok(name) = name.try_to_name() { + self.get(&(name, algorithm)).cloned() + } else { + None + } + } +} diff --git a/src/rdata/tsig.rs b/src/rdata/tsig.rs index f0f4bcfc9..e9730fe6f 100644 --- a/src/rdata/tsig.rs +++ b/src/rdata/tsig.rs @@ -4,21 +4,26 @@ //! //! [RFC 2845]: https://tools.ietf.org/html/rfc2845 +use core::cmp::Ordering; +use core::{fmt, hash}; + +#[cfg(all(feature = "std", not(test)))] +use std::time::SystemTime; + +#[cfg(all(feature = "std", test))] +use mock_instant::thread_local::SystemTime; +use octseq::builder::OctetsBuilder; +use octseq::octets::{Octets, OctetsFrom, OctetsInto}; +use octseq::parse::Parser; + use crate::base::cmp::CanonicalOrd; use crate::base::iana::{Rtype, TsigRcode}; use crate::base::name::{FlattenInto, ParsedName, ToName}; use crate::base::rdata::{ - ComposeRecordData, LongRecordData, ParseRecordData, RecordData + ComposeRecordData, LongRecordData, ParseRecordData, RecordData, }; use crate::base::wire::{Compose, Composer, Parse, ParseError}; use crate::utils::base64; -use core::cmp::Ordering; -use core::{fmt, hash}; -use octseq::builder::OctetsBuilder; -use octseq::octets::{Octets, OctetsFrom, OctetsInto}; -use octseq::parse::Parser; -#[cfg(feature = "std")] -use std::time::SystemTime; //------------ Tsig ---------------------------------------------------------- diff --git a/src/tsig/mod.rs b/src/tsig/mod.rs index 2710f9691..d88362f76 100644 --- a/src/tsig/mod.rs +++ b/src/tsig/mod.rs @@ -103,7 +103,7 @@ pub type KeyName = Name>; /// [`new`]: #method.new /// [`min_mac_len`]: #method.min_mac_len /// [`signing_len`]: #method.signing_len -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Key { /// The key’s bits and algorithm. key: hmac::Key, @@ -261,6 +261,70 @@ impl Key { self.signing_len } + /// Returns the space needed for a TSIG RR for this key. + pub fn compose_len(&self) -> u16 { + // The length of the generated TSIG RR is governed by variable and + // fixed parts. The variable parts are the key name, the algorithm + // name and the MAC. Given the key we can work this out. + + // https://datatracker.ietf.org/doc/html/rfc8945#section-4.2 + // 4.2. TSIG Record Format + // "The fields of the TSIG RR are described below. All multi-octet + // integers in the record are sent in network byte order (see + // Section 2.3.2 of [RFC1035]). + // + // Field Description RFC 1035 Size + // ----------------------------------------------------------------- + // NAME: The name of the key used, in domain name (variable) + // syntax. [...] + // TYPE: This MUST be TSIG (250: Transaction two octets + // SIGnature). + // CLASS: This MUST be ANY. two octets + // TTL: This MUST be 0. 32-bit + // RDLENGTH: (variable) 16-bit + // RDATA: The RDATA for a TSIG RR consists of a (variable) + // number of fields, described below: + // + // 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 3 3 + // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // / Algorithm Name / + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | | + // | Time Signed +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | | Fudge | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | MAC Size | / + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ MAC / + // / / + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Original ID | Error | + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // | Other Len | / + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Other Data / + // / / + // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + // " + let rdata_len = self.algorithm().to_name().compose_len() // Algorithm Name + + 6 // Time Signed + + 2 // Fudge + + 2 // MAC Size + + self.signing_len() as u16 + + 2 // Original ID + + 2 // Error + + 2; // Other Len + //+ 0; // Other Data (assume a successful response) + + let rr_len = self.name().compose_len() + + 2 // TYPE + + 2 // CLASS + + 4 // TTL + + 2 // RDLENGTH + + rdata_len; + + rr_len + } + /// Checks whether the key in the record is this key. fn check_tsig( &self, @@ -305,7 +369,7 @@ impl Key { /// if that is required. /// /// The method fails if the TSIG record doesn’t fit into the message - /// anymore, in which case the builder is returned unharmed. + /// anymore, in which case the builder is left unmodified. fn complete_message( &self, message: &mut AdditionalBuilder, @@ -398,7 +462,7 @@ where //------------ ClientTransaction --------------------------------------------- -/// TSIG Client Transaction State. +/// TSIG Client transaction state. /// /// This types allows signing a DNS request with a given key and validate an /// answer received for it. @@ -425,14 +489,15 @@ impl> ClientTransaction { /// builder and a key. It signs the message with the key and adds the /// signature as a TSIG record to the message’s additional section. It /// also creates a transaction value that can later be used to validate - /// the response. It returns both the message and the transaction. + /// the response. It modifies the given message and returns the created + /// transaction. /// - /// The function can fail if the TSIG record doesn’t actually fit into - /// the message anymore. In this case, the function returns an error and - /// the untouched message. + /// The function can fail if the TSIG record doesn’t fit into the message. + /// In this case, the function returns an error and leave the given + /// message unmodified. /// - /// Unlike [`request_with_fudge`], this function uses the - /// recommended default value for _fudge:_ 300 seconds. + /// Unlike [`request_with_fudge`], this function uses the recommended + /// default value for _fudge:_ 300 seconds. /// /// [`request_with_fudge`]: #method.request_with_fudge pub fn request( @@ -586,8 +651,7 @@ impl> ServerTransaction { /// TSIG record must be the last record and returns it. /// /// If appending the TSIG record fails, which can only happen if there - /// isn’t enough space left, it returns the builder unchanged as the - /// error case. + /// isn’t enough space left, it returns an error. pub fn answer( self, message: &mut AdditionalBuilder, @@ -598,7 +662,7 @@ impl> ServerTransaction { /// Produces a signed answer with a given fudge. /// - /// This method is similar to [`answer`] but lets you explicitely state + /// This method is similar to [`answer`] but lets you explicitly state /// the `fudge`, i.e., the number of seconds the recipient’s clock is /// allowed to differ from your current time when checking the signature. /// The default, suggested by the RFC, is 300. @@ -625,6 +689,14 @@ impl> ServerTransaction { pub fn key(&self) -> &Key { self.context.key() } + + /// Returns a reference to the transaction's key. + /// + /// This is the same as [`Self::key`] but doesn't lose the original key + /// type information. + pub fn wrapped_key(&self) -> &K { + self.context.wrapped_key() + } } //------------ ClientSequence ------------------------------------------------ @@ -952,6 +1024,33 @@ impl> ServerSequence { pub fn key(&self) -> &Key { self.context.key() } + + /// Returns a reference to the transaction's key. + /// + /// This is the same as [`Self::key`] but doesn't lose the original key + /// type information. + pub fn wrapped_key(&self) -> &K { + self.context.wrapped_key() + } +} + +//--- From + +/// Convert an unused [`ServerTransaction`] to a [`ServerSequence`] +/// +/// If [`ServerTransaction::request()`] was used to verify a TSIG signed +/// request but then you need to sign multiple responses, +/// [`ServerTransaction`] will not suffice as it can only sign a single +/// response. To resolve this you can use this function to convert the +/// [`ServerTransaction`] to a [`ServerSequence`] which can be used to sign +/// multiple responses. +impl From> for ServerSequence { + fn from(txn: ServerTransaction) -> Self { + Self { + context: txn.context, + first: true, + } + } } //------------ SigningContext ------------------------------------------------ @@ -1161,11 +1260,19 @@ impl> SigningContext { } } - /// Returns a references to the key that was used to create the context. + /// Returns a reference to the key that was used to create the context. fn key(&self) -> &Key { self.key.as_ref() } + /// Returns a reference to the key that was used to create the context. + /// + /// This is the same as [`key`] but doesn't lose the original key type + /// information. + fn wrapped_key(&self) -> &K { + &self.key + } + /// Applies a signature to the signing context. /// /// The `data` argument must be the actual signature that has already been @@ -1404,8 +1511,8 @@ impl Variables { Class::ANY, 0, // The only reason creating TSIG record data can fail here is - // that the hmac is unreasonable large. Since we control its - // creation, panicing in this case is fine. + // that the hmac is unreasonably large. Since we control its + // creation, panicking in this case is fine. Tsig::new( key.algorithm().to_name(), self.time_signed, diff --git a/test-data/server/edns_downstream_cookies.rpl b/test-data/server/edns_downstream_cookies.rpl index a86bdf1e9..f20ed4096 100644 --- a/test-data/server/edns_downstream_cookies.rpl +++ b/test-data/server/edns_downstream_cookies.rpl @@ -44,7 +44,7 @@ ENTRY_END STEP 11 CHECK_ANSWER ENTRY_BEGIN MATCH all -REPLY QR RD AA NOERROR +REPLY QR RD NOERROR ; AA SECTION QUESTION test. IN TXT SECTION ANSWER @@ -118,7 +118,7 @@ ENTRY_END STEP 41 CHECK_ANSWER ENTRY_BEGIN MATCH all server_cookie -REPLY QR RD AA DO NOERROR +REPLY QR RD DO NOERROR ; AA SECTION QUESTION test. IN TXT SECTION ANSWER @@ -145,7 +145,7 @@ ENTRY_END STEP 51 CHECK_ANSWER ENTRY_BEGIN MATCH all server_cookie -REPLY QR RD AA DO NOERROR +REPLY QR RD DO NOERROR ; AA SECTION QUESTION test. IN TXT SECTION ANSWER @@ -174,7 +174,7 @@ ENTRY_END STEP 61 CHECK_ANSWER ENTRY_BEGIN MATCH all server_cookie -REPLY QR RD AA DO NOERROR +REPLY QR RD DO NOERROR ; AA SECTION QUESTION test. IN TXT SECTION ANSWER @@ -227,7 +227,7 @@ ENTRY_END STEP 81 CHECK_ANSWER ENTRY_BEGIN MATCH all server_cookie -REPLY QR RD AA DO NOERROR +REPLY QR RD DO NOERROR ; AA SECTION QUESTION test. IN TXT SECTION ANSWER diff --git a/test-data/server/tsig.rpl b/test-data/server/tsig.rpl new file mode 100644 index 000000000..61bf065c5 --- /dev/null +++ b/test-data/server/tsig.rpl @@ -0,0 +1,56 @@ +;------------ Server configuration -------------------------------------------- + +server: + ; Define an in-memory zone to be served by the server. + local-data: "example.com. 3600 IN SOA ns.example.com. hostmaster.example.com. 1 3600 900 86400 3600" + local-data: "example.com. 3600 IN NS ns.example.net." + local-data: "www.example.com. 3600 IN A 1.2.3.4" +CONFIG_END + +;------------ Test definition ------------------------------------------------ + +SCENARIO_BEGIN Test TSIG signed SOA query receives correctly signed reply. + +;--- Mock replies + +; None + +;--- Test steps + +; NOTE: See test-data/server/README.md regarding the effect of MOCK_CLIENT +; that is used here. +STEP 10 QUERY +ENTRY_BEGIN +MATCH UDP +MATCH MOCK_CLIENT +SECTION QUESTION + example.com. IN SOA +SECTION ADDITIONAL +; Stelline doesn't support parsing zone entries that use the ( multiline ) +; format, otherwise we could use that here. + TESTKEY 0 CLASS255 TYPE250 \# 61 0b686d61632d73686132353600 000000000000 012c 0020 a1c86ced1815d60903129a525a14494516895d99ea94bf0b5b04338126a4d625 0000 0000 0000 +; ^ Other Len +; ^ Error +; ^ Original ID +; ^ MAC +; ^ MAC Size +; ^ Fudge +; ^ Algorithm Name ^ Time Signed +; ^ RDATA byte length. +; ^ RFC 3597 CLASSNN TYPENN \# encoding of unknown DNS RR types. +; We use this so that we can define the RDATA using HEX bytes. +ENTRY_END + +STEP 11 CHECK_ANSWER +ENTRY_BEGIN +MATCH all +REPLY QR NOERROR +SECTION QUESTION + example.com. IN SOA +SECTION ANSWER + example.com. 3600 IN SOA ns.example.com. hostmaster.example.com. 1 3600 900 86400 3600 +SECTION ADDITIONAL + TESTKEY 0 CLASS255 TYPE250 \# 61 0b686d61632d73686132353600 000000000000 012c 0020 6d7f9c5a14c2b48d4a0549000af29808e5eb25f7a80c22a2b1c0cf2ef3929bcd 0000 0000 0000 +ENTRY_END + +SCENARIO_END \ No newline at end of file