215 lines
5.6 KiB
Rust
215 lines
5.6 KiB
Rust
use chrono::{DateTime, Duration, Utc};
|
|
use std::{collections::BTreeMap, error::Error, fmt};
|
|
|
|
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)]
|
|
pub struct Config {
|
|
pub expires_after: Duration,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub enum VerifyError {
|
|
Validate(ValidateError),
|
|
Parse(ParseSignatureError),
|
|
}
|
|
|
|
impl Config {
|
|
pub fn begin_sign(
|
|
&self,
|
|
method: &str,
|
|
path_and_query: &str,
|
|
headers: BTreeMap<String, String>,
|
|
) -> Unsigned {
|
|
let mut headers = headers
|
|
.into_iter()
|
|
.map(|(k, v)| (k.to_lowercase(), v))
|
|
.collect();
|
|
let sig_headers = build_headers_list(&headers);
|
|
|
|
let created = Utc::now();
|
|
let expires = created + self.expires_after;
|
|
|
|
let signing_string = build_signing_string(
|
|
method,
|
|
path_and_query,
|
|
Some(created),
|
|
Some(expires),
|
|
&sig_headers,
|
|
&mut headers,
|
|
);
|
|
|
|
Unsigned {
|
|
signing_string,
|
|
sig_headers,
|
|
created,
|
|
expires,
|
|
}
|
|
}
|
|
|
|
pub fn begin_verify(
|
|
&self,
|
|
method: &str,
|
|
path_and_query: &str,
|
|
headers: BTreeMap<String, String>,
|
|
) -> Result<Unverified, VerifyError> {
|
|
let mut headers: BTreeMap<String, String> = 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);
|
|
|
|
Ok(unvalidated.validate(self.expires_after)?)
|
|
}
|
|
}
|
|
|
|
fn build_headers_list(btm: &BTreeMap<String, String>) -> Vec<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);
|
|
|
|
sig_headers
|
|
}
|
|
|
|
fn build_signing_string(
|
|
method: &str,
|
|
path_and_query: &str,
|
|
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 fmt::Display for VerifyError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match *self {
|
|
VerifyError::Validate(ref e) => fmt::Display::fmt(e, f),
|
|
VerifyError::Parse(ref e) => fmt::Display::fmt(e, f),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for VerifyError {
|
|
fn description(&self) -> &str {
|
|
match *self {
|
|
VerifyError::Validate(ref e) => e.description(),
|
|
VerifyError::Parse(ref e) => e.description(),
|
|
}
|
|
}
|
|
|
|
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
match *self {
|
|
VerifyError::Validate(ref e) => Some(e),
|
|
VerifyError::Parse(ref e) => Some(e),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ValidateError> for VerifyError {
|
|
fn from(v: ValidateError) -> Self {
|
|
VerifyError::Validate(v)
|
|
}
|
|
}
|
|
|
|
impl From<ParseSignatureError> for VerifyError {
|
|
fn from(p: ParseSignatureError) -> Self {
|
|
VerifyError::Parse(p)
|
|
}
|
|
}
|
|
|
|
impl Default for Config {
|
|
fn default() -> Self {
|
|
Config {
|
|
expires_after: Duration::seconds(10),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
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 round_trip() {
|
|
let headers = prepare_headers();
|
|
let config = Config::default();
|
|
|
|
let authorization_header = config
|
|
.begin_sign("GET", "/foo?bar=baz", headers)
|
|
.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);
|
|
}
|
|
}
|