diff --git a/actix-extractor/Cargo.toml b/actix-extractor/Cargo.toml index 731c1ee..0e760f9 100644 --- a/actix-extractor/Cargo.toml +++ b/actix-extractor/Cargo.toml @@ -19,4 +19,5 @@ subtle = { version = "2.4.1", optional = true } [dev-dependencies] actix-web = { version = "4", features = ["macros"] } +openssl = "0.10.43" thiserror = "1" diff --git a/actix-extractor/examples/server.rs b/actix-extractor/examples/server.rs index 29ee56e..c7cff61 100644 --- a/actix-extractor/examples/server.rs +++ b/actix-extractor/examples/server.rs @@ -27,7 +27,7 @@ pub struct Cfg; pub struct Key; #[derive(Debug, thiserror::Error)] -pub enum MyError { +pub enum VerifyError { #[error("Unsupported algorithm")] Algorithm, @@ -46,7 +46,7 @@ impl ConfigGenerator for Cfg { #[async_trait::async_trait(?Send)] impl VerifyKey for Key { - type Error = MyError; + type Error = VerifyError; async fn init( _: &HttpRequest, @@ -55,11 +55,11 @@ impl VerifyKey for Key { ) -> Result { match algorithm { Some(Algorithm::Hs2019 | Algorithm::Deprecated(DeprecatedAlgorithm::RsaSha256)) => (), - _ => return Err(MyError::Algorithm), + _ => return Err(VerifyError::Algorithm), }; if key_id != "my-key-id" { - return Err(MyError::Key); + return Err(VerifyError::Key); } Ok(Key) @@ -68,16 +68,13 @@ impl VerifyKey for Key { fn verify(&mut self, signature: &str, signing_string: &str) -> Result { use subtle::ConstantTimeEq; - let decoded = match base64::decode(&signature) { - Ok(decoded) => decoded, - Err(_) => return Err(MyError::Decode), - }; + let decoded = base64::decode(&signature).map_err(|_| VerifyError::Decode)?; Ok(decoded.ct_eq(signing_string.as_bytes()).into()) } } -impl ResponseError for MyError { +impl ResponseError for VerifyError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } diff --git a/actix-extractor/src/lib.rs b/actix-extractor/src/lib.rs index 0410ba9..cfe49d0 100644 --- a/actix-extractor/src/lib.rs +++ b/actix-extractor/src/lib.rs @@ -1,6 +1,101 @@ // #![deny(missing_docs)] -//! Experimental Extractor for request signatures +//! # 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::{ @@ -8,65 +103,38 @@ pub use http_signature_normalization::{ 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>; + 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), } -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 - } -} - -pub struct SignatureScheme { +/// 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, @@ -74,12 +142,17 @@ pub struct SignatureScheme { } #[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, @@ -87,37 +160,174 @@ pub struct Signature { digest_parts: Option>, } -pub trait DigestName { - const NAME: &'static str; -} - +/// 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 SignatureScheme +impl actix_web_lab::extract::RequestSignatureScheme for SignedRequest where C: ConfigGenerator, D: VerifyDigest, @@ -137,7 +347,7 @@ where req.headers(), )?; - Ok(SignatureScheme { + Ok(SignedRequest { config_generator: std::marker::PhantomData, key: std::marker::PhantomData, digest_verifier: Some(D::default()), @@ -283,3 +493,47 @@ impl VerifyDigest for () { 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 + } +}