common_s3_headers/
aws_math.rs

1//! AWS-specific math.
2//!
3//! Crypto goes here.
4//!
5use crate::aws_format::{query_params_string, security_token_string, to_short_datetime};
6use hmac::{Hmac, Mac};
7use sha2::{Digest, Sha256};
8use std::borrow::Cow;
9use time::OffsetDateTime;
10
11// Create alias for HMAC-SHA256
12// @see https://docs.rs/hmac/latest/hmac/
13pub type HmacSha256 = Hmac<Sha256>;
14#[allow(dead_code)]
15type HeaderMap<'a> = Vec<(Cow<'a, str>, Cow<'a, str>)>;
16
17/// Gets the SHA256 hash of the value. Returns a hex string. Never panics.
18///
19/// # Examples
20///
21/// ```
22/// use common_s3_headers::aws_math::get_sha256;
23///
24/// let result = get_sha256(b"");
25/// assert_eq!(result, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
26/// ```
27///
28/// ```
29/// use common_s3_headers::aws_math::get_sha256;
30///
31/// let result = get_sha256(b"hello world");
32/// assert_eq!(result, "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9");
33/// ```
34///
35pub fn get_sha256(value: &[u8]) -> String {
36  // There is a Rust analyzer bug that forces us to use `as Digest` here.
37  let mut hasher = <Sha256 as Digest>::new();
38  hasher.update(value);
39  hex::encode(hasher.finalize().as_slice())
40}
41
42/// Signs data with the key using Hmac<Sha256>. Never panics.
43pub fn sign(key: &[u8], data: &[u8]) -> HmacSha256 {
44  // Never panics; the algorithm we're using can accept any length of bytes.
45  let mut hmac: HmacSha256 = Hmac::new_from_slice(key).expect("HMAC can take key of any size");
46  hmac.update(data);
47  hmac
48}
49
50/// AWS uses the previous HMAC to calculate each new item.
51///
52/// @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
53/// @private
54fn fold_hmacs(items: &[&[u8]]) -> Vec<u8> {
55  assert!(items.len() > 1);
56
57  let mut hmac: HmacSha256 = sign(items[0], items[1]);
58  for data in items[2..].iter() {
59    hmac = sign(&hmac.finalize().into_bytes(), data);
60  }
61  hmac.finalize().into_bytes().to_vec()
62}
63
64/// Generate the AWS signing key, derived from the secret key, date, region,
65/// and service name.
66///
67/// # Examples
68///
69/// ```
70/// use common_s3_headers::aws_math::get_signature_key;
71/// use time::OffsetDateTime;
72///
73/// let datetime = OffsetDateTime::from_unix_timestamp(0).unwrap();
74/// let result = get_signature_key(&datetime, "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "us-east-1", "iam");
75/// assert_eq!(result, vec![
76///  66, 8, 135, 252, 134, 148, 53, 127, 234, 31, 244, 66, 17, 242, 120, 186, 172, 171, 173, 40, 246, 5, 142, 3, 34, 117, 41, 147, 34, 13, 122, 223
77/// ]);
78/// ```
79///
80/// @see https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
81/// @see https://docs.aws.amazon.com/translate/latest/dg/examples-sigv4.html
82pub fn get_signature_key(datetime: &OffsetDateTime, secret_key: &str, region: &str, service: &str) -> Vec<u8> {
83  let secret = format!("AWS4{}", secret_key);
84  let formatted_datetime = to_short_datetime(datetime);
85
86  fold_hmacs(&[
87    secret.as_bytes(),
88    formatted_datetime.as_bytes(),
89    region.as_bytes(),
90    service.as_bytes(),
91    b"aws4_request",
92  ])
93}
94
95/// Gets the authorization header for AWS.
96///
97/// # Examples
98///
99/// ```
100/// use common_s3_headers::aws_math::authorization_query_params_no_sig;
101/// use common_s3_headers::aws_format::to_short_datetime;
102/// use time::OffsetDateTime;
103///
104/// // Preset datetime for testing.
105/// let datetime = OffsetDateTime::from_unix_timestamp(0).unwrap();
106/// let result = authorization_query_params_no_sig(
107/// "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
108/// &datetime,
109/// "us-east-1",
110/// "iam",
111/// 86400,
112/// None,
113/// None,
114/// );
115/// assert_eq!(result, "?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=wJalrXUtnFEMI%2FK7MDENG%2FbPxRfiCYEXAMPLEKEY%2F19700101%2Fus-east-1%2Fiam%2Faws4_request&X-Amz-Date=19700101T000000Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host");
116/// ```
117///
118#[allow(dead_code)]
119pub fn authorization_query_params_no_sig(
120  access_key: &str,
121  datetime: &OffsetDateTime,
122  region: &str,
123  service: &str,
124  expires: u32,
125  custom_headers: Option<&HeaderMap>,
126  token: Option<&String>,
127) -> String {
128  let signed_headers = if let Some(custom_headers) = &custom_headers {
129    let mut list = Vec::with_capacity(custom_headers.len() + 1);
130    list.push("host");
131    custom_headers.iter().for_each(|(k, _)| list.push(k));
132    list.sort();
133    list
134  } else {
135    vec!["host"]
136  };
137
138  let mut query_params = query_params_string(&signed_headers, access_key, datetime, region, service, expires);
139
140  if let Some(token) = token {
141    query_params += &security_token_string(token);
142  }
143
144  query_params
145}
146
147#[cfg(test)]
148mod tests {
149  use super::*;
150  use crate::{aws_canonical::to_canonical_headers, aws_format};
151  use common_testing::assert;
152  use hmac::{Hmac, Mac};
153  use sha2::Sha256;
154  use time::Date;
155  use url::Url;
156
157  #[test]
158  fn test_signing_key() {
159    let datetime = &Date::from_calendar_date(2015, 8.try_into().unwrap(), 30)
160      .unwrap()
161      .with_hms(0, 0, 0)
162      .unwrap()
163      .assume_utc();
164    let secret_key = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
165    let region = "us-east-1";
166    let service = "iam";
167
168    let result = get_signature_key(datetime, secret_key, region, service);
169
170    assert::equal_hex_bytes(
171      &result,
172      "c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9",
173    );
174  }
175
176  const EXPECTED_SHA: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
177
178  const EXPECTED_CANONICAL_REQUEST: &str = "GET\n\
179    /test.txt\n\
180    \n\
181    host:examplebucket.s3.amazonaws.com\n\
182    range:bytes=0-9\n\
183    x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\n\
184    x-amz-date:20130524T000000Z\n\
185    \n\
186    host;range;x-amz-content-sha256;x-amz-date\n\
187    e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
188
189  const EXPECTED_STRING_TO_SIGN: &str = "AWS4-HMAC-SHA256\n\
190    20130524T000000Z\n\
191    20130524/us-east-1/s3/aws4_request\n\
192    7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972";
193
194  #[test]
195  fn test_signing() {
196    let url = Url::parse("https://examplebucket.s3.amazonaws.com/test.txt").unwrap();
197    let headers = vec![
198      ("x-amz-date", "20130524T000000Z"),
199      ("Range", "bytes=0-9"),
200      ("Host", "examplebucket.s3.amazonaws.com"),
201      ("x-amz-content-sha256", EXPECTED_SHA),
202    ];
203    let service = "s3";
204    let canonical_headers = to_canonical_headers(&headers);
205    let canonical_string = aws_format::canonical_request_string("GET", &url, &canonical_headers, EXPECTED_SHA);
206    assert_eq!(EXPECTED_CANONICAL_REQUEST, canonical_string);
207
208    let datetime = Date::from_calendar_date(2013, 5.try_into().unwrap(), 24)
209      .unwrap()
210      .with_hms(0, 0, 0)
211      .unwrap()
212      .assume_utc();
213    let string_to_sign = aws_format::string_to_sign(&datetime, "us-east-1", service, &canonical_string);
214    assert_eq!(EXPECTED_STRING_TO_SIGN, string_to_sign);
215
216    let expected = "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41";
217    let secret = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
218    let signing_key = get_signature_key(&datetime, secret, "us-east-1", "s3");
219    let mut hmac = Hmac::<Sha256>::new_from_slice(&signing_key).unwrap();
220    hmac.update(string_to_sign.as_bytes());
221    assert_eq!(expected, hex::encode(hmac.finalize().into_bytes()));
222  }
223}