Allow for generating requests without created field

This commit is contained in:
asonix 2020-03-17 18:54:00 -05:00
parent df4ac1e2cf
commit b1224f2774
10 changed files with 138 additions and 49 deletions

View file

@ -1,7 +1,7 @@
[package] [package]
name = "http-signature-normalization" name = "http-signature-normalization"
description = "An HTTP Signatures library that leaves the signing to you" description = "An HTTP Signatures library that leaves the signing to you"
version = "0.3.0" version = "0.4.0"
authors = ["asonix <asonix@asonix.dog>"] authors = ["asonix <asonix@asonix.dog>"]
license-file = "LICENSE" license-file = "LICENSE"
readme = "README.md" readme = "README.md"

View file

@ -29,8 +29,9 @@ actix-web = "3.0.0-alpha.1"
actix-http = "2.0.0-alpha.2" actix-http = "2.0.0-alpha.2"
base64 = { version = "0.11", optional = true } base64 = { version = "0.11", optional = true }
bytes = "0.5.4" bytes = "0.5.4"
chrono = "0.4.6"
futures = "0.3" 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 } log = { version = "0.4", optional = true }
sha2 = { version = "0.8", optional = true } sha2 = { version = "0.8", optional = true }
sha3 = { version = "0.8", optional = true } sha3 = { version = "0.8", optional = true }

View file

@ -1,19 +1,17 @@
use actix_web::client::Client; use actix_web::client::Client;
use http_signature_normalization_actix::prelude::*; use http_signature_normalization_actix::prelude::*;
use sha2::{Digest, Sha256}; use sha2::{Digest, Sha256};
use std::time::SystemTime;
#[actix_rt::main] async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
async fn main() -> Result<(), Box<dyn std::error::Error>> {
std::env::set_var("RUST_LOG", "info");
pretty_env_logger::init();
let config = Config::default();
let mut digest = Sha256::new(); let mut digest = Sha256::new();
let mut response = Client::default() let mut response = Client::default()
.post("http://127.0.0.1:8010/") .post("http://127.0.0.1:8010/")
.header("User-Agent", "Actix Web") .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| { .signature_with_digest(&config, "my-key-id", &mut digest, "Hewwo-owo", |s| {
println!("Signing String\n{}", s);
Ok(base64::encode(s)) as Result<_, MyError> Ok(base64::encode(s)) as Result<_, MyError>
})? })?
.send() .send()
@ -32,6 +30,18 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(()) Ok(())
} }
#[actix_rt::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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)] #[derive(Debug, thiserror::Error)]
pub enum MyError { pub enum MyError {
#[error("Failed to read header, {0}")] #[error("Failed to read header, {0}")]

View file

@ -32,6 +32,8 @@ impl SignatureVerify for MyVerify {
Err(_) => return err(MyError::Decode), Err(_) => return err(MyError::Decode),
}; };
println!("Signing String\n{}", signing_string);
ok(decoded == signing_string.as_bytes()) ok(decoded == signing_string.as_bytes())
} }
} }

View file

