use crate::hash::HashAlgorithm; use crate::http::http_request::{HttpRequest, HttpRequestError}; use crate::key::{PrivateKey, PublicKey}; use crate::signature::{SignatureAlgorithm, SignatureError}; use base64::engine::general_purpose; use base64::{DecodeError, Engine as _}; use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::fmt::{self, Debug}; use std::str::FromStr; use thiserror::Error; // === error type === // #[derive(Debug, Error)] #[non_exhaustive] pub enum HttpSignatureError { /// couldn't decode base64 #[error("couldn't decode base64: {source}")] Base64Decoding { source: DecodeError }, /// signature is not yet valid #[error("signature is not yet valid (created: {created}, now: {now})")] NotYetValid { created: u64, now: u64 }, /// signature expired #[error("signature expired (not after: {not_after}, now: {now})")] Expired { not_after: u64, now: u64 }, /// signature error occurred #[error("signature error: {source}")] Signature { source: SignatureError }, /// couldn't generate signing string #[error("couldn't generate signing string: {source}")] SigningStringGeneration { source: HttpRequestError }, /// invalid signing string #[error("signing string invalid for line `{line}`")] InvalidSigningString { line: String }, /// missing required builder argument #[error("missing required builder argument `{arg}`")] MissingBuilderArgument { arg: &'static str }, /// builder requires a non empty `headers` parameter #[error("builder requires a non empty `headers` parameter")] BuilderEmptyHeaders, /// `headers` parameter shouldn't be provided when using builder with a pre-generated signing string #[error("`headers` parameter shouldn't be provided when using builder with a pre-generated signing string")] BuilderHeadersProvidedWithPreGenerated, /// required parameter is missing from http signature string #[error("required parameter is missing from http signature string: {parameter}")] MissingRequiredParameter { parameter: &'static str }, /// a parameter is present but invalid #[error("invalid parameter: {parameter}")] InvalidParameter { parameter: &'static str }, /// incompatible 'algorithm' parameter with provided signature verification method #[error("incompatible 'algorithm' parameter: {value:?}")] IncompatibleAlgorithm { value: SignatureAlgorithm }, } impl From for HttpSignatureError { fn from(e: DecodeError) -> Self { Self::Base64Decoding { source: e } } } impl From for HttpSignatureError { fn from(e: SignatureError) -> Self { Self::Signature { source: e } } } impl From for HttpSignatureError { fn from(e: HttpRequestError) -> Self { Self::SigningStringGeneration { source: e } } } // === header parameter === #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum Header { /// Lowercased HTTP header field name Name(String), /// Special `(request-target)` header field RequestTarget, /// Special `(created)` header field Created, /// Special `(expires)` header field Expires, } impl Header { pub const REQUEST_TARGET_STR: &'static str = "(request-target)"; pub const CREATED_STR: &'static str = "(created)"; pub const EXPIRES_STR: &'static str = "(expires)"; pub fn new_name(mut name: String) -> Self { name.make_ascii_lowercase(); Self::Name(name) } pub fn as_str(&self) -> &str { match self { Header::Name(header_name) => header_name.as_str(), Header::RequestTarget => Self::REQUEST_TARGET_STR, Header::Created => Self::CREATED_STR, Header::Expires => Self::EXPIRES_STR, } } } impl fmt::Display for Header { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.as_str()) } } impl From<&str> for Header { fn from(s: &str) -> Self { match s { Self::REQUEST_TARGET_STR => Self::RequestTarget, Self::CREATED_STR => Self::Created, Self::EXPIRES_STR => Self::Expires, _ => Self::new_name(s.to_owned()), } } } // === signature algorithm === // #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub enum HttpSigAlgorithm { Known(SignatureAlgorithm), Custom(String), } impl HttpSigAlgorithm { pub fn as_known(&self) -> Option { if let Self::Known(algo) = self { Some(*algo) } else { None } } pub fn is_known(&self) -> bool { self.as_known().is_some() } pub fn as_custom(&self) -> Option<&str> { if let Self::Custom(name) = self { Some(name.as_str()) } else { None } } pub fn is_custom(&self) -> bool { self.as_custom().is_some() } pub fn as_str(&self) -> &str { match self { Self::Known(algo) => to_http_sig_algo_str(*algo), Self::Custom(name) => name, } } } // === http signature === /// Contains signature parameters. #[derive(Debug, Clone, Hash, PartialEq, Eq)] pub struct HttpSignature { /// An opaque string that the server can /// use to look up the component they need to validate the signature. pub key_id: String, /// In original string format, `headers` should be a lowercased, quoted list of HTTP header /// fields, separated by a single space character. /// /// For instance : `(request-target) (created) host date cache-control x-emptyheader x-example`. pub headers: Vec
, /// The `created` field expresses when the signature was /// created. The value MUST be a Unix timestamp integer value. A /// signature with a `created` timestamp value that is in the future MUST /// NOT be processed. pub created: Option, /// The `expires` field expresses when the signature ceases to /// be valid. The value MUST be a Unix timestamp integer value. A /// signature with an `expires` timestamp value that is in the past MUST /// NOT be processed. pub expires: Option, /// Base 64 encoded digital signature, as described in RFC4648, Section 4. The /// client uses the `algorithm` and `headers` signature parameters to /// form a canonicalized `signing string`. This `signing string` is then /// signed with the key associated with `key_id` and the algorithm /// corresponding to `algorithm`. The `signature` parameter is then set /// to the base 64 encoding of the signature. pub signature: String, /// Used to specify the signature string construction mechanism. /// Implementers SHOULD derive the digital signature algorithm used by an implementation from /// the key metadata identified by the `keyId` rather than from this field. If `algorithm` /// is provided and differs from the key metadata identified by the `keyId`, for example /// `rsa-sha256` but an EdDSA key is identified via `keyId`, then an implementation /// MUST produce an error. /// Note: as of draft 12 there is only one signature string construction mechanism. As such /// this parameter is only used to hint the digital signature algorithm. pub algorithm: Option, legacy: bool, } impl HttpSignature { pub fn verifier(&self) -> HttpSignatureVerifier<'_> { HttpSignatureVerifier { http_signature: self, inner: Default::default(), } } pub fn to_signing_string(&self) -> String { let mut acc = Vec::with_capacity(5); if self.legacy { acc.push(format!( "{} {}={}", HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id )); } else { acc.push(format!( "{} {}=\"{}\"", HTTP_SIGNATURE_HEADER, HTTP_SIGNATURE_KEY_ID, self.key_id )); match &self.algorithm { Some(HttpSigAlgorithm::Custom(algorithm_name)) => { acc.push(format!("{HTTP_SIGNATURE_ALGORITHM}=\"{algorithm_name}\"")); } Some(HttpSigAlgorithm::Known(algorithm)) => { acc.push(format!( "{}=\"{}\"", HTTP_SIGNATURE_ALGORITHM, to_http_sig_algo_str(*algorithm) )); } None => {} } } if let Some(created) = self.created { acc.push(format!("{HTTP_SIGNATURE_CREATED}={created}")); } if let Some(expires) = self.expires { acc.push(format!("{HTTP_SIGNATURE_EXPIRES}={expires}")); } if self.legacy { acc.push(format!( "{}={}", HTTP_SIGNATURE_HEADERS, self.headers .iter() .map(|header| header.as_str()) .collect::>() .join(" "), )); acc.push(format!("{}={}", HTTP_SIGNATURE_SIGNATURE, self.signature)); } else { acc.push(format!( "{}=\"{}\"", HTTP_SIGNATURE_HEADERS, self.headers .iter() .map(|header| header.as_str()) .collect::>() .join(" "), )); acc.push(format!("{}=\"{}\"", HTTP_SIGNATURE_SIGNATURE, self.signature)); } acc.join(",") } } const HTTP_SIGNATURE_HEADER: &str = "Signature"; const HTTP_SIGNATURE_KEY_ID: &str = "keyId"; const HTTP_SIGNATURE_SIGNATURE: &str = "signature"; const HTTP_SIGNATURE_CREATED: &str = "created"; const HTTP_SIGNATURE_EXPIRES: &str = "expires"; const HTTP_SIGNATURE_HEADERS: &str = "headers"; const HTTP_SIGNATURE_ALGORITHM: &str = "algorithm"; impl fmt::Display for HttpSignature { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.to_signing_string()) } } impl FromStr for HttpSignature { type Err = HttpSignatureError; fn from_str(http_authorization_header: &str) -> Result { let items = http_authorization_header .trim_start_matches(HTTP_SIGNATURE_HEADER) .split(',') .collect::>(); let mut keys = HashMap::new(); for item in items { if let Some(index) = item.find('=') { let (key, value) = item.split_at(index); let value = value[1..].trim().trim_matches('"'); keys.insert(key.trim(), value.trim().to_owned()); } } let headers = { if let Some(headers_str) = keys.remove(HTTP_SIGNATURE_HEADERS) { let headers_str_vec = headers_str.split(' ').collect::>(); let mut headers = Vec::with_capacity(headers_str_vec.len()); for header_str in headers_str_vec { headers.push(Header::from(header_str)); } headers } else { vec![] } }; let created = if let Some(created) = keys.remove(HTTP_SIGNATURE_CREATED) { Some( created .parse::() .map_err(|_| HttpSignatureError::InvalidParameter { parameter: HTTP_SIGNATURE_CREATED, })?, ) } else { None }; let expires = if let Some(created) = keys.remove(HTTP_SIGNATURE_EXPIRES) { Some( created .parse::() .map_err(|_| HttpSignatureError::InvalidParameter { parameter: HTTP_SIGNATURE_EXPIRES, })?, ) } else { None }; let algorithm = keys.remove(HTTP_SIGNATURE_ALGORITHM).map(|val| { if let Some(algo) = from_http_sig_algo_str(&val) { HttpSigAlgorithm::Known(algo) } else { HttpSigAlgorithm::Custom(val) } }); let signature = keys .remove(HTTP_SIGNATURE_SIGNATURE) .ok_or(HttpSignatureError::MissingRequiredParameter { parameter: HTTP_SIGNATURE_SIGNATURE, })?; let legacy = !signature.contains(['/', '+']); Ok(HttpSignature { key_id: keys .remove(HTTP_SIGNATURE_KEY_ID) .ok_or(HttpSignatureError::MissingRequiredParameter { parameter: HTTP_SIGNATURE_KEY_ID, })?, headers, created, expires, signature, algorithm, legacy, }) } } // === http signature builder === // macro_rules! builder_argument_missing_err { ($field:ident) => {{ const _: fn() = || { let HttpSignatureBuilderInner { $field: _, .. }; }; HttpSignatureError::MissingBuilderArgument { arg: stringify!($field), } }}; } #[derive(Clone)] enum SigningStringGenMethod<'a> { PreGenerated(&'a str), FromHttpRequest(&'a dyn HttpRequest), } impl Debug for SigningStringGenMethod<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "SigningStringGenMethod::")?; match self { SigningStringGenMethod::PreGenerated(signing_string) => write!(f, "PreGenerated({signing_string})"), SigningStringGenMethod::FromHttpRequest(_) => write!(f, "FromHttpRequest(...)"), } } } #[derive(Default, Clone, Debug)] struct HttpSignatureBuilderInner<'a> { key_id: Option, signature_method: Option<(&'a PrivateKey, SignatureAlgorithm)>, created: Option, expires: Option, headers: Vec
, signing_string_generation: Option>, legacy: bool, } #[derive(Default, Clone, Debug)] /// Utility to generate `HttpSignature`s pub struct HttpSignatureBuilder<'a> { inner: RefCell>, } impl<'a> HttpSignatureBuilder<'a> { pub fn new() -> Self { Self::default() } #[inline] /// Required pub fn key_id>(&self, key_id: S) -> &Self { self.inner.borrow_mut().key_id = Some(key_id.into()); self } #[inline] /// Required pub fn signature_method(&self, private_key: &'a PrivateKey, signature_type: SignatureAlgorithm) -> &Self { self.inner.borrow_mut().signature_method = Some((private_key, signature_type)); self } #[inline] /// If generating signing string, at least one of `created`, `expires`, `request_target` /// or `http_header` is required otherwise DO NOT provide. pub fn created(&self, unix_timestamp: u64) -> &Self { let mut inner_mut = self.inner.borrow_mut(); inner_mut.created = Some(unix_timestamp); inner_mut.headers.push(Header::Created); drop(inner_mut); self } #[inline] /// If generating signing string, at least one of `created`, `expires`, `request_target` /// or `http_header` is required otherwise DO NOT provide. pub fn expires(&self, unix_timestamp: u64) -> &Self { let mut inner_mut = self.inner.borrow_mut(); inner_mut.expires = Some(unix_timestamp); inner_mut.headers.push(Header::Expires); drop(inner_mut); self } #[inline] /// If generating signing string, at least one of `created`, `expires`, `request_target` /// or `http_header` is required otherwise DO NOT provide. pub fn request_target(&self) -> &Self { self.inner.borrow_mut().headers.push(Header::RequestTarget); self } #[inline] /// If generating signing string, at least one of `created`, `expires`, `request_target` /// or `http_header` is required otherwise DO NOT provide. pub fn http_header>(&self, header: S) -> &Self { self.inner.borrow_mut().headers.push(Header::new_name(header.into())); self } #[inline] /// Required (alternative: `pre_generated_signing_string`). pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self { self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request)); self } #[inline] /// Required (alternative: `generate_signing_string_using_http_request`). pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self { self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string)); self } #[inline] #[doc(hidden)] pub fn legacy(&self) -> &Self { self.inner.borrow_mut().legacy = true; self } pub fn build(&self) -> Result { let mut inner = self.inner.borrow_mut(); let (private_key, signature_type) = { inner .signature_method .take() .ok_or(builder_argument_missing_err!(signature_method))? }; let key_id = inner.key_id.take().ok_or(builder_argument_missing_err!(key_id))?; let signing_string_generation = inner .signing_string_generation .take() .ok_or(builder_argument_missing_err!(signing_string_generation))?; let mut created = inner.created.take(); let mut expires = inner.expires.take(); let mut headers: Vec
= inner.headers.drain(..).collect(); let legacy = inner.legacy; drop(inner); let signature_binary = match signing_string_generation { SigningStringGenMethod::PreGenerated(signing_string) => { if !headers.is_empty() { return Err(HttpSignatureError::BuilderHeadersProvidedWithPreGenerated); } // parse pre-generated signing string to fill our HttpSignature struct properly. for line in signing_string.lines() { let mut split = line.split(':'); let key = split.next().expect("there is always at least one element in the split"); if let Some(value) = split.next() { match key { Header::REQUEST_TARGET_STR => { headers.push(Header::RequestTarget); } Header::CREATED_STR => { headers.push(Header::Created); created = Some(value.trim().parse().map_err(|_| { HttpSignatureError::InvalidSigningString { line: line.to_owned() } })?); } Header::EXPIRES_STR => { headers.push(Header::Expires); expires = Some(value.trim().parse().map_err(|_| { HttpSignatureError::InvalidSigningString { line: line.to_owned() } })?); } header_name => headers.push(Header::new_name(header_name.to_owned())), } } else { return Err(HttpSignatureError::InvalidSigningString { line: line.to_owned() }); } } signature_type.sign(signing_string.as_bytes(), private_key)? } SigningStringGenMethod::FromHttpRequest(http_request) => { // Generate signing string. // See https://tools.ietf.org/html/draft-cavage-http-signatures-12#section-2.3 if headers.is_empty() { return Err(HttpSignatureError::BuilderEmptyHeaders); } let mut acc = Vec::with_capacity(headers.len()); for header in &headers { match header { Header::Name(header_name) => { let concatenated_values = http_request.get_header_concatenated_values(header_name)?; if concatenated_values.is_empty() { acc.push(format!("{}:", header_name.as_str())); } else { acc.push(format!("{}: {}", header_name.as_str(), concatenated_values)); } } Header::RequestTarget => { acc.push(format!( "{}: {} {}", header.as_str(), http_request.get_lowercased_method()?, http_request.get_target()? )); } Header::Created => acc.push(format!( "{}: {}", header.as_str(), created.expect("Some by builder construction") )), Header::Expires => acc.push(format!( "{}: {}", header.as_str(), expires.expect("Some by builder construction") )), } } let signing_string = acc.join("\n"); signature_type.sign(signing_string.as_bytes(), private_key)? } }; Ok(HttpSignature { key_id, headers, created, expires, signature: if legacy { general_purpose::URL_SAFE_NO_PAD.encode(&signature_binary) } else { general_purpose::STANDARD.encode(&signature_binary) }, algorithm: Some(HttpSigAlgorithm::Known(signature_type)), legacy, }) } } // === http signature verifier === // macro_rules! verifier_argument_missing_err { ($field:ident) => {{ const _: fn() = || { let HttpSignatureVerifierInner { $field: _, .. }; }; HttpSignatureError::MissingBuilderArgument { arg: stringify!($field), } }}; } #[derive(Default, Clone, Debug)] struct HttpSignatureVerifierInner<'a> { now: Option, leeway: u64, signature_method: Option<(&'a PublicKey, SignatureAlgorithm)>, signing_string_generation: Option>, } #[derive(Clone, Debug)] /// Utility to verify `HttpSignature`s pub struct HttpSignatureVerifier<'a> { http_signature: &'a HttpSignature, inner: RefCell>, } impl<'a> HttpSignatureVerifier<'a> { #[inline] /// Optional. Required only if http signature contains (expires) or (created) parameters. pub fn now(&self, unix_timestamp: u64) -> &Self { self.inner.borrow_mut().now = Some(unix_timestamp); self } #[inline] /// Optional. Add leeway to check expiration and creation times. pub fn leeway(&self, leeway: u64) -> &Self { self.inner.borrow_mut().leeway = leeway; self } #[inline] /// Required pub fn signature_method(&self, public_key: &'a PublicKey, signature_type: SignatureAlgorithm) -> &Self { self.inner.borrow_mut().signature_method = Some((public_key, signature_type)); self } #[inline] /// Required (alternative: `pre_generated_signing_string`). pub fn generate_signing_string_using_http_request(&self, http_request: &'a dyn HttpRequest) -> &Self { self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::FromHttpRequest(http_request)); self } #[inline] /// Required (alternative: `generate_signing_string_using_http_request`). pub fn pre_generated_signing_string(&self, signing_string: &'a str) -> &Self { self.inner.borrow_mut().signing_string_generation = Some(SigningStringGenMethod::PreGenerated(signing_string)); self } pub fn verify(&self) -> Result<(), HttpSignatureError> { let mut inner = self.inner.borrow_mut(); let (public_key, signature_type) = { inner .signature_method .take() .ok_or(verifier_argument_missing_err!(signature_method))? }; // Sanity checks based on optional http signature parameter "algorithm" if let Some(HttpSigAlgorithm::Known(http_sig_algo)) = self.http_signature.algorithm { if http_sig_algo != signature_type || !is_algo_compatible_with_key(http_sig_algo, public_key) { return Err(HttpSignatureError::IncompatibleAlgorithm { value: http_sig_algo }); } } let signing_string_generation = inner .signing_string_generation .take() .ok_or(verifier_argument_missing_err!(signing_string_generation))?; if let Some(expires) = self.http_signature.expires { let now = inner.now.ok_or(verifier_argument_missing_err!(now))?; if now - inner.leeway > expires { return Err(HttpSignatureError::Expired { not_after: expires, now, }); } } if let Some(created) = self.http_signature.created { let now = inner.now.ok_or(verifier_argument_missing_err!(now))?; if now + inner.leeway < created { return Err(HttpSignatureError::NotYetValid { created, now }); } } drop(inner); let signing_string = match signing_string_generation { SigningStringGenMethod::PreGenerated(signing_string) => Cow::Borrowed(signing_string), SigningStringGenMethod::FromHttpRequest(http_request) => { let headers = if self.http_signature.headers.is_empty() { &[Header::Created][..] } else { self.http_signature.headers.as_slice() }; let mut acc = Vec::with_capacity(headers.len()); for header in headers { match header { Header::Name(header_name) => { let concatenated_values = http_request.get_header_concatenated_values(header_name)?; if concatenated_values.is_empty() { acc.push(format!("{}:", header_name.as_str())); } else { acc.push(format!("{}: {}", header_name.as_str(), concatenated_values)); } } Header::RequestTarget => { acc.push(format!( "{}: {} {}", header.as_str(), http_request.get_lowercased_method()?, http_request.get_target()? )); } Header::Created => acc.push(format!( "{}: {}", header.as_str(), self.http_signature .created .ok_or(HttpSignatureError::MissingRequiredParameter { parameter: HTTP_SIGNATURE_CREATED, })? )), Header::Expires => acc.push(format!( "{}: {}", header.as_str(), self.http_signature .expires .ok_or(HttpSignatureError::MissingRequiredParameter { parameter: HTTP_SIGNATURE_EXPIRES, })? )), } } Cow::Owned(acc.join("\n")) } }; let decoded_signature = if self.http_signature.legacy { general_purpose::URL_SAFE_NO_PAD.decode(&self.http_signature.signature)? } else { general_purpose::STANDARD.decode(&self.http_signature.signature)? }; signature_type.verify(public_key, signing_string.as_bytes(), &decoded_signature)?; Ok(()) } } // === http signature algorithms === // const HTTP_SIG_ALGO_RSA_MD5: &str = "rsa-md5"; const HTTP_SIG_ALGO_RSA_SHA_1: &str = "rsa-sha1"; const HTTP_SIG_ALGO_RSA_SHA_224: &str = "rsa-sha224"; const HTTP_SIG_ALGO_RSA_SHA_256: &str = "rsa-sha256"; const HTTP_SIG_ALGO_RSA_SHA_384: &str = "rsa-sha384"; const HTTP_SIG_ALGO_RSA_SHA_512: &str = "rsa-sha512"; const HTTP_SIG_ALGO_RSA_SHA2_224: &str = "rsa-sha2-224"; const HTTP_SIG_ALGO_RSA_SHA2_256: &str = "rsa-sha2-256"; const HTTP_SIG_ALGO_RSA_SHA2_384: &str = "rsa-sha2-384"; const HTTP_SIG_ALGO_RSA_SHA2_512: &str = "rsa-sha2-512"; const HTTP_SIG_ALGO_RSA_SHA3_384: &str = "rsa-sha3-384"; const HTTP_SIG_ALGO_RSA_SHA3_512: &str = "rsa-sha3-512"; const HTTP_SIG_ALGO_ECDSA_SHA_256: &str = "ecdsa-sha256"; const HTTP_SIG_ALGO_ECDSA_SHA_384: &str = "ecdsa-sha384"; const HTTP_SIG_ALGO_ED25519_SHA512: &str = "ed25519-sha512"; fn to_http_sig_algo_str(algo: SignatureAlgorithm) -> &'static str { match algo { SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5) => HTTP_SIG_ALGO_RSA_MD5, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) => HTTP_SIG_ALGO_RSA_SHA_1, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) => HTTP_SIG_ALGO_RSA_SHA_224, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_RSA_SHA_256, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_RSA_SHA_384, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) => HTTP_SIG_ALGO_RSA_SHA_512, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) => HTTP_SIG_ALGO_RSA_SHA3_384, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) => HTTP_SIG_ALGO_RSA_SHA3_512, SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) => HTTP_SIG_ALGO_ECDSA_SHA_256, SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) => HTTP_SIG_ALGO_ECDSA_SHA_384, SignatureAlgorithm::Ed25519 => HTTP_SIG_ALGO_ED25519_SHA512, SignatureAlgorithm::Ecdsa(_) => "ECDSA unsupported algorithm", } } fn from_http_sig_algo_str(s: &str) -> Option { match s { HTTP_SIG_ALGO_RSA_MD5 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::MD5)), HTTP_SIG_ALGO_RSA_SHA_1 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1)), HTTP_SIG_ALGO_RSA_SHA_224 | HTTP_SIG_ALGO_RSA_SHA2_224 => { Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224)) } HTTP_SIG_ALGO_RSA_SHA_256 | HTTP_SIG_ALGO_RSA_SHA2_256 => { Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) } HTTP_SIG_ALGO_RSA_SHA_384 | HTTP_SIG_ALGO_RSA_SHA2_384 => { Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)) } HTTP_SIG_ALGO_RSA_SHA_512 | HTTP_SIG_ALGO_RSA_SHA2_512 => { Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512)) } HTTP_SIG_ALGO_RSA_SHA3_384 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384)), HTTP_SIG_ALGO_RSA_SHA3_512 => Some(SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512)), HTTP_SIG_ALGO_ECDSA_SHA_256 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256)), HTTP_SIG_ALGO_ECDSA_SHA_384 => Some(SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384)), HTTP_SIG_ALGO_ED25519_SHA512 => Some(SignatureAlgorithm::Ed25519), _ => None, } } fn is_algo_compatible_with_key(algo: SignatureAlgorithm, key: &PublicKey) -> bool { use picky_asn1_x509::oids::*; let key_algo = Into::::into(key.as_inner().algorithm.oid()); match algo { // Currently, SignatureHashType only contains RSA methods, so this is a an auto-win _ if key_algo == RSA_ENCRYPTION => true, // Otherwise we need to check for specific hash algorithm SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1) if key_algo == SHA1_WITH_RSA_ENCRYPTION => true, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_224) if key_algo == SHA224_WITH_RSA_ENCRYPTION => true, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256) if key_algo == SHA256_WITH_RSA_ENCRYPTION => true, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384) if key_algo == SHA384_WITH_RSA_ENCRYPTION => true, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_512) if key_algo == SHA512_WITH_RSA_ENCRYPTION => true, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_384) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_384 => { true } SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA3_512) if key_algo == ID_RSASSA_PKCS1_V1_5_WITH_SHA3_512 => { true } SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_256) if key_algo == ECDSA_WITH_SHA256 => true, SignatureAlgorithm::Ecdsa(HashAlgorithm::SHA2_384) if key_algo == ECDSA_WITH_SHA384 => true, SignatureAlgorithm::Ed25519 if key_algo == ED25519 => true, // Key metadata is incompatible with this algorithm _ => false, } } #[cfg(test)] mod tests { use super::*; use crate::pem::Pem; use http::method::Method; use http::{header, request}; use picky_asn1_x509::{AlgorithmIdentifier, SubjectPublicKeyInfo}; const HTTP_SIGNATURE_EXAMPLE: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"rsa-sha256\"\ ,created=1402170695,headers=\"(request-target) (created) date\",\ signature=\"bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\ p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\ OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\ 4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\ MJ7h++UmOW/0Iw74CL2UGQ==\""; const HTTP_SIGNATURE_WEIRD_FORMAT: &str = "Signature keyId = my-rsa-key ,created= \"1402170695\",\ ,algorithm =\"rsa-sha256 \",headers=(request-target) (created) date ,\ signature=bw579lDtTDsp7zif/F7Fy93KXrM6qUfCb43JMJtiL4+3nazIPlxcxVsRJEgZzK/QQPDoeUQ\ p4BYCzi2CbthYhHJMn/Wv008gNMcQQTuEw/KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIa\ OHvrvEoQhl3sqpv1z7yCVbQtJHwnPOWoy/11p+SU3X2ARJXN555q5wSn+DykM0Ohq1cXD84MHXP5ulI0Fa8\ 4zQ5waxoXsieex4FI+zXSlngGmchBPXMUC437u2wXA1zLA4KGUL/uNScL1MKrTMqgV0MK4o6sR0LHOqHmIi\ MJ7h++UmOW/0Iw74CL2UGQ=="; fn private_key_1() -> PrivateKey { let pem = picky_test_data::RSA_2048_PK_7.parse::().expect("pem 1"); PrivateKey::from_pem(&pem).expect("private key 1") } fn private_key_2() -> PrivateKey { let pem = picky_test_data::RSA_2048_PK_1.parse::().expect("pem 2"); PrivateKey::from_pem(&pem).expect("private key 2") } #[test] fn sign() { let private_key = private_key_1(); let http_signature_builder = HttpSignatureBuilder::new(); http_signature_builder .key_id("my-rsa-key") .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) .request_target() .created(1402170695) .http_header("Date"); let req = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .header(header::CACHE_CONTROL, "max-age=60") // unused for signature .header(header::CACHE_CONTROL, "must-revalidate") // unused for signature .body(()) .expect("couldn't build request"); let (parts, _) = req.into_parts(); let http_signature = http_signature_builder .clone() .generate_signing_string_using_http_request(&parts) .build() .expect("couldn't generate http signature"); let http_signature_str = http_signature.to_signing_string(); pretty_assertions::assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE); // changing unused headers should not change signature let req_2 = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .header(header::CACHE_CONTROL, "max-age=222") // unused for signature .body(()) .expect("couldn't build request"); let (parts_2, _) = req_2.into_parts(); let http_signature_2 = http_signature_builder .generate_signing_string_using_http_request(&parts_2) .build() .expect("couldn't generate http signature 2"); let http_signature_str_2 = http_signature_2.to_signing_string(); pretty_assertions::assert_eq!(http_signature_str_2, http_signature_str); } #[test] fn verify() { let req = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .header("something-else", "owowo") // unused for signature .body(()) .expect("couldn't build request"); let (parts, _) = req.into_parts(); for http_signature in &[ HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature example"), HttpSignature::from_str(HTTP_SIGNATURE_WEIRD_FORMAT).expect("http signature weird format"), ] { assert!(!http_signature.legacy); http_signature .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts) .verify() .expect("couldn't verify"); } } #[test] fn invalid_signature_err() { let req = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .body(()) .expect("couldn't build request"); let (parts, _) = req.into_parts(); let http_signature = HttpSignatureBuilder::new() .key_id("my-rsa-key") .signature_method( &private_key_1(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .request_target() .created(1402170695) .expires(1402170705) .http_header("Date") .generate_signing_string_using_http_request(&parts) .build() .expect("couldn't generate http signature"); let err = http_signature .verifier() .now(1402170700) .signature_method( &private_key_2().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!(err.to_string(), "signature error: invalid signature"); let err = http_signature .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!( err.to_string(), "incompatible \'algorithm\' parameter: RsaPkcs1v15(SHA2_256)" ); let err = http_signature .verifier() .now(1402170710) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!( err.to_string(), "signature expired (not after: 1402170705, now: 1402170710)" ); let err = http_signature .verifier() .now(1402170600) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!( err.to_string(), "signature is not yet valid (created: 1402170695, now: 1402170600)" ); let req_2 = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 08 Jun 2014 20:51:35 GMT") .body(()) .expect("couldn't build request"); let (parts_2, _) = req_2.into_parts(); let err = http_signature .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts_2) .verify() .expect_err("verify"); assert_eq!(err.to_string(), "signature error: invalid signature"); let mut invalid_algorithm_http_sig = http_signature; invalid_algorithm_http_sig.algorithm = None; let err = invalid_algorithm_http_sig .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA1), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!(err.to_string(), "signature error: invalid signature"); } #[test] fn sign_with_pre_generated_signing_string() { let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; let http_signature = HttpSignatureBuilder::new() .key_id("my-rsa-key") .signature_method( &private_key_1(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .pre_generated_signing_string(signing_string) .build() .expect("couldn't generate http signature using pre-generated signing string"); let http_signature_str = http_signature.to_signing_string(); assert_eq!(http_signature_str, HTTP_SIGNATURE_EXAMPLE); } #[test] fn verify_with_pre_generated_signing_string() { let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature"); http_signature .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .pre_generated_signing_string(signing_string) .verify() .expect("couldn't verify"); } #[test] fn verify_with_leeway() { let signing_string = "(request-target): get /foo\n(created): 1402170695\ndate: Tue, 07 Jun 2014 20:51:35 GMT"; let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_EXAMPLE).expect("http signature"); http_signature .verifier() .now(1402170690) .leeway(10) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .pre_generated_signing_string(signing_string) .verify() .expect("couldn't verify"); } fn parse_err(http_signature: &str) -> String { http_signature .parse::() .expect_err("no parse error") .to_string() } #[test] fn http_signature_parse_err() { pretty_assertions::assert_eq!( parse_err("Signature signature=\"some sig\""), "required parameter is missing from http signature string: keyId" ); pretty_assertions::assert_eq!( parse_err( "Signature keyId=\"my-rsa-key\", created=\"HQHQHQ\", \ signature=\"some sig\"" ), "invalid parameter: created" ); } const HTTP_SIGNATURE_LEGACY: &str = "Signature keyId=my-rsa-key,created=1402170695,\ headers=(request-target) (created) date,\ signature=bw579lDtTDsp7zif_F7Fy93KXrM6qUfCb43JMJtiL4-3nazIPlxcxVsRJEgZzK_QQPDoeUQp4B\ YCzi2CbthYhHJMn_Wv008gNMcQQTuEw_KcnMrFWxqqUnVZQbCQvNai2y80WrBiOFZvN2VIdLUSO4SoIaOHvr\ vEoQhl3sqpv1z7yCVbQtJHwnPOWoy_11p-SU3X2ARJXN555q5wSn-DykM0Ohq1cXD84MHXP5ulI0Fa84zQ5w\ axoXsieex4FI-zXSlngGmchBPXMUC437u2wXA1zLA4KGUL_uNScL1MKrTMqgV0MK4o6sR0LHOqHmIiMJ7h--\ UmOW_0Iw74CL2UGQ"; #[test] fn legacy() { let req = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .body(()) .expect("couldn't build request"); let (parts, _) = req.into_parts(); { // sign let private_key = private_key_1(); let http_signature = HttpSignatureBuilder::new() .key_id("my-rsa-key") .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256)) .request_target() .created(1402170695) .generate_signing_string_using_http_request(&parts) .http_header("Date") .legacy() .build() .expect("build http signature"); pretty_assertions::assert_eq!(http_signature.to_signing_string(), HTTP_SIGNATURE_LEGACY); } { // verify let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_LEGACY).expect("http signature legacy"); http_signature .verifier() .now(1402170700) .signature_method( &private_key_1().to_public_key().unwrap(), SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_256), ) .generate_signing_string_using_http_request(&parts) .verify() .expect("couldn't verify"); } } #[test] fn incompatible_algorithm_err() { let req = request::Builder::new() .method(Method::GET) .uri("/foo") .header(header::DATE, "Tue, 07 Jun 2014 20:51:35 GMT") .body(()) .expect("couldn't build request"); let (parts, _) = req.into_parts(); let private_key = private_key_1(); let http_signature = HttpSignatureBuilder::new() .key_id("my-rsa-key") .signature_method(&private_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384)) .request_target() .created(1402170695) .generate_signing_string_using_http_request(&parts) .http_header("Date") .build() .expect("build http signature"); let mut spki = SubjectPublicKeyInfo::from(private_key_1().to_public_key().unwrap()); spki.algorithm = AlgorithmIdentifier::new_sha512_with_rsa_encryption(); let sha512_only_key = PublicKey::from(spki); let err = http_signature .verifier() .now(1402170700) .signature_method( &sha512_only_key, SignatureAlgorithm::RsaPkcs1v15(HashAlgorithm::SHA2_384), ) .generate_signing_string_using_http_request(&parts) .verify() .expect_err("verify"); assert_eq!( err.to_string(), "incompatible 'algorithm' parameter: RsaPkcs1v15(SHA2_384)" ); } const HTTP_SIGNATURE_UNKNOWN_ALGO: &str = "Signature keyId=\"my-rsa-key\",algorithm=\"magical-algo\",\ headers=\"(request-target)\",signature=\"GARBAGE\""; #[test] fn unknown_algorithms_are_ignored() { let http_signature = HttpSignature::from_str(HTTP_SIGNATURE_UNKNOWN_ALGO).expect("from str"); assert_eq!(http_signature.algorithm.unwrap().as_str(), "magical-algo"); } }