dbcrossbarlib/clouds/aws/
signing.rs

1//! S3 URL signing.
2
3use base64::{prelude::BASE64_STANDARD, Engine};
4use chrono::{DateTime, Utc};
5use hmac::{Hmac, Mac};
6use sha1::Sha1;
7
8use super::AwsCredentials;
9use crate::common::*;
10
11/// Sign an `s3://` URL for use with AWS. Returns the signed URL and an optional
12/// value for the `x-amz-security-token` header.
13pub(crate) fn sign_s3_url<'creds>(
14    credentials: &'creds AwsCredentials,
15    method: &str,
16    expires: DateTime<Utc>,
17    url: &Url,
18) -> Result<(Url, &'creds Option<String>)> {
19    if url.scheme() != "s3" {
20        return Err(format_err!("can't sign non-S3 URL {}", url));
21    }
22    let host = url
23        .host()
24        .ok_or_else(|| format_err!("no host in URL {}", url))?;
25
26    let mut mac =
27        Hmac::<Sha1>::new_from_slice(credentials.secret_access_key.as_bytes())
28            .map_err(|err| format_err!("cannot compute signature: {}", err))?;
29    let full_path = format!("/{}{}", host, url.path());
30    let payload = format!("{}\n\n\n{}\n{}", method, expires.timestamp(), full_path,);
31    mac.update(payload.as_bytes());
32    let signature = BASE64_STANDARD.encode(mac.finalize().into_bytes());
33    let mut signed: Url = format!("https://s3.amazonaws.com{}", full_path).parse()?;
34    signed
35        .query_pairs_mut()
36        .append_pair("AWSAccessKeyId", &credentials.access_key_id)
37        .append_pair("Expires", &format!("{}", expires.timestamp()))
38        .append_pair("Signature", &signature);
39    Ok((signed, &credentials.session_token))
40}
41
42#[test]
43fn signatures_are_valid() {
44    use chrono::NaiveDateTime;
45
46    // Example is taken from
47    // https://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html.
48    let creds = AwsCredentials {
49        access_key_id: "44CF9590006BF252F707".to_owned(),
50        secret_access_key: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_owned(),
51        session_token: None,
52    };
53    let (signed_url, _x_amz_security_token) = sign_s3_url(
54        &creds,
55        "GET",
56        DateTime::from_utc(
57            NaiveDateTime::from_timestamp_opt(1_141_889_120, 0).unwrap(),
58            Utc,
59        ),
60        &"s3://quotes/nelson".parse().unwrap(),
61    )
62    .unwrap();
63    let expected: Url =
64        "https://s3.amazonaws.com/quotes/nelson?AWSAccessKeyId=44CF9590006BF252F707&Expires=1141889120&Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D".parse().unwrap();
65    assert_eq!(signed_url, expected);
66}