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::date_time::format_date;
7use aws_smithy_runtime_api::client::identity::Identity;
8use hmac::{digest::FixedOutput, Hmac, Mac};
9use sha2::{Digest, Sha256};
10use std::time::SystemTime;
11
12/// HashedPayload = Lowercase(HexEncode(Hash(requestPayload)))
13#[allow(dead_code)] // Unused when compiling without certain features
14pub(crate) fn sha256_hex_string(bytes: impl AsRef<[u8]>) -> String {
15    let mut hasher = Sha256::new();
16    hasher.update(bytes);
17    hex::encode(hasher.finalize_fixed())
18}
19
20/// Calculates a Sigv4 signature
21pub fn calculate_signature(signing_key: impl AsRef<[u8]>, string_to_sign: &[u8]) -> String {
22    let mut mac = Hmac::<Sha256>::new_from_slice(signing_key.as_ref())
23        .expect("HMAC can take key of any size");
24    mac.update(string_to_sign);
25    hex::encode(mac.finalize_fixed())
26}
27
28/// Generates a signing key for Sigv4
29pub fn generate_signing_key(
30    secret: &str,
31    time: SystemTime,
32    region: &str,
33    service: &str,
34) -> impl AsRef<[u8]> {
35    // kSecret = your secret access key
36    // kDate = HMAC("AWS4" + kSecret, Date)
37    // kRegion = HMAC(kDate, Region)
38    // kService = HMAC(kRegion, Service)
39    // kSigning = HMAC(kService, "aws4_request")
40
41    let secret = format!("AWS4{}", secret);
42    let mut mac =
43        Hmac::<Sha256>::new_from_slice(secret.as_ref()).expect("HMAC can take key of any size");
44    mac.update(format_date(time).as_bytes());
45    let tag = mac.finalize_fixed();
46
47    // sign region
48    let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
49    mac.update(region.as_bytes());
50    let tag = mac.finalize_fixed();
51
52    // sign service
53    let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
54    mac.update(service.as_bytes());
55    let tag = mac.finalize_fixed();
56
57    // sign request
58    let mut mac = Hmac::<Sha256>::new_from_slice(&tag).expect("HMAC can take key of any size");
59    mac.update("aws4_request".as_bytes());
60    mac.finalize_fixed()
61}
62
63/// Parameters to use when signing.
64#[derive(Debug)]
65#[non_exhaustive]
66pub struct SigningParams<'a, S> {
67    /// The identity to use when signing a request
68    pub(crate) identity: &'a Identity,
69
70    /// Region to sign for.
71    pub(crate) region: &'a str,
72    /// Service Name to sign for.
73    ///
74    /// NOTE: Endpoint resolution rules may specify a name that differs from the typical service name.
75    pub(crate) name: &'a str,
76    /// Timestamp to use in the signature (should be `SystemTime::now()` unless testing).
77    pub(crate) time: SystemTime,
78
79    /// Additional signing settings. These differ between HTTP and Event Stream.
80    pub(crate) settings: S,
81}
82
83const HMAC_256: &str = "AWS4-HMAC-SHA256";
84
85impl<'a, S> SigningParams<'a, S> {
86    /// Returns the region that will be used to sign SigV4 requests
87    pub fn region(&self) -> &str {
88        self.region
89    }
90
91    /// Returns the signing name that will be used to sign requests
92    pub fn name(&self) -> &str {
93        self.name
94    }
95
96    /// Return the name of the algorithm used to sign requests
97    pub fn algorithm(&self) -> &'static str {
98        HMAC_256
99    }
100}
101
102impl<'a, S: Default> SigningParams<'a, S> {
103    /// Returns a builder that can create new `SigningParams`.
104    pub fn builder() -> signing_params::Builder<'a, S> {
105        Default::default()
106    }
107}
108
109/// Builder and error for creating [`SigningParams`]
110pub mod signing_params {
111    use super::SigningParams;
112    use aws_smithy_runtime_api::client::identity::Identity;
113    use std::error::Error;
114    use std::fmt;
115    use std::time::SystemTime;
116
117    /// [`SigningParams`] builder error
118    #[derive(Debug)]
119    pub struct BuildError {
120        reason: &'static str,
121    }
122    impl BuildError {
123        fn new(reason: &'static str) -> Self {
124            Self { reason }
125        }
126    }
127
128    impl fmt::Display for BuildError {
129        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130            write!(f, "{}", self.reason)
131        }
132    }
133
134    impl Error for BuildError {}
135
136    /// Builder that can create new [`SigningParams`]
137    #[derive(Debug, Default)]
138    pub struct Builder<'a, S> {
139        identity: Option<&'a Identity>,
140        region: Option<&'a str>,
141        name: Option<&'a str>,
142        time: Option<SystemTime>,
143        settings: Option<S>,
144    }
145
146    impl<'a, S> Builder<'a, S> {
147        builder_methods!(
148            set_identity,
149            identity,
150            &'a Identity,
151            "Sets the identity (required)",
152            set_region,
153            region,
154            &'a str,
155            "Sets the region (required)",
156            set_name,
157            name,
158            &'a str,
159            "Sets the name (required)",
160            set_time,
161            time,
162            SystemTime,
163            "Sets the time to be used in the signature (required)",
164            set_settings,
165            settings,
166            S,
167            "Sets additional signing settings (required)"
168        );
169
170        /// Builds an instance of [`SigningParams`]. Will yield a [`BuildError`] if
171        /// a required argument was not given.
172        pub fn build(self) -> Result<SigningParams<'a, S>, BuildError> {
173            Ok(SigningParams {
174                identity: self
175                    .identity
176                    .ok_or_else(|| BuildError::new("identity is required"))?,
177                region: self
178                    .region
179                    .ok_or_else(|| BuildError::new("region is required"))?,
180                name: self
181                    .name
182                    .ok_or_else(|| BuildError::new("name is required"))?,
183                time: self
184                    .time
185                    .ok_or_else(|| BuildError::new("time is required"))?,
186                settings: self
187                    .settings
188                    .ok_or_else(|| BuildError::new("settings are required"))?,
189            })
190        }
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::{calculate_signature, generate_signing_key, sha256_hex_string};
197    use crate::date_time::test_parsers::parse_date_time;
198    use crate::http_request::test;
199
200    #[test]
201    fn test_signature_calculation() {
202        let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
203        let creq = test::v4::test_canonical_request("iam");
204        let time = parse_date_time("20150830T123600Z").unwrap();
205
206        let derived_key = generate_signing_key(secret, time, "us-east-1", "iam");
207        let signature = calculate_signature(derived_key, creq.as_bytes());
208
209        let expected = "5d672d79c15b13162d9279b0855cfba6789a8edb4c82c400e06b5924a6f2b5d7";
210        assert_eq!(expected, &signature);
211    }
212
213    #[test]
214    fn sign_payload_empty_string() {
215        let expected = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
216        let actual = sha256_hex_string([]);
217        assert_eq!(expected, actual);
218    }
219}