Add ability to require headers in signature

This commit is contained in:
asonix 2020-04-23 12:54:56 -05:00
parent 08686beb8f
commit 90660b7f19
13 changed files with 131 additions and 54 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.4.2"
version = "0.5.0"
authors = ["asonix <asonix@asonix.dog>"]
license-file = "LICENSE"
readme = "README.md"

View file

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

View file

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

View file

@ -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")]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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