1use std::collections::BTreeMap;
4
5use base64::Engine;
6use base64::engine::general_purpose::STANDARD as BASE64;
7use hmac::{Hmac, Mac};
8use sha1::Sha1;
9use sha2::Sha256;
10
11use crate::error::{Result, StsError};
12
13type HmacSha1 = Hmac<Sha1>;
14type HmacSha256 = Hmac<Sha256>;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum SignatureVersion {
22 #[default]
24 V1_0 = 1,
25 V2_0 = 2,
27}
28
29impl SignatureVersion {
30 pub fn as_method_str(&self) -> &'static str {
32 match self {
33 SignatureVersion::V1_0 => "HMAC-SHA1",
34 SignatureVersion::V2_0 => "HMAC-SHA256",
35 }
36 }
37
38 pub fn as_version_str(&self) -> &'static str {
40 match self {
41 SignatureVersion::V1_0 => "1.0",
42 SignatureVersion::V2_0 => "2.0",
43 }
44 }
45}
46
47pub(crate) fn percent_encode(s: &str) -> String {
56 const HEX_DIGITS: &[u8; 16] = b"0123456789ABCDEF";
58
59 let mut encoded = String::with_capacity(s.len() * 3);
61
62 let mut buf = [0u8; 3];
63 for byte in s.bytes() {
64 match byte {
65 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
66 unsafe {
69 encoded.as_mut_vec().push(byte);
70 }
71 }
72 _ => {
73 buf[0] = b'%';
75 buf[1] = HEX_DIGITS[(byte >> 4) as usize];
76 buf[2] = HEX_DIGITS[(byte & 0x0F) as usize];
77 unsafe {
80 encoded.push_str(std::str::from_utf8_unchecked(&buf));
81 }
82 }
83 }
84 }
85 encoded
86}
87
88pub(crate) fn sign_request(
104 params: &BTreeMap<String, String>,
105 access_key_secret: &str,
106 http_method: &str,
107 version: SignatureVersion,
108) -> Result<String> {
109 let canonical_query: String = params
111 .iter()
112 .map(|(k, v)| format!("{}={}", percent_encode(k), percent_encode(v)))
113 .collect::<Vec<_>>()
114 .join("&");
115
116 let string_to_sign = format!(
118 "{}&{}&{}",
119 http_method,
120 percent_encode("/"),
121 percent_encode(&canonical_query)
122 );
123
124 let signing_key = format!("{}&", access_key_secret);
126
127 let signature = match version {
128 SignatureVersion::V1_0 => {
129 let mut mac = HmacSha1::new_from_slice(signing_key.as_bytes())
130 .map_err(|e| StsError::Signature(format!("HMAC-SHA1 key error: {}", e)))?;
131 mac.update(string_to_sign.as_bytes());
132 BASE64.encode(mac.finalize().into_bytes())
133 }
134 SignatureVersion::V2_0 => {
135 let mut mac = HmacSha256::new_from_slice(signing_key.as_bytes())
136 .map_err(|e| StsError::Signature(format!("HMAC-SHA256 key error: {}", e)))?;
137 mac.update(string_to_sign.as_bytes());
138 BASE64.encode(mac.finalize().into_bytes())
139 }
140 };
141
142 Ok(signature)
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn percent_encode_unreserved_chars() {
151 assert_eq!(percent_encode("abcXYZ019"), "abcXYZ019");
152 assert_eq!(percent_encode("-._~"), "-._~");
153 }
154
155 #[test]
156 fn percent_encode_spaces() {
157 assert_eq!(percent_encode("hello world"), "hello%20world");
158 }
159
160 #[test]
161 fn percent_encode_special_chars() {
162 assert_eq!(percent_encode("/"), "%2F");
163 assert_eq!(percent_encode("="), "%3D");
164 assert_eq!(percent_encode("&"), "%26");
165 assert_eq!(percent_encode("+"), "%2B");
166 assert_eq!(percent_encode("*"), "%2A");
167 }
168
169 #[test]
170 fn percent_encode_chinese() {
171 let encoded = percent_encode("中文");
172 assert_eq!(encoded, "%E4%B8%AD%E6%96%87");
173 }
174
175 #[test]
176 fn percent_encode_all_unreserved() {
177 let unreserved = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
179 assert_eq!(percent_encode(unreserved), unreserved);
180 }
181
182 #[test]
183 fn percent_encode_empty() {
184 assert_eq!(percent_encode(""), "");
185 }
186
187 #[test]
188 fn percent_encode_reserved_chars() {
189 assert_eq!(percent_encode("!"), "%21");
190 assert_eq!(percent_encode("\""), "%22");
191 assert_eq!(percent_encode("#"), "%23");
192 assert_eq!(percent_encode("$"), "%24");
193 assert_eq!(percent_encode("%"), "%25");
194 assert_eq!(percent_encode("&"), "%26");
195 assert_eq!(percent_encode("'"), "%27");
196 assert_eq!(percent_encode("("), "%28");
197 assert_eq!(percent_encode(")"), "%29");
198 assert_eq!(percent_encode("*"), "%2A");
199 assert_eq!(percent_encode("+"), "%2B");
200 assert_eq!(percent_encode(","), "%2C");
201 assert_eq!(percent_encode(":"), "%3A");
202 assert_eq!(percent_encode(";"), "%3B");
203 assert_eq!(percent_encode("<"), "%3C");
204 assert_eq!(percent_encode(">"), "%3E");
205 assert_eq!(percent_encode("?"), "%3F");
206 assert_eq!(percent_encode("@"), "%40");
207 assert_eq!(percent_encode("["), "%5B");
208 assert_eq!(percent_encode("]"), "%5D");
209 }
210
211 #[test]
212 fn percent_encode_mixed() {
213 assert_eq!(percent_encode("test@example.com"), "test%40example.com");
214 assert_eq!(percent_encode("a/b c"), "a%2Fb%20c");
215 assert_eq!(percent_encode("100%"), "100%25");
216 }
217
218 #[test]
219 fn percent_encode_uppercase_hex() {
220 let encoded = percent_encode("\x00");
222 assert_eq!(encoded, "%00");
223 let encoded = percent_encode("\u{00FF}");
224 assert_eq!(encoded, "%C3%BF");
225 }
226
227 #[test]
228 fn percent_encode_byte_0x7f() {
229 assert_eq!(percent_encode("\x7F"), "%7F");
231 }
232
233 #[test]
234 fn percent_encode_performance_no_realloc() {
235 let input = "Action=AssumeRole&Version=2015-04-01";
237 let encoded = percent_encode(input);
238 assert!(encoded.contains("Action"));
240 assert!(encoded.contains("%3D")); assert!(encoded.contains("%26")); }
243
244 #[test]
245 fn percent_encode_multibyte_sequences() {
246 assert_eq!(percent_encode("テスト"), "%E3%83%86%E3%82%B9%E3%83%88");
248 assert_eq!(
250 percent_encode("السلام"),
251 "%D8%A7%D9%84%D8%B3%D9%84%D8%A7%D9%85"
252 );
253 }
254
255 #[test]
256 fn sign_request_sha256_deterministic() {
257 let mut params = BTreeMap::new();
258 params.insert("Action".to_string(), "AssumeRole".to_string());
259 params.insert("Format".to_string(), "JSON".to_string());
260 params.insert("Version".to_string(), "2015-04-01".to_string());
261 params.insert("AccessKeyId".to_string(), "testid".to_string());
262 params.insert("SignatureMethod".to_string(), "HMAC-SHA256".to_string());
263 params.insert("SignatureVersion".to_string(), "2.0".to_string());
264 params.insert("SignatureNonce".to_string(), "fixed-nonce".to_string());
265 params.insert("Timestamp".to_string(), "2024-01-01T00:00:00Z".to_string());
266 params.insert(
267 "RoleArn".to_string(),
268 "acs:ram::123456:role/test".to_string(),
269 );
270 params.insert("RoleSessionName".to_string(), "session".to_string());
271
272 let sig1 = sign_request(¶ms, "testsecret", "POST", SignatureVersion::V2_0).unwrap();
273 let sig2 = sign_request(¶ms, "testsecret", "POST", SignatureVersion::V2_0).unwrap();
274 assert_eq!(sig1, sig2, "SHA-256 signature must be deterministic");
275 assert!(!sig1.is_empty());
276 }
277
278 #[test]
279 fn sign_request_different_secrets_differ() {
280 let mut params = BTreeMap::new();
281 params.insert("Action".to_string(), "GetCallerIdentity".to_string());
282 params.insert("Format".to_string(), "JSON".to_string());
283
284 let sig1 = sign_request(¶ms, "secret1", "POST", SignatureVersion::V2_0).unwrap();
285 let sig2 = sign_request(¶ms, "secret2", "POST", SignatureVersion::V2_0).unwrap();
286 assert_ne!(sig1, sig2);
287 }
288
289 #[test]
290 fn sign_request_different_methods_differ() {
291 let mut params = BTreeMap::new();
292 params.insert("Action".to_string(), "GetCallerIdentity".to_string());
293
294 let sig_post = sign_request(¶ms, "secret", "POST", SignatureVersion::V2_0).unwrap();
295 let sig_get = sign_request(¶ms, "secret", "GET", SignatureVersion::V2_0).unwrap();
296 assert_ne!(sig_post, sig_get);
297 }
298
299 #[test]
300 fn sign_request_is_base64() {
301 let mut params = BTreeMap::new();
302 params.insert("Action".to_string(), "Test".to_string());
303
304 let sig = sign_request(¶ms, "key", "POST", SignatureVersion::V2_0).unwrap();
305 assert!(BASE64.decode(&sig).is_ok());
306 }
307
308 #[test]
309 fn signature_version_default() {
310 assert_eq!(SignatureVersion::default(), SignatureVersion::V1_0);
311 }
312
313 #[test]
314 fn signature_version_strings() {
315 assert_eq!(SignatureVersion::V1_0.as_method_str(), "HMAC-SHA1");
316 assert_eq!(SignatureVersion::V1_0.as_version_str(), "1.0");
317 assert_eq!(SignatureVersion::V2_0.as_method_str(), "HMAC-SHA256");
318 assert_eq!(SignatureVersion::V2_0.as_version_str(), "2.0");
319 }
320
321 #[test]
322 fn sign_request_sha1_deterministic() {
323 let mut params = BTreeMap::new();
324 params.insert("Action".to_string(), "AssumeRole".to_string());
325 params.insert("Format".to_string(), "JSON".to_string());
326 params.insert("Version".to_string(), "2015-04-01".to_string());
327 params.insert("AccessKeyId".to_string(), "testid".to_string());
328 params.insert("SignatureMethod".to_string(), "HMAC-SHA1".to_string());
329 params.insert("SignatureVersion".to_string(), "1.0".to_string());
330 params.insert("SignatureNonce".to_string(), "fixed-nonce".to_string());
331 params.insert("Timestamp".to_string(), "2024-01-01T00:00:00Z".to_string());
332 params.insert(
333 "RoleArn".to_string(),
334 "acs:ram::123456:role/test".to_string(),
335 );
336 params.insert("RoleSessionName".to_string(), "session".to_string());
337
338 let sig1 = sign_request(¶ms, "testsecret", "POST", SignatureVersion::V1_0).unwrap();
339 let sig2 = sign_request(¶ms, "testsecret", "POST", SignatureVersion::V1_0).unwrap();
340 assert_eq!(sig1, sig2, "SHA-1 signature must be deterministic");
341 assert!(!sig1.is_empty());
342 }
343}