http-signature-normalization/src/lib.rs
2019-09-11 00:17:30 -05:00

150 lines
3.8 KiB
Rust

use chrono::{DateTime, Duration, Utc};
use http::{
header::{HeaderMap, ToStrError},
method::Method,
uri::PathAndQuery,
};
use std::collections::BTreeMap;
pub mod create;
pub mod verify;
use self::{
create::Unsigned,
verify::{Unvalidated, ValidateError},
};
const REQUEST_TARGET: &'static str = "(request-target)";
const CREATED: &'static str = "(crated)";
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)]
pub struct Config {
pub expires: Duration,
}
impl Config {
pub fn normalize(
&self,
method: Method,
path_and_query: &PathAndQuery,
headers: &HeaderMap,
) -> Result<Unsigned, ToStrError> {
let (sig_headers, mut btm) = build_headers_list(headers)?;
let created = Utc::now();
let expires = created + self.expires;
let signing_string = build_signing_string(
method,
path_and_query,
Some(created),
Some(expires),
&sig_headers,
&mut btm,
);
Ok(Unsigned {
signing_string,
sig_headers,
created,
expires,
})
}
pub fn validate<F, T>(&self, unvalidated: Unvalidated, f: F) -> Result<T, ValidateError>
where
F: FnOnce(&[u8], &str) -> T,
{
if let Some(expires) = unvalidated.expires {
if expires < unvalidated.parsed_at {
return Err(ValidateError::Expired);
}
}
if let Some(created) = unvalidated.created {
if created + self.expires < unvalidated.parsed_at {
return Err(ValidateError::Expired);
}
}
let v = base64::decode(&unvalidated.signature).map_err(|_| ValidateError::Decode)?;
Ok((f)(&v, &unvalidated.signing_string))
}
}
fn build_headers_list(
headers: &HeaderMap,
) -> Result<(Vec<String>, BTreeMap<String, String>), ToStrError> {
let btm: BTreeMap<String, String> = headers
.iter()
.map(|(k, v)| {
v.to_str()
.map(|v| (k.as_str().to_lowercase().to_owned(), v.to_owned()))
})
.collect::<Result<BTreeMap<String, String>, _>>()?;
let http_header_keys: Vec<String> = btm.keys().cloned().collect();
let mut sig_headers = vec![
REQUEST_TARGET.to_owned(),
CREATED.to_owned(),
EXPIRES.to_owned(),
];
sig_headers.extend(http_header_keys);
Ok((sig_headers, btm))
}
fn build_signing_string(
method: Method,
path_and_query: &PathAndQuery,
created: Option<DateTime<Utc>>,
expires: Option<DateTime<Utc>>,
sig_headers: &[String],
btm: &mut BTreeMap<String, String>,
) -> String {
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::<Vec<_>>()
.join("\n");
signing_string
}
impl Default for Config {
fn default() -> Self {
Config {
expires: Duration::seconds(10),
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}