mirror of
https://git.asonix.dog/asonix/http-signature-normalization.git
synced 2024-11-25 10:51:01 +00:00
Test round trip
This commit is contained in:
parent
aefb08e627
commit
c73da59045
3 changed files with 226 additions and 52 deletions
|
@ -5,6 +5,7 @@ use crate::{
|
|||
SIGNATURE_FIELD,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Signed {
|
||||
signature: String,
|
||||
sig_headers: Vec<String>,
|
||||
|
@ -13,6 +14,7 @@ pub struct Signed {
|
|||
key_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unsigned {
|
||||
pub(crate) signing_string: String,
|
||||
pub(crate) sig_headers: Vec<String>,
|
||||
|
|
138
src/lib.rs
138
src/lib.rs
|
@ -1,16 +1,16 @@
|
|||
use chrono::{DateTime, Duration, Utc};
|
||||
use std::collections::BTreeMap;
|
||||
use std::{collections::BTreeMap, error::Error, fmt};
|
||||
|
||||
pub mod create;
|
||||
pub mod verify;
|
||||
|
||||
use self::{
|
||||
create::Unsigned,
|
||||
verify::{Unvalidated, ValidateError},
|
||||
verify::{ParseSignatureError, ParsedHeader, Unverified, ValidateError},
|
||||
};
|
||||
|
||||
const REQUEST_TARGET: &'static str = "(request-target)";
|
||||
const CREATED: &'static str = "(crated)";
|
||||
const CREATED: &'static str = "(created)";
|
||||
const EXPIRES: &'static str = "(expires)";
|
||||
|
||||
const KEY_ID_FIELD: &'static str = "keyId";
|
||||
|
@ -21,22 +21,32 @@ const EXPIRES_FIELD: &'static str = "expires";
|
|||
const HEADERS_FIELD: &'static str = "headers";
|
||||
const SIGNATURE_FIELD: &'static str = "signature";
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub expires: Duration,
|
||||
pub expires_after: Duration,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum VerifyError {
|
||||
Validate(ValidateError),
|
||||
Parse(ParseSignatureError),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn normalize(
|
||||
pub fn begin_sign(
|
||||
&self,
|
||||
method: &str,
|
||||
path_and_query: &str,
|
||||
headers: &mut BTreeMap<String, String>,
|
||||
headers: BTreeMap<String, String>,
|
||||
) -> Unsigned {
|
||||
let sig_headers = build_headers_list(headers);
|
||||
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;
|
||||
let expires = created + self.expires_after;
|
||||
|
||||
let signing_string = build_signing_string(
|
||||
method,
|
||||
|
@ -44,7 +54,7 @@ impl Config {
|
|||
Some(created),
|
||||
Some(expires),
|
||||
&sig_headers,
|
||||
headers,
|
||||
&mut headers,
|
||||
);
|
||||
|
||||
Unsigned {
|
||||
|
@ -55,24 +65,26 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
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 v = base64::decode(&unvalidated.signature).map_err(|_| ValidateError::Decode)?;
|
||||
let header = headers
|
||||
.remove("authorization")
|
||||
.or_else(|| headers.remove("signature"))
|
||||
.ok_or(ValidateError::Missing)?;
|
||||
|
||||
Ok((f)(&v, &unvalidated.signing_string))
|
||||
let parsed_header: ParsedHeader = header.parse()?;
|
||||
let unvalidated = parsed_header.into_unvalidated(method, path_and_query, &mut headers);
|
||||
|
||||
Ok(unvalidated.validate(self.expires_after)?)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,18 +129,86 @@ fn build_signing_string(
|
|||
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: Duration::seconds(10),
|
||||
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 it_works() {
|
||||
assert_eq!(2 + 2, 4);
|
||||
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.as_bytes().to_vec()) 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(|bytes, string| string.as_bytes() == bytes);
|
||||
|
||||
assert!(verified);
|
||||
}
|
||||
}
|
||||
|
|
138
src/verify.rs
138
src/verify.rs
|
@ -1,4 +1,4 @@
|
|||
use chrono::{DateTime, TimeZone, Utc};
|
||||
use chrono::{DateTime, Duration, TimeZone, Utc};
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
error::Error,
|
||||
|
@ -11,6 +11,15 @@ use crate::{
|
|||
KEY_ID_FIELD, SIGNATURE_FIELD,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unverified {
|
||||
key_id: String,
|
||||
signature: Vec<u8>,
|
||||
algorithm: Option<Algorithm>,
|
||||
signing_string: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Unvalidated {
|
||||
pub(crate) key_id: String,
|
||||
pub(crate) signature: String,
|
||||
|
@ -21,6 +30,7 @@ pub struct Unvalidated {
|
|||
pub(crate) signing_string: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ParsedHeader {
|
||||
signature: String,
|
||||
key_id: String,
|
||||
|
@ -56,6 +66,7 @@ pub enum Algorithm {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ValidateError {
|
||||
Missing,
|
||||
Expired,
|
||||
Decode,
|
||||
}
|
||||
|
@ -63,7 +74,7 @@ pub enum ValidateError {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct ParseSignatureError(&'static str);
|
||||
|
||||
impl Unvalidated {
|
||||
impl Unverified {
|
||||
pub fn key_id(&self) -> &str {
|
||||
&self.key_id
|
||||
}
|
||||
|
@ -71,10 +82,41 @@ impl Unvalidated {
|
|||
pub fn algorithm(&self) -> Option<&Algorithm> {
|
||||
self.algorithm.as_ref()
|
||||
}
|
||||
|
||||
pub fn verify<F, T>(&self, f: F) -> T
|
||||
where
|
||||
F: FnOnce(&[u8], &str) -> T,
|
||||
{
|
||||
(f)(&self.signature, &self.signing_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl Unvalidated {
|
||||
pub fn validate(self, expires_after: Duration) -> Result<Unverified, ValidateError> {
|
||||
if let Some(expires) = self.expires {
|
||||
if expires < self.parsed_at {
|
||||
return Err(ValidateError::Expired);
|
||||
}
|
||||
}
|
||||
if let Some(created) = self.created {
|
||||
if created + expires_after < self.parsed_at {
|
||||
return Err(ValidateError::Expired);
|
||||
}
|
||||
}
|
||||
|
||||
let signature = base64::decode(&self.signature).map_err(|_| ValidateError::Decode)?;
|
||||
|
||||
Ok(Unverified {
|
||||
key_id: self.key_id,
|
||||
algorithm: self.algorithm,
|
||||
signing_string: self.signing_string,
|
||||
signature,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ParsedHeader {
|
||||
pub fn to_unvalidated(
|
||||
pub fn into_unvalidated(
|
||||
self,
|
||||
method: &str,
|
||||
path_and_query: &str,
|
||||
|
@ -113,7 +155,7 @@ impl FromStr for ParsedHeader {
|
|||
|
||||
if let Some(key) = i.next() {
|
||||
if let Some(value) = i.next() {
|
||||
return Some((key.to_owned(), value.to_owned()));
|
||||
return Some((key.to_owned(), value.trim_matches('"').to_owned()));
|
||||
}
|
||||
}
|
||||
None
|
||||
|
@ -131,7 +173,7 @@ impl FromStr for ParsedHeader {
|
|||
.remove(HEADERS_FIELD)
|
||||
.map(|h| h.split_whitespace().map(|s| s.to_owned()).collect())
|
||||
.unwrap_or_else(|| vec![CREATED.to_owned()]),
|
||||
algorithm: hm.remove(ALGORITHM_FIELD).map(Algorithm::from),
|
||||
algorithm: hm.remove(ALGORITHM_FIELD).map(|s| Algorithm::from(s)),
|
||||
created: parse_time(&mut hm, CREATED_FIELD)?,
|
||||
expires: parse_time(&mut hm, EXPIRES_FIELD)?,
|
||||
parsed_at: Utc::now(),
|
||||
|
@ -188,32 +230,82 @@ impl From<&str> for Algorithm {
|
|||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ValidateError::Expired => write!(f, "Http Signature is expired"),
|
||||
ValidateError::Decode => write!(f, "Http Signature could not be decoded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseSignatureError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "Error when parsing {} from Http Signature", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ValidateError {
|
||||
fn description(&self) -> &'static str {
|
||||
match *self {
|
||||
ValidateError::Expired => "Http Signature is expired",
|
||||
ValidateError::Decode => "Http Signature could not be decoded",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ParseSignatureError {
|
||||
fn description(&self) -> &'static str {
|
||||
"There was an error parsing the Http Signature"
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidateError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ValidateError::Missing => write!(f, "Http Signature is missing"),
|
||||
ValidateError::Expired => write!(f, "Http Signature is expired"),
|
||||
ValidateError::Decode => write!(f, "Http Signature could not be decoded"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for ValidateError {
|
||||
fn description(&self) -> &'static str {
|
||||
match *self {
|
||||
ValidateError::Missing => "Http Signature is missing",
|
||||
ValidateError::Expired => "Http Signature is expired",
|
||||
ValidateError::Decode => "Http Signature could not be decoded",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::Utc;
|
||||
|
||||
use super::ParsedHeader;
|
||||
|
||||
#[test]
|
||||
fn parses_header_succesfully_1() {
|
||||
let time1 = Utc::now().timestamp();
|
||||
let time2 = Utc::now().timestamp();
|
||||
|
||||
let h = format!(r#"Signature keyId="my-key-id",algorithm="hs2019",created="{}",expires="{}",headers="(request-target) (created) (expires) date content-type",signature="blah blah blah""#, time1, time2);
|
||||
|
||||
parse_signature(&h)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_header_succesfully_2() {
|
||||
let time1 = Utc::now().timestamp();
|
||||
let time2 = Utc::now().timestamp();
|
||||
|
||||
let h = format!(r#"Signature keyId="my-key-id",algorithm="rsa-sha256",created="{}",expires="{}",signature="blah blah blah""#, time1, time2);
|
||||
|
||||
parse_signature(&h)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_header_succesfully_3() {
|
||||
let time1 = Utc::now().timestamp();
|
||||
|
||||
let h = format!(r#"Signature keyId="my-key-id",algorithm="rsa-sha256",created="{}",headers="(request-target) (created) date content-type",signature="blah blah blah""#, time1);
|
||||
|
||||
parse_signature(&h)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_header_succesfully_4() {
|
||||
let h = r#"Signature keyId="my-key-id",algorithm="rsa-sha256",headers="(request-target) date content-type",signature="blah blah blah""#;
|
||||
|
||||
parse_signature(h)
|
||||
}
|
||||
|
||||
fn parse_signature(s: &str) {
|
||||
let ph: ParsedHeader = s.parse().unwrap();
|
||||
println!("{:?}", ph);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue