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]
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 <asonix@asonix.dog>"]
license-file = "LICENSE"
readme = "README.md"

View file

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

View file

@ -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<dyn std::error::Error>> {
std::env::set_var("RUST_LOG", "info");
pretty_env_logger::init();
let config = Config::default();
async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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)]
pub enum MyError {
#[error("Failed to read header, {0}")]

View file

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

View file

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

View file

@ -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 = ".." }

View file

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

View file

@ -13,8 +13,8 @@ use crate::{
pub struct Signed {
signature: String,
sig_headers: Vec<String>,
created: DateTime<Utc>,
expires: DateTime<Utc>,
created: Option<DateTime<Utc>>,
expires: Option<DateTime<Utc>>,
key_id: String,
}
@ -26,8 +26,8 @@ pub struct Signed {
pub struct Unsigned {
pub(crate) signing_string: String,
pub(crate) sig_headers: Vec<String>,
pub(crate) created: DateTime<Utc>,
pub(crate) expires: DateTime<Utc>,
pub(crate) created: Option<DateTime<Utc>>,
pub(crate) expires: Option<DateTime<Utc>>,
}
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()

View file

@ -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<dyn std::error::Error>> {
//! 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<String, String>) -> Vec<String> {
let http_header_keys: Vec<String> = btm.keys().cloned().collect();
fn build_headers_list(&self, btm: &BTreeMap<String, String>) -> Vec<String> {
let http_header_keys: Vec<String> = 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,
}
}
}

View file

@ -36,6 +36,7 @@ pub struct Unvalidated {
pub(crate) created: Option<DateTime<Utc>>,
pub(crate) expires: Option<DateTime<Utc>>,
pub(crate) parsed_at: DateTime<Utc>,
pub(crate) date: Option<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 {
key_id: self.key_id,
algorithm: self.algorithm,
@ -198,6 +208,8 @@ impl ParsedHeader {
path_and_query: &str,
headers: &mut BTreeMap<String, String>,
) -> 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,
}
}