1use crate::{
7 date_time::{format_date, format_date_time},
8 http_request::SigningError,
9 SigningOutput,
10};
11use aws_credential_types::Credentials;
12use aws_smithy_runtime_api::{client::identity::Identity, http::Headers};
13use bytes::Bytes;
14use hmac::{digest::FixedOutput, Hmac, Mac};
15use sha2::{Digest, Sha256};
16use std::time::SystemTime;
17
18#[allow(dead_code)] pub(crate) fn sha256_hex_string(bytes: impl AsRef<[u8]>) -> String {
21 let mut hasher = Sha256::new();
22 hasher.update(bytes);
23 hex::encode(hasher.finalize_fixed())
24}
25
26pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String {
28 let mut mac = Hmac::<Sha256>::new_from_slice(signing_key.as_ref())
29 .expect("HMAC can take key of any size");
30 mac.update(string_to_sign);
31 hex::encode(mac.finalize_fixed())
32}
33
34pub fn generate_signing_key(
36 secret: &str,
37 time: SystemTime,
38 region: &str,
39 service: &str,
40) -> impl AsRef<[u8]> {
41 let secret = format!("AWS4{secret}");
48 let mut mac =
49 Hmac::<Sha256>::new_from_slice(secret.as_ref()).expect("HMAC can take key of any size");
50 mac.update(format_date(time).as_bytes());
51 let tag = mac.finalize_fixed();
52
53 let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
55 mac.update(region.as_bytes());
56 let tag = mac.finalize_fixed();
57
58 let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
60 mac.update(service.as_bytes());
61 let tag = mac.finalize_fixed();
62
63 let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
65 mac.update("aws4_request".as_bytes());
66 mac.finalize_fixed()
67}
68
69#[derive(Debug)]
71#[non_exhaustive]
72pub struct SigningParams<'a, S> {
73 pub(crate) identity: &'a Identity,
75
76 pub(crate) region: &'a str,
78 pub(crate) name: &'a str,
82 pub(crate) time: SystemTime,
84
85 pub(crate) settings: S,
87}
88
89pub(crate) const HMAC_SHA256: &str = "AWS4-HMAC-SHA256";
90const HMAC_SHA256_PAYLOAD: &str = "AWS4-HMAC-SHA256-PAYLOAD";
91const HMAC_SHA256_TRAILER: &str = "AWS4-HMAC-SHA256-TRAILER";
92
93impl<S> SigningParams<'_, S> {
94 pub fn region(&self) -> &str {
96 self.region
97 }
98
99 pub fn name(&self) -> &str {
101 self.name
102 }
103
104 pub fn algorithm(&self) -> &'static str {
106 HMAC_SHA256
107 }
108}
109
110impl<'a, S: Default> SigningParams<'a, S> {
111 pub fn builder() -> signing_params::Builder<'a, S> {
113 Default::default()
114 }
115}
116
117pub mod signing_params {
119 use super::SigningParams;
120 use aws_smithy_runtime_api::client::identity::Identity;
121 use std::error::Error;
122 use std::fmt;
123 use std::time::SystemTime;
124
125 #[derive(Debug)]
127 pub struct BuildError {
128 reason: &'static str,
129 }
130 impl BuildError {
131 fn new(reason: &'static str) -> Self {
132 Self { reason }
133 }
134 }
135
136 impl fmt::Display for BuildError {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 write!(f, "{}", self.reason)
139 }
140 }
141
142 impl Error for BuildError {}
143
144 #[derive(Debug, Default)]
146 pub struct Builder<'a, S> {
147 identity: Option<&'a Identity>,
148 region: Option<&'a str>,
149 name: Option<&'a str>,
150 time: Option<SystemTime>,
151 settings: Option<S>,
152 }
153
154 impl<'a, S> Builder<'a, S> {
155 builder_methods!(
156 set_identity,
157 identity,
158 &'a Identity,
159 "Sets the identity (required)",
160 set_region,
161 region,
162 &'a str,
163 "Sets the region (required)",
164 set_name,
165 name,
166 &'a str,
167 "Sets the name (required)",
168 set_time,
169 time,
170 SystemTime,
171 "Sets the time to be used in the signature (required)",
172 set_settings,
173 settings,
174 S,
175 "Sets additional signing settings (required)"
176 );
177
178 pub fn build(self) -> Result<SigningParams<'a, S>, BuildError> {
181 Ok(SigningParams {
182 identity: self
183 .identity
184 .ok_or_else(|| BuildError::new("identity is required"))?,
185 region: self
186 .region
187 .ok_or_else(|| BuildError::new("region is required"))?,
188 name: self
189 .name
190 .ok_or_else(|| BuildError::new("name is required"))?,
191 time: self
192 .time
193 .ok_or_else(|| BuildError::new("time is required"))?,
194 settings: self
195 .settings
196 .ok_or_else(|| BuildError::new("settings are required"))?,
197 })
198 }
199 }
200}
201
202pub fn sign_chunk<'a, S>(
206 chunk: &Bytes,
207 running_signature: &'a str,
208 params: &'a SigningParams<'a, S>,
209) -> Result<SigningOutput<()>, SigningError> {
210 let payload_hash = format!("{}\n{}", sha256_hex_string([]), sha256_hex_string(chunk));
211 sign_streaming_payload(
212 HMAC_SHA256_PAYLOAD,
213 running_signature,
214 params,
215 &payload_hash,
216 )
217}
218
219pub fn sign_trailer<'a, S>(
223 headers: &'a Headers,
224 running_signature: &'a str,
225 params: &'a SigningParams<'a, S>,
226) -> Result<SigningOutput<()>, SigningError> {
227 fn canonical_headers(headers: &Headers) -> Vec<u8> {
228 let mut sorted_headers: Vec<_> = headers.iter().collect();
229 sorted_headers.sort_by_key(|(name, _)| name.to_lowercase());
230 let mut buf = Vec::with_capacity(sorted_headers.len());
231 for (name, value) in sorted_headers.iter() {
232 buf.extend_from_slice(name.to_lowercase().as_bytes());
233 buf.extend_from_slice(b":");
234 buf.extend_from_slice(value.trim().as_bytes());
235 buf.extend_from_slice(b"\n");
236 }
237 buf
238 }
239
240 let payload_hash = sha256_hex_string(canonical_headers(headers));
241 sign_streaming_payload(
242 HMAC_SHA256_TRAILER,
243 running_signature,
244 params,
245 &payload_hash,
246 )
247}
248
249fn sign_streaming_payload<'a, S>(
250 algorithm: &str,
251 running_signature: &'a str,
252 params: &'a SigningParams<'a, S>,
253 payload_hash: &str,
254) -> Result<SigningOutput<()>, SigningError> {
255 let creds = params
256 .identity
257 .data::<Credentials>()
258 .expect("identity must contain credentials");
259
260 let signing_key = generate_signing_key(
261 creds.secret_access_key(),
262 params.time,
263 params.region,
264 params.name,
265 );
266
267 let scope = format!(
268 "{}/{}/{}/aws4_request",
269 format_date(params.time),
270 params.region,
271 params.name
272 );
273
274 let string_to_sign = format!(
275 "{}\n{}\n{}\n{}\n{}",
276 algorithm,
277 format_date_time(params.time),
278 scope,
279 running_signature,
280 payload_hash,
281 );
282
283 let signature = calculate_signature(signing_key, string_to_sign.as_bytes());
284 Ok(SigningOutput::new((), signature))
285}
286
287#[cfg(test)]
288mod tests {
289 use super::{calculate_signature, generate_signing_key, sha256_hex_string};
290 use crate::date_time::test_parsers::parse_date_time;
291
292 #[test]
293 fn test_signature_calculation() {
294 let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
295 let creq = r#"AWS4-HMAC-SHA256
29620150830T123600Z
29720150830/us-east-1/iam/aws4_request
298f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59"#;
299 let time = parse_date_time("20150830T123600Z").unwrap();
300
301 let derived_key = generate_signing_key(secret, time, "us-east-1", "iam");
302 let signature = calculate_signature(derived_key, creq.as_bytes());
303
304 let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7";
305 assert_eq!(expected, &signature);
306 }
307
308 #[test]
309 fn sign_payload_empty_string() {
310 let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
311 let actual = sha256_hex_string([]);
312 assert_eq!(expected, actual);
313 }
314}