Initial commit

This commit is contained in:
Daan Boerlage 2023-11-14 23:16:59 +01:00
commit 9373fe944d
Signed by: daan
GPG key ID: FCE070E1E4956606
7 changed files with 1726 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

1428
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View 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"),
}
}
}