#![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) //! - [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. //! //! ```rust //! use http_signature_normalization::Config; //! use std::{collections::BTreeMap, time::Duration}; //! //! fn main() -> Result<(), Box> { //! let config = Config::default().set_expiration(Duration::from_secs(5)); //! //! let headers = BTreeMap::new(); //! //! let signature_header_value = config //! .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> //! })? //! .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(()) //! } //! ``` use std::{ collections::{BTreeMap, HashSet}, num::ParseIntError, time::{Duration, SystemTime, UNIX_EPOCH}, }; pub mod create; pub mod verify; use self::{ create::Unsigned, verify::{ParseSignatureError, ParsedHeader, Unverified, ValidateError}, }; 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"; #[derive(Clone, Debug)] /// 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 { expires_after: Duration, use_created_field: bool, required_headers: Vec, } #[derive(Debug)] /// Error preparing a header for validation /// /// This could be due to a missing header, and unparsable header, or an expired header pub enum PrepareVerifyError { /// Error validating the header Validate(ValidateError), /// Error parsing the header Parse(ParseSignatureError), /// Missing required headers Required(RequiredError), } impl std::fmt::Display for PrepareVerifyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Validate(ref e) => std::fmt::Display::fmt(e, f), Self::Parse(ref e) => std::fmt::Display::fmt(e, f), Self::Required(ref e) => std::fmt::Display::fmt(e, f), } } } impl std::error::Error for PrepareVerifyError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { Self::Validate(ref e) => Some(e), Self::Parse(ref e) => Some(e), Self::Required(ref e) => Some(e), } } } impl From for PrepareVerifyError { fn from(e: ValidateError) -> Self { Self::Validate(e) } } impl From for PrepareVerifyError { fn from(e: ParseSignatureError) -> Self { Self::Parse(e) } } impl From for PrepareVerifyError { fn from(e: RequiredError) -> Self { Self::Required(e) } } #[derive(Debug)] /// Failed to build a signing string due to missing required headers pub struct RequiredError(HashSet); impl std::fmt::Display for RequiredError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "Missing required headers {:?}", self.0) } } impl std::error::Error for RequiredError {} impl RequiredError { /// Retrieve the missing headers from the error pub fn headers(&self) -> &HashSet { &self.0 } /// Take the headers from the error pub fn take_headers(&mut self) -> HashSet { std::mem::take(&mut self.0) } } impl Config { /// Create a new Config with a default expiration of 10 seconds pub const fn new() -> Self { Config { expires_after: Duration::from_secs(10), use_created_field: true, required_headers: Vec::new(), } } /// 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 /// /// 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 /// /// 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.require_header("date") } /// Set the expiration to a custom duration pub const fn set_expiration(mut self, expires_after: Duration) -> Self { self.expires_after = expires_after; self } /// Mark a header as required pub fn require_header(mut self, header: &str) -> Self { self.required_headers.push(header.to_lowercase()); self } /// Perform the neccessary operations to produce an [`Unsigned`] type, which can be used to /// sign the header pub fn begin_sign( &self, method: &str, path_and_query: &str, headers: BTreeMap, ) -> Result { let mut headers = headers .into_iter() .map(|(k, v)| (k.to_lowercase(), v)) .collect(); let sig_headers = self.build_headers_list(&headers); let (created, expires) = if self.use_created_field { let created = SystemTime::now(); let expires = created + self.expires_after; (Some(created), Some(expires)) } else { (None, None) }; let signing_string = build_signing_string( method, path_and_query, created, expires, &sig_headers, &mut headers, self.required_headers.iter().cloned().collect(), )?; Ok(Unsigned { signing_string, sig_headers, created, expires, }) } /// Perform the neccessary operations to produce and [`Unverified`] type, which can be used to /// verify the header pub fn begin_verify( &self, method: &str, path_and_query: &str, headers: BTreeMap, ) -> Result { let mut headers: BTreeMap = headers .into_iter() .map(|(k, v)| (k.to_lowercase(), v)) .collect(); let header = headers .remove("authorization") .or_else(|| headers.remove("signature")) .ok_or(ValidateError::Missing)?; let parsed_header: ParsedHeader = header.parse()?; let unvalidated = parsed_header.into_unvalidated( method, path_and_query, &mut headers, self.required_headers.iter().cloned().collect(), )?; Ok(unvalidated.validate(self.expires_after)?) } fn build_headers_list(&self, btm: &BTreeMap) -> Vec { let http_header_keys: Vec = btm.keys().cloned().collect(); 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()] }; sig_headers.extend(http_header_keys); sig_headers } } fn build_signing_string( method: &str, path_and_query: &str, created: Option, expires: Option, sig_headers: &[String], btm: &mut BTreeMap, mut required_headers: HashSet, ) -> Result { let request_target = format!("{} {}", method.to_string().to_lowercase(), path_and_query); btm.insert(REQUEST_TARGET.to_owned(), request_target); if let Some(created) = created { btm.insert(CREATED.to_owned(), unix_timestamp(created).to_string()); } if let Some(expires) = expires { btm.insert(EXPIRES.to_owned(), unix_timestamp(expires).to_string()); } let signing_string = sig_headers .iter() .filter_map(|h| { let opt = btm.remove(h).map(|v| format!("{}: {}", h, v)); if opt.is_some() { required_headers.remove(h); } opt }) .collect::>() .join("\n"); if !required_headers.is_empty() { return Err(RequiredError(required_headers)); } Ok(signing_string) } impl Default for Config { fn default() -> Self { Self::new() } } fn unix_timestamp(time: SystemTime) -> u64 { time.duration_since(UNIX_EPOCH) .expect("UNIX_EPOCH is never in the future") .as_secs() } fn parse_unix_timestamp(s: &str) -> Result { let u: u64 = s.parse()?; Ok(UNIX_EPOCH + Duration::from_secs(u)) } #[cfg(test)] mod tests { use super::Config; use std::collections::BTreeMap; fn prepare_headers() -> BTreeMap { let mut headers = BTreeMap::new(); headers.insert( "Content-Type".to_owned(), "application/activity+json".to_owned(), ); 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().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> }) .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() .verify(|sig, signing_string| sig == signing_string); assert!(verified); } #[test] fn round_trip_signature() { let headers = prepare_headers(); let config = Config::default(); 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> }) .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); } }