diff --git a/src/algorithm.rs b/src/algorithm.rs index d3d186d..d682d49 100644 --- a/src/algorithm.rs +++ b/src/algorithm.rs @@ -1,34 +1,78 @@ -use std::fmt::Display; - -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum Algorithm { - HS2019, - RSA_SHA1, - RSA_SHA256, - HMAC_SHA256, - ECDSA_SHA256 +use std::{fmt::Debug, io::Write}; + +use ring::{rand::{SecureRandom}, signature::RsaKeyPair}; + +/// The signature algorithm used to generate the HTTP message signature. The signature +/// algorithm determines determines the hashing and signing algorithms used in computing +/// the signature. Technically, it also determines the canonicalization algorithm used to +/// build the string to sign, but as all signature algorithms share the same +/// canonicalization algorithm, this trait does not include that feature. +pub trait SignatureAlgorithm { + /// The name which will be used for the "algorithm" signature parameter. + fn name(&self) -> &str; + + /// The id of the key, which will be used for the "keyId" signature parameter. + fn key_id(&self) -> &str; + + /// Is the (created) signature element allowed? + fn allows_created(&self) -> bool; + + /// Hash a block of data. + fn hash(&self, data: &[u8], output: &mut dyn Write) -> std::io::Result<()>; + + /// Digitally sign a block of data. + fn sign(&self, data: &[u8], output: &mut dyn Write) -> std::io::Result<()>; +} + +pub struct RsaSha256 { + key_id: String, + key: RsaKeyPair, + random: Rand, } -impl Algorithm { - fn name(&self) -> &'static str { - match self { - Algorithm::HS2019 => "hs2019", - Algorithm::RSA_SHA1 => "rsa-sha1", - Algorithm::RSA_SHA256 => "rsa-sha256", - Algorithm::HMAC_SHA256 => "hmac-sha256", - Algorithm::ECDSA_SHA256 => "ecdsa-sha256", +impl RsaSha256 { + pub fn new(key_id: impl Into, key: RsaKeyPair, random: Rand) -> Self { + Self { + key_id: key_id.into(), + key, + random } } } -impl Default for Algorithm { - fn default() -> Self { - Self::HS2019 +impl SignatureAlgorithm for RsaSha256 { + fn name(&self) -> &str { + "rsa-sha256" + } + + fn key_id(&self) -> &str { + &self.key_id + } + + fn allows_created(&self) -> bool { + false + } + + fn hash(&self, data: &[u8], output: &mut dyn Write) -> std::io::Result<()> { + let digest = ring::digest::digest(&ring::digest::SHA1_FOR_LEGACY_USE_ONLY, data); + output.write_all(digest.as_ref()) + } + + fn sign(&self, data: &[u8], output: &mut dyn Write) -> std::io::Result<()> { + // 1024 bytes is enough for RSA-8192 keys. + let mut signature = [0u8; 1024]; + let signature = &mut signature[..self.key.public_modulus_len()]; + self.key.sign(&ring::signature::RSA_PKCS1_SHA256, &self.random, data, signature) + .expect("Failed to compute RSA_PKCS1_SHA256"); + output.write_all(signature) } } -impl Display for Algorithm { +impl Debug for RsaSha256 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(self.name()) + f.debug_struct("RsaSha256") + .field("key_id", &self.key_id) + .field("key", &self.key) + .finish() } } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 8d9fd8a..3e31b53 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,5 @@ pub mod algorithm; pub mod request; -pub mod sign; \ No newline at end of file +pub mod sign; +pub mod signature; \ No newline at end of file diff --git a/src/request.rs b/src/request.rs index f85026d..9464fff 100644 --- a/src/request.rs +++ b/src/request.rs @@ -114,6 +114,22 @@ pub enum Method { Patch, } +impl Method { + pub fn lowercase(&self) -> &'static [u8] { + match self { + Self::Get => b"get", + Self::Post => b"post", + Self::Put => b"put", + Self::Delete => b"delete", + Self::Head => b"head", + Self::Options => b"options", + Self::Connect => b"connect", + Self::Patch => b"patch", + Self::Trace => b"trace", + } + } +} + pub trait Headers { type NameIter<'a> : Iterator; type ValueIter<'a> : Iterator; diff --git a/src/sign.rs b/src/sign.rs index cbefa28..7e5ad4f 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -10,27 +10,12 @@ use ring::rand::SecureRandom; use ring::signature::RsaKeyPair; trait MethodExt { - fn lowercase(&self) -> &'static [u8]; fn is_body_mandatory(&self) -> bool; fn required_object_storage_signing_elements(self) -> &'static [SignatureElement<'static>]; fn required_non_object_storage_signing_elements(&self) -> &'static [SignatureElement<'static>]; } impl MethodExt for Method { - fn lowercase(&self) -> &'static [u8] { - match self { - Self::Get => b"get", - Self::Post => b"post", - Self::Put => b"put", - Self::Delete => b"delete", - Self::Head => b"head", - Self::Options => b"options", - Self::Connect => b"connect", - Self::Patch => b"patch", - Self::Trace => b"trace", - } - } - fn is_body_mandatory(&self) -> bool { match self { Self::Put | Self::Patch | Self::Post => true, diff --git a/src/signature.rs b/src/signature.rs new file mode 100644 index 0000000..c237eb3 --- /dev/null +++ b/src/signature.rs @@ -0,0 +1,263 @@ +use std::{io::Write, time::{Duration, UNIX_EPOCH}}; +use std::time::SystemTime; + +use crate::{algorithm::SignatureAlgorithm, request::{Headers, Request}}; + +/// An element that contributes to the signature calculation. Standard HTTP headers may +/// be included in the signature, as well as special non-header fields such as +/// `(request-target)`, `(created)`, and `(expires)`. The list of signature elements +/// determines which parts of the HTTP message are protected by the signature. The order +/// of signature elements that is chosen is also important in that it determines the +/// order in which the signature input string is formed. +#[derive(Debug, Eq, PartialEq)] +pub enum SignatureElement<'a> { + /// The `(request-target)` special field. Results in the concatenation of the lowercase + /// request method, an ASCII space, and the request path. Can be used with any + /// algorithm. + /// + /// RECOMMENDED. + RequestTarget, + + /// The `(created)` special field, indicating when the signature was created. + /// Expressed as a Unix timestamp (at seconds granularity). Cannot be used with RSA, + /// HMAC, or ECDSA algorithms. + /// + /// OPTIONAL. + Created, + + /// The `(expires)` special field, indicating when the signature will expire. + /// Expressed as a Unix timestamp (at seconds granularity). Can be used with any + /// algorithm. + /// + /// OPTIONAL. + Expires, + + /// A standard HTTP header element. Results in the header name being concatenated with + /// the literal string ": ", followed by every corresponding value for the header + /// being concatenated by ", ". The names of the headers specified must be all lower + /// case. + Header(&'a str), +} + +/// Which of the two signature schemes defined by the standard will be used. The signature +/// scheme determines which HTTP header the signature will be placed into, as well as the +/// format of that header. +#[derive(Debug, Eq, PartialEq, Copy, Clone)] +pub enum SignatureScheme { + /// The `Authorization: Signature ` scheme. + SignatureHeader, +} + +impl SignatureScheme { + fn header_prefix(&self) -> &str { + match self { + SignatureScheme::AuthorizationHeader => "Signature ", + SignatureScheme::SignatureHeader => "", + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum SignError { + /// No signature elements were provided. + EmptySignatureElements, + + /// The (created) signature element was requested, but the SignatureAlgorithm does not + /// allow it to be used. + CreatedNotAllowed, + + /// A SignatureElement::Header was specified, but the named header was not present in + /// the request. + MissingHeader(String), + + /// A SignatureElement was specified more than once. + DuplicateElement(String), + + /// A SignatureElement was specified with a non-lowercase header name. + NotLowercaseHeader(String), + + /// An unexpected internal error. + Internal(&'static str) +} + +pub fn sign<'sig_elems, SigAlg, Req, IntoSigElements>( + scheme: SignatureScheme, + sig_alg: &SigAlg, + request: &mut Req, + expiration: Duration, + signature_elements: &[SignatureElement<'_>], + ) -> Result<(), SignError> + where + SigAlg: SignatureAlgorithm, + Req: Request, +{ + validate_signature_elements(sig_alg, request, signature_elements)?; + let now = SystemTime::now(); + let created = now.duration_since(UNIX_EPOCH) + .map_err(|_err| SignError::Internal("Unable to determine (created) Unix timestamp"))? + .as_secs(); + let expires = (now + expiration).duration_since(UNIX_EPOCH) + .map_err(|_err| SignError::Internal("Unable to determine (expires) Unix timestamp"))? + .as_secs(); + let signature_input = build_canonical_signature_input( + sig_alg, request, created, expires, signature_elements)?; + let encoded_signature = get_encoded_signature(sig_alg, signature_input)?; + let signature_header = build_final_header(scheme, sig_alg, encoded_signature, created, expires, signature_elements)?; + match scheme { + SignatureScheme::AuthorizationHeader => request.headers_mut().insert_header("authorization", signature_header.as_slice()), + SignatureScheme::SignatureHeader => request.headers_mut().insert_header("signature", signature_header.as_slice()) + } + Ok(()) +} + +fn validate_signature_elements( + sig_alg: &SigAlg, + request: &mut Req, + signature_elements: &[SignatureElement<'_>], + ) -> Result<(), SignError> { + if signature_elements.is_empty() { + return Err(SignError::EmptySignatureElements); + } + + for element in signature_elements { + if let SignatureElement::Header(header) = element { + // Make sure header is all lowercase. + if !header.chars().all(|c| !c.is_alphabetic() || c.is_lowercase()) { + return Err(SignError::NotLowercaseHeader(header.to_string())); + } + + // Make sure referenced header exists. + if !request.headers().contains_header(header) { + return Err(SignError::MissingHeader(header.to_string())); + } + } + + // Created can only be specified for certain algorithms. + if let (SignatureElement::Created, false) = (element, sig_alg.allows_created()) { + return Err(SignError::CreatedNotAllowed); + } + + // Check for duplicates. Use an O(n^2) loop instead of a HashSet to save allocations. + // There isn't expected to be a large number of elements. + let occurrences = signature_elements.iter().filter(|elem| *elem == element).count(); + if occurrences > 1 { + return Err(SignError::DuplicateElement(match element { + SignatureElement::RequestTarget => "(request-target)".to_string(), + SignatureElement::Created => "(created)".to_string(), + SignatureElement::Expires => "(expires)".to_string(), + SignatureElement::Header(name) => name.to_string() + })); + } + } + Ok(()) +} + +fn build_canonical_signature_input<'sig_elems, SigAlg, Req>( + sig_alg: &SigAlg, + request: &mut Req, + created: u64, + expires: u64, + signature_elements: &[SignatureElement<'_>], + ) -> Result, SignError> + where + SigAlg: SignatureAlgorithm, + Req: Request, +{ + let mut canonical = Vec::with_capacity(1024); + for element in signature_elements { + match element { + SignatureElement::RequestTarget => { + canonical.extend_from_slice(b"(request-target): "); + canonical.extend_from_slice(request.method().lowercase()); + canonical.push(b' '); + // TODO: url-encode the path and query string? + canonical.extend_from_slice(request.path().as_bytes()); + if let Some(query) = request.query_string() { + canonical.push(b'?'); + canonical.extend_from_slice(query.as_bytes()); + } + canonical.push(b'\n'); + } + SignatureElement::Created => { + write!(canonical, "(created): {}\n", created) + .map_err(|_err| SignError::Internal("Failed to format (created) canonical entry"))?; + } + SignatureElement::Expires => { + write!(canonical, "(expires): {}\n", expires) + .map_err(|_err| SignError::Internal("Failed to format (expires) canonical entry"))?; + } + SignatureElement::Header(name) => { + canonical.extend_from_slice(name.as_bytes()); + canonical.extend_from_slice(b": "); + if request.headers().header_values(name).any(|_| true) { + for value in request.headers().header_values(name) { + // If header value is a valid UTF-8 string, then trim it, otherwise use the raw bytes + if let Ok(value_str) = std::str::from_utf8(value) { + canonical.extend_from_slice(value_str.trim().as_bytes()); + } else { + canonical.extend_from_slice(value); + } + canonical.extend_from_slice(b", "); + } + // remove last ", ". We know there is at least one. + assert_eq!(Some(b' '), canonical.pop()); + assert_eq!(Some(b','), canonical.pop()); + } + canonical.push(b'\n'); + } + } + } + Ok(canonical) +} + +fn get_encoded_signature( + sig_alg: &SigAlg, + mut signature_input: Vec, + ) -> Result { + let mut signature = Vec::new(); + sig_alg.sign(signature_input.as_slice(), &mut signature) + .map_err(|_err| SignError::Internal("IO error when signing"))?; + // Reuse the signature_input for the base64 output, since we're not using it anymore. + signature_input.clear(); + let mut encoded = String::from_utf8(signature_input) + .map_err(|_err| SignError::Internal("Unable to resuse siganture_input allocation for base64 output"))?; + base64::encode_config_buf(signature, base64::STANDARD, &mut encoded); + Ok(encoded) +} + +fn build_final_header( + scheme: SignatureScheme, + sig_alg: &SigAlg, + encoded_signature: String, + created: u64, + expires: u64, + signature_elements: &[SignatureElement<'_>], + ) -> Result, SignError> { + let mut header = Vec::new(); + + header.extend_from_slice(scheme.header_prefix().as_bytes()); + header.extend_from_slice(b"keyId=\""); + header.extend_from_slice(sig_alg.key_id().as_bytes()); + header.extend_from_slice(b"\",algorithm=\""); + header.extend_from_slice(sig_alg.name().as_bytes()); + write!(header, "\",created={},expires={},headers=\"", created, expires) + .map_err(|_err| SignError::Internal("Unable to write to final header buffer"))?; + for element in signature_elements { + header.extend_from_slice(match element { + SignatureElement::RequestTarget => b"(request-target)", + SignatureElement::Created => b"(created)", + SignatureElement::Expires => b"(expires)", + SignatureElement::Header(name) => name.as_bytes() + }); + header.push(b' '); + } + assert_eq!(Some(b' '), header.pop()); + header.extend_from_slice(b"\",signature=\""); + header.extend_from_slice(encoded_signature.as_bytes()); + header.push(b'"'); + + Ok(header) +} \ No newline at end of file