From 90660b7f19aaec15cb2e1230f3b119bc54caf9b6 Mon Sep 17 00:00:00 2001 From: asonix Date: Thu, 23 Apr 2020 12:54:56 -0500 Subject: [PATCH] Add ability to require headers in signature --- Cargo.toml | 2 +- http-signature-normalization-actix/Cargo.toml | 2 +- .../examples/client.rs | 7 +- .../examples/server.rs | 4 +- .../src/digest/mod.rs | 8 +-- .../src/digest/sign.rs | 11 ++- http-signature-normalization-actix/src/lib.rs | 42 +++++++++-- .../src/middleware.rs | 4 ++ .../src/sign.rs | 13 ++-- http-signature-normalization-http/Cargo.toml | 2 +- .../Cargo.toml | 2 +- src/lib.rs | 72 +++++++++++++++---- src/verify.rs | 16 +++-- 13 files changed, 131 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 88dc6d7..c9f71ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "http-signature-normalization" description = "An HTTP Signatures library that leaves the signing to you" -version = "0.4.2" +version = "0.5.0" authors = ["asonix "] license-file = "LICENSE" readme = "README.md" diff --git a/http-signature-normalization-actix/Cargo.toml b/http-signature-normalization-actix/Cargo.toml index 6d92f81..650227c 100644 --- a/http-signature-normalization-actix/Cargo.toml +++ b/http-signature-normalization-actix/Cargo.toml @@ -31,7 +31,7 @@ base64 = { version = "0.12", optional = true } bytes = "0.5.4" chrono = "0.4.6" futures = "0.3" -http-signature-normalization = { version = "0.4.2", path = ".." } +http-signature-normalization = { version = "0.5.0", path = ".." } log = "0.4" 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 83782ca..db9039e 100644 --- a/http-signature-normalization-actix/examples/client.rs +++ b/http-signature-normalization-actix/examples/client.rs @@ -9,6 +9,7 @@ async fn request(config: Config) -> Result<(), Box> { let mut response = Client::default() .post("http://127.0.0.1:8010/") .header("User-Agent", "Actix Web") + .header("Accept", "text/plain") .set(actix_web::http::header::Date(SystemTime::now().into())) .signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| { println!("Signing String\n{}", s); @@ -36,7 +37,7 @@ async fn main() -> Result<(), Box> { std::env::set_var("RUST_LOG", "info"); pretty_env_logger::init(); - let config = Config::default(); + let config = Config::default().require_header("accept"); request(config.clone()).await?; request(config.dont_use_created_field()).await?; @@ -45,8 +46,8 @@ async fn main() -> Result<(), Box> { #[derive(Debug, thiserror::Error)] pub enum MyError { - #[error("Failed to read header, {0}")] - Convert(#[from] ToStrError), + #[error("Failed to create signing string, {0}")] + Convert(#[from] PrepareSignError), #[error("Failed to create header, {0}")] Header(#[from] InvalidHeaderValue), diff --git a/http-signature-normalization-actix/examples/server.rs b/http-signature-normalization-actix/examples/server.rs index 2cec33e..ffbed83 100644 --- a/http-signature-normalization-actix/examples/server.rs +++ b/http-signature-normalization-actix/examples/server.rs @@ -56,7 +56,7 @@ async fn main() -> Result<(), Box> { std::env::set_var("RUST_LOG", "info"); pretty_env_logger::init(); - let config = Config::default(); + let config = Config::default().require_header("accept"); HttpServer::new(move || { App::new() @@ -74,7 +74,7 @@ async fn main() -> Result<(), Box> { #[derive(Debug, thiserror::Error)] enum MyError { - #[error("Failed to verify, {}", _0)] + #[error("Failed to verify, {0}")] Verify(#[from] PrepareVerifyError), #[error("Unsupported algorithm")] diff --git a/http-signature-normalization-actix/src/digest/mod.rs b/http-signature-normalization-actix/src/digest/mod.rs index ed8fcd9..9812cac 100644 --- a/http-signature-normalization-actix/src/digest/mod.rs +++ b/http-signature-normalization-actix/src/digest/mod.rs @@ -8,11 +8,11 @@ use actix_web::{ client::{ClientRequest, ClientResponse, SendRequestError}, dev::Payload, error::BlockingError, - http::header::{InvalidHeaderValue, ToStrError}, + http::header::InvalidHeaderValue, }; use std::{fmt::Display, future::Future, pin::Pin}; -use crate::{Config, Sign}; +use crate::{Config, PrepareSignError, Sign}; pub mod middleware; #[cfg(feature = "sha-2")] @@ -57,7 +57,7 @@ pub trait SignExt: Sign { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -79,7 +79,7 @@ pub trait SignExt: Sign { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send diff --git a/http-signature-normalization-actix/src/digest/sign.rs b/http-signature-normalization-actix/src/digest/sign.rs index f8e1063..411daa8 100644 --- a/http-signature-normalization-actix/src/digest/sign.rs +++ b/http-signature-normalization-actix/src/digest/sign.rs @@ -1,14 +1,11 @@ use actix_web::{ - client::ClientRequest, - error::BlockingError, - http::header::{InvalidHeaderValue, ToStrError}, - web, + client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web, }; use std::{fmt::Display, future::Future, pin::Pin}; use crate::{ digest::{DigestClient, DigestCreate, SignExt}, - Config, Sign, + Config, PrepareSignError, Sign, }; impl SignExt for ClientRequest { @@ -23,7 +20,7 @@ impl SignExt for ClientRequest { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -60,7 +57,7 @@ impl SignExt for ClientRequest { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send diff --git a/http-signature-normalization-actix/src/lib.rs b/http-signature-normalization-actix/src/lib.rs index 0990bf4..d3881ce 100644 --- a/http-signature-normalization-actix/src/lib.rs +++ b/http-signature-normalization-actix/src/lib.rs @@ -135,8 +135,8 @@ //! //! #[derive(Debug, thiserror::Error)] //! pub enum MyError { -//! #[error("Failed to read header, {0}")] -//! Convert(#[from] ToStrError), +//! #[error("Failed to create signing string, {0}")] +//! Convert(#[from] PrepareSignError), //! //! #[error("Failed to create header, {0}")] //! Header(#[from] InvalidHeaderValue), @@ -180,12 +180,14 @@ pub mod digest; pub mod create; pub mod middleware; +pub use http_signature_normalization::RequiredError; + /// Useful types and traits for using this library in Actix Web pub mod prelude { pub use crate::{ middleware::{SignatureVerified, VerifySignature}, verify::{Algorithm, DeprecatedAlgorithm, Unverified}, - Config, PrepareVerifyError, Sign, SignatureVerify, + Config, PrepareSignError, PrepareVerifyError, RequiredError, Sign, SignatureVerify, }; #[cfg(feature = "digest")] @@ -242,7 +244,7 @@ pub trait Sign { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -260,7 +262,7 @@ pub trait Sign { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -297,6 +299,22 @@ pub enum PrepareVerifyError { #[error("Failed to read header, {0}")] /// An error converting the header to a string for validation Header(#[from] ToStrError), + + #[error("{0}")] + /// Required headers were missing from request + Required(#[from] RequiredError), +} + +#[derive(Debug, thiserror::Error)] +/// An error when preparing to sign a request +pub enum PrepareSignError { + #[error("Failed to read header, {0}")] + /// An error occurred when reading the request's headers + Header(#[from] ToStrError), + + #[error("{0}")] + /// Some headers were marked as required, but are missing + RequiredError(#[from] RequiredError), } impl From for PrepareVerifyError { @@ -311,6 +329,9 @@ impl From for PrepareVerifyErr hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing, hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired, }, + hsn::PrepareVerifyError::Required(required_error) => { + PrepareVerifyError::Required(required_error) + } } } } @@ -337,13 +358,20 @@ impl Config { } } + /// Require a header on signed and verified requests + pub fn require_header(self, header: &str) -> Self { + Config { + config: self.config.require_header(header), + } + } + /// Begin the process of singing a request pub fn begin_sign( &self, method: &Method, path_and_query: Option<&PathAndQuery>, headers: HeaderMap, - ) -> Result { + ) -> Result { let headers = headers .iter() .map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string()))) @@ -355,7 +383,7 @@ impl Config { let unsigned = self .config - .begin_sign(&method.to_string(), &path_and_query, headers); + .begin_sign(&method.to_string(), &path_and_query, headers)?; Ok(Unsigned { unsigned }) } diff --git a/http-signature-normalization-actix/src/middleware.rs b/http-signature-normalization-actix/src/middleware.rs index 02cc7cf..efba818 100644 --- a/http-signature-normalization-actix/src/middleware.rs +++ b/http-signature-normalization-actix/src/middleware.rs @@ -124,6 +124,10 @@ where debug!("Failed to parse header {}", e); return Box::pin(err(VerifyError.into())); } + Err(PrepareVerifyError::Required(req)) => { + debug!("Missing required headers, {:?}", req); + return Box::pin(err(VerifyError.into())); + } }; let algorithm = unverified.algorithm().map(|a| a.clone()); diff --git a/http-signature-normalization-actix/src/sign.rs b/http-signature-normalization-actix/src/sign.rs index 131b4f9..aabe448 100644 --- a/http-signature-normalization-actix/src/sign.rs +++ b/http-signature-normalization-actix/src/sign.rs @@ -1,12 +1,9 @@ use actix_web::{ - client::ClientRequest, - error::BlockingError, - http::header::{InvalidHeaderValue, ToStrError}, - web, + client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web, }; use std::{fmt::Display, future::Future, pin::Pin}; -use crate::{create::Signed, Config, Sign}; +use crate::{create::Signed, Config, PrepareSignError, Sign}; impl Sign for ClientRequest { fn authorization_signature( @@ -18,7 +15,7 @@ impl Sign for ClientRequest { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -42,7 +39,7 @@ impl Sign for ClientRequest { where F: FnOnce(&str) -> Result + Send + 'static, E: From> - + From + + From + From + std::fmt::Debug + Send @@ -66,7 +63,7 @@ async fn prepare( ) -> Result where F: FnOnce(&str) -> Result + Send + 'static, - E: From> + From + std::fmt::Debug + Send + 'static, + E: From> + From + std::fmt::Debug + Send + 'static, K: Display, { let unsigned = config.begin_sign( diff --git a/http-signature-normalization-http/Cargo.toml b/http-signature-normalization-http/Cargo.toml index d051311..feec389 100644 --- a/http-signature-normalization-http/Cargo.toml +++ b/http-signature-normalization-http/Cargo.toml @@ -13,4 +13,4 @@ edition = "2018" [dependencies] http = "0.2" -http-signature-normalization = { version = "0.4.0", path = ".." } +http-signature-normalization = { version = "0.5.0", path = ".." } diff --git a/http-signature-normalization-reqwest/Cargo.toml b/http-signature-normalization-reqwest/Cargo.toml index 207335e..97792de 100644 --- a/http-signature-normalization-reqwest/Cargo.toml +++ b/http-signature-normalization-reqwest/Cargo.toml @@ -16,7 +16,7 @@ bytes = "0.5.3" futures = "0.3.1" chrono = "0.4.10" http = "0.2.0" -http-signature-normalization = { version = "0.4.0", path = ".." } +http-signature-normalization = { version = "0.5.0", path = ".." } reqwest = "0.10.1" serde = { version = "1.0.104", features = ["derive"], optional = true } serde_json = { version = "1.0.44", optional = true } diff --git a/src/lib.rs b/src/lib.rs index a0064da..7dd45d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,7 +20,7 @@ //! let headers = BTreeMap::new(); //! //! let signature_header_value = config -//! .begin_sign("GET", "/foo?bar=baz", headers) +//! .begin_sign("GET", "/foo?bar=baz", headers)? //! .sign("my-key-id".to_owned(), |signing_string| { //! // sign the string here //! Ok(signing_string.to_owned()) as Result<_, Box> @@ -43,7 +43,7 @@ //! ``` use chrono::{DateTime, Duration, Utc}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, HashSet}; pub mod create; pub mod verify; @@ -73,6 +73,7 @@ const SIGNATURE_FIELD: &'static str = "signature"; pub struct Config { expires_after: Duration, use_created_field: bool, + required_headers: HashSet, } #[derive(Debug, thiserror::Error)] @@ -87,8 +88,17 @@ pub enum PrepareVerifyError { #[error("{0}")] /// Error parsing the header Parse(#[from] ParseSignatureError), + + #[error("{0}")] + /// Missing required headers + Required(#[from] RequiredError), } +#[derive(Debug, thiserror::Error)] +#[error("Missing required headers {0:?}")] +/// Failed to build a signing string due to missing required headers +pub struct RequiredError(HashSet); + impl Config { /// Create a new Config with a default expiration of 10 seconds pub fn new() -> Self { @@ -97,10 +107,13 @@ impl Config { /// Opt out of using the (created) and (expires) fields introduced in draft 11 /// - /// Use this for compatibility with mastodon + /// Use this for compatibility with mastodon. + /// + /// Note that by not requiring the created field, the Date header becomes required. This is to + /// prevent replay attacks. pub fn dont_use_created_field(mut self) -> Self { self.use_created_field = false; - self + self.require_header("date") } /// Set the expiration to a custom duration @@ -109,6 +122,13 @@ impl Config { self } + /// Mark a header as required + pub fn require_header(mut self, header: &str) -> Self { + self.required_headers + .insert(header.to_lowercase().to_owned()); + self + } + /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to /// sign the header pub fn begin_sign( @@ -116,7 +136,7 @@ impl Config { method: &str, path_and_query: &str, headers: BTreeMap, - ) -> Unsigned { + ) -> Result { let mut headers = headers .into_iter() .map(|(k, v)| (k.to_lowercase(), v)) @@ -140,14 +160,15 @@ impl Config { expires, &sig_headers, &mut headers, - ); + self.required_headers.clone(), + )?; - Unsigned { + Ok(Unsigned { signing_string, sig_headers, created, expires, - } + }) } /// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to @@ -169,7 +190,12 @@ impl Config { .ok_or(ValidateError::Missing)?; let parsed_header: ParsedHeader = header.parse()?; - let unvalidated = parsed_header.into_unvalidated(method, path_and_query, &mut headers); + let unvalidated = parsed_header.into_unvalidated( + method, + path_and_query, + &mut headers, + self.required_headers.clone(), + )?; Ok(unvalidated.validate(self.expires_after)?) } @@ -200,7 +226,16 @@ fn build_signing_string( expires: Option>, sig_headers: &[String], btm: &mut BTreeMap, -) -> String { + mut required_headers: HashSet, +) -> Result { + for key in btm.keys() { + required_headers.remove(key); + } + + if !required_headers.is_empty() { + return Err(RequiredError(required_headers)); + } + let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query); btm.insert(REQUEST_TARGET.to_owned(), request_target.clone()); @@ -217,7 +252,7 @@ fn build_signing_string( .collect::>() .join("\n"); - signing_string + Ok(signing_string) } impl Default for Config { @@ -225,6 +260,7 @@ impl Default for Config { Config { expires_after: Duration::seconds(10), use_created_field: true, + required_headers: HashSet::new(), } } } @@ -243,13 +279,24 @@ mod tests { headers } + #[test] + fn required_header() { + let headers = prepare_headers(); + let config = Config::default().require_header("date"); + + let res = config.begin_sign("GET", "/foo?bar=baz", headers); + + assert!(res.is_err()) + } + #[test] fn round_trip_authorization() { let headers = prepare_headers(); - let config = Config::default(); + let config = Config::default().require_header("content-type"); let authorization_header = config .begin_sign("GET", "/foo?bar=baz", headers) + .unwrap() .sign("hi".to_owned(), |s| { Ok(s.to_owned()) as Result<_, std::io::Error> }) @@ -274,6 +321,7 @@ mod tests { let signature_header = config .begin_sign("GET", "/foo?bar=baz", headers) + .unwrap() .sign("hi".to_owned(), |s| { Ok(s.to_owned()) as Result<_, std::io::Error> }) diff --git a/src/verify.rs b/src/verify.rs index 2116784..e9083c9 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -1,15 +1,15 @@ //! Types and methods to verify a signature or authorization header use chrono::{DateTime, Duration, TimeZone, Utc}; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, error::Error, fmt, str::FromStr, }; use crate::{ - build_signing_string, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD, HEADERS_FIELD, - KEY_ID_FIELD, SIGNATURE_FIELD, + build_signing_string, RequiredError, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD, + HEADERS_FIELD, KEY_ID_FIELD, SIGNATURE_FIELD, }; #[derive(Debug)] @@ -214,7 +214,8 @@ impl ParsedHeader { method: &str, path_and_query: &str, headers: &mut BTreeMap, - ) -> Unvalidated { + required_headers: HashSet, + ) -> Result { let date = headers.get("date").cloned(); let signing_string = build_signing_string( @@ -224,9 +225,10 @@ impl ParsedHeader { self.expires, &self.headers, headers, - ); + required_headers, + )?; - Unvalidated { + Ok(Unvalidated { key_id: self.key_id, signature: self.signature, parsed_at: self.parsed_at, @@ -235,7 +237,7 @@ impl ParsedHeader { expires: self.expires, date, signing_string, - } + }) } }