use std::convert::TryInto;
use std::sync::Arc;
use std::time::SystemTime;
use chrono::Utc;
use http::header::{HeaderName, HeaderValue, AUTHORIZATION, DATE, HOST};
use itertools::Itertools;
use thiserror::Error;
use sha2::Digest;
use crate::algorithm::{HttpDigest, HttpSignatureSign};
use crate::canonicalize::{CanonicalizeConfig, CanonicalizeError, CanonicalizeExt, RequestLike};
use crate::header::{Header, PseudoHeader};
use crate::{DefaultDigestAlgorithm, DefaultSignatureAlgorithm, DATE_FORMAT};
pub trait ClientRequestLike: RequestLike {
fn host(&self) -> Option<String> {
None
}
fn set_header(&mut self, header: HeaderName, value: HeaderValue);
fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option<String>;
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum SigningError {
#[error("Failed to canonicalize request")]
Canonicalize(#[source] CanonicalizeError),
#[error("Signature creation date was in the future")]
InvalidSignatureCreationDate,
#[error("Signature expires date was in the past")]
InvalidSignatureExpiresDate,
}
impl From<CanonicalizeError> for SigningError {
fn from(other: CanonicalizeError) -> Self {
Self::Canonicalize(other)
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum SignatureCreated {
Omit,
Automatic,
Absolute(i64),
}
impl SignatureCreated {
fn get(self, ts: i64) -> Option<i64> {
match self {
Self::Omit => None,
Self::Automatic => Some(ts),
Self::Absolute(ts) => Some(ts),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum SignatureExpires {
Omit,
Relative(i64),
Absolute(i64),
}
impl SignatureExpires {
fn get(self, ts: i64) -> Option<i64> {
match self {
Self::Omit => None,
Self::Relative(offset) => Some(ts.saturating_add(offset)),
Self::Absolute(ts) => Some(ts),
}
}
}
#[derive(Debug, Clone)]
pub struct SigningConfig {
signature: Arc<dyn HttpSignatureSign>,
digest: Arc<dyn HttpDigest>,
key_id: String,
headers: Vec<Header>,
compute_digest: bool,
add_date: bool,
add_host: bool,
skip_missing: bool,
signature_created: SignatureCreated,
signature_expires: SignatureExpires,
}
impl SigningConfig {
pub fn new_default(key_id: &str, key: &[u8]) -> Self {
Self::new(key_id, DefaultSignatureAlgorithm::new(key))
}
pub fn new<SigAlg: HttpSignatureSign>(key_id: &str, signature: SigAlg) -> Self {
SigningConfig {
signature: Arc::new(signature),
digest: Arc::new(DefaultDigestAlgorithm::new()),
key_id: key_id.into(),
headers: [
Header::Pseudo(PseudoHeader::RequestTarget),
Header::Normal(HOST),
Header::Normal(DATE),
Header::Normal(HeaderName::from_static("digest")),
]
.to_vec(),
compute_digest: true,
add_date: true,
add_host: true,
skip_missing: true,
signature_created: SignatureCreated::Omit,
signature_expires: SignatureExpires::Omit,
}
}
pub fn key_id(&self) -> &str {
self.key_id.as_ref()
}
pub fn digest(&self) -> &dyn HttpDigest {
&*self.digest
}
fn set_digest<DigestAlg: HttpDigest>(&mut self, digest: DigestAlg) -> &mut Self {
self.digest = Arc::new(digest);
self
}
pub fn with_digest<DigestAlg: HttpDigest>(mut self, digest: DigestAlg) -> Self {
self.set_digest(digest);
self
}
pub fn compute_digest(&self) -> bool {
self.compute_digest
}
pub fn set_compute_digest(&mut self, compute_digest: bool) -> &mut Self {
self.compute_digest = compute_digest;
self
}
pub fn with_compute_digest(mut self, compute_digest: bool) -> Self {
self.set_compute_digest(compute_digest);
self
}
pub fn add_date(&self) -> bool {
self.add_date
}
pub fn set_add_date(&mut self, add_date: bool) -> &mut Self {
self.add_date = add_date;
self
}
pub fn with_add_date(mut self, add_date: bool) -> Self {
self.set_add_date(add_date);
self
}
pub fn add_host(&self) -> bool {
self.add_host
}
pub fn set_add_host(&mut self, add_host: bool) -> &mut Self {
self.add_host = add_host;
self
}
pub fn with_add_host(mut self, add_host: bool) -> Self {
self.set_add_host(add_host);
self
}
pub fn headers(&self) -> impl IntoIterator<Item = &Header> {
&self.headers
}
pub fn set_headers(&mut self, headers: &[Header]) -> &mut Self {
self.headers = headers.to_vec();
self
}
pub fn with_headers(mut self, headers: &[Header]) -> Self {
self.set_headers(headers);
self
}
pub fn skip_missing(&self) -> bool {
self.skip_missing
}
pub fn set_skip_missing(&mut self, skip_missing: bool) -> &mut Self {
self.skip_missing = skip_missing;
self
}
pub fn with_skip_missing(mut self, skip_missing: bool) -> Self {
self.set_skip_missing(skip_missing);
self
}
pub fn set_signature_created_auto(&mut self) -> &mut Self {
self.signature_created = SignatureCreated::Automatic;
self
}
pub fn with_signature_created_auto(mut self) -> Self {
self.signature_created = SignatureCreated::Automatic;
self
}
pub fn signature_created_auto(&self) -> bool {
self.signature_created == SignatureCreated::Automatic
}
pub fn set_signature_created_at(&mut self, ts: i64) -> &mut Self {
self.signature_created = SignatureCreated::Absolute(ts);
self
}
pub fn with_signature_created_at(mut self, ts: i64) -> Self {
self.signature_created = SignatureCreated::Absolute(ts);
self
}
pub fn signature_created_at(&self) -> Option<i64> {
if let SignatureCreated::Absolute(ts) = self.signature_created {
Some(ts)
} else {
None
}
}
pub fn set_signature_expires_relative(&mut self, offset: i64) -> &mut Self {
self.signature_expires = SignatureExpires::Relative(offset);
self
}
pub fn with_signature_expires_auto(mut self, offset: i64) -> Self {
self.signature_expires = SignatureExpires::Relative(offset);
self
}
pub fn signature_expires_relative(&self) -> Option<i64> {
if let SignatureExpires::Relative(offset) = self.signature_expires {
Some(offset)
} else {
None
}
}
pub fn set_signature_expires_at(&mut self, ts: i64) -> &mut Self {
self.signature_expires = SignatureExpires::Absolute(ts);
self
}
pub fn with_signature_expires_at(mut self, ts: i64) -> Self {
self.signature_expires = SignatureExpires::Absolute(ts);
self
}
pub fn signature_expires_at(&self) -> Option<i64> {
if let SignatureExpires::Absolute(ts) = self.signature_expires {
Some(ts)
} else {
None
}
}
}
pub trait SigningExt: Sized {
fn signed(mut self, config: &SigningConfig) -> Result<Self, SigningError> {
self.sign(config)?;
Ok(self)
}
fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError>;
}
fn add_auto_headers<R: ClientRequestLike>(request: &mut R, config: &SigningConfig) -> Vec<Header> {
let digest_header = HeaderName::from_static("digest");
if config.add_date && !request.has_header(&DATE.into()) {
let date = Utc::now().format(DATE_FORMAT).to_string();
request.set_header(
DATE,
date.try_into()
.expect("Dates should always be valid header values"),
);
}
if config.add_host && !request.has_header(&HOST.into()) {
if let Some(host) = request.host() {
request.set_header(
HOST,
host.try_into()
.expect("Host should be valid in a HTTP header"),
);
}
}
if config.compute_digest && !request.has_header(&digest_header.clone().into()) {
if let Some(digest_str) = request.compute_digest(&*config.digest) {
let digest = format!("{}={}", config.digest.name(), digest_str);
request.set_header(
digest_header,
digest
.try_into()
.expect("Digest should be valid in a HTTP header"),
);
}
}
if config.skip_missing {
config
.headers
.iter()
.filter(|header| request.has_header(header))
.cloned()
.collect()
} else {
config.headers.clone()
}
}
fn unix_timestamp() -> i64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("Unix time to be positive")
.as_secs() as i64
}
impl<R: ClientRequestLike> SigningExt for R {
fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError> {
let headers = add_auto_headers(self, config);
let joined_headers = headers.iter().map(|header| header.as_str()).join(" ");
let ts = unix_timestamp();
let mut canonicalize_config = CanonicalizeConfig::new().with_headers(headers);
if let Some(created) = config.signature_created.get(ts) {
if created > ts {
return Err(SigningError::InvalidSignatureCreationDate);
}
canonicalize_config.set_signature_created(created.into());
}
if let Some(expires) = config.signature_expires.get(ts) {
if expires < ts {
return Err(SigningError::InvalidSignatureExpiresDate);
}
canonicalize_config.set_signature_expires(expires.into());
}
let content = self.canonicalize(&canonicalize_config)?;
let signature = config.signature.http_sign(content.as_bytes());
let auth_header = format!(
r#"Signature keyId="{}",algorithm="{}",signature="{}",headers="{}"#,
config.key_id, "hs2019", signature, joined_headers
);
self.set_header(
AUTHORIZATION,
auth_header
.try_into()
.expect("Signature scheme should generate a valid header"),
);
Ok(())
}
}