#![deny(missing_docs)] //! # Http Signature Normalization Actix Extractor //! _Experimental Extractor for request signatures_ //! //! This library takes a different approach from the other implementation, which prefers //! Middlewares. //! //! ```rust //! use actix_web::{http::StatusCode, web, App, HttpRequest, HttpResponse, HttpServer, ResponseError}; //! use http_signature_normalization_actix_extractor::{ //! Algorithm, Config, ConfigGenerator, DeprecatedAlgorithm, Signed, VerifyKey, //! }; //! use sha2::Sha256; //! //! #[actix_web::main] //! async fn main() -> std::io::Result<()> { //! /* //! HttpServer::new(|| App::new().route("/", web::post().to(protected))) //! .bind("127.0.0.1:8010")? //! .run() //! .await //! */ //! Ok(()) //! } //! //! async fn protected(signed_request: Signed) -> &'static str { //! let (value, signature) = signed_request.into_parts(); //! //! println!("{}", value); //! println!("{:#?}", signature); //! //! "hewwo, mr obama" //! } //! //! pub struct Cfg; //! //! #[derive(Debug)] //! pub struct Key; //! //! #[derive(Debug, thiserror::Error)] //! pub enum VerifyError { //! #[error("Unsupported algorithm")] //! Algorithm, //! //! #[error("Couldn't decode signature")] //! Decode, //! //! #[error("Invalid key")] //! Key, //! } //! //! impl ConfigGenerator for Cfg { //! fn config() -> Config { //! Config::new().require_header("accept") //! } //! } //! //! #[async_trait::async_trait(?Send)] //! impl VerifyKey for Key { //! type Error = VerifyError; //! //! async fn init( //! _: &HttpRequest, //! key_id: &str, //! algorithm: Option<&Algorithm>, //! ) -> Result { //! match algorithm { //! Some(Algorithm::Hs2019 | Algorithm::Deprecated(DeprecatedAlgorithm::RsaSha256)) => (), //! _ => return Err(VerifyError::Algorithm), //! }; //! //! if key_id != "my-key-id" { //! return Err(VerifyError::Key); //! } //! //! Ok(Key) //! } //! //! fn verify(&mut self, signature: &str, signing_string: &str) -> Result { //! use subtle::ConstantTimeEq; //! //! let decoded = base64::decode(&signature).map_err(|_| VerifyError::Decode)?; //! //! Ok(decoded.ct_eq(signing_string.as_bytes()).into()) //! } //! } //! //! impl ResponseError for VerifyError { //! fn status_code(&self) -> StatusCode { //! StatusCode::BAD_REQUEST //! } //! //! fn error_response(&self) -> HttpResponse { //! HttpResponse::BadRequest().finish() //! } //! } //! ``` pub use actix_web_lab::extract::RequestSignature; pub use http_signature_normalization::{ verify::{Algorithm, DeprecatedAlgorithm}, Config, PrepareVerifyError, }; /// An alias to simplify extracting a signed request /// ```rust,ignore /// async fn protected(_: Signed) -> &'static str { /// "hewwo, mr obama" /// } /// ``` pub type Signed = RequestSignature>; #[cfg(feature = "sha-2")] mod sha2_digest; #[derive(Debug)] /// Errors produced by the Extractor pub enum Error { /// The provided Digest header was invalid Digest, /// The provided Signature or Authorization header was invalid Signature, /// Another header required for verifying the signature is invalid InvalidHeaderValue, /// There was an error preparing for verification PrepareVerify(PrepareVerifyError), } /// The SignedRequest Signature scheme /// /// This implements SignatureScheme to allow extracing a signed request pub struct SignedRequest { config_generator: std::marker::PhantomData, key: std::marker::PhantomData, digest_verifier: Option, unverified: http_signature_normalization::verify::Unverified, } #[derive(Debug)] /// A parsed part of a Digest header pub struct DigestPart { /// The Algorithm this digest was computed with pub algorithm: String, /// The base64-encoded digest pub digest: String, } #[derive(Debug)] /// All the required pieces for validating a request signature pub struct Signature { key: K, unverified: http_signature_normalization::verify::Unverified, digest_verifier: D, digest_parts: Option>, } /// Injects a customized [`Config`] type for signature verification /// /// If you don't need to customize the `Config`, you can use `()` /// /// ```rust /// use http_signature_normalization_actix_extractor::{Config, ConfigGenerator}; /// /// struct Gen; /// /// impl ConfigGenerator for Gen { /// fn config() -> Config { /// Config::new() /// } /// } /// ``` pub trait ConfigGenerator { /// Produce a new `Config` fn config() -> Config; } #[async_trait::async_trait(?Send)] /// Extracts a key from the request /// /// ```rust /// use actix_web::{http::StatusCode, web::Data, HttpRequest, HttpResponse, ResponseError}; /// use http_signature_normalization_actix_extractor::{Algorithm, DeprecatedAlgorithm, VerifyKey}; /// use openssl::{hash::MessageDigest, pkey::{PKey, Public}, sign::Verifier}; /// /// pub struct OpenSSLPublicKey(PKey); /// /// #[async_trait::async_trait(?Send)] /// impl VerifyKey for OpenSSLPublicKey { /// type Error = VerifyError; /// /// async fn init( /// req: &HttpRequest, /// key_id: &str, /// algorithm: Option<&Algorithm>, /// ) -> Result { /// match algorithm { /// Some(Algorithm::Hs2019 | Algorithm::Deprecated(DeprecatedAlgorithm::RsaSha256)) => (), /// _ => return Err(VerifyError::Algorithm), /// }; /// /// if key_id != "my-key-id" { /// return Err(VerifyError::Key); /// } /// /// let key = req.app_data::>>().expect("Key loaded").as_ref().clone(); /// /// Ok(OpenSSLPublicKey(key)) /// } /// /// fn verify(&mut self, signature: &str, signing_string: &str) -> Result { /// let decoded = openssl::base64::decode_block(&signature).map_err(|_| VerifyError::Decode)?; /// /// let verifier = Verifier::new(MessageDigest::sha256(), &self.0).map_err(|_| VerifyError::Verifier)?; /// /// verifier.verify(&decoded).map_err(|_| VerifyError::Verify) /// } /// } /// /// #[derive(Debug, thiserror::Error)] /// pub enum VerifyError { /// #[error("Unsupported algorithm")] /// Algorithm, /// /// #[error("Couldn't decode signature")] /// Decode, /// /// #[error("Invalid key")] /// Key, /// /// #[error("Failed to create Verifier from key")] /// Verifier, /// /// #[error("Failed to verify signature")] /// Verify, /// } /// /// impl ResponseError for VerifyError { /// fn status_code(&self) -> StatusCode { /// StatusCode::BAD_REQUEST /// } /// /// fn error_response(&self) -> HttpResponse { /// HttpResponse::BadRequest().finish() /// } /// } /// ``` pub trait VerifyKey: Sized + Send { /// Errors that can happen when extracting keys or verifying the signature type Error: Into; /// Extract the key from the request, given the key_id and algorithm async fn init( req: &actix_web::HttpRequest, key_id: &str, algorithm: Option<&Algorithm>, ) -> Result; /// Verify the signature with the given signing string fn verify(&mut self, signature: &str, signing_string: &str) -> Result; } /// Verifies the Digest header from the request /// /// For endpoints that do not accept request bodies, `()` can be used as the verifier /// /// ### Example: /// ```rust /// use http_signature_normalization_actix_extractor::{DigestPart, VerifyDigest}; /// use openssl::sha::Sha256; /// /// struct OpenSSLSha256(Option); /// /// impl Default for OpenSSLSha256 { /// fn default() -> Self { /// Self::new() /// } /// } /// /// impl OpenSSLSha256 { /// fn new() -> Self { /// Self(Some(Sha256::new())) /// } /// } /// /// impl VerifyDigest for OpenSSLSha256 { /// fn update(&mut self, bytes: &[u8]) { /// self.0.as_mut().expect("Update called after verify").update(bytes); /// } /// /// fn verify(&mut self, parts: &[DigestPart]) -> bool { /// if let Some(decoded) = parts.iter().find_map(|p| { /// if p.algorithm.to_lowercase() == "sha-256" { /// openssl::base64::decode_block(&p.digest).ok() /// } else { /// None /// } /// }) { /// return openssl::memcmp::eq( /// &self.0.take().expect("verify called more than once").finish(), /// &decoded, /// ); /// } /// /// false /// } /// } /// ``` pub trait VerifyDigest: Default + Send + 'static { /// Whether to run this verifier const REQUIRED: bool = true; /// Update the verifier with th eprovided bytes fn update(&mut self, bytes: &[u8]); /// Given a slice of parts, verify that the one matching the current verifier is valid fn verify(&mut self, parts: &[DigestPart]) -> bool; } trait DigestName { const NAME: &'static str; } #[async_trait::async_trait(?Send)] impl actix_web_lab::extract::RequestSignatureScheme for SignedRequest where C: ConfigGenerator, D: VerifyDigest, K: VerifyKey, { type Signature = Signature; type Error = actix_web::Error; async fn init(req: &actix_web::HttpRequest) -> Result { let config = C::config(); let unverified = begin_verify( &config, req.method(), req.uri().path_and_query(), req.headers(), )?; Ok(SignedRequest { config_generator: std::marker::PhantomData, key: std::marker::PhantomData, digest_verifier: Some(D::default()), unverified, }) } async fn consume_chunk( &mut self, _: &actix_web::HttpRequest, chunk: actix_web::web::Bytes, ) -> Result<(), Self::Error> { if D::REQUIRED { let mut verifier = self .digest_verifier .take() .expect("consume_chunk polled concurrently"); self.digest_verifier = Some( actix_web::web::block(move || { verifier.update(&chunk); verifier }) .await .expect("Panic in verifier update"), ); } Ok(()) } async fn finalize( mut self, req: &actix_web::HttpRequest, ) -> Result { let key = K::init(req, self.unverified.key_id(), self.unverified.algorithm()) .await .map_err(Into::into)?; let digest_parts = if D::REQUIRED { req.headers().get("digest").and_then(parse_digest) } else { None }; Ok(Signature { key, unverified: self.unverified, digest_verifier: self .digest_verifier .take() .expect("finalize called more than once"), digest_parts, }) } fn verify( signature: Self::Signature, _: &actix_web::HttpRequest, ) -> Result { let Signature { mut key, unverified, mut digest_verifier, digest_parts, } = signature; if !unverified .verify(|sig, signing_string| key.verify(sig, signing_string)) .map_err(Into::into)? { return Err(Error::Signature.into()); } if D::REQUIRED && !digest_verifier.verify(digest_parts.as_deref().unwrap_or(&[])) { return Err(Error::Digest.into()); } let signature = Signature { key, unverified, digest_verifier, digest_parts, }; Ok(signature) } } fn parse_digest(h: &actix_web::http::header::HeaderValue) -> Option> { let h = h.to_str().ok()?.split(';').next()?; let v: Vec<_> = h .split(',') .filter_map(|p| { let mut iter = p.splitn(2, '='); iter.next() .and_then(|alg| iter.next().map(|value| (alg, value))) }) .map(|(alg, value)| DigestPart { algorithm: alg.to_owned(), digest: value.to_owned(), }) .collect(); if v.is_empty() { None } else { Some(v) } } fn begin_verify( config: &Config, method: &actix_web::http::Method, path_and_query: Option<&actix_web::http::uri::PathAndQuery>, headers: &actix_web::http::header::HeaderMap, ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) .collect::, actix_web::http::header::ToStrError>>( )?; let path_and_query = path_and_query .map(|p| p.to_string()) .unwrap_or_else(|| "/".to_string()); let unverified = config.begin_verify(method.as_ref(), &path_and_query, headers)?; Ok(unverified) } impl ConfigGenerator for () { fn config() -> Config { Config::new() } } impl VerifyDigest for () { const REQUIRED: bool = false; fn update(&mut self, _: &[u8]) {} fn verify(&mut self, _: &[DigestPart]) -> bool { true } } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Digest => write!(f, "Digest is inavlid"), Self::Signature => write!(f, "Signature is invalid"), Self::InvalidHeaderValue => write!(f, "Invalid header value"), Self::PrepareVerify(_) => write!(f, "Error preparint verification"), } } } impl std::error::Error for Error { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::PrepareVerify(ref e) => Some(e), Self::Digest => None, Self::Signature => None, Self::InvalidHeaderValue => None, } } } impl actix_web::ResponseError for Error { fn status_code(&self) -> actix_web::http::StatusCode { actix_web::http::StatusCode::BAD_REQUEST } fn error_response(&self) -> actix_web::HttpResponse { actix_web::HttpResponse::build(self.status_code()).finish() } } impl From for Error { fn from(e: PrepareVerifyError) -> Self { Error::PrepareVerify(e) } } impl From for Error { fn from(_: actix_web::http::header::ToStrError) -> Self { Error::InvalidHeaderValue } }