actix_web_lab/
request_signature.rs

1use std::fmt;
2
3use actix_http::BoxedPayloadStream;
4use actix_web::{dev, web::Bytes, Error, FromRequest, HttpRequest};
5use async_trait::async_trait;
6use derive_more::Display;
7use futures_core::future::LocalBoxFuture;
8use futures_util::{FutureExt as _, StreamExt as _, TryFutureExt as _};
9use local_channel::mpsc;
10use tokio::try_join;
11use tracing::trace;
12
13/// Define a scheme for deriving and verifying some kind of signature from request parts.
14///
15/// There are 4 phases to calculating a signature while a request is being received:
16/// 1. [Initialize](Self::init): Construct the signature scheme type and perform any pre-body
17///   calculation steps with request head parts.
18/// 1. [Consume body](Self::consume_chunk): For each body chunk received, fold it to the signature
19///   calculation.
20/// 1. [Finalize](Self::finalize): Perform post-body calculation steps and finalize signature type.
21/// 1. [Verify](Self::verify): Check the _true signature_ against a _candidate signature_; for
22///   example, a header added by the client. This phase is optional.
23///
24/// You'll need to use the [`async-trait`](https://docs.rs/async-trait) when implementing. Annotate
25/// your implementations with `#[async_trait(?Send)]`.
26///
27/// # Bring Your Own Crypto
28/// It is up to the implementor to ensure that best security practices are being followed when
29/// implementing this trait, and in particular the `verify` method. There is no inherent preference
30/// for certain crypto ecosystems though many of the examples shown here will use types from
31/// [RustCrypto](https://github.com/RustCrypto).
32///
33/// # `RequestSignature` Extractor
34/// Types that implement this trait can be used with the [`RequestSignature`] extractor to
35/// declaratively derive the request signature alongside the desired body extractor.
36///
37/// # Examples
38/// This trait can be used to define:
39/// - API authentication schemes that requires a signature to be attached to the request, either
40///   with static keys or dynamic, per-user keys that are looked asynchronously from a database.
41/// - Request hashes derived from specific parts for cache lookups.
42///
43/// This example implementation does a simple HMAC calculation on the body using a static key.
44/// It does not implement verification.
45/// ```
46/// use actix_web::{web::Bytes, Error, HttpRequest};
47/// use actix_web_lab::extract::RequestSignatureScheme;
48/// use async_trait::async_trait;
49/// use hmac::{digest::CtOutput, Mac, SimpleHmac};
50/// use sha2::Sha256;
51///
52/// struct AbcApi {
53///     /// Running state.
54///     hmac: SimpleHmac<Sha256>,
55/// }
56///
57/// #[async_trait(?Send)]
58/// impl RequestSignatureScheme for AbcApi {
59///     /// The constant-time verifiable output of the HMAC type.
60///     type Signature = CtOutput<SimpleHmac<Sha256>>;
61///     type Error = Error;
62///
63///     async fn init(req: &HttpRequest) -> Result<Self, Self::Error> {
64///         // acquire HMAC signing key
65///         let key = req.app_data::<[u8; 32]>().unwrap();
66///
67///         // construct HMAC signer
68///         let hmac = SimpleHmac::new_from_slice(&key[..]).unwrap();
69///         Ok(AbcApi { hmac })
70///     }
71///
72///     async fn consume_chunk(
73///         &mut self,
74///         _req: &HttpRequest,
75///         chunk: Bytes,
76///     ) -> Result<(), Self::Error> {
77///         // digest body chunk
78///         self.hmac.update(&chunk);
79///         Ok(())
80///     }
81///
82///     async fn finalize(self, _req: &HttpRequest) -> Result<Self::Signature, Self::Error> {
83///         // construct signature type
84///         Ok(self.hmac.finalize())
85///     }
86/// }
87/// ```
88#[async_trait(?Send)]
89pub trait RequestSignatureScheme: Sized {
90    /// The signature type returned from [`finalize`](Self::finalize) and checked in
91    /// [`verify`](Self::verify).
92    ///
93    /// Ideally, this type has constant-time equality capabilities.
94    type Signature;
95
96    /// Error type used by all trait methods to signal missing precondition, processing errors, or
97    /// verification failures.
98    ///
99    /// Must be convertible to an error response; i.e., implements [`ResponseError`].
100    ///
101    /// [`ResponseError`]: https://docs.rs/actix-web/4/actix_web/trait.ResponseError.html
102    type Error: Into<Error>;
103
104    /// Initialize signature scheme for incoming request.
105    ///
106    /// Possible steps that should be included in `init` implementations:
107    /// - initialization of signature scheme type
108    /// - key lookup / initialization
109    /// - pre-body digest updates
110    async fn init(req: &HttpRequest) -> Result<Self, Self::Error>;
111
112    /// Fold received body chunk into signature.
113    ///
114    /// If processing the request body one chunk at a time is not equivalent to processing it all at
115    /// once, then the chunks will need to be added to a buffer.
116    async fn consume_chunk(&mut self, req: &HttpRequest, chunk: Bytes) -> Result<(), Self::Error>;
117
118    /// Finalize and output `Signature` type.
119    ///
120    /// Possible steps that should be included in `finalize` implementations:
121    /// - post-body digest updates
122    /// - signature finalization
123    async fn finalize(self, req: &HttpRequest) -> Result<Self::Signature, Self::Error>;
124
125    /// Verify _true signature_ against _candidate signature_.
126    ///
127    /// The _true signature_ is what has been calculated during request processing by the other
128    /// methods in this trait. The _candidate signature_ might be a signature provided by the client
129    /// in order to prove ownership of a key or some other known signature to validate against.
130    ///
131    /// Implementations should return `signature` if it is valid and return an error if it is not.
132    /// The default implementation does no checks and just returns `signature` as-is.
133    ///
134    /// # Security
135    /// To avoid timing attacks, equality checks should be constant-time; check the docs of your
136    /// chosen crypto library.
137    #[allow(unused_variables)]
138    #[inline]
139    fn verify(
140        signature: Self::Signature,
141        req: &HttpRequest,
142    ) -> Result<Self::Signature, Self::Error> {
143        Ok(signature)
144    }
145}
146
147/// Wraps an extractor and calculates a request signature hash alongside.
148///
149/// Warning: Currently, this will always take the body meaning that if a body extractor is used,
150/// this needs to wrap it or else it will not work.
151#[derive(Clone)]
152pub struct RequestSignature<T, S: RequestSignatureScheme> {
153    extractor: T,
154    signature: S::Signature,
155}
156
157impl<T, S: RequestSignatureScheme> RequestSignature<T, S> {
158    /// Returns tuple containing body type, and owned hash.
159    pub fn into_parts(self) -> (T, S::Signature) {
160        (self.extractor, self.signature)
161    }
162}
163
164/// Errors that can occur when extracting and processing request signatures.
165#[derive(Display)]
166#[non_exhaustive]
167pub enum RequestSignatureError<T, S>
168where
169    T: FromRequest,
170    T::Error: fmt::Debug + fmt::Display,
171
172    S: RequestSignatureScheme,
173    S::Error: fmt::Debug + fmt::Display,
174{
175    /// Inner extractor error.
176    #[display(fmt = "Inner extractor error: {_0}")]
177    Extractor(T::Error),
178
179    /// Signature calculation error.
180    #[display(fmt = "Signature calculation error: {_0}")]
181    Signature(S::Error),
182}
183
184impl<T, S> fmt::Debug for RequestSignatureError<T, S>
185where
186    T: FromRequest,
187    T::Error: fmt::Debug + fmt::Display,
188
189    S: RequestSignatureScheme,
190    S::Error: fmt::Debug + fmt::Display,
191{
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        match self {
194            Self::Extractor(err) => f
195                .debug_tuple("RequestSignatureError::Extractor")
196                .field(err)
197                .finish(),
198
199            Self::Signature(err) => f
200                .debug_tuple("RequestSignatureError::Signature")
201                .field(err)
202                .finish(),
203        }
204    }
205}
206
207impl<T, S> From<RequestSignatureError<T, S>> for actix_web::Error
208where
209    T: FromRequest,
210    T::Error: fmt::Debug + fmt::Display,
211
212    S: RequestSignatureScheme,
213    S::Error: fmt::Debug + fmt::Display,
214{
215    fn from(err: RequestSignatureError<T, S>) -> Self {
216        match err {
217            RequestSignatureError::Extractor(err) => err.into(),
218            RequestSignatureError::Signature(err) => err.into(),
219        }
220    }
221}
222
223impl<T, S> FromRequest for RequestSignature<T, S>
224where
225    T: FromRequest + 'static,
226    T::Error: fmt::Debug + fmt::Display,
227
228    S: RequestSignatureScheme + 'static,
229    S::Error: fmt::Debug + fmt::Display,
230{
231    type Error = RequestSignatureError<T, S>;
232    type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
233
234    fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
235        let req = req.clone();
236        let payload = payload.take();
237
238        Box::pin(async move {
239            let (tx, mut rx) = mpsc::channel();
240
241            // wrap payload in stream that reads chunks and clones them (cheaply) back here
242            let proxy_stream: BoxedPayloadStream = Box::pin(payload.inspect(move |res| {
243                if let Ok(chunk) = res {
244                    trace!("yielding {} byte chunk", chunk.len());
245                    tx.send(chunk.clone()).unwrap();
246                }
247            }));
248
249            trace!("creating proxy payload");
250            let mut proxy_payload = dev::Payload::from(proxy_stream);
251            let body_fut =
252                T::from_request(&req, &mut proxy_payload).map_err(RequestSignatureError::Extractor);
253
254            trace!("initializing signature scheme");
255            let mut sig_scheme = S::init(&req)
256                .await
257                .map_err(RequestSignatureError::Signature)?;
258
259            // run update function as chunks are yielded from channel
260            let hash_fut = actix_web::rt::spawn({
261                let req = req.clone();
262
263                async move {
264                    while let Some(chunk) = rx.recv().await {
265                        trace!("digesting chunk");
266                        sig_scheme.consume_chunk(&req, chunk).await?;
267                    }
268
269                    trace!("finalizing signature");
270                    sig_scheme.finalize(&req).await
271                }
272            })
273            .map(Result::unwrap)
274            .map_err(RequestSignatureError::Signature);
275
276            trace!("driving both futures");
277            let (body, signature) = try_join!(body_fut, hash_fut)?;
278
279            trace!("verifying signature");
280            let signature = S::verify(signature, &req).map_err(RequestSignatureError::Signature)?;
281
282            let out = Self {
283                extractor: body,
284                signature,
285            };
286
287            Ok(out)
288        })
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use std::convert::Infallible;
295
296    use actix_web::{
297        http::StatusCode,
298        test,
299        web::{self, Bytes},
300        App,
301    };
302    use digest::{CtOutput, Digest as _};
303    use hex_literal::hex;
304    use sha2::Sha256;
305
306    use super::*;
307    use crate::extract::Json;
308
309    #[derive(Debug, Default)]
310    struct JustHash(sha2::Sha256);
311
312    #[async_trait(?Send)]
313    impl RequestSignatureScheme for JustHash {
314        type Signature = CtOutput<sha2::Sha256>;
315        type Error = Infallible;
316
317        async fn init(head: &HttpRequest) -> Result<Self, Self::Error> {
318            let mut hasher = Sha256::new();
319
320            if let Some(path) = head.uri().path_and_query() {
321                hasher.update(path.as_str().as_bytes())
322            }
323
324            Ok(Self(hasher))
325        }
326
327        async fn consume_chunk(
328            &mut self,
329            _req: &HttpRequest,
330            chunk: Bytes,
331        ) -> Result<(), Self::Error> {
332            self.0.update(&chunk);
333            Ok(())
334        }
335
336        async fn finalize(self, _req: &HttpRequest) -> Result<Self::Signature, Self::Error> {
337            let hash = self.0.finalize();
338            Ok(CtOutput::new(hash))
339        }
340    }
341
342    #[actix_web::test]
343    async fn correctly_hashes_payload() {
344        let app = test::init_service(App::new().route(
345            "/service/path",
346            web::get().to(|body: RequestSignature<Bytes, JustHash>| async move {
347                let (_, sig) = body.into_parts();
348                sig.into_bytes().to_vec()
349            }),
350        ))
351        .await;
352
353        let req = test::TestRequest::with_uri("/service/path").to_request();
354        let body = test::call_and_read_body(&app, req).await;
355        assert_eq!(
356            body,
357            hex!("a5441a3d ec265f82 3758d164 1188ab1d d1093972 45012a45 fa66df70 32d02177")
358                .as_ref()
359        );
360
361        let req = test::TestRequest::with_uri("/service/path")
362            .set_payload("abc")
363            .to_request();
364        let body = test::call_and_read_body(&app, req).await;
365        assert_eq!(
366            body,
367            hex!("555290a8 9e75260d fb0afead 2d5d3d70 f058c85d 1ff98bf3 06807301 7ce4c847")
368                .as_ref()
369        );
370    }
371
372    #[actix_web::test]
373    async fn respects_inner_extractor_errors() {
374        let app = test::init_service(App::new().route(
375            "/",
376            web::get().to(
377                |body: RequestSignature<Json<u64, 4>, JustHash>| async move {
378                    let (_, sig) = body.into_parts();
379                    sig.into_bytes().to_vec()
380                },
381            ),
382        ))
383        .await;
384
385        let req = test::TestRequest::default().set_json(1234).to_request();
386        let body = test::call_and_read_body(&app, req).await;
387        assert_eq!(
388            body,
389            hex!("4f373f6c cadfaba3 1a32cf52 04cf3db9 367609ee 6a7d7113 8e4f28ef 7c1a87a9")
390                .as_ref()
391        );
392
393        // no body would expect a 400 content type error
394        let req = test::TestRequest::default().to_request();
395        let body = test::call_service(&app, req).await;
396        assert_eq!(body.status(), StatusCode::BAD_REQUEST);
397
398        // body too big would expect a 413 request payload too large
399        let req = test::TestRequest::default().set_json(12345).to_request();
400        let body = test::call_service(&app, req).await;
401        assert_eq!(body.status(), StatusCode::PAYLOAD_TOO_LARGE);
402    }
403}