@ -151,7 +151,7 @@ use actix_web::http::{
uri::PathAndQuery, uri::PathAndQuery,
Method, Method,
}; };
use chrono::Duration;
use std::{collections::BTreeMap, fmt::Display, future::Future}; use std::{collections::BTreeMap, fmt::Display, future::Future};
mod sign; mod sign;
@ -166,7 +166,7 @@ pub mod middleware;
pub mod prelude { pub mod prelude {
pub use crate::{ pub use crate::{
middleware::{SignatureVerified, VerifySignature}, middleware::{SignatureVerified, VerifySignature},
verify::{Algorithm, Unverified}, verify::{Algorithm, DeprecatedAlgorithm, Unverified},
Config, PrepareVerifyError, Sign, SignatureVerify, Config, PrepareVerifyError, Sign, SignatureVerify,
}; };
@ -232,10 +232,13 @@ pub trait Sign {
} }
#[derive(Clone, Debug, Default)] #[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 { pub struct Config {
/// The inner config type /// The inner config type
pub config: http_signature_normalization::Config, config: http_signature_normalization::Config,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -251,6 +254,27 @@ pub enum PrepareVerifyError {
} }
impl Config { 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 /// Begin the process of singing a request
pub fn begin_sign( pub fn begin_sign(
&self, &self,

View file

@ -13,4 +13,4 @@ edition = "2018"
[dependencies] [dependencies]
http = "0.2" http = "0.2"
http-signature-normalization = { version = "0.3.0", path = ".." } http-signature-normalization = { version = "0.4.0", path = ".." }

View file

@ -16,7 +16,7 @@ bytes = "0.5.3"
futures = "0.3.1" futures = "0.3.1"
chrono = "0.4.10" chrono = "0.4.10"
http = "0.2.0" http = "0.2.0"
http-signature-normalization = { version = "0.3.0", path = ".." } http-signature-normalization = { version = "0.4.0", path = ".." }
reqwest = "0.10.1" reqwest = "0.10.1"
serde = { version = "1.0.104", features = ["derive"], optional = true } serde = { version = "1.0.104", features = ["derive"], optional = true }
serde_json = { version = "1.0.44", optional = true } serde_json = { version = "1.0.44", optional = true }

View file

@ -13,8 +13,8 @@ use crate::{
pub struct Signed { pub struct Signed {
signature: String, signature: String,
sig_headers: Vec<String>, sig_headers: Vec<String>,
created: DateTime<Utc>, created: Option<DateTime<Utc>>,
expires: DateTime<Utc>, expires: Option<DateTime<Utc>>,
key_id: String, key_id: String,
} }
@ -26,8 +26,8 @@ pub struct Signed {
pub struct Unsigned { pub struct Unsigned {
pub(crate) signing_string: String, pub(crate) signing_string: String,
pub(crate) sig_headers: Vec<String>, pub(crate) sig_headers: Vec<String>,
pub(crate) created: DateTime<Utc>, pub(crate) created: Option<DateTime<Utc>>,
pub(crate) expires: DateTime<Utc>, pub(crate) expires: Option<DateTime<Utc>>,
} }
impl Signed { impl Signed {
@ -46,14 +46,24 @@ impl Signed {
} }
fn into_header(self) -> String { fn into_header(self) -> String {
let header_parts = [ 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), (KEY_ID_FIELD, self.key_id),
(ALGORITHM_FIELD, ALGORITHM_VALUE.to_owned()), (ALGORITHM_FIELD, ALGORITHM_VALUE.to_owned()),
(CREATED_FIELD, self.created.timestamp().to_string()), (CREATED_FIELD, created.timestamp().to_string()),
(EXPIRES_FIELD, self.expires.timestamp().to_string()), (EXPIRES_FIELD, expires.timestamp().to_string()),
(HEADERS_FIELD, self.sig_headers.join(" ")), (HEADERS_FIELD, self.sig_headers.join(" ")),
(SIGNATURE_FIELD, self.signature), (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 header_parts
.iter() .iter()

View file

@ -5,7 +5,7 @@
//! //!
//! - [crates.io](https://crates.io/crates/http-signature-normalization) //! - [crates.io](https://crates.io/crates/http-signature-normalization)
//! - [docs.rs](https://docs.rs/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. //! 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; //! use std::collections::BTreeMap;
//! //!
//! fn main() -> Result<(), Box<dyn std::error::Error>> { //! fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let config = Config { //! let config = Config::default().set_expiration(Duration::seconds(5));
//! expires_after: Duration::seconds(5),
//! };
//! //!
//! let headers = BTreeMap::new(); //! let headers = BTreeMap::new();
//! //!
@ -70,11 +68,11 @@ const SIGNATURE_FIELD: &'static str = "signature";
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
/// Configuration for signing and verifying signatures /// Configuration for signing and verifying signatures
/// ///
/// Currently, the only configuration provided is how long a signature should be considered valid /// By default, the config is set up to create and verify signatures that expire after 10
/// before it expires. /// seconds, and use the `(created)` and `(expires)` fields that were introduced in draft 11
pub struct Config { pub struct Config {
/// How long a singature is valid expires_after: Duration,
pub expires_after: Duration, use_created_field: bool,
} }
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
@ -92,6 +90,25 @@ pub enum PrepareVerifyError {
} }
impl Config { 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 /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to
/// sign the header /// sign the header
pub fn begin_sign( pub fn begin_sign(
@ -104,16 +121,23 @@ impl Config {
.into_iter() .into_iter()
.map(|(k, v)| (k.to_lowercase(), v)) .map(|(k, v)| (k.to_lowercase(), v))
.collect(); .collect();
let sig_headers = build_headers_list(&headers);
let sig_headers = self.build_headers_list(&headers);
let (created, expires) = if self.use_created_field {
let created = Utc::now(); let created = Utc::now();
let expires = created + self.expires_after; let expires = created + self.expires_after;
(Some(created), Some(expires))
} else {
(None, None)
};
let signing_string = build_signing_string( let signing_string = build_signing_string(
method, method,
path_and_query, path_and_query,
Some(created), created,
Some(expires), expires,
&sig_headers, &sig_headers,
&mut headers, &mut headers,
); );
@ -149,20 +173,24 @@ impl Config {
Ok(unvalidated.validate(self.expires_after)?) Ok(unvalidated.validate(self.expires_after)?)
} }
}
fn build_headers_list(btm: &BTreeMap<String, String>) -> Vec<String> { fn build_headers_list(&self, btm: &BTreeMap<String, String>) -> Vec<String> {
let http_header_keys: Vec<String> = btm.keys().cloned().collect(); let http_header_keys: Vec<String> = btm.keys().cloned().collect();
let mut sig_headers = vec![ let mut sig_headers = if self.use_created_field {
vec![
REQUEST_TARGET.to_owned(), REQUEST_TARGET.to_owned(),
CREATED.to_owned(), CREATED.to_owned(),
EXPIRES.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( fn build_signing_string(
@ -196,6 +224,7 @@ impl Default for Config {
fn default() -> Self { fn default() -> Self {
Config { Config {
expires_after: Duration::seconds(10), expires_after: Duration::seconds(10),
use_created_field: true,
} }
} }
} }

View file

@ -36,6 +36,7 @@ pub struct Unvalidated {
pub(crate) created: Option<DateTime<Utc>>, pub(crate) created: Option<DateTime<Utc>>,
pub(crate) expires: Option<DateTime<Utc>>, pub(crate) expires: Option<DateTime<Utc>>,
pub(crate) parsed_at: DateTime<Utc>, pub(crate) parsed_at: DateTime<Utc>,
pub(crate) date: Option<String>,
pub(crate) signing_string: String, 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<Utc> = datetime.into();
if datetime + expires_after < self.parsed_at {
return Err(ValidateError::Expired);
}
}
}
Ok(Unverified { Ok(Unverified {
key_id: self.key_id, key_id: self.key_id,
algorithm: self.algorithm, algorithm: self.algorithm,
@ -198,6 +208,8 @@ impl ParsedHeader {
path_and_query: &str, path_and_query: &str,
headers: &mut BTreeMap<String, String>, headers: &mut BTreeMap<String, String>,
) -> Unvalidated { ) -> Unvalidated {
let date = headers.get("date").cloned();
let signing_string = build_signing_string( let signing_string = build_signing_string(
method, method,
path_and_query, path_and_query,
@ -214,6 +226,7 @@ impl ParsedHeader {
algorithm: self.algorithm, algorithm: self.algorithm,
created: self.created, created: self.created,
expires: self.expires, expires: self.expires,
date,
signing_string, signing_string,
} }
} }