mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-22 01:11:00 +00:00
Allow for generating requests without created field
This commit is contained in:
parent
df4ac1e2cf
commit
b1224f2774
10 changed files with 138 additions and 49 deletions
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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}")]
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = ".." }
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 = [
|
||||
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, self.created.timestamp().to_string()),
|
||||
(EXPIRES_FIELD, self.expires.timestamp().to_string()),
|
||||
(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()
|
||||
|
|
59
src/lib.rs
59
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<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 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,21 +173,25 @@ impl Config {
|
|||
|
||||
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 mut sig_headers = vec![
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
fn build_signing_string(
|
||||
method: &str,
|
||||
|
@ -196,6 +224,7 @@ impl Default for Config {
|
|||
fn default() -> Self {
|
||||
Config {
|
||||
expires_after: Duration::seconds(10),
|
||||
use_created_field: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue