// #![deny(missing_docs)] //! Experimental Extractor for request signatures pub use actix_web_lab::extract::RequestSignature; pub use http_signature_normalization::{verify::Algorithm, Config}; pub type Signed = RequestSignature>; #[cfg(feature = "sha-2")] mod sha2_digest; #[derive(Debug)] pub enum Error { Digest, Signature, InvalidHeaderValue, PrepareVerify(http_signature_normalization::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: http_signature_normalization::PrepareVerifyError) -> Self { Error::PrepareVerify(e) } } impl From for Error { fn from(_: actix_web::http::header::ToStrError) -> Self { Error::InvalidHeaderValue } } pub struct SignatureScheme { config_generator: std::marker::PhantomData, key: std::marker::PhantomData, digest_verifier: Option, unverified: http_signature_normalization::verify::Unverified, } #[derive(Debug)] pub struct DigestPart { pub algorithm: String, pub digest: String, } #[derive(Debug)] pub struct Signature { key: K, unverified: http_signature_normalization::verify::Unverified, digest_verifier: D, digest_parts: Option>, } pub trait DigestName { const NAME: &'static str; } pub trait ConfigGenerator { fn config() -> Config; } #[async_trait::async_trait(?Send)] pub trait PrivateKey: Sized + Send { type Error: Into; async fn init( req: &actix_web::HttpRequest, key_id: &str, algorithm: Option<&Algorithm>, ) -> Result; fn verify(&mut self, signature: &str, signing_string: &str) -> Result; } pub trait VerifyDigest: Default + Send + 'static { const REQUIRED: bool = true; fn update(&mut self, bytes: &[u8]); fn verify(&mut self, parts: &[DigestPart]) -> bool; } #[async_trait::async_trait(?Send)] impl actix_web_lab::extract::RequestSignatureScheme for SignatureScheme where C: ConfigGenerator, D: VerifyDigest, K: PrivateKey, { 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(SignatureScheme { 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(|digest| parse_digest(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 { if !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 } }