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}