mirror of
https://github.com/minio/minio-rs.git
synced 2025-12-06 15:26:51 +08:00
More fixes to signature v4 implementation
This commit is contained in:
parent
46f0aa8706
commit
393e1645b4
@ -6,3 +6,5 @@ edition = "2018"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
hyper = "0.12.28"
|
hyper = "0.12.28"
|
||||||
|
ring = "0.14.6"
|
||||||
|
time = "0.1.42"
|
||||||
|
|||||||
67
src/minio.rs
67
src/minio.rs
@ -1,15 +1,19 @@
|
|||||||
use hyper::Uri;
|
mod sign;
|
||||||
use std::{env, string::String};
|
|
||||||
|
|
||||||
#[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 {
|
pub struct Credentials {
|
||||||
access_key: String,
|
access_key: String,
|
||||||
secret_key: String,
|
secret_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Credentials{
|
impl Credentials {
|
||||||
pub fn new(ak: &str, sk: &str) -> Credentials {
|
pub fn new(ak: &str, sk: &str) -> Credentials {
|
||||||
Credentials{
|
Credentials {
|
||||||
access_key: ak.to_string(),
|
access_key: ak.to_string(),
|
||||||
secret_key: sk.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"));
|
let (ak, sk) = (env::var("MINIO_ACCESS_KEY"), env::var("MINIO_SECRET_KEY"));
|
||||||
match (ak, sk) {
|
match (ak, sk) {
|
||||||
(Ok(ak), Ok(sk)) => Ok(Credentials::new(ak.as_str(), sk.as_str())),
|
(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)]
|
#[derive(Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
server: Uri,
|
server: Uri,
|
||||||
credentials: Option<Credentials>,
|
region: Region,
|
||||||
|
pub credentials: Option<Credentials>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client{
|
impl Client {
|
||||||
pub fn new(server: &str) -> Result<Client, Err> {
|
pub fn new(server: &str) -> Result<Client, Err> {
|
||||||
let v = server.parse::<Uri>();
|
let v = server.parse::<Uri>();
|
||||||
match v {
|
match v {
|
||||||
Ok(s) => if s.host().is_none() {
|
Ok(s) => {
|
||||||
Err(Err::InvalidUrl("no host specified!".to_string()))
|
if s.host().is_none() {
|
||||||
} else if s.scheme_str() != Some("http") && s.scheme_str() != Some("https") {
|
Err(Err::InvalidUrl("no host specified!".to_string()))
|
||||||
Err(Err::InvalidUrl("invalid scheme!".to_string()))
|
} else if s.scheme_str() != Some("http") && s.scheme_str() != Some("https") {
|
||||||
} else {
|
Err(Err::InvalidUrl("invalid scheme!".to_string()))
|
||||||
Ok(Client{
|
} else {
|
||||||
server: s,
|
Ok(Client {
|
||||||
credentials: None,
|
server: s,
|
||||||
})
|
region: String::from(""),
|
||||||
},
|
credentials: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(err) => Err(Err::InvalidUrl(err.to_string())),
|
Err(err) => Err(Err::InvalidUrl(err.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,11 +70,28 @@ impl Client{
|
|||||||
self.credentials = Some(credentials);
|
self.credentials = Some(credentials);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_region(&mut self, r: Region) {
|
||||||
|
self.region = r;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_play_client() -> Client {
|
pub fn get_play_client() -> Client {
|
||||||
Client {
|
Client {
|
||||||
server: "https://play.min.io:9000".parse::<Uri>().unwrap(),
|
server: "https://play.min.io:9000".parse::<Uri>().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<String>,
|
||||||
|
object: Option<String>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
query: HashMap<String, Option<String>>,
|
||||||
|
body: Body,
|
||||||
|
ts: Tm,
|
||||||
}
|
}
|
||||||
|
|||||||
178
src/minio/sign.rs
Normal file
178
src/minio/sign.rs
Normal file
@ -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<HeaderName> = 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, Option<String>>) -> String {
|
||||||
|
let mut hs: Vec<(String, Option<String>)> = q.clone().drain().collect();
|
||||||
|
hs.sort();
|
||||||
|
let vs: Vec<String> = 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<u8> {
|
||||||
|
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<u8>) -> 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<Vec<(HeaderName, HeaderValue)>> {
|
||||||
|
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::<Vec<String>>()
|
||||||
|
.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]
|
||||||
|
})
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user