blob: ca49a2836a57fddf33890892aeed60ab019d506f [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! DLF Request Signer implementations for Alibaba Cloud Data Lake Formation.
//!
//! This module provides two signature algorithms for authenticating requests
//! to Alibaba Cloud Data Lake Formation (DLF) service:
//!
//! # Signature Algorithms
//!
//! ## 1. DLF4-HMAC-SHA256 (Default Signer)
//!
//! Used for VPC endpoints (e.g., `*-vpc.dlf.aliyuncs.com`).
//!
//! **Algorithm Overview:**
//! 1. Build a canonical request string from HTTP method, path, query params, and headers
//! 2. Create a string-to-sign with algorithm, timestamp, credential scope, and hashed canonical request
//! 3. Derive a signing key through multiple HMAC-SHA256 operations
//! 4. Calculate the signature and construct the Authorization header
//!
//! **Signing Key Derivation:**
//! ```text
//! kSecret = "aliyun_v4" + AccessKeySecret
//! kDate = HMAC-SHA256(kSecret, Date)
//! kRegion = HMAC-SHA256(kDate, Region)
//! kService = HMAC-SHA256(kRegion, "DlfNext")
//! kSigning = HMAC-SHA256(kService, "aliyun_v4_request")
//! ```
//!
//! **Authorization Header Format:**
//! ```text
//! DLF4-HMAC-SHA256 Credential=AccessKeyId/Date/Region/DlfNext/aliyun_v4_request,Signature=hex_signature
//! ```
//!
//! ## 2. HMAC-SHA1 (OpenAPI Signer)
//!
//! Used for public network endpoints (e.g., `dlfnext.*.aliyuncs.com`).
//! Follows Alibaba Cloud ROA v2 signature style.
//!
//! **Algorithm Overview:**
//! 1. Build canonicalized headers from x-acs-* headers
//! 2. Build canonicalized resource from path and query params
//! 3. Create string-to-sign with method, headers, and resource
//! 4. Calculate signature using HMAC-SHA1
//!
//! **Authorization Header Format:**
//! ```text
//! acs AccessKeyId:base64_signature
//! ```
//!
//! # References
//!
//! - [Alibaba Cloud API Signature](https://help.aliyun.com/document_detail/315526.html)
//! - [DLF OpenAPI](https://help.aliyun.com/document_detail/197826.html)
//!
//! # Usage
//!
//! The signer is automatically selected based on the endpoint URI:
//! - VPC endpoints → `DLFDefaultSigner` (DLF4-HMAC-SHA256)
//! - Public endpoints with "dlfnext" or "openapi" → `DLFOpenApiSigner` (HMAC-SHA1)
use std::collections::HashMap;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine};
use chrono::{DateTime, Utc};
use hmac::{Hmac, Mac};
use md5::Md5;
use sha1::Sha1;
use sha2::{Digest, Sha256};
use uuid::Uuid;
use super::base::RESTAuthParameter;
use super::dlf_provider::DLFToken;
type HmacSha256 = Hmac<Sha256>;
type HmacSha1 = Hmac<Sha1>;
/// Trait for DLF request signers.
///
/// Different signers implement different signature algorithms for
/// authenticating requests to Alibaba Cloud DLF service.
///
/// # Implementations
///
/// - [`DLFDefaultSigner`]: Uses DLF4-HMAC-SHA256 for VPC endpoints
/// - [`DLFOpenApiSigner`]: Uses HMAC-SHA1 for public endpoints
pub trait DLFRequestSigner: Send + Sync {
/// Generate signature headers for the request.
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
host: &str,
) -> HashMap<String, String>;
/// Generate the Authorization header value.
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
host: &str,
sign_headers: &HashMap<String, String>,
) -> String;
#[allow(dead_code)]
/// Get the identifier for this signer.
fn identifier(&self) -> &str;
}
/// Default DLF signer using DLF4-HMAC-SHA256 algorithm.
///
/// This signer is used for VPC endpoints (e.g., `cn-hangzhou-vpc.dlf.aliyuncs.com`).
///
/// # Algorithm Details
///
/// The DLF4-HMAC-SHA256 algorithm is similar to AWS Signature Version 4:
///
/// 1. **Canonical Request**: Combine HTTP method, URI, query string, headers, and payload hash
/// 2. **String-to-Sign**: Include algorithm, timestamp, credential scope, and canonical request hash
/// 3. **Signing Key**: Derived through chained HMAC operations
/// 4. **Signature**: HMAC-SHA256 of string-to-sign using the signing key
///
/// # Required Headers
///
/// The following headers are included in the signature calculation:
/// - `content-md5`: MD5 hash of request body (if present)
/// - `content-type`: Media type (if body present)
/// - `x-dlf-content-sha256`: Always "UNSIGNED-PAYLOAD"
/// - `x-dlf-date`: Request timestamp in format `%Y%m%dT%H%M%SZ`
/// - `x-dlf-version`: API version ("v1")
/// - `x-dlf-security-token`: Security token for temporary credentials (optional)
///
/// # Example
///
/// ```ignore
/// use paimon::api::auth::{DLFDefaultSigner, DLFRequestSigner};
///
/// let signer = DLFDefaultSigner::new("cn-hangzhou");
/// let headers = signer.sign_headers(Some(r#"{"key":"value"}"#), &Utc::now(), None, "dlf.aliyuncs.com");
/// ```
pub struct DLFDefaultSigner {
region: String,
}
impl DLFDefaultSigner {
pub const IDENTIFIER: &'static str = "default";
const VERSION: &'static str = "v1";
const SIGNATURE_ALGORITHM: &'static str = "DLF4-HMAC-SHA256";
const PRODUCT: &'static str = "DlfNext";
const REQUEST_TYPE: &'static str = "aliyun_v4_request";
const SIGNATURE_KEY: &'static str = "Signature";
const NEW_LINE: &'static str = "\n";
// Header keys
const DLF_CONTENT_MD5_HEADER_KEY: &'static str = "Content-MD5";
const DLF_CONTENT_TYPE_KEY: &'static str = "Content-Type";
const DLF_DATE_HEADER_KEY: &'static str = "x-dlf-date";
const DLF_SECURITY_TOKEN_HEADER_KEY: &'static str = "x-dlf-security-token";
const DLF_AUTH_VERSION_HEADER_KEY: &'static str = "x-dlf-version";
const DLF_CONTENT_SHA256_HEADER_KEY: &'static str = "x-dlf-content-sha256";
const DLF_CONTENT_SHA256_VALUE: &'static str = "UNSIGNED-PAYLOAD";
const AUTH_DATE_TIME_FORMAT: &'static str = "%Y%m%dT%H%M%SZ";
const MEDIA_TYPE: &'static str = "application/json";
const SIGNED_HEADERS: &'static [&'static str] = &[
"content-md5",
"content-type",
"x-dlf-content-sha256",
"x-dlf-date",
"x-dlf-version",
"x-dlf-security-token",
];
/// Create a new DLFDefaultSigner with the given region.
pub fn new(region: impl Into<String>) -> Self {
Self {
region: region.into(),
}
}
fn md5_base64(raw: &str) -> String {
let mut hasher = Md5::new();
hasher.update(raw.as_bytes());
let hash = hasher.finalize();
BASE64_STANDARD.encode(hash)
}
fn hmac_sha256(key: &[u8], data: &str) -> Vec<u8> {
let mut mac = HmacSha256::new_from_slice(key).expect("HMAC can take key of any size");
mac.update(data.as_bytes());
mac.finalize().into_bytes().to_vec()
}
fn sha256_hex(raw: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(raw.as_bytes());
hex::encode(hasher.finalize())
}
fn hex_encode(raw: &[u8]) -> String {
hex::encode(raw)
}
fn trim(value: &str) -> &str {
value.trim()
}
fn get_canonical_request(
&self,
rest_auth_parameter: &RESTAuthParameter,
headers: &HashMap<String, String>,
) -> String {
let mut parts = vec![
rest_auth_parameter.method.clone(),
rest_auth_parameter.path.clone(),
];
let canonical_query_string =
self.build_canonical_query_string(&rest_auth_parameter.parameters);
parts.push(canonical_query_string);
let sorted_headers = self.build_sorted_signed_headers_map(headers);
for (key, value) in sorted_headers {
parts.push(format!("{key}:{value}"));
}
let content_sha256 = headers
.get(Self::DLF_CONTENT_SHA256_HEADER_KEY)
.map(|s| s.as_str())
.unwrap_or(Self::DLF_CONTENT_SHA256_VALUE);
parts.push(content_sha256.to_string());
parts.join(Self::NEW_LINE)
}
fn build_canonical_query_string(&self, parameters: &HashMap<String, String>) -> String {
if parameters.is_empty() {
return String::new();
}
let mut sorted_params: Vec<_> = parameters.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_parts: Vec<String> = sorted_params
.iter()
.map(|(key, value)| {
let key = Self::trim(key);
if !value.is_empty() {
let value = Self::trim(value);
format!("{key}={value}")
} else {
key.to_string()
}
})
.collect();
query_parts.join("&")
}
fn build_sorted_signed_headers_map(
&self,
headers: &HashMap<String, String>,
) -> Vec<(String, String)> {
let mut sorted_headers: Vec<(String, String)> = headers
.iter()
.filter(|(key, _)| {
let lower_key = key.to_lowercase();
Self::SIGNED_HEADERS.contains(&lower_key.as_str())
})
.map(|(key, value)| (key.to_lowercase(), Self::trim(value).to_string()))
.collect();
sorted_headers.sort_by(|a, b| a.0.cmp(&b.0));
sorted_headers
}
}
impl DLFRequestSigner for DLFDefaultSigner {
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
_host: &str,
) -> HashMap<String, String> {
let mut sign_headers = HashMap::new();
let date_time = now.format(Self::AUTH_DATE_TIME_FORMAT).to_string();
sign_headers.insert(Self::DLF_DATE_HEADER_KEY.to_string(), date_time);
sign_headers.insert(
Self::DLF_CONTENT_SHA256_HEADER_KEY.to_string(),
Self::DLF_CONTENT_SHA256_VALUE.to_string(),
);
sign_headers.insert(
Self::DLF_AUTH_VERSION_HEADER_KEY.to_string(),
Self::VERSION.to_string(),
);
if let Some(body_content) = body {
if !body_content.is_empty() {
sign_headers.insert(
Self::DLF_CONTENT_TYPE_KEY.to_string(),
Self::MEDIA_TYPE.to_string(),
);
sign_headers.insert(
Self::DLF_CONTENT_MD5_HEADER_KEY.to_string(),
Self::md5_base64(body_content),
);
}
}
if let Some(token) = security_token {
sign_headers.insert(
Self::DLF_SECURITY_TOKEN_HEADER_KEY.to_string(),
token.to_string(),
);
}
sign_headers
}
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
_host: &str,
sign_headers: &HashMap<String, String>,
) -> String {
let date_time = sign_headers.get(Self::DLF_DATE_HEADER_KEY).unwrap();
let date = &date_time[..8];
let canonical_request = self.get_canonical_request(rest_auth_parameter, sign_headers);
let string_to_sign = [
Self::SIGNATURE_ALGORITHM.to_string(),
date_time.clone(),
format!(
"{}/{}/{}/{}",
date,
self.region,
Self::PRODUCT,
Self::REQUEST_TYPE
),
Self::sha256_hex(&canonical_request),
]
.join(Self::NEW_LINE);
// Derive signing key
let date_key = Self::hmac_sha256(
format!("aliyun_v4{}", token.access_key_secret).as_bytes(),
date,
);
let date_region_key = Self::hmac_sha256(&date_key, &self.region);
let date_region_service_key = Self::hmac_sha256(&date_region_key, Self::PRODUCT);
let signing_key = Self::hmac_sha256(&date_region_service_key, Self::REQUEST_TYPE);
let signature_bytes = Self::hmac_sha256(&signing_key, &string_to_sign);
let signature = Self::hex_encode(&signature_bytes);
format!(
"{} Credential={}/{}/{}/{}/{},{}={}",
Self::SIGNATURE_ALGORITHM,
token.access_key_id,
date,
self.region,
Self::PRODUCT,
Self::REQUEST_TYPE,
Self::SIGNATURE_KEY,
signature
)
}
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
/// DLF OpenAPI signer using HMAC-SHA1 algorithm.
///
/// This signer follows the Alibaba Cloud ROA v2 signature style and is used
/// for public network endpoints (e.g., `dlfnext.cn-asdnbhwf.aliyuncs.com`).
///
/// # Algorithm Details
///
/// The HMAC-SHA1 algorithm is the traditional Alibaba Cloud API signature method:
///
/// 1. **Canonicalized Headers**: Sort and format all `x-acs-*` headers
/// 2. **Canonicalized Resource**: URL-decoded path with sorted query parameters
/// 3. **String-to-Sign**: Combine HTTP method, standard headers, canonicalized headers, and resource
/// 4. **Signature**: Base64-encoded HMAC-SHA1 of string-to-sign
///
/// # Required Headers
///
/// The following headers are included in requests:
/// - `Date`: Request timestamp in RFC 1123 format
/// - `Accept`: Always "application/json"
/// - `Content-MD5`: MD5 hash of request body (if present)
/// - `Content-Type`: Always "application/json" (if body present)
/// - `Host`: Endpoint host
/// - `x-acs-signature-method`: Always "HMAC-SHA1"
/// - `x-acs-signature-nonce`: Unique UUID for each request
/// - `x-acs-signature-version`: Always "1.0"
/// - `x-acs-version`: API version ("2026-01-18")
/// - `x-acs-security-token`: Security token for temporary credentials (optional)
///
/// # Example
///
/// ```ignore
/// use paimon::api::auth::{DLFOpenApiSigner, DLFRequestSigner};
///
/// let signer = DLFOpenApiSigner;
/// let headers = signer.sign_headers(Some(r#"{"key":"value"}"#), &Utc::now(), None, "dlfnext.aliyuncs.com");
/// ```
pub struct DLFOpenApiSigner;
impl DLFOpenApiSigner {
pub const IDENTIFIER: &'static str = "openapi";
// Header constants
const DATE_HEADER: &'static str = "Date";
const ACCEPT_HEADER: &'static str = "Accept";
const CONTENT_MD5_HEADER: &'static str = "Content-MD5";
const CONTENT_TYPE_HEADER: &'static str = "Content-Type";
const HOST_HEADER: &'static str = "Host";
const X_ACS_SIGNATURE_METHOD: &'static str = "x-acs-signature-method";
const X_ACS_SIGNATURE_NONCE: &'static str = "x-acs-signature-nonce";
const X_ACS_SIGNATURE_VERSION: &'static str = "x-acs-signature-version";
const X_ACS_VERSION: &'static str = "x-acs-version";
const X_ACS_SECURITY_TOKEN: &'static str = "x-acs-security-token";
// Values
const DATE_FORMAT: &'static str = "%a, %d %b %Y %H:%M:%S GMT";
const ACCEPT_VALUE: &'static str = "application/json";
const CONTENT_TYPE_VALUE: &'static str = "application/json";
const SIGNATURE_METHOD_VALUE: &'static str = "HMAC-SHA1";
const SIGNATURE_VERSION_VALUE: &'static str = "1.0";
const API_VERSION: &'static str = "2026-01-18";
fn md5_base64(data: &str) -> String {
let mut hasher = Md5::new();
hasher.update(data.as_bytes());
let hash = hasher.finalize();
BASE64_STANDARD.encode(hash)
}
fn hmac_sha1_base64(key: &str, data: &str) -> String {
let mut mac =
HmacSha1::new_from_slice(key.as_bytes()).expect("HMAC can take key of any size");
mac.update(data.as_bytes());
BASE64_STANDARD.encode(mac.finalize().into_bytes())
}
fn trim(value: &str) -> &str {
value.trim()
}
fn build_canonicalized_headers(&self, headers: &HashMap<String, String>) -> String {
let mut sorted_headers: Vec<(String, String)> = headers
.iter()
.filter(|(key, _)| key.to_lowercase().starts_with("x-acs-"))
.map(|(key, value)| (key.to_lowercase(), Self::trim(value).to_string()))
.collect();
sorted_headers.sort_by(|a, b| a.0.cmp(&b.0));
let mut result = String::new();
for (key, value) in sorted_headers {
result.push_str(&format!("{key}:{value}\n"));
}
result
}
fn build_canonicalized_resource(&self, rest_auth_parameter: &RESTAuthParameter) -> String {
let path = urlencoding::decode(&rest_auth_parameter.path).unwrap_or_default();
if rest_auth_parameter.parameters.is_empty() {
return path.to_string();
}
let mut sorted_params: Vec<_> = rest_auth_parameter.parameters.iter().collect();
sorted_params.sort_by(|a, b| a.0.cmp(b.0));
let query_parts: Vec<String> = sorted_params
.iter()
.map(|(key, value)| {
let decoded_value = urlencoding::decode(value).unwrap_or_default();
if !decoded_value.is_empty() {
format!("{key}={decoded_value}")
} else {
key.to_string()
}
})
.collect();
format!("{}?{}", path, query_parts.join("&"))
}
fn build_string_to_sign(
&self,
rest_auth_parameter: &RESTAuthParameter,
headers: &HashMap<String, String>,
canonicalized_headers: &str,
canonicalized_resource: &str,
) -> String {
let parts = [
rest_auth_parameter.method.clone(),
headers
.get(Self::ACCEPT_HEADER)
.cloned()
.unwrap_or_default(),
headers
.get(Self::CONTENT_MD5_HEADER)
.cloned()
.unwrap_or_default(),
headers
.get(Self::CONTENT_TYPE_HEADER)
.cloned()
.unwrap_or_default(),
headers.get(Self::DATE_HEADER).cloned().unwrap_or_default(),
canonicalized_headers.to_string(),
];
parts.join("\n") + canonicalized_resource
}
}
impl DLFRequestSigner for DLFOpenApiSigner {
fn sign_headers(
&self,
body: Option<&str>,
now: &DateTime<Utc>,
security_token: Option<&str>,
host: &str,
) -> HashMap<String, String> {
let mut headers = HashMap::new();
// Date header in RFC 1123 format
headers.insert(
Self::DATE_HEADER.to_string(),
now.format(Self::DATE_FORMAT).to_string(),
);
// Accept header
headers.insert(
Self::ACCEPT_HEADER.to_string(),
Self::ACCEPT_VALUE.to_string(),
);
// Content-MD5 and Content-Type (if body exists)
if let Some(body_content) = body {
if !body_content.is_empty() {
headers.insert(
Self::CONTENT_MD5_HEADER.to_string(),
Self::md5_base64(body_content),
);
headers.insert(
Self::CONTENT_TYPE_HEADER.to_string(),
Self::CONTENT_TYPE_VALUE.to_string(),
);
}
}
// Host header
headers.insert(Self::HOST_HEADER.to_string(), host.to_string());
// x-acs-* headers
headers.insert(
Self::X_ACS_SIGNATURE_METHOD.to_string(),
Self::SIGNATURE_METHOD_VALUE.to_string(),
);
headers.insert(
Self::X_ACS_SIGNATURE_NONCE.to_string(),
Uuid::new_v4().to_string(),
);
headers.insert(
Self::X_ACS_SIGNATURE_VERSION.to_string(),
Self::SIGNATURE_VERSION_VALUE.to_string(),
);
headers.insert(
Self::X_ACS_VERSION.to_string(),
Self::API_VERSION.to_string(),
);
// Security token (if present)
if let Some(token) = security_token {
headers.insert(Self::X_ACS_SECURITY_TOKEN.to_string(), token.to_string());
}
headers
}
fn authorization(
&self,
rest_auth_parameter: &RESTAuthParameter,
token: &DLFToken,
_host: &str,
sign_headers: &HashMap<String, String>,
) -> String {
let canonicalized_headers = self.build_canonicalized_headers(sign_headers);
let canonicalized_resource = self.build_canonicalized_resource(rest_auth_parameter);
let string_to_sign = self.build_string_to_sign(
rest_auth_parameter,
sign_headers,
&canonicalized_headers,
&canonicalized_resource,
);
let signature = Self::hmac_sha1_base64(&token.access_key_secret, &string_to_sign);
format!("acs {}:{}", token.access_key_id, signature)
}
fn identifier(&self) -> &str {
Self::IDENTIFIER
}
}
/// use paimon::api::auth::DLFSignerFactory;
///
/// // Auto-detect from URI
/// let signer = DLFSignerFactory::create_signer("default", "cn-hangzhou");
/// let algo = DLFSignerFactory::parse_signing_algo_from_uri(Some("http://dlfnext.ajinnbjug.aliyuncs.com"));
/// assert_eq!(algo, "openapi");
/// ```
pub struct DLFSignerFactory;
impl DLFSignerFactory {
/// Create a signer based on the signing algorithm.
pub fn create_signer(signing_algorithm: &str, region: &str) -> Box<dyn DLFRequestSigner> {
if signing_algorithm == DLFOpenApiSigner::IDENTIFIER {
Box::new(DLFOpenApiSigner)
} else {
Box::new(DLFDefaultSigner::new(region))
}
}
}