2019-09-21 16:26:11 +00:00
#![ deny(missing_docs) ]
//! # HTTP Signature Normaliztion
//! _An HTTP Signatures library that leaves the signing to you_
//!
//! - [crates.io](https://crates.io/crates/http-signature-normalization)
//! - [docs.rs](https://docs.rs/http-signature-normalization)
2020-03-17 23:54:00 +00:00
//! - [Hit me up on Mastodon](https://asonix.dog/@asonix)
2019-09-21 16:26:11 +00:00
//!
//! 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.
//!
//! ```rust
//! use chrono::Duration;
//! use http_signature_normalization::Config;
//! use std::collections::BTreeMap;
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
2020-03-17 23:54:00 +00:00
//! let config = Config::default().set_expiration(Duration::seconds(5));
2019-09-21 16:26:11 +00:00
//!
//! let headers = BTreeMap::new();
//!
//! let signature_header_value = config
2020-04-23 17:54:56 +00:00
//! .begin_sign("GET", "/foo?bar=baz", headers)?
2019-09-21 16:26:11 +00:00
//! .sign("my-key-id".to_owned(), |signing_string| {
//! // sign the string here
//! Ok(signing_string.to_owned()) as Result<_, Box<dyn std::error::Error>>
//! })?
//! .signature_header();
//!
//! let mut headers = BTreeMap::new();
//! headers.insert("Signature".to_owned(), signature_header_value);
//!
//! let verified = config
//! .begin_verify("GET", "/foo?bar=baz", headers)?
//! .verify(|sig, signing_string| {
//! // Verify the signature here
//! sig == signing_string
//! });
//!
//! assert!(verified);
//! Ok(())
//! }
//! ```
2019-09-11 05:17:30 +00:00
use chrono ::{ DateTime , Duration , Utc } ;
2020-04-23 17:54:56 +00:00
use std ::collections ::{ BTreeMap , HashSet } ;
2019-09-11 05:17:30 +00:00
pub mod create ;
pub mod verify ;
use self ::{
create ::Unsigned ,
2019-09-11 06:24:51 +00:00
verify ::{ ParseSignatureError , ParsedHeader , Unverified , ValidateError } ,
2019-09-11 05:17:30 +00:00
} ;
2020-04-26 01:31:38 +00:00
const REQUEST_TARGET : & str = " (request-target) " ;
const CREATED : & str = " (created) " ;
const EXPIRES : & str = " (expires) " ;
const KEY_ID_FIELD : & str = " keyId " ;
const ALGORITHM_FIELD : & str = " algorithm " ;
const ALGORITHM_VALUE : & str = " hs2019 " ;
const CREATED_FIELD : & str = " created " ;
const EXPIRES_FIELD : & str = " expires " ;
const HEADERS_FIELD : & str = " headers " ;
const SIGNATURE_FIELD : & str = " signature " ;
2019-09-11 05:17:30 +00:00
2019-09-11 06:24:51 +00:00
#[ derive(Clone, Debug) ]
2019-09-21 16:26:11 +00:00
/// Configuration for signing and verifying signatures
///
2020-03-17 23:54:00 +00:00
/// 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
2019-09-11 05:17:30 +00:00
pub struct Config {
2020-03-17 23:54:00 +00:00
expires_after : Duration ,
use_created_field : bool ,
2020-04-23 17:54:56 +00:00
required_headers : HashSet < String > ,
2019-09-11 06:24:51 +00:00
}
2020-03-16 00:29:47 +00:00
#[ derive(Debug, thiserror::Error) ]
2019-09-21 16:26:11 +00:00
/// Error preparing a header for validation
///
/// This could be due to a missing header, and unparsable header, or an expired header
pub enum PrepareVerifyError {
2020-03-16 00:29:47 +00:00
#[ error( " {0} " ) ]
2019-09-21 16:26:11 +00:00
/// Error validating the header
2020-03-16 00:29:47 +00:00
Validate ( #[ from ] ValidateError ) ,
#[ error( " {0} " ) ]
2019-09-21 16:26:11 +00:00
/// Error parsing the header
2020-03-16 00:29:47 +00:00
Parse ( #[ from ] ParseSignatureError ) ,
2020-04-23 17:54:56 +00:00
#[ error( " {0} " ) ]
/// Missing required headers
Required ( #[ from ] RequiredError ) ,
2019-09-11 05:17:30 +00:00
}
2020-04-23 17:54:56 +00:00
#[ 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 > ) ;
2021-09-18 00:32:45 +00:00
impl RequiredError {
/// Retrieve the missing headers from the error
pub fn headers ( & self ) -> & HashSet < String > {
& self . 0
}
/// Take the headers from the error
pub fn take_headers ( & mut self ) -> HashSet < String > {
std ::mem ::take ( & mut self . 0 )
}
}
2019-09-11 05:17:30 +00:00
impl Config {
2020-03-17 23:54:00 +00:00
/// Create a new Config with a default expiration of 10 seconds
pub fn new ( ) -> Self {
Config ::default ( )
}
2020-09-29 23:57:59 +00:00
/// Enable mastodon compatibility
///
/// This is the same as disabling the use of `(created)` and `(expires)` signature fields,
/// requiring the Date header, and requiring the Host header
pub fn mastodon_compat ( self ) -> Self {
self . dont_use_created_field ( ) . require_header ( " host " )
}
/// Require the Digest header be set
2020-03-17 23:54:00 +00:00
///
2020-09-29 23:57:59 +00:00
/// This is useful for POST, PUT, and PATCH requests, but doesn't make sense for GET or DELETE.
pub fn require_digest ( self ) -> Self {
self . require_header ( " Digest " )
}
/// Opt out of using the (created) and (expires) fields introduced in draft 11
2020-04-23 17:54:56 +00:00
///
/// Note that by not requiring the created field, the Date header becomes required. This is to
/// prevent replay attacks.
2020-03-17 23:54:00 +00:00
pub fn dont_use_created_field ( mut self ) -> Self {
self . use_created_field = false ;
2020-04-23 17:54:56 +00:00
self . require_header ( " date " )
2020-03-17 23:54:00 +00:00
}
/// Set the expiration to a custom duration
pub fn set_expiration ( mut self , expires_after : Duration ) -> Self {
self . expires_after = expires_after ;
self
}
2020-04-23 17:54:56 +00:00
/// Mark a header as required
pub fn require_header ( mut self , header : & str ) -> Self {
2020-04-26 01:31:38 +00:00
self . required_headers . insert ( header . to_lowercase ( ) ) ;
2020-04-23 17:54:56 +00:00
self
}
2019-09-21 16:26:11 +00:00
/// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to
/// sign the header
2019-09-11 06:24:51 +00:00
pub fn begin_sign (
2019-09-11 05:17:30 +00:00
& self ,
2019-09-11 05:24:04 +00:00
method : & str ,
path_and_query : & str ,
2019-09-11 06:24:51 +00:00
headers : BTreeMap < String , String > ,
2020-04-23 17:54:56 +00:00
) -> Result < Unsigned , RequiredError > {
2019-09-11 06:24:51 +00:00
let mut headers = headers
. into_iter ( )
. map ( | ( k , v ) | ( k . to_lowercase ( ) , v ) )
. collect ( ) ;
2019-09-11 05:17:30 +00:00
2020-03-17 23:54:00 +00:00
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 )
} ;
2019-09-11 05:17:30 +00:00
let signing_string = build_signing_string (
method ,
path_and_query ,
2020-03-17 23:54:00 +00:00
created ,
expires ,
2019-09-11 05:17:30 +00:00
& sig_headers ,
2019-09-11 06:24:51 +00:00
& mut headers ,
2020-04-23 17:54:56 +00:00
self . required_headers . clone ( ) ,
) ? ;
2019-09-11 05:17:30 +00:00
2020-04-23 17:54:56 +00:00
Ok ( Unsigned {
2019-09-11 05:17:30 +00:00
signing_string ,
sig_headers ,
created ,
expires ,
2020-04-23 17:54:56 +00:00
} )
2019-09-11 05:17:30 +00:00
}
2019-09-21 16:26:11 +00:00
/// Perform the neccessary operations to produce and [`Unerified`] type, which can be used to
/// verify the header
2019-09-11 06:24:51 +00:00
pub fn begin_verify (
& self ,
method : & str ,
path_and_query : & str ,
headers : BTreeMap < String , String > ,
2019-09-21 16:26:11 +00:00
) -> Result < Unverified , PrepareVerifyError > {
2019-09-11 06:24:51 +00:00
let mut headers : BTreeMap < String , String > = headers
. into_iter ( )
2020-04-26 01:31:38 +00:00
. map ( | ( k , v ) | ( k . to_lowercase ( ) , v ) )
2019-09-11 06:24:51 +00:00
. collect ( ) ;
let header = headers
. remove ( " authorization " )
. or_else ( | | headers . remove ( " signature " ) )
. ok_or ( ValidateError ::Missing ) ? ;
let parsed_header : ParsedHeader = header . parse ( ) ? ;
2020-04-23 17:54:56 +00:00
let unvalidated = parsed_header . into_unvalidated (
method ,
path_and_query ,
& mut headers ,
self . required_headers . clone ( ) ,
) ? ;
2019-09-11 06:24:51 +00:00
Ok ( unvalidated . validate ( self . expires_after ) ? )
2019-09-11 05:17:30 +00:00
}
2020-03-17 23:54:00 +00:00
fn build_headers_list ( & self , btm : & BTreeMap < String , String > ) -> Vec < String > {
let http_header_keys : Vec < String > = btm . keys ( ) . cloned ( ) . collect ( ) ;
2019-09-11 05:17:30 +00:00
2020-03-17 23:54:00 +00:00
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 ( ) ]
} ;
2019-09-11 05:17:30 +00:00
2020-03-17 23:54:00 +00:00
sig_headers . extend ( http_header_keys ) ;
2019-09-11 05:17:30 +00:00
2020-03-17 23:54:00 +00:00
sig_headers
}
2019-09-11 05:17:30 +00:00
}
fn build_signing_string (
2019-09-11 05:24:04 +00:00
method : & str ,
path_and_query : & str ,
2019-09-11 05:17:30 +00:00
created : Option < DateTime < Utc > > ,
expires : Option < DateTime < Utc > > ,
sig_headers : & [ String ] ,
btm : & mut BTreeMap < String , String > ,
2020-04-23 17:54:56 +00:00
mut required_headers : HashSet < String > ,
) -> Result < String , RequiredError > {
2019-09-11 05:17:30 +00:00
let request_target = format! ( " {} {} " , method . to_string ( ) . to_lowercase ( ) , path_and_query ) ;
2020-04-26 01:31:38 +00:00
btm . insert ( REQUEST_TARGET . to_owned ( ) , request_target ) ;
2019-09-11 05:17:30 +00:00
if let Some ( created ) = created {
btm . insert ( CREATED . to_owned ( ) , created . timestamp ( ) . to_string ( ) ) ;
}
if let Some ( expires ) = expires {
btm . insert ( EXPIRES . to_owned ( ) , expires . timestamp ( ) . to_string ( ) ) ;
}
let signing_string = sig_headers
. iter ( )
2020-04-23 19:46:14 +00:00
. filter_map ( | h | {
let opt = btm . remove ( h ) . map ( | v | format! ( " {} : {} " , h , v ) ) ;
if opt . is_some ( ) {
required_headers . remove ( h ) ;
}
opt
} )
2019-09-11 05:17:30 +00:00
. collect ::< Vec < _ > > ( )
. join ( " \n " ) ;
2020-04-23 19:46:14 +00:00
if ! required_headers . is_empty ( ) {
return Err ( RequiredError ( required_headers ) ) ;
}
2020-04-23 17:54:56 +00:00
Ok ( signing_string )
2019-09-11 05:17:30 +00:00
}
impl Default for Config {
fn default ( ) -> Self {
Config {
2019-09-11 06:24:51 +00:00
expires_after : Duration ::seconds ( 10 ) ,
2020-03-17 23:54:00 +00:00
use_created_field : true ,
2020-04-23 17:54:56 +00:00
required_headers : HashSet ::new ( ) ,
2019-09-11 05:17:30 +00:00
}
}
}
#[ cfg(test) ]
mod tests {
2019-09-11 06:24:51 +00:00
use super ::Config ;
use std ::collections ::BTreeMap ;
fn prepare_headers ( ) -> BTreeMap < String , String > {
let mut headers = BTreeMap ::new ( ) ;
headers . insert (
" Content-Type " . to_owned ( ) ,
" application/activity+json " . to_owned ( ) ,
) ;
headers
}
2020-04-23 17:54:56 +00:00
#[ 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 ( ) )
}
2019-09-11 05:17:30 +00:00
#[ test ]
2019-09-21 16:26:11 +00:00
fn round_trip_authorization ( ) {
2019-09-11 06:24:51 +00:00
let headers = prepare_headers ( ) ;
2020-04-23 17:54:56 +00:00
let config = Config ::default ( ) . require_header ( " content-type " ) ;
2019-09-11 06:24:51 +00:00
let authorization_header = config
. begin_sign ( " GET " , " /foo?bar=baz " , headers )
2020-04-23 17:54:56 +00:00
. unwrap ( )
2019-09-11 06:24:51 +00:00
. sign ( " hi " . to_owned ( ) , | s | {
2019-09-13 22:55:51 +00:00
Ok ( s . to_owned ( ) ) as Result < _ , std ::io ::Error >
2019-09-11 06:24:51 +00:00
} )
. unwrap ( )
. authorization_header ( ) ;
let mut headers = prepare_headers ( ) ;
headers . insert ( " Authorization " . to_owned ( ) , authorization_header ) ;
let verified = config
. begin_verify ( " GET " , " /foo?bar=baz " , headers )
. unwrap ( )
2019-09-13 22:55:51 +00:00
. verify ( | sig , signing_string | sig = = signing_string ) ;
2019-09-11 06:24:51 +00:00
assert! ( verified ) ;
2019-09-11 05:17:30 +00:00
}
2019-09-21 16:26:11 +00:00
#[ test ]
fn round_trip_signature ( ) {
let headers = prepare_headers ( ) ;
let config = Config ::default ( ) ;
let signature_header = config
. begin_sign ( " GET " , " /foo?bar=baz " , headers )
2020-04-23 17:54:56 +00:00
. unwrap ( )
2019-09-21 16:26:11 +00:00
. sign ( " hi " . to_owned ( ) , | s | {
Ok ( s . to_owned ( ) ) as Result < _ , std ::io ::Error >
} )
. unwrap ( )
. signature_header ( ) ;
let mut headers = prepare_headers ( ) ;
headers . insert ( " Signature " . to_owned ( ) , signature_header ) ;
let verified = config
. begin_verify ( " GET " , " /foo?bar=baz " , headers )
. unwrap ( )
. verify ( | sig , signing_string | sig = = signing_string ) ;
assert! ( verified ) ;
}
2019-09-11 05:17:30 +00:00
}