From b1224f2774a1d025cf116c032276bee40caa116f Mon Sep 17 00:00:00 2001 From: asonix Date: Tue, 17 Mar 2020 18:54:00 -0500 Subject: [PATCH] Allow for generating requests without created field --- Cargo.toml | 2 +- http-signature-normalization-actix/Cargo.toml | 3 +- .../examples/client.rs | 22 ++++-- .../examples/server.rs | 2 + http-signature-normalization-actix/src/lib.rs | 32 +++++++- http-signature-normalization-http/Cargo.toml | 2 +- .../Cargo.toml | 2 +- src/create.rs | 34 ++++++--- src/lib.rs | 75 +++++++++++++------ src/verify.rs | 13 ++++ 10 files changed, 138 insertions(+), 49 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fed1a40..2fce513 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.3.0" +version = "0.4.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 12f85e3..c9d6b09 100644 --- a/http-signature-normalization-actix/Cargo.toml +++ b/http-signature-normalization-actix/Cargo.toml @@ -29,8 +29,9 @@ actix-web = "3.0.0-alpha.1" actix-http = "2.0.0-alpha.2" base64 = { version = "0.11", optional = true } bytes = "0.5.4" +chrono = "0.4.6" futures = "0.3" -http-signature-normalization = { version = "0.3.0", path = ".." } +http-signature-normalization = { version = "0.4.0", path = ".." } log = { version = "0.4", optional = true } 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 a4f1672..537d28b 100644 --- a/http-signature-normalization-actix/examples/client.rs +++ b/http-signature-normalization-actix/examples/client.rs @@ -1,19 +1,17 @@ use actix_web::client::Client; use http_signature_normalization_actix::prelude::*; use sha2::{Digest, Sha256}; +use std::time::SystemTime; -#[actix_rt::main] -async fn main() -> Result<(), Box> { - std::env::set_var("RUST_LOG", "info"); - pretty_env_logger::init(); - - let config = Config::default(); +async fn request(config: Config) -> Result<(), Box> { let mut digest = Sha256::new(); let mut response = Client::default() .post("http://127.0.0.1:8010/") .header("User-Agent", "Actix Web") + .set(actix_web::http::header::Date(SystemTime::now().into())) .signature_with_digest(&config, "my-key-id", &mut digest, "Hewwo-owo", |s| { + println!("Signing String\n{}", s); Ok(base64::encode(s)) as Result<_, MyError> })? .send() @@ -32,6 +30,18 @@ async fn main() -> Result<(), Box> { Ok(()) } +#[actix_rt::main] +async fn main() -> Result<(), Box> { + std::env::set_var("RUST_LOG", "info"); + pretty_env_logger::init(); + + let config = Config::default(); + + request(config.clone()).await?; + request(config.dont_use_created_field()).await?; + Ok(()) +} + #[derive(Debug, thiserror::Error)] pub enum MyError { #[error("Failed to read header, {0}")] diff --git a/http-signature-normalization-actix/examples/server.rs b/http-signature-normalization-actix/examples/server.rs index 1b7599a..961bd5f 100644 --- a/http-signature-normalization-actix/examples/server.rs +++ b/http-signature-normalization-actix/examples/server.rs @@ -32,6 +32,8 @@ impl SignatureVerify for MyVerify { Err(_) => return err(MyError::Decode), }; + println!("Signing String\n{}", signing_string); + ok(decoded == signing_string.as_bytes()) } } diff --git a/http-signature-normalization-actix/src/lib.rs b/http-signature-normalization-actix/src/lib.rs index 1b54ba5..48ce1a8 100644 --- a/http-signature-normalization-actix/src/lib.rs +++ b/http-signature-normalization-actix/src/lib.rs @@ -151,7 +151,7 @@ use actix_web::http::{ uri::PathAndQuery, Method, }; - +use chrono::Duration; use std::{collections::BTreeMap, fmt::Display, future::Future}; mod sign; @@ -166,7 +166,7 @@ pub mod middleware; pub mod prelude { pub use crate::{ middleware::{SignatureVerified, VerifySignature}, - verify::{Algorithm, Unverified}, + verify::{Algorithm, DeprecatedAlgorithm, Unverified}, Config, PrepareVerifyError, Sign, SignatureVerify, }; @@ -232,10 +232,13 @@ pub trait Sign { } #[derive(Clone, Debug, Default)] -/// A thin wrapper around the underlying library's config type +/// Configuration for signing and verifying signatures +/// +/// By default, the config is set up to create and verify signatures that expire after 10 +/// seconds, and use the `(created)` and `(expires)` fields that were introduced in draft 11 pub struct Config { /// The inner config type - pub config: http_signature_normalization::Config, + config: http_signature_normalization::Config, } #[derive(Debug, thiserror::Error)] @@ -251,6 +254,27 @@ pub enum PrepareVerifyError { } impl Config { + /// Create a new Config with a default expiration of 10 seconds + pub fn new() -> Self { + Config::default() + } + + /// Opt out of using the (created) and (expires) fields introduced in draft 11 + /// + /// Use this for compatibility with mastodon + pub fn dont_use_created_field(self) -> Self { + Config { + config: self.config.dont_use_created_field(), + } + } + + /// Set the expiration to a custom duration + pub fn set_expiration(self, expires_after: Duration) -> Self { + Config { + config: self.config.set_expiration(expires_after), + } + } + /// Begin the process of singing a request pub fn begin_sign( &self, diff --git a/http-signature-normalization-http/Cargo.toml b/http-signature-normalization-http/Cargo.toml index 76687b4..d051311 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.3.0", path = ".." } +http-signature-normalization = { version = "0.4.0", path = ".." } diff --git a/http-signature-normalization-reqwest/Cargo.toml b/http-signature-normalization-reqwest/Cargo.toml index 0efa5b0..207335e 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.3.0", path = ".." } +http-signature-normalization = { version = "0.4.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/create.rs b/src/create.rs index 82f48d8..404987e 100644 --- a/src/create.rs +++ b/src/create.rs @@ -13,8 +13,8 @@ use crate::{ pub struct Signed { signature: String, sig_headers: Vec, - created: DateTime, - expires: DateTime, + created: Option>, + expires: Option>, key_id: String, } @@ -26,8 +26,8 @@ pub struct Signed { pub struct Unsigned { pub(crate) signing_string: String, pub(crate) sig_headers: Vec, - pub(crate) created: DateTime, - pub(crate) expires: DateTime, + pub(crate) created: Option>, + pub(crate) expires: Option>, } impl Signed { @@ -46,14 +46,24 @@ impl Signed { } fn into_header(self) -> String { - let header_parts = [ - (KEY_ID_FIELD, self.key_id), - (ALGORITHM_FIELD, ALGORITHM_VALUE.to_owned()), - (CREATED_FIELD, self.created.timestamp().to_string()), - (EXPIRES_FIELD, self.expires.timestamp().to_string()), - (HEADERS_FIELD, self.sig_headers.join(" ")), - (SIGNATURE_FIELD, self.signature), - ]; + let mapped = self.created.and_then(|c| self.expires.map(|e| (c, e))); + let header_parts = if let Some((created, expires)) = mapped { + vec![ + (KEY_ID_FIELD, self.key_id), + (ALGORITHM_FIELD, ALGORITHM_VALUE.to_owned()), + (CREATED_FIELD, created.timestamp().to_string()), + (EXPIRES_FIELD, expires.timestamp().to_string()), + (HEADERS_FIELD, self.sig_headers.join(" ")), + (SIGNATURE_FIELD, self.signature), + ] + } else { + vec![ + (KEY_ID_FIELD, self.key_id), + (ALGORITHM_FIELD, ALGORITHM_VALUE.to_owned()), + (HEADERS_FIELD, self.sig_headers.join(" ")), + (SIGNATURE_FIELD, self.signature), + ] + }; header_parts .iter() diff --git a/src/lib.rs b/src/lib.rs index fdb8bf2..a0064da 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,7 @@ //! //! - [crates.io](https://crates.io/crates/http-signature-normalization) //! - [docs.rs](https://docs.rs/http-signature-normalization) -//! - [Join the discussion on Matrix](https://matrix.to/#/!IRQaBCMWKbpBWKjQgx:asonix.dog?via=asonix.dog) +//! - [Hit me up on Mastodon](https://asonix.dog/@asonix) //! //! Http Signature Normalization is a minimal-dependency crate for producing HTTP Signatures with user-provided signing and verification. The API is simple; there's a series of steps for creation and verification with types that ensure reasonable usage. //! @@ -15,9 +15,7 @@ //! use std::collections::BTreeMap; //! //! fn main() -> Result<(), Box> { -//! let config = Config { -//! expires_after: Duration::seconds(5), -//! }; +//! let config = Config::default().set_expiration(Duration::seconds(5)); //! //! let headers = BTreeMap::new(); //! @@ -70,11 +68,11 @@ const SIGNATURE_FIELD: &'static str = "signature"; #[derive(Clone, Debug)] /// Configuration for signing and verifying signatures /// -/// Currently, the only configuration provided is how long a signature should be considered valid -/// before it expires. +/// By default, the config is set up to create and verify signatures that expire after 10 +/// seconds, and use the `(created)` and `(expires)` fields that were introduced in draft 11 pub struct Config { - /// How long a singature is valid - pub expires_after: Duration, + expires_after: Duration, + use_created_field: bool, } #[derive(Debug, thiserror::Error)] @@ -92,6 +90,25 @@ pub enum PrepareVerifyError { } impl Config { + /// Create a new Config with a default expiration of 10 seconds + pub fn new() -> Self { + Config::default() + } + + /// Opt out of using the (created) and (expires) fields introduced in draft 11 + /// + /// Use this for compatibility with mastodon + pub fn dont_use_created_field(mut self) -> Self { + self.use_created_field = false; + self + } + + /// Set the expiration to a custom duration + pub fn set_expiration(mut self, expires_after: Duration) -> Self { + self.expires_after = expires_after; + self + } + /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to /// sign the header pub fn begin_sign( @@ -104,16 +121,23 @@ impl Config { .into_iter() .map(|(k, v)| (k.to_lowercase(), v)) .collect(); - let sig_headers = build_headers_list(&headers); - let created = Utc::now(); - let expires = created + self.expires_after; + let sig_headers = self.build_headers_list(&headers); + + let (created, expires) = if self.use_created_field { + let created = Utc::now(); + let expires = created + self.expires_after; + + (Some(created), Some(expires)) + } else { + (None, None) + }; let signing_string = build_signing_string( method, path_and_query, - Some(created), - Some(expires), + created, + expires, &sig_headers, &mut headers, ); @@ -149,20 +173,24 @@ impl Config { Ok(unvalidated.validate(self.expires_after)?) } -} -fn build_headers_list(btm: &BTreeMap) -> Vec { - let http_header_keys: Vec = btm.keys().cloned().collect(); + fn build_headers_list(&self, btm: &BTreeMap) -> Vec { + let http_header_keys: Vec = btm.keys().cloned().collect(); - let mut sig_headers = vec![ - REQUEST_TARGET.to_owned(), - CREATED.to_owned(), - EXPIRES.to_owned(), - ]; + let mut sig_headers = if self.use_created_field { + vec![ + REQUEST_TARGET.to_owned(), + CREATED.to_owned(), + EXPIRES.to_owned(), + ] + } else { + vec![REQUEST_TARGET.to_owned()] + }; - sig_headers.extend(http_header_keys); + sig_headers.extend(http_header_keys); - sig_headers + sig_headers + } } fn build_signing_string( @@ -196,6 +224,7 @@ impl Default for Config { fn default() -> Self { Config { expires_after: Duration::seconds(10), + use_created_field: true, } } } diff --git a/src/verify.rs b/src/verify.rs index 86f4fa8..3ad7e1b 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -36,6 +36,7 @@ pub struct Unvalidated { pub(crate) created: Option>, pub(crate) expires: Option>, pub(crate) parsed_at: DateTime, + pub(crate) date: Option, pub(crate) signing_string: String, } @@ -181,6 +182,15 @@ impl Unvalidated { } } + if let Some(date) = self.date { + if let Ok(datetime) = DateTime::parse_from_rfc2822(&date) { + let datetime: DateTime = datetime.into(); + if datetime + expires_after < self.parsed_at { + return Err(ValidateError::Expired); + } + } + } + Ok(Unverified { key_id: self.key_id, algorithm: self.algorithm, @@ -198,6 +208,8 @@ impl ParsedHeader { path_and_query: &str, headers: &mut BTreeMap, ) -> Unvalidated { + let date = headers.get("date").cloned(); + let signing_string = build_signing_string( method, path_and_query, @@ -214,6 +226,7 @@ impl ParsedHeader { algorithm: self.algorithm, created: self.created, expires: self.expires, + date, signing_string, } }