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