reqsign_volcengine_tos/
sign_request.rs1use http::header::AUTHORIZATION;
19use http::{HeaderName, HeaderValue, header};
20use log::debug;
21use percent_encoding::percent_decode_str;
22use reqsign_core::hash::{hex_hmac_sha256, hex_sha256, hmac_sha256};
23use reqsign_core::time::Timestamp;
24use reqsign_core::{Context, Result, SignRequest, SigningRequest};
25use std::fmt::Write;
26use std::sync::LazyLock;
27
28use crate::constants::*;
29use crate::credential::Credential;
30use crate::uri::{percent_encode_path, percent_encode_query};
31
32static HEADER_TOS_DATE: LazyLock<HeaderName> =
33 LazyLock::new(|| HeaderName::from_static("x-tos-date"));
34static HEADER_TOS_SECURITY_TOKEN: LazyLock<HeaderName> =
35 LazyLock::new(|| HeaderName::from_static("x-tos-security-token"));
36
37#[derive(Debug)]
41pub struct RequestSigner {
42 region: String,
43 time: Option<Timestamp>,
44}
45
46impl RequestSigner {
47 pub fn new(region: &str) -> Self {
49 Self {
50 region: region.to_string(),
51 time: None,
52 }
53 }
54
55 #[cfg(test)]
62 pub fn with_time(mut self, time: Timestamp) -> Self {
63 self.time = Some(time);
64 self
65 }
66}
67impl SignRequest for RequestSigner {
68 type Credential = Credential;
69
70 async fn sign_request(
71 &self,
72 _ctx: &Context,
73 req: &mut http::request::Parts,
74 credential: Option<&Self::Credential>,
75 _expires_in: Option<std::time::Duration>,
76 ) -> Result<()> {
77 let Some(cred) = credential else {
78 return Ok(());
79 };
80
81 let now = self.time.unwrap_or_else(Timestamp::now);
82
83 let mut signing_req = SigningRequest::build(req)?;
84
85 if signing_req.headers.get(header::HOST).is_none() {
87 signing_req.headers.insert(
88 header::HOST,
89 signing_req.authority.as_str().parse().map_err(|e| {
90 reqsign_core::Error::unexpected(format!(
91 "failed to parse authority as header value: {e}"
92 ))
93 })?,
94 );
95 }
96
97 let date_str = now.format_iso8601();
98 let date_only = now.format_date();
99
100 signing_req
101 .headers
102 .insert(&*HEADER_TOS_DATE, date_str.parse()?);
103
104 if let Some(token) = &cred.session_token {
105 signing_req
106 .headers
107 .insert(&*HEADER_TOS_SECURITY_TOKEN, token.parse()?);
108 }
109
110 canonicalize_query(&mut signing_req);
111 let (canonical_request_hash, _) = canonical_request_hash(&mut signing_req)?;
112
113 let credential_scope = format!("{}/{}/tos/request", date_only, self.region);
115
116 let string_to_sign = {
123 let mut s = String::new();
124 writeln!(s, "TOS4-HMAC-SHA256")?;
125 writeln!(s, "{}", date_str)?;
126 writeln!(s, "{}", credential_scope)?;
127 s.push_str(&canonical_request_hash);
128 s
129 };
130
131 debug!("string to sign: {}", &string_to_sign);
132
133 let signed_headers_str = signing_req.header_name_to_vec_sorted().join(";");
134
135 let signing_key = generate_signing_key(&cred.secret_access_key, &date_only, &self.region);
136 let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes());
137
138 let authorization = format!(
139 "TOS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
140 cred.access_key_id, credential_scope, signed_headers_str, signature
141 );
142
143 debug!("authorization: {}", &authorization);
144
145 let mut auth_value: HeaderValue = authorization.parse()?;
146 auth_value.set_sensitive(true);
147 signing_req.headers.insert(AUTHORIZATION, auth_value);
148
149 signing_req.apply(req)
150 }
151}
152
153fn canonicalize_query(ctx: &mut SigningRequest) {
154 ctx.query = ctx
155 .query
156 .iter()
157 .map(|(k, v)| (percent_encode_query(k), percent_encode_query(v)))
158 .collect();
159 ctx.query.sort();
161}
162
163fn canonical_request_hash(ctx: &mut SigningRequest) -> Result<(String, String)> {
164 let mut canonical_request = String::with_capacity(256);
165
166 canonical_request.push_str(ctx.method.as_str());
168 canonical_request.push('\n');
169
170 let path = percent_decode_str(&ctx.path)
172 .decode_utf8()
173 .map_err(|e| reqsign_core::Error::unexpected(format!("failed to decode path: {e}")))?;
174 let canonical_path = percent_encode_path(&path);
175 canonical_request.push_str(&canonical_path);
176 canonical_request.push('\n');
177
178 let query_string = ctx
180 .query
181 .iter()
182 .map(|(k, v)| format!("{}={}", k, v))
183 .collect::<Vec<_>>()
184 .join("&");
185
186 canonical_request.push_str(&query_string);
187 canonical_request.push('\n');
188
189 let signed_headers = ctx.header_name_to_vec_sorted();
191
192 for header in &signed_headers {
193 let value = &ctx.headers[*header];
194 canonical_request.push_str(header);
195 canonical_request.push(':');
196 if let Ok(value_str) = value.to_str() {
197 canonical_request.push_str(value_str.trim());
198 }
199 canonical_request.push('\n');
200 }
201
202 canonical_request.push('\n');
203 canonical_request.push_str(signed_headers.join(";").as_str());
204 canonical_request.push('\n');
205
206 canonical_request.push_str(EMPTY_PAYLOAD_SHA256);
207
208 let hash = hex_sha256(canonical_request.as_bytes());
209
210 Ok((hash, canonical_request))
211}
212
213fn generate_signing_key(secret: &str, date: &str, region: &str) -> Vec<u8> {
214 let sign_date = hmac_sha256(secret.as_bytes(), date.as_bytes());
216 let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes());
218 let sign_service = hmac_sha256(sign_region.as_slice(), "tos".as_bytes());
220 hmac_sha256(sign_service.as_slice(), "request".as_bytes())
222}
223
224#[cfg(test)]
225mod tests {
226 use std::str::FromStr;
227
228 use http::Uri;
229
230 use super::*;
231 use crate::provide_credential::StaticCredentialProvider;
232 use reqsign_core::{Context, OsEnv, Signer};
233 use reqsign_file_read_tokio::TokioFileRead;
234 use reqsign_http_send_reqwest::ReqwestHttpSend;
235
236 #[tokio::test]
237 async fn test_sign_request() -> Result<()> {
238 let _ = env_logger::builder().is_test(true).try_init();
239
240 let loader = StaticCredentialProvider::new("testAK", "testSK");
241 let signer = RequestSigner::new("cn-beijing")
242 .with_time(Timestamp::parse_rfc2822("Sat, 1 Jan 2022 00:00:00 GMT")?);
243
244 let ctx = Context::new()
245 .with_file_read(TokioFileRead)
246 .with_http_send(ReqwestHttpSend::default())
247 .with_env(OsEnv);
248
249 let signer = Signer::new(ctx, loader, signer);
250
251 let get_req = "https://examplebucket.tos-cn-beijing.volces.com/exampleobject";
252 let mut req = http::Request::get(Uri::from_str(get_req)?).body(())?;
253 req.headers_mut().insert(
254 HeaderName::from_str("x-tos-content-sha256")?,
255 HeaderValue::from_str(
256 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
257 )?,
258 );
259
260 let (mut parts, _) = req.into_parts();
261 signer.sign(&mut parts, None).await?;
262
263 let headers = parts.headers;
264 let tos_date = headers.get("x-tos-date").unwrap();
265 let auth = headers.get("Authorization").unwrap();
266
267 assert!(
268 tos_date.to_str()?.starts_with("2022"),
269 "x-tos-date should be in ISO8601 format"
270 );
271 assert_eq!(
272 "TOS4-HMAC-SHA256 Credential=testAK/20220101/cn-beijing/tos/request, SignedHeaders=host;x-tos-content-sha256;x-tos-date, Signature=d40b66cf0054d1642843670d10fa095e1609c7896f25df217770b0abe717693b",
273 auth.to_str()?
274 );
275
276 Ok(())
277 }
278
279 #[tokio::test]
280 async fn test_sign_list_objects() -> Result<()> {
281 let _ = env_logger::builder().is_test(true).try_init();
282 let loader = StaticCredentialProvider::new("testAK", "testSK");
283
284 let signer = RequestSigner::new("cn-beijing").with_time("2026-02-03T12:24:12Z".parse()?);
285
286 let ctx = Context::new()
287 .with_file_read(TokioFileRead)
288 .with_http_send(ReqwestHttpSend::default())
289 .with_env(OsEnv);
290 let signer = Signer::new(ctx, loader, signer);
291
292 let req = http::Request::get("https://bucket.tos-cn-beijing.volces.com?list-type=2&prefix=abc&delimiter=%2F&max-keys=5&continuation-token=whvFnl2rE5vm9cWvQSgxwpc7QXHY7dgUGQ7nxlsVxFymg2%2BK227j5IHQZ32h").body(())?;
293 let (mut parts, _) = req.into_parts();
294
295 signer.sign(&mut parts, None).await?;
296
297 let headers = parts.headers;
298 let auth = headers.get("Authorization").unwrap();
299
300 assert_eq!(
301 "TOS4-HMAC-SHA256 Credential=testAK/20260203/cn-beijing/tos/request, SignedHeaders=host;x-tos-date, Signature=db01ee877fa24847ec042703353a76a0e11bd9b6ce68eabe5ccb2924420156b0",
302 auth.to_str()?
303 );
304 Ok(())
305 }
306}