Add Http Signature verification middleware and...

Move base64 out of base lib
This commit is contained in:
asonix 2019-09-13 17:55:51 -05:00
parent c468513d5a
commit 686e52213e
14 changed files with 276 additions and 59 deletions

View file

@ -16,5 +16,4 @@ members = [
]
[dependencies]
base64 = "0.10"
chrono = "0.4"

View file

@ -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 }

View file

@ -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()

View file

@ -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<Config>)) -> 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<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),
};
let decoded = base64::decode(signature).map_err(|_| MyError::Decode)?;
Ok(decoded == signing_string.as_bytes())
}
}
if unverified.verify(|bytes, string| bytes == string.as_bytes()) {
Ok("Eyyyyup")
} else {
Ok("Nope")
}
fn index() -> &'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()
.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<VerifyError> for MyError {

View file

@ -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<F, E>(self, key_id: String, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
{
let signed = self.unsigned.sign(key_id, f)?;
Ok(Signed { signed })

View file

@ -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,15 +103,13 @@ where
Either::B(err(VerifyError.into()))
}
}))
} else {
if self.1 {
} else if self.1 {
Box::new(err(VerifyError.into()))
} else {
Box::new(self.0.borrow_mut().call(req))
}
}
}
}
fn parse_digest(h: &HeaderValue) -> Option<Vec<DigestPart>> {
let h = h.to_str().ok()?.split(";").next()?;

View file

@ -37,7 +37,7 @@ pub trait SignExt: Sign {
f: F,
) -> Result<DigestClient<V>, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
D: DigestCreate,
@ -53,7 +53,7 @@ pub trait SignExt: Sign {
f: F,
) -> Result<DigestClient<V>, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
D: DigestCreate,

View file

@ -19,7 +19,7 @@ impl SignExt for ClientRequest {
f: F,
) -> Result<DigestClient<V>, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
D: DigestCreate,
@ -42,7 +42,7 @@ impl SignExt for ClientRequest {
f: F,
) -> Result<DigestClient<V>, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
D: DigestCreate,

View file

@ -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<Unverified, VerifyError>;
}
pub trait SignatureVerify {
type Error: actix_web::ResponseError;
type Future: IntoFuture<Item = bool, Error = Self::Error>;
fn signature_verify(
&mut self,
algorithm: Option<Algorithm>,
key_id: &str,
signature: &str,
signing_string: &str,
) -> Self::Future;
}
pub trait Sign {
fn authorization_signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
Self: Sized;
fn signature<F, E, K>(self, config: &Config, key_id: K, f: F) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
Self: Sized;
}
#[derive(Clone, Default)]
#[derive(Clone, Debug, Default)]
pub struct Config {
pub config: http_signature_normalization::Config,
}

View file

@ -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>(T, Config, HeaderKind, bool);
#[derive(Clone, Debug)]
pub struct VerifyMiddleware<T, S>(Rc<RefCell<S>>, 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<T> VerifySignature<T>
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<T, S> VerifyMiddleware<T, S>
where
T: SignatureVerify + 'static,
T::Future: 'static,
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<Body>,
Error = actix_web::Error,
> + 'static,
S::Error: 'static,
{
fn handle(
&mut self,
req: ServiceRequest,
) -> Box<dyn Future<Item = ServiceResponse<Body>, 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::<actix_web::Error>()
.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<T, S> Transform<S> for VerifySignature<T>
where
T: SignatureVerify + Clone + 'static,
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<Body>,
Error = actix_web::Error,
> + 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<Body>;
type Error = actix_web::Error;
type Transform = VerifyMiddleware<T, S>;
type InitError = ();
type Future = FutureResult<Self::Transform, Self::InitError>;
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<T, S> Service for VerifyMiddleware<T, S>
where
T: SignatureVerify + Clone + 'static,
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<Body>,
Error = actix_web::Error,
> + 'static,
S::Error: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<Body>;
type Error = actix_web::Error;
type Future = Box<dyn Future<Item = Self::Response, Error = Self::Error>>;
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()
}
}

View file

@ -14,7 +14,7 @@ impl Sign for ClientRequest {
f: F,
) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
{
@ -25,7 +25,7 @@ impl Sign for ClientRequest {
fn signature<F, E, K>(mut self, config: &Config, key_id: K, f: F) -> Result<Self, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError> + From<InvalidHeaderValue>,
K: Display,
{
@ -37,7 +37,7 @@ impl Sign for ClientRequest {
fn prepare<F, E, K>(request: &ClientRequest, config: &Config, key_id: K, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
E: From<ToStrError>,
K: Display,
{

View file

@ -52,10 +52,10 @@ impl Signed {
impl Unsigned {
pub fn sign<F, E>(self, key_id: String, f: F) -> Result<Signed, E>
where
F: FnOnce(&str) -> Result<Vec<u8>, E>,
F: FnOnce(&str) -> Result<String, E>,
{
(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,

View file

@ -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);
}

View file

@ -14,7 +14,7 @@ use crate::{
#[derive(Debug)]
pub struct Unverified {
key_id: String,
signature: Vec<u8>,
signature: String,
algorithm: Option<Algorithm>,
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<F, T>(&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",
}
}
}