Skip to content

Instantly share code, notes, and snippets.

@algesten
Last active June 1, 2021 09:00
Show Gist options
  • Save algesten/0c9e0b1998beb55f0b9cdcdf7002d109 to your computer and use it in GitHub Desktop.
Save algesten/0c9e0b1998beb55f0b9cdcdf7002d109 to your computer and use it in GitHub Desktop.
ureq aws sign
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac, NewMac};
use once_cell::sync::Lazy;
// notice this is percent_encoding=1.0.1 since I haven't bothered to
// fix this for latter versions of the crate.
use percent_encoding::{define_encode_set, utf8_percent_encode, SIMPLE_ENCODE_SET};
use sha2::{Digest, Sha256};
use std::env;
use std::fmt::Write;
pub *static* AWS_CREDS: Lazy<AwsCreds> = Lazy::new(|| {
let key = env::var("AWS_ACCESS_KEY_ID").expect("Missing AWS_ACCESS_KEY_ID");
let secret = env::var("AWS_SECRET_ACCESS_KEY").expect("Missing AWS_SECRET_ACCESS_KEY");
AwsCreds::new(key, secret)
});
/// Credentials for an S3Client
#[derive(Debug, Clone)]
pub struct AwsCreds {
pub key: String,
pub secret: String,
}
impl AwsCreds {
pub fn new(key: String, secret: String) -> Self {
AwsCreds { key, secret }
}
}
// Create alias for HMAC-SHA256
type HmacSha256 = Hmac<Sha256>;
fn hmac(key: &[u8], inp: &str) -> Vec<u8> {
let *mut* mac = HmacSha256::new_varkey(key).unwrap();
mac.update(inp.as_bytes());
mac.finalize().into_bytes().as_slice().to_owned()
}
fn sha256(inp: &[u8]) -> Vec<u8> {
let *mut* hasher = Sha256::default();
hasher.update(inp);
hasher.finalize().as_slice().to_owned()
}
// https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
// kSecret = your secret access key
// kDate = HMAC("AWS4" + kSecret, Date)
// kRegion = HMAC(kDate, Region)
// kService = HMAC(kRegion, Service)
// kSigning = HMAC(kService, "aws4_request")
#[allow(non_snake_case)]
fn derive_key(secret: &str, datestamp: &str, region: &str, service: &str) -> Vec<u8> {
let pfkey = format!("AWS4{}", secret);
let kDate = hmac(pfkey.as_bytes(), datestamp); // 20150830
let kRegion = hmac(&kDate, region);
let kService = hmac(&kRegion, service);
hmac(&kService, "aws4_request")
}
pub trait AwsSign: Sized {
fn aws_sign(
self,
host: &str,
path: &str,
region: &str,
service: &str,
time: &DateTime<Utc>,
creds: &AwsCreds,
) -> Result<Self, ureq::Error>;
}
impl AwsSign for ureq::Request {
fn aws_sign(
self,
host: &str,
path: &str,
region: &str,
service: &str,
time: &DateTime<Utc>,
creds: &AwsCreds,
) -> Result<Self, ureq::Error> {
//
// # Step 0
let amzdate = format!("{}", time.format("%Y%m%dT%H%M%SZ"));
let datestamp = format!("{}", time.format("%Y%m%d")); // Date w/o time, used in credential scope
let req_url = self.request_url()?;
// # Step 1: is to define the verb (GET, POST, etc.)--already done.
let method = self.method().to_string();
// # Step 2: Create canonical URI--the part of the URI from domain to query
// # string (use '/' if no path)
let canonical_uri = if path == "" { "/" } else { path };
// # Step 3: Create the canonical query string.
let *mut* keyval = req_url.query_pairs();
// Sort the parameter names by character code point in ascending order.
// For example, a parameter name that begins with the uppercase letter
// F precedes a parameter name that begins with a lowercase letter b.
(&*mut* keyval).sort_by(|a, b| a.0.cmp(&b.0));
// * Build the canonical query string by starting with the first parameter name
// in the sorted list.
// * For each parameter, append the URI-encoded parameter name, followed by the equals sign
// character (=), followed by the URI-encoded parameter value. Use an empty string for
// parameters that have no value.
// * Append the ampersand character (&) after each parameter value, except for the
// last value in the list.
let canonical_querystring = keyval
.into_iter()
.map(|(key, val)| format!("{}={}", percent_encode(&key), percent_encode(&val)))
.collect::<Vec<String>>()
.join("&");
// # Step 6: Create payload hash (hash of the request body content). For GET
// # requests, the payload is an empty string ("").
let payload_hash = sha256(b"").to_hex();
// # Step 4: Create the canonical headers and signed headers. Header names
// # must be trimmed and lowercase, and sorted in code point order from
// # low to high. Note that there is a trailing \n.
let canonical_headers = format!(
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
host, payload_hash, amzdate
);
// # Step 5: Create the list of signed headers. This lists the headers
// # in the canonical_headers list, delimited with ";" and in alpha order.
// # Note: The request can include any headers; canonical_headers and
// # signed_headers lists those that you want to be included in the
// # hash of the request. "Host" and "x-amz-date" are always required.
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
//# Step 7: Combine elements to create canonical request
// method + '\n' +
// canonical_uri + '\n' +
// canonical_querystring + '\n' +
// canonical_headers + '\n' +
// signed_headers + '\n' +
// payload_hash
let canonical_request = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
method,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash
);
let canonical_request_hash = sha256(canonical_request.as_bytes()).to_hex();
// # ************* TASK 2: CREATE THE STRING TO SIGN*************
// # Match the algorithm to the hashing algorithm you use, either SHA-1 or
// # SHA-256 (recommended)
// algorithm + '\n' +
// amzdate + '\n' +
// credential_scope + '\n' +
// canonical_request_hash
let algorithm = "AWS4-HMAC-SHA256";
let credential_scope = format!("{}/{}/{}/aws4_request", datestamp, region, service);
let string_to_sign = format!(
"{}\n{}\n{}\n{}",
algorithm, amzdate, credential_scope, canonical_request_hash
);
// # ************* TASK 3: CALCULATE THE SIGNATURE *************
// # Create the signing key using the function defined above.
let signing_key = derive_key(&creds.secret, &datestamp, region, service);
// # Sign the string_to_sign using the signing_key
let signature = hmac(&signing_key, &string_to_sign).to_hex();
// # ************* TASK 4: ADD SIGNING INFORMATION TO THE REQUEST *************
// # The signing information can be either in a query string value or in
// # a header named Authorization. This code shows how to use a header.
// # Create authorization header and add to request headers
let authorization_header = format!(
"{} Credential={}/{}, SignedHeaders={}, Signature={}",
algorithm, &creds.key, credential_scope, signed_headers, signature
);
Ok(self
.set("x-amz-date", &amzdate)
.set("x-amz-content-sha256", &payload_hash)
.set("Authorization", &authorization_header))
}
}
define_encode_set! {
// SIMPLE_ENCODE_SET: All ASCII charcters less than hexidecimal 20 and
// greater than 7E are encoded.
// AWS docs: Do not URI-encode any of the unreserved characters that
// RFC 3986 defines: A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ),
// and tilde ( ~ ).
pub AWS_ENCODE_SET = [SIMPLE_ENCODE_SET] | {' ', '!', '#', '$', '%', '&', '\'', '(', ')',
'*', '+', ',', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '`',
'{', '|', '}'
}
}
fn percent_encode(s: &str) -> String {
utf8_percent_encode(s, AWS_ENCODE_SET).to_string()
}
trait ToHex {
fn to_hex(&self) -> String;
}
impl ToHex for Vec<u8> {
fn to_hex(&self) -> String {
let *mut* s = String::new();
for &byte in self.iter() {
write!(&*mut* s, "{:02x}", byte).unwrap();
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::TimeZone;
#[test]
pub fn test_derive_key() {
let secret = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
let key = derive_key(secret, "20150830", "us-east-1", "iam");
assert_eq!(
"c4afb1cc5771d871763a393e44b703571b55cc28424d1a5e86da6ed3c154a4b9",
key.to_hex()
);
}
#[test]
pub fn test_aws_sign() {
//
// test creds as provided in Amazon's documentation
let creds = AwsCreds::new(
"AKIDEXAMPLE".to_owned(),
"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY".to_owned(),
);
// 20150830T123600Z
let time: DateTime<Utc> = Utc.ymd(2015, 08, 30).and_hms(12, 36, 00);
let request = ureq::get("https://example.amazonaws.com/");
let request = request
.aws_sign(
"example.amazonaws.com",
"/",
"us-east-1",
"service",
&time,
&creds,
)
.unwrap();
assert_eq!(request.header("x-amz-date").unwrap(), "20150830T123600Z");
assert_eq!(
request.header("authorization").unwrap(),
"AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, \
SignedHeaders=host;x-amz-content-sha256;x-amz-date, \
Signature=726c5c4879a6b4ccbbd3b24edbd6b8826d34f87450fbbf4e85546fc7ba9c1642"
);
}
#[test]
pub fn test_s3_sign_percent_encode() {
assert_eq!(percent_encode("abc-A_B~C.0129"), "abc-A_B~C.0129");
assert_eq!(percent_encode("/+$%foo"), "%2F%2B%24%25foo");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment