#![deny(missing_docs)] //! # Integration of Http Signature Normalization with Actix Web //! //! This library provides middlewares for verifying HTTP Signature headers and, optionally, Digest //! headers with the `digest` feature enabled. It also extends awc's ClientRequest type to //! add signatures and digests to the request //! //! ### Use it in a server //! ```rust,ignore //! use actix_web::{http::StatusCode, web, App, HttpRequest, HttpResponse, HttpServer, ResponseError}; //! use http_signature_normalization_actix::prelude::*; //! use sha2::{Digest, Sha256}; //! use std::future::{ready, Ready}; //! use tracing::info; //! use tracing_actix_web::TracingLogger; //! use tracing_error::ErrorLayer; //! use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; //! //! #[derive(Clone, Debug)] //! struct MyVerify; //! //! impl SignatureVerify for MyVerify { //! type Error = MyError; //! type Future = Ready>; //! //! fn signature_verify( //! &mut self, //! algorithm: Option, //! key_id: String, //! signature: String, //! signing_string: String, //! ) -> Self::Future { //! match algorithm { //! Some(Algorithm::Hs2019) => (), //! _ => return ready(Err(MyError::Algorithm)), //! }; //! //! if key_id != "my-key-id" { //! return ready(Err(MyError::Key)); //! } //! //! let decoded = match base64::decode(&signature) { //! Ok(decoded) => decoded, //! Err(_) => return ready(Err(MyError::Decode)), //! }; //! //! info!("Signing String\n{}", signing_string); //! //! ready(Ok(decoded == signing_string.as_bytes())) //! } //! } //! //! async fn index( //! (_, sig_verified): (DigestVerified, SignatureVerified), //! req: HttpRequest, //! _body: web::Bytes, //! ) -> &'static str { //! info!("Verified request for {}", sig_verified.key_id()); //! info!("{:?}", req); //! "Eyyyyup" //! } //! //! #[actix_rt::main] //! async fn main() -> Result<(), Box> { //! let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); //! //! let subscriber = tracing_subscriber::Registry::default() //! .with(env_filter) //! .with(ErrorLayer::default()) //! .with(tracing_subscriber::fmt::layer()); //! //! tracing::subscriber::set_global_default(subscriber)?; //! //! let config = Config::default().require_header("accept").require_digest(); //! //! HttpServer::new(move || { //! App::new() //! .wrap(VerifyDigest::new(Sha256::new()).optional()) //! .wrap(VerifySignature::new(MyVerify, config.clone()).optional()) //! .wrap(TracingLogger::default()) //! .route("/", web::post().to(index)) //! }) //! .bind("127.0.0.1:8010")? //! .run() //! .await?; //! //! Ok(()) //! } //! //! #[derive(Debug, thiserror::Error)] //! enum MyError { //! #[error("Failed to verify, {0}")] //! Verify(#[from] PrepareVerifyError), //! //! #[error("Unsupported algorithm")] //! Algorithm, //! //! #[error("Couldn't decode signature")] //! Decode, //! //! #[error("Invalid key")] //! Key, //! } //! //! impl ResponseError for MyError { //! fn status_code(&self) -> StatusCode { //! StatusCode::BAD_REQUEST //! } //! //! fn error_response(&self) -> HttpResponse { //! HttpResponse::BadRequest().finish() //! } //! } //! ``` //! //! ### Use it in a client //! ```rust,ignore //! use actix_rt::task::JoinError; //! use awc::Client; //! use http_signature_normalization_actix::prelude::*; //! use sha2::{Digest, Sha256}; //! use std::time::SystemTime; //! use tracing::{error, info}; //! use tracing_error::ErrorLayer; //! use tracing_subscriber::{layer::SubscriberExt, EnvFilter}; //! //! async fn request(config: Config) -> Result<(), Box> { //! let digest = Sha256::new(); //! //! let mut response = Client::default() //! .post("http://127.0.0.1:8010/") //! .append_header(("User-Agent", "Actix Web")) //! .append_header(("Accept", "text/plain")) //! .insert_header(actix_web::http::header::Date(SystemTime::now().into())) //! .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| { //! info!("Signing String\n{}", s); //! Ok(base64::encode(s)) as Result<_, MyError> //! }) //! .await? //! .send() //! .await //! .map_err(|e| { //! error!("Error, {}", e); //! MyError::SendRequest //! })?; //! //! let body = response.body().await.map_err(|e| { //! error!("Error, {}", e); //! MyError::Body //! })?; //! //! info!("{:?}", body); //! Ok(()) //! } //! //! #[actix_rt::main] //! async fn main() -> Result<(), Box> { //! let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); //! //! let subscriber = tracing_subscriber::Registry::default() //! .with(env_filter) //! .with(ErrorLayer::default()) //! .with(tracing_subscriber::fmt::layer()); //! //! tracing::subscriber::set_global_default(subscriber)?; //! //! let config = Config::default().require_header("accept").require_digest(); //! //! request(config.clone()).await?; //! request(config.mastodon_compat()).await?; //! Ok(()) //! } //! //! #[derive(Debug, thiserror::Error)] //! pub enum MyError { //! #[error("Failed to create signing string, {0}")] //! Convert(#[from] PrepareSignError), //! //! #[error("Failed to create header, {0}")] //! Header(#[from] InvalidHeaderValue), //! //! #[error("Failed to send request")] //! SendRequest, //! //! #[error("Failed to retrieve request body")] //! Body, //! //! #[error("Blocking operation was canceled")] //! Canceled, //! } //! //! impl From for MyError { //! fn from(_: JoinError) -> Self { //! MyError::Canceled //! } //! } //! ``` use std::time::Duration; #[cfg(any(feature = "client", feature = "server"))] use actix_http::{ header::{HeaderMap, ToStrError}, uri::PathAndQuery, Method, }; #[cfg(any(feature = "client", feature = "server"))] use std::collections::BTreeMap; #[cfg(feature = "client")] mod sign; #[cfg(feature = "digest")] pub mod digest; #[cfg(feature = "client")] pub mod create; #[cfg(feature = "server")] pub mod middleware; pub use http_signature_normalization::RequiredError; /// Useful types and traits for using this library in Actix Web pub mod prelude { pub use crate::{Config, RequiredError}; #[cfg(feature = "client")] pub use crate::{PrepareSignError, Sign}; #[cfg(feature = "server")] pub use crate::{ middleware::{SignatureVerified, VerifySignature}, verify::{Algorithm, DeprecatedAlgorithm, Unverified}, PrepareVerifyError, SignatureVerify, }; #[cfg(all(feature = "digest", feature = "client"))] pub use crate::digest::{DigestClient, DigestCreate, SignExt}; #[cfg(all(feature = "digest", feature = "server"))] pub use crate::digest::{ middleware::{DigestVerified, VerifyDigest}, DigestPart, DigestVerify, }; pub use actix_http::header::{InvalidHeaderValue, ToStrError}; } #[cfg(feature = "server")] /// Types for Verifying an HTTP Signature pub mod verify { pub use http_signature_normalization::verify::{ Algorithm, DeprecatedAlgorithm, ParseSignatureError, ParsedHeader, Unvalidated, Unverified, ValidateError, }; } #[cfg(feature = "client")] pub use self::client::{PrepareSignError, Sign}; #[cfg(feature = "server")] pub use self::server::{PrepareVerifyError, SignatureVerify}; #[derive(Clone, Debug, Default)] /// Configuration for signing and verifying signatures /// /// By default, the config is set up to create and verify signatures that expire after 10 /// seconds, and use the `(created)` and `(expires)` fields that were introduced in draft 11 pub struct Config { /// The inner config type config: http_signature_normalization::Config, /// Whether to set the Host header set_host: bool, /// Whether to set the Date header set_date: bool, } #[cfg(feature = "client")] mod client { use super::{Config, RequiredError}; use actix_http::header::{InvalidHeaderValue, ToStrError}; use actix_rt::task::JoinError; use std::{fmt::Display, future::Future, pin::Pin}; /// A trait implemented by the awc ClientRequest type to add an HTTP signature to the request pub trait Sign { /// Add an Authorization Signature to the request fn authorization_signature( self, config: Config, key_id: K, f: F, ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From + From + std::fmt::Debug + Send + 'static, K: Display + 'static, Self: Sized; /// Add a Signature to the request fn signature( self, config: Config, key_id: K, f: F, ) -> Pin>>> where F: FnOnce(&str) -> Result + Send + 'static, E: From + From + From + std::fmt::Debug + Send + 'static, K: Display + 'static, Self: Sized; } #[derive(Debug, thiserror::Error)] /// An error when preparing to sign a request pub enum PrepareSignError { #[error("Failed to read header")] /// An error occurred when reading the request's headers Header(#[from] ToStrError), #[error("Missing required header")] /// Some headers were marked as required, but are missing RequiredError(#[from] RequiredError), #[error("No host provided for URL, {0}")] /// Missing host Host(String), #[error("Failed to set header")] /// Invalid Date header InvalidHeader(#[from] actix_http::header::InvalidHeaderValue), } } #[cfg(feature = "server")] mod server { use super::RequiredError; use actix_http::header::ToStrError; use std::future::Future; /// A trait for verifying signatures pub trait SignatureVerify { /// An error produced while attempting to verify the signature. This can be anything /// implementing ResponseError type Error: actix_web::ResponseError; /// The future that resolves to the verification state of the signature type Future: Future>; /// Given the algorithm, key_id, signature, and signing_string, produce a future that resulves /// to a the verification status fn signature_verify( &mut self, algorithm: Option, key_id: String, signature: String, signing_string: String, ) -> Self::Future; } #[derive(Debug, thiserror::Error)] /// An error when preparing to verify a request pub enum PrepareVerifyError { #[error("Header is missing")] /// Header is missing Missing, #[error("{0}")] /// Header is expired Expired(String), #[error("Couldn't parse required field, {0}")] /// Couldn't parse required field ParseField(&'static str), #[error("Failed to read header, {0}")] /// An error converting the header to a string for validation Header(#[from] ToStrError), #[error("{0}")] /// Required headers were missing from request Required(#[from] RequiredError), } impl From for PrepareVerifyError { fn from(e: http_signature_normalization::PrepareVerifyError) -> Self { use http_signature_normalization as hsn; match e { hsn::PrepareVerifyError::Parse(parse_error) => { PrepareVerifyError::ParseField(parse_error.missing_field()) } hsn::PrepareVerifyError::Validate(validate_error) => match validate_error { hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing, e @ hsn::verify::ValidateError::Expired { .. } => { PrepareVerifyError::Expired(e.to_string()) } }, hsn::PrepareVerifyError::Required(required_error) => { PrepareVerifyError::Required(required_error) } } } } } impl Config { /// Create a new Config with a default expiration of 10 seconds pub fn new() -> Self { Config::default() } /// Since manually setting the Host header doesn't work so well in AWC, you can use this method /// to enable setting the Host header for signing requests without breaking client /// functionality pub fn set_host_header(self) -> Self { Config { config: self.config, set_host: true, set_date: self.set_date, } } /// Enable mastodon compatibility /// /// This is the same as disabling the use of `(created)` and `(expires)` signature fields, /// requiring the Date header, and requiring the Host header pub fn mastodon_compat(self) -> Self { Config { config: self.config.mastodon_compat(), set_host: true, set_date: true, } } /// Require the Digest header be set /// /// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE. pub fn require_digest(self) -> Self { Config { config: self.config.require_digest(), set_host: self.set_host, set_date: self.set_date, } } /// Opt out of using the (created) and (expires) fields introduced in draft 11 /// /// Note that by enabling this, the Date header becomes required on requests. This is to /// prevent replay attacks pub fn dont_use_created_field(self) -> Self { Config { config: self.config.dont_use_created_field(), set_host: self.set_host, set_date: self.set_date, } } /// Set the expiration to a custom duration pub fn set_expiration(self, expires_after: Duration) -> Self { Config { config: self.config.set_expiration(expires_after), set_host: self.set_host, set_date: self.set_date, } } /// Require a header on signed and verified requests pub fn require_header(self, header: &str) -> Self { Config { config: self.config.require_header(header), set_host: self.set_host, set_date: self.set_date, } } #[cfg(feature = "client")] /// Begin the process of singing a request pub fn begin_sign( &self, method: &Method, path_and_query: Option<&PathAndQuery>, headers: HeaderMap, ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) .collect::, ToStrError>>()?; let path_and_query = path_and_query .map(|p| p.to_string()) .unwrap_or_else(|| "/".to_string()); let unsigned = self .config .begin_sign(method.as_ref(), &path_and_query, headers)?; Ok(self::create::Unsigned { unsigned }) } #[cfg(feature = "server")] /// Begin the proess of verifying a request pub fn begin_verify( &self, method: &Method, path_and_query: Option<&PathAndQuery>, headers: HeaderMap, ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) .collect::, ToStrError>>()?; let path_and_query = path_and_query .map(|p| p.to_string()) .unwrap_or_else(|| "/".to_string()); let unverified = self .config .begin_verify(method.as_ref(), &path_and_query, headers)?; Ok(unverified) } }