aws_sign_v4/
lib.rs

1use chrono::{DateTime, Utc};
2use http::header::HeaderMap;
3use url::Url;
4use std::collections::HashMap;
5use sha256::{digest};
6
7const SHORT_DATE: &str = "%Y%m%d";
8const LONG_DATETIME: &str = "%Y%m%dT%H%M%SZ";
9
10#[derive(Debug)]
11pub struct AwsSign<'a, T: 'a>
12where
13    &'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>,
14{
15    method: &'a str,
16    url: Url,
17    datetime: &'a DateTime<Utc>,
18    region: &'a str,
19    access_key: &'a str,
20    secret_key: &'a str,
21    headers: T,
22
23    /* 
24    service is the <aws-service-code> that can be found in the service-quotas api.
25    
26    For example, use the value `ServiceCode` for this `service` property.
27    Thus, for "Amazon Simple Storage Service (Amazon S3)", you would use value "s3"
28
29    ```
30    > aws service-quotas list-services
31    {
32        "Services": [
33            ...
34            {
35                "ServiceCode": "a4b",
36                "ServiceName": "Alexa for Business"
37            },
38            ...
39            {
40                "ServiceCode": "s3",
41                "ServiceName": "Amazon Simple Storage Service (Amazon S3)"
42            },
43            ...
44    ```
45    This is not absolute, so you might need to poke around at the service you're interesed in.
46    See:
47    [AWS General Reference -> Service endpoints and quotas](https://docs.aws.amazon.com/general/latest/gr/aws-service-information.html) - to look up "service" names and codes
48
49    added in 0.2.0
50    */
51    service: &'a str,
52
53    /// body, such as in an http POST
54    body: &'a [u8],
55}
56
57impl<'a> AwsSign<'a, HashMap<String, String>> {
58    pub fn new<B: AsRef<[u8]> + ?Sized>(
59        method: &'a str,
60        url: &'a str,
61        datetime: &'a DateTime<Utc>,
62        headers: &'a HeaderMap,
63        region: &'a str,
64        access_key: &'a str,
65        secret_key: &'a str,
66        service: &'a str,
67        body: &'a B,
68    ) -> Self {
69        let url: Url = url.parse().unwrap();
70        let headers: HashMap<String, String> = headers
71            .iter()
72            .filter_map(|(key, value)| {
73                if let Ok(value_inner) = value.to_str() {
74                    Some((key.as_str().to_owned(), value_inner.to_owned()))
75                } else {
76                    None
77                }
78            })
79            .collect();
80        Self {
81            method,
82            url,
83            datetime,
84            region,
85            access_key,
86            secret_key,
87            headers,
88            service,
89            body: body.as_ref(),
90        }
91    }
92}
93
94impl<'a, T> AwsSign<'a, T>
95where
96    &'a T: std::iter::IntoIterator<Item = (&'a String, &'a String)>,
97{
98    //Thanks https://github.com/durch/rust-s3 for the signing implementation.
99
100    pub fn canonical_header_string(&'a self) -> String {
101        let mut keyvalues = self
102            .headers
103            .into_iter()
104            .map(|(key, value)| key.to_lowercase() + ":" + value.trim())
105            .collect::<Vec<String>>();
106        keyvalues.sort();
107        keyvalues.join("\n")
108    }
109
110    pub fn signed_header_string(&'a self) -> String {
111        let mut keys = self
112            .headers
113            .into_iter()
114            .map(|(key, _)| key.to_lowercase())
115            .collect::<Vec<String>>();
116        keys.sort();
117        keys.join(";")
118    }
119
120    pub fn canonical_request(&'a self) -> String {
121        let url: &str = self.url.path().into();
122
123        format!(
124            "{method}\n{uri}\n{query_string}\n{headers}\n\n{signed}\n{sha256}",
125            method = self.method,
126            uri = url,
127            query_string = canonical_query_string(&self.url),
128            headers = self.canonical_header_string(),
129            signed = self.signed_header_string(),
130            sha256 = digest(self.body),
131        )
132    }
133    pub fn sign(&'a self) -> String {
134        let canonical = self.canonical_request();
135        let string_to_sign = string_to_sign(self.datetime, self.region, &canonical, self.service);
136        let signing_key = signing_key(self.datetime, self.secret_key, self.region, self.service);
137        let key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, &signing_key.unwrap());
138        let tag = ring::hmac::sign(&key, string_to_sign.as_bytes());
139        let signature = hex::encode(tag.as_ref());
140        let signed_headers = self.signed_header_string();
141
142        format!(
143            "AWS4-HMAC-SHA256 Credential={access_key}/{scope},\
144             SignedHeaders={signed_headers},Signature={signature}",
145            access_key = self.access_key,
146            scope = scope_string(self.datetime, self.region, self.service),
147            signed_headers = signed_headers,
148            signature = signature
149        )
150    }
151}
152
153pub fn uri_encode(string: &str, encode_slash: bool) -> String {
154    let mut result = String::with_capacity(string.len() * 2);
155    for c in string.chars() {
156        match c {
157            'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '~' | '.' => result.push(c),
158            '/' if encode_slash => result.push_str("%2F"),
159            '/' if !encode_slash => result.push('/'),
160            _ => {
161                result.push('%');
162                result.push_str(
163                    &format!("{}", c)
164                        .bytes()
165                        .map(|b| format!("{:02X}", b))
166                        .collect::<String>(),
167                );
168            }
169        }
170    }
171    result
172}
173
174pub fn canonical_query_string(uri: &Url) -> String {
175    let mut keyvalues = uri
176        .query_pairs()
177        .map(|(key, value)| uri_encode(&key, true) + "=" + &uri_encode(&value, true))
178        .collect::<Vec<String>>();
179    keyvalues.sort();
180    keyvalues.join("&")
181}
182
183pub fn scope_string(datetime: &DateTime<Utc>, region: &str, service: &str) -> String {
184    format!(
185        "{date}/{region}/{service}/aws4_request",
186        date = datetime.format(SHORT_DATE),
187        region = region,
188        service = service
189    )
190}
191
192pub fn string_to_sign(datetime: &DateTime<Utc>, region: &str, canonical_req: &str, service: &str) -> String {
193    let hash = ring::digest::digest(&ring::digest::SHA256, canonical_req.as_bytes());
194    format!(
195        "AWS4-HMAC-SHA256\n{timestamp}\n{scope}\n{hash}",
196        timestamp = datetime.format(LONG_DATETIME),
197        scope = scope_string(datetime, region, service),
198        hash = hex::encode(hash.as_ref())
199    )
200}
201
202pub fn signing_key(
203    datetime: &DateTime<Utc>,
204    secret_key: &str,
205    region: &str,
206    service: &str,
207) -> Result<Vec<u8>, String> {
208    let secret = String::from("AWS4") + secret_key;
209
210    let date_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, secret.as_bytes());
211    let date_tag = ring::hmac::sign(
212        &date_key,
213        datetime.format(SHORT_DATE).to_string().as_bytes(),
214    );
215
216    let region_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, date_tag.as_ref());
217    let region_tag = ring::hmac::sign(&region_key, region.to_string().as_bytes());
218
219    let service_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, region_tag.as_ref());
220    let service_tag = ring::hmac::sign(&service_key, service.as_bytes());
221
222    let signing_key = ring::hmac::Key::new(ring::hmac::HMAC_SHA256, service_tag.as_ref());
223    let signing_tag = ring::hmac::sign(&signing_key, b"aws4_request");
224    Ok(signing_tag.as_ref().to_vec())
225}
226
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn sample_canonical_request() {
234        let datetime = chrono::Utc::now();
235        let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
236        let map: HeaderMap = HeaderMap::new();
237        let aws_sign = AwsSign::new(
238            "GET", 
239            url, 
240            &datetime, 
241            &map, 
242            "us-east-1", 
243            "a", 
244            "b", 
245            "s3", 
246            ""
247        );
248        let s = aws_sign.canonical_request();
249        assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
250    }
251
252    #[test]
253    fn sample_canonical_request_using_u8_body() {
254        let datetime = chrono::Utc::now();
255        let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
256        let map: HeaderMap = HeaderMap::new();
257        let aws_sign = AwsSign::new(
258            "GET",
259            url,
260            &datetime,
261            &map,
262            "us-east-1",
263            "a",
264            "b",
265            "s3",
266            "".as_bytes(),
267        );
268        let s = aws_sign.canonical_request();
269        assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
270    }
271
272    #[test]
273    fn sample_canonical_request_using_vec_body() {
274        let datetime = chrono::Utc::now();
275        let url: &str = "https://hi.s3.us-east-1.amazonaws.com/Prod/graphql";
276        let map: HeaderMap = HeaderMap::new();
277        let body = Vec::new();
278        let aws_sign = AwsSign::new(
279            "GET",
280            url,
281            &datetime,
282            &map,
283            "us-east-1",
284            "a",
285            "b",
286            "s3",
287            &body,
288        );
289        let s = aws_sign.canonical_request();
290        assert_eq!(s, "GET\n/Prod/graphql\n\n\n\n\ne3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855");
291    }
292}