Skip to main content

actix_web_lab/
request_signature.rs

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