This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch fix-s3-oidc in repository https://gitbox.apache.org/repos/asf/opendal-reqsign.git
commit e6ef53e98a264d9803133e2e98e20f3aa7ae8b5f Author: Xuanwo <[email protected]> AuthorDate: Mon Oct 13 14:07:10 2025 +0800 fix(services-aws-v4): Ensure token has been trimmed and encoded Signed-off-by: Xuanwo <[email protected]> --- core/src/hash.rs | 4 +- .../assume_role_with_web_identity.rs | 131 ++++++++++++++++++++- services/aws-v4/tests/signing/standard.rs | 2 +- 3 files changed, 131 insertions(+), 6 deletions(-) diff --git a/core/src/hash.rs b/core/src/hash.rs index 14dc1d4..fda4e97 100644 --- a/core/src/hash.rs +++ b/core/src/hash.rs @@ -43,7 +43,7 @@ pub fn base64_decode(content: &str) -> crate::Result<Vec<u8>> { /// Use this function instead of `hex::encode(sha1(content))` can reduce /// extra copy. pub fn hex_sha1(content: &[u8]) -> String { - hex::encode(Sha1::digest(content).as_slice()) + hex::encode(Sha1::digest(content)) } /// Hex encoded SHA256 hash. @@ -51,7 +51,7 @@ pub fn hex_sha1(content: &[u8]) -> String { /// Use this function instead of `hex::encode(sha256(content))` can reduce /// extra copy. pub fn hex_sha256(content: &[u8]) -> String { - hex::encode(Sha256::digest(content).as_slice()) + hex::encode(Sha256::digest(content)) } /// HMAC with SHA256 hash. diff --git a/services/aws-v4/src/provide_credential/assume_role_with_web_identity.rs b/services/aws-v4/src/provide_credential/assume_role_with_web_identity.rs index 196892b..ac4c6e1 100644 --- a/services/aws-v4/src/provide_credential/assume_role_with_web_identity.rs +++ b/services/aws-v4/src/provide_credential/assume_role_with_web_identity.rs @@ -19,6 +19,7 @@ use crate::Credential; use crate::provide_credential::utils::{parse_sts_error, sts_endpoint}; use async_trait::async_trait; use bytes::Bytes; +use form_urlencoded::Serializer; use quick_xml::de; use reqsign_core::{Context, Error, ProvideCredential, Result, utils::Redact}; use serde::Deserialize; @@ -123,6 +124,7 @@ impl ProvideCredential for AssumeRoleWithWebIdentityCredentialProvider { .with_context(format!("file: {token_file}")) .with_context("hint: check if the token file exists and is readable") })?; + let token = token.trim().to_string(); // Get region from config or environment let region = self @@ -150,9 +152,14 @@ impl ProvideCredential for AssumeRoleWithWebIdentityCredentialProvider { .unwrap_or_else(|| "reqsign".to_string()); // Construct request to AWS STS Service. - let url = format!( - "https://{endpoint}/?Action=AssumeRoleWithWebIdentity&RoleArn={role_arn}&WebIdentityToken={token}&Version=2011-06-15&RoleSessionName={session_name}" - ); + let query = Serializer::new(String::new()) + .append_pair("Action", "AssumeRoleWithWebIdentity") + .append_pair("RoleArn", &role_arn) + .append_pair("WebIdentityToken", &token) + .append_pair("Version", "2011-06-15") + .append_pair("RoleSessionName", &session_name) + .finish(); + let url = format!("https://{endpoint}/?{query}"); let req = http::request::Request::builder() .method("GET") .uri(url) @@ -258,6 +265,10 @@ impl Debug for AssumeRoleWithWebIdentityCredentials { #[cfg(test)] mod tests { use super::*; + use async_trait::async_trait; + use reqsign_core::{FileRead, HttpSend, StaticEnv}; + use std::collections::HashMap; + use std::sync::{Arc, Mutex}; #[test] fn test_parse_assume_role_with_web_identity_response() -> Result<()> { @@ -297,4 +308,118 @@ mod tests { Ok(()) } + + #[derive(Debug)] + struct TestFileRead { + expected_path: String, + content: Vec<u8>, + } + + #[async_trait] + impl FileRead for TestFileRead { + async fn file_read(&self, path: &str) -> Result<Vec<u8>> { + assert_eq!(path, self.expected_path); + Ok(self.content.clone()) + } + } + + #[derive(Clone, Debug)] + struct CaptureHttpSend { + uri: Arc<Mutex<Option<String>>>, + body: String, + } + + impl CaptureHttpSend { + fn new(body: impl Into<String>) -> Self { + Self { + uri: Arc::new(Mutex::new(None)), + body: body.into(), + } + } + + fn uri(&self) -> Option<String> { + self.uri.lock().unwrap().clone() + } + } + + #[async_trait] + impl HttpSend for CaptureHttpSend { + async fn http_send(&self, req: http::Request<Bytes>) -> Result<http::Response<Bytes>> { + *self.uri.lock().unwrap() = Some(req.uri().to_string()); + let resp = http::Response::builder() + .status(http::StatusCode::OK) + .header("x-amzn-requestid", "test-request") + .body(Bytes::from(self.body.clone())) + .expect("response must build"); + Ok(resp) + } + } + + #[tokio::test] + async fn test_assume_role_with_web_identity_encodes_query_parameters() -> Result<()> { + let _ = env_logger::builder().is_test(true).try_init(); + + let token_path = "/mock/token"; + let raw_token = "header.payload+signature/\n"; + let trimmed_token = "header.payload+signature/"; + + let file_read = TestFileRead { + expected_path: token_path.to_string(), + content: raw_token.as_bytes().to_vec(), + }; + + let http_body = r#"<AssumeRoleWithWebIdentityResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/"> + <AssumeRoleWithWebIdentityResult> + <Credentials> + <AccessKeyId>access_key_id</AccessKeyId> + <SecretAccessKey>secret_access_key</SecretAccessKey> + <SessionToken>session_token</SessionToken> + <Expiration>2124-05-25T11:45:17Z</Expiration> + </Credentials> + </AssumeRoleWithWebIdentityResult> +</AssumeRoleWithWebIdentityResponse>"#; + let http_send = CaptureHttpSend::new(http_body); + + let ctx = Context::new() + .with_file_read(file_read) + .with_http_send(http_send.clone()) + .with_env(StaticEnv { + home_dir: None, + envs: HashMap::new(), + }); + + let provider = AssumeRoleWithWebIdentityCredentialProvider::with_config( + "arn:aws:iam::123456789012:role/test-role".to_string(), + token_path.into(), + ); + + let cred = provider + .provide_credential(&ctx) + .await? + .expect("credential must be loaded"); + + assert_eq!(cred.access_key_id, "access_key_id"); + assert_eq!(cred.secret_access_key, "secret_access_key"); + assert_eq!( + cred.session_token.as_deref(), + Some("session_token"), + "session token must be populated" + ); + + let recorded_uri = http_send + .uri() + .expect("http_send must capture outgoing uri"); + let expected_query = Serializer::new(String::new()) + .append_pair("Action", "AssumeRoleWithWebIdentity") + .append_pair("RoleArn", "arn:aws:iam::123456789012:role/test-role") + .append_pair("WebIdentityToken", trimmed_token) + .append_pair("Version", "2011-06-15") + .append_pair("RoleSessionName", "reqsign") + .finish(); + let expected_uri = format!("https://sts.amazonaws.com/?{expected_query}"); + + assert_eq!(recorded_uri, expected_uri); + + Ok(()) + } } diff --git a/services/aws-v4/tests/signing/standard.rs b/services/aws-v4/tests/signing/standard.rs index 43d4e13..da644ff 100644 --- a/services/aws-v4/tests/signing/standard.rs +++ b/services/aws-v4/tests/signing/standard.rs @@ -67,7 +67,7 @@ async fn test_put_object() -> Result<()> { let cred = load_static_credential()?; let body = "Hello, World!"; - let body_digest = hex::encode(Sha256::digest(body).as_slice()); + let body_digest = hex::encode(Sha256::digest(body.as_bytes())); let mut req = Request::new(body.to_string()); req.headers_mut().insert(
