mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-25 10:51:01 +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]
|
[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"
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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}")]
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 = ".." }
|
||||||
|
|
|
@ -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 }
|
||||||
|
|
|
@ -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()
|
||||||
|
|
59
src/lib.rs
59
src/lib.rs
|
@ -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,21 +173,25 @@ 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(
|
||||||
method: &str,
|
method: &str,
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue