http-signature-normalization/http-signature-normalization-actix/src/lib.rs

414 lines
12 KiB
Rust

#![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<Result<bool, Self::Error>>;
//!
//! fn signature_verify(
//! &mut self,
//! algorithm: Option<Algorithm>,
//! 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<dyn std::error::Error>> {
//! 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<dyn std::error::Error>> {
//! 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<BlockingError<MyError>> for MyError {
//! fn from(b: BlockingError<MyError>) -> 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<Output = Result<bool, Self::Error>>;
/// 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<Algorithm>,
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<F, E, K>(
self,
config: Config,
key_id: K,
f: F,
) -> Pin<Box<dyn Future<Output = Result<Self, E>>>>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>>
+ From<PrepareSignError>
+ From<InvalidHeaderValue>
+ std::fmt::Debug
+ Send
+ 'static,
K: Display + 'static,
Self: Sized;
/// Add a Signature to the request
fn signature<F, E, K>(
self,
config: Config,
key_id: K,
f: F,
) -> Pin<Box<dyn Future<Output = Result<Self, E>>>>
where
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
E: From<BlockingError<E>>
+ From<PrepareSignError>
+ From<InvalidHeaderValue>
+ 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<http_signature_normalization::PrepareVerifyError> 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<Unsigned, PrepareSignError> {
let headers = headers
.iter()
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
.collect::<Result<BTreeMap<_, _>, 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<Unverified, PrepareVerifyError> {
let headers = headers
.iter()
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
.collect::<Result<BTreeMap<_, _>, 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)
}
}