Skip to content

Commit bf074c1

Browse files
RUST-1529 Use AWS SDK for sigv4 signing (#1438)
Co-authored-by: Isabel Atkinson <[email protected]>
1 parent 751d888 commit bf074c1

File tree

3 files changed

+154
-12
lines changed

3 files changed

+154
-12
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ dns-resolver = ["dep:hickory-resolver", "dep:hickory-proto"]
4141
cert-key-password = ["dep:pem", "dep:pkcs8"]
4242

4343
# Enable support for MONGODB-AWS authentication.
44-
aws-auth = ["dep:reqwest", "dep:aws-config", "dep:aws-types", "dep:aws-credential-types"]
44+
aws-auth = ["dep:reqwest", "dep:aws-config", "dep:aws-types", "dep:aws-credential-types", "dep:aws-sigv4", "dep:http"]
4545

4646
# Enable support for on-demand Azure KMS credentials.
4747
azure-kms = ["dep:reqwest"]
@@ -138,6 +138,17 @@ version = "1.2.4"
138138
optional = true
139139
default-features = false
140140

141+
[dependencies.aws-sigv4]
142+
version = "1.3.3"
143+
optional = true
144+
default-features = false
145+
features = ["sign-http"]
146+
147+
[dependencies.http]
148+
version = "1.3"
149+
optional = true
150+
default-features = false
151+
141152
[dependencies.bson2]
142153
git = "https://github.com/mongodb/bson-rust"
143154
branch = "2.15.x"

src/client/auth/aws.rs

Lines changed: 140 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ use aws_config::BehaviorVersion;
3131
#[cfg(feature = "aws-auth")]
3232
use aws_credential_types::{provider::ProvideCredentials, Credentials};
3333

34+
#[cfg(feature = "aws-auth")]
35+
use aws_sigv4::{
36+
http_request::{sign, SignableBody, SignableRequest, SigningSettings},
37+
sign::v4::SigningParams,
38+
};
39+
40+
#[cfg(feature = "aws-auth")]
41+
use http::Request;
42+
3443
const AWS_ECS_IP: &str = "169.254.170.2";
3544
const AWS_EC2_IP: &str = "169.254.169.254";
3645
const AWS_LONG_DATE_FMT: &str = "%Y%m%dT%H%M%SZ";
@@ -117,25 +126,32 @@ async fn authenticate_stream_inner(
117126
let creds = get_aws_credentials(credential).await.map_err(|e| {
118127
Error::authentication_error(MECH_NAME, &format!("failed to get creds: {e}"))
119128
})?;
120-
let aws_credential = AwsCredential::from_sdk_creds(creds);
121129

122130
let date = Utc::now();
123131

124-
let authorization_header = aws_credential.compute_authorization_header(
132+
// Generate authorization header using original implementation without AWS SDK
133+
// let authorization_header = aws_credential.compute_authorization_header(
134+
// date,
135+
// &server_first.sts_host,
136+
// &server_first.server_nonce,
137+
// )?;
138+
139+
// let mut client_second_payload = doc! {
140+
// "a": authorization_header,
141+
// "d": date.format(AWS_LONG_DATE_FMT).to_string(),
142+
// };
143+
144+
// if let Some(security_token) = aws_credential.session_token {
145+
// client_second_payload.insert("t", security_token);
146+
// }
147+
148+
let client_second_payload = compute_aws_sigv4_payload(
149+
creds,
125150
date,
126151
&server_first.sts_host,
127152
&server_first.server_nonce,
128153
)?;
129154

130-
let mut client_second_payload = doc! {
131-
"a": authorization_header,
132-
"d": date.format(AWS_LONG_DATE_FMT).to_string(),
133-
};
134-
135-
if let Some(security_token) = aws_credential.session_token {
136-
client_second_payload.insert("t", security_token);
137-
}
138-
139155
let mut client_second_payload_bytes = vec![];
140156
client_second_payload.to_writer(&mut client_second_payload_bytes)?;
141157

@@ -197,6 +213,119 @@ pub(crate) async fn get_aws_credentials(credential: &Credential) -> Result<Crede
197213
}
198214
}
199215

216+
pub fn compute_aws_sigv4_payload(
217+
creds: Credentials,
218+
date: DateTime<Utc>,
219+
host: &str,
220+
server_nonce: &[u8],
221+
) -> Result<Document> {
222+
let region = if host == "sts.amazonaws.com" {
223+
"us-east-1"
224+
} else {
225+
let parts: Vec<_> = host.split('.').collect();
226+
parts.get(1).copied().unwrap_or("us-east-1")
227+
};
228+
229+
let url = format!("https://{host}");
230+
let date_str = date.format("%Y%m%dT%H%M%SZ").to_string();
231+
let body_str = "Action=GetCallerIdentity&Version=2011-06-15";
232+
let body_bytes = body_str.as_bytes();
233+
let nonce_b64 = base64::encode(server_nonce);
234+
235+
// Create the HTTP request
236+
let mut builder = Request::builder()
237+
.method("POST")
238+
.uri(&url)
239+
.header("host", host)
240+
.header("content-type", "application/x-www-form-urlencoded")
241+
.header("content-length", body_bytes.len())
242+
.header("x-amz-date", &date_str)
243+
.header("x-mongodb-gs2-cb-flag", "n")
244+
.header("x-mongodb-server-nonce", &nonce_b64);
245+
246+
if let Some(token) = creds.session_token() {
247+
builder = builder.header("x-amz-security-token", token);
248+
}
249+
250+
let mut request = builder.body(body_str.to_string()).map_err(|e| {
251+
Error::authentication_error(MECH_NAME, &format!("Failed to build request: {e}"))
252+
})?;
253+
254+
let service = "sts";
255+
let identity = creds.into();
256+
257+
// Set up signing parameters
258+
let signing_settings = SigningSettings::default();
259+
let signing_params = SigningParams::builder()
260+
.identity(&identity)
261+
.region(region)
262+
.name(service)
263+
.time(date.into())
264+
.settings(signing_settings)
265+
.build()
266+
.map_err(|e| {
267+
Error::authentication_error(MECH_NAME, &format!("Failed to build signing params: {e}"))
268+
})?
269+
.into();
270+
let headers: Result<Vec<_>> = request
271+
.headers()
272+
.iter()
273+
.map(|(k, v)| {
274+
let v = v.to_str().map_err(|_| {
275+
Error::authentication_error(
276+
MECH_NAME,
277+
"Failed to convert header value to valid UTF-8",
278+
)
279+
})?;
280+
Ok((k.as_str(), v))
281+
})
282+
.collect();
283+
284+
let signable_request = SignableRequest::new(
285+
request.method().as_str(),
286+
request.uri().to_string(),
287+
headers?.into_iter(),
288+
SignableBody::Bytes(request.body().as_bytes()),
289+
)
290+
.map_err(|e| {
291+
Error::authentication_error(MECH_NAME, &format!("Failed to create SignableRequest: {e}"))
292+
})?;
293+
294+
let (signing_instructions, _signature) = sign(signable_request, &signing_params)
295+
.map_err(|e| Error::authentication_error(MECH_NAME, &format!("Signing failed: {e}")))?
296+
.into_parts();
297+
signing_instructions.apply_to_request_http1x(&mut request);
298+
299+
let headers = request.headers();
300+
let authorization_header = headers
301+
.get("authorization")
302+
.ok_or_else(|| Error::authentication_error(MECH_NAME, "Missing authorization header"))?
303+
.to_str()
304+
.map_err(|e| {
305+
Error::authentication_error(MECH_NAME, &format!("Invalid header value: {e}"))
306+
})?;
307+
308+
let token_header = headers
309+
.get("x-amz-security-token")
310+
.map(|v| {
311+
v.to_str().map_err(|e| {
312+
Error::authentication_error(MECH_NAME, &format!("Invalid token header: {e}"))
313+
})
314+
})
315+
.transpose()?;
316+
317+
let mut payload = doc! {
318+
"a": authorization_header,
319+
"d": date_str,
320+
};
321+
322+
if let Some(token) = token_header {
323+
payload.insert("t", token);
324+
}
325+
326+
Ok(payload)
327+
}
328+
200329
/// Contains the credentials for MONGODB-AWS authentication.
201330
// RUST-1529 note: dead_code tag added to avoid unused warnings on expiration field
202331
#[allow(dead_code)]

0 commit comments

Comments
 (0)