aws_sigv4/sign/
v4.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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/// HashedPayload = Lowercase(HexEncode(Hash(requestPayload)))
19#[allow(dead_code)] // Unused when compiling without certain features
20pub(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
26/// Calculates a Sigv4 signature
27pub 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
34/// Generates a signing key for Sigv4
35pub fn generate_signing_key(
36    secret: &str,
37    time: SystemTime,
38    region: &str,
39    service: &str,
40) -> impl AsRef<[u8]> {
41    // kSecret = your secret access key
42    // kDate = HMAC("AWS4" + kSecret, Date)
43    // kRegion = HMAC(kDate, Region)
44    // kService = HMAC(kRegion, Service)
45    // kSigning = HMAC(kService, "aws4_request")
46
47    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    // sign region
54    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    // sign service
59    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    // sign request
64    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/// Parameters to use when signing.
70#[derive(Debug)]
71#[non_exhaustive]
72pub struct SigningParams<'a, S> {
73    /// The identity to use when signing a request
74    pub(crate) identity: &'a Identity,
75
76    /// Region to sign for.
77    pub(crate) region: &'a str,
78    /// Service Name to sign for.
79    ///
80    /// NOTE: Endpoint resolution rules may specify a name that differs from the typical service name.
81    pub(crate) name: &'a str,
82    /// Timestamp to use in the signature (should be `SystemTime::now()` unless testing).
83    pub(crate) time: SystemTime,
84
85    /// Additional signing settings. These differ between HTTP and Event Stream.
86    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    /// Returns the region that will be used to sign SigV4 requests
95    pub fn region(&self) -> &str {
96        self.region
97    }
98
99    /// Returns the signing name that will be used to sign requests
100    pub fn name(&self) -> &str {
101        self.name
102    }
103
104    /// Return the name of the algorithm used to sign requests
105    pub fn algorithm(&self) -> &'static str {
106        HMAC_SHA256
107    }
108}
109
110impl<'a, S: Default> SigningParams<'a, S> {
111    /// Returns a builder that can create new `SigningParams`.
112    pub fn builder() -> signing_params::Builder<'a, S> {
113        Default::default()
114    }
115}
116
117/// Builder and error for creating [`SigningParams`]
118pub 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    /// [`SigningParams`] builder error
126    #[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    /// Builder that can create new [`SigningParams`]
145    #[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        /// Builds an instance of [`SigningParams`]. Will yield a [`BuildError`] if
179        /// a required argument was not given.
180        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
202/// Signs `chunk` with the given `running_signature` and `params`.
203///
204/// See [signature calculation details](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html#sigv4-chunked-body-definition).
205pub 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
219/// Signs trailing headers with the given `running_signature` and `params`.
220///
221/// See [signature calculation details](https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming-trailers.html#example-signature-calculations-trailing-header).
222pub 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}