From 686e52213e750f760b3f8686d7e7349d4166b873 Mon Sep 17 00:00:00 2001 From: asonix Date: Fri, 13 Sep 2019 17:55:51 -0500 Subject: [PATCH] Add Http Signature verification middleware and... Move base64 out of base lib --- Cargo.toml | 1 - http-signature-normalization-actix/Cargo.toml | 4 +- .../examples/client.rs | 4 +- .../examples/server.rs | 51 +++-- .../src/create.rs | 11 +- .../src/digest/middleware.rs | 10 +- .../src/digest/mod.rs | 4 +- .../src/digest/sign.rs | 4 +- http-signature-normalization-actix/src/lib.rs | 36 +++- .../src/middleware.rs | 183 ++++++++++++++++++ .../src/sign.rs | 6 +- src/create.rs | 6 +- src/lib.rs | 4 +- src/verify.rs | 11 +- 14 files changed, 276 insertions(+), 59 deletions(-) create mode 100644 http-signature-normalization-actix/src/middleware.rs diff --git a/Cargo.toml b/Cargo.toml index c2d388e..1a046b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,5 +16,4 @@ members = [ ] [dependencies] -base64 = "0.10" chrono = "0.4" diff --git a/http-signature-normalization-actix/Cargo.toml b/http-signature-normalization-actix/Cargo.toml index 8885e48..d6c4734 100644 --- a/http-signature-normalization-actix/Cargo.toml +++ b/http-signature-normalization-actix/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] default = [] -digest = ["base64", "futures"] +digest = ["base64"] sha-2 = ["digest", "sha2"] sha-3 = ["digest", "sha3"] @@ -26,7 +26,7 @@ required-features = ["sha-2"] actix-web = "1.0" base64 = { version = "0.10", optional = true } failure = "0.1" -futures = { version = "0.1", optional = true } +futures = "0.1" http-signature-normalization = { version = "0.1.0", path = ".." } sha2 = { version = "0.8", optional = true } sha3 = { version = "0.8", optional = true } diff --git a/http-signature-normalization-actix/examples/client.rs b/http-signature-normalization-actix/examples/client.rs index 6bacab9..3e4dd0b 100644 --- a/http-signature-normalization-actix/examples/client.rs +++ b/http-signature-normalization-actix/examples/client.rs @@ -18,8 +18,8 @@ fn main() { &config, "my-key-id", &mut digest, - "Hewwo owo", - |s| Ok(s.as_bytes().to_vec()), + "Hewwo-owo", + |s| Ok(base64::encode(s)), ) .unwrap() .send() diff --git a/http-signature-normalization-actix/examples/server.rs b/http-signature-normalization-actix/examples/server.rs index 69c920b..fdc3a70 100644 --- a/http-signature-normalization-actix/examples/server.rs +++ b/http-signature-normalization-actix/examples/server.rs @@ -1,33 +1,47 @@ use actix::System; -use actix_web::{web, App, HttpRequest, HttpServer, ResponseError}; +use actix_web::{web, App, HttpResponse, HttpServer, ResponseError}; use failure::Fail; use http_signature_normalization_actix::{prelude::*, verify::Algorithm}; use sha2::{Digest, Sha256}; -fn index((req, config): (HttpRequest, web::Data)) -> Result<&'static str, MyError> { - let unverified = req.begin_verify(&config)?; +#[derive(Clone, Debug)] +struct MyVerify; - if let Some(a) = unverified.algorithm() { - match *a { - Algorithm::Hs2019 => (), +impl SignatureVerify for MyVerify { + type Error = MyError; + type Future = Result; + + fn signature_verify( + &mut self, + algorithm: Option, + _key_id: &str, + signature: &str, + signing_string: &str, + ) -> Self::Future { + match algorithm { + Some(Algorithm::Hs2019) => (), _ => return Err(MyError::Algorithm), - } - } + }; - if unverified.verify(|bytes, string| bytes == string.as_bytes()) { - Ok("Eyyyyup") - } else { - Ok("Nope") + let decoded = base64::decode(signature).map_err(|_| MyError::Decode)?; + + Ok(decoded == signing_string.as_bytes()) } } +fn index() -> &'static str { + "Eyyyyup" +} + fn main() -> Result<(), Box> { let sys = System::new("server-example"); + let config = Config::default(); + HttpServer::new(move || { App::new() - .data(Config::default()) .wrap(VerifyDigest::new(Sha256::new())) + .wrap(VerifySignature::new(MyVerify, config.clone()).authorization()) .route("/", web::post().to(index)) }) .bind("127.0.0.1:8010")? @@ -44,10 +58,19 @@ enum MyError { #[fail(display = "Unsupported algorithm")] Algorithm, + + #[fail(display = "Couldn't decode signature")] + Decode, } impl ResponseError for MyError { - // default 500 + fn error_response(&self) -> HttpResponse { + HttpResponse::BadRequest().finish() + } + + fn render_response(&self) -> HttpResponse { + self.error_response() + } } impl From for MyError { diff --git a/http-signature-normalization-actix/src/create.rs b/http-signature-normalization-actix/src/create.rs index 12033e7..d4c15a3 100644 --- a/http-signature-normalization-actix/src/create.rs +++ b/http-signature-normalization-actix/src/create.rs @@ -12,19 +12,18 @@ pub struct Unsigned { impl Signed { pub fn signature_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { + let sig_header = self.signed.signature_header(); hm.insert( HeaderName::from_static("Signature"), - HeaderValue::from_str(&self.signed.signature_header())?, + HeaderValue::from_str(&sig_header)?, ); Ok(()) } pub fn authorization_header(self, hm: &mut HeaderMap) -> Result<(), InvalidHeaderValue> { - hm.insert( - AUTHORIZATION, - HeaderValue::from_str(&self.signed.authorization_header())?, - ); + let auth_header = self.signed.authorization_header(); + hm.insert(AUTHORIZATION, HeaderValue::from_str(&auth_header)?); Ok(()) } } @@ -32,7 +31,7 @@ impl Signed { impl Unsigned { pub fn sign(self, key_id: String, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, { let signed = self.unsigned.sign(key_id, f)?; Ok(Signed { signed }) diff --git a/http-signature-normalization-actix/src/digest/middleware.rs b/http-signature-normalization-actix/src/digest/middleware.rs index 046bc1d..3d96141 100644 --- a/http-signature-normalization-actix/src/digest/middleware.rs +++ b/http-signature-normalization-actix/src/digest/middleware.rs @@ -81,7 +81,6 @@ where } fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - let mut verify_digest = self.2.clone(); if let Some(digest) = req.headers().get("Digest") { let vec = match parse_digest(digest) { Some(vec) => vec, @@ -89,6 +88,7 @@ where }; let payload = req.take_payload(); let service = self.0.clone(); + let mut verify_digest = self.2.clone(); Box::new(payload.concat2().from_err().and_then(move |bytes| { if verify_digest.verify(&vec, &bytes.as_ref()) { @@ -103,12 +103,10 @@ where Either::B(err(VerifyError.into())) } })) + } else if self.1 { + Box::new(err(VerifyError.into())) } else { - if self.1 { - Box::new(err(VerifyError.into())) - } else { - Box::new(self.0.borrow_mut().call(req)) - } + Box::new(self.0.borrow_mut().call(req)) } } } diff --git a/http-signature-normalization-actix/src/digest/mod.rs b/http-signature-normalization-actix/src/digest/mod.rs index 51c5244..d27a486 100644 --- a/http-signature-normalization-actix/src/digest/mod.rs +++ b/http-signature-normalization-actix/src/digest/mod.rs @@ -37,7 +37,7 @@ pub trait SignExt: Sign { f: F, ) -> Result, E> where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, D: DigestCreate, @@ -53,7 +53,7 @@ pub trait SignExt: Sign { f: F, ) -> Result, E> where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, D: DigestCreate, diff --git a/http-signature-normalization-actix/src/digest/sign.rs b/http-signature-normalization-actix/src/digest/sign.rs index ce6e0d8..267849c 100644 --- a/http-signature-normalization-actix/src/digest/sign.rs +++ b/http-signature-normalization-actix/src/digest/sign.rs @@ -19,7 +19,7 @@ impl SignExt for ClientRequest { f: F, ) -> Result, E> where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, D: DigestCreate, @@ -42,7 +42,7 @@ impl SignExt for ClientRequest { f: F, ) -> Result, E> where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, D: DigestCreate, diff --git a/http-signature-normalization-actix/src/lib.rs b/http-signature-normalization-actix/src/lib.rs index 87ed363..398decb 100644 --- a/http-signature-normalization-actix/src/lib.rs +++ b/http-signature-normalization-actix/src/lib.rs @@ -4,6 +4,7 @@ use actix_web::http::{ Method, }; use failure::Fail; +use futures::future::IntoFuture; use std::{collections::BTreeMap, fmt::Display}; mod sign; @@ -11,9 +12,13 @@ mod sign; #[cfg(feature = "digest")] pub mod digest; -pub mod verify; +pub mod create; +pub mod middleware; pub mod prelude { - pub use crate::{verify::Unverified, Config, Sign, Verify, VerifyError}; + pub use crate::{ + middleware::VerifySignature, verify::Unverified, Config, Sign, SignatureVerify, Verify, + VerifyError, + }; #[cfg(feature = "digest")] pub use crate::digest::{ @@ -22,32 +27,47 @@ pub mod prelude { pub use actix_web::http::header::{InvalidHeaderValue, ToStrError}; } +pub mod verify; -pub mod create; - -use self::{create::Unsigned, verify::Unverified}; +use self::{ + create::Unsigned, + verify::{Algorithm, Unverified}, +}; pub trait Verify { fn begin_verify(&self, config: &Config) -> Result; } +pub trait SignatureVerify { + type Error: actix_web::ResponseError; + type Future: IntoFuture; + + fn signature_verify( + &mut self, + algorithm: Option, + key_id: &str, + signature: &str, + signing_string: &str, + ) -> Self::Future; +} + pub trait Sign { fn authorization_signature(self, config: &Config, key_id: K, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, Self: Sized; fn signature(self, config: &Config, key_id: K, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, Self: Sized; } -#[derive(Clone, Default)] +#[derive(Clone, Debug, Default)] pub struct Config { pub config: http_signature_normalization::Config, } diff --git a/http-signature-normalization-actix/src/middleware.rs b/http-signature-normalization-actix/src/middleware.rs new file mode 100644 index 0000000..49a2b77 --- /dev/null +++ b/http-signature-normalization-actix/src/middleware.rs @@ -0,0 +1,183 @@ +use actix_web::{ + body::Body, + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + HttpResponse, ResponseError, +}; +use failure::Fail; +use futures::{ + future::{err, ok, Either, FutureResult, IntoFuture}, + Future, Poll, +}; +use std::{cell::RefCell, rc::Rc}; + +use crate::{Config, SignatureVerify}; + +#[derive(Clone, Debug)] +pub struct VerifySignature(T, Config, HeaderKind, bool); +#[derive(Clone, Debug)] +pub struct VerifyMiddleware(Rc>, Config, HeaderKind, bool, T); +#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum HeaderKind { + Authorization, + Signature, +} +#[derive(Clone, Debug, Fail)] +#[fail(display = "Failed to verify http signature")] +pub struct VerifyError; + +impl VerifySignature +where + T: SignatureVerify, +{ + pub fn new(verify_signature: T, config: Config) -> Self { + VerifySignature(verify_signature, config, HeaderKind::Signature, true) + } + + pub fn authorization(self) -> Self { + VerifySignature(self.0, self.1, HeaderKind::Authorization, self.3) + } + + pub fn optional(self) -> Self { + VerifySignature(self.0, self.1, self.2, false) + } +} + +impl VerifyMiddleware +where + T: SignatureVerify + 'static, + T::Future: 'static, + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + > + 'static, + S::Error: 'static, +{ + fn handle( + &mut self, + req: ServiceRequest, + ) -> Box, Error = actix_web::Error>> { + let res = self.1.begin_verify( + req.method(), + req.uri().path_and_query(), + req.headers().clone(), + ); + + let unverified = match res { + Ok(unverified) => unverified, + Err(_) => return Box::new(err(VerifyError.into())), + }; + + let algorithm = unverified.algorithm().map(|a| a.clone()); + let key_id = unverified.key_id().to_owned(); + + let verified = unverified.verify(|signature, signing_string| { + self.4 + .signature_verify(algorithm, &key_id, signature, signing_string) + }); + + let service = self.0.clone(); + + Box::new( + verified + .into_future() + .from_err::() + .and_then(move |verified| { + if verified { + Either::A(service.borrow_mut().call(req)) + } else { + Either::B(err(VerifyError.into())) + } + }), + ) + } +} + +impl HeaderKind { + pub fn is_authorization(&self) -> bool { + HeaderKind::Authorization == *self + } + + pub fn is_signature(&self) -> bool { + HeaderKind::Signature == *self + } +} + +impl Transform for VerifySignature +where + T: SignatureVerify + Clone + 'static, + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + > + 'static, + S::Error: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = VerifyMiddleware; + type InitError = (); + type Future = FutureResult; + + fn new_transform(&self, service: S) -> Self::Future { + ok(VerifyMiddleware( + Rc::new(RefCell::new(service)), + self.1.clone(), + self.2, + self.3, + self.0.clone(), + )) + } +} + +impl Service for VerifyMiddleware +where + T: SignatureVerify + Clone + 'static, + S: Service< + Request = ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + > + 'static, + S::Error: 'static, +{ + type Request = ServiceRequest; + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = Box>; + + fn poll_ready(&mut self) -> Poll<(), Self::Error> { + self.0.borrow_mut().poll_ready() + } + + fn call(&mut self, req: ServiceRequest) -> Self::Future { + let authorization = req.headers().get("Authorization").is_some(); + let signature = req.headers().get("Signature").is_some(); + + if authorization || signature { + if self.2.is_authorization() && authorization { + return self.handle(req); + } + + if self.2.is_signature() && signature { + return self.handle(req); + } + + Box::new(err(VerifyError.into())) + } else if self.3 { + Box::new(self.0.borrow_mut().call(req)) + } else { + Box::new(err(VerifyError.into())) + } + } +} + +impl ResponseError for VerifyError { + fn error_response(&self) -> HttpResponse { + HttpResponse::BadRequest().finish() + } + + fn render_response(&self) -> HttpResponse { + self.error_response() + } +} diff --git a/http-signature-normalization-actix/src/sign.rs b/http-signature-normalization-actix/src/sign.rs index 4e7a592..27d1157 100644 --- a/http-signature-normalization-actix/src/sign.rs +++ b/http-signature-normalization-actix/src/sign.rs @@ -14,7 +14,7 @@ impl Sign for ClientRequest { f: F, ) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, { @@ -25,7 +25,7 @@ impl Sign for ClientRequest { fn signature(mut self, config: &Config, key_id: K, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From + From, K: Display, { @@ -37,7 +37,7 @@ impl Sign for ClientRequest { fn prepare(request: &ClientRequest, config: &Config, key_id: K, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, E: From, K: Display, { diff --git a/src/create.rs b/src/create.rs index 3e81521..35931b0 100644 --- a/src/create.rs +++ b/src/create.rs @@ -52,10 +52,10 @@ impl Signed { impl Unsigned { pub fn sign(self, key_id: String, f: F) -> Result where - F: FnOnce(&str) -> Result, E>, + F: FnOnce(&str) -> Result, { - (f)(&self.signing_string).map(|v| Signed { - signature: base64::encode(&v), + (f)(&self.signing_string).map(|signature| Signed { + signature, sig_headers: self.sig_headers, created: self.created, expires: self.expires, diff --git a/src/lib.rs b/src/lib.rs index bef9060..cf619e1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,7 +196,7 @@ mod tests { let authorization_header = config .begin_sign("GET", "/foo?bar=baz", headers) .sign("hi".to_owned(), |s| { - Ok(s.as_bytes().to_vec()) as Result<_, std::io::Error> + Ok(s.to_owned()) as Result<_, std::io::Error> }) .unwrap() .authorization_header(); @@ -207,7 +207,7 @@ mod tests { let verified = config .begin_verify("GET", "/foo?bar=baz", headers) .unwrap() - .verify(|bytes, string| string.as_bytes() == bytes); + .verify(|sig, signing_string| sig == signing_string); assert!(verified); } diff --git a/src/verify.rs b/src/verify.rs index 9196995..91b2c1f 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -14,7 +14,7 @@ use crate::{ #[derive(Debug)] pub struct Unverified { key_id: String, - signature: Vec, + signature: String, algorithm: Option, signing_string: String, } @@ -68,7 +68,6 @@ pub enum Algorithm { pub enum ValidateError { Missing, Expired, - Decode, } #[derive(Clone, Debug)] @@ -85,7 +84,7 @@ impl Unverified { pub fn verify(&self, f: F) -> T where - F: FnOnce(&[u8], &str) -> T, + F: FnOnce(&str, &str) -> T, { (f)(&self.signature, &self.signing_string) } @@ -104,13 +103,11 @@ impl Unvalidated { } } - let signature = base64::decode(&self.signature).map_err(|_| ValidateError::Decode)?; - Ok(Unverified { key_id: self.key_id, algorithm: self.algorithm, signing_string: self.signing_string, - signature, + signature: self.signature, }) } } @@ -247,7 +244,6 @@ impl fmt::Display for ValidateError { match *self { ValidateError::Missing => write!(f, "Http Signature is missing"), ValidateError::Expired => write!(f, "Http Signature is expired"), - ValidateError::Decode => write!(f, "Http Signature could not be decoded"), } } } @@ -257,7 +253,6 @@ impl Error for ValidateError { match *self { ValidateError::Missing => "Http Signature is missing", ValidateError::Expired => "Http Signature is expired", - ValidateError::Decode => "Http Signature could not be decoded", } } }