mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-22 09:21:00 +00:00
332 lines
9.7 KiB
Rust
332 lines
9.7 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 actix_web's ClientRequest type to
|
|
//! add signatures and digests to the request
|
|
//!
|
|
//! ### Use it in a server
|
|
//! ```rust,ignore
|
|
//! use actix::System;
|
|
//! use actix_web::{web, App, HttpResponse, HttpServer, ResponseError};
|
|
//! use failure::Fail;
|
|
//! use http_signature_normalization_actix::{prelude::*, verify::Algorithm};
|
|
//! use sha2::{Digest, Sha256};
|
|
//!
|
|
//! #[derive(Clone, Debug)]
|
|
//! struct MyVerify;
|
|
//!
|
|
//! impl SignatureVerify for MyVerify {
|
|
//! type Error = MyError;
|
|
//! type Future = 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 = base64::decode(signature).map_err(|_| MyError::Decode)?;
|
|
//!
|
|
//! // In a real system, you'd want to actually verify a signature, not just check for
|
|
//! // byte equality
|
|
//! Ok(decoded == signing_string.as_bytes())
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! fn index(_: (DigestVerified, SignatureVerified)) -> &'static str {
|
|
//! "Eyyyyup"
|
|
//! }
|
|
//!
|
|
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
//! let sys = System::new("server-example");
|
|
//!
|
|
//! 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")?
|
|
//! .start();
|
|
//!
|
|
//! sys.run()?;
|
|
//! Ok(())
|
|
//! }
|
|
//!
|
|
//! #[derive(Debug, Fail)]
|
|
//! enum MyError {
|
|
//! #[fail(display = "Failed to verify, {}", _0)]
|
|
//! Verify(#[cause] PrepareVerifyError),
|
|
//!
|
|
//! #[fail(display = "Unsupported algorithm")]
|
|
//! Algorithm,
|
|
//!
|
|
//! #[fail(display = "Couldn't decode signature")]
|
|
//! Decode,
|
|
//!
|
|
//! #[fail(display = "Invalid key")]
|
|
//! Key,
|
|
//! }
|
|
//!
|
|
//! impl ResponseError for MyError {
|
|
//! fn error_response(&self) -> HttpResponse {
|
|
//! HttpResponse::BadRequest().finish()
|
|
//! }
|
|
//!
|
|
//! fn render_response(&self) -> HttpResponse {
|
|
//! self.error_response()
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! impl From<PrepareVerifyError> for MyError {
|
|
//! fn from(e: PrepareVerifyError) -> Self {
|
|
//! MyError::Verify(e)
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
//!
|
|
//! ### Use it in a client
|
|
//! ```rust,ignore
|
|
//! use actix::System;
|
|
//! use actix_web::client::Client;
|
|
//! use failure::Fail;
|
|
//! use futures::future::{lazy, Future};
|
|
//! use http_signature_normalization_actix::prelude::*;
|
|
//! use sha2::{Digest, Sha256};
|
|
//!
|
|
//! fn main() {
|
|
//! System::new("client-example")
|
|
//! .block_on(lazy(|| {
|
|
//! let config = Config::default();
|
|
//! let mut digest = Sha256::new();
|
|
//!
|
|
//! 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| {
|
|
//! // In a real-world system, you'd actually want to sign the string,
|
|
//! // not just base64 encode it
|
|
//! Ok(base64::encode(s)) as Result<_, MyError>
|
|
//! },
|
|
//! )
|
|
//! .unwrap()
|
|
//! .send()
|
|
//! .map_err(|_| ())
|
|
//! .and_then(|mut res| res.body().map_err(|_| ()))
|
|
//! .map(|body| {
|
|
//! println!("{:?}", body);
|
|
//! })
|
|
//! }))
|
|
//! .unwrap();
|
|
//! }
|
|
//!
|
|
//! #[derive(Debug, Fail)]
|
|
//! pub enum MyError {
|
|
//! #[fail(display = "Failed to read header, {}", _0)]
|
|
//! Convert(#[cause] ToStrError),
|
|
//!
|
|
//! #[fail(display = "Failed to create header, {}", _0)]
|
|
//! Header(#[cause] InvalidHeaderValue),
|
|
//! }
|
|
//!
|
|
//! impl From<ToStrError> for MyError {
|
|
//! fn from(e: ToStrError) -> Self {
|
|
//! MyError::Convert(e)
|
|
//! }
|
|
//! }
|
|
//!
|
|
//! impl From<InvalidHeaderValue> for MyError {
|
|
//! fn from(e: InvalidHeaderValue) -> Self {
|
|
//! MyError::Header(e)
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
|
|
use actix_web::http::{
|
|
header::{HeaderMap, InvalidHeaderValue, ToStrError},
|
|
uri::PathAndQuery,
|
|
Method,
|
|
};
|
|
|
|
use failure::Fail;
|
|
use futures::future::IntoFuture;
|
|
use std::{collections::BTreeMap, fmt::Display};
|
|
|
|
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::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: IntoFuture<Item = bool, Error = 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, Fail)]
|
|
/// An error when preparing to verify a request
|
|
pub enum PrepareVerifyError {
|
|
#[fail(display = "Signature error, {}", _0)]
|
|
/// An error validating the request
|
|
Sig(#[cause] http_signature_normalization::PrepareVerifyError),
|
|
|
|
#[fail(display = "Failed to read header, {}", _0)]
|
|
/// An error converting the header to a string for validation
|
|
Header(#[cause] 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)
|
|
}
|
|
}
|
|
|
|
impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
|
|
fn from(e: http_signature_normalization::PrepareVerifyError) -> Self {
|
|
PrepareVerifyError::Sig(e)
|
|
}
|
|
}
|
|
|
|
impl From<ToStrError> for PrepareVerifyError {
|
|
fn from(e: ToStrError) -> Self {
|
|
PrepareVerifyError::Header(e)
|
|
}
|
|
}
|