#![warn(missing_docs)]
#[derive(Debug, thiserror::Error)]
pub enum ParseError {
#[error("Parameter pair did not contain =")]
MissingEquals,
#[error("No signature found in parameters")]
MissingSignature,
#[error("Parameter contained invalid characters")]
InvalidCharacters,
#[error("Failed to parse number")]
Number(std::num::ParseIntError),
#[error("Failed to parse signature bytes")]
Base64(base64::DecodeError),
}
#[derive(Debug, thiserror::Error)]
pub enum SignError<T: std::fmt::Debug> {
#[error("IO error occurred")]
IO(#[from] std::io::Error),
#[error("Failed in user sign call")]
User(T),
}
#[derive(Debug, thiserror::Error)]
pub enum VerifyError<T: std::fmt::Debug> {
#[error("IO error occurred")]
IO(#[from] std::io::Error),
#[error("Failed in user verify call")]
User(T),
}
enum SignatureHeaderName {
RequestTarget,
Created,
Expires,
NormalHeader(http::header::HeaderName),
}
impl SignatureHeaderName {
pub fn as_str(&self) -> &str {
match self {
SignatureHeaderName::RequestTarget => "(request-target)",
SignatureHeaderName::Created => "(created)",
SignatureHeaderName::Expires => "(expires)",
SignatureHeaderName::NormalHeader(header) => header.as_str(),
}
}
}
impl From<http::header::HeaderName> for SignatureHeaderName {
fn from(src: http::header::HeaderName) -> Self {
SignatureHeaderName::NormalHeader(src)
}
}
impl std::str::FromStr for SignatureHeaderName {
type Err = http::header::InvalidHeaderName;
fn from_str(src: &str) -> Result<Self, http::header::InvalidHeaderName> {
if src == "(request-target)" {
Ok(SignatureHeaderName::RequestTarget)
} else if src == "(created)" {
Ok(SignatureHeaderName::Created)
} else if src == "(expires)" {
Ok(SignatureHeaderName::Expires)
} else {
Ok(SignatureHeaderName::NormalHeader(src.parse()?))
}
}
}
fn parse_maybe_quoted<'a>(src: &'a str) -> &'a str {
if src.starts_with('"') && src.ends_with('"') {
&src[1..(src.len() - 1)]
} else {
src
}
}
pub struct Signature<'a> {
algorithm: Option<http::header::HeaderName>,
created: Option<u64>,
expires: Option<u64>,
headers: Option<Vec<SignatureHeaderName>>,
key_id: Option<&'a str>,
signature: Vec<u8>,
}
impl<'a> Signature<'a> {
pub fn create<E: std::fmt::Debug>(
key_id: &'a str,
request_method: &http::method::Method,
request_path_and_query: &str,
lifetime_secs: u64,
headers: &http::header::HeaderMap,
sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
) -> Result<Self, SignError<E>> {
use std::io::Write;
let created = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("Timestamp is wildly unrealistic (before epoch)")
.as_secs();
let expires = created + lifetime_secs;
let mut body = Vec::new();
write!(
body,
"(request-target): {} {}\n(created): {}\n(expires): {}",
request_method.as_str().to_lowercase(),
request_path_and_query,
created,
expires,
)?;
for name in headers.keys() {
write!(body, "\n{}: ", name)?;
let mut first = true;
for value in headers.get_all(name) {
if first {
first = false;
} else {
write!(body, ", ")?;
}
body.extend(value.as_bytes());
}
}
let header_names: Vec<_> = vec![
SignatureHeaderName::RequestTarget,
SignatureHeaderName::Created,
SignatureHeaderName::Expires,
]
.into_iter()
.chain(headers.keys().cloned().map(Into::into))
.collect();
let signature = sign(body).map_err(SignError::User)?;
Ok(Self {
algorithm: Some(http::header::HeaderName::from_static("hs2019")),
created: Some(created),
expires: Some(expires),
headers: Some(header_names),
key_id: Some(key_id),
signature,
})
}
pub fn create_legacy<E: std::fmt::Debug>(
key_id: &'a str,
request_method: &http::method::Method,
request_path_and_query: &str,
headers: &http::header::HeaderMap,
sign: impl FnOnce(Vec<u8>) -> Result<Vec<u8>, E>,
) -> Result<Self, SignError<E>> {
use std::io::Write;
if !headers.contains_key(http::header::DATE) {
panic!("legacy signatures must contain Date header");
}
let mut body = Vec::new();
write!(
body,
"(request-target): {} {}",
request_method.as_str().to_lowercase(),
request_path_and_query,
)?;
for name in headers.keys() {
write!(body, "\n{}: ", name)?;
let mut first = true;
for value in headers.get_all(name) {
if first {
first = false;
} else {
write!(body, ", ")?;
}
body.extend(value.as_bytes());
}
}
let header_names: Vec<_> = std::iter::once(SignatureHeaderName::RequestTarget)
.chain(headers.keys().cloned().map(Into::into))
.collect();
let signature = sign(body).map_err(SignError::User)?;
Ok(Self {
algorithm: Some(http::header::HeaderName::from_static("hs2019")),
created: None,
expires: None,
headers: Some(header_names),
key_id: Some(key_id),
signature,
})
}
pub fn parse(value: &'a http::header::HeaderValue) -> Result<Self, ParseError> {
let mut algorithm = None;
let mut created = None;
let mut expires = None;
let mut headers = None;
let mut key_id = None;
let mut signature = None;
for field_src in value
.to_str()
.map_err(|_| ParseError::InvalidCharacters)?
.split(',')
{
let eqidx = field_src.find('=').ok_or(ParseError::MissingEquals)?;
let key = &field_src[..eqidx];
let value = parse_maybe_quoted(&field_src[(eqidx + 1)..]);
match key {
"algorithm" => {
algorithm = Some(value.parse().map_err(|_| ParseError::InvalidCharacters)?);
}
"created" => {
created = Some(value.parse().map_err(ParseError::Number)?);
}
"expires" => {
expires = Some(value.parse().map_err(ParseError::Number)?);
}
"headers" => {
headers = Some(
value
.split(' ')
.map(|x| x.parse().map_err(|_| ParseError::InvalidCharacters))
.collect::<Result<Vec<_>, _>>()?,
);
}
"key_id" => {
key_id = Some(value);
}
"signature" => {
signature = Some(base64::decode(value).map_err(ParseError::Base64)?);
}
_ => {}
}
}
Ok(Self {
algorithm,
created,
expires,
headers,
key_id,
signature: signature.ok_or(ParseError::MissingSignature)?,
})
}
pub fn to_header(&self) -> http::header::HeaderValue {
use std::fmt::Write;
let mut params = String::new();
write!(params, "headers=\"").unwrap();
if let Some(ref headers) = self.headers {
for (idx, name) in headers.iter().enumerate() {
if idx != 0 {
write!(params, " ").unwrap();
}
write!(params, "{}", name.as_str()).unwrap();
}
} else {
write!(params, "(created)").unwrap();
}
write!(params, "\"").unwrap();
if let Some(ref algorithm) = self.algorithm {
write!(params, ",algorithm={}", algorithm).unwrap();
}
if let Some(created) = self.created {
write!(params, ",created={}", created).unwrap();
}
if let Some(expires) = self.expires {
write!(params, ",expires={}", expires).unwrap();
}
if let Some(key_id) = self.key_id {
write!(params, ",keyId=\"{}\"", key_id).unwrap();
}
write!(params, ",signature=\"").unwrap();
base64::encode_config_buf(&self.signature, base64::STANDARD, &mut params);
write!(params, "\"").unwrap();
http::header::HeaderValue::from_bytes(params.as_bytes()).unwrap()
}
pub fn verify<E: std::fmt::Debug>(
&self,
request_method: &http::method::Method,
request_path_and_query: &str,
headers: &http::header::HeaderMap,
verify: impl FnOnce(&[u8], &[u8]) -> Result<bool, E>,
) -> Result<bool, VerifyError<E>> {
use std::io::Write;
let now = std::time::SystemTime::now()
.duration_since(std::time::SystemTime::UNIX_EPOCH)
.expect("Timestamp is wildly inaccurate")
.as_secs();
if let Some(expires) = self.expires {
if expires < now {
return Ok(false);
}
}
let mut body = Vec::new();
if let Some(header_names) = &self.headers {
for (idx, name) in header_names.iter().enumerate() {
if idx != 0 {
write!(body, "\n")?;
}
match name {
SignatureHeaderName::RequestTarget => {
write!(
body,
"(request-target): {} {}",
request_method.as_str().to_lowercase(),
request_path_and_query
)?;
}
SignatureHeaderName::Created => {
if let Some(created) = self.created {
write!(body, "(created): {}", created)?;
} else {
return Ok(false);
}
}
SignatureHeaderName::Expires => {
if let Some(expires) = self.expires {
write!(body, "(expires): {}", expires)?;
} else {
return Ok(false);
}
}
SignatureHeaderName::NormalHeader(name) => {
write!(body, "{}: ", name)?;
let mut first = true;
for value in headers.get_all(name) {
if first {
first = false;
} else {
write!(body, ", ")?;
}
body.extend(value.as_bytes());
}
}
}
}
} else {
if let Some(created) = self.created {
write!(body, "(created): {}", created)?;
} else {
return Ok(false);
}
}
verify(&body, &self.signature).map_err(VerifyError::User)
}
}