#![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 chrono::Duration; //! use http_signature_normalization::Config; //! use std::collections::BTreeMap; //! //! fn main() -> Result<(), Box> { //! let config = Config::default().set_expiration(Duration::seconds(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 chrono::{DateTime, Duration, Utc}; use std::collections::{BTreeMap, HashSet}; pub mod create; pub mod verify; use self::{ create::Unsigned, verify::{ParseSignatureError, ParsedHeader, Unverified, ValidateError}, }; const REQUEST_TARGET: &'static str = "(request-target)"; const CREATED: &'static str = "(created)"; const EXPIRES: &'static str = "(expires)"; const KEY_ID_FIELD: &'static str = "keyId"; const ALGORITHM_FIELD: &'static str = "algorithm"; const ALGORITHM_VALUE: &'static str = "hs2019"; const CREATED_FIELD: &'static str = "created"; const EXPIRES_FIELD: &'static str = "expires"; const HEADERS_FIELD: &'static str = "headers"; const SIGNATURE_FIELD: &'static 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: HashSet, } #[derive(Debug, thiserror::Error)] /// 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("{0}")] /// Error validating the header Validate(#[from] ValidateError), #[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); impl Config { /// Create a new Config with a default expiration of 10 seconds pub fn new() -> Self { Config::default() } /// Opt out of using the (created) and (expires) fields introduced in draft 11 /// /// 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.require_header("date") } /// Set the expiration to a custom duration pub 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 .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( &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 = Utc::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.clone(), )?; Ok(Unsigned { signing_string, sig_headers, created, expires, }) } /// Perform the neccessary operations to produce and [`Unerified`] 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().to_owned(), 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.clone(), )?; 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 { 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()); 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() .filter_map(|h| btm.remove(h).map(|v| format!("{}: {}", h, v))) .collect::>() .join("\n"); Ok(signing_string) } impl Default for Config { fn default() -> Self { Config { expires_after: Duration::seconds(10), use_created_field: true, required_headers: HashSet::new(), } } } #[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); } }