http_signature_normalization_actix/
lib.rs

1#![deny(missing_docs)]
2
3//! # Integration of Http Signature Normalization with Actix Web
4//!
5//! This library provides middlewares for verifying HTTP Signature headers and, optionally, Digest
6//! headers with the `digest` feature enabled. It also extends awc's ClientRequest type to
7//! add signatures and digests to the request
8//!
9//! ### Use it in a server
10//! ```rust,ignore
11//! use actix_web::{http::StatusCode, web, App, HttpRequest, HttpResponse, HttpServer, ResponseError};
12//! use http_signature_normalization_actix::prelude::*;
13//! use sha2::{Digest, Sha256};
14//! use std::future::{ready, Ready};
15//! use tracing::info;
16//! use tracing_actix_web::TracingLogger;
17//! use tracing_error::ErrorLayer;
18//! use tracing_subscriber::{layer::SubscriberExt, EnvFilter};
19//!
20//! #[derive(Clone, Debug)]
21//! struct MyVerify;
22//!
23//! impl SignatureVerify for MyVerify {
24//!     type Error = MyError;
25//!     type Future = Ready<Result<bool, Self::Error>>;
26//!
27//!     fn signature_verify(
28//!         &mut self,
29//!         algorithm: Option<Algorithm>,
30//!         key_id: String,
31//!         signature: String,
32//!         signing_string: String,
33//!     ) -> Self::Future {
34//!         match algorithm {
35//!             Some(Algorithm::Hs2019) => (),
36//!             _ => return ready(Err(MyError::Algorithm)),
37//!         };
38//!
39//!         if key_id != "my-key-id" {
40//!             return ready(Err(MyError::Key));
41//!         }
42//!
43//!         let decoded = match base64::decode(&signature) {
44//!             Ok(decoded) => decoded,
45//!             Err(_) => return ready(Err(MyError::Decode)),
46//!         };
47//!
48//!         info!("Signing String\n{}", signing_string);
49//!
50//!         ready(Ok(decoded == signing_string.as_bytes()))
51//!     }
52//! }
53//!
54//! async fn index(
55//!     (_, sig_verified): (DigestVerified, SignatureVerified),
56//!     req: HttpRequest,
57//!     _body: web::Bytes,
58//! ) -> &'static str {
59//!     info!("Verified request for {}", sig_verified.key_id());
60//!     info!("{:?}", req);
61//!     "Eyyyyup"
62//! }
63//!
64//! #[actix_rt::main]
65//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
66//!     let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
67//!
68//!     let subscriber = tracing_subscriber::Registry::default()
69//!         .with(env_filter)
70//!         .with(ErrorLayer::default())
71//!         .with(tracing_subscriber::fmt::layer());
72//!
73//!     tracing::subscriber::set_global_default(subscriber)?;
74//!
75//!     let config = Config::default().require_header("accept").require_digest();
76//!
77//!     HttpServer::new(move || {
78//!         App::new()
79//!             .wrap(VerifyDigest::new(Sha256::new()).optional())
80//!             .wrap(VerifySignature::new(MyVerify, config.clone()).optional())
81//!             .wrap(TracingLogger::default())
82//!             .route("/", web::post().to(index))
83//!     })
84//!     .bind("127.0.0.1:8010")?
85//!     .run()
86//!     .await?;
87//!
88//!     Ok(())
89//! }
90//!
91//! #[derive(Debug, thiserror::Error)]
92//! enum MyError {
93//!     #[error("Failed to verify, {0}")]
94//!     Verify(#[from] PrepareVerifyError),
95//!
96//!     #[error("Unsupported algorithm")]
97//!     Algorithm,
98//!
99//!     #[error("Couldn't decode signature")]
100//!     Decode,
101//!
102//!     #[error("Invalid key")]
103//!     Key,
104//! }
105//!
106//! impl ResponseError for MyError {
107//!     fn status_code(&self) -> StatusCode {
108//!         StatusCode::BAD_REQUEST
109//!     }
110//!
111//!     fn error_response(&self) -> HttpResponse {
112//!         HttpResponse::BadRequest().finish()
113//!     }
114//! }
115//! ```
116//!
117//! ### Use it in a client
118//! ```rust,ignore
119//! use actix_rt::task::JoinError;
120//! use awc::Client;
121//! use http_signature_normalization_actix::prelude::*;
122//! use sha2::{Digest, Sha256};
123//! use std::time::SystemTime;
124//! use tracing::{error, info};
125//! use tracing_error::ErrorLayer;
126//! use tracing_subscriber::{layer::SubscriberExt, EnvFilter};
127//!
128//! async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
129//!     let digest = Sha256::new();
130//!
131//!     let mut response = Client::default()
132//!         .post("http://127.0.0.1:8010/")
133//!         .append_header(("User-Agent", "Actix Web"))
134//!         .append_header(("Accept", "text/plain"))
135//!         .insert_header(actix_web::http::header::Date(SystemTime::now().into()))
136//!         .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
137//!             info!("Signing String\n{}", s);
138//!             Ok(base64::encode(s)) as Result<_, MyError>
139//!         })
140//!         .await?
141//!         .send()
142//!         .await
143//!         .map_err(|e| {
144//!             error!("Error, {}", e);
145//!             MyError::SendRequest
146//!         })?;
147//!
148//!     let body = response.body().await.map_err(|e| {
149//!         error!("Error, {}", e);
150//!         MyError::Body
151//!     })?;
152//!
153//!     info!("{:?}", body);
154//!     Ok(())
155//! }
156//!
157//! #[actix_rt::main]
158//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
159//!     let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
160//!
161//!     let subscriber = tracing_subscriber::Registry::default()
162//!         .with(env_filter)
163//!         .with(ErrorLayer::default())
164//!         .with(tracing_subscriber::fmt::layer());
165//!
166//!     tracing::subscriber::set_global_default(subscriber)?;
167//!
168//!     let config = Config::default().require_header("accept").require_digest();
169//!
170//!     request(config.clone()).await?;
171//!     request(config.mastodon_compat()).await?;
172//!     Ok(())
173//! }
174//!
175//! #[derive(Debug, thiserror::Error)]
176//! pub enum MyError {
177//!     #[error("Failed to create signing string, {0}")]
178//!     Convert(#[from] PrepareSignError),
179//!
180//!     #[error("Failed to create header, {0}")]
181//!     Header(#[from] InvalidHeaderValue),
182//!
183//!     #[error("Failed to send request")]
184//!     SendRequest,
185//!
186//!     #[error("Failed to retrieve request body")]
187//!     Body,
188//!
189//!     #[error("Blocking operation was canceled")]
190//!     Canceled,
191//! }
192//!
193//! impl From<JoinError> for MyError {
194//!     fn from(_: JoinError) -> Self {
195//!         MyError::Canceled
196//!     }
197//! }
198//! ```
199
200use std::time::Duration;
201
202#[cfg(any(feature = "client", feature = "server"))]
203use actix_http::{
204    header::{HeaderMap, ToStrError},
205    uri::PathAndQuery,
206    Method,
207};
208#[cfg(any(feature = "client", feature = "server"))]
209use std::collections::BTreeMap;
210
211#[cfg(feature = "client")]
212mod sign;
213
214#[cfg(feature = "digest")]
215pub mod digest;
216
217#[cfg(feature = "client")]
218pub mod create;
219#[cfg(feature = "server")]
220pub mod middleware;
221
222pub use http_signature_normalization::RequiredError;
223
224/// Useful types and traits for using this library in Actix Web
225pub mod prelude {
226    pub use crate::{Config, RequiredError};
227
228    #[cfg(feature = "client")]
229    pub use crate::{PrepareSignError, Sign};
230
231    #[cfg(feature = "server")]
232    pub use crate::{
233        middleware::{SignatureVerified, VerifySignature},
234        verify::{Algorithm, DeprecatedAlgorithm, Unverified},
235        PrepareVerifyError, SignatureVerify,
236    };
237
238    #[cfg(all(feature = "digest", feature = "client"))]
239    pub use crate::digest::{DigestClient, DigestCreate, SignExt};
240
241    #[cfg(all(feature = "digest", feature = "server"))]
242    pub use crate::digest::{
243        middleware::{DigestVerified, VerifyDigest},
244        DigestPart, DigestVerify,
245    };
246
247    pub use actix_http::header::{InvalidHeaderValue, ToStrError};
248}
249
250#[cfg(feature = "server")]
251/// Types for Verifying an HTTP Signature
252pub mod verify {
253    pub use http_signature_normalization::verify::{
254        Algorithm, DeprecatedAlgorithm, ParseSignatureError, ParsedHeader, Unvalidated, Unverified,
255        ValidateError,
256    };
257}
258
259#[cfg(feature = "client")]
260pub use self::client::{PrepareSignError, Sign};
261
262#[cfg(feature = "server")]
263pub use self::server::{PrepareVerifyError, SignatureVerify};
264
265#[derive(Clone, Debug, Default)]
266/// Configuration for signing and verifying signatures
267///
268/// By default, the config is set up to create and verify signatures that expire after 10
269/// seconds, and use the `(created)` and `(expires)` fields that were introduced in draft 11
270pub struct Config<Spawner = DefaultSpawner> {
271    /// The inner config type
272    config: http_signature_normalization::Config,
273
274    /// Whether to set the Host header
275    set_host: bool,
276
277    /// Whether to set the Date header
278    set_date: bool,
279
280    /// The spawner used to create blocking operations
281    spawner: Spawner,
282}
283
284/// A default implementation of Spawner for spawning blocking operations
285#[derive(Clone, Copy, Debug, Default)]
286pub struct DefaultSpawner;
287
288/// An error that indicates a blocking operation panicked and cannot return a response
289#[derive(Debug)]
290pub struct Canceled;
291
292impl std::fmt::Display for Canceled {
293    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294        write!(f, "Operation was canceled")
295    }
296}
297
298impl std::error::Error for Canceled {}
299
300/// A trait dictating how to spawn a future onto a blocking threadpool. By default,
301/// http-signature-normalization-actix will use actix_rt's built-in blocking threadpool, but this
302/// can be customized
303pub trait Spawn {
304    /// The future type returned by spawn_blocking
305    type Future<T>: std::future::Future<Output = Result<T, Canceled>>;
306
307    /// Spawn the blocking function onto the threadpool
308    fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
309    where
310        Func: FnOnce() -> Out + Send + 'static,
311        Out: Send + 'static;
312}
313
314/// The future returned by DefaultSpawner when spawning blocking operations on the actix_rt
315/// blocking threadpool
316pub struct DefaultSpawnerFuture<Out> {
317    inner: actix_rt::task::JoinHandle<Out>,
318}
319
320impl Spawn for DefaultSpawner {
321    type Future<T> = DefaultSpawnerFuture<T>;
322
323    fn spawn_blocking<Func, Out>(&self, func: Func) -> Self::Future<Out>
324    where
325        Func: FnOnce() -> Out + Send + 'static,
326        Out: Send + 'static,
327    {
328        DefaultSpawnerFuture {
329            inner: actix_rt::task::spawn_blocking(func),
330        }
331    }
332}
333
334impl<Out> std::future::Future for DefaultSpawnerFuture<Out> {
335    type Output = Result<Out, Canceled>;
336
337    fn poll(
338        mut self: std::pin::Pin<&mut Self>,
339        cx: &mut std::task::Context<'_>,
340    ) -> std::task::Poll<Self::Output> {
341        let res = std::task::ready!(std::pin::Pin::new(&mut self.inner).poll(cx));
342
343        std::task::Poll::Ready(res.map_err(|_| Canceled))
344    }
345}
346
347#[cfg(feature = "client")]
348mod client {
349    use super::{Config, RequiredError, Spawn};
350    use actix_http::header::{InvalidHeaderValue, ToStrError};
351    use actix_rt::task::JoinError;
352    use std::{fmt::Display, future::Future, pin::Pin};
353
354    /// A trait implemented by the awc ClientRequest type to add an HTTP signature to the request
355    pub trait Sign {
356        /// Add an Authorization Signature to the request
357        fn authorization_signature<F, E, K, S>(
358            self,
359            config: Config<S>,
360            key_id: K,
361            f: F,
362        ) -> Pin<Box<dyn Future<Output = Result<Self, E>>>>
363        where
364            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
365            E: From<JoinError>
366                + From<PrepareSignError>
367                + From<crate::Canceled>
368                + From<InvalidHeaderValue>
369                + std::fmt::Debug
370                + Send
371                + 'static,
372            K: Display + 'static,
373            S: Spawn + 'static,
374            Self: Sized;
375
376        /// Add a Signature to the request
377        fn signature<F, E, K, S>(
378            self,
379            config: Config<S>,
380            key_id: K,
381            f: F,
382        ) -> Pin<Box<dyn Future<Output = Result<Self, E>>>>
383        where
384            F: FnOnce(&str) -> Result<String, E> + Send + 'static,
385            E: From<JoinError>
386                + From<PrepareSignError>
387                + From<crate::Canceled>
388                + From<InvalidHeaderValue>
389                + std::fmt::Debug
390                + Send
391                + 'static,
392            K: Display + 'static,
393            S: Spawn + 'static,
394            Self: Sized;
395    }
396
397    #[derive(Debug, thiserror::Error)]
398    /// An error when preparing to sign a request
399    pub enum PrepareSignError {
400        #[error("Failed to read header")]
401        /// An error occurred when reading the request's headers
402        Header(#[from] ToStrError),
403
404        #[error("Missing required header")]
405        /// Some headers were marked as required, but are missing
406        RequiredError(#[from] RequiredError),
407
408        #[error("No host provided for URL, {0}")]
409        /// Missing host
410        Host(String),
411
412        #[error("Failed to set header")]
413        /// Invalid Date header
414        InvalidHeader(#[from] actix_http::header::InvalidHeaderValue),
415    }
416}
417
418#[cfg(feature = "server")]
419mod server {
420    use super::RequiredError;
421    use actix_http::header::ToStrError;
422    use std::future::Future;
423
424    /// A trait for verifying signatures
425    pub trait SignatureVerify {
426        /// An error produced while attempting to verify the signature. This can be anything
427        /// implementing ResponseError
428        type Error: actix_web::ResponseError;
429
430        /// The future that resolves to the verification state of the signature
431        type Future: Future<Output = Result<bool, Self::Error>>;
432
433        /// Given the algorithm, key_id, signature, and signing_string, produce a future that resulves
434        /// to a the verification status
435        fn signature_verify(
436            &mut self,
437            algorithm: Option<super::verify::Algorithm>,
438            key_id: String,
439            signature: String,
440            signing_string: String,
441        ) -> Self::Future;
442    }
443
444    #[derive(Debug, thiserror::Error)]
445    /// An error when preparing to verify a request
446    pub enum PrepareVerifyError {
447        #[error("Header is missing")]
448        /// Header is missing
449        Missing,
450
451        #[error("{0}")]
452        /// Header is expired
453        Expired(String),
454
455        #[error("Couldn't parse required field, {0}")]
456        /// Couldn't parse required field
457        ParseField(&'static str),
458
459        #[error("Failed to read header, {0}")]
460        /// An error converting the header to a string for validation
461        Header(#[from] ToStrError),
462
463        #[error("{0}")]
464        /// Required headers were missing from request
465        Required(#[from] RequiredError),
466    }
467
468    impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
469        fn from(e: http_signature_normalization::PrepareVerifyError) -> Self {
470            use http_signature_normalization as hsn;
471
472            match e {
473                hsn::PrepareVerifyError::Parse(parse_error) => {
474                    PrepareVerifyError::ParseField(parse_error.missing_field())
475                }
476                hsn::PrepareVerifyError::Validate(validate_error) => match validate_error {
477                    hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing,
478                    e @ hsn::verify::ValidateError::Expired { .. } => {
479                        PrepareVerifyError::Expired(e.to_string())
480                    }
481                },
482                hsn::PrepareVerifyError::Required(required_error) => {
483                    PrepareVerifyError::Required(required_error)
484                }
485            }
486        }
487    }
488
489    impl actix_web::ResponseError for super::Canceled {
490        fn status_code(&self) -> actix_http::StatusCode {
491            actix_http::StatusCode::INTERNAL_SERVER_ERROR
492        }
493
494        fn error_response(&self) -> actix_web::HttpResponse<actix_http::body::BoxBody> {
495            actix_web::HttpResponse::new(self.status_code())
496        }
497    }
498}
499
500impl Config {
501    /// Create a new Config with a default expiration of 10 seconds
502    pub fn new() -> Self {
503        Config::default()
504    }
505}
506
507impl<Spawner> Config<Spawner> {
508    /// Since manually setting the Host header doesn't work so well in AWC, you can use this method
509    /// to enable setting the Host header for signing requests without breaking client
510    /// functionality
511    pub fn set_host_header(self) -> Self {
512        Config {
513            config: self.config,
514            set_host: true,
515            set_date: self.set_date,
516            spawner: self.spawner,
517        }
518    }
519
520    #[cfg(feature = "client")]
521    /// Set the spawner for spawning blocking tasks
522    ///
523    /// http-signature-normalization-actix offloads signing messages and generating hashes to a
524    /// blocking threadpool, which can be configured by providing a custom spawner.
525    pub fn spawner<S>(self, spawner: S) -> Config<S>
526    where
527        S: Spawn,
528    {
529        Config {
530            config: self.config,
531            set_host: self.set_host,
532            set_date: self.set_date,
533            spawner,
534        }
535    }
536
537    /// Enable mastodon compatibility
538    ///
539    /// This is the same as disabling the use of `(created)` and `(expires)` signature fields,
540    /// requiring the Date header, and requiring the Host header
541    pub fn mastodon_compat(self) -> Self {
542        Config {
543            config: self.config.mastodon_compat(),
544            set_host: true,
545            set_date: true,
546            spawner: self.spawner,
547        }
548    }
549
550    /// Require the Digest header be set
551    ///
552    /// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE.
553    pub fn require_digest(self) -> Self {
554        Config {
555            config: self.config.require_digest(),
556            set_host: self.set_host,
557            set_date: self.set_date,
558            spawner: self.spawner,
559        }
560    }
561
562    /// Opt out of using the (created) and (expires) fields introduced in draft 11
563    ///
564    /// Note that by enabling this, the Date header becomes required on requests. This is to
565    /// prevent replay attacks
566    pub fn dont_use_created_field(self) -> Self {
567        Config {
568            config: self.config.dont_use_created_field(),
569            set_host: self.set_host,
570            set_date: self.set_date,
571            spawner: self.spawner,
572        }
573    }
574
575    /// Set the expiration to a custom duration
576    pub fn set_expiration(self, expires_after: Duration) -> Self {
577        Config {
578            config: self.config.set_expiration(expires_after),
579            set_host: self.set_host,
580            set_date: self.set_date,
581            spawner: self.spawner,
582        }
583    }
584
585    /// Require a header on signed and verified requests
586    pub fn require_header(self, header: &str) -> Self {
587        Config {
588            config: self.config.require_header(header),
589            set_host: self.set_host,
590            set_date: self.set_date,
591            spawner: self.spawner,
592        }
593    }
594
595    #[cfg(feature = "client")]
596    /// Begin the process of singing a request
597    pub fn begin_sign(
598        &self,
599        method: &Method,
600        path_and_query: Option<&PathAndQuery>,
601        headers: HeaderMap,
602    ) -> Result<self::create::Unsigned, PrepareSignError> {
603        let headers = headers
604            .iter()
605            .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
606            .collect::<Result<BTreeMap<_, _>, ToStrError>>()?;
607
608        let path_and_query = path_and_query
609            .map(|p| p.to_string())
610            .unwrap_or_else(|| "/".to_string());
611
612        let unsigned = self
613            .config
614            .begin_sign(method.as_ref(), &path_and_query, headers)?;
615
616        Ok(self::create::Unsigned { unsigned })
617    }
618
619    #[cfg(feature = "server")]
620    /// Begin the proess of verifying a request
621    pub fn begin_verify(
622        &self,
623        method: &Method,
624        path_and_query: Option<&PathAndQuery>,
625        headers: HeaderMap,
626    ) -> Result<self::verify::Unverified, PrepareVerifyError> {
627        let headers = headers
628            .iter()
629            .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
630            .collect::<Result<BTreeMap<_, _>, ToStrError>>()?;
631
632        let path_and_query = path_and_query
633            .map(|p| p.to_string())
634            .unwrap_or_else(|| "/".to_string());
635
636        let unverified = self
637            .config
638            .begin_verify(method.as_ref(), &path_and_query, headers)?;
639
640        Ok(unverified)
641    }
642}