use {
crate::{
chronoutil::ParseISO8601, crypto::sha256_hex, SigV4Authenticator, SigV4AuthenticatorBuilder, SignatureError,
SignatureOptions,
},
bytes::Bytes,
chrono::{offset::FixedOffset, DateTime, Utc},
encoding::{all::UTF_8, label::encoding_from_whatwg_label, types::DecoderTrap},
http::{
header::{HeaderMap, HeaderValue},
request::Parts,
uri::Uri,
},
lazy_static::lazy_static,
log::trace,
regex::Regex,
ring::digest::{digest, SHA256, SHA256_OUTPUT_LEN},
std::{
borrow::Cow,
collections::HashMap,
fmt::{Debug, Formatter, Result as FmtResult},
str::from_utf8,
},
};
const APPLICATION_X_WWW_FORM_URLENCODED: &str = "application/x-www-form-urlencoded";
const AUTHORIZATION: &str = "authorization";
const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
const AWS4_HMAC_SHA256_BYTES: &[u8] = b"AWS4-HMAC-SHA256";
const CHARSET: &str = "charset";
const CONTENT_TYPE: &str = "content-type";
const CREDENTIAL: &[u8] = b"Credential";
const DATE: &str = "date";
const HEX_DIGITS_UPPER: [u8; 16] =
[b'0', b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8', b'9', b'A', b'B', b'C', b'D', b'E', b'F'];
const MSG_AUTH_HEADER_REQ_CREDENTIAL: &str = "Authorization header requires 'Credential' parameter.";
const MSG_AUTH_HEADER_REQ_DATE: &str =
"Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.";
const MSG_AUTH_HEADER_REQ_SIGNATURE: &str = "Authorization header requires 'Signature' parameter.";
const MSG_AUTH_HEADER_REQ_SIGNED_HEADERS: &str = "Authorization header requires 'SignedHeaders' parameter.";
const MSG_HOST_AUTHORITY_MUST_BE_SIGNED: &str =
"'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.";
const MSG_ILLEGAL_HEX_CHAR: &str = "Illegal hex character in escape % pattern: %";
const MSG_INCOMPLETE_TRAILING_ESCAPE: &str = "Incomplete trailing escape % sequence";
const MSG_QUERY_STRING_MUST_INCLUDE_CREDENTIAL: &str = "AWS query-string parameters must include 'X-Amz-Credential'.";
const MSG_QUERY_STRING_MUST_INCLUDE_SIGNATURE: &str = "AWS query-string parameters must include 'X-Amz-Signature'.";
const MSG_QUERY_STRING_MUST_INCLUDE_SIGNED_HEADERS: &str =
"AWS query-string parameters must include 'X-Amz-SignedHeaders'.";
const MSG_QUERY_STRING_MUST_INCLUDE_DATE: &str = "AWS query-string parameters must include 'X-Amz-Date'.";
const MSG_REEXAMINE_QUERY_STRING_PARAMS: &str = "Re-examine the query-string parameters.";
const MSG_REQUEST_MISSING_AUTH_TOKEN: &str = "Request is missing Authentication Token";
const MSG_UNSUPPORTED_ALGORITHM: &str = "Unsupported AWS 'algorithm': ";
const SIGNATURE: &[u8] = b"Signature";
const SIGNED_HEADERS: &[u8] = b"SignedHeaders";
const X_AMZ_ALGORITHM: &str = "X-Amz-Algorithm";
const X_AMZ_CREDENTIAL: &str = "X-Amz-Credential";
const X_AMZ_DATE: &str = "X-Amz-Date";
const X_AMZ_DATE_LOWER: &str = "x-amz-date";
const X_AMZ_SECURITY_TOKEN: &str = "X-Amz-Security-Token";
const X_AMZ_SECURITY_TOKEN_LOWER: &str = "x-amz-security-token";
const X_AMZ_SIGNATURE: &str = "X-Amz-Signature";
const X_AMZ_SIGNED_HEADERS: &str = "X-Amz-SignedHeaders";
lazy_static! {
static ref MULTISLASH: Regex = Regex::new("//+").unwrap();
static ref MULTISPACE: Regex = Regex::new(" +").unwrap();
static ref AWS4_HMAC_SHA256_RE: Regex = Regex::new(r"\s*AWS4-HMAC-SHA256(?:\s+|$)").unwrap();
}
#[derive(Debug)]
pub(crate) struct AuthParams {
pub(crate) builder: SigV4AuthenticatorBuilder,
pub(crate) signed_headers: Vec<String>,
pub(crate) timestamp_str: String,
}
pub struct CanonicalRequest {
request_method: String,
canonical_path: String,
query_parameters: HashMap<String, Vec<String>>,
headers: HashMap<String, Vec<Vec<u8>>>,
body_sha256: String,
}
impl CanonicalRequest {
pub fn from_request_parts(
mut parts: Parts,
mut body: Bytes,
options: SignatureOptions,
) -> Result<(Self, Parts, Bytes), SignatureError> {
let canonical_path = canonicalize_uri_path(&parts.uri.path(), options.s3)?;
let content_type = get_content_type_and_charset(&parts.headers);
let mut query_parameters = query_string_to_normalized_map(&parts.uri.query().unwrap_or(""))?;
if options.url_encode_form {
if let Some(content_type) = content_type {
if content_type.content_type == APPLICATION_X_WWW_FORM_URLENCODED {
trace!("Body is application/x-www-form-urlencoded; converting to query parameters");
let encoding = match &content_type.charset {
Some(charset) => match encoding_from_whatwg_label(charset.as_str()) {
Some(encoding) => encoding,
None => {
return Err(SignatureError::InvalidBodyEncoding(format!(
"application/x-www-form-urlencoded body uses unsupported charset '{}'",
charset
)))
}
},
None => {
trace!("Falling back to UTF-8 for application/x-www-form-urlencoded body");
UTF_8
}
};
let body_query = match encoding.decode(&body, DecoderTrap::Strict) {
Ok(body) => body,
Err(_) => {
return Err(SignatureError::InvalidBodyEncoding(format!(
"Invalid body data encountered parsing application/x-www-form-urlencoded with charset '{}'",
encoding.whatwg_name().unwrap_or(encoding.name())
)))
}
};
query_parameters.extend(query_string_to_normalized_map(body_query.as_str())?.into_iter());
let qs = canonicalize_query_to_string(&query_parameters);
trace!("Rebuilding URI with new query string: {}", qs);
let mut pq = canonical_path.clone();
if !qs.is_empty() {
pq.push('?');
pq.push_str(&qs);
}
parts.uri =
Uri::builder().path_and_query(pq).build().expect("failed to rebuild URI with new query string");
body = Bytes::from("");
}
}
}
let headers = normalize_headers(&parts.headers);
let body_sha256 = sha256_hex(body.as_ref());
Ok((
CanonicalRequest {
request_method: parts.method.to_string(),
canonical_path,
query_parameters,
headers,
body_sha256,
},
parts,
body,
))
}
#[inline]
pub fn request_method(&self) -> &str {
&self.request_method
}
#[inline]
pub fn canonical_path(&self) -> &str {
&self.canonical_path
}
#[inline]
pub fn query_parameters(&self) -> &HashMap<String, Vec<String>> {
&self.query_parameters
}
#[inline]
pub fn headers(&self) -> &HashMap<String, Vec<Vec<u8>>> {
&self.headers
}
#[inline]
pub fn body_sha256(&self) -> &str {
&self.body_sha256
}
pub fn canonical_query_string(&self) -> String {
canonicalize_query_to_string(&self.query_parameters)
}
pub fn canonical_request(&self, signed_headers: &Vec<String>) -> Vec<u8> {
let mut result = Vec::with_capacity(1024);
result.extend(self.request_method.as_bytes());
result.push(b'\n');
result.extend(self.canonical_path.as_bytes());
result.push(b'\n');
result.extend(self.canonical_query_string().as_bytes());
result.push(b'\n');
for header in signed_headers {
let values = self.headers.get(header);
if let Some(values) = values {
for (i, value) in values.iter().enumerate() {
if i == 0 {
result.extend(header.as_bytes());
result.push(b':');
} else {
result.push(b',');
}
result.extend(value);
}
result.push(b'\n')
}
}
result.push(b'\n');
result.extend(signed_headers.join(";").as_bytes());
result.push(b'\n');
result.extend(self.body_sha256.as_bytes());
trace!("Canonical request:\n{}", String::from_utf8_lossy(&result));
result
}
pub fn canonical_request_sha256(&self, signed_headers: &Vec<String>) -> [u8; SHA256_OUTPUT_LEN] {
let canonical_request = self.canonical_request(&signed_headers);
let result_digest = digest(&SHA256, canonical_request.as_ref());
let result_slice = result_digest.as_ref();
assert!(result_slice.len() == SHA256_OUTPUT_LEN);
let mut result: [u8; SHA256_OUTPUT_LEN] = [0; SHA256_OUTPUT_LEN];
result.as_mut_slice().clone_from_slice(&result_slice);
result
}
pub fn get_authenticator(
&self,
signed_header_requirements: &SignedHeaderRequirements,
) -> Result<SigV4Authenticator, SignatureError> {
let auth_params = self.get_auth_parameters(signed_header_requirements)?;
self.get_authenticator_from_auth_parameters(auth_params)
}
pub(crate) fn get_authenticator_from_auth_parameters(
&self,
auth_params: AuthParams,
) -> Result<SigV4Authenticator, SignatureError> {
let timestamp_str = auth_params.timestamp_str.as_str();
let timestamp = DateTime::<FixedOffset>::parse_from_iso8601(timestamp_str)
.map_err(|_| {
SignatureError::IncompleteSignature(format!(
"Date must be in ISO-8601 'basic format'. Got '{}'. See http://en.wikipedia.org/wiki/ISO_8601",
auth_params.timestamp_str
))
})?
.with_timezone(&Utc);
let mut builder = auth_params.builder;
builder.request_timestamp(timestamp);
let signed_headers = auth_params.signed_headers;
builder.canonical_request_sha256(self.canonical_request_sha256(&signed_headers));
Ok(builder.build().expect("all fields should be set"))
}
pub(crate) fn get_auth_parameters(
&self,
signed_header_requirements: &SignedHeaderRequirements,
) -> Result<AuthParams, SignatureError> {
let auth_header = self.headers.get(AUTHORIZATION);
let sig_algs = self.query_parameters.get(X_AMZ_ALGORITHM);
let params = match (auth_header, sig_algs) {
(Some(auth_header), None) => self.get_auth_parameters_from_auth_header(&auth_header[0])?,
(None, Some(sig_algs)) => self.get_auth_parameters_from_query_parameters(&sig_algs[0])?,
(Some(_), Some(_)) => return Err(SignatureError::SignatureDoesNotMatch(None)),
(None, None) => {
return Err(SignatureError::MissingAuthenticationToken(MSG_REQUEST_MISSING_AUTH_TOKEN.to_string()))
}
};
let mut found_host = false;
for header in ¶ms.signed_headers {
if header == "host" || header == ":authority" {
found_host = true;
break;
}
}
if !found_host {
return Err(SignatureError::SignatureDoesNotMatch(Some(MSG_HOST_AUTHORITY_MUST_BE_SIGNED.to_string())));
}
for header in signed_header_requirements.always_present() {
let header_lower = header.to_lowercase();
if !params.signed_headers.contains(&header_lower) {
return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
"'{}' must be a 'SignedHeader' in the AWS Authorization.",
header
))));
}
}
for header in signed_header_requirements.if_in_request() {
let header_lower = header.to_lowercase();
if self.headers.contains_key(&header_lower) && !params.signed_headers.contains(&header_lower) {
return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
"'{}' must be a 'SignedHeader' in the AWS Authorization.",
header
))));
}
}
for header in signed_header_requirements.prefixes() {
let header_lower = header.to_lowercase();
for http_header in self.headers.keys() {
if http_header.starts_with(&header_lower) && !params.signed_headers.contains(http_header) {
return Err(SignatureError::SignatureDoesNotMatch(Some(format!(
"'{}' must be a 'SignedHeader' in the AWS Authorization.",
http_header
))));
}
}
}
Ok(params)
}
fn get_auth_parameters_from_auth_header<'a>(&'a self, auth_header: &'a [u8]) -> Result<AuthParams, SignatureError> {
let auth_header = trim_ascii(auth_header);
let parts = auth_header.splitn(2, |c| *c == b' ').collect::<Vec<&'a [u8]>>();
let algorithm = parts[0];
if algorithm != AWS4_HMAC_SHA256_BYTES {
return Err(SignatureError::IncompleteSignature(format!(
"{}'{}'.",
MSG_UNSUPPORTED_ALGORITHM,
String::from_utf8_lossy(algorithm)
)));
}
let parameters = if parts.len() > 1 {
parts[1]
} else {
b""
};
let mut parameter_map = HashMap::new();
for parameter_untrimmed in parameters.split(|c| *c == b',') {
let parameter = trim_ascii(parameter_untrimmed);
if parameter.is_empty() {
continue;
}
let parts = parameter.splitn(2, |c| *c == b'=').collect::<Vec<&'a [u8]>>();
if parts.len() != 2 {
return Err(SignatureError::IncompleteSignature(format!(
"'{}' not a valid key=value pair (missing equal-sign) in Authorization header: '{}'",
latin1_to_string(parameter),
latin1_to_string(auth_header)
)));
}
parameter_map.insert(parts[0], parts[1]);
}
let mut missing_messages = Vec::new();
let mut builder = SigV4Authenticator::builder();
if let Some(credential) = parameter_map.get(CREDENTIAL) {
builder.credential(latin1_to_string(credential));
} else {
missing_messages.push(MSG_AUTH_HEADER_REQ_CREDENTIAL);
}
if let Some(signature) = parameter_map.get(SIGNATURE) {
builder.signature(latin1_to_string(signature));
} else {
missing_messages.push(MSG_AUTH_HEADER_REQ_SIGNATURE);
}
let mut signed_headers = if let Some(signed_headers) = parameter_map.get(SIGNED_HEADERS) {
signed_headers.split(|c| *c == b';').map(|s| latin1_to_string(s)).collect()
} else {
missing_messages.push(MSG_AUTH_HEADER_REQ_SIGNED_HEADERS);
Vec::new()
};
signed_headers.sort();
let mut timestamp_str = None;
if let Some(date) = self.headers.get(X_AMZ_DATE_LOWER) {
timestamp_str = Some(latin1_to_string(&date[0]));
} else if let Some(date) = self.headers.get(DATE) {
timestamp_str = Some(latin1_to_string(&date[0]));
} else {
missing_messages.push(MSG_AUTH_HEADER_REQ_DATE);
}
if !missing_messages.is_empty() {
return Err(SignatureError::IncompleteSignature(format!(
"{} Authorization={}",
missing_messages.join(" "),
latin1_to_string(algorithm)
)));
}
if let Some(token) = self.headers.get(X_AMZ_SECURITY_TOKEN_LOWER) {
builder.session_token(latin1_to_string(&token[0]));
}
let timestamp_str = timestamp_str.expect("date_str should be set");
Ok(AuthParams {
builder,
signed_headers,
timestamp_str,
})
}
fn get_auth_parameters_from_query_parameters(&self, query_alg: &str) -> Result<AuthParams, SignatureError> {
if query_alg != AWS4_HMAC_SHA256 {
return Err(SignatureError::MissingAuthenticationToken(MSG_REQUEST_MISSING_AUTH_TOKEN.to_string()));
}
let mut missing_messages = Vec::new();
let mut builder = SigV4Authenticator::builder();
if let Some(credential) = self.query_parameters.get(X_AMZ_CREDENTIAL) {
builder.credential(credential[0].clone());
} else {
missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_CREDENTIAL);
}
if let Some(signature) = self.query_parameters.get(X_AMZ_SIGNATURE) {
builder.signature(signature[0].clone());
} else {
missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_SIGNATURE);
}
let mut signed_headers = if let Some(signed_headers) = self.query_parameters.get(X_AMZ_SIGNED_HEADERS) {
let unescaped_signed_headers = unescape_uri_encoding(&signed_headers[0]);
unescaped_signed_headers.split(';').map(|s| s.to_string()).collect::<Vec<String>>()
} else {
missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_SIGNED_HEADERS);
Vec::new()
};
signed_headers.sort();
let timestamp_str = self.query_parameters.get(X_AMZ_DATE);
if timestamp_str.is_none() {
missing_messages.push(MSG_QUERY_STRING_MUST_INCLUDE_DATE);
}
if !missing_messages.is_empty() {
return Err(SignatureError::IncompleteSignature(format!(
"{} {}",
missing_messages.join(" "),
MSG_REEXAMINE_QUERY_STRING_PARAMS
)));
}
if let Some(token) = self.query_parameters.get(X_AMZ_SECURITY_TOKEN) {
builder.session_token(token[0].clone());
}
let timestamp_str = timestamp_str.expect("date_str should be set")[0].clone();
Ok(AuthParams {
builder,
signed_headers,
timestamp_str,
})
}
}
impl Debug for CanonicalRequest {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let headers = debug_headers(&self.headers);
f.debug_struct("CanonicalRequest")
.field("request_method", &self.request_method)
.field("canonical_path", &self.canonical_path)
.field("query_parameters", &self.query_parameters)
.field("headers", &headers)
.field("body_sha256", &self.body_sha256)
.finish()
}
}
pub struct ContentTypeCharset {
pub content_type: String,
pub charset: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SignedHeaderRequirements {
always_present: Vec<String>,
if_in_request: Vec<String>,
prefixes: Vec<String>,
}
impl SignedHeaderRequirements {
pub fn new(mut always_present: Vec<String>, mut if_in_request: Vec<String>, mut prefixes: Vec<String>) -> Self {
always_present.sort();
if_in_request.sort();
prefixes.sort();
SignedHeaderRequirements {
always_present,
if_in_request,
prefixes,
}
}
#[inline]
pub fn always_present(&self) -> &[String] {
&self.always_present
}
#[inline]
pub fn if_in_request(&self) -> &[String] {
&self.if_in_request
}
#[inline]
pub fn prefixes(&self) -> &[String] {
&self.prefixes
}
pub fn add_always_present(&mut self, header: &str) {
self.always_present.push(header.to_string());
self.always_present.sort();
self.always_present.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
}
pub fn add_if_in_request(&mut self, header: &str) {
self.if_in_request.push(header.to_string());
self.if_in_request.sort();
self.if_in_request.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
}
pub fn add_prefix(&mut self, prefix: &str) {
self.prefixes.push(prefix.to_string());
self.prefixes.sort();
self.prefixes.dedup_by(|a, b| a.eq_ignore_ascii_case(b));
}
pub fn remove_always_present(&mut self, header: &str) {
self.always_present.retain(|h| !h.eq_ignore_ascii_case(header));
}
pub fn remove_if_in_request(&mut self, header: &str) {
self.if_in_request.retain(|h| !h.eq_ignore_ascii_case(header));
}
pub fn remove_prefix(&mut self, prefix: &str) {
self.prefixes.retain(|p| !p.eq_ignore_ascii_case(prefix));
}
}
enum UriElement {
Path,
Query,
}
pub fn canonicalize_query_to_string(query_parameters: &HashMap<String, Vec<String>>) -> String {
let mut results = Vec::new();
for (key, values) in query_parameters.iter() {
if key != X_AMZ_SIGNATURE {
for value in values.iter() {
results.push(format!("{}={}", key, value));
}
}
}
results.sort_unstable();
results.join("&")
}
pub(crate) fn canonicalize_uri_path(uri_path: &str, s3: bool) -> Result<String, SignatureError> {
if uri_path.is_empty() || uri_path == "/" {
return Ok("/".to_string());
}
if !uri_path.starts_with('/') {
return Err(SignatureError::InvalidURIPath(format!("Path is not absolute: {}", uri_path)));
}
let uri_path = if s3 {
Cow::Borrowed(uri_path)
} else {
MULTISLASH.replace_all(uri_path, "/")
};
let mut components: Vec<String> = uri_path.split('/').map(|s| s.to_string()).collect();
let mut i = 1; while i < components.len() {
let component = normalize_uri_path_component(&components[i])?;
if component == "." && !s3 {
components.remove(i);
} else if component == ".." && !s3 {
if i <= 1 {
return Err(SignatureError::InvalidURIPath(format!(
"Relative path entry '..' navigates above root: {}",
uri_path
)));
}
components.remove(i - 1);
components.remove(i - 1);
i -= 1;
} else {
components[i] = component;
i += 1;
}
}
assert!(!components.is_empty());
match components.len() {
1 => Ok("/".to_string()),
_ => Ok(components.join("/")),
}
}
fn debug_headers(headers: &HashMap<String, Vec<Vec<u8>>>) -> String {
use std::io::Write;
let mut result = Vec::new();
for (key, values) in headers.iter() {
for value in values {
match String::from_utf8(value.clone()) {
Ok(s) => writeln!(result, "{}: {}", key, s).unwrap(),
Err(_) => writeln!(result, "{}: {:?}", key, value).unwrap(),
}
}
}
if result.is_empty() {
return String::new();
}
let result_except_last = &result[..result.len() - 1];
String::from_utf8_lossy(result_except_last).to_string()
}
pub fn get_content_type_and_charset(headers: &HeaderMap<HeaderValue>) -> Option<ContentTypeCharset> {
let content_type_opts = match headers.get(CONTENT_TYPE) {
Some(value) => value.as_ref(),
None => return None,
};
let mut parts = content_type_opts.split(|c| *c == b';').map(|s| trim_ascii(s));
let content_type = latin1_to_string(parts.next().expect("split always returns at least one element"));
for option in parts {
let opt_trim = trim_ascii(option);
let mut opt_parts = opt_trim.splitn(2, |c| *c == b'=');
let opt_name = opt_parts.next().unwrap();
if latin1_to_string(opt_name).to_lowercase() == CHARSET {
if let Some(opt_value) = opt_parts.next() {
return Some(ContentTypeCharset {
content_type,
charset: Some(latin1_to_string(opt_value)),
});
}
}
}
Some(ContentTypeCharset {
content_type,
charset: None,
})
}
#[inline]
pub fn is_rfc3986_unreserved(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'-' || c == b'.' || c == b'_' || c == b'~'
}
fn latin1_to_string(bytes: &[u8]) -> String {
let mut result = String::new();
for b in bytes {
result.push(*b as char);
}
result
}
pub fn normalize_headers(headers: &HeaderMap<HeaderValue>) -> HashMap<String, Vec<Vec<u8>>> {
let mut result = HashMap::<String, Vec<Vec<u8>>>::new();
for (key, value) in headers.iter() {
let key = key.as_str().to_lowercase();
let value = normalize_header_value(value.as_bytes());
result.entry(key).or_insert_with(Vec::new).push(value);
}
result
}
fn normalize_header_value(value: &[u8]) -> Vec<u8> {
let mut result = Vec::with_capacity(value.len());
let mut last_was_space = true;
for c in value {
if *c == b' ' {
if !last_was_space {
result.push(' ' as u8);
last_was_space = true;
}
} else {
result.push(*c);
last_was_space = false;
}
}
if last_was_space {
while result.last() == Some(&(' ' as u8)) {
result.pop();
}
}
result
}
fn normalize_query_string_element(element: &str) -> Result<String, SignatureError> {
normalize_uri_element(element, UriElement::Query)
}
pub(crate) fn normalize_uri_path_component(path: &str) -> Result<String, SignatureError> {
normalize_uri_element(path, UriElement::Path)
}
fn normalize_uri_element(uri_el: &str, uri_el_type: UriElement) -> Result<String, SignatureError> {
let path_component = uri_el.as_bytes();
let mut i = 0;
let result = &mut Vec::<u8>::new();
while i < path_component.len() {
let c = path_component[i];
if is_rfc3986_unreserved(c) {
result.push(c);
i += 1;
} else if c == b'%' {
if i + 2 >= path_component.len() {
return Err(match uri_el_type {
UriElement::Path => {
SignatureError::InvalidURIPath(MSG_INCOMPLETE_TRAILING_ESCAPE.to_string())
}
UriElement::Query => {
SignatureError::MalformedQueryString(MSG_INCOMPLETE_TRAILING_ESCAPE.to_string())
}
});
}
let hex_digits = &path_component[i + 1..i + 3];
match hex::decode(hex_digits) {
Ok(value) => {
assert_eq!(value.len(), 1);
let c = value[0];
if is_rfc3986_unreserved(c) {
result.push(c);
} else {
result.push(b'%');
result.extend(u8_to_upper_hex(c));
}
i += 3;
}
Err(_) => {
let message = format!("{}{}{}", MSG_ILLEGAL_HEX_CHAR, hex_digits[0] as char, hex_digits[1] as char);
return Err(match uri_el_type {
UriElement::Path => SignatureError::InvalidURIPath(message),
UriElement::Query => SignatureError::MalformedQueryString(message),
});
}
}
} else if c == b'+' {
result.extend_from_slice(b"%20");
i += 1;
} else {
result.push(b'%');
result.extend(u8_to_upper_hex(c));
i += 1;
}
}
Ok(from_utf8(result.as_slice()).unwrap().to_string())
}
pub fn query_string_to_normalized_map(query_string: &str) -> Result<HashMap<String, Vec<String>>, SignatureError> {
if query_string.is_empty() {
return Ok(HashMap::new());
}
let components = query_string.split('&');
let mut result = HashMap::<String, Vec<String>>::new();
for component in components {
if component.is_empty() {
continue;
}
let parts: Vec<&str> = component.splitn(2, '=').collect();
let key = parts[0];
let value = if parts.len() > 1 {
parts[1]
} else {
""
};
let norm_key = normalize_query_string_element(key)?;
let norm_value = normalize_query_string_element(value)?;
if let Some(result_value) = result.get_mut(&norm_key) {
result_value.push(norm_value);
} else {
result.insert(norm_key, vec![norm_value]);
}
}
Ok(result)
}
const fn trim_ascii_start(bytes: &[u8]) -> &[u8] {
let mut bytes = bytes;
while let [first, rest @ ..] = bytes {
if first.is_ascii_whitespace() {
bytes = rest;
} else {
break;
}
}
bytes
}
const fn trim_ascii_end(bytes: &[u8]) -> &[u8] {
let mut bytes = bytes;
while let [rest @ .., last] = bytes {
if last.is_ascii_whitespace() {
bytes = rest;
} else {
break;
}
}
bytes
}
const fn trim_ascii(bytes: &[u8]) -> &[u8] {
trim_ascii_end(trim_ascii_start(bytes))
}
#[inline]
const fn u8_to_upper_hex(b: u8) -> [u8; 2] {
let result: [u8; 2] = [HEX_DIGITS_UPPER[((b >> 4) & 0xf) as usize], HEX_DIGITS_UPPER[(b & 0xf) as usize]];
result
}
fn unescape_uri_encoding(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.bytes();
while let Some(c) = chars.next() {
if c == b'%' {
let mut hex_digits = [0u8; 2];
hex_digits[0] = chars.next().expect(MSG_INCOMPLETE_TRAILING_ESCAPE) as u8;
hex_digits[1] = chars.next().expect(MSG_INCOMPLETE_TRAILING_ESCAPE) as u8;
match u8::from_str_radix(from_utf8(&hex_digits).unwrap(), 16) {
Ok(c) => result.push(c as char),
Err(_) => panic!("{}{}{}", MSG_ILLEGAL_HEX_CHAR, hex_digits[0] as char, hex_digits[1] as char),
}
} else {
result.push(c as char);
}
}
result
}
#[cfg(test)]
mod tests {
use {
super::{debug_headers, u8_to_upper_hex},
crate::{
canonical::{
canonicalize_query_to_string, canonicalize_uri_path, normalize_uri_path_component,
query_string_to_normalized_map, unescape_uri_encoding,
},
CanonicalRequest, SignatureError, SignatureOptions, SignedHeaderRequirements,
},
bytes::Bytes,
http::{
method::Method,
request::Request,
uri::{PathAndQuery, Uri},
},
scratchstack_errors::ServiceError,
std::{collections::HashMap, mem::transmute},
};
macro_rules! expect_err {
($test:expr, $expected:ident) => {
match $test {
Ok(ref v) => panic!("Expected Err({}); got Ok({:?})", stringify!($expected), v),
Err(ref e) => match e {
SignatureError::$expected(_) => e.to_string(),
_ => panic!("Expected {}; got {:#?}: {}", stringify!($expected), &e, &e),
},
}
};
}
#[test_log::test]
fn canonicalize_uri_path_empty() {
assert_eq!(canonicalize_uri_path("", false).unwrap(), "/".to_string());
assert_eq!(canonicalize_uri_path("/", false).unwrap(), "/".to_string());
}
#[test_log::test]
fn canonicalize_valid() {
assert_eq!(canonicalize_uri_path("/hello/world", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello///world", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/./world", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/foo/../world", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/foo/%2E%2E/world", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/%77%6F%72%6C%64", false).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/w*rld", false).unwrap(), "/hello/w%2Arld".to_string());
assert_eq!(canonicalize_uri_path("/hello/w%2arld", false).unwrap(), "/hello/w%2Arld".to_string());
assert_eq!(canonicalize_uri_path("/hello/w+rld", false).unwrap(), "/hello/w%20rld".to_string());
assert_eq!(canonicalize_uri_path("/hello/world", true).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello///world", true).unwrap(), "/hello///world".to_string());
assert_eq!(canonicalize_uri_path("/hello/./world", true).unwrap(), "/hello/./world".to_string());
assert_eq!(canonicalize_uri_path("/hello/foo/../world", true).unwrap(), "/hello/foo/../world".to_string());
assert_eq!(canonicalize_uri_path("/hello/%77%6F%72%6C%64", true).unwrap(), "/hello/world".to_string());
assert_eq!(canonicalize_uri_path("/hello/w*rld", true).unwrap(), "/hello/w%2Arld".to_string());
assert_eq!(canonicalize_uri_path("/hello/w%2arld", true).unwrap(), "/hello/w%2Arld".to_string());
assert_eq!(canonicalize_uri_path("/hello/w+rld", true).unwrap(), "/hello/w%20rld".to_string());
assert_eq!(canonicalize_uri_path("/hello/../../world", true).unwrap(), "/hello/../../world".to_string());
assert_eq!(canonicalize_uri_path("/hello/%2e%2e/%2e%2e/world", true).unwrap(), "/hello/../../world".to_string());
}
#[test_log::test]
fn canonicalize_invalid() {
let e = expect_err!(canonicalize_uri_path("hello/world", false), InvalidURIPath);
assert_eq!(e.to_string(), "Path is not absolute: hello/world");
let e = canonicalize_uri_path("/hello/../../world", false).unwrap_err();
if let SignatureError::InvalidURIPath(_) = e {
assert_eq!(e.to_string(), "Relative path entry '..' navigates above root: /hello/../../world");
assert_eq!(e.error_code(), "InvalidURIPath");
assert_eq!(e.http_status(), 400);
} else {
panic!("Expected InvalidURIPath; got {:#?}", &e);
}
let e = canonicalize_uri_path("/hello/%2E%2E/%2E%2E/world", false).unwrap_err();
if let SignatureError::InvalidURIPath(_) = e {
assert_eq!(e.to_string(), "Relative path entry '..' navigates above root: /hello/%2E%2E/%2E%2E/world");
assert_eq!(e.error_code(), "InvalidURIPath");
assert_eq!(e.http_status(), 400);
} else {
panic!("Expected InvalidURIPath; got {:#?}", &e);
}
}
#[test_log::test]
fn canonicalize_query_excludes_signature() {
let query = HashMap::from([
("X-Amz-Signature".to_string(), vec!["abcdef".to_string()]),
("b".to_string(), vec!["B".to_string()]),
("c".to_string(), vec!["C".to_string()]),
("a".to_string(), vec!["A".to_string()]),
("e".to_string(), vec!["E".to_string()]),
("d".to_string(), vec!["d".to_string()]),
]);
let query = canonicalize_query_to_string(&query);
assert_eq!(query, "a=A&b=B&c=C&d=d&e=E");
}
#[test_log::test]
fn normalize_valid1() {
let result = query_string_to_normalized_map("Hello=World&foo=bar&baz=bomb&foo=2&name").unwrap();
let hello = result.get("Hello").unwrap();
assert_eq!(hello.len(), 1);
assert_eq!(hello[0], "World");
let foo = result.get("foo").unwrap();
assert_eq!(foo.len(), 2);
assert_eq!(foo[0], "bar");
assert_eq!(foo[1], "2");
let baz = result.get("baz").unwrap();
assert_eq!(baz.len(), 1);
assert_eq!(baz[0], "bomb");
let name = result.get("name").unwrap();
assert_eq!(name.len(), 1);
assert_eq!(name[0], "");
}
#[test_log::test]
fn normalize_empty() {
let result = query_string_to_normalized_map("Hello=World&&foo=bar");
let v = result.unwrap();
let hello = v.get("Hello").unwrap();
assert_eq!(hello.len(), 1);
assert_eq!(hello[0], "World");
let foo = v.get("foo").unwrap();
assert_eq!(foo.len(), 1);
assert_eq!(foo[0], "bar");
assert!(v.get("").is_none());
}
#[test_log::test]
fn normalize_invalid_hex() {
let e = expect_err!(normalize_uri_path_component("abcd%yy"), InvalidURIPath);
assert_eq!(e.as_str(), "Illegal hex character in escape % pattern: %yy");
expect_err!(normalize_uri_path_component("abcd%yy"), InvalidURIPath);
expect_err!(normalize_uri_path_component("abcd%0"), InvalidURIPath);
expect_err!(normalize_uri_path_component("abcd%"), InvalidURIPath);
assert_eq!(normalize_uri_path_component("abcd%65").unwrap(), "abcde");
}
struct PathAndQuerySimulate {
data: Bytes,
_query: u16,
}
#[test_log::test]
fn normalize_invalid_hex_path_cr() {
for (path, error_message) in vec![
("/abcd%yy", "Illegal hex character in escape % pattern: %yy"),
("/abcd%0", "Incomplete trailing escape % sequence"),
("/abcd%", "Incomplete trailing escape % sequence"),
] {
let mut fake_path = "/".to_string();
while fake_path.len() < path.len() {
fake_path.push_str("a");
}
let mut pq = PathAndQuery::from_maybe_shared(fake_path.clone()).unwrap();
let pq_path = Bytes::from_static(path.as_bytes());
unsafe {
let pq_ptr: *mut PathAndQuerySimulate = transmute(&mut pq);
(*pq_ptr).data = pq_path;
}
let uri = Uri::builder().path_and_query(pq).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("authorization", "Basic foobar")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::InvalidURIPath(msg) = e {
assert_eq!(msg.as_str(), error_message);
}
}
}
#[test_log::test]
fn normalize_invalid_hex_query_cr() {
for (path, error_message) in vec![
("/?x=abcd%yy", "Illegal hex character in escape % pattern: %yy"),
("/?x=abcd%0", "Incomplete trailing escape % sequence"),
("/?x=abcd%", "Incomplete trailing escape % sequence"),
] {
let mut fake_path = "/?x=".to_string();
while fake_path.len() < path.len() {
fake_path.push_str("a");
}
let mut pq = PathAndQuery::from_maybe_shared(fake_path.clone()).unwrap();
let pq_path = Bytes::from_static(path.as_bytes());
unsafe {
let pq_ptr: *mut PathAndQuerySimulate = transmute(&mut pq);
(*pq_ptr).data = pq_path;
}
let uri = Uri::builder().path_and_query(pq).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("authorization", "Basic foobar")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::MalformedQueryString(msg) = e {
assert_eq!(msg.as_str(), error_message);
}
}
}
#[test_log::test]
fn normalize_query_parameters_missing_value() {
let result = query_string_to_normalized_map("Key1=Value1&Key2&Key3=Value3");
assert!(result.is_ok());
let result = result.unwrap();
assert_eq!(result["Key1"], vec!["Value1"]);
assert_eq!(result["Key2"], vec![""]);
assert_eq!(result["Key3"], vec!["Value3"]);
}
#[test_log::test]
fn test_multiple_algorithms() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("authorization", "Basic foobar")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let _ = format!("{:?}", cr);
assert_eq!(cr.request_method(), "GET");
assert_eq!(cr.canonical_path(), "/");
assert!(cr.query_parameters().is_empty());
assert_eq!(cr.headers().len(), 2);
assert_eq!(cr.headers().get("authorization").unwrap().len(), 2);
assert_eq!(
cr.headers().get("authorization").unwrap()[0],
b"AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678"
);
assert_eq!(cr.body_sha256(), "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
let required_headers = SignedHeaderRequirements::default();
let params = cr.get_auth_parameters(&required_headers).unwrap();
let _ = format!("{:?}", params);
assert_eq!(params.signed_headers, vec!["date", "host"]);
}
#[test_log::test]
fn test_bad_form_urlencoded_charset() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; hello=world; charset=foobar")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from_static(b"foo=ba\x80r"))
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::InvalidBodyEncoding(_) = e {
assert_eq!(e.to_string(), "application/x-www-form-urlencoded body uses unsupported charset 'foobar'");
assert_eq!(e.error_code(), "InvalidBodyEncoding");
assert_eq!(e.http_status(), 400);
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
fn test_empty_form() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from_static(b""))
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
assert!(cr.query_parameters().is_empty());
}
#[test_log::test]
fn test_default_form_encoding() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
assert_eq!(cr.query_parameters.get("foo").unwrap(), &vec!["bar%C3%BF".to_string()]);
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; hello=world")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
assert_eq!(cr.query_parameters.get("foo").unwrap(), &vec!["bar%C3%BF".to_string()]);
}
#[test_log::test]
fn test_no_map_form() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar\xc3\xbf".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::default()).unwrap();
assert!(cr.query_parameters.get("foo").is_none());
}
#[test_log::test]
fn test_bad_debug_headers() {
let mut headers = HashMap::new();
headers.insert("Host".to_string(), vec![vec![0xffu8]]);
let debug = debug_headers(&headers);
assert_eq!(debug, "Host: [255]");
assert_eq!(debug_headers(&HashMap::new()), "");
}
#[test_log::test]
fn test_bad_form_encoding() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=ba\x80r".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::InvalidBodyEncoding(msg) = e {
assert_eq!(
msg.as_str(),
"Invalid body data encountered parsing application/x-www-form-urlencoded with charset 'utf-8'"
)
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
fn test_bad_form_charset_param() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let (_, _, body) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
assert_eq!(body.as_ref(), b"");
}
#[test_log::test]
fn test_bad_form_urlencoding() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar%yy".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::MalformedQueryString(msg) = e {
assert_eq!(msg.as_str(), "Illegal hex character in escape % pattern: %yy")
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo%tt=bar".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::MalformedQueryString(msg) = e {
assert_eq!(msg.as_str(), "Illegal hex character in escape % pattern: %tt")
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/x-www-form-urlencoded; charset=utf-8")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.body(Bytes::from(b"foo=bar%y".to_vec()))
.unwrap();
let (parts, body) = request.into_parts();
let e = CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap_err();
if let SignatureError::MalformedQueryString(msg) = e {
assert_eq!(msg.as_str(), "Incomplete trailing escape % sequence")
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
fn test_u8_to_upper_hex() {
for i in 0..=255 {
let result = u8_to_upper_hex(i);
assert_eq!(String::from_utf8_lossy(result.as_slice()), format!("{:02X}", i));
}
}
#[test_log::test]
fn test_missing_auth_header_components() {
for i in 0..15 {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let mut error_messages = Vec::with_capacity(4);
let mut auth_header = Vec::with_capacity(3);
if i & 1 != 0 {
auth_header.push(" Credential=1234 ");
} else {
error_messages.push("Authorization header requires 'Credential' parameter.");
}
if i & 2 != 0 {
auth_header.push(" Signature=5678 ");
} else {
error_messages.push("Authorization header requires 'Signature' parameter.");
}
if i & 4 != 0 {
auth_header.push(" SignedHeaders=host;x-amz-date");
} else {
error_messages.push("Authorization header requires 'SignedHeaders' parameter.");
}
let auth_header = format!("AWS4-HMAC-SHA256 {}", auth_header.join(", "));
let builder = Request::builder().method(Method::GET).uri(uri).header("authorization", auth_header);
let builder = if i & 8 != 0 {
builder.header("x-amz-date", "20150830T123600Z")
} else {
error_messages
.push("Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.");
builder
};
let request = builder.body(Bytes::new()).unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::IncompleteSignature(msg) = e {
let error_message = format!("{} Authorization=AWS4-HMAC-SHA256", error_messages.join(" "));
assert_eq!(msg.as_str(), error_message.as_str());
} else {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test_log::test]
fn test_malformed_auth_header() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("x-amz-date", "20150830T123600Z")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeadersdate;host")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::IncompleteSignature(msg) = e {
assert_eq!(msg.as_str(), "'SignedHeadersdate;host' not a valid key=value pair (missing equal-sign) in Authorization header: 'AWS4-HMAC-SHA256 Credential=1234, SignedHeadersdate;host'");
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
fn test_missing_auth_query_components() {
for i in 0..15 {
let mut error_messages = Vec::with_capacity(4);
let mut auth_query = Vec::with_capacity(5);
auth_query.push("X-Amz-Algorithm=AWS4-HMAC-SHA256");
if i & 1 != 0 {
auth_query.push("X-Amz-Credential=1234");
} else {
error_messages.push("AWS query-string parameters must include 'X-Amz-Credential'.");
}
if i & 2 != 0 {
auth_query.push("X-Amz-Signature=5678");
} else {
error_messages.push("AWS query-string parameters must include 'X-Amz-Signature'.");
}
if i & 4 != 0 {
auth_query.push("X-Amz-SignedHeaders=host;x-amz-date");
} else {
error_messages.push("AWS query-string parameters must include 'X-Amz-SignedHeaders'.");
}
if i & 8 != 0 {
auth_query.push("X-Amz-Date=20150830T123600Z")
} else {
error_messages.push("AWS query-string parameters must include 'X-Amz-Date'.");
};
let query_string = auth_query.join("&");
let pq = PathAndQuery::from_maybe_shared(format!("/?{}", query_string)).unwrap();
let uri = Uri::builder().path_and_query(pq).build().unwrap();
let builder = Request::builder().method(Method::GET).uri(uri);
let request = builder.body(Bytes::new()).unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::IncompleteSignature(msg) = e {
let error_message = format!("{} Re-examine the query-string parameters.", error_messages.join(" "));
assert_eq!(msg.as_str(), error_message.as_str());
} else {
panic!("Unexpected error: {:?}", e);
}
}
}
#[test_log::test]
fn test_auth_component_ordering() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=date;host, Signature=5678, Credential=ABCD, SignedHeaders=foo;bar;host, Signature=DEFG")
.header("authorization", "AWS3 Credential=1234, SignedHeaders=date;host, Signature=5678, Credential=ABCD, SignedHeaders=foo;bar;host, Signature=DEFG")
.header("host", "example.amazonaws.com")
.header("x-amz-date", "20150830T123600Z")
.header("x-amz-date", "20161231T235959Z")
.header("x-amz-security-token", "Test1")
.header("x-amz-security-token", "Test2")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let auth = cr.get_auth_parameters(&required_headers).unwrap();
assert_eq!(auth.builder.get_credential(), &Some("ABCD".to_string()));
assert_eq!(auth.builder.get_signature(), &Some("DEFG".to_string()));
assert_eq!(auth.signed_headers, vec!["bar", "foo", "host"]);
assert_eq!(auth.builder.get_session_token(), &Some(Some("Test1".to_string())));
assert_eq!(auth.timestamp_str, "20150830T123600Z");
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Algorithm=AWS3&X-Amz-Credential=1234&X-Amz-SignedHeaders=date%3Bhost&X-Amz-Signature=5678&X-Amz-Security-Token=Test1&X-Amz-Date=20150830T123600Z&X-Amz-Credential=ABCD&X-Amz-SignedHeaders=foo%3Bbar%3Bhost&X-Amz-Signature=DEFG&X-Amz-SecurityToken=Test2&X-Amz-Date=20161231T235959Z")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("host", "example.amazonaws.com")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let auth = cr.get_auth_parameters(&required_headers).unwrap();
assert_eq!(auth.builder.get_credential(), &Some("1234".to_string()));
assert_eq!(auth.builder.get_signature(), &Some("5678".to_string()));
assert_eq!(auth.builder.get_session_token(), &Some(Some("Test1".to_string())));
assert_eq!(auth.timestamp_str, "20150830T123600Z");
assert_eq!(auth.signed_headers, vec!["date", "host"]);
let required_headers = SignedHeaderRequirements::default();
let auth = cr.get_authenticator(&required_headers);
assert!(auth.is_ok());
}
#[test_log::test]
fn test_signed_headers_missing_host() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let required_headers2 = required_headers.clone();
assert_eq!(&required_headers, &required_headers2);
assert_eq!(format!("{:?}", required_headers), format!("{:?}", required_headers2));
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::SignatureDoesNotMatch(msg) = e {
let msg = msg.expect("Expected error message");
assert_eq!(msg.as_str(), "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.");
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=&X-Amz-Signature=5678&X-Amz-Date=20150830T123600Z&X-Amz-SecurityToken=Foo")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("host", "example.amazonaws.com")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::SignatureDoesNotMatch(msg) = e {
let msg = msg.expect("Expected error message");
assert_eq!(msg.as_str(), "'Host' or ':authority' must be a 'SignedHeader' in the AWS Authorization.");
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
fn test_missing_signed_header() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.header(
"authorization",
"AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=a;host;x-amz-date, Signature=5678",
)
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let a = cr.get_auth_parameters(&required_headers).unwrap();
assert_eq!(a.signed_headers, vec!["a", "host", "x-amz-date"]);
let cr_bytes = cr.canonical_request(&a.signed_headers);
assert!(!cr_bytes.is_empty());
}
#[test_log::test]
fn test_bad_algorithms() {
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::POST)
.uri(uri)
.header("content-type", "application/json")
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.body(Bytes::from_static(b"{}"))
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::MissingAuthenticationToken(msg) = e {
assert_eq!(msg.as_str(), "Request is missing Authentication Token");
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=&X-Amz-Signature=5678&X-Amz-Date=20150830T123600Z&X-Amz-SecurityToken=Foo")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS4-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::SignatureDoesNotMatch(ref msg) = e {
assert!(msg.is_none());
assert_eq!(e.to_string(), "");
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("authorization", "AWS3-HMAC-SHA256 Credential=1234, SignedHeaders=x-amz-date, Signature=5678")
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::IncompleteSignature(msg) = e {
assert_eq!(msg.as_str(), "Unsupported AWS 'algorithm': 'AWS3-HMAC-SHA256'.");
} else {
panic!("Unexpected error: {:?}", e);
}
let uri = Uri::builder().path_and_query(PathAndQuery::from_static("/?X-Amz-Algorithm=AWS3-HMAC-SHA256&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=1234&X-Amz-SignedHeaders=date%3Bhost&X-Amz-Signature=5678&X-Amz-Security-Token=Test1&X-Amz-Date=20150830T123600Z&X-Amz-Credential=ABCD&X-Amz-SignedHeaders=foo%3Bbar%3Bhost&X-Amz-Signature=DEFG&X-Amz-SecurityToken=Test2&X-Amz-Date=20161231T235959Z")).build().unwrap();
let request = Request::builder()
.method(Method::GET)
.uri(uri)
.header("x-amz-date", "20150830T123600Z")
.header("host", "example.amazonaws.com")
.body(Bytes::new())
.unwrap();
let (parts, body) = request.into_parts();
let (cr, _, _) =
CanonicalRequest::from_request_parts(parts, body, SignatureOptions::url_encode_form()).unwrap();
let required_headers = SignedHeaderRequirements::default();
let e = cr.get_auth_parameters(&required_headers).unwrap_err();
if let SignatureError::MissingAuthenticationToken(msg) = e {
assert_eq!(msg.as_str(), "Request is missing Authentication Token");
} else {
panic!("Unexpected error: {:?}", e);
}
}
#[test_log::test]
#[should_panic]
fn unescape_uri_encoding_invalid_panics() {
unescape_uri_encoding("%YY");
}
}