http-signature-normalization-actix 0.3.0-alpha.2

An HTTP Signatures library that leaves the signing to you
Documentation
#![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: &str,
//!         signature: &str,
//!         signing_string: &str,
//!     ) -> 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(_: (DigestVerified, SignatureVerified)) -> &'static str {
//!     "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;
//! 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 mut digest = Sha256::new();
//!
//!     let mut response = Client::default()
//!         .post("http://127.0.0.1:8010/")
//!         .header("User-Agent", "Actix Web")
//!         .authorization_signature_with_digest(&config, "my-key-id", &mut digest, "Hewwo-owo", |s| {
//!             Ok(base64::encode(s)) as Result<_, MyError>
//!         })?
//!         .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 read header, {0}")]
//!     Convert(#[from] ToStrError),
//!
//!     #[error("Failed to create header, {0}")]
//!     Header(#[from] InvalidHeaderValue),
//!
//!     #[error("Failed to send request")]
//!     SendRequest,
//!
//!     #[error("Failed to retrieve request body")]
//!     Body,
//! }
//! ```

use actix_web::http::{
    header::{HeaderMap, InvalidHeaderValue, ToStrError},
    uri::PathAndQuery,
    Method,
};

use std::{collections::BTreeMap, fmt::Display, future::Future};

mod sign;

#[cfg(feature = "digest")]
pub mod digest;

pub mod create;
pub mod middleware;

/// Useful types and traits for using this library in Actix Web
pub mod prelude {
    pub use crate::{
        middleware::{SignatureVerified, VerifySignature},
        verify::{Algorithm, Unverified},
        Config, PrepareVerifyError, 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: &str,
        signature: &str,
        signing_string: &str,
    ) -> 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) -> Result<Self, E>
    where
        F: FnOnce(&str) -> Result<String, E>,
        E: From<ToStrError> + From<InvalidHeaderValue>,
        K: Display,
        Self: Sized;

    /// Add a Signature to the request
    fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
    where
        F: FnOnce(&str) -> Result<String, E>,
        E: From<ToStrError> + From<InvalidHeaderValue>,
        K: Display,
        Self: Sized;
}

#[derive(Clone, Debug, Default)]
/// A thin wrapper around the underlying library's config type
pub struct Config {
    /// The inner config type
    pub config: http_signature_normalization::Config,
}

#[derive(Debug, thiserror::Error)]
/// An error when preparing to verify a request
pub enum PrepareVerifyError {
    #[error("Signature error, {0}")]
    /// An error validating the request
    Sig(#[from] http_signature_normalization::PrepareVerifyError),

    #[error("Failed to read header, {0}")]
    /// An error converting the header to a string for validation
    Header(#[from] ToStrError),
}

impl Config {
    /// Begin the process of singing a request
    pub fn begin_sign(
        &self,
        method: &Method,
        path_and_query: Option<&PathAndQuery>,
        headers: HeaderMap,
    ) -> Result<Unsigned, ToStrError> {
        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)
    }
}