From 8ce5206dd24b5d965c35dba680af910236bb5cb2 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 3 Sep 2023 16:31:31 +0800 Subject: [PATCH] feat: init commit --- Cargo.toml | 4 ++ src/main.rs | 3 + src/v4/access_keys.rs | 25 +++++++++ src/v4/algorithm.rs | 60 ++++++++++++++++++++ src/v4/aliyun_util.rs | 127 ++++++++++++++++++++++++++++++++++++++++++ src/v4/common_util.rs | 39 +++++++++++++ src/v4/constants.rs | 18 ++++++ src/v4/mod.rs | 5 ++ 8 files changed, 281 insertions(+) create mode 100644 src/v4/access_keys.rs create mode 100644 src/v4/algorithm.rs create mode 100644 src/v4/aliyun_util.rs create mode 100644 src/v4/common_util.rs create mode 100644 src/v4/constants.rs create mode 100644 src/v4/mod.rs diff --git a/Cargo.toml b/Cargo.toml index e7f707f..e053c78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,5 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +hex = "0.4.3" hmac-sha256 = "1.1.7" hmac-sm3 = "0.1.0" +sha256 = "1.4.0" +sm3 = "0.4.2" +zeroize = "1.6.0" diff --git a/src/main.rs b/src/main.rs index e7a11a9..d51f7ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +mod v4; +// mod v4_algorithm; + fn main() { println!("Hello, world!"); } diff --git a/src/v4/access_keys.rs b/src/v4/access_keys.rs new file mode 100644 index 0000000..02b3fe2 --- /dev/null +++ b/src/v4/access_keys.rs @@ -0,0 +1,25 @@ +use zeroize::Zeroize; + +pub struct AccessKey { + pub access_key_id: String, + pub access_key_secret: String, +} + +impl Drop for AccessKey { + fn drop(&mut self) { + self.access_key_id.zeroize(); + self.access_key_secret.zeroize(); + } +} + +pub struct DerivedAccessKey { + pub access_key_id: String, + pub derived_access_key_secret: Vec, +} + +impl Drop for DerivedAccessKey { + fn drop(&mut self) { + self.access_key_id.zeroize(); + self.derived_access_key_secret.zeroize(); + } +} diff --git a/src/v4/algorithm.rs b/src/v4/algorithm.rs new file mode 100644 index 0000000..9d84461 --- /dev/null +++ b/src/v4/algorithm.rs @@ -0,0 +1,60 @@ +use sm3::{Digest, Sm3}; + +use crate::v4::constants::{ACS4_HMAC_SHA256, ACS4_HMAC_SM3}; + +#[derive(Clone, Copy)] +pub enum SignAlgorithm { + Sha256, + Sm3, +} + +impl SignAlgorithm { + pub fn digest(&self, messge: &[u8]) -> String { + inner_digest(self, messge) + } + + pub fn hmac_sign(&self, message: &[u8], key: &[u8]) -> Vec { + inner_hmac_sign(self, message, key) + } + + pub fn as_aliyun_name(&self) -> &'static str { + match self { + SignAlgorithm::Sha256 => ACS4_HMAC_SHA256, + SignAlgorithm::Sm3 => ACS4_HMAC_SM3, + } + } +} + +fn inner_digest(sign_algorithm: &SignAlgorithm, message: &[u8]) -> String { + match sign_algorithm { + SignAlgorithm::Sha256 => sha256::digest(message), + SignAlgorithm::Sm3 => { + let mut sm3 = Sm3::new(); + sm3.update(&message); + hex::encode(sm3.finalize().as_slice()) + } + } +} + +fn inner_hmac_sign(sign_algorithm: &SignAlgorithm, message: &[u8], key: &[u8]) -> Vec { + match sign_algorithm { + SignAlgorithm::Sha256 => { + let mut hsha256 = hmac_sha256::HMAC::new(key); + hsha256.update(message); + hsha256.finalize().to_vec() + } + SignAlgorithm::Sm3 => hmac_sm3::hmac_sm3(key, message) + } +} + +#[test] +fn test_digest() { + assert_eq!( + "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e", + SignAlgorithm::Sha256.digest(b"Hello World") + ); + assert_eq!( + "77015816143ee627f4fa410b6dad2bdb9fcbdf1e061a452a686b8711a484c5d7", + SignAlgorithm::Sm3.digest(b"Hello World") + ); +} diff --git a/src/v4/aliyun_util.rs b/src/v4/aliyun_util.rs new file mode 100644 index 0000000..a485188 --- /dev/null +++ b/src/v4/aliyun_util.rs @@ -0,0 +1,127 @@ +use std::collections::BTreeMap; + +use sm3::Digest; + +use crate::v4::access_keys::DerivedAccessKey; +use crate::v4::algorithm::SignAlgorithm; +use crate::v4::common_util::{join_slices, percent_encode}; +use crate::v4::constants::{ALIYUN_V4, ALIYUN_V4_REQUEST, HEADER_CONTENT_TYPE, HEADER_HOST, HEADER_X_ACS_PREFIX, REGION_CENTER}; + +fn get_authorization(sign_algorithm: SignAlgorithm, + access_key: &DerivedAccessKey, + date: &str, region: &str, product: &str, + pathname: &str, + method: &str, + query: &BTreeMap, + headers: &BTreeMap, + payload: &str) -> String { + let signature = get_signature(sign_algorithm, &access_key.derived_access_key_secret, + pathname, method, query, headers, payload); + let signed_headers_str = get_signed_headers(headers).iter().map(|(k, _)| k.as_str()).collect::>().join(";"); + + let mut authorization = String::with_capacity(512); + authorization.push_str(sign_algorithm.as_aliyun_name()); + authorization.push_str(" Credential="); + authorization.push_str(&access_key.access_key_id); + authorization.push('/'); + authorization.push_str(date); + authorization.push('/'); + authorization.push_str(region); + authorization.push('/'); + authorization.push_str(product); + authorization.push_str("/aliyun_v4_request,SignedHeaders="); + authorization.push_str(&signed_headers_str); + authorization.push_str(",Signature="); + authorization.push_str(&signature); + authorization +} + +fn get_signature(sign_algorithm: SignAlgorithm, + signing_key: &[u8], + pathname: &str, + method: &str, + query: &BTreeMap, + headers: &BTreeMap, + payload: &str) -> String { + let canonical_uri = if pathname.is_empty() { "/" } else { pathname }; + let canonicalized_resource = build_canonicalized_resource(query); + let canonicalized_headers = build_canonicalized_headers(headers); + let signed_headers_str = get_signed_headers(headers).iter().map(|(k, _)| k.as_str()).collect::>().join(";"); + + let mut string_to_sign = String::new(); + string_to_sign.push_str(method); + string_to_sign.push('\n'); + string_to_sign.push_str(canonical_uri); + string_to_sign.push('\n'); + string_to_sign.push_str(&canonicalized_resource); + string_to_sign.push('\n'); + string_to_sign.push_str(&canonicalized_headers); + string_to_sign.push('\n'); + string_to_sign.push_str(&signed_headers_str); + string_to_sign.push('\n'); + string_to_sign.push_str(payload); + + let string_to_sign_digest_hex = sign_algorithm.digest(string_to_sign.as_bytes()); + string_to_sign.push_str(sign_algorithm.as_aliyun_name()); + string_to_sign.push('\n'); + string_to_sign.push_str(&string_to_sign_digest_hex); + + hex::encode(sign_algorithm.hmac_sign(string_to_sign.as_bytes(), signing_key)) +} + +fn get_signing_key(sign_algorithm: SignAlgorithm, secret: &str, date: &str, region: &str, product: &str) -> Vec { + let sc1 = join_slices(ALIYUN_V4.as_bytes(), secret.as_bytes()); + let sc2 = sign_algorithm.hmac_sign(date.as_bytes(), &sc1); + let sc3 = sign_algorithm.hmac_sign(region.as_bytes(), &sc2); + let sc4 = sign_algorithm.hmac_sign(product.as_bytes(), &sc3); + sign_algorithm.hmac_sign(ALIYUN_V4_REQUEST.as_bytes(), &sc4) +} + +fn get_region(product: &str, endpoint: &str) -> String { + if product.is_empty() || endpoint.is_empty() { + return REGION_CENTER.into(); + } + let popcode = product.to_lowercase(); + let mut region = endpoint.replace(popcode.as_str(), ""); + region = region.replace("aliyuncs.com", ""); + region = region.replace(".", ""); + + if region.is_empty() { REGION_CENTER.into() } else { region } +} + +fn build_canonicalized_resource(query: &BTreeMap) -> String { + let mut canonicalized_resources = Vec::with_capacity(query.len()); + for (key, value) in query { + let mut resource = String::with_capacity(key.len() + 1 + value.len()); + resource.push_str(&percent_encode(key)); + if !value.is_empty() { + resource.push('='); + resource.push_str(&percent_encode(value)); + } + canonicalized_resources.push(resource); + } + canonicalized_resources.join("&") +} + +fn build_canonicalized_headers(headers: &BTreeMap) -> String { + let mut canonicalized_headers = String::new(); + let signed_headers = get_signed_headers(headers); + for (key, value) in &signed_headers { + canonicalized_headers.push_str(key); + canonicalized_headers.push(':'); + canonicalized_headers.push_str(value); + canonicalized_headers.push('\n'); + } + canonicalized_headers +} + +fn get_signed_headers(headers: &BTreeMap) -> Vec<(String, &String)> { + let mut signed_headers = Vec::with_capacity(headers.len()); + for (key, value) in headers { + let lower_key = key.to_lowercase(); + if lower_key.starts_with(HEADER_X_ACS_PREFIX) || (lower_key == HEADER_HOST) || (lower_key == HEADER_CONTENT_TYPE) { + signed_headers.push((lower_key, value)); + } + } + signed_headers +} diff --git a/src/v4/common_util.rs b/src/v4/common_util.rs new file mode 100644 index 0000000..c030fb7 --- /dev/null +++ b/src/v4/common_util.rs @@ -0,0 +1,39 @@ +pub(crate) fn percent_encode(message: &str) -> String { + let mut encoded = String::with_capacity(message.len() + 16); + message.as_bytes().iter().for_each(|b| { + match b { + b if (b >= &b'a' && b <= &b'z') + || (b >= &b'A' && b <= &b'Z') + || (b >= &b'0' && b <= &b'9') => encoded.push(*b as char), + b'~' | b'_' | b'-' | b'.' => encoded.push(*b as char), + _ => encoded.push_str(&format!("%{:X}", b)), + } + }); + encoded +} + +#[inline] +pub(crate) fn join_slices(part1: &[u8], part2: &[u8]) -> Vec { + let mut joined = part1.to_vec(); + joined.extend_from_slice(part2); + joined +} + +#[test] +fn test_percent_encode() { + assert_eq!("0123456789", percent_encode("0123456789")); + assert_eq!("abcdefghijklmnopqrstuvwxyz", percent_encode("abcdefghijklmnopqrstuvwxyz")); + assert_eq!("ABCDEFGHIJKLMNOPQRSTUVWXYZ", percent_encode("ABCDEFGHIJKLMNOPQRSTUVWXYZ")); + assert_eq!("Hello%20World%21", percent_encode("Hello World!")); + assert_eq!("~%21%40%23%24%25%5E%26%2A%28%29_%2B-%3D", percent_encode("~!@#$%^&*()_+-=")); + assert_eq!("%E8%BF%99%E6%98%AF%E4%B8%80%E4%B8%AA%E6%B5%8B%E8%AF%95", percent_encode("这是一个测试")); + assert_eq!("%F0%9F%98%84", percent_encode("😄")); + assert_eq!("%2C.%2F%3C%3E%3F%3B%27%3A%22%5B%5D%5C%7B%7D%7C", percent_encode(",./<>?;':\"[]\\{}|")); + assert_eq!("%21%27%28%29%2A-._~", percent_encode("!'()*-._~")); + assert_eq!("%21%23%24%26%27%28%29%2A%2B%2C-.%2F%3A%3B%3D%3F%40_~", percent_encode("!#$&'()*+,-./:;=?@_~")); +} + +#[test] +fn test_join_slices() { + assert_eq!(&b"Hello World"[..], &join_slices(b"Hello", b" World")[..]) +} diff --git a/src/v4/constants.rs b/src/v4/constants.rs new file mode 100644 index 0000000..b607e26 --- /dev/null +++ b/src/v4/constants.rs @@ -0,0 +1,18 @@ +pub(crate) const ACS4_HMAC_SHA256: &str = "ACS4-HMAC-SHA256"; +pub(crate) const ACS4_HMAC_SM3: &str = "ACS4-HMAC-SM3"; + +pub(crate) const REGION_CENTER: &str = "center"; +pub(crate) const ALIYUN_V4: &str = "aliyun_v4"; +pub(crate) const ALIYUN_V4_REQUEST: &str = "aliyun_v4_request"; + +pub(crate) const HEADER_X_ACS_PREFIX: &str = "x-acs-"; +pub(crate) const HEADER_HOST: &str = "host"; +pub(crate) const HEADER_ACCEPT: &str = "accept"; +pub(crate) const HEADER_USER_AGENT: &str = "user-agent"; +pub(crate) const HEADER_CONTENT_TYPE: &str = "content-type"; +pub(crate) const HEADER_X_ASC_VERSION: &str = "x-acs-version"; +pub(crate) const HEADER_X_ASC_ACTION: &str = "x-acs-action"; +pub(crate) const HEADER_X_ASC_DATE: &str = "x-acs-date"; +pub(crate) const HEADER_X_ASC_SIGNATURE_NONCE: &str = "x-acs-signature-nonce"; + +pub(crate) const CONTENT_TYPE_APPLICATION_JSON: &str = "application/json"; diff --git a/src/v4/mod.rs b/src/v4/mod.rs new file mode 100644 index 0000000..19eaa72 --- /dev/null +++ b/src/v4/mod.rs @@ -0,0 +1,5 @@ +mod constants; +mod algorithm; +mod access_keys; +mod common_util; +mod aliyun_util;