Last active
June 1, 2021 09:00
-
-
Save algesten/0c9e0b1998beb55f0b9cdcdf7002d109 to your computer and use it in GitHub Desktop.
ureq aws sign
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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