mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-21 17:00:59 +00:00
Add ability to require headers in signature
This commit is contained in:
parent
08686beb8f
commit
90660b7f19
13 changed files with 131 additions and 54 deletions
|
@ -1,7 +1,7 @@
|
|||
[package]
|
||||
name = "http-signature-normalization"
|
||||
description = "An HTTP Signatures library that leaves the signing to you"
|
||||
version = "0.4.2"
|
||||
version = "0.5.0"
|
||||
authors = ["asonix <asonix@asonix.dog>"]
|
||||
license-file = "LICENSE"
|
||||
readme = "README.md"
|
||||
|
|
|
@ -31,7 +31,7 @@ base64 = { version = "0.12", optional = true }
|
|||
bytes = "0.5.4"
|
||||
chrono = "0.4.6"
|
||||
futures = "0.3"
|
||||
http-signature-normalization = { version = "0.4.2", path = ".." }
|
||||
http-signature-normalization = { version = "0.5.0", path = ".." }
|
||||
log = "0.4"
|
||||
sha2 = { version = "0.8", optional = true }
|
||||
sha3 = { version = "0.8", optional = true }
|
||||
|
|
|
@ -9,6 +9,7 @@ async fn request(config: Config) -> Result<(), Box<dyn std::error::Error>> {
|
|||
let mut response = Client::default()
|
||||
.post("http://127.0.0.1:8010/")
|
||||
.header("User-Agent", "Actix Web")
|
||||
.header("Accept", "text/plain")
|
||||
.set(actix_web::http::header::Date(SystemTime::now().into()))
|
||||
.signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
|
||||
println!("Signing String\n{}", s);
|
||||
|
@ -36,7 +37,7 @@ 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 config = Config::default().require_header("accept");
|
||||
|
||||
request(config.clone()).await?;
|
||||
request(config.dont_use_created_field()).await?;
|
||||
|
@ -45,8 +46,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MyError {
|
||||
#[error("Failed to read header, {0}")]
|
||||
Convert(#[from] ToStrError),
|
||||
#[error("Failed to create signing string, {0}")]
|
||||
Convert(#[from] PrepareSignError),
|
||||
|
||||
#[error("Failed to create header, {0}")]
|
||||
Header(#[from] InvalidHeaderValue),
|
||||
|
|
|
@ -56,7 +56,7 @@ 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 config = Config::default().require_header("accept");
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
|
@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum MyError {
|
||||
#[error("Failed to verify, {}", _0)]
|
||||
#[error("Failed to verify, {0}")]
|
||||
Verify(#[from] PrepareVerifyError),
|
||||
|
||||
#[error("Unsupported algorithm")]
|
||||
|
|
|
@ -8,11 +8,11 @@ use actix_web::{
|
|||
client::{ClientRequest, ClientResponse, SendRequestError},
|
||||
dev::Payload,
|
||||
error::BlockingError,
|
||||
http::header::{InvalidHeaderValue, ToStrError},
|
||||
http::header::InvalidHeaderValue,
|
||||
};
|
||||
use std::{fmt::Display, future::Future, pin::Pin};
|
||||
|
||||
use crate::{Config, Sign};
|
||||
use crate::{Config, PrepareSignError, Sign};
|
||||
|
||||
pub mod middleware;
|
||||
#[cfg(feature = "sha-2")]
|
||||
|
@ -57,7 +57,7 @@ pub trait SignExt: Sign {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -79,7 +79,7 @@ pub trait SignExt: Sign {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
use actix_web::{
|
||||
client::ClientRequest,
|
||||
error::BlockingError,
|
||||
http::header::{InvalidHeaderValue, ToStrError},
|
||||
web,
|
||||
client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
|
||||
};
|
||||
use std::{fmt::Display, future::Future, pin::Pin};
|
||||
|
||||
use crate::{
|
||||
digest::{DigestClient, DigestCreate, SignExt},
|
||||
Config, Sign,
|
||||
Config, PrepareSignError, Sign,
|
||||
};
|
||||
|
||||
impl SignExt for ClientRequest {
|
||||
|
@ -23,7 +20,7 @@ impl SignExt for ClientRequest {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -60,7 +57,7 @@ impl SignExt for ClientRequest {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
|
|
@ -135,8 +135,8 @@
|
|||
//!
|
||||
//! #[derive(Debug, thiserror::Error)]
|
||||
//! pub enum MyError {
|
||||
//! #[error("Failed to read header, {0}")]
|
||||
//! Convert(#[from] ToStrError),
|
||||
//! #[error("Failed to create signing string, {0}")]
|
||||
//! Convert(#[from] PrepareSignError),
|
||||
//!
|
||||
//! #[error("Failed to create header, {0}")]
|
||||
//! Header(#[from] InvalidHeaderValue),
|
||||
|
@ -180,12 +180,14 @@ pub mod digest;
|
|||
pub mod create;
|
||||
pub mod middleware;
|
||||
|
||||
pub use http_signature_normalization::RequiredError;
|
||||
|
||||
/// Useful types and traits for using this library in Actix Web
|
||||
pub mod prelude {
|
||||
pub use crate::{
|
||||
middleware::{SignatureVerified, VerifySignature},
|
||||
verify::{Algorithm, DeprecatedAlgorithm, Unverified},
|
||||
Config, PrepareVerifyError, Sign, SignatureVerify,
|
||||
Config, PrepareSignError, PrepareVerifyError, RequiredError, Sign, SignatureVerify,
|
||||
};
|
||||
|
||||
#[cfg(feature = "digest")]
|
||||
|
@ -242,7 +244,7 @@ pub trait Sign {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -260,7 +262,7 @@ pub trait Sign {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -297,6 +299,22 @@ pub enum PrepareVerifyError {
|
|||
#[error("Failed to read header, {0}")]
|
||||
/// An error converting the header to a string for validation
|
||||
Header(#[from] ToStrError),
|
||||
|
||||
#[error("{0}")]
|
||||
/// Required headers were missing from request
|
||||
Required(#[from] RequiredError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
/// An error when preparing to sign a request
|
||||
pub enum PrepareSignError {
|
||||
#[error("Failed to read header, {0}")]
|
||||
/// An error occurred when reading the request's headers
|
||||
Header(#[from] ToStrError),
|
||||
|
||||
#[error("{0}")]
|
||||
/// Some headers were marked as required, but are missing
|
||||
RequiredError(#[from] RequiredError),
|
||||
}
|
||||
|
||||
impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyError {
|
||||
|
@ -311,6 +329,9 @@ impl From<http_signature_normalization::PrepareVerifyError> for PrepareVerifyErr
|
|||
hsn::verify::ValidateError::Missing => PrepareVerifyError::Missing,
|
||||
hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired,
|
||||
},
|
||||
hsn::PrepareVerifyError::Required(required_error) => {
|
||||
PrepareVerifyError::Required(required_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -337,13 +358,20 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
/// Require a header on signed and verified requests
|
||||
pub fn require_header(self, header: &str) -> Self {
|
||||
Config {
|
||||
config: self.config.require_header(header),
|
||||
}
|
||||
}
|
||||
|
||||
/// Begin the process of singing a request
|
||||
pub fn begin_sign(
|
||||
&self,
|
||||
method: &Method,
|
||||
path_and_query: Option<&PathAndQuery>,
|
||||
headers: HeaderMap,
|
||||
) -> Result<Unsigned, ToStrError> {
|
||||
) -> Result<Unsigned, PrepareSignError> {
|
||||
let headers = headers
|
||||
.iter()
|
||||
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
|
||||
|
@ -355,7 +383,7 @@ impl Config {
|
|||
|
||||
let unsigned = self
|
||||
.config
|
||||
.begin_sign(&method.to_string(), &path_and_query, headers);
|
||||
.begin_sign(&method.to_string(), &path_and_query, headers)?;
|
||||
|
||||
Ok(Unsigned { unsigned })
|
||||
}
|
||||
|
|
|
@ -124,6 +124,10 @@ where
|
|||
debug!("Failed to parse header {}", e);
|
||||
return Box::pin(err(VerifyError.into()));
|
||||
}
|
||||
Err(PrepareVerifyError::Required(req)) => {
|
||||
debug!("Missing required headers, {:?}", req);
|
||||
return Box::pin(err(VerifyError.into()));
|
||||
}
|
||||
};
|
||||
|
||||
let algorithm = unverified.algorithm().map(|a| a.clone());
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
use actix_web::{
|
||||
client::ClientRequest,
|
||||
error::BlockingError,
|
||||
http::header::{InvalidHeaderValue, ToStrError},
|
||||
web,
|
||||
client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
|
||||
};
|
||||
use std::{fmt::Display, future::Future, pin::Pin};
|
||||
|
||||
use crate::{create::Signed, Config, Sign};
|
||||
use crate::{create::Signed, Config, PrepareSignError, Sign};
|
||||
|
||||
impl Sign for ClientRequest {
|
||||
fn authorization_signature<F, E, K>(
|
||||
|
@ -18,7 +15,7 @@ impl Sign for ClientRequest {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -42,7 +39,7 @@ impl Sign for ClientRequest {
|
|||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>>
|
||||
+ From<ToStrError>
|
||||
+ From<PrepareSignError>
|
||||
+ From<InvalidHeaderValue>
|
||||
+ std::fmt::Debug
|
||||
+ Send
|
||||
|
@ -66,7 +63,7 @@ async fn prepare<F, E, K>(
|
|||
) -> Result<Signed, E>
|
||||
where
|
||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||
E: From<BlockingError<E>> + From<ToStrError> + std::fmt::Debug + Send + 'static,
|
||||
E: From<BlockingError<E>> + From<PrepareSignError> + std::fmt::Debug + Send + 'static,
|
||||
K: Display,
|
||||
{
|
||||
let unsigned = config.begin_sign(
|
||||
|
|
|
@ -13,4 +13,4 @@ edition = "2018"
|
|||
|
||||
[dependencies]
|
||||
http = "0.2"
|
||||
http-signature-normalization = { version = "0.4.0", path = ".." }
|
||||
http-signature-normalization = { version = "0.5.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.4.0", path = ".." }
|
||||
http-signature-normalization = { version = "0.5.0", path = ".." }
|
||||
reqwest = "0.10.1"
|
||||
serde = { version = "1.0.104", features = ["derive"], optional = true }
|
||||
serde_json = { version = "1.0.44", optional = true }
|
||||
|
|
72
src/lib.rs
72
src/lib.rs
|
@ -20,7 +20,7 @@
|
|||
//! let headers = BTreeMap::new();
|
||||
//!
|
||||
//! let signature_header_value = config
|
||||
//! .begin_sign("GET", "/foo?bar=baz", headers)
|
||||
//! .begin_sign("GET", "/foo?bar=baz", headers)?
|
||||
//! .sign("my-key-id".to_owned(), |signing_string| {
|
||||
//! // sign the string here
|
||||
//! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>>
|
||||
|
@ -43,7 +43,7 @@
|
|||
//! ```
|
||||
|
||||
use chrono::{DateTime, Duration, Utc};
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
pub mod create;
|
||||
pub mod verify;
|
||||
|
@ -73,6 +73,7 @@ const SIGNATURE_FIELD: &'static str = "signature";
|
|||
pub struct Config {
|
||||
expires_after: Duration,
|
||||
use_created_field: bool,
|
||||
required_headers: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
|
@ -87,8 +88,17 @@ pub enum PrepareVerifyError {
|
|||
#[error("{0}")]
|
||||
/// Error parsing the header
|
||||
Parse(#[from] ParseSignatureError),
|
||||
|
||||
#[error("{0}")]
|
||||
/// Missing required headers
|
||||
Required(#[from] RequiredError),
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("Missing required headers {0:?}")]
|
||||
/// Failed to build a signing string due to missing required headers
|
||||
pub struct RequiredError(HashSet<String>);
|
||||
|
||||
impl Config {
|
||||
/// Create a new Config with a default expiration of 10 seconds
|
||||
pub fn new() -> Self {
|
||||
|
@ -97,10 +107,13 @@ impl Config {
|
|||
|
||||
/// Opt out of using the (created) and (expires) fields introduced in draft 11
|
||||
///
|
||||
/// Use this for compatibility with mastodon
|
||||
/// Use this for compatibility with mastodon.
|
||||
///
|
||||
/// Note that by not requiring the created field, the Date header becomes required. This is to
|
||||
/// prevent replay attacks.
|
||||
pub fn dont_use_created_field(mut self) -> Self {
|
||||
self.use_created_field = false;
|
||||
self
|
||||
self.require_header("date")
|
||||
}
|
||||
|
||||
/// Set the expiration to a custom duration
|
||||
|
@ -109,6 +122,13 @@ impl Config {
|
|||
self
|
||||
}
|
||||
|
||||
/// Mark a header as required
|
||||
pub fn require_header(mut self, header: &str) -> Self {
|
||||
self.required_headers
|
||||
.insert(header.to_lowercase().to_owned());
|
||||
self
|
||||
}
|
||||
|
||||
/// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to
|
||||
/// sign the header
|
||||
pub fn begin_sign(
|
||||
|
@ -116,7 +136,7 @@ impl Config {
|
|||
method: &str,
|
||||
path_and_query: &str,
|
||||
headers: BTreeMap<String, String>,
|
||||
) -> Unsigned {
|
||||
) -> Result<Unsigned, RequiredError> {
|
||||
let mut headers = headers
|
||||
.into_iter()
|
||||
.map(|(k, v)| (k.to_lowercase(), v))
|
||||
|
@ -140,14 +160,15 @@ impl Config {
|
|||
expires,
|
||||
&sig_headers,
|
||||
&mut headers,
|
||||
);
|
||||
self.required_headers.clone(),
|
||||
)?;
|
||||
|
||||
Unsigned {
|
||||
Ok(Unsigned {
|
||||
signing_string,
|
||||
sig_headers,
|
||||
created,
|
||||
expires,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to
|
||||
|
@ -169,7 +190,12 @@ impl Config {
|
|||
.ok_or(ValidateError::Missing)?;
|
||||
|
||||
let parsed_header: ParsedHeader = header.parse()?;
|
||||
let unvalidated = parsed_header.into_unvalidated(method, path_and_query, &mut headers);
|
||||
let unvalidated = parsed_header.into_unvalidated(
|
||||
method,
|
||||
path_and_query,
|
||||
&mut headers,
|
||||
self.required_headers.clone(),
|
||||
)?;
|
||||
|
||||
Ok(unvalidated.validate(self.expires_after)?)
|
||||
}
|
||||
|
@ -200,7 +226,16 @@ fn build_signing_string(
|
|||
expires: Option<DateTime<Utc>>,
|
||||
sig_headers: &[String],
|
||||
btm: &mut BTreeMap<String, String>,
|
||||
) -> String {
|
||||
mut required_headers: HashSet<String>,
|
||||
) -> Result<String, RequiredError> {
|
||||
for key in btm.keys() {
|
||||
required_headers.remove(key);
|
||||
}
|
||||
|
||||
if !required_headers.is_empty() {
|
||||
return Err(RequiredError(required_headers));
|
||||
}
|
||||
|
||||
let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query);
|
||||
|
||||
btm.insert(REQUEST_TARGET.to_owned(), request_target.clone());
|
||||
|
@ -217,7 +252,7 @@ fn build_signing_string(
|
|||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
|
||||
signing_string
|
||||
Ok(signing_string)
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
|
@ -225,6 +260,7 @@ impl Default for Config {
|
|||
Config {
|
||||
expires_after: Duration::seconds(10),
|
||||
use_created_field: true,
|
||||
required_headers: HashSet::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,13 +279,24 @@ mod tests {
|
|||
headers
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn required_header() {
|
||||
let headers = prepare_headers();
|
||||
let config = Config::default().require_header("date");
|
||||
|
||||
let res = config.begin_sign("GET", "/foo?bar=baz", headers);
|
||||
|
||||
assert!(res.is_err())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_authorization() {
|
||||
let headers = prepare_headers();
|
||||
let config = Config::default();
|
||||
let config = Config::default().require_header("content-type");
|
||||
|
||||
let authorization_header = config
|
||||
.begin_sign("GET", "/foo?bar=baz", headers)
|
||||
.unwrap()
|
||||
.sign("hi".to_owned(), |s| {
|
||||
Ok(s.to_owned()) as Result<_, std::io::Error>
|
||||
})
|
||||
|
@ -274,6 +321,7 @@ mod tests {
|
|||
|
||||
let signature_header = config
|
||||
.begin_sign("GET", "/foo?bar=baz", headers)
|
||||
.unwrap()
|
||||
.sign("hi".to_owned(), |s| {
|
||||
Ok(s.to_owned()) as Result<_, std::io::Error>
|
||||
})
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
//! Types and methods to verify a signature or authorization header
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
collections::{BTreeMap, HashMap, HashSet},
|
||||
error::Error,
|
||||
fmt,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
build_signing_string, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD, HEADERS_FIELD,
|
||||
KEY_ID_FIELD, SIGNATURE_FIELD,
|
||||
build_signing_string, RequiredError, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD,
|
||||
HEADERS_FIELD, KEY_ID_FIELD, SIGNATURE_FIELD,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -214,7 +214,8 @@ impl ParsedHeader {
|
|||
method: &str,
|
||||
path_and_query: &str,
|
||||
headers: &mut BTreeMap<String, String>,
|
||||
) -> Unvalidated {
|
||||
required_headers: HashSet<String>,
|
||||
) -> Result<Unvalidated, RequiredError> {
|
||||
let date = headers.get("date").cloned();
|
||||
|
||||
let signing_string = build_signing_string(
|
||||
|
@ -224,9 +225,10 @@ impl ParsedHeader {
|
|||
self.expires,
|
||||
&self.headers,
|
||||
headers,
|
||||
);
|
||||
required_headers,
|
||||
)?;
|
||||
|
||||
Unvalidated {
|
||||
Ok(Unvalidated {
|
||||
key_id: self.key_id,
|
||||
signature: self.signature,
|
||||
parsed_at: self.parsed_at,
|
||||
|
@ -235,7 +237,7 @@ impl ParsedHeader {
|
|||
expires: self.expires,
|
||||
date,
|
||||
signing_string,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue