http-signature-normalization/src/lib.rs

421 lines
12 KiB
Rust
Raw Permalink Normal View History

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)
//! - [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 http_signature_normalization::Config;
2022-01-17 22:49:01 +00:00
//! use std::{collections::BTreeMap, time::Duration};
2019-09-21 16:26:11 +00:00
//!
//! fn main() -> Result<(), Box<dyn std::error::Error>> {
2022-01-17 22:49:01 +00:00
//! let config = Config::default().set_expiration(Duration::from_secs(5));
2019-09-21 16:26:11 +00:00
//!
//! let headers = BTreeMap::new();
//!
//! let signature_header_value = config
//! .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(())
//! }
//! ```
2022-01-17 22:49:01 +00:00
use std::{
collections::{BTreeMap, HashSet},
num::ParseIntError,
time::{Duration, SystemTime, UNIX_EPOCH},
};
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
///
/// 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 {
expires_after: Duration,
use_created_field: bool,
2022-11-29 00:25:00 +00:00
required_headers: Vec<String>,
2019-09-11 06:24:51 +00:00
}
2022-01-17 22:49:01 +00:00
#[derive(Debug)]
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 {
/// Error validating the header
2022-01-17 22:49:01 +00:00
Validate(ValidateError),
2019-09-21 16:26:11 +00:00
/// Error parsing the header
2022-01-17 22:49:01 +00:00
Parse(ParseSignatureError),
/// Missing required headers
2022-01-17 22:49:01 +00:00
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<ValidateError> for PrepareVerifyError {
fn from(e: ValidateError) -> Self {
Self::Validate(e)
}
}
impl From<ParseSignatureError> for PrepareVerifyError {
fn from(e: ParseSignatureError) -> Self {
Self::Parse(e)
}
}
impl From<RequiredError> for PrepareVerifyError {
fn from(e: RequiredError) -> Self {
Self::Required(e)
}
2019-09-11 05:17:30 +00:00
}
2022-01-17 22:49:01 +00:00
#[derive(Debug)]
/// Failed to build a signing string due to missing required headers
pub struct RequiredError(HashSet<String>);
2022-01-17 22:49:01 +00:00
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<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 {
/// Create a new Config with a default expiration of 10 seconds
2022-11-29 00:25:00 +00:00
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
2022-11-29 00:25:00 +00:00
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 {
2022-11-29 00:25:00 +00:00
self.required_headers.push(header.to_lowercase());
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>,
) -> 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
let sig_headers = self.build_headers_list(&headers);
let (created, expires) = if self.use_created_field {
2022-01-17 22:49:01 +00:00
let created = SystemTime::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,
created,
expires,
2019-09-11 05:17:30 +00:00
&sig_headers,
2019-09-11 06:24:51 +00:00
&mut headers,
2022-11-29 00:25:00 +00:00
self.required_headers.iter().cloned().collect(),
)?;
2019-09-11 05:17:30 +00:00
Ok(Unsigned {
2019-09-11 05:17:30 +00:00
signing_string,
sig_headers,
created,
expires,
})
2019-09-11 05:17:30 +00:00
}
2022-11-29 00:40:02 +00:00
/// Perform the neccessary operations to produce and [`Unverified`] type, which can be used to
2019-09-21 16:26:11 +00:00
/// 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()?;
let unvalidated = parsed_header.into_unvalidated(
method,
path_and_query,
&mut headers,
2022-11-29 00:25:00 +00:00
self.required_headers.iter().cloned().collect(),
)?;
2019-09-11 06:24:51 +00:00
Ok(unvalidated.validate(self.expires_after)?)
2019-09-11 05:17:30 +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
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
sig_headers.extend(http_header_keys);
2019-09-11 05:17:30 +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,
2022-01-17 22:49:01 +00:00
created: Option<SystemTime>,
expires: Option<SystemTime>,
2019-09-11 05:17:30 +00:00
sig_headers: &[String],
btm: &mut BTreeMap<String, String>,
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 {
2022-01-17 22:49:01 +00:00
btm.insert(CREATED.to_owned(), unix_timestamp(created).to_string());
2019-09-11 05:17:30 +00:00
}
if let Some(expires) = expires {
2022-01-17 22:49:01 +00:00
btm.insert(EXPIRES.to_owned(), unix_timestamp(expires).to_string());
2019-09-11 05:17:30 +00:00
}
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
})
2019-09-11 05:17:30 +00:00
.collect::<Vec<_>>()
.join("\n");
if !required_headers.is_empty() {
return Err(RequiredError(required_headers));
}
Ok(signing_string)
2019-09-11 05:17:30 +00:00
}
impl Default for Config {
fn default() -> Self {
2022-11-29 00:25:00 +00:00
Self::new()
2019-09-11 05:17:30 +00:00
}
}
2022-01-17 22:49:01 +00:00
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<SystemTime, ParseIntError> {
let u: u64 = s.parse()?;
Ok(UNIX_EPOCH + Duration::from_secs(u))
}
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
}
#[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();
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)
.unwrap()
2019-09-11 06:24:51 +00:00
.sign("hi".to_owned(), |s| {
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()
.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)
.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
}