#![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 actix_web'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, HttpResponse, HttpServer, ResponseError}; //! use futures::future::{err, ok, Ready}; //! use http_signature_normalization_actix::prelude::*; //! use sha2::{Digest, Sha256}; //! //! #[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 err(MyError::Algorithm), //! }; //! //! if key_id != "my-key-id" { //! return err(MyError::Key); //! } //! //! let decoded = match base64::decode(&signature) { //! Ok(decoded) => decoded, //! Err(_) => return err(MyError::Decode), //! }; //! //! ok(decoded == signing_string.as_bytes()) //! } //! } //! //! async fn index((_, sig_verified): (DigestVerified, SignatureVerified)) -> &'static str { //! println!("Signature verified for {}", sig_verified.key_id()); //! "Eyyyyup" //! } //! //! #[actix_rt::main] //! async fn main() -> Result<(), Box> { //! let config = Config::default(); //! //! HttpServer::new(move || { //! App::new() //! .wrap(VerifyDigest::new(Sha256::new()).optional()) //! .wrap( //! VerifySignature::new(MyVerify, config.clone()) //! .authorization() //! .optional(), //! ) //! .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_web::{client::Client, error::BlockingError}; //! use http_signature_normalization_actix::prelude::*; //! use sha2::{Digest, Sha256}; //! //! #[actix_rt::main] //! async fn main() -> Result<(), Box> { //! let config = Config::default(); //! let digest = Sha256::new(); //! //! let mut response = Client::default() //! .post("http://127.0.0.1:8010/") //! .header("User-Agent", "Actix Web") //! .set(actix_web::http::header::Date(SystemTime::now().into())) //! .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| { //! println!("Signing String\n{}", s); //! Ok(base64::encode(s)) as Result<_, MyError> //! }) //! .await? //! .send() //! .await //! .map_err(|e| { //! eprintln!("Error, {}", e); //! MyError::SendRequest //! })?; //! //! let body = response.body().await.map_err(|e| { //! eprintln!("Error, {}", e); //! MyError::Body //! })?; //! //! println!("{:?}", body); //! 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(b: BlockingError) -> Self { //! match b { //! BlockingError::Error(e) => e, //! _ => MyError::Canceled, //! } //! } //! } //! ``` use actix_web::{ error::BlockingError, http::{ header::{HeaderMap, InvalidHeaderValue, ToStrError}, uri::PathAndQuery, Method, }, }; use chrono::Duration; use std::{collections::BTreeMap, fmt::Display, future::Future, pin::Pin}; mod sign; #[cfg(feature = "digest")] pub mod digest; pub mod create; 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::{ middleware::{SignatureVerified, VerifySignature}, verify::{Algorithm, DeprecatedAlgorithm, Unverified}, Config, PrepareSignError, PrepareVerifyError, RequiredError, Sign, SignatureVerify, }; #[cfg(feature = "digest")] pub use crate::digest::{ middleware::{DigestVerified, VerifyDigest}, DigestClient, DigestCreate, DigestPart, DigestVerify, SignExt, }; pub use actix_web::http::header::{InvalidHeaderValue, ToStrError}; } /// Types for Verifying an HTTP Signature pub mod verify { pub use http_signature_normalization::verify::{ Algorithm, DeprecatedAlgorithm, ParseSignatureError, ParsedHeader, Unvalidated, Unverified, ValidateError, }; } use self::{ create::Unsigned, verify::{Algorithm, Unverified}, }; /// 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; } /// A trait implemented by the Actix Web 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(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, } #[derive(Debug, thiserror::Error)] /// An error when preparing to verify a request pub enum PrepareVerifyError { #[error("Header is missing")] /// Header is missing Missing, #[error("Header is expired")] /// Header is expired Expired, #[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), } #[derive(Debug, thiserror::Error)] /// An error when preparing to sign a request pub enum PrepareSignError { #[error("Failed to read header, {0}")] /// An error occurred when reading the request's headers Header(#[from] ToStrError), #[error("{0}")] /// Some headers were marked as required, but are missing RequiredError(#[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, hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired, }, 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() } /// Opt out of using the (created) and (expires) fields introduced in draft 11 /// /// Use this for compatibility with mastodon pub fn dont_use_created_field(self) -> Self { Config { config: self.config.dont_use_created_field(), } } /// Set the expiration to a custom duration pub fn set_expiration(self, expires_after: Duration) -> Self { Config { config: self.config.set_expiration(expires_after), } } /// Require a header on signed and verified requests pub fn require_header(self, header: &str) -> Self { Config { config: self.config.require_header(header), } } /// 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(String::from("/")); let unsigned = self .config .begin_sign(&method.to_string(), &path_and_query, headers)?; Ok(Unsigned { unsigned }) } /// 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(String::from("/")); let unverified = self .config .begin_verify(&method.to_string(), &path_and_query, headers)?; Ok(unverified) } }