Initial commit
This commit is contained in:
commit
9373fe944d
7 changed files with 1726 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
1428
Cargo.lock
generated
Normal file
1428
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
19
Cargo.toml
Normal file
19
Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "sigv4"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = "0.4"
|
||||
hex = "0.4"
|
||||
hmac = "0.12"
|
||||
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "gzip"] }
|
||||
serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"]}
|
||||
sha2 = "0.10"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"]}
|
||||
url = "2"
|
||||
urlencoding = "2"
|
5
readme.md
Normal file
5
readme.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
# AWS-SIG-V4
|
||||
|
||||
Attempt at manually implementing aws-sig-v4
|
||||
|
||||
https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
|
50
src/main.rs
Normal file
50
src/main.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
mod request_builder;
|
||||
mod types;
|
||||
|
||||
use crate::types::AwsRegion;
|
||||
use request_builder::RequestBuilder;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
const LOG_FILTER: &str = "DEBUG,rustls=INFO,hyper::proto=INFO";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_default_env().unwrap_or(LOG_FILTER.into()))
|
||||
.with(tracing_subscriber::fmt::layer().pretty())
|
||||
.init();
|
||||
|
||||
let access_id = match std::env::var("AWS_ACCESS_KEY_ID") {
|
||||
Ok(x) => x,
|
||||
Err(_) => panic!("Missing environment variable 'AWS_ACCESS_KEY_ID'"),
|
||||
};
|
||||
|
||||
let access_key = match std::env::var("AWS_SECRET_ACCESS_KEY") {
|
||||
Ok(x) => x,
|
||||
Err(_) => panic!("Missing environment variable 'AWS_SECRET_ACCESS_KEY'"),
|
||||
};
|
||||
|
||||
list_buckets(access_id, access_key).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_buckets(access_id: String, access_key: String) -> anyhow::Result<()> {
|
||||
let req = RequestBuilder::new(
|
||||
access_id,
|
||||
access_key,
|
||||
"https://s3.amazonaws.com/".to_string(),
|
||||
AwsRegion::UsEast1,
|
||||
);
|
||||
|
||||
let response = req.send().await;
|
||||
|
||||
match response {
|
||||
Ok(x) => {
|
||||
println!("{}", x.text().await?)
|
||||
}
|
||||
Err(x) => println!("{:?}", x),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
183
src/request_builder.rs
Normal file
183
src/request_builder.rs
Normal file
|
@ -0,0 +1,183 @@
|
|||
use crate::types::{AwsRegion, AwsService, HttpVerbs};
|
||||
use chrono::prelude::*;
|
||||
use hmac::{Hmac, Mac};
|
||||
use reqwest::header::{HeaderMap, HeaderName};
|
||||
use reqwest::Client;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub struct RequestBuilder {
|
||||
http_verb: HttpVerbs,
|
||||
now_long: String,
|
||||
now_short: String,
|
||||
access_key_id: String,
|
||||
access_key_secret: String,
|
||||
url: Url,
|
||||
headers: HashMap<String, String>,
|
||||
region: AwsRegion,
|
||||
service: AwsService,
|
||||
}
|
||||
|
||||
impl RequestBuilder {
|
||||
pub fn new(
|
||||
access_key_id: String,
|
||||
access_key_secret: String,
|
||||
endpoint: String,
|
||||
aws_region: AwsRegion,
|
||||
) -> Self {
|
||||
let url = Url::parse(&endpoint).expect("invalid endpoint format");
|
||||
|
||||
let now: DateTime<Utc> = Utc::now();
|
||||
let now_long = now.format("%Y%m%dT%H%M%SZ").to_string();
|
||||
let now_short = now.format("%Y%m%d").to_string();
|
||||
|
||||
let mut default_headers = HashMap::<String, String>::new();
|
||||
default_headers.insert(
|
||||
"host".to_string(),
|
||||
url.host().expect("missing host").to_string(),
|
||||
);
|
||||
default_headers.insert("x-amz-date".to_string(), now_long.clone());
|
||||
default_headers.insert("x-amz-content-sha256".to_string(), hash("".to_string()));
|
||||
|
||||
Self {
|
||||
http_verb: HttpVerbs::Get,
|
||||
now_short,
|
||||
now_long,
|
||||
access_key_id,
|
||||
access_key_secret,
|
||||
url,
|
||||
headers: default_headers,
|
||||
region: aws_region,
|
||||
service: AwsService::S3,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_scope(&self) -> String {
|
||||
format!(
|
||||
"{}/{}/{}/aws4_request",
|
||||
self.now_short, self.region, self.service
|
||||
)
|
||||
}
|
||||
|
||||
fn get_signing_key(&self) -> Vec<u8> {
|
||||
let date_key = digest_raw(
|
||||
format!("AWS4{}", self.access_key_secret).into_bytes(),
|
||||
&self.now_short,
|
||||
);
|
||||
let date_region_key = digest_raw(date_key, &self.region.to_string());
|
||||
let date_region_service_key = digest_raw(date_region_key, &self.service.to_string());
|
||||
digest_raw(date_region_service_key, "aws4_request")
|
||||
}
|
||||
|
||||
fn get_canonical_request(&self) -> String {
|
||||
let qs = self
|
||||
.url
|
||||
.query_pairs()
|
||||
.into_iter()
|
||||
.map(|(key, val)| {
|
||||
format!(
|
||||
"{}={}",
|
||||
urlencoding::encode(&key),
|
||||
urlencoding::encode(&val)
|
||||
)
|
||||
})
|
||||
.collect::<Vec<String>>()
|
||||
.join("&");
|
||||
|
||||
let mut headers = self
|
||||
.headers
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|(key, val)| format!("{}:{}", key.to_lowercase(), val.trim()))
|
||||
.collect::<Vec<String>>();
|
||||
headers.sort();
|
||||
let headers = headers.join("\n");
|
||||
|
||||
let mut signed_headers = self.headers.clone().into_keys().collect::<Vec<String>>();
|
||||
signed_headers.sort();
|
||||
let signed_headers = signed_headers.join(";");
|
||||
|
||||
let canonical_request = format!(
|
||||
"{}\n{}\n{}\n{}\n\n{}\n{}",
|
||||
self.http_verb,
|
||||
self.url.path(),
|
||||
qs,
|
||||
headers,
|
||||
signed_headers,
|
||||
hash("".to_string())
|
||||
);
|
||||
|
||||
canonical_request
|
||||
}
|
||||
|
||||
fn get_signature(&self) -> String {
|
||||
let canon = hash(self.get_canonical_request());
|
||||
|
||||
let unsigned = format!(
|
||||
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
|
||||
self.now_long,
|
||||
self.get_scope(),
|
||||
canon
|
||||
);
|
||||
|
||||
hex::encode(digest_raw(self.get_signing_key(), &unsigned))
|
||||
}
|
||||
|
||||
pub fn get_authorization_header(&self) -> String {
|
||||
let mut signed_headers = self.headers.clone().into_keys().collect::<Vec<String>>();
|
||||
signed_headers.sort();
|
||||
let signed_headers = signed_headers.join(";");
|
||||
format!(
|
||||
"AWS4-HMAC-SHA256 Credential={}/{},SignedHeaders={},Signature={}",
|
||||
self.access_key_id,
|
||||
self.get_scope(),
|
||||
signed_headers,
|
||||
self.get_signature()
|
||||
)
|
||||
}
|
||||
|
||||
fn get_header_map(&self) -> HeaderMap {
|
||||
let mut header_map = HeaderMap::new();
|
||||
self.headers.iter().for_each(|(key, val)| {
|
||||
header_map.insert(
|
||||
HeaderName::try_from(key).expect("unknown header"),
|
||||
val.parse().unwrap(),
|
||||
);
|
||||
});
|
||||
header_map.insert(
|
||||
reqwest::header::USER_AGENT,
|
||||
"sigv4-test/0.1.0".parse().unwrap(),
|
||||
);
|
||||
header_map.insert(
|
||||
reqwest::header::AUTHORIZATION,
|
||||
self.get_authorization_header().parse().unwrap(),
|
||||
);
|
||||
|
||||
header_map
|
||||
}
|
||||
|
||||
pub async fn send(&self) -> Result<reqwest::Response, reqwest::Error> {
|
||||
let header_map = self.get_header_map();
|
||||
|
||||
Client::new()
|
||||
.get(self.url.to_string())
|
||||
.headers(header_map)
|
||||
.send()
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn hash(val: String) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(val.as_bytes());
|
||||
hex::encode(hasher.finalize())
|
||||
}
|
||||
|
||||
fn digest_raw(key: Vec<u8>, val: &str) -> Vec<u8> {
|
||||
let mut mac =
|
||||
Hmac::<Sha256>::new_from_slice(key.as_slice()).expect("HMAC can take key of any size");
|
||||
mac.update(val.as_bytes());
|
||||
|
||||
mac.finalize().into_bytes().to_vec()
|
||||
}
|
40
src/types.rs
Normal file
40
src/types.rs
Normal file
|
@ -0,0 +1,40 @@
|
|||
use std::fmt::{Display, Formatter};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum AwsRegion {
|
||||
UsEast1,
|
||||
}
|
||||
|
||||
impl Display for AwsRegion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AwsRegion::UsEast1 => write!(f, "us-east-1"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum AwsService {
|
||||
S3,
|
||||
}
|
||||
|
||||
impl Display for AwsService {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AwsService::S3 => write!(f, "s3"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||
pub enum HttpVerbs {
|
||||
Get,
|
||||
}
|
||||
|
||||
impl Display for HttpVerbs {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
HttpVerbs::Get => write!(f, "GET"),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue