From 393e1645b437a53cbd62848dbb20a01e7a4efed1 Mon Sep 17 00:00:00 2001 From: Aditya Manthramurthy Date: Wed, 15 May 2019 16:28:39 -0700 Subject: [PATCH] More fixes to signature v4 implementation --- Cargo.toml | 2 + src/minio.rs | 67 +++++++++++------ src/minio/sign.rs | 178 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 227 insertions(+), 20 deletions(-) create mode 100644 src/minio/sign.rs diff --git a/Cargo.toml b/Cargo.toml index f54861a..17b51c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,3 +6,5 @@ edition = "2018" [dependencies] hyper = "0.12.28" +ring = "0.14.6" +time = "0.1.42" diff --git a/src/minio.rs b/src/minio.rs index e45bf81..2133778 100644 --- a/src/minio.rs +++ b/src/minio.rs @@ -1,15 +1,19 @@ -use hyper::Uri; -use std::{env, string::String}; +mod sign; -#[derive(Debug)] +use hyper::{body::Body, header::HeaderMap, Method, Uri}; +use std::collections::HashMap; +use std::{env, string::String}; +use time::Tm; + +#[derive(Debug, Clone)] pub struct Credentials { access_key: String, secret_key: String, } -impl Credentials{ +impl Credentials { pub fn new(ak: &str, sk: &str) -> Credentials { - Credentials{ + Credentials { access_key: ak.to_string(), secret_key: sk.to_string(), } @@ -19,7 +23,9 @@ impl Credentials{ let (ak, sk) = (env::var("MINIO_ACCESS_KEY"), env::var("MINIO_SECRET_KEY")); match (ak, sk) { (Ok(ak), Ok(sk)) => Ok(Credentials::new(ak.as_str(), sk.as_str())), - _ => Err(Err::InvalidEnv("Missing MINIO_ACCESS_KEY or MINIO_SECRET_KEY environment variables".to_string())), + _ => Err(Err::InvalidEnv( + "Missing MINIO_ACCESS_KEY or MINIO_SECRET_KEY environment variables".to_string(), + )), } } } @@ -35,23 +41,27 @@ pub enum Err { #[derive(Debug)] pub struct Client { server: Uri, - credentials: Option, + region: Region, + pub credentials: Option, } -impl Client{ +impl Client { pub fn new(server: &str) -> Result { let v = server.parse::(); match v { - Ok(s) => if s.host().is_none() { - Err(Err::InvalidUrl("no host specified!".to_string())) - } else if s.scheme_str() != Some("http") && s.scheme_str() != Some("https") { - Err(Err::InvalidUrl("invalid scheme!".to_string())) - } else { - Ok(Client{ - server: s, - credentials: None, - }) - }, + Ok(s) => { + if s.host().is_none() { + Err(Err::InvalidUrl("no host specified!".to_string())) + } else if s.scheme_str() != Some("http") && s.scheme_str() != Some("https") { + Err(Err::InvalidUrl("invalid scheme!".to_string())) + } else { + Ok(Client { + server: s, + region: String::from(""), + credentials: None, + }) + } + } Err(err) => Err(Err::InvalidUrl(err.to_string())), } } @@ -60,11 +70,28 @@ impl Client{ self.credentials = Some(credentials); } + pub fn set_region(&mut self, r: Region) { + self.region = r; + } + pub fn get_play_client() -> Client { Client { server: "https://play.min.io:9000".parse::().unwrap(), - credentials: Some(Credentials::new("Q3AM3UQ867SPQQA43P2F", "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG")), + region: String::from(""), + credentials: Some(Credentials::new( + "Q3AM3UQ867SPQQA43P2F", + "zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG", + )), } } - +} + +pub struct S3Req { + method: Method, + bucket: Option, + object: Option, + headers: HeaderMap, + query: HashMap>, + body: Body, + ts: Tm, } diff --git a/src/minio/sign.rs b/src/minio/sign.rs new file mode 100644 index 0000000..77b4e6e --- /dev/null +++ b/src/minio/sign.rs @@ -0,0 +1,178 @@ +use hyper::header::{ + HeaderMap, HeaderName, HeaderValue, AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, USER_AGENT, +}; +use ring::{digest, hmac}; +use std::collections::{HashMap, HashSet}; +use time::Tm; + +use crate::minio; + +fn aws_format_time(t: &Tm) -> String { + t.strftime("%Y%m%dT%H%M%SZ").unwrap().to_string() +} + +fn mk_scope(t: &Tm, r: &minio::Region) -> String { + let scope_time = t.strftime("%Y%m%d").unwrap().to_string(); + format!("{}/{}/s3/aws4_request", scope_time, r) +} + +// Returns list of SORTED headers that will be signed. TODO: verify +// that input headermap contains only ASCII valued headers +fn get_headers_to_sign(h: &HeaderMap) -> Vec<(String, String)> { + let mut ignored_hdrs: HashSet = HashSet::new(); + ignored_hdrs.insert(AUTHORIZATION); + ignored_hdrs.insert(CONTENT_LENGTH); + ignored_hdrs.insert(CONTENT_TYPE); + ignored_hdrs.insert(USER_AGENT); + let mut res: Vec<(String, String)> = h + .iter() + .map(|(x, y)| (x.clone(), y.clone())) + .filter(|(x, _)| !ignored_hdrs.contains(x)) + .map(|(x, y)| { + ( + x.as_str().to_string(), + y.to_str() + .expect("Unexpected non-ASCII header value!") + .to_string(), + ) + }) + .collect(); + res.sort(); + res +} + +fn uri_encode(c: char, encode_slash: bool) -> String { + if c == '/' { + if encode_slash { + "%2F".to_string() + } else { + "/".to_string() + } + } else if c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.' || c == '~' { + c.to_string() + } else { + let mut b = [0; 8]; + let cs = c.encode_utf8(&mut b).as_bytes(); + cs.iter().map(|x| format!("%{:X}", x)).collect() + } +} + +fn uri_encode_str(s: &str, encode_slash: bool) -> String { + s.chars().map(|x| uri_encode(x, encode_slash)).collect() +} + +fn get_canonical_querystr(q: &HashMap>) -> String { + let mut hs: Vec<(String, Option)> = q.clone().drain().collect(); + hs.sort(); + let vs: Vec = hs + .drain(..) + .map(|(x, y)| match y { + Some(s) => uri_encode_str(&x, true) + "=" + &uri_encode_str(&s, true), + None => uri_encode_str(&x, true), + }) + .collect(); + vs[..].join("&") +} + +fn mk_path(r: &minio::S3Req) -> String { + let mut res: String = String::from(""); + if let Some(s) = &r.bucket { + res.push_str(&s); + if let Some(o) = &r.object { + let s1 = format!("/{}", o); + res.push_str(&s1); + } + }; + res +} + +fn get_canonical_request( + r: &minio::S3Req, + hdrs_to_use: &Vec<(String, String)>, + signed_hdrs_str: &str, +) -> String { + let path_str = mk_path(r); + let canonical_qstr = get_canonical_querystr(&r.query); + let canonical_hdrs: String = hdrs_to_use + .iter() + .map(|(x, y)| format!("{}:{}\n", x.clone(), y.clone())) + .collect(); + + // FIXME: using only unsigned payload for now - need to add + // hashing of payload. + let payload_hash_str = String::from("UNSIGNED-PAYLOAD"); + let res = vec![ + r.method.to_string(), + uri_encode_str(&path_str, false), + canonical_qstr, + canonical_hdrs, + signed_hdrs_str.to_string(), + payload_hash_str, + ]; + res.join("\n") +} + +fn string_to_sign(ts: &Tm, scope: &str, canonical_request: &str) -> String { + let sha256_digest: String = digest::digest(&digest::SHA256, canonical_request.as_bytes()) + .as_ref() + .iter() + .map(|x| format!("{:X}", x)) + .collect(); + vec![ + "AWS4-HMAC-SHA256", + &aws_format_time(&ts), + scope, + &sha256_digest, + ] + .join("\n") +} + +fn hmac_sha256(msg: &str, key: &[u8]) -> hmac::Signature { + let key = hmac::SigningKey::new(&digest::SHA256, key); + hmac::sign(&key, msg.as_bytes()) +} + +fn get_signing_key(ts: &Tm, region: &str, secret_key: &str) -> Vec { + let kstr = format!("AWS4{}", secret_key); + let s1 = hmac_sha256(&aws_format_time(&ts), kstr.as_bytes()); + let s2 = hmac_sha256(®ion, s1.as_ref()); + let s3 = hmac_sha256("s3", s2.as_ref()); + let s4 = hmac_sha256("aws4_request", s3.as_ref()); + // FIXME: can this be done better? + s4.as_ref().iter().map(|x| x.clone()).collect() +} + +fn compute_sign(str_to_sign: &str, key: &Vec) -> String { + let s1 = hmac_sha256(&str_to_sign, key.as_slice()); + s1.as_ref().iter().map(|x| format!("{:X}", x)).collect() +} + +pub fn sign_v4(r: &minio::S3Req, c: &minio::Client) -> Option> { + let creds = c.credentials.clone(); + creds.map(|creds| { + let scope = mk_scope(&r.ts, &c.region); + let date_hdr = ( + HeaderName::from_static("x-amz-date"), + HeaderValue::from_str(&aws_format_time(&r.ts)).unwrap(), + ); + let mut hmap = r.headers.clone(); + hmap.insert(date_hdr.0.clone(), date_hdr.1.clone()); + let hs = get_headers_to_sign(&hmap); + let signed_hdrs_str: String = hs + .iter() + .map(|(x, _)| x.clone()) + .collect::>() + .join(";"); + let cr = get_canonical_request(r, &hs, &signed_hdrs_str); + let s2s = string_to_sign(&r.ts, &scope, &cr); + let skey = get_signing_key(&r.ts, &c.region, &creds.secret_key); + let signature = compute_sign(&s2s, &skey); + + let auth_hdr_val = format!( + "AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}", + &creds.access_key, &scope, &signed_hdrs_str, &signature, + ); + let auth_hdr = (AUTHORIZATION, HeaderValue::from_str(&auth_hdr_val).unwrap()); + vec![auth_hdr, date_hdr] + }) +}