mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-24 18:31:01 +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]
|
[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.4.2"
|
version = "0.5.0"
|
||||||
authors = ["asonix <asonix@asonix.dog>"]
|
authors = ["asonix <asonix@asonix.dog>"]
|
||||||
license-file = "LICENSE"
|
license-file = "LICENSE"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
|
@ -31,7 +31,7 @@ base64 = { version = "0.12", optional = true }
|
||||||
bytes = "0.5.4"
|
bytes = "0.5.4"
|
||||||
chrono = "0.4.6"
|
chrono = "0.4.6"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
http-signature-normalization = { version = "0.4.2", path = ".." }
|
http-signature-normalization = { version = "0.5.0", path = ".." }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
sha2 = { version = "0.8", optional = true }
|
sha2 = { version = "0.8", optional = true }
|
||||||
sha3 = { 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()
|
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")
|
||||||
|
.header("Accept", "text/plain")
|
||||||
.set(actix_web::http::header::Date(SystemTime::now().into()))
|
.set(actix_web::http::header::Date(SystemTime::now().into()))
|
||||||
.signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
|
.signature_with_digest(config, "my-key-id", digest, "Hewwo-owo", |s| {
|
||||||
println!("Signing String\n{}", 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");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let config = Config::default();
|
let config = Config::default().require_header("accept");
|
||||||
|
|
||||||
request(config.clone()).await?;
|
request(config.clone()).await?;
|
||||||
request(config.dont_use_created_field()).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)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum MyError {
|
pub enum MyError {
|
||||||
#[error("Failed to read header, {0}")]
|
#[error("Failed to create signing string, {0}")]
|
||||||
Convert(#[from] ToStrError),
|
Convert(#[from] PrepareSignError),
|
||||||
|
|
||||||
#[error("Failed to create header, {0}")]
|
#[error("Failed to create header, {0}")]
|
||||||
Header(#[from] InvalidHeaderValue),
|
Header(#[from] InvalidHeaderValue),
|
||||||
|
|
|
@ -56,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
std::env::set_var("RUST_LOG", "info");
|
std::env::set_var("RUST_LOG", "info");
|
||||||
pretty_env_logger::init();
|
pretty_env_logger::init();
|
||||||
|
|
||||||
let config = Config::default();
|
let config = Config::default().require_header("accept");
|
||||||
|
|
||||||
HttpServer::new(move || {
|
HttpServer::new(move || {
|
||||||
App::new()
|
App::new()
|
||||||
|
@ -74,7 +74,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
enum MyError {
|
enum MyError {
|
||||||
#[error("Failed to verify, {}", _0)]
|
#[error("Failed to verify, {0}")]
|
||||||
Verify(#[from] PrepareVerifyError),
|
Verify(#[from] PrepareVerifyError),
|
||||||
|
|
||||||
#[error("Unsupported algorithm")]
|
#[error("Unsupported algorithm")]
|
||||||
|
|
|
@ -8,11 +8,11 @@ use actix_web::{
|
||||||
client::{ClientRequest, ClientResponse, SendRequestError},
|
client::{ClientRequest, ClientResponse, SendRequestError},
|
||||||
dev::Payload,
|
dev::Payload,
|
||||||
error::BlockingError,
|
error::BlockingError,
|
||||||
http::header::{InvalidHeaderValue, ToStrError},
|
http::header::InvalidHeaderValue,
|
||||||
};
|
};
|
||||||
use std::{fmt::Display, future::Future, pin::Pin};
|
use std::{fmt::Display, future::Future, pin::Pin};
|
||||||
|
|
||||||
use crate::{Config, Sign};
|
use crate::{Config, PrepareSignError, Sign};
|
||||||
|
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
#[cfg(feature = "sha-2")]
|
#[cfg(feature = "sha-2")]
|
||||||
|
@ -57,7 +57,7 @@ pub trait SignExt: Sign {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -79,7 +79,7 @@ pub trait SignExt: Sign {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
client::ClientRequest,
|
client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
|
||||||
error::BlockingError,
|
|
||||||
http::header::{InvalidHeaderValue, ToStrError},
|
|
||||||
web,
|
|
||||||
};
|
};
|
||||||
use std::{fmt::Display, future::Future, pin::Pin};
|
use std::{fmt::Display, future::Future, pin::Pin};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
digest::{DigestClient, DigestCreate, SignExt},
|
digest::{DigestClient, DigestCreate, SignExt},
|
||||||
Config, Sign,
|
Config, PrepareSignError, Sign,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl SignExt for ClientRequest {
|
impl SignExt for ClientRequest {
|
||||||
|
@ -23,7 +20,7 @@ impl SignExt for ClientRequest {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -60,7 +57,7 @@ impl SignExt for ClientRequest {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
|
|
@ -135,8 +135,8 @@
|
||||||
//!
|
//!
|
||||||
//! #[derive(Debug, thiserror::Error)]
|
//! #[derive(Debug, thiserror::Error)]
|
||||||
//! pub enum MyError {
|
//! pub enum MyError {
|
||||||
//! #[error("Failed to read header, {0}")]
|
//! #[error("Failed to create signing string, {0}")]
|
||||||
//! Convert(#[from] ToStrError),
|
//! Convert(#[from] PrepareSignError),
|
||||||
//!
|
//!
|
||||||
//! #[error("Failed to create header, {0}")]
|
//! #[error("Failed to create header, {0}")]
|
||||||
//! Header(#[from] InvalidHeaderValue),
|
//! Header(#[from] InvalidHeaderValue),
|
||||||
|
@ -180,12 +180,14 @@ pub mod digest;
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod middleware;
|
pub mod middleware;
|
||||||
|
|
||||||
|
pub use http_signature_normalization::RequiredError;
|
||||||
|
|
||||||
/// Useful types and traits for using this library in Actix Web
|
/// Useful types and traits for using this library in Actix Web
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
middleware::{SignatureVerified, VerifySignature},
|
middleware::{SignatureVerified, VerifySignature},
|
||||||
verify::{Algorithm, DeprecatedAlgorithm, Unverified},
|
verify::{Algorithm, DeprecatedAlgorithm, Unverified},
|
||||||
Config, PrepareVerifyError, Sign, SignatureVerify,
|
Config, PrepareSignError, PrepareVerifyError, RequiredError, Sign, SignatureVerify,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(feature = "digest")]
|
#[cfg(feature = "digest")]
|
||||||
|
@ -242,7 +244,7 @@ pub trait Sign {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -260,7 +262,7 @@ pub trait Sign {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -297,6 +299,22 @@ pub enum PrepareVerifyError {
|
||||||
#[error("Failed to read header, {0}")]
|
#[error("Failed to read header, {0}")]
|
||||||
/// An error converting the header to a string for validation
|
/// An error converting the header to a string for validation
|
||||||
Header(#[from] ToStrError),
|
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 {
|
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::Missing => PrepareVerifyError::Missing,
|
||||||
hsn::verify::ValidateError::Expired => PrepareVerifyError::Expired,
|
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
|
/// Begin the process of singing a request
|
||||||
pub fn begin_sign(
|
pub fn begin_sign(
|
||||||
&self,
|
&self,
|
||||||
method: &Method,
|
method: &Method,
|
||||||
path_and_query: Option<&PathAndQuery>,
|
path_and_query: Option<&PathAndQuery>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> Result<Unsigned, ToStrError> {
|
) -> Result<Unsigned, PrepareSignError> {
|
||||||
let headers = headers
|
let headers = headers
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
|
.map(|(k, v)| v.to_str().map(|v| (k.to_string(), v.to_string())))
|
||||||
|
@ -355,7 +383,7 @@ impl Config {
|
||||||
|
|
||||||
let unsigned = self
|
let unsigned = self
|
||||||
.config
|
.config
|
||||||
.begin_sign(&method.to_string(), &path_and_query, headers);
|
.begin_sign(&method.to_string(), &path_and_query, headers)?;
|
||||||
|
|
||||||
Ok(Unsigned { unsigned })
|
Ok(Unsigned { unsigned })
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,6 +124,10 @@ where
|
||||||
debug!("Failed to parse header {}", e);
|
debug!("Failed to parse header {}", e);
|
||||||
return Box::pin(err(VerifyError.into()));
|
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());
|
let algorithm = unverified.algorithm().map(|a| a.clone());
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
client::ClientRequest,
|
client::ClientRequest, error::BlockingError, http::header::InvalidHeaderValue, web,
|
||||||
error::BlockingError,
|
|
||||||
http::header::{InvalidHeaderValue, ToStrError},
|
|
||||||
web,
|
|
||||||
};
|
};
|
||||||
use std::{fmt::Display, future::Future, pin::Pin};
|
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 {
|
impl Sign for ClientRequest {
|
||||||
fn authorization_signature<F, E, K>(
|
fn authorization_signature<F, E, K>(
|
||||||
|
@ -18,7 +15,7 @@ impl Sign for ClientRequest {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -42,7 +39,7 @@ impl Sign for ClientRequest {
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
||||||
E: From<BlockingError<E>>
|
E: From<BlockingError<E>>
|
||||||
+ From<ToStrError>
|
+ From<PrepareSignError>
|
||||||
+ From<InvalidHeaderValue>
|
+ From<InvalidHeaderValue>
|
||||||
+ std::fmt::Debug
|
+ std::fmt::Debug
|
||||||
+ Send
|
+ Send
|
||||||
|
@ -66,7 +63,7 @@ async fn prepare<F, E, K>(
|
||||||
) -> Result<Signed, E>
|
) -> Result<Signed, E>
|
||||||
where
|
where
|
||||||
F: FnOnce(&str) -> Result<String, E> + Send + 'static,
|
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,
|
K: Display,
|
||||||
{
|
{
|
||||||
let unsigned = config.begin_sign(
|
let unsigned = config.begin_sign(
|
||||||
|
|
|
@ -13,4 +13,4 @@ edition = "2018"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
http = "0.2"
|
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"
|
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.4.0", path = ".." }
|
http-signature-normalization = { version = "0.5.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 }
|
||||||
|
|
72
src/lib.rs
72
src/lib.rs
|
@ -20,7 +20,7 @@
|
||||||
//! let headers = BTreeMap::new();
|
//! let headers = BTreeMap::new();
|
||||||
//!
|
//!
|
||||||
//! let signature_header_value = config
|
//! 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("my-key-id".to_owned(), |signing_string| {
|
||||||
//! // sign the string here
|
//! // sign the string here
|
||||||
//! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>>
|
//! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
use chrono::{DateTime, Duration, Utc};
|
use chrono::{DateTime, Duration, Utc};
|
||||||
use std::collections::BTreeMap;
|
use std::collections::{BTreeMap, HashSet};
|
||||||
|
|
||||||
pub mod create;
|
pub mod create;
|
||||||
pub mod verify;
|
pub mod verify;
|
||||||
|
@ -73,6 +73,7 @@ const SIGNATURE_FIELD: &'static str = "signature";
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
expires_after: Duration,
|
expires_after: Duration,
|
||||||
use_created_field: bool,
|
use_created_field: bool,
|
||||||
|
required_headers: HashSet<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
@ -87,8 +88,17 @@ pub enum PrepareVerifyError {
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
/// Error parsing the header
|
/// Error parsing the header
|
||||||
Parse(#[from] ParseSignatureError),
|
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 {
|
impl Config {
|
||||||
/// Create a new Config with a default expiration of 10 seconds
|
/// Create a new Config with a default expiration of 10 seconds
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
|
@ -97,10 +107,13 @@ impl Config {
|
||||||
|
|
||||||
/// Opt out of using the (created) and (expires) fields introduced in draft 11
|
/// 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 {
|
pub fn dont_use_created_field(mut self) -> Self {
|
||||||
self.use_created_field = false;
|
self.use_created_field = false;
|
||||||
self
|
self.require_header("date")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the expiration to a custom duration
|
/// Set the expiration to a custom duration
|
||||||
|
@ -109,6 +122,13 @@ impl Config {
|
||||||
self
|
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
|
/// 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(
|
||||||
|
@ -116,7 +136,7 @@ impl Config {
|
||||||
method: &str,
|
method: &str,
|
||||||
path_and_query: &str,
|
path_and_query: &str,
|
||||||
headers: BTreeMap<String, String>,
|
headers: BTreeMap<String, String>,
|
||||||
) -> Unsigned {
|
) -> Result<Unsigned, RequiredError> {
|
||||||
let mut headers = headers
|
let mut headers = headers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| (k.to_lowercase(), v))
|
.map(|(k, v)| (k.to_lowercase(), v))
|
||||||
|
@ -140,14 +160,15 @@ impl Config {
|
||||||
expires,
|
expires,
|
||||||
&sig_headers,
|
&sig_headers,
|
||||||
&mut headers,
|
&mut headers,
|
||||||
);
|
self.required_headers.clone(),
|
||||||
|
)?;
|
||||||
|
|
||||||
Unsigned {
|
Ok(Unsigned {
|
||||||
signing_string,
|
signing_string,
|
||||||
sig_headers,
|
sig_headers,
|
||||||
created,
|
created,
|
||||||
expires,
|
expires,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to
|
/// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to
|
||||||
|
@ -169,7 +190,12 @@ impl Config {
|
||||||
.ok_or(ValidateError::Missing)?;
|
.ok_or(ValidateError::Missing)?;
|
||||||
|
|
||||||
let parsed_header: ParsedHeader = header.parse()?;
|
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)?)
|
Ok(unvalidated.validate(self.expires_after)?)
|
||||||
}
|
}
|
||||||
|
@ -200,7 +226,16 @@ fn build_signing_string(
|
||||||
expires: Option<DateTime<Utc>>,
|
expires: Option<DateTime<Utc>>,
|
||||||
sig_headers: &[String],
|
sig_headers: &[String],
|
||||||
btm: &mut BTreeMap<String, 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);
|
let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query);
|
||||||
|
|
||||||
btm.insert(REQUEST_TARGET.to_owned(), request_target.clone());
|
btm.insert(REQUEST_TARGET.to_owned(), request_target.clone());
|
||||||
|
@ -217,7 +252,7 @@ fn build_signing_string(
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
signing_string
|
Ok(signing_string)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
impl Default for Config {
|
||||||
|
@ -225,6 +260,7 @@ impl Default for Config {
|
||||||
Config {
|
Config {
|
||||||
expires_after: Duration::seconds(10),
|
expires_after: Duration::seconds(10),
|
||||||
use_created_field: true,
|
use_created_field: true,
|
||||||
|
required_headers: HashSet::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,13 +279,24 @@ mod tests {
|
||||||
headers
|
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]
|
#[test]
|
||||||
fn round_trip_authorization() {
|
fn round_trip_authorization() {
|
||||||
let headers = prepare_headers();
|
let headers = prepare_headers();
|
||||||
let config = Config::default();
|
let config = Config::default().require_header("content-type");
|
||||||
|
|
||||||
let authorization_header = config
|
let authorization_header = config
|
||||||
.begin_sign("GET", "/foo?bar=baz", headers)
|
.begin_sign("GET", "/foo?bar=baz", headers)
|
||||||
|
.unwrap()
|
||||||
.sign("hi".to_owned(), |s| {
|
.sign("hi".to_owned(), |s| {
|
||||||
Ok(s.to_owned()) as Result<_, std::io::Error>
|
Ok(s.to_owned()) as Result<_, std::io::Error>
|
||||||
})
|
})
|
||||||
|
@ -274,6 +321,7 @@ mod tests {
|
||||||
|
|
||||||
let signature_header = config
|
let signature_header = config
|
||||||
.begin_sign("GET", "/foo?bar=baz", headers)
|
.begin_sign("GET", "/foo?bar=baz", headers)
|
||||||
|
.unwrap()
|
||||||
.sign("hi".to_owned(), |s| {
|
.sign("hi".to_owned(), |s| {
|
||||||
Ok(s.to_owned()) as Result<_, std::io::Error>
|
Ok(s.to_owned()) as Result<_, std::io::Error>
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
//! Types and methods to verify a signature or authorization header
|
//! Types and methods to verify a signature or authorization header
|
||||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt,
|
fmt,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
build_signing_string, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD, HEADERS_FIELD,
|
build_signing_string, RequiredError, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD,
|
||||||
KEY_ID_FIELD, SIGNATURE_FIELD,
|
HEADERS_FIELD, KEY_ID_FIELD, SIGNATURE_FIELD,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -214,7 +214,8 @@ impl ParsedHeader {
|
||||||
method: &str,
|
method: &str,
|
||||||
path_and_query: &str,
|
path_and_query: &str,
|
||||||
headers: &mut BTreeMap<String, String>,
|
headers: &mut BTreeMap<String, String>,
|
||||||
) -> Unvalidated {
|
required_headers: HashSet<String>,
|
||||||
|
) -> Result<Unvalidated, RequiredError> {
|
||||||
let date = headers.get("date").cloned();
|
let date = headers.get("date").cloned();
|
||||||
|
|
||||||
let signing_string = build_signing_string(
|
let signing_string = build_signing_string(
|
||||||
|
@ -224,9 +225,10 @@ impl ParsedHeader {
|
||||||
self.expires,
|
self.expires,
|
||||||
&self.headers,
|
&self.headers,
|
||||||
headers,
|
headers,
|
||||||
);
|
required_headers,
|
||||||
|
)?;
|
||||||
|
|
||||||
Unvalidated {
|
Ok(Unvalidated {
|
||||||
key_id: self.key_id,
|
key_id: self.key_id,
|
||||||
signature: self.signature,
|
signature: self.signature,
|
||||||
parsed_at: self.parsed_at,
|
parsed_at: self.parsed_at,
|
||||||
|
@ -235,7 +237,7 @@ impl ParsedHeader {
|
||||||
expires: self.expires,
|
expires: self.expires,
|
||||||
date,
|
date,
|
||||||
signing_string,
|
signing_string,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue