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