449 lines
14 KiB
Rust
449 lines
14 KiB
Rust
//! Types and methods to verify a signature or authorization header
|
|
use chrono::{DateTime, Duration, TimeZone, Utc};
|
|
use std::{
|
|
collections::{BTreeMap, HashMap, HashSet},
|
|
error::Error,
|
|
fmt,
|
|
str::FromStr,
|
|
};
|
|
|
|
use crate::{
|
|
build_signing_string, RequiredError, ALGORITHM_FIELD, CREATED, CREATED_FIELD, EXPIRES_FIELD,
|
|
HEADERS_FIELD, KEY_ID_FIELD, SIGNATURE_FIELD,
|
|
};
|
|
|
|
#[derive(Debug)]
|
|
/// The Unverified step of the verification process
|
|
///
|
|
/// This type is the result of performing some basic validation on the parsed header, and can be
|
|
/// used to verify the header
|
|
pub struct Unverified {
|
|
key_id: String,
|
|
signature: String,
|
|
algorithm: Option<Algorithm>,
|
|
signing_string: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
/// The Unvalidated stage
|
|
///
|
|
/// This is created after generating the signing string from a parsed header, and transitions into
|
|
/// the Unverified type by applying some basic validations
|
|
pub struct Unvalidated {
|
|
pub(crate) key_id: String,
|
|
pub(crate) signature: String,
|
|
pub(crate) algorithm: Option<Algorithm>,
|
|
pub(crate) created: Option<DateTime<Utc>>,
|
|
pub(crate) expires: Option<DateTime<Utc>>,
|
|
pub(crate) parsed_at: DateTime<Utc>,
|
|
pub(crate) date: Option<String>,
|
|
pub(crate) signing_string: String,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
/// The successful result of parsing a Signature or Authorization header
|
|
pub struct ParsedHeader {
|
|
signature: String,
|
|
key_id: String,
|
|
headers: Vec<String>,
|
|
algorithm: Option<Algorithm>,
|
|
created: Option<DateTime<Utc>>,
|
|
expires: Option<DateTime<Utc>>,
|
|
parsed_at: DateTime<Utc>,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
/// Algorithms that may be present in an HTTP Signature's `algorithm` field, but are considered
|
|
/// deprecated due to security issues
|
|
///
|
|
/// Most of these are Deprecated solely because the presence of the algorithm's name in the request
|
|
/// could be used to gain insight into ways to forge requests. This doesn't mean that using these
|
|
/// algorithms to sign and verify requests is bad, it just means that stating which algorithm is in
|
|
/// use is dangerous. In the case of the SHA1 variants, they were deprecated for being weak
|
|
/// hashes.
|
|
///
|
|
/// This library only produces HTTP Signatures with the "HS2019" algorithm type, and leaves
|
|
/// deciding which algorithm to actually use to implementors
|
|
pub enum DeprecatedAlgorithm {
|
|
/// HMAC SHA-1
|
|
HmacSha1,
|
|
/// HMAC SHA-256
|
|
HmacSha256,
|
|
/// HMAC SHA-384
|
|
HmacSha384,
|
|
/// HMAC SHA-512
|
|
HmacSha512,
|
|
/// RSA SHA-1
|
|
RsaSha1,
|
|
/// RSA SHA-256
|
|
RsaSha256,
|
|
/// RSA SHA-384
|
|
RsaSha384,
|
|
/// RSA SHA-512
|
|
RsaSha512,
|
|
/// ECDSA SHA-1
|
|
EcdsaSha1,
|
|
/// ECDSA SHA-256
|
|
EcdsaSha256,
|
|
/// ECDSA SHA-384
|
|
EcdsaSha384,
|
|
/// ECDSA SHA-512
|
|
EcdsaSha512,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
/// Kinds of algorithms
|
|
///
|
|
/// This library knows about HS2019 as a supported algorithm, and any other algorithms are either
|
|
/// unknown at the time of writing, or deprecated
|
|
pub enum Algorithm {
|
|
/// The only officially supported algorithm from the current HTTP Signatures specification
|
|
Hs2019,
|
|
/// Algorithms that have been used historically, but are deprecated
|
|
Deprecated(DeprecatedAlgorithm),
|
|
/// Algorithms that may be used by custom implementations and are unknown to the spec
|
|
Unknown(String),
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
/// Kinds of errors for validating a request
|
|
pub enum ValidateError {
|
|
/// The Authorization or Signature header is not present
|
|
Missing,
|
|
/// The request's `created` or `expires` field indicates it is too old to be valid
|
|
Expired,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
/// The error produced when parsing the HTTPT Signature fails, including the name of the field that
|
|
/// was invalid.
|
|
pub struct ParseSignatureError(&'static str);
|
|
|
|
impl ParseSignatureError {
|
|
/// Get the name of the missing field
|
|
pub fn missing_field(&self) -> &'static str {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl Unverified {
|
|
/// Get the Key ID from an Unverified type
|
|
///
|
|
/// This is useful for looking up the proper verification key to verify the request
|
|
pub fn key_id(&self) -> &str {
|
|
&self.key_id
|
|
}
|
|
|
|
/// Get the Algorithm used in the request, if one is present
|
|
///
|
|
/// If the algorithm is present and is not what an implementor expected, they should not
|
|
/// attempt to verify the signature
|
|
pub fn algorithm(&self) -> Option<&Algorithm> {
|
|
self.algorithm.as_ref()
|
|
}
|
|
|
|
/// Get the signing string used to create the signature
|
|
pub fn signing_string(&self) -> &str {
|
|
&self.signing_string
|
|
}
|
|
|
|
/// Get the signature itself
|
|
pub fn signature(&self) -> &str {
|
|
&self.signature
|
|
}
|
|
|
|
/// Verify the signature with the signature and the signing string
|
|
///
|
|
/// ```rust,ignore
|
|
/// unverified.verify(|signature, signing_string| {
|
|
/// let bytes = match base64::decode(signature) {
|
|
/// Ok(bytes) => bytes,
|
|
/// Err(_) => return false,
|
|
/// };
|
|
///
|
|
/// public_key
|
|
/// .verify(bytes, signing_string)
|
|
/// .unwrap_or(false)
|
|
/// })
|
|
/// ```
|
|
pub fn verify<F, T>(&self, f: F) -> T
|
|
where
|
|
F: FnOnce(&str, &str) -> T,
|
|
{
|
|
(f)(&self.signature, &self.signing_string)
|
|
}
|
|
}
|
|
|
|
impl Unvalidated {
|
|
/// Validate parts of the header, ensuring that the provided dates don't indicate that it is
|
|
/// expired.
|
|
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);
|
|
}
|
|
}
|
|
|
|
if let Some(date) = self.date {
|
|
if let Ok(datetime) = DateTime::parse_from_rfc2822(&date) {
|
|
let datetime: DateTime<Utc> = datetime.into();
|
|
if datetime + expires_after < self.parsed_at {
|
|
return Err(ValidateError::Expired);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(Unverified {
|
|
key_id: self.key_id,
|
|
algorithm: self.algorithm,
|
|
signing_string: self.signing_string,
|
|
signature: self.signature,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ParsedHeader {
|
|
/// Generate a Signing String from the header
|
|
pub fn into_unvalidated(
|
|
self,
|
|
method: &str,
|
|
path_and_query: &str,
|
|
headers: &mut BTreeMap<String, String>,
|
|
required_headers: HashSet<String>,
|
|
) -> Result<Unvalidated, RequiredError> {
|
|
let date = headers.get("date").cloned();
|
|
|
|
let signing_string = build_signing_string(
|
|
method,
|
|
path_and_query,
|
|
self.created,
|
|
self.expires,
|
|
&self.headers,
|
|
headers,
|
|
required_headers,
|
|
)?;
|
|
|
|
Ok(Unvalidated {
|
|
key_id: self.key_id,
|
|
signature: self.signature,
|
|
parsed_at: self.parsed_at,
|
|
algorithm: self.algorithm,
|
|
created: self.created,
|
|
expires: self.expires,
|
|
date,
|
|
signing_string,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl FromStr for ParsedHeader {
|
|
type Err = ParseSignatureError;
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
let s = s.trim_start_matches("Signature").trim();
|
|
let mut hm: HashMap<String, String> = s
|
|
.split(',')
|
|
.filter_map(|part| {
|
|
let mut i = part.splitn(2, '=');
|
|
|
|
if let Some(key) = i.next() {
|
|
if let Some(value) = i.next() {
|
|
return Some((key.to_owned(), value.trim_matches('"').to_owned()));
|
|
}
|
|
}
|
|
None
|
|
})
|
|
.collect();
|
|
|
|
Ok(ParsedHeader {
|
|
signature: hm
|
|
.remove(SIGNATURE_FIELD)
|
|
.ok_or(ParseSignatureError(SIGNATURE_FIELD))?,
|
|
key_id: hm
|
|
.remove(KEY_ID_FIELD)
|
|
.ok_or(ParseSignatureError(KEY_ID_FIELD))?,
|
|
headers: hm
|
|
.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),
|
|
created: parse_time(&mut hm, CREATED_FIELD)?,
|
|
expires: parse_time(&mut hm, EXPIRES_FIELD)?,
|
|
parsed_at: Utc::now(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse_time(
|
|
hm: &mut HashMap<String, String>,
|
|
key: &'static str,
|
|
) -> Result<Option<DateTime<Utc>>, ParseSignatureError> {
|
|
let r = hm.remove(key).map(|s| {
|
|
s.parse()
|
|
.map(|timestamp| Utc.timestamp(timestamp, 0))
|
|
.map_err(|_| ParseSignatureError(key))
|
|
});
|
|
|
|
match r {
|
|
Some(Ok(t)) => Ok(Some(t)),
|
|
Some(Err(e)) => Err(e),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
impl From<DeprecatedAlgorithm> for Algorithm {
|
|
fn from(d: DeprecatedAlgorithm) -> Algorithm {
|
|
Algorithm::Deprecated(d)
|
|
}
|
|
}
|
|
|
|
impl From<String> for Algorithm {
|
|
fn from(s: String) -> Self {
|
|
Algorithm::from(s.as_str())
|
|
}
|
|
}
|
|
|
|
impl From<&str> for Algorithm {
|
|
fn from(s: &str) -> Self {
|
|
match s {
|
|
"hmac-sha1" => DeprecatedAlgorithm::HmacSha1.into(),
|
|
"hmac-sha256" => DeprecatedAlgorithm::HmacSha256.into(),
|
|
"hmac-sha384" => DeprecatedAlgorithm::HmacSha384.into(),
|
|
"hmac-sha512" => DeprecatedAlgorithm::HmacSha512.into(),
|
|
"rsa-sha1" => DeprecatedAlgorithm::RsaSha1.into(),
|
|
"rsa-sha256" => DeprecatedAlgorithm::RsaSha256.into(),
|
|
"rsa-sha384" => DeprecatedAlgorithm::RsaSha384.into(),
|
|
"rsa-sha512" => DeprecatedAlgorithm::RsaSha512.into(),
|
|
"ecdsa-sha1" => DeprecatedAlgorithm::EcdsaSha1.into(),
|
|
"ecdsa-sha256" => DeprecatedAlgorithm::EcdsaSha256.into(),
|
|
"ecdsa-sha384" => DeprecatedAlgorithm::EcdsaSha384.into(),
|
|
"ecdsa-sha512" => DeprecatedAlgorithm::EcdsaSha512.into(),
|
|
"hs2019" => Algorithm::Hs2019,
|
|
other => Algorithm::Unknown(other.into()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for DeprecatedAlgorithm {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
let s = match self {
|
|
DeprecatedAlgorithm::HmacSha1 => "hmac-sha1",
|
|
DeprecatedAlgorithm::HmacSha256 => "hmac-sha256",
|
|
DeprecatedAlgorithm::HmacSha384 => "hmac-sha384",
|
|
DeprecatedAlgorithm::HmacSha512 => "hmac-sha512",
|
|
DeprecatedAlgorithm::RsaSha1 => "rsa-sha1",
|
|
DeprecatedAlgorithm::RsaSha256 => "rsa-sha256",
|
|
DeprecatedAlgorithm::RsaSha384 => "rsa-sha384",
|
|
DeprecatedAlgorithm::RsaSha512 => "rsa-sha512",
|
|
DeprecatedAlgorithm::EcdsaSha1 => "ecdsa-sha1",
|
|
DeprecatedAlgorithm::EcdsaSha256 => "ecdsa-sha256",
|
|
DeprecatedAlgorithm::EcdsaSha384 => "ecdsa-sha384",
|
|
DeprecatedAlgorithm::EcdsaSha512 => "ecdsa-sha512",
|
|
};
|
|
|
|
write!(f, "{}", s)
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for Algorithm {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
match self {
|
|
Algorithm::Hs2019 => write!(f, "hs2019"),
|
|
Algorithm::Deprecated(d) => d.fmt(f),
|
|
Algorithm::Unknown(other) => write!(f, "{}", other),
|
|
}
|
|
}
|
|
}
|
|
|
|
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 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"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Error for ValidateError {
|
|
fn description(&self) -> &'static str {
|
|
match *self {
|
|
ValidateError::Missing => "Http Signature is missing",
|
|
ValidateError::Expired => "Http Signature is expired",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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);
|
|
}
|
|
}
|