mirror of
https://github.com/ForeverPyrite/r2client.git
synced 2025-12-10 01:38:07 +00:00
we are so back (first real commit)
This commit is contained in:
33
aws_sigv4/Cargo.toml
Normal file
33
aws_sigv4/Cargo.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
[package]
|
||||
name = "aws_sigv4"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
|
||||
[dependencies]
|
||||
# For the very specific date-time format Amazon wants
|
||||
chrono = "0.4.42"
|
||||
# SHA256 Hashing
|
||||
sha2 = "0.10.9"
|
||||
# Representing binary data in valid strings
|
||||
hex = "0.4.3"
|
||||
# Used to derive the signing key and sign the string
|
||||
hmac = "0.12.1"
|
||||
# For the HeaderMap struct and Method enum
|
||||
http = "1.3.1"
|
||||
# The base URL encoding
|
||||
urlencoding = "2.1.3"
|
||||
|
||||
# Logging (final result is traced with log::trace!)
|
||||
log = "0.4.28"
|
||||
|
||||
[dev-dependencies]
|
||||
# To get env vars for test
|
||||
dotenv = "0.15.0"
|
||||
# For the client
|
||||
reqwest = { version = "0.12.23", features = ["blocking"] }
|
||||
# For the Uri, unfortunately (why wouldn't reqwest re-export that?)
|
||||
# Perhaps it's Url type works fine, but still, I would like to let
|
||||
# users have as much agency over their http client as possible.
|
||||
http = "1.3.1"
|
||||
78
aws_sigv4/README.md
Normal file
78
aws_sigv4/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# aws_signing library
|
||||
Yeah so this is a library for signing things with AWS SigV4.
|
||||
Pretty straightforward.
|
||||
In fact, there is are only 3 methods on the public API
|
||||
1. to make the client
|
||||
2. Change the client region (cause why not?)
|
||||
3. To transform an unsigned request that will be sent to an AWS compatible server to one with proper SigV4 headers
|
||||
(With this wording, should I take a mutable reference to headers instead of...hmmm, we will see if I get bored enough to improve it myself.)
|
||||
|
||||
If I'm being completely honest, this was part of my own `r2client` that I wrote, which is uses S3
|
||||
So I don't imagine that I come back to this to actually make a proper AWS Signing library.
|
||||
I could imagine it being useful to *someone* who also wants to make an AWS abstraction that doesn't have 2 **BILLION** dependences.
|
||||
|
||||
## Usage
|
||||
|
||||
wait that's what the doc comments and `cargo doc` command is for lmao,
|
||||
guess I'll get rid of this block
|
||||
|
||||
### Unexpected Errors
|
||||
If you're getting errors related to invalid signatures and you're thinking "It's probably that damn crate I'm using from that newbie!",
|
||||
you're probably right.
|
||||
If you got here because crates.io search brought you here, nice.
|
||||
Didn't mean for that to happen, sorry!
|
||||
There are much more developed options out there, like [reqsign](https://github.com/apache/opendal-reqsign), which will also handle more
|
||||
of the service related things for you.
|
||||
|
||||
Alternatively, you can try to stick with this crate (mistake)
|
||||
Using log level trace with whatever logging crate you use will print out all steps of the AWS signing process, so you can follow along
|
||||
with your favorite documentation/examples and figure out where things go wrong.
|
||||
With this you can either raise an issue, or review the code yourself to implement a working solution.
|
||||
|
||||
## Todo
|
||||
|
||||
- [ ] Create unit tests
|
||||
- [ ] Add option for session keys
|
||||
- [ ] Additional client or whatever for weirdos who want to use SigV4a
|
||||
- [ ] Perhaps drop `http` as a dependency?
|
||||
- [ ] Alternatively, have users pass a mutable reference to a HeaderMap instead of cloning and returning a new one.
|
||||
- This can kinda hurt ease of use...maybe, but I feel like the slight performance gain and easier management of custom headers makes it worth.
|
||||
- That's also acting like the crate is mature enough to do anything but provide the minimal headers for SigV4
|
||||
- [ ] SigV4a
|
||||
- Is this even really used?
|
||||
|
||||
## Contributing
|
||||
What.
|
||||
|
||||
This crate is supposed to be a really simple way to sign AWS request headers and nothing else.
|
||||
Again, I created it for my Cloudflare R2 (AWS S3) client and I imagine there are a few gaps for other services,
|
||||
as well as some exceptions to the general programmatic things I saw in the documentation.
|
||||
|
||||
If you'd like to help improve it, maybe to make sign requests for some other AWS service, then I'd
|
||||
appreciate if as specific unit test was added to ensure functionality.
|
||||
This should be along with the other unit tests remaining intact and passing (unless they are blatantly incompatible
|
||||
with some AWS documentation)
|
||||
|
||||
The code base (one lib.rs file) is pretty straightforward, it very directly follows the outline that AWS's SigV4 documentation provides,
|
||||
explicitly containing all prerequisite functions and having a function call for all 5 of the steps.
|
||||
I did this since the other examples were hard to follow, just a chunk of code that doesn't explain itself at any point.
|
||||
All prerequisite functions, along with functions that don't require any of the keys/credential fields (in SigV4Credentials)
|
||||
are defined outside of the struct.
|
||||
|
||||
Gl;hf
|
||||
|
||||
# welp
|
||||
figure I'd try my hand at writing some public stuff like this, even though I'm not planing on publishing this publicly, only
|
||||
compiled in the r2client.
|
||||
I feel...okay about it.
|
||||
I still feel like this stuff will be so small no matter what that I can still put a little personality into it.
|
||||
|
||||
I am incredibly verbose, shocking I know, but I think that's a bad thing.
|
||||
I should be more brief, and then more verbose when relevant within the code.
|
||||
Outside of that though, I think this is...fine.
|
||||
I just need to get better at less yapping.
|
||||
|
||||
I really think this crate is too intermingled with the R2 Client to be useful to anyone else, really, although I think this was
|
||||
*fine* for organizational purposes.
|
||||
And who knows.
|
||||
Maybe I'll need it again.
|
||||
230
aws_sigv4/examples.md
Normal file
230
aws_sigv4/examples.md
Normal file
@@ -0,0 +1,230 @@
|
||||
Example: GET Object
|
||||
The following example gets the first 10 bytes of an object (test.txt) from examplebucket. For more information about the API action, see GetObject.
|
||||
|
||||
|
||||
```
|
||||
```
|
||||
GET /test.txt HTTP/1.1
|
||||
Host: examplebucket.s3.amazonaws.com
|
||||
Authorization: SignatureToBeCalculated
|
||||
Range: bytes=0-9
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
x-amz-date: 20130524T000000Z
|
||||
Because this GET request does not provide any body content, the x-amz-content-sha256 value can either be the hash of the empty request body or the literal string "UNSIGNED-PAYLOAD". The following steps show signature calculations and construction of the Authorization header using the hash of an empty string.
|
||||
```
|
||||
```
|
||||
1. StringToSign
|
||||
|
||||
a. CanonicalRequest
|
||||
|
||||
```
|
||||
GET
|
||||
/test.txt
|
||||
|
||||
host:examplebucket.s3.amazonaws.com
|
||||
range:bytes=0-9
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
x-amz-date:20130524T000000Z
|
||||
|
||||
host;range;x-amz-content-sha256;x-amz-date
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
```
|
||||
In the canonical request string, the last line is the hash of the empty request body. The third line is empty because there are no query parameters in the request.
|
||||
```
|
||||
```
|
||||
|
||||
b. StringToSign
|
||||
|
||||
```
|
||||
AWS4-HMAC-SHA256
|
||||
20130524T000000Z
|
||||
20130524/us-east-1/s3/aws4_request
|
||||
7344ae5b7ee6c3e7e6b0fe0640412a37625d1fbfff95c48bbb2dc43964946972
|
||||
```
|
||||
|
||||
2. SigningKey
|
||||
|
||||
|
||||
```
|
||||
signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<YourSecretAccessKey>","20130524"),"us-east-1"),"s3"),"aws4_request")
|
||||
```
|
||||
|
||||
3. Signature
|
||||
|
||||
|
||||
```
|
||||
f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41
|
||||
```
|
||||
|
||||
4. Authorization header
|
||||
The resulting Authorization header is as follows:
|
||||
|
||||
```
|
||||
AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;range;x-amz-content-sha256;x-amz-date,Signature=f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41
|
||||
```
|
||||
|
||||
Example: PUT Object
|
||||
This example PUT request creates an object (test$file.text) in examplebucket . The example assumes the following:
|
||||
|
||||
|
||||
You are requesting REDUCED_REDUNDANCY as the storage class by adding the x-amz-storage-class request header. For information about storage classes, see Storage Classes in the Amazon Simple Storage Service User Guide.
|
||||
|
||||
The content of the uploaded file is a string, "Welcome to Amazon S3." The value of x-amz-content-sha256 in the request is based on this string.
|
||||
|
||||
For information about the API action, see PutObject.
|
||||
|
||||
|
||||
PUT test$file.text HTTP/1.1
|
||||
Host: examplebucket.s3.amazonaws.com
|
||||
Date: Fri, 24 May 2013 00:00:00 GMT
|
||||
Authorization: SignatureToBeCalculated
|
||||
x-amz-date: 20130524T000000Z
|
||||
x-amz-storage-class: REDUCED_REDUNDANCY
|
||||
x-amz-content-sha256: 44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072
|
||||
|
||||
<Payload>
|
||||
The following steps show signature calculations.
|
||||
|
||||
StringToSign
|
||||
CanonicalRequest
|
||||
|
||||
|
||||
PUT
|
||||
/test%24file.text
|
||||
|
||||
date:Fri, 24 May 2013 00:00:00 GMT
|
||||
host:examplebucket.s3.amazonaws.com
|
||||
x-amz-content-sha256:44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072
|
||||
x-amz-date:20130524T000000Z
|
||||
x-amz-storage-class:REDUCED_REDUNDANCY
|
||||
|
||||
date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class
|
||||
44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072
|
||||
In the canonical request, the third line is empty because there are no query parameters in the request. The x-amz-content-sha256 canonical header may optionally be signed since its payload hash is already provided at the bottom of the request. The last line is the hash of the body, which should be the same as the x-amz-content-sha256 header value sent to S3 in the HTTP request.
|
||||
|
||||
StringToSign
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256
|
||||
20130524T000000Z
|
||||
20130524/us-east-1/s3/aws4_request
|
||||
9e0e90d9c76de8fa5b200d8c849cd5b8dc7a3be3951ddb7f6a76b4158342019d
|
||||
|
||||
SigningKey
|
||||
|
||||
|
||||
signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<YourSecretAccessKey>","20130524"),"us-east-1"),"s3"),"aws4_request")
|
||||
|
||||
Signature
|
||||
|
||||
|
||||
98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd
|
||||
|
||||
Authorization header
|
||||
The resulting Authorization header is as follows:
|
||||
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=date;host;x-amz-content-sha256;x-amz-date;x-amz-storage-class,Signature=98ad721746da40c64f1a55b78f14c238d841ea1380cd77a1b5971af0ece108bd
|
||||
|
||||
Example: GET Bucket Lifecycle
|
||||
The following GET request retrieves the lifecycle configuration of examplebucket. For information about the API action, see GetBucketLifecycleConfiguration.
|
||||
|
||||
|
||||
GET ?lifecycle HTTP/1.1
|
||||
Host: examplebucket.s3.amazonaws.com
|
||||
Authorization: SignatureToBeCalculated
|
||||
x-amz-date: 20130524T000000Z
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
Because the request does not provide any body content, the x-amz-content-sha256 header value is the hash of the empty request body. The following steps show signature calculations.
|
||||
|
||||
StringToSign
|
||||
CanonicalRequest
|
||||
|
||||
|
||||
GET
|
||||
/
|
||||
lifecycle=
|
||||
host:examplebucket.s3.amazonaws.com
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
x-amz-date:20130524T000000Z
|
||||
|
||||
host;x-amz-content-sha256;x-amz-date
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
In the canonical request, the last line is the hash of the empty request body.
|
||||
|
||||
StringToSign
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256
|
||||
20130524T000000Z
|
||||
20130524/us-east-1/s3/aws4_request
|
||||
9766c798316ff2757b517bc739a67f6213b4ab36dd5da2f94eaebf79c77395ca
|
||||
|
||||
SigningKey
|
||||
|
||||
|
||||
signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<YourSecretAccessKey>","20130524"),"us-east-1"),"s3"),"aws4_request")
|
||||
|
||||
Signature
|
||||
|
||||
|
||||
fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543
|
||||
|
||||
Authorization header
|
||||
The resulting Authorization header is as follows:
|
||||
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=fea454ca298b7da1c68078a5d1bdbfbbe0d65c699e0f91ac7a200a0136783543
|
||||
|
||||
Example: Get Bucket (List Objects)
|
||||
The following example retrieves a list of objects from examplebucket bucket. For information about the API action, see ListObjects.
|
||||
|
||||
|
||||
GET ?max-keys=2&prefix=J HTTP/1.1
|
||||
Host: examplebucket.s3.amazonaws.com
|
||||
Authorization: SignatureToBeCalculated
|
||||
x-amz-date: 20130524T000000Z
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
Because the request does not provide a body, the value of x-amz-content-sha256 is the hash of the empty request body. The following steps show signature calculations.
|
||||
|
||||
StringToSign
|
||||
CanonicalRequest
|
||||
|
||||
|
||||
GET
|
||||
/
|
||||
max-keys=2&prefix=J
|
||||
host:examplebucket.s3.amazonaws.com
|
||||
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
x-amz-date:20130524T000000Z
|
||||
|
||||
host;x-amz-content-sha256;x-amz-date
|
||||
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
||||
In the canonical string, the last line is the hash of the empty request body.
|
||||
|
||||
StringToSign
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256
|
||||
20130524T000000Z
|
||||
20130524/us-east-1/s3/aws4_request
|
||||
df57d21db20da04d7fa30298dd4488ba3a2b47ca3a489c74750e0f1e7df1b9b7
|
||||
|
||||
SigningKey
|
||||
|
||||
|
||||
signing key = HMAC-SHA256(HMAC-SHA256(HMAC-SHA256(HMAC-SHA256("AWS4" + "<YourSecretAccessKey>","20130524"),"us-east-1"),"s3"),"aws4_request")
|
||||
|
||||
Signature
|
||||
|
||||
|
||||
34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7
|
||||
|
||||
Authorization header
|
||||
The resulting Authorization header is as follows:
|
||||
|
||||
|
||||
|
||||
AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=host;x-amz-content-sha256;x-amz-date,Signature=34b48302e7b5fa45bde8084f4b7868a86f0a534bc59db6670ed5711ef69dc6f7
|
||||
390
aws_sigv4/src/lib.rs
Normal file
390
aws_sigv4/src/lib.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use chrono::Utc;
|
||||
use hmac::{Hmac, Mac};
|
||||
use log::trace;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
type Hmac256 = Hmac<Sha256>;
|
||||
|
||||
const EMPTY_PAYLOAD_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
||||
|
||||
// --- Utility functions ---
|
||||
fn lowercase(string: &str) -> String {
|
||||
string.to_lowercase()
|
||||
}
|
||||
|
||||
fn hex<T: AsRef<[u8]>>(data: T) -> String {
|
||||
hex::encode(data)
|
||||
}
|
||||
|
||||
fn sha256hash<T: AsRef<[u8]>>(data: T) -> [u8; 32] {
|
||||
Sha256::digest(data).into()
|
||||
}
|
||||
|
||||
fn hmac_sha256(signing_key: &[u8], message: &str) -> Vec<u8> {
|
||||
let mut mac = Hmac256::new_from_slice(signing_key).expect("bad key :pensive:");
|
||||
mac.update(message.as_bytes());
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
||||
|
||||
fn trim(string: &str) -> String {
|
||||
string.trim().to_string()
|
||||
}
|
||||
|
||||
pub fn hash<T: AsRef<[u8]>>(payload: T) -> String {
|
||||
hex(sha256hash(payload))
|
||||
}
|
||||
|
||||
pub fn url_encode(url: &str) -> String {
|
||||
let mut url = urlencoding::encode(url).into_owned();
|
||||
let encoded_to_replacement: [(&str, &str); 4] =
|
||||
[("+", "%20"), ("*", "%2A"), ("%7E", "~"), ("%2F", "/")];
|
||||
for (encoded_chars_pattern, replacement) in encoded_to_replacement {
|
||||
url = url.replace(encoded_chars_pattern, replacement)
|
||||
}
|
||||
url
|
||||
}
|
||||
|
||||
// --- Signing Functions ---
|
||||
// These don't use any parts of the SigV4Credentials, so they are external
|
||||
|
||||
// --- Canonical request ---
|
||||
fn create_canonical_request(
|
||||
method: http::Method,
|
||||
uri: http::Uri,
|
||||
mut headers: Vec<(String, String)>,
|
||||
hashed_payload: &str,
|
||||
) -> (String, Vec<(String, String)>, String) {
|
||||
// HTTPMethod
|
||||
let http_method = method.to_string();
|
||||
|
||||
// CanonicalURI = *path only* (spec forbids scheme+host here)
|
||||
let canonical_uri = if uri.path().is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
uri.path().to_string()
|
||||
};
|
||||
|
||||
// CanonicalQueryString (URL-encoded, sorted by key)
|
||||
let canonical_query_string = if let Some(query_string) = uri.query() {
|
||||
let mut pairs = query_string
|
||||
.split('&')
|
||||
.map(|query| {
|
||||
let (k, v) = query.split_once('=').unwrap_or((query, ""));
|
||||
(url_encode(k), url_encode(v))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pairs.sort_by(|a, b| a.0.cmp(&b.0));
|
||||
pairs
|
||||
.into_iter()
|
||||
.map(|(k, v)| format!("{k}={v}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("&")
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
// checks for proper host headers
|
||||
let host = uri
|
||||
.host()
|
||||
.expect("uri passed without a proper host")
|
||||
.to_string();
|
||||
if !headers.iter().any(|(k, _)| k.eq_ignore_ascii_case("host")) {
|
||||
headers.push(("host".to_string(), host));
|
||||
}
|
||||
|
||||
if !headers
|
||||
.iter()
|
||||
.any(|(k, _)| k.eq_ignore_ascii_case("x-amz-content-sha256"))
|
||||
{
|
||||
headers.push((
|
||||
"x-amz-content-sha256".to_string(),
|
||||
hashed_payload.to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
// CanonicalHeaders + SignedHeaders
|
||||
let mut http_headers = headers
|
||||
.iter()
|
||||
.map(|(name, value)| (lowercase(name), trim(value)))
|
||||
.collect::<Vec<_>>();
|
||||
http_headers.sort_by(|(k1, _), (k2, _)| k1.cmp(k2));
|
||||
|
||||
let canonical_headers: String = http_headers
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{k}:{v}\n"))
|
||||
.collect();
|
||||
|
||||
let signed_headers: String = http_headers
|
||||
.iter()
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
|
||||
// Final canonical request
|
||||
let canonical_request = format!(
|
||||
"{http_method}\n{canonical_uri}\n{canonical_query_string}\n{canonical_headers}\n{signed_headers}\n{hashed_payload}"
|
||||
);
|
||||
|
||||
(canonical_request, http_headers, signed_headers)
|
||||
}
|
||||
|
||||
/// This is really an superfluous wrapper to the hmac_sha256 function.
|
||||
/// Since it was it's own step in AWS's documentation, I initially gave it it's own function.
|
||||
/// However I think a comment over calculate_signature could do now.
|
||||
fn calculate_signature(signing_key: &[u8], string_to_sign: &str) -> Vec<u8> {
|
||||
hmac_sha256(signing_key, string_to_sign)
|
||||
}
|
||||
|
||||
fn string_to_sign(scope: &str, amz_date: &str, hashed_canonical_request: &str) -> String {
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}",
|
||||
"AWS4-HMAC-SHA256", amz_date, scope, hashed_canonical_request
|
||||
)
|
||||
}
|
||||
/// Structure containing all the data relevant for an AWS Service utilizing SigV4.
|
||||
/// Service: String containing the AWS service (e.g. "ec2" or "s3")
|
||||
/// Region: String containing the AWS region you're working in (e.g. "auto" or "us-east-1")
|
||||
/// Access Key: The "Access Key" to use with the AWS service (crazy, ik)
|
||||
/// Secret Key: The "Secret Key" that is used for cryptographic signing for the AWS Service (woah)
|
||||
///
|
||||
/// ```no_run
|
||||
/// use aws_signing::SigV4Credentials;
|
||||
/// use http::{Method, Uri};
|
||||
///
|
||||
/// let s3_client = SigV4Credentials::new(
|
||||
/// "s3",
|
||||
/// "us-east-1",
|
||||
/// std::env::var("S3_ACCESS_KEY").unwrap(),
|
||||
/// std::env::var("S3_SECRET_KEY").unwrap(),
|
||||
/// );
|
||||
/// let (_, request_headers) = s3_client.signature(
|
||||
/// Method::GET,
|
||||
/// Uri::from_static("https://s3.us-east-1.amazonaws.com/example-bucket/file.txt"),
|
||||
/// vec![("content-type", "text/plain")],
|
||||
/// "" // Since it's a GET request, the payload is ""
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
// A more mature client would also have session_key: Option<String>, but not my problem
|
||||
pub struct SigV4Credentials {
|
||||
// Would it makes more sense for these to be type generics
|
||||
// with trait param ToString?
|
||||
// Either that or just &str or String...wait, union?
|
||||
// Nah there has to be a better way to do it than...that
|
||||
// but I don't wanna enum!!!
|
||||
service: String,
|
||||
region: String,
|
||||
access_key: String,
|
||||
secret_key: String,
|
||||
}
|
||||
/// NOTE: This only impliments functions that require one of the SigV4Credentials fields.
|
||||
/// For other functions related to the signing proccess, they are defined above, including the
|
||||
/// prequisite functions defined at https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv-create-signed-request.html
|
||||
impl SigV4Credentials {
|
||||
/// Creates a new instance of the SigV4Credentials for a particular service, in a region, with your
|
||||
/// private and public access keys.
|
||||
///
|
||||
/// For some reason this function will take any values that impl Into<String>, so you can pass
|
||||
/// &str, String, or something else if you decide to get freaky.
|
||||
pub fn new(
|
||||
service: impl Into<String>,
|
||||
region: impl Into<String>,
|
||||
pub_key: impl Into<String>,
|
||||
priv_key: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
service: service.into(),
|
||||
region: region.into(),
|
||||
access_key: pub_key.into(),
|
||||
secret_key: priv_key.into(),
|
||||
}
|
||||
}
|
||||
|
||||
// In a more mature client, this might be an enum of AWSRegions
|
||||
// I also don't even know if this could ever be useful lol, wouldn't you have individual
|
||||
// clients for each region or use "auto" for AWS to figure it out for you? whatever.
|
||||
pub fn set_region(&mut self, region: impl Into<String>) {
|
||||
self.region = region.into()
|
||||
}
|
||||
|
||||
fn credential_scope(&self, date: &str) -> String {
|
||||
format!(
|
||||
"{}/{}/{}/aws4_request",
|
||||
date,
|
||||
lowercase(&self.region),
|
||||
lowercase(&self.service)
|
||||
)
|
||||
}
|
||||
|
||||
fn derive_signing_key(&self, date: &str) -> Vec<u8> {
|
||||
let secret_key = &self.secret_key;
|
||||
let key = format!("AWS4{secret_key}");
|
||||
let date_key = hmac_sha256(key.as_bytes(), date);
|
||||
let date_region_key = hmac_sha256(&date_key, &self.region);
|
||||
let date_region_service_key = hmac_sha256(&date_region_key, &self.service);
|
||||
hmac_sha256(&date_region_service_key, "aws4_request")
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
/// This is the only function to use <3
|
||||
pub fn signature<T: AsRef<[u8]>>(
|
||||
&self,
|
||||
method: http::Method,
|
||||
uri: http::Uri,
|
||||
// Should probably make this a header map, then turn it into a Vec(String, String) to sort
|
||||
// by header name cause Amazon said so.
|
||||
mut headers: Vec<(String, String)>,
|
||||
payload: T,
|
||||
) -> (String, http::HeaderMap) {
|
||||
let auth_algorithm = "AWS4-HMAC-SHA256";
|
||||
let now = Utc::now();
|
||||
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||
let date = now.format("%Y%m%d").to_string();
|
||||
let payload_as_bytes = payload.as_ref();
|
||||
let payload_hash = if payload_as_bytes.is_empty() {
|
||||
EMPTY_PAYLOAD_HASH.to_string()
|
||||
} else {
|
||||
hash(payload_as_bytes)
|
||||
};
|
||||
|
||||
// Add x-amz-date header if not already present
|
||||
if !headers
|
||||
.iter()
|
||||
.any(|(k, _)| k.eq_ignore_ascii_case("x-amz-date"))
|
||||
{
|
||||
headers.push(("x-amz-date".to_string(), amz_date.clone()));
|
||||
}
|
||||
|
||||
// Canonical request
|
||||
let (canonical_request, mut headers, signed_headers) =
|
||||
create_canonical_request(method, uri, headers, &payload_hash);
|
||||
|
||||
// String to sign
|
||||
let scope = self.credential_scope(&date);
|
||||
let hashed_canonical_request = hash(&canonical_request);
|
||||
let string_to_sign = string_to_sign(&scope, &amz_date, &hashed_canonical_request);
|
||||
|
||||
// Signing key + signature
|
||||
let signing_key = self.derive_signing_key(&date);
|
||||
let signature = hex(calculate_signature(&signing_key, &string_to_sign));
|
||||
|
||||
// Authorization header
|
||||
let access_key = &self.access_key;
|
||||
let credential = format!("{access_key}/{scope}");
|
||||
let auth_header = format!(
|
||||
"{auth_algorithm} Credential={credential}, SignedHeaders={signed_headers}, Signature={signature}"
|
||||
);
|
||||
|
||||
trace!("\n--- AWS SigV4 Debug ---");
|
||||
trace!("1. CanonicalRequest:\n---\n{canonical_request}\n---");
|
||||
trace!("2. StringToSign:\n---\n{string_to_sign}\n---");
|
||||
trace!("3. SigningKey:\n---\n{}\n---", hex(&signing_key));
|
||||
trace!("4. Signature:\n---\n{signature}\n---");
|
||||
trace!("5. Authorization Header:\n---\n{auth_header}\n---");
|
||||
|
||||
headers.push(("authorization".to_string(), auth_header));
|
||||
|
||||
let mut header_map: http::HeaderMap = http::HeaderMap::new();
|
||||
for (header, value) in headers.clone() {
|
||||
header_map.insert(
|
||||
http::HeaderName::from_lowercase(header.to_lowercase().as_bytes()).unwrap(),
|
||||
http::HeaderValue::from_str(&value).unwrap(),
|
||||
);
|
||||
}
|
||||
(signature, header_map)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use dotenv::dotenv;
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO: Since I can't figure out a problem, I figure the best way to approach this is to use
|
||||
// test driven development.
|
||||
// Given a certain input, you should have a specific output after all, so it should make sense
|
||||
//
|
||||
// I went ahead and added the 5 major steps
|
||||
// If one doesn't work, it would make sense to break it into smaller unit tests, but for now
|
||||
// these five should do.
|
||||
//
|
||||
// This link and examples.md should help out a LOT
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||
//
|
||||
// Alternatively give up and see if "reqsign" will work better.
|
||||
//
|
||||
// INFO: THERE IS NO PROBLEM!!!!
|
||||
// Sike I lied.
|
||||
// It still makes sense to do unit tests for things like session keys which I haven't
|
||||
// implimented yet, to make it usable for other clients.
|
||||
#[test]
|
||||
fn canonical_request() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_request_hash() {
|
||||
todo!();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn string_to_sign() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_signing_key() {
|
||||
todo!();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculate_signature() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_sig_to_req() {
|
||||
todo!()
|
||||
}
|
||||
|
||||
fn create_client() -> SigV4Credentials {
|
||||
SigV4Credentials::new(
|
||||
"s3",
|
||||
"us-east-1",
|
||||
std::env::var("AWS_ACCESS_KEY").unwrap_or("AKIAIOSFODNN7EXAMPLE".to_string()),
|
||||
std::env::var("AWS_SECRET_KEY")
|
||||
.unwrap_or("wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn s3_upload_test() {
|
||||
// Initialize .env and vars
|
||||
dotenv().ok();
|
||||
let bucket = std::env::var("S3_BUCKET").unwrap();
|
||||
let file_key = url_encode("s3-sigv4-test.txt");
|
||||
// "Use from parts!!" no.
|
||||
let url = format!(
|
||||
"{}/{}/{}",
|
||||
std::env::var("S3_ENDPOINT").unwrap(),
|
||||
bucket,
|
||||
file_key
|
||||
);
|
||||
let endpoint: http::Uri = url.parse().unwrap();
|
||||
|
||||
let headers = vec![("host".to_string(), endpoint.host().unwrap().to_owned())];
|
||||
|
||||
let signer = create_client();
|
||||
let (_, header_map) = signer.signature(http::Method::GET, endpoint, headers, b"");
|
||||
|
||||
let res = reqwest::blocking::Client::new()
|
||||
.get(&url)
|
||||
.headers(header_map)
|
||||
.send()
|
||||
.unwrap();
|
||||
let status = res.status();
|
||||
|
||||
assert!(status.is_success())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user