From 77ecf6eaa775a0ee377ff4856473bbc63b1e9071 Mon Sep 17 00:00:00 2001 From: asonix Date: Fri, 17 Jan 2020 16:31:44 -0600 Subject: [PATCH] Include digest verification --- http-signature-normalization-warp/Cargo.toml | 15 ++- .../src/digest/mod.rs | 119 ++++++++++++++++++ .../src/digest/sha_2.rs | 30 +++++ http-signature-normalization-warp/src/lib.rs | 3 + 4 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 http-signature-normalization-warp/src/digest/mod.rs create mode 100644 http-signature-normalization-warp/src/digest/sha_2.rs diff --git a/http-signature-normalization-warp/Cargo.toml b/http-signature-normalization-warp/Cargo.toml index 02dd66f..25aa401 100644 --- a/http-signature-normalization-warp/Cargo.toml +++ b/http-signature-normalization-warp/Cargo.toml @@ -5,9 +5,20 @@ authors = ["asonix "] edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = ["sha-2"] +digest = ["base64", "serde", "serde_json", "serde_urlencoded", "thiserror"] +sha-2 = ["digest", "sha2"] [dependencies] +base64 = { version = "0.11.0", optional = true } +bytes = "0.5.3" +futures = "0.3.1" http = "0.2.0" http-signature-normalization-http = { version = "0.2.0", path = "../http-signature-normalization-http" } -warp = { git = "https://github.com/seanmonstar/warp" } -futures = "0.3.1" +serde = { version = "1.0.104", features = ["derive"], optional = true } +serde_json = { version = "1.0.44", optional = true } +serde_urlencoded = { version = "0.6.1", optional = true } +sha2 = { version = "0.8.1", optional = true } +thiserror = { version = "1.0.9", optional = true } +warp = "0.2.0" diff --git a/http-signature-normalization-warp/src/digest/mod.rs b/http-signature-normalization-warp/src/digest/mod.rs new file mode 100644 index 0000000..0eadf6f --- /dev/null +++ b/http-signature-normalization-warp/src/digest/mod.rs @@ -0,0 +1,119 @@ +use bytes::Bytes; +use std::str::FromStr; +use warp::{body, header, Filter, Rejection}; + +#[cfg(feature = "sha-2")] +mod sha_2; + +pub trait DigestVerify { + fn verify(&mut self, digests: &[DigestPart], payload: &[u8]) -> bool; +} + +pub struct DigestPart { + pub algorithm: String, + pub digest: String, +} + +struct Digest { + parts: Vec, +} + +#[derive(Clone, Debug, thiserror::Error)] +#[error("Could not verify digest")] +pub struct VerifyError; + +#[derive(Clone, Debug, thiserror::Error)] +pub enum ParseDigestError { + #[error("Missing algorithm")] + Algorithm, + + #[error("Missing digest")] + Digest, +} + +#[derive(Clone, Debug, thiserror::Error)] +#[error("Could not parse request body")] +pub struct ParseBodyError; + +pub fn verify_digest_bytes( + verifier: impl Filter + Clone, +) -> impl Filter + Clone +where + D: DigestVerify + Send, +{ + verifier + .and(parse_digest_header()) + .and(body::bytes()) + .and_then(|mut verifier: D, parts: Vec, bytes: Bytes| { + async move { + if verifier.verify(&parts, &bytes) { + Ok(bytes) + } else { + Err(warp::reject::custom(VerifyError)) + } + } + }) +} + +pub fn verify_digest_json( + verifier: impl Filter + Clone, +) -> impl Filter + Clone +where + D: DigestVerify + Send, + T: serde::de::DeserializeOwned, +{ + verify_digest_bytes(verifier).and_then(|bytes: Bytes| { + async move { + serde_json::from_slice(&bytes).map_err(|_| warp::reject::custom(ParseBodyError)) + } + }) +} + +pub fn verify_digest_form( + verifier: impl Filter + Clone, +) -> impl Filter + Clone +where + D: DigestVerify + Send, + T: serde::de::DeserializeOwned, +{ + verify_digest_bytes(verifier).and_then(|bytes: Bytes| { + async move { + serde_urlencoded::from_bytes(&bytes).map_err(|_| warp::reject::custom(ParseBodyError)) + } + }) +} + +fn parse_digest_header() -> impl Filter,), Error = Rejection> + Clone { + header::header::("Digest").map(|d: Digest| d.parts) +} + +impl FromStr for DigestPart { + type Err = ParseDigestError; + + fn from_str(s: &str) -> Result { + let mut iter = s.splitn(2, "="); + let algorithm = iter.next().ok_or(ParseDigestError::Algorithm)?; + let digest = iter.next().ok_or(ParseDigestError::Digest)?; + + Ok(DigestPart { + algorithm: algorithm.to_owned(), + digest: digest.to_owned(), + }) + } +} + +impl FromStr for Digest { + type Err = ParseDigestError; + + fn from_str(s: &str) -> Result { + let parts: Result, Self::Err> = s + .split(",") + .map(|s: &str| DigestPart::from_str(s)) + .collect(); + + Ok(Digest { parts: parts? }) + } +} + +impl warp::reject::Reject for VerifyError {} +impl warp::reject::Reject for ParseBodyError {} diff --git a/http-signature-normalization-warp/src/digest/sha_2.rs b/http-signature-normalization-warp/src/digest/sha_2.rs new file mode 100644 index 0000000..b5f0ce9 --- /dev/null +++ b/http-signature-normalization-warp/src/digest/sha_2.rs @@ -0,0 +1,30 @@ +use super::{DigestPart, DigestVerify}; +use sha2::Digest; + +fn verify(verifier: &mut D, parts: &[DigestPart], payload: &[u8], name: &str) -> bool +where + D: Digest, +{ + let part = if let Some(part) = parts.iter().find(|p| p.algorithm == name) { + part + } else { + return false; + }; + + verifier.input(payload); + let computed = base64::encode(&verifier.result_reset()); + + computed == part.digest +} + +impl DigestVerify for sha2::Sha256 { + fn verify(&mut self, digests: &[DigestPart], payload: &[u8]) -> bool { + verify(self, digests, payload, "sha-256") + } +} + +impl DigestVerify for sha2::Sha512 { + fn verify(&mut self, digests: &[DigestPart], payload: &[u8]) -> bool { + verify(self, digests, payload, "sha-512") + } +} diff --git a/http-signature-normalization-warp/src/lib.rs b/http-signature-normalization-warp/src/lib.rs index 38970be..d5cd303 100644 --- a/http-signature-normalization-warp/src/lib.rs +++ b/http-signature-normalization-warp/src/lib.rs @@ -6,6 +6,9 @@ pub use http_signature_normalization_http::{ use std::{future::Future, pin::Pin}; use warp::{path::FullPath, Filter, Rejection}; +#[cfg(feature = "digest")] +pub mod digest; + pub fn prepare_unverified( config: Config, ) -> impl Filter + Clone {