From ba79fa7ebff0f3375ceb205eaf18ab79eb10ac33 Mon Sep 17 00:00:00 2001 From: Aarni Koskela Date: Tue, 9 Jun 2026 17:52:56 +0300 Subject: [PATCH] perf: cache derived AWS V4 and OSS V4 keys --- services/aliyun-oss/src/sign_request.rs | 60 +++++++++++++++++----- services/aws-v4/src/sign_request.rs | 68 ++++++++++++++++++------- 2 files changed, 99 insertions(+), 29 deletions(-) diff --git a/services/aliyun-oss/src/sign_request.rs b/services/aliyun-oss/src/sign_request.rs index 4f528f9f..d35c82b6 100644 --- a/services/aliyun-oss/src/sign_request.rs +++ b/services/aliyun-oss/src/sign_request.rs @@ -27,7 +27,8 @@ use reqsign_core::time::Timestamp; use reqsign_core::{Context, Error, SignRequest, SigningRequest}; use std::collections::HashSet; use std::fmt::Write; -use std::sync::LazyLock; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, LazyLock, Mutex}; use std::time::Duration; const CONTENT_MD5: &str = "content-md5"; @@ -92,6 +93,29 @@ pub struct RequestSigner { region: Option, signing_version: SigningVersion, time: Option, + v4_signing_key_cache: Mutex>, +} + +/// Cached OSS V4 signing key. +// NOTE: `region` is NOT part of the cache key; +// it is expected to stay static over the lifetime +// of the `RequestSigner` instance. +struct CachedV4SigningKey { + /// Days since the Unix epoch (UTC) (derived from the `date` we'd sign) + day: i64, + /// The secret the key was derived from, used to detect rotation. + secret: String, + /// The derived signing key. + key: Arc<[u8]>, +} + +impl Debug for CachedV4SigningKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CachedV4SigningKey") + .field("day", &self.day) + .field("secret_prefix", &&self.secret[..3]) + .finish_non_exhaustive() + } } impl RequestSigner { @@ -102,6 +126,7 @@ impl RequestSigner { region: None, signing_version: SigningVersion::V1, time: None, + v4_signing_key_cache: Mutex::new(None), } } @@ -503,7 +528,8 @@ impl RequestSigner { let scope = self.v4_scope(signing_time, region); let string_to_sign = self.build_v4_string_to_sign(signing_time, &scope, &canonical_request)?; - let signature = self.build_v4_signature(cred, signing_time, region, &string_to_sign); + let signing_key = self.v4_signing_key(&cred.access_key_secret, region, signing_time); + let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes()); if expires_in.is_some() { signing_req.query_push(X_OSS_SIGNATURE, signature); @@ -716,21 +742,31 @@ impl RequestSigner { Ok(s) } - fn build_v4_signature( - &self, - cred: &Credential, - signing_time: Timestamp, - region: &str, - string_to_sign: &str, - ) -> String { + fn v4_signing_key(&self, secret: &str, region: &str, signing_time: Timestamp) -> Arc<[u8]> { + let day = signing_time.as_second().div_euclid(86_400); + + let mut slot = self.v4_signing_key_cache.lock().expect("lock poisoned"); + if let Some(cached) = slot.as_ref() { + // *NOT* comparing region. + if cached.day == day && cached.secret == secret { + return Arc::clone(&cached.key); + } + } + let date_key = hmac_sha256( - format!("aliyun_v4{}", cred.access_key_secret).as_bytes(), + format!("aliyun_v4{secret}").as_bytes(), signing_time.format_date().as_bytes(), ); let region_key = hmac_sha256(&date_key, region.as_bytes()); let service_key = hmac_sha256(®ion_key, OSS_V4_SERVICE.as_bytes()); - let signing_key = hmac_sha256(&service_key, OSS_V4_REQUEST.as_bytes()); - hex_hmac_sha256(&signing_key, string_to_sign.as_bytes()) + let key: Arc<[u8]> = hmac_sha256(&service_key, OSS_V4_REQUEST.as_bytes()).into(); + + *slot = Some(CachedV4SigningKey { + day, + secret: secret.to_owned(), + key: Arc::clone(&key), + }); + key } fn v4_scope(&self, signing_time: Timestamp, region: &str) -> String { diff --git a/services/aws-v4/src/sign_request.rs b/services/aws-v4/src/sign_request.rs index 6ac23171..0728ebda 100644 --- a/services/aws-v4/src/sign_request.rs +++ b/services/aws-v4/src/sign_request.rs @@ -28,6 +28,8 @@ use reqsign_core::hash::{hex_hmac_sha256, hex_sha256, hmac_sha256}; use reqsign_core::time::Timestamp; use reqsign_core::{Context, Result, SignRequest, SigningRequest}; use std::fmt::Write; +use std::fmt::{Debug, Formatter}; +use std::sync::{Arc, Mutex}; use std::time::Duration; /// RequestSigner that implement AWS SigV4. @@ -37,8 +39,31 @@ use std::time::Duration; pub struct RequestSigner { service: String, region: String, - time: Option, + signing_key_cache: Mutex>, +} + +/// Cached SigV4 signing key. +// +// NOTE: `region` and `service` are NOT part of the cache key, +// as those are expected to stay static over the lifetime +// of the `RequestSigner` instance. +struct CachedV4SigningKey { + /// Days since the Unix epoch (UTC) (derived from the `date` we'd sign) + day: i64, + /// The secret the key was derived from, used to detect rotation. + secret: String, + /// The derived signing key. + key: Arc<[u8]>, +} + +impl Debug for CachedV4SigningKey { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("CachedV4SigningKey") + .field("day", &self.day) + .field("secret_prefix", &&self.secret[..3]) + .finish_non_exhaustive() + } } impl RequestSigner { @@ -47,11 +72,34 @@ impl RequestSigner { Self { service: service.into(), region: region.into(), - time: None, + signing_key_cache: Mutex::new(None), } } + fn signing_key(&self, secret: &str, now: Timestamp) -> Arc<[u8]> { + let day = now.as_second().div_euclid(86_400); + + let mut slot = self.signing_key_cache.lock().expect("lock poisoned"); + if let Some(cached) = slot.as_ref() { + // *NOT* comparing region and service. + if cached.day == day && cached.secret == secret { + return Arc::clone(&cached.key); + } + } + + let sign_date = hmac_sha256(secret.as_bytes(), now.format_date().as_bytes()); + let sign_region = hmac_sha256(sign_date.as_slice(), &self.region.as_bytes()); + let sign_service = hmac_sha256(sign_region.as_slice(), &self.service.as_bytes()); + let key: Arc<[u8]> = hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes()).into(); + *slot = Some(CachedV4SigningKey { + day, + secret: secret.to_owned(), + key: Arc::clone(&key), + }); + key + } + /// Specify the signing time. /// /// # Note @@ -129,8 +177,7 @@ impl SignRequest for RequestSigner { }; debug!("calculated string to sign: {string_to_sign}"); - let signing_key = - generate_signing_key(&cred.secret_access_key, now, &self.region, &self.service); + let signing_key = self.signing_key(&cred.secret_access_key, now); let signature = hex_hmac_sha256(&signing_key, string_to_sign.as_bytes()); if expires_in.is_some() { @@ -348,19 +395,6 @@ fn canonicalize_query( Ok(()) } -fn generate_signing_key(secret: &str, time: Timestamp, region: &str, service: &str) -> Vec { - // Sign secret - let secret = format!("AWS4{secret}"); - // Sign date - let sign_date = hmac_sha256(secret.as_bytes(), time.format_date().as_bytes()); - // Sign region - let sign_region = hmac_sha256(sign_date.as_slice(), region.as_bytes()); - // Sign service - let sign_service = hmac_sha256(sign_region.as_slice(), service.as_bytes()); - // Sign request - hmac_sha256(sign_service.as_slice(), "aws4_request".as_bytes()) -} - #[cfg(test)] mod tests { use super::*;