You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@opendal.apache.org by xu...@apache.org on 2026/03/18 18:03:37 UTC
(opendal-reqsign) 02/02: feat(aliyun-oss): add signature v2 signing support
This is an automated email from the ASF dual-hosted git repository.
xuanwo pushed a commit to branch xuanwo/oss-pr8-signature-v2
in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
commit 38d5c9fa3b64df8b1e58a03c1e4756b15f56516c
Author: Xuanwo <gi...@xuanwo.io>
AuthorDate: Thu Mar 19 02:03:24 2026 +0800
feat(aliyun-oss): add signature v2 signing support
---
services/aliyun-oss/README.md | 20 +-
services/aliyun-oss/src/lib.rs | 24 +-
services/aliyun-oss/src/sign_request.rs | 545 +++++++++++++++++++++++++++++++-
3 files changed, 549 insertions(+), 40 deletions(-)
diff --git a/services/aliyun-oss/README.md b/services/aliyun-oss/README.md
index 29c1a4f..83c0952 100644
--- a/services/aliyun-oss/README.md
+++ b/services/aliyun-oss/README.md
@@ -10,14 +10,9 @@ This crate provides signing support for Alibaba Cloud Object Storage Service (OS
```rust
use reqsign_aliyun_oss::{
-<<<<<<< HEAD
- AssumeRoleWithOidcCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider,
- RequestSigner, SigningVersion, StaticCredentialProvider,
-=======
AssumeRoleWithOidcCredentialProvider, ConfigFileCredentialProvider,
CredentialsFileCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider,
OssProfileCredentialProvider, RequestSigner, SigningVersion, StaticCredentialProvider,
->>>>>>> origin/main
};
use reqsign_core::{Context, Result, Signer};
use reqsign_file_read_tokio::TokioFileRead;
@@ -66,13 +61,8 @@ async fn main() -> Result<()> {
## Features
-- **V1 and V4 Signing**: Supports both legacy OSS V1 signatures and Signature V4
-<<<<<<< HEAD
-- **Multiple Credential Sources**: Environment variables and OIDC-based STS exchange
-=======
+- **V1, V2, and V4 Signing**: Supports legacy OSS V1, SHA256-based V2, and region-aware V4
- **Multiple Credential Sources**: Environment variables, OSS profile files, Alibaba shared credential/config files, and OIDC-based STS exchange
-- **V1 and V4 Signing**: Supports both legacy OSS V1 signatures and Signature V4
->>>>>>> origin/main
- **STS Support**: Temporary credentials via Security Token Service
- **All OSS Operations**: Object, bucket, and multipart operations
@@ -80,6 +70,14 @@ async fn main() -> Result<()> {
`RequestSigner::new("bucket")` keeps the current V1 behavior.
+To opt into V2 signing:
+
+```rust
+use reqsign_aliyun_oss::{RequestSigner, SigningVersion};
+
+let signer = RequestSigner::new("bucket").with_signing_version(SigningVersion::V2);
+```
+
To opt into V4 signing, configure both the region and signing version:
```rust
diff --git a/services/aliyun-oss/src/lib.rs b/services/aliyun-oss/src/lib.rs
index c81feab..9da4e2f 100644
--- a/services/aliyun-oss/src/lib.rs
+++ b/services/aliyun-oss/src/lib.rs
@@ -22,29 +22,20 @@
//!
//! ## Overview
//!
-//! Aliyun OSS uses a custom signing algorithm based on HMAC-SHA1. This crate implements
-//! the complete signing process along with credential loading from various sources
-//! including environment variables, configuration files, and STS tokens.
+//! Aliyun OSS supports multiple signature generations. This crate keeps V1 as the
+//! default and also supports V2 and V4 signing for callers that need stronger hashing
+//! or modern region-aware signing.
//!
-//! `RequestSigner` defaults to V1 signing and supports opting into V4 signing
-<<<<<<< HEAD
-//! when a region is configured.
-=======
-//! when both region and signing version are configured.
->>>>>>> origin/main
+//! `RequestSigner` defaults to V1 signing and supports opting into V2 or V4 signing.
+//! V4 additionally requires a configured region.
//!
//! ## Quick Start
//!
//! ```no_run
//! use reqsign_aliyun_oss::{
-<<<<<<< HEAD
-//! AssumeRoleWithOidcCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider,
-//! RequestSigner, SigningVersion, StaticCredentialProvider,
-=======
//! AssumeRoleWithOidcCredentialProvider, ConfigFileCredentialProvider,
//! CredentialsFileCredentialProvider, DefaultCredentialProvider, EnvCredentialProvider,
//! OssProfileCredentialProvider, RequestSigner, SigningVersion, StaticCredentialProvider,
->>>>>>> origin/main
//! };
//! use reqsign_core::{Context, Signer, Result};
//! use reqsign_file_read_tokio::TokioFileRead;
@@ -75,7 +66,10 @@
//!
//! // Create request builder
//! let builder = RequestSigner::new("bucket");
-//! // Or opt into V4:
+//! // Or opt into V2/V4:
+//! // let builder = RequestSigner::new("bucket")
+//! // .with_signing_version(SigningVersion::V2);
+//!
//! // let builder = RequestSigner::new("bucket")
//! // .with_region("cn-beijing")
//! // .with_signing_version(SigningVersion::V4);
diff --git a/services/aliyun-oss/src/sign_request.rs b/services/aliyun-oss/src/sign_request.rs
index 39a1a17..4661db0 100644
--- a/services/aliyun-oss/src/sign_request.rs
+++ b/services/aliyun-oss/src/sign_request.rs
@@ -20,7 +20,9 @@ use http::HeaderValue;
use http::header::{AUTHORIZATION, CONTENT_TYPE, DATE, HOST};
use percent_encoding::{AsciiSet, NON_ALPHANUMERIC, percent_decode_str, utf8_percent_encode};
use reqsign_core::Result;
-use reqsign_core::hash::{base64_hmac_sha1, hex_hmac_sha256, hex_sha256, hmac_sha256};
+use reqsign_core::hash::{
+ base64_hmac_sha1, base64_hmac_sha256, hex_hmac_sha256, hex_sha256, hmac_sha256,
+};
use reqsign_core::time::Timestamp;
use reqsign_core::{Context, Error, SignRequest, SigningRequest};
use std::collections::HashSet;
@@ -29,11 +31,15 @@ use std::sync::LazyLock;
use std::time::Duration;
const CONTENT_MD5: &str = "content-md5";
+const IF_MODIFIED_SINCE: &str = "if-modified-since";
+const OSS_V2_ALGORITHM: &str = "OSS2";
const OSS_V4_ALGORITHM: &str = "OSS4-HMAC-SHA256";
const OSS_V4_REQUEST: &str = "aliyun_v4_request";
const OSS_V4_SERVICE: &str = "oss";
+const RANGE: &str = "range";
const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
const X_OSS_ADDITIONAL_HEADERS: &str = "x-oss-additional-headers";
+const X_OSS_ACCESS_KEY_ID: &str = "x-oss-access-key-id";
const X_OSS_CONTENT_SHA256: &str = "x-oss-content-sha256";
const X_OSS_CREDENTIAL: &str = "x-oss-credential";
const X_OSS_DATE: &str = "x-oss-date";
@@ -55,11 +61,20 @@ static OSS_V4_QUERY_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC
.remove(b'_')
.remove(b'~');
+static OSS_V2_ENCODE_SET: AsciiSet = NON_ALPHANUMERIC
+ .remove(b'-')
+ .remove(b'.')
+ .remove(b'_')
+ .remove(b'~');
+
+const OSS_V2_DEFAULT_ADDITIONAL_HEADERS: &[&str] = &[IF_MODIFIED_SINCE, RANGE];
+
/// SigningVersion controls which OSS signing algorithm the signer uses.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum SigningVersion {
V1,
+ V2,
V4,
}
@@ -85,7 +100,7 @@ impl RequestSigner {
/// Set the OSS region.
///
- /// Signature V4 requires this value. V1 keeps ignoring it.
+ /// Signature V4 requires this value. V1 and V2 keep ignoring it.
pub fn with_region(mut self, region: impl Into<String>) -> Self {
self.region = Some(region.into());
self
@@ -93,8 +108,8 @@ impl RequestSigner {
/// Set the signing version.
///
- /// The signer defaults to V1. Use V4 together with [`RequestSigner::with_region`]
- /// to opt into OSS Signature Version 4.
+ /// The signer defaults to V1. Use V2 for SHA256-based legacy signing,
+ /// or V4 together with [`RequestSigner::with_region`] for region-aware signing.
pub fn with_signing_version(mut self, signing_version: SigningVersion) -> Self {
self.signing_version = signing_version;
self
@@ -140,6 +155,9 @@ impl SignRequest for RequestSigner {
self.sign_header(req, cred, signing_time)?;
}
}
+ SigningVersion::V2 => {
+ self.sign_v2(req, cred, signing_time, expires_in)?;
+ }
SigningVersion::V4 => {
self.sign_v4(req, cred, signing_time, expires_in)?;
}
@@ -150,6 +168,93 @@ impl SignRequest for RequestSigner {
}
impl RequestSigner {
+ fn sign_v2(
+ &self,
+ req: &mut http::request::Parts,
+ cred: &Credential,
+ signing_time: Timestamp,
+ expires_in: Option<Duration>,
+ ) -> Result<()> {
+ match expires_in {
+ Some(expires) => self.sign_v2_query(req, cred, signing_time, expires),
+ None => self.sign_v2_header(req, cred, signing_time),
+ }
+ }
+
+ fn sign_v2_header(
+ &self,
+ req: &mut http::request::Parts,
+ cred: &Credential,
+ signing_time: Timestamp,
+ ) -> Result<()> {
+ let date = signing_time.format_http_date();
+ req.headers.insert(DATE, date.parse()?);
+
+ if let Some(token) = &cred.security_token {
+ req.headers.insert(X_OSS_SECURITY_TOKEN, token.parse()?);
+ }
+
+ let additional_headers = self.v2_additional_headers(req, false);
+ let query_pairs = self.query_pairs(req);
+ let string_to_sign =
+ self.build_v2_string_to_sign(req, &query_pairs, &date, &additional_headers)?;
+ let signature =
+ base64_hmac_sha256(cred.access_key_secret.as_bytes(), string_to_sign.as_bytes());
+
+ let authorization = if additional_headers.is_empty() {
+ format!(
+ "{OSS_V2_ALGORITHM} AccessKeyId:{},Signature:{signature}",
+ cred.access_key_id
+ )
+ } else {
+ format!(
+ "{OSS_V2_ALGORITHM} AccessKeyId:{},AdditionalHeaders:{},Signature:{signature}",
+ cred.access_key_id,
+ additional_headers.join(";")
+ )
+ };
+ let mut value: HeaderValue = authorization.parse()?;
+ value.set_sensitive(true);
+ req.headers.insert(AUTHORIZATION, value);
+
+ Ok(())
+ }
+
+ fn sign_v2_query(
+ &self,
+ req: &mut http::request::Parts,
+ cred: &Credential,
+ signing_time: Timestamp,
+ expires: Duration,
+ ) -> Result<()> {
+ let expiration_time = (signing_time + expires).as_second().to_string();
+ let additional_headers = self.v2_additional_headers(req, true);
+ let mut query_pairs = self.query_pairs(req);
+ query_pairs.push((
+ X_OSS_SIGNATURE_VERSION.to_string(),
+ OSS_V2_ALGORITHM.to_string(),
+ ));
+ query_pairs.push((X_OSS_EXPIRES.to_string(), expiration_time.clone()));
+ query_pairs.push((X_OSS_ACCESS_KEY_ID.to_string(), cred.access_key_id.clone()));
+ if !additional_headers.is_empty() {
+ query_pairs.push((
+ X_OSS_ADDITIONAL_HEADERS.to_string(),
+ additional_headers.join(";"),
+ ));
+ }
+ if let Some(token) = &cred.security_token {
+ query_pairs.push(("security-token".to_string(), token.clone()));
+ }
+
+ let string_to_sign =
+ self.build_v2_string_to_sign(req, &query_pairs, &expiration_time, &additional_headers)?;
+ let signature =
+ base64_hmac_sha256(cred.access_key_secret.as_bytes(), string_to_sign.as_bytes());
+ query_pairs.push((X_OSS_SIGNATURE.to_string(), signature));
+
+ self.apply_query_pairs(req, &query_pairs)
+ }
+
fn sign_header(
&self,
req: &mut http::request::Parts,
@@ -250,6 +355,47 @@ impl RequestSigner {
Ok(())
}
+ fn build_v2_string_to_sign(
+ &self,
+ req: &http::request::Parts,
+ query_pairs: &[(String, String)],
+ date_or_expires: &str,
+ additional_headers: &[String],
+ ) -> Result<String> {
+ let mut s = String::new();
+ writeln!(&mut s, "{}", req.method)?;
+ writeln!(
+ &mut s,
+ "{}",
+ req.headers
+ .get(CONTENT_MD5)
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or("")
+ )?;
+ writeln!(
+ &mut s,
+ "{}",
+ req.headers
+ .get(CONTENT_TYPE)
+ .and_then(|v| v.to_str().ok())
+ .unwrap_or("")
+ )?;
+ writeln!(&mut s, "{date_or_expires}")?;
+ write!(
+ &mut s,
+ "{}",
+ self.v2_canonicalized_headers(req, additional_headers)?
+ )?;
+ writeln!(&mut s, "{}", additional_headers.join(";"))?;
+ write!(
+ &mut s,
+ "{}",
+ self.v2_canonicalized_resource(req, query_pairs)?
+ )?;
+
+ Ok(s)
+ }
+
fn build_string_to_sign(
&self,
req: &http::request::Parts,
@@ -620,16 +766,157 @@ impl RequestSigner {
Ok(utf8_percent_encode(&resource_path, &OSS_V4_URI_ENCODE_SET).to_string())
}
-<<<<<<< HEAD
- fn canonicalize_headers(&self, req: &http::request::Parts, cred: &Credential) -> String {
-=======
+ fn v2_additional_headers(&self, req: &http::request::Parts, is_presign: bool) -> Vec<String> {
+ let defaults = if is_presign {
+ &[][..]
+ } else {
+ OSS_V2_DEFAULT_ADDITIONAL_HEADERS
+ };
+
+ let mut headers = defaults
+ .iter()
+ .copied()
+ .filter(|name| req.headers.contains_key(*name))
+ .map(str::to_string)
+ .collect::<Vec<_>>();
+ headers.sort();
+ headers.dedup();
+ headers
+ }
+
+ fn v2_canonicalized_headers(
+ &self,
+ req: &http::request::Parts,
+ additional_headers: &[String],
+ ) -> Result<String> {
+ let mut headers = Vec::new();
+
+ for (name, value) in &req.headers {
+ let name = name.as_str().to_ascii_lowercase();
+ if name == AUTHORIZATION.as_str() {
+ continue;
+ }
+ if name.starts_with("x-oss-") || additional_headers.binary_search(&name).is_ok() {
+ let value = value
+ .to_str()
+ .map_err(|e| {
+ Error::request_invalid("invalid header value for V2 signing").with_source(e)
+ })?
+ .trim()
+ .to_string();
+ headers.push((name, value));
+ }
+ }
+
+ headers.sort_by(|a, b| a.0.cmp(&b.0));
+
+ let mut s = String::new();
+ for (name, value) in headers {
+ writeln!(&mut s, "{name}:{value}")?;
+ }
+ Ok(s)
+ }
+
+ fn v2_canonicalized_resource(
+ &self,
+ req: &http::request::Parts,
+ query_pairs: &[(String, String)],
+ ) -> Result<String> {
+ let decoded_path = percent_decode_str(req.uri.path())
+ .decode_utf8()
+ .map_err(|e| Error::request_invalid("invalid request path").with_source(e))?;
+ let authority = req.uri.authority().map(|v| v.as_str()).unwrap_or("");
+ let resource_path = if authority.starts_with(&format!("{}.", self.bucket)) {
+ format!("/{}{}", self.bucket, decoded_path)
+ } else {
+ decoded_path.into_owned()
+ };
+
+ let mut resource = utf8_percent_encode(&resource_path, &OSS_V2_ENCODE_SET).to_string();
+ let canonical_query = self.v2_canonicalized_query(query_pairs);
+ if !canonical_query.is_empty() {
+ resource.push('?');
+ resource.push_str(&canonical_query);
+ }
+
+ Ok(resource)
+ }
+
+ fn v2_canonicalized_query(&self, query_pairs: &[(String, String)]) -> String {
+ let mut encoded_pairs = query_pairs
+ .iter()
+ .enumerate()
+ .map(|(idx, (key, value))| {
+ (
+ idx,
+ utf8_percent_encode(key, &OSS_V2_ENCODE_SET).to_string(),
+ utf8_percent_encode(value, &OSS_V2_ENCODE_SET).to_string(),
+ )
+ })
+ .collect::<Vec<_>>();
+ encoded_pairs.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.cmp(&b.2)).then(a.0.cmp(&b.0)));
+
+ encoded_pairs
+ .into_iter()
+ .map(|(_, key, value)| {
+ if value.is_empty() {
+ key
+ } else {
+ format!("{key}={value}")
+ }
+ })
+ .collect::<Vec<_>>()
+ .join("&")
+ }
+
+ fn query_pairs(&self, req: &http::request::Parts) -> Vec<(String, String)> {
+ req.uri
+ .query()
+ .map(|query| {
+ form_urlencoded::parse(query.as_bytes())
+ .map(|(key, value)| (key.into_owned(), value.into_owned()))
+ .collect()
+ })
+ .unwrap_or_default()
+ }
+
+ fn apply_query_pairs(
+ &self,
+ req: &mut http::request::Parts,
+ query_pairs: &[(String, String)],
+ ) -> Result<()> {
+ let query_string = query_pairs
+ .iter()
+ .map(|(key, value)| {
+ let key = utf8_percent_encode(key, &OSS_V2_ENCODE_SET).to_string();
+ if value.is_empty() {
+ key
+ } else {
+ format!("{key}={}", utf8_percent_encode(value, &OSS_V2_ENCODE_SET))
+ }
+ })
+ .collect::<Vec<_>>()
+ .join("&");
+
+ let path = req.uri.path();
+ let new_path_and_query = if query_string.is_empty() {
+ path.to_string()
+ } else {
+ format!("{path}?{query_string}")
+ };
+ let mut parts = req.uri.clone().into_parts();
+ parts.path_and_query = Some(new_path_and_query.try_into()?);
+ req.uri = http::Uri::from_parts(parts)?;
+
+ Ok(())
+ }
+
fn canonicalize_headers(
&self,
req: &http::request::Parts,
include_implicit_security_token: bool,
cred: &Credential,
) -> String {
->>>>>>> origin/main
let mut oss_headers = Vec::new();
// Collect x-oss-* headers
@@ -827,10 +1114,6 @@ static SUBRESOURCES: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
mod tests {
use super::*;
use crate::Credential;
-<<<<<<< HEAD
- use reqsign_core::{Context, SigningRequest};
-
-=======
use http::Request;
use reqsign_core::{Context, SigningRequest};
@@ -921,8 +1204,6 @@ mod tests {
);
assert!(!string_to_sign.contains("x-oss-security-token:sts-token"));
}
-
->>>>>>> origin/main
#[test]
fn test_request_signer_accepts_region_configuration() {
let signer = RequestSigner::new("bucket")
@@ -977,6 +1258,242 @@ mod tests {
);
}
+ #[test]
+ fn test_v2_header_signature_matches_official_put_object_example() {
+ let credential = Credential {
+ access_key_id: "44CF9590006BF252F707".to_string(),
+ access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(),
+ security_token: None,
+ expires_in: None,
+ };
+ let time = Timestamp::from_second(1_487_151_431).expect("timestamp must build");
+ let signer = RequestSigner::new("oss-example")
+ .with_signing_version(SigningVersion::V2)
+ .with_time(time);
+
+ let req = http::Request::put("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .header(CONTENT_MD5, "FxqG8Ca0qEJPOghSihJ8Ew==")
+ .header(CONTENT_TYPE, "text/plain")
+ .header("x-oss-object-acl", "private")
+ .header(DATE, "Wed, 15 Feb 2017 09:37:11 GMT")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+
+ let additional_headers = signer.v2_additional_headers(&req, false);
+ let string_to_sign = signer
+ .build_v2_string_to_sign(
+ &req,
+ &signer.query_pairs(&req),
+ "Wed, 15 Feb 2017 09:37:11 GMT",
+ &additional_headers,
+ )
+ .expect("string to sign must build");
+ assert_eq!(
+ string_to_sign,
+ "PUT\nFxqG8Ca0qEJPOghSihJ8Ew==\ntext/plain\nWed, 15 Feb 2017 09:37:11 GMT\nx-oss-object-acl:private\n\n%2Foss-example%2Fnelson"
+ );
+
+ let mut signed_req =
+ http::Request::put("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .header(CONTENT_MD5, "FxqG8Ca0qEJPOghSihJ8Ew==")
+ .header(CONTENT_TYPE, "text/plain")
+ .header("x-oss-object-acl", "private")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ signer
+ .sign_v2_header(&mut signed_req, &credential, time)
+ .expect("v2 header signing must succeed");
+
+ assert_eq!(
+ signed_req.headers.get(DATE).and_then(|v| v.to_str().ok()),
+ Some("Wed, 15 Feb 2017 09:37:11 GMT")
+ );
+ assert_eq!(
+ signed_req
+ .headers
+ .get(AUTHORIZATION)
+ .and_then(|v| v.to_str().ok()),
+ Some(
+ "OSS2 AccessKeyId:44CF9590006BF252F707,Signature:5Am2ewK1tL0gXX7GV6dwybZtj7efOEtc0Mo2FR6CkM8="
+ )
+ );
+ }
+
+ #[test]
+ fn test_v2_header_signature_matches_official_additional_headers_example() {
+ let credential = Credential {
+ access_key_id: "44CF9590006BF252F707".to_string(),
+ access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(),
+ security_token: None,
+ expires_in: None,
+ };
+ let time = Timestamp::from_second(1_487_210_979).expect("timestamp must build");
+ let signer = RequestSigner::new("oss-example")
+ .with_signing_version(SigningVersion::V2)
+ .with_time(time);
+
+ let req = http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .header(RANGE, "bytes=0-7")
+ .header(IF_MODIFIED_SINCE, "Thu, 16 Feb 2017 02:10:39 GMT")
+ .header(DATE, "Thu, 16 Feb 2017 02:09:39 GMT")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+
+ let additional_headers = signer.v2_additional_headers(&req, false);
+ assert_eq!(
+ additional_headers,
+ vec![IF_MODIFIED_SINCE.to_string(), RANGE.to_string()]
+ );
+ let string_to_sign = signer
+ .build_v2_string_to_sign(
+ &req,
+ &signer.query_pairs(&req),
+ "Thu, 16 Feb 2017 02:09:39 GMT",
+ &additional_headers,
+ )
+ .expect("string to sign must build");
+ assert_eq!(
+ string_to_sign,
+ "GET\n\n\nThu, 16 Feb 2017 02:09:39 GMT\nif-modified-since:Thu, 16 Feb 2017 02:10:39 GMT\nrange:bytes=0-7\nif-modified-since;range\n%2Foss-example%2Fnelson"
+ );
+
+ let mut signed_req =
+ http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .header(RANGE, "bytes=0-7")
+ .header(IF_MODIFIED_SINCE, "Thu, 16 Feb 2017 02:10:39 GMT")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ signer
+ .sign_v2_header(&mut signed_req, &credential, time)
+ .expect("v2 header signing must succeed");
+
+ assert_eq!(
+ signed_req
+ .headers
+ .get(AUTHORIZATION)
+ .and_then(|v| v.to_str().ok()),
+ Some(
+ "OSS2 AccessKeyId:44CF9590006BF252F707,AdditionalHeaders:if-modified-since;range,Signature:YG9mKO3m4S0Jx9Hk6Lq64VchJg/TOTkyCX4DaeeOYxE="
+ )
+ );
+ }
+
+ #[test]
+ fn test_v2_presign_signature_matches_official_example() {
+ let credential = Credential {
+ access_key_id: "44CF9590006BF252F707".to_string(),
+ access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(),
+ security_token: None,
+ expires_in: None,
+ };
+ let time = Timestamp::from_second(1_487_151_431).expect("timestamp must build");
+ let signer = RequestSigner::new("oss-example")
+ .with_signing_version(SigningVersion::V2)
+ .with_time(time);
+
+ let req = http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ let expiration_time = "1487152431".to_string();
+ let mut query_pairs = signer.query_pairs(&req);
+ query_pairs.push((
+ X_OSS_SIGNATURE_VERSION.to_string(),
+ OSS_V2_ALGORITHM.to_string(),
+ ));
+ query_pairs.push((X_OSS_EXPIRES.to_string(), expiration_time.clone()));
+ query_pairs.push((
+ X_OSS_ACCESS_KEY_ID.to_string(),
+ credential.access_key_id.clone(),
+ ));
+ let additional_headers = signer.v2_additional_headers(&req, true);
+ let string_to_sign = signer
+ .build_v2_string_to_sign(&req, &query_pairs, &expiration_time, &additional_headers)
+ .expect("string to sign must build");
+ assert_eq!(
+ string_to_sign,
+ "GET\n\n\n1487152431\n\n%2Foss-example%2Fnelson?x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487152431&x-oss-signature-version=OSS2"
+ );
+
+ let mut signed_req =
+ http::Request::get("https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson")
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ signer
+ .sign_v2_query(
+ &mut signed_req,
+ &credential,
+ time,
+ Duration::from_secs(1_000),
+ )
+ .expect("v2 presign must succeed");
+
+ assert_eq!(
+ signed_req.uri.query(),
+ Some(
+ "x-oss-signature-version=OSS2&x-oss-expires=1487152431&x-oss-access-key-id=44CF9590006BF252F707&x-oss-signature=ps%2F%2BMLhd1WKkVi%2FQlOiliJsTaBMBk93f6UYVscDNHCQ%3D"
+ )
+ );
+ }
+
+ #[test]
+ fn test_v2_presign_signs_all_query_parameters() {
+ let credential = Credential {
+ access_key_id: "44CF9590006BF252F707".to_string(),
+ access_key_secret: "OtxrzxIsfpFjA7SwPzILwy8Bw21TLhquhboDYROV".to_string(),
+ security_token: None,
+ expires_in: None,
+ };
+ let time = Timestamp::from_second(1_487_210_979).expect("timestamp must build");
+ let signer = RequestSigner::new("oss-example")
+ .with_signing_version(SigningVersion::V2)
+ .with_time(time);
+
+ let req = http::Request::get(
+ "https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson?x-oss-signature-version=OSS2&extra-query=1&x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487211619",
+ )
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ let string_to_sign = signer
+ .build_v2_string_to_sign(&req, &signer.query_pairs(&req), "1487211619", &[])
+ .expect("string to sign must build");
+ assert_eq!(
+ string_to_sign,
+ "GET\n\n\n1487211619\n\n%2Foss-example%2Fnelson?extra-query=1&x-oss-access-key-id=44CF9590006BF252F707&x-oss-expires=1487211619&x-oss-signature-version=OSS2"
+ );
+
+ let mut signed_req = http::Request::get(
+ "https://oss-example.oss-cn-hangzhou.aliyuncs.com/nelson?extra-query=1",
+ )
+ .body(())
+ .expect("request must build")
+ .into_parts()
+ .0;
+ signer
+ .sign_v2_query(&mut signed_req, &credential, time, Duration::from_secs(640))
+ .expect("v2 presign must succeed");
+
+ assert_eq!(
+ signed_req.uri.query(),
+ Some(
+ "extra-query=1&x-oss-signature-version=OSS2&x-oss-expires=1487211619&x-oss-access-key-id=44CF9590006BF252F707&x-oss-signature=wsARTPqvZdbdPjYpZfDZ%2FjisUaacYq7gGOdB3f1BgTE%3D"
+ )
+ );
+ }
+
#[tokio::test]
async fn test_v4_signing_requires_region() {
let mut req =