From bba8986699661fc7161e790350de3557865e70f4 Mon Sep 17 00:00:00 2001 From: Diggory Blake Date: Thu, 16 Apr 2020 17:36:59 +0100 Subject: [PATCH] Split out canonicalization into a separate module so it can be shared between signing and verification --- Cargo.toml | 2 + http-sig-validator/src/main.rs | 91 ++++++++--- src/algorithm.rs | 21 +-- src/algorithm/openssl.rs | 13 +- src/canonicalize.rs | 169 +++++++++++++++++++ src/lib.rs | 3 + src/mock_request.rs | 32 +++- src/reqwest_impls.rs | 24 ++- src/rouille_impls.rs | 8 +- src/signing.rs | 290 ++++++++++++++++++++++++++------- src/verifying.rs | 139 +++++++--------- 11 files changed, 582 insertions(+), 210 deletions(-) create mode 100644 src/canonicalize.rs diff --git a/Cargo.toml b/Cargo.toml index 07bb04d..1d73ae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,5 @@ hmac = "0.7.1" openssl = { version = "0.10.29", optional = true } log = "0.4.8" anyhow = "1.0.28" +itertools = "0.9.0" +thiserror = "1.0.15" diff --git a/http-sig-validator/src/main.rs b/http-sig-validator/src/main.rs index 7966d4f..6de7209 100644 --- a/http-sig-validator/src/main.rs +++ b/http-sig-validator/src/main.rs @@ -1,14 +1,14 @@ use std::error::Error; use std::fs; -use std::io; +use std::io::{self, Write}; use std::path::PathBuf; use std::sync::Arc; -use anyhow::Context; +use anyhow::{anyhow, Context}; use http_sig::mock_request::MockRequest; use http_sig::{ - Header, RsaSha256Sign, RsaSha256Verify, SigningConfig, SigningExt, SimpleKeyProvider, - VerifyingConfig, VerifyingExt, + CanonicalizeConfig, CanonicalizeExt, Header, RsaSha256Sign, RsaSha256Verify, SigningConfig, + SigningExt, SimpleKeyProvider, VerifyingConfig, VerifyingExt, }; use structopt::StructOpt; @@ -27,8 +27,9 @@ struct Opt { mode: Mode, /// A list of header names, optionally quoted - #[structopt(short = "d", long, global = true)] - headers: Option, + #[allow(clippy::option_option)] + #[structopt(short = "d", long, global = true, min_values = 0)] + headers: Option>, /// A Key Id string. #[structopt(short, long = "keyId", global = true)] @@ -60,6 +61,35 @@ struct Opt { } impl Opt { + fn parse_headers(&self) -> Result>, Box> { + Ok(if let Some(headers) = &self.headers { + Some(if let Some(headers) = headers { + let headers: Vec
= headers + .split_ascii_whitespace() + .map(|s| s.parse::
().with_context(|| format!("{:?}", s))) + .collect::>()?; + headers + } else { + Vec::new() + }) + } else { + None + }) + } + fn canonicalize_config(&self) -> Result> { + let mut config = CanonicalizeConfig::default(); + if let Some(created) = self.created { + config.set_signature_created(created.into()); + } + if let Some(expires) = self.expires { + config.set_signature_expires(expires.into()); + } + if let Some(headers) = self.parse_headers()? { + config.set_headers(headers); + } + + Ok(config) + } fn signing_config(&self) -> Result> { let key_id = self.key_id.clone().unwrap_or_default(); let key_data = if let Some(key) = self.private_key.as_ref() { @@ -68,26 +98,37 @@ impl Opt { None }; - #[allow(clippy::single_match)] - let mut config = match (self.algorithm.as_deref(), key_data) { - (Some("rsa-sha256"), Some(pkey)) => { + match self.algorithm.as_deref() { + Some("rsa-sha256") | Some("hs2019") | None => {} + Some(other) => return Err(anyhow!("Unknown algorithm: {}", other).into()), + } + + let mut config = match (self.key_type.as_deref(), key_data) { + (Some("rsa"), Some(pkey)) | (Some("RSA"), Some(pkey)) => { SigningConfig::new(&key_id, RsaSha256Sign::new_pem(&pkey)?) } - _ => SigningConfig::new_default(&key_id, b""), + (Some(_), None) => return Err(anyhow!("No key provided").into()), + (Some(other), Some(_)) => return Err(anyhow!("Unknown key type: {}", other).into()), + (None, _) => SigningConfig::new_default(&key_id, b""), }; - if let Some(headers) = &self.headers { - let headers: Vec
= headers - .split_ascii_whitespace() - .map(|s| s.parse::
().with_context(|| format!("{:?}", s))) - .collect::>()?; + if let Some(headers) = self.parse_headers()? { config.set_headers(&headers); } + if let Some(created) = self.created { + config.set_signature_created_at(created); + } + + if let Some(expires) = self.expires { + config.set_signature_expires_at(expires); + } + // Disable various convenience options that would mess up the test suite config.set_add_date(false); config.set_compute_digest(false); config.set_add_host(false); + config.set_skip_missing(false); Ok(config) } @@ -101,12 +142,18 @@ impl Opt { let mut key_provider = SimpleKeyProvider::default(); - #[allow(clippy::single_match)] - match (self.algorithm.as_deref(), key_data) { - (Some("rsa-sha256"), Some(pkey)) => { + match self.algorithm.as_deref() { + Some("hs2019") | None => {} + Some(other) => return Err(anyhow!("Unknown algorithm: {}", other).into()), + } + + match (self.key_type.as_deref(), key_data) { + (Some("rsa"), Some(pkey)) | (Some("RSA"), Some(pkey)) => { key_provider.add(&key_id, Arc::new(RsaSha256Verify::new_pem(&pkey)?)); } - _ => {} + (Some(_), None) => return Err(anyhow!("No key provided").into()), + (Some(other), Some(_)) => return Err(anyhow!("Unknown key type: {}", other).into()), + (None, _) => {} } let mut config = VerifyingConfig::new(key_provider); @@ -127,10 +174,12 @@ fn main() -> Result<(), Box> { let mut req = MockRequest::from_reader(&mut io::stdin().lock())?; + log::info!("{:?}", req); + match opt.mode { Mode::Canonicalize => { - let res = req.canonicalize(&opt.signing_config()?)?; - print!("{}", res); + let res = req.canonicalize(&opt.canonicalize_config()?)?; + io::stdout().lock().write_all(res.as_bytes())?; } Mode::Sign => { req.sign(&opt.signing_config()?)?; diff --git a/src/algorithm.rs b/src/algorithm.rs index 9f5eb32..61da2ea 100644 --- a/src/algorithm.rs +++ b/src/algorithm.rs @@ -3,21 +3,9 @@ use std::fmt::Debug; use hmac::{Hmac, Mac}; use sha2::{Digest, Sha256, Sha512}; -/// Implementations of this trait correspond to signature algorithms -/// listed here: -/// https://tools.ietf.org/id/draft-cavage-http-signatures-12.html#hsa-registry -/// -/// If the HTTP signatures draft is accepted, these will be moved to a registry -/// managed by the IANA. -pub trait HttpSignature: Debug + Send + Sync + 'static { - /// Must return the name exactly as specified in the above list of HTTP - /// signature algorithms. - fn name(&self) -> &str; -} - /// Implements the signing half of an HTTP signature algorithm. For symmetric /// algorithms the same type implements both signing and verification. -pub trait HttpSignatureSign: HttpSignature { +pub trait HttpSignatureSign: Debug + Send + Sync + 'static { /// Returns the encoded signature, ready for inclusion in the HTTP Authorization /// header. For all currently supported signature schemes, the encoding is /// specified to be base64. @@ -26,7 +14,7 @@ pub trait HttpSignatureSign: HttpSignature { /// Implements the verification half of an HTTP signature algorithm. For symmetric /// algorithms the same type implements both signing and verification. -pub trait HttpSignatureVerify: HttpSignature { +pub trait HttpSignatureVerify: Debug + Send + Sync + 'static { /// Returns true if the signature is valid for the provided content. The /// implementation should be sure to perform any comparisons in constant /// time. @@ -63,11 +51,6 @@ macro_rules! hmac_signature { } } - impl HttpSignature for $typename { - fn name(&self) -> &str { - $name - } - } impl HttpSignatureSign for $typename { fn http_sign(&self, bytes_to_sign: &[u8]) -> String { let mut hmac = self.0.clone(); diff --git a/src/algorithm/openssl.rs b/src/algorithm/openssl.rs index f928a47..4bf1993 100644 --- a/src/algorithm/openssl.rs +++ b/src/algorithm/openssl.rs @@ -6,7 +6,7 @@ use openssl::rsa::Padding; use openssl::rsa::Rsa; use openssl::sign::{Signer, Verifier}; -use crate::{HttpSignature, HttpSignatureSign, HttpSignatureVerify}; +use crate::{HttpSignatureSign, HttpSignatureVerify}; macro_rules! rsa_signature { ({$sign_name:ident, $verify_name:ident}($hash_alg:ident) = $name:literal) => { @@ -63,16 +63,6 @@ macro_rules! rsa_signature { } } - impl HttpSignature for $sign_name { - fn name(&self) -> &str { - $name - } - } - impl HttpSignature for $verify_name { - fn name(&self) -> &str { - $name - } - } impl HttpSignatureSign for $sign_name { fn http_sign(&self, bytes_to_sign: &[u8]) -> String { let mut signer = Signer::new(MessageDigest::$hash_alg(), &self.0).unwrap(); @@ -89,7 +79,6 @@ macro_rules! rsa_signature { }; let mut verifier = Verifier::new(MessageDigest::$hash_alg(), &self.0).unwrap(); verifier.set_rsa_padding(Padding::PKCS1).unwrap(); - dbg!(std::str::from_utf8(bytes_to_verify).unwrap(), signature); match verifier.verify_oneshot(&tag, bytes_to_verify) { Ok(true) => true, Ok(false) => false, diff --git a/src/canonicalize.rs b/src/canonicalize.rs new file mode 100644 index 0000000..b7d814b --- /dev/null +++ b/src/canonicalize.rs @@ -0,0 +1,169 @@ +use http::HeaderValue; +use itertools::{Either, Itertools}; +use thiserror::Error; + +use crate::header::{Header, PseudoHeader}; + +/// The types of error which may occur whilst computing the canonical "signature string" +/// for a request. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum CanonicalizeError { + /// One or more headers required to be part of the signature was not present + /// on the request, and the `skip_missing` configuration option + /// was disabled. + #[error("Missing headers required for signature: {0:?}")] + MissingHeaders(Vec
), +} + +/// Base trait for all request types +pub trait RequestLike { + /// Returns an existing header on the request. This method *must* reflect changes made + /// be the `ClientRequestLike::set_header` method, with the possible exception of the + /// `Authorization` header itself. + fn header(&self, header: &Header) -> Option; + + /// Returns true if this request contains a value for the specified header. If this + /// returns true, following requests to `header()` for the same name must return a + /// value. + fn has_header(&self, header: &Header) -> bool { + self.header(header).is_some() + } +} + +impl RequestLike for &T { + fn header(&self, header: &Header) -> Option { + (**self).header(header) + } +} + +/// Configuration for computing the canonical "signature string" of a request. +#[derive(Default)] +pub struct CanonicalizeConfig { + headers: Option>, + signature_created: Option, + signature_expires: Option, +} + +impl CanonicalizeConfig { + /// Creates a new canonicalization configuration using the default values. + pub fn new() -> Self { + Self::default() + } + /// Set the headers to include in the signature + pub fn with_headers(mut self, headers: Vec
) -> Self { + self.headers = Some(headers); + self + } + /// Set the headers to include in the signature + pub fn set_headers(&mut self, headers: Vec
) -> &mut Self { + self.headers = Some(headers); + self + } + /// Get the headers to include in the signature + pub fn headers(&self) -> Option> { + self.headers.as_ref() + } + /// Set the "signature created" pseudo-header + pub fn with_signature_created(mut self, signature_created: HeaderValue) -> Self { + self.signature_created = Some(signature_created); + self + } + /// Set the "signature created" pseudo-header + pub fn set_signature_created(&mut self, signature_created: HeaderValue) -> &mut Self { + self.signature_created = Some(signature_created); + self + } + /// Get the "signature created" pseudo-header + pub fn signature_created(&self) -> Option<&HeaderValue> { + self.signature_created.as_ref() + } + /// Set the "signature expires" pseudo-header + pub fn with_signature_expires(mut self, signature_expires: HeaderValue) -> Self { + self.signature_expires = Some(signature_expires); + self + } + /// Set the "signature expires" pseudo-header + pub fn set_signature_expires(&mut self, signature_expires: HeaderValue) -> &mut Self { + self.signature_expires = Some(signature_expires); + self + } + /// Get the "signature expires" pseudo-header + pub fn signature_expires(&self) -> Option<&HeaderValue> { + self.signature_expires.as_ref() + } +} + +/// Extension method for computing the canonical "signature string" of a request. +pub trait CanonicalizeExt { + /// Compute the canonical representation of this request + fn canonicalize( + &self, + config: &CanonicalizeConfig, + ) -> Result; +} + +const DEFAULT_HEADERS: &[Header] = &[Header::Pseudo(PseudoHeader::Created)]; + +/// Opaque struct storing a computed signature string. +pub struct SignatureString { + content: Vec, + pub(crate) headers: Vec<(Header, HeaderValue)>, +} + +impl SignatureString { + /// Obtain a view of this signature string as a byte slice + pub fn as_bytes(&self) -> &[u8] { + &self.content + } +} + +impl From for Vec { + fn from(other: SignatureString) -> Self { + other.content + } +} + +impl CanonicalizeExt for T { + fn canonicalize( + &self, + config: &CanonicalizeConfig, + ) -> Result { + // Find value of each header + let (headers, missing_headers): (Vec<_>, Vec<_>) = config + .headers + .as_deref() + .unwrap_or_else(|| DEFAULT_HEADERS) + .iter() + .cloned() + .partition_map(|header| { + if let Some(header_value) = match header { + Header::Pseudo(PseudoHeader::Created) => config.signature_created.clone(), + Header::Pseudo(PseudoHeader::Expires) => config.signature_expires.clone(), + _ => self.header(&header), + } { + Either::Left((header, header_value)) + } else { + Either::Right(header) + } + }); + + // Check for missing headers + if !missing_headers.is_empty() { + return Err(CanonicalizeError::MissingHeaders(missing_headers)); + } + + // Build signature string block + let mut content = Vec::new(); + for (name, value) in &headers { + if !content.is_empty() { + content.push(b'\n'); + } + content.extend(name.as_str().as_bytes()); + content.extend(b": "); + content.extend(value.as_bytes()); + } + + Ok(SignatureString { content, headers }) + } +} diff --git a/src/lib.rs b/src/lib.rs index cbb9be1..095ad70 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,6 +68,9 @@ pub use algorithm::*; mod header; pub use header::*; +mod canonicalize; +pub use canonicalize::*; + mod signing; pub use signing::*; diff --git a/src/mock_request.rs b/src/mock_request.rs index eac8aeb..f3a8706 100644 --- a/src/mock_request.rs +++ b/src/mock_request.rs @@ -8,7 +8,7 @@ use anyhow::Context; use http::{header::HeaderName, HeaderValue, Method}; use url::Url; -use crate::{ClientRequestLike, Header, HttpDigest, PseudoHeader, ServerRequestLike}; +use crate::{ClientRequestLike, Header, HttpDigest, PseudoHeader, RequestLike, ServerRequestLike}; /// Generic error returned when the input to `from_reader` does not look like /// a HTTP request. @@ -32,6 +32,23 @@ pub struct MockRequest { } impl MockRequest { + /// Returns the method used by this mock request + pub fn method(&self) -> Method { + self.method.clone() + } + /// Returns the path used by this mock request + pub fn path(&self) -> &str { + &self.path + } + /// Returns the headers used by this mock request + pub fn headers(&self) -> impl IntoIterator { + &self.headers + } + /// Returns the body used by this mock request + pub fn body(&self) -> Option<&[u8]> { + self.body.as_deref() + } + /// Constructs a new mock request pub fn new(method: Method, url: &str) -> Self { let url: Url = url.parse().unwrap(); @@ -143,10 +160,7 @@ impl MockRequest { } } -impl ClientRequestLike for MockRequest { - fn host(&self) -> Option { - None - } +impl RequestLike for MockRequest { fn header(&self, header: &Header) -> Option { match header { Header::Normal(header_name) => self.headers.get(header_name).cloned(), @@ -157,6 +171,9 @@ impl ClientRequestLike for MockRequest { _ => None, } } +} + +impl ClientRequestLike for MockRequest { fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option { self.body.as_ref().map(|b| digest.http_digest(b)) } @@ -168,9 +185,6 @@ impl ClientRequestLike for MockRequest { impl<'a> ServerRequestLike for &'a MockRequest { type Remnant = (); - fn header(&self, header: &Header) -> Option { - ClientRequestLike::header(*self, header) - } fn complete_with_digest(self, digest: &dyn HttpDigest) -> (Option, Self::Remnant) { if let Some(body) = self.body.as_ref() { let computed_digest = digest.http_digest(body); @@ -234,6 +248,7 @@ mod tests { } /// https://tools.ietf.org/id/draft-cavage-http-signatures-12.html#default-test + /// This test is currently broken in the spec, so it's been adjusted to pass... #[test] fn default_test() { // Expect successful validation @@ -243,6 +258,7 @@ mod tests { Signature \ keyId=\"Test\", \ algorithm=\"rsa-sha256\", \ + headers=\"date\", \ signature=\"SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz\ 6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB\ 6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM=\"\ diff --git a/src/reqwest_impls.rs b/src/reqwest_impls.rs index 898d80d..fa38994 100644 --- a/src/reqwest_impls.rs +++ b/src/reqwest_impls.rs @@ -4,10 +4,7 @@ use http::header::{HeaderName, HeaderValue}; use super::*; -impl ClientRequestLike for reqwest::Request { - fn host(&self) -> Option { - self.url().host_str().map(Into::into) - } +impl RequestLike for reqwest::Request { fn header(&self, header: &Header) -> Option { match header { Header::Normal(header_name) => self.headers().get(header_name).cloned(), @@ -19,6 +16,12 @@ impl ClientRequestLike for reqwest::Request { _ => None, } } +} + +impl ClientRequestLike for reqwest::Request { + fn host(&self) -> Option { + self.url().host_str().map(Into::into) + } fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option { self.body()?.as_bytes().map(|b| digest.http_digest(b)) } @@ -27,10 +30,7 @@ impl ClientRequestLike for reqwest::Request { } } -impl ClientRequestLike for reqwest::blocking::Request { - fn host(&self) -> Option { - self.url().host_str().map(Into::into) - } +impl RequestLike for reqwest::blocking::Request { fn header(&self, header: &Header) -> Option { match header { Header::Normal(header_name) => self.headers().get(header_name).cloned(), @@ -48,6 +48,12 @@ impl ClientRequestLike for reqwest::blocking::Request { _ => None, } } +} + +impl ClientRequestLike for reqwest::blocking::Request { + fn host(&self) -> Option { + self.url().host_str().map(Into::into) + } fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option { let bytes_to_digest = self.body_mut().as_mut()?.buffer().ok()?; Some(digest.http_digest(bytes_to_digest)) @@ -86,7 +92,7 @@ mod tests { let with_sig = without_sig.signed(&config).unwrap(); - assert_eq!(with_sig.headers().get(AUTHORIZATION).unwrap(), "Signature keyId=\"test_key\",algorithm=\"hmac-sha256\",signature=\"F8gZiriO7dtKFiP5eSZ+Oh1h61JIrAR6D5Mdh98DjqA=\",headers=\"(request-target) host date digest"); + assert_eq!(with_sig.headers().get(AUTHORIZATION).unwrap(), "Signature keyId=\"test_key\",algorithm=\"hs2019\",signature=\"F8gZiriO7dtKFiP5eSZ+Oh1h61JIrAR6D5Mdh98DjqA=\",headers=\"(request-target) host date digest"); assert_eq!( with_sig .headers() diff --git a/src/rouille_impls.rs b/src/rouille_impls.rs index 1e211aa..355704e 100644 --- a/src/rouille_impls.rs +++ b/src/rouille_impls.rs @@ -36,9 +36,7 @@ impl<'a> Read for RouilleBody<'a> { } } -impl<'a> ServerRequestLike for &'a rouille::Request { - type Remnant = Option>; - +impl RequestLike for rouille::Request { fn header(&self, header: &Header) -> Option { match header { Header::Normal(header) => rouille::Request::header(self, header.as_str()) @@ -51,6 +49,10 @@ impl<'a> ServerRequestLike for &'a rouille::Request { _ => None, } } +} +impl<'a> ServerRequestLike for &'a rouille::Request { + type Remnant = Option>; + fn complete_with_digest(self, digest: &dyn HttpDigest) -> (Option, Self::Remnant) { if let Some(mut body) = self.data() { let mut result = Vec::new(); diff --git a/src/signing.rs b/src/signing.rs index ca69e0b..4f6145f 100644 --- a/src/signing.rs +++ b/src/signing.rs @@ -1,56 +1,97 @@ use std::convert::TryInto; -use std::error::Error; -use std::fmt; 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}; /// This trait is to be implemented for types representing an outgoing /// HTTP request. The HTTP signing extension methods are available on /// any type implementing this trait. -pub trait ClientRequestLike { +pub trait ClientRequestLike: RequestLike { /// Returns the host for the request (eg. "example.com") in case the Host header has /// not been set explicitly. /// When implementing this trait, do not just read the `Host` header from the request - /// this method will only be called when the `Host` header is not set. - fn host(&self) -> Option; + fn host(&self) -> Option { + None + } /// Add a header to the request. This function may be used to set the `Date` and `Digest` /// headers if not already present depending on the configuration. The `Authorization` /// header will always be set assuming the message was signed successfully. fn set_header(&mut self, header: HeaderName, value: HeaderValue); - /// Returns an existing header on the request. This method *must* reflect changes made - /// be the `set_header` method, with the possible exception of the `Authorization` - /// header itself. - fn header(&self, header: &Header) -> Option; /// Compute the digest using the provided HTTP digest algorithm. If this is not possible, /// then return `None`. This may require buffering the request data into memory. fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option; } /// The types of error which may occur whilst signing. -#[derive(Debug)] +#[derive(Debug, Error)] #[non_exhaustive] pub enum SigningError { - #[doc(hidden)] - Unknown, + /// A header required to be part of the signature was not present + /// on the request, and the `skip_missing` configuration option + /// was disabled. + #[error("Failed to canonicalize request")] + Canonicalize(#[source] CanonicalizeError), + + /// The signature creation date was in the future + #[error("Signature creation date was in the future")] + InvalidSignatureCreationDate, + + /// The signature expires date was in the past + #[error("Signature expires date was in the past")] + InvalidSignatureExpiresDate, } -impl fmt::Display for SigningError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { +impl From 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 { match self { - SigningError::Unknown => f.write_str("Unknown error"), + Self::Omit => None, + Self::Automatic => Some(ts), + Self::Absolute(ts) => Some(ts), } } } -impl Error for SigningError {} +#[derive(Debug, Copy, Clone, PartialEq)] +enum SignatureExpires { + Omit, + Relative(i64), + Absolute(i64), +} + +impl SignatureExpires { + fn get(self, ts: i64) -> Option { + match self { + Self::Omit => None, + Self::Relative(offset) => Some(ts.saturating_add(offset)), + Self::Absolute(ts) => Some(ts), + } + } +} /// The configuration used for signing HTTP requests. #[derive(Debug, Clone)] @@ -62,6 +103,9 @@ pub struct SigningConfig { compute_digest: bool, add_date: bool, add_host: bool, + skip_missing: bool, + signature_created: SignatureCreated, + signature_expires: SignatureExpires, } impl SigningConfig { @@ -88,6 +132,9 @@ impl SigningConfig { compute_digest: true, add_date: true, add_host: true, + skip_missing: true, + signature_created: SignatureCreated::Omit, + signature_expires: SignatureExpires::Omit, } } @@ -201,6 +248,133 @@ impl SigningConfig { self.set_headers(headers); self } + /// Returns whether the missing headers will be skipped + /// when not present, or if signing will fail instead. + /// + /// This is set to `true` by default. + pub fn skip_missing(&self) -> bool { + self.skip_missing + } + /// Controls whether the missing headers will be skipped + /// when not present, or if signing will fail instead. + /// + /// This is set to `true` by default. + pub fn set_skip_missing(&mut self, skip_missing: bool) -> &mut Self { + self.skip_missing = skip_missing; + self + } + /// Controls whether the missing headers will be skipped + /// when not present, or if signing will fail instead. + /// + /// This is set to `true` by default. + pub fn with_skip_missing(mut self, skip_missing: bool) -> Self { + self.set_skip_missing(skip_missing); + self + } + /// Ensures a signature created date will be added + /// automatically with the current time. + /// + /// This is off by default. + pub fn set_signature_created_auto(&mut self) -> &mut Self { + self.signature_created = SignatureCreated::Automatic; + self + } + /// Ensures a signature created date will be added + /// automatically with the current time. + /// + /// This is off by default. + pub fn with_signature_created_auto(mut self) -> Self { + self.signature_created = SignatureCreated::Automatic; + self + } + /// Determines if a signature created date will be added + /// automatically with the current time. + /// + /// This is off by default. + pub fn signature_created_auto(&self) -> bool { + self.signature_created == SignatureCreated::Automatic + } + /// Ensures a signature created date will be added + /// with the specified unix timestamp. + /// + /// This is off by default. + pub fn set_signature_created_at(&mut self, ts: i64) -> &mut Self { + self.signature_created = SignatureCreated::Absolute(ts); + self + } + /// Ensures a signature created date will be added + /// with the specified unix timestamp. + /// + /// This is off by default. + pub fn with_signature_created_at(mut self, ts: i64) -> Self { + self.signature_created = SignatureCreated::Absolute(ts); + self + } + /// Determines if a signature created date will be added + /// with a specific unix timestamp. + /// + /// This is off by default. + pub fn signature_created_at(&self) -> Option { + if let SignatureCreated::Absolute(ts) = self.signature_created { + Some(ts) + } else { + None + } + } + /// Ensures a signature expires date will be added + /// automatically relative to the current time. + /// + /// This is off by default. + pub fn set_signature_expires_relative(&mut self, offset: i64) -> &mut Self { + self.signature_expires = SignatureExpires::Relative(offset); + self + } + /// Ensures a signature expires date will be added + /// automatically relative to the current time. + /// + /// This is off by default. + pub fn with_signature_expires_auto(mut self, offset: i64) -> Self { + self.signature_expires = SignatureExpires::Relative(offset); + self + } + /// Determines if a signature expires date will be added + /// automatically relative to the current time. + /// + /// This is off by default. + pub fn signature_expires_relative(&self) -> Option { + if let SignatureExpires::Relative(offset) = self.signature_expires { + Some(offset) + } else { + None + } + } + /// Ensures a signature expires date will be added + /// with the specified unix timestamp. + /// + /// This is off by default. + pub fn set_signature_expires_at(&mut self, ts: i64) -> &mut Self { + self.signature_expires = SignatureExpires::Absolute(ts); + self + } + /// Ensures a signature expires date will be added + /// with the specified unix timestamp. + /// + /// This is off by default. + pub fn with_signature_expires_at(mut self, ts: i64) -> Self { + self.signature_expires = SignatureExpires::Absolute(ts); + self + } + /// Determines if a signature expires date will be added + /// with a specific unix timestamp. + /// + /// This is off by default. + pub fn signature_expires_at(&self) -> Option { + if let SignatureExpires::Absolute(ts) = self.signature_expires { + Some(ts) + } else { + None + } + } } /// Import this trait to get access to access the `signed` and `sign` methods on all types implementing @@ -212,21 +386,15 @@ pub trait SigningExt: Sized { Ok(self) } - /// Compute the canonical representation of this request - fn canonicalize(&mut self, config: &SigningConfig) -> Result; - /// Signs the request in-place according to the provided configuration. fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError>; } -fn internal_canonicalize( - request: &mut R, - config: &SigningConfig, -) -> Result<(String, String), SigningError> { +fn add_auto_headers(request: &mut R, config: &SigningConfig) -> Vec
{ let digest_header = HeaderName::from_static("digest"); // Add missing date header - if config.add_date && request.header(&DATE.into()).is_none() { + if config.add_date && !request.has_header(&DATE.into()) { let date = Utc::now().format(DATE_FORMAT).to_string(); request.set_header( DATE, @@ -235,7 +403,7 @@ fn internal_canonicalize( ); } // Add missing host header - if config.add_host && request.header(&HOST.into()).is_none() { + if config.add_host && !request.has_header(&HOST.into()) { if let Some(host) = request.host() { request.set_header( HOST, @@ -245,7 +413,7 @@ fn internal_canonicalize( } } // Add missing digest header - if config.compute_digest && request.header(&digest_header.clone().into()).is_none() { + 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( @@ -258,39 +426,50 @@ fn internal_canonicalize( } // Build the content block - let (header_vec, content_vec): (Vec<_>, Vec<_>) = config - .headers - .iter() - .filter_map(|header| { - // Lookup header values, and filter out any headers that are missing - request - .header(&header) - .as_ref() - .and_then(|value| value.to_str().ok()) - .map(|value| (header.as_str(), value.to_owned())) - }) - .map(|(header, value)| { - // Construct the content to be signed - (header, format!("{}: {}", header, value)) - }) - .unzip(); - - let headers = header_vec.join(" "); - let content = content_vec.join("\n"); + if config.skip_missing { + config + .headers + .iter() + .filter(|header| request.has_header(header)) + .cloned() + .collect() + } else { + config.headers.clone() + } +} - Ok((headers, content)) +fn unix_timestamp() -> i64 { + SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .expect("Unix time to be positive") + .as_secs() as i64 } impl SigningExt for R { - /// Compute the canonical representation of this request - fn canonicalize(&mut self, config: &SigningConfig) -> Result { - let (_, content) = internal_canonicalize(self, config)?; - Ok(content) - } - fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError> { - // Canonicalize the request - let (headers, content) = internal_canonicalize(self, config)?; + // Add missing headers + let headers = add_auto_headers(self, config); + + let joined_headers = headers.iter().map(|header| header.as_str()).join(" "); + + // Determine config for canonicalization + 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()); + } + + // Compute canonical representation + let content = self.canonicalize(&canonicalize_config)?; // Sign the content let signature = config.signature.http_sign(content.as_bytes()); @@ -298,10 +477,7 @@ impl SigningExt for R { // Construct the authorization header let auth_header = format!( r#"Signature keyId="{}",algorithm="{}",signature="{}",headers="{}"#, - config.key_id, - config.signature.name(), - signature, - headers + config.key_id, "hs2019", signature, joined_headers ); // Attach the authorization header to the request diff --git a/src/verifying.rs b/src/verifying.rs index 514c07e..e9e2398 100644 --- a/src/verifying.rs +++ b/src/verifying.rs @@ -10,8 +10,9 @@ use sha2::{Digest, Sha256, Sha512}; use subtle::ConstantTimeEq; use crate::algorithm::{HttpDigest, HttpSignatureVerify}; +use crate::canonicalize::{CanonicalizeConfig, CanonicalizeExt}; use crate::header::{Header, PseudoHeader}; -use crate::{DefaultDigestAlgorithm, DATE_FORMAT}; +use crate::{DefaultDigestAlgorithm, RequestLike, DATE_FORMAT}; /// This error indicates that we failed to verify the request. As a result /// the request should be ignored. @@ -47,7 +48,7 @@ pub trait KeyProvider: Debug + Sync + 'static { /// Given the name of an algorithm (eg. `hmac-sha256`) and the key ID, return a set /// of possible keys and algorithms. Returns an empty Vec if no appropriate key/algorithm /// combination could be found. - fn provide_keys(&self, name: Option<&str>, key_id: &str) -> Vec>; + fn provide_keys(&self, key_id: &str) -> Vec>; } /// Implementation of a simple key store. @@ -91,16 +92,8 @@ impl SimpleKeyProvider { } impl KeyProvider for SimpleKeyProvider { - fn provide_keys(&self, name: Option<&str>, key_id: &str) -> Vec> { - // `hs2019` is a special value that could mean any algorithm - let name = name.filter(|n| !n.eq_ignore_ascii_case("hs2019")); - self.keys - .get(key_id) - .unwrap_or(&Vec::new()) - .iter() - .filter(|alg| name.map_or(true, |n| alg.name().eq_ignore_ascii_case(n))) - .cloned() - .collect() + fn provide_keys(&self, key_id: &str) -> Vec> { + self.keys.get(key_id).unwrap_or(&Vec::new()).to_vec() } } @@ -305,17 +298,12 @@ impl VerifyingConfig { /// /// Typically this trait is implemented for references or mutable references to those /// request types rather than for the request type itself. -pub trait ServerRequestLike { +pub trait ServerRequestLike: RequestLike { /// For some request types, the verification process may be a destructive operation. /// This associated type can be used to return information that might otherwise /// be lost. type Remnant; - /// Return the value for the given header, or `None` if it's not set. For normal headers - /// implementations should return the unmodified header only if it is present in the - /// original request: they should not try to "guess" the value for missing headers. - fn header(&self, header: &Header) -> Option; - /// Complete the verification process, indicating that we want to compute a digest of the /// request body. This may require buffering the whole request body into memory. /// @@ -413,23 +401,12 @@ fn verify_signature_only( None })?; let algorithm_name = auth_args.get("algorithm").copied(); - let created = auth_args.get("created").copied(); - let expires = auth_args.get("expires").copied(); let verification_details = VerificationDetails { key_id: key_id.into(), }; - // Pull out the ordered list of headers - let headers: Vec<_> = auth_args - .get("headers") - .copied() - .unwrap_or("date") - .split(' ') - .map(str::to_ascii_lowercase) - .collect(); - // Find the appropriate key - let algorithms = config.key_provider.provide_keys(algorithm_name, key_id); + let algorithms = config.key_provider.provide_keys(key_id); if algorithms.is_empty() { info!( "Verification Failed: Unknown key (keyId={:?}, algorithm={:?})", @@ -439,65 +416,65 @@ fn verify_signature_only( return None; } - // Parse header names - let header_vec = headers - .iter() - .map(|header| { - let header_name = header.parse().ok().or_else(|| { - info!("Verification Failed: Invalid header name '{}' in signature", header); + // Determine config for canonicalization + let mut canonicalize_config = CanonicalizeConfig::new(); + if let Some(headers) = auth_args.get("headers") { + canonicalize_config.set_headers( + headers + .split(' ') + .map(str::to_ascii_lowercase) + .map(|header| { + header.parse::
().ok().or_else(|| { + info!("Verification Failed: Invalid header name {:?}", header); + None + }) + }) + .collect::>()?, + ); + } + if let Some(created) = auth_args.get("created") { + canonicalize_config.set_signature_created(created.parse::().ok().or_else( + || { + info!( + "Verification Failed: Invalid signature creation date {:?}", + created + ); None - })?; - let value = match &header_name { - Header::Pseudo(PseudoHeader::Created) => created.or_else(|| { - info!("Verification Failed: Missing header '(created)' which is included in the signature"); - None - })?.parse().ok().or_else(|| { - info!("Verification Failed: Invalid creation date '{}'", created.unwrap_or_default()); - None - })?, - Header::Pseudo(PseudoHeader::Expires) => expires.or_else(|| { - info!("Verification Failed: Missing header '(expires)' which is included in the signature"); - None - })?.parse().ok().or_else(|| { - info!("Verification Failed: Invalid expiry date '{}'", created.unwrap_or_default()); - None - })?, - _ => req.header(&header_name).or_else(|| { - info!("Verification Failed: Missing header '{}' which is included in the signature", header_name.as_str()); - None - })? - }; - Some((header_name, value)) - }) - .collect::>>()?; - - // Build the content block - let content_vec = header_vec - .iter() - .map(|(name, value)| { - Some(format!( - "{}: {}", - name.as_str(), - value.to_str().ok().or_else(|| { - info!( - "Verification Failed: Non-ascii value for '{}' header", - name.as_str() - ); - None - })? - )) + }, + )?); + } + if let Some(expires) = auth_args.get("expires") { + canonicalize_config.set_signature_expires(expires.parse::().ok().or_else( + || { + info!( + "Verification Failed: Invalid signature expires date {:?}", + expires + ); + None + }, + )?); + } + + // Canonicalize the request + let content = req + .canonicalize(&canonicalize_config) + .map_err(|e| { + info!("Canonicalization Failed: {}", e); }) - .collect::>>()?; - let content = content_vec.join("\n"); + .ok()?; // Verify the signature of the content - for algorithm in algorithms { + for algorithm in &algorithms { if algorithm.http_verify(content.as_bytes(), provided_signature) { - return Some((header_vec.into_iter().collect(), verification_details)); + return Some((content.headers.into_iter().collect(), verification_details)); } } - info!("Verification Failed: Invalid signature provided"); + if algorithms.is_empty() { + info!("Verification Failed: No keys found for this keyId"); + } else { + info!("Verification Failed: Invalid signature provided"); + } None }