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

538 lines
17 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 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<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 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<dyn std::error::Error>> {
//! 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<dyn std::error::Error>> {
//! 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<dyn std::error::Error>> {
//! 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<JoinError> 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<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<JoinError>
+ 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<JoinError>
+ From<PrepareSignError>
+ From<InvalidHeaderValue>
+ 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<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<super::verify::Algorithm>,
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<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,
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<self::create::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_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<self::verify::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_else(|| "/".to_string());
let unverified = self
.config
.begin_verify(method.as_ref(), &path_and_query, headers)?;
Ok(unverified)
}
}