1use crate::auth::{
7 apply_signing_instructions, extract_endpoint_auth_scheme_signing_name,
8 extract_endpoint_auth_scheme_signing_options, SigV4OperationSigningConfig, SigV4SigningError,
9};
10use aws_credential_types::Credentials;
11use aws_sigv4::http_request::{sign, SignableBody, SignableRequest, SigningSettings};
12use aws_sigv4::sign::v4a;
13use aws_smithy_runtime_api::box_error::BoxError;
14use aws_smithy_runtime_api::client::auth::{
15 AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, Sign,
16};
17use aws_smithy_runtime_api::client::identity::{Identity, SharedIdentityResolver};
18use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
19use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
20use aws_smithy_types::config_bag::ConfigBag;
21use aws_types::region::{Region, SigningRegionSet};
22use aws_types::SigningName;
23use std::borrow::Cow;
24use std::time::SystemTime;
25
26const EXPIRATION_WARNING: &str = "Presigned request will expire before the given \
27 `expires_in` duration because the credentials used to sign it will expire first.";
28
29pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4a");
31
32#[derive(Debug, Default)]
34pub struct SigV4aAuthScheme {
35 signer: SigV4aSigner,
36}
37
38impl SigV4aAuthScheme {
39 pub fn new() -> Self {
41 Default::default()
42 }
43}
44
45impl AuthScheme for SigV4aAuthScheme {
46 fn scheme_id(&self) -> AuthSchemeId {
47 SCHEME_ID
48 }
49
50 fn identity_resolver(
51 &self,
52 identity_resolvers: &dyn GetIdentityResolver,
53 ) -> Option<SharedIdentityResolver> {
54 identity_resolvers.identity_resolver(self.scheme_id())
55 }
56
57 fn signer(&self) -> &dyn Sign {
58 &self.signer
59 }
60}
61
62#[derive(Debug, Default)]
64#[non_exhaustive]
65pub struct SigV4aSigner;
66
67impl SigV4aSigner {
68 pub fn new() -> Self {
70 Self
71 }
72
73 fn settings(operation_config: &SigV4OperationSigningConfig) -> SigningSettings {
74 super::settings(operation_config)
75 }
76
77 fn signing_params<'a>(
78 settings: SigningSettings,
79 identity: &'a Identity,
80 operation_config: &'a SigV4OperationSigningConfig,
81 request_timestamp: SystemTime,
82 ) -> Result<v4a::SigningParams<'a, SigningSettings>, SigV4SigningError> {
83 if let Some(expires_in) = settings.expires_in {
84 if let Some(identity_expiration) = identity.expiration() {
85 let presigned_expires_time = request_timestamp + expires_in;
86 if presigned_expires_time > identity_expiration {
87 tracing::warn!(EXPIRATION_WARNING);
88 }
89 }
90 }
91
92 Ok(v4a::SigningParams::builder()
93 .identity(identity)
94 .region_set(
95 operation_config
96 .region_set
97 .as_ref()
98 .ok_or(SigV4SigningError::MissingSigningRegionSet)?
99 .as_ref(),
100 )
101 .name(
102 operation_config
103 .name
104 .as_ref()
105 .ok_or(SigV4SigningError::MissingSigningName)?
106 .as_ref(),
107 )
108 .time(request_timestamp)
109 .settings(settings)
110 .build()
111 .expect("all required fields set"))
112 }
113
114 fn extract_operation_config<'a>(
115 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'a>,
116 config_bag: &'a ConfigBag,
117 ) -> Result<Cow<'a, SigV4OperationSigningConfig>, SigV4SigningError> {
118 let operation_config = config_bag
119 .load::<SigV4OperationSigningConfig>()
120 .ok_or(SigV4SigningError::MissingOperationSigningConfig)?;
121
122 let name = extract_endpoint_auth_scheme_signing_name(&auth_scheme_endpoint_config)?
123 .or(config_bag.load::<SigningName>().cloned());
124
125 let region_set = config_bag
126 .load::<SigningRegionSet>()
127 .cloned()
128 .or(extract_endpoint_auth_scheme_signing_region_set(
129 &auth_scheme_endpoint_config,
130 )?)
131 .or(config_bag
132 .load::<Region>()
133 .cloned()
134 .map(SigningRegionSet::from));
135
136 let signing_options = extract_endpoint_auth_scheme_signing_options(
137 &auth_scheme_endpoint_config,
138 &operation_config.signing_options,
139 )?;
140
141 match (region_set, name, signing_options) {
142 (None, None, Cow::Borrowed(_)) => Ok(Cow::Borrowed(operation_config)),
143 (region_set, name, signing_options) => {
144 let mut operation_config = operation_config.clone();
145 operation_config.region_set = region_set.or(operation_config.region_set);
146 operation_config.name = name.or(operation_config.name);
147 operation_config.signing_options = match signing_options {
148 Cow::Owned(opts) => opts,
149 Cow::Borrowed(_) => operation_config.signing_options,
150 };
151 Ok(Cow::Owned(operation_config))
152 }
153 }
154 }
155}
156
157fn extract_endpoint_auth_scheme_signing_region_set(
158 endpoint_config: &AuthSchemeEndpointConfig<'_>,
159) -> Result<Option<SigningRegionSet>, SigV4SigningError> {
160 use aws_smithy_types::Document::Array;
161 use SigV4SigningError::BadTypeInEndpointAuthSchemeConfig as UnexpectedType;
162
163 match super::extract_field_from_endpoint_config("signingRegionSet", endpoint_config) {
164 Some(Array(docs)) => {
165 let region_set: SigningRegionSet =
167 docs.iter().filter_map(|doc| doc.as_string()).collect();
168
169 Ok(Some(region_set))
170 }
171 None => Ok(None),
172 _it => Err(UnexpectedType("signingRegionSet")),
173 }
174}
175
176impl Sign for SigV4aSigner {
177 fn sign_http_request(
178 &self,
179 request: &mut HttpRequest,
180 identity: &Identity,
181 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
182 runtime_components: &RuntimeComponents,
183 config_bag: &ConfigBag,
184 ) -> Result<(), BoxError> {
185 let operation_config =
186 Self::extract_operation_config(auth_scheme_endpoint_config, config_bag)?;
187 let request_time = crate::service_clock_skew::signing_time(
189 runtime_components.time_source().unwrap_or_default().now(),
190 operation_config.signing_options.signature_type,
191 config_bag,
192 );
193
194 if identity.data::<Credentials>().is_none() {
195 return Err(SigV4SigningError::WrongIdentityType(identity.clone()).into());
196 }
197
198 let settings = Self::settings(&operation_config);
199 let signing_params =
200 Self::signing_params(settings, identity, &operation_config, request_time)?;
201
202 let (signing_instructions, _signature) = {
203 let signable_body = operation_config
206 .signing_options
207 .payload_override
208 .as_ref()
209 .cloned()
212 .unwrap_or_else(|| {
213 request
214 .body()
215 .bytes()
216 .map(SignableBody::Bytes)
217 .unwrap_or(SignableBody::UnsignedPayload)
218 });
219
220 let signable_request = SignableRequest::new(
221 request.method(),
222 request.uri().to_string(),
223 request.headers().iter(),
224 signable_body,
225 )?;
226 sign(signable_request, &signing_params.into())?
227 }
228 .into_parts();
229
230 apply_signing_instructions(signing_instructions, request)?;
231 Ok(())
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::{SigV4OperationSigningConfig, SigV4aSigner, EXPIRATION_WARNING};
238 use crate::auth::{HttpSignatureType, SigningOptions};
239 use aws_credential_types::Credentials;
240 use aws_sigv4::http_request::SigningSettings;
241 use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig;
242 use aws_smithy_types::config_bag::{ConfigBag, Layer};
243 use aws_smithy_types::Document;
244 use aws_types::region::{Region, SigningRegionSet};
245 use aws_types::SigningName;
246 use std::borrow::Cow;
247 use std::time::{Duration, SystemTime};
248 use tracing_test::traced_test;
249
250 #[test]
251 #[traced_test]
252 fn expiration_warning() {
253 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
254 let creds_expire_in = Duration::from_secs(100);
255
256 let mut settings = SigningSettings::default();
257 settings.expires_in = Some(creds_expire_in - Duration::from_secs(10));
258
259 let identity = Credentials::new(
260 "test-access-key",
261 "test-secret-key",
262 Some("test-session-token".into()),
263 Some(now + creds_expire_in),
264 "test",
265 )
266 .into();
267 let operation_config = SigV4OperationSigningConfig {
268 region_set: Some("test".into()),
269 name: Some(SigningName::from_static("test")),
270 signing_options: SigningOptions {
271 double_uri_encode: true,
272 content_sha256_header: true,
273 normalize_uri_path: true,
274 omit_session_token: true,
275 signature_type: HttpSignatureType::HttpRequestHeaders,
276 signing_optional: false,
277 expires_in: None,
278 payload_override: None,
279 },
280 ..Default::default()
281 };
282 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
283 assert!(!logs_contain(EXPIRATION_WARNING));
284
285 let mut settings = SigningSettings::default();
286 settings.expires_in = Some(creds_expire_in + Duration::from_secs(10));
287
288 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
289 assert!(logs_contain(EXPIRATION_WARNING));
290 }
291
292 #[test]
293 fn endpoint_config_overrides_region_and_service() {
294 let mut layer = Layer::new("test");
295 layer.store_put(SigV4OperationSigningConfig {
296 region_set: Some("test".into()),
297 name: Some(SigningName::from_static("override-this-service")),
298 ..Default::default()
299 });
300 let config = Document::Object({
301 let mut out = aws_smithy_types::document::DocumentObject::new();
302 out.insert("name".to_owned(), "sigv4a".to_owned().into());
303 out.insert("signingName".to_owned(), "qldb-override".to_owned().into());
304 out.insert(
305 "signingRegionSet".to_string(),
306 Document::Array(vec!["us-east-override".to_string().into()]),
307 );
308 out
309 });
310 let config = AuthSchemeEndpointConfig::from(Some(&config));
311
312 let cfg = ConfigBag::of_layers(vec![layer]);
313 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
314
315 assert_eq!(result.region_set, Some("us-east-override".into()));
316 assert_eq!(result.name, Some(SigningName::from_static("qldb-override")));
317 assert!(matches!(result, Cow::Owned(_)));
318 }
319
320 #[test]
321 fn endpoint_config_supports_fallback_when_region_or_service_are_unset() {
322 let mut layer = Layer::new("test");
323 layer.store_put(SigV4OperationSigningConfig {
324 region_set: Some("us-east-1".into()),
325 name: Some(SigningName::from_static("qldb")),
326 ..Default::default()
327 });
328 let cfg = ConfigBag::of_layers(vec![layer]);
329 let config = AuthSchemeEndpointConfig::empty();
330
331 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
332
333 assert_eq!(result.region_set, Some("us-east-1".into()));
334 assert_eq!(result.name, Some(SigningName::from_static("qldb")));
335 assert!(matches!(result, Cow::Borrowed(_)));
336 }
337
338 #[test]
339 fn user_config_wins_over_endpoint_rules() {
340 let mut layer = Layer::new("test");
341 layer.store_put(SigV4OperationSigningConfig::default());
342 layer.store_put(SigningRegionSet::from("*"));
343 let config = Document::Object({
344 let mut out = aws_smithy_types::document::DocumentObject::new();
345 out.insert("name".to_owned(), "sigv4a".to_owned().into());
346 out.insert(
347 "signingRegionSet".to_string(),
348 Document::Array(vec!["us-west-2".to_string().into()]),
349 );
350 out
351 });
352 let config = AuthSchemeEndpointConfig::from(Some(&config));
353
354 let cfg = ConfigBag::of_layers(vec![layer]);
355 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
356
357 assert_eq!(result.region_set, Some("*".into()));
358 }
359
360 #[test]
361 fn endpoint_rules_used_when_no_user_config() {
362 let mut layer = Layer::new("test");
363 layer.store_put(SigV4OperationSigningConfig::default());
364 let config = Document::Object({
365 let mut out = aws_smithy_types::document::DocumentObject::new();
366 out.insert("name".to_owned(), "sigv4a".to_owned().into());
367 out.insert(
368 "signingRegionSet".to_string(),
369 Document::Array(vec!["*".to_string().into()]),
370 );
371 out
372 });
373 let config = AuthSchemeEndpointConfig::from(Some(&config));
374
375 let cfg = ConfigBag::of_layers(vec![layer]);
376 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
377
378 assert_eq!(result.region_set, Some("*".into()));
379 }
380
381 #[test]
382 fn falls_back_to_client_region_when_nothing_configured() {
383 let mut layer = Layer::new("test");
384 layer.store_put(SigV4OperationSigningConfig::default());
385 layer.store_put(Region::new("us-west-2"));
386 let config = AuthSchemeEndpointConfig::empty();
387
388 let cfg = ConfigBag::of_layers(vec![layer]);
389 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
390
391 assert_eq!(result.region_set, Some("us-west-2".into()));
392 }
393
394 #[test]
395 fn endpoint_rules_win_over_client_region_when_no_user_config() {
396 let mut layer = Layer::new("test");
397 layer.store_put(SigV4OperationSigningConfig::default());
398 layer.store_put(Region::new("us-west-2"));
399 let config = Document::Object({
400 let mut out = aws_smithy_types::document::DocumentObject::new();
401 out.insert("name".to_owned(), "sigv4a".to_owned().into());
402 out.insert(
403 "signingRegionSet".to_string(),
404 Document::Array(vec!["*".to_string().into()]),
405 );
406 out
407 });
408 let config = AuthSchemeEndpointConfig::from(Some(&config));
409
410 let cfg = ConfigBag::of_layers(vec![layer]);
411 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
412
413 assert_eq!(result.region_set, Some("*".into()));
414 }
415
416 #[test]
417 fn user_config_wins_over_both_endpoint_and_region() {
418 let mut layer = Layer::new("test");
419 layer.store_put(SigV4OperationSigningConfig::default());
420 layer.store_put(SigningRegionSet::from("eu-west-1"));
421 layer.store_put(Region::new("us-west-2"));
422 let config = Document::Object({
423 let mut out = aws_smithy_types::document::DocumentObject::new();
424 out.insert("name".to_owned(), "sigv4a".to_owned().into());
425 out.insert(
426 "signingRegionSet".to_string(),
427 Document::Array(vec!["*".to_string().into()]),
428 );
429 out
430 });
431 let config = AuthSchemeEndpointConfig::from(Some(&config));
432
433 let cfg = ConfigBag::of_layers(vec![layer]);
434 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
435
436 assert_eq!(result.region_set, Some("eu-west-1".into()));
437 }
438
439 #[test]
440 fn region_set_is_none_when_nothing_is_configured() {
441 let mut layer = Layer::new("test");
442 layer.store_put(SigV4OperationSigningConfig::default());
443 let config = AuthSchemeEndpointConfig::empty();
444
445 let cfg = ConfigBag::of_layers(vec![layer]);
446 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
447
448 assert_eq!(result.region_set, None);
449 }
450
451 #[test]
452 fn multi_region_endpoint_rules_preserved() {
453 let mut layer = Layer::new("test");
454 layer.store_put(SigV4OperationSigningConfig::default());
455 let config = Document::Object({
456 let mut out = aws_smithy_types::document::DocumentObject::new();
457 out.insert("name".to_owned(), "sigv4a".to_owned().into());
458 out.insert(
459 "signingRegionSet".to_string(),
460 Document::Array(vec![
461 "us-east-1".to_string().into(),
462 "eu-west-1".to_string().into(),
463 ]),
464 );
465 out
466 });
467 let config = AuthSchemeEndpointConfig::from(Some(&config));
468
469 let cfg = ConfigBag::of_layers(vec![layer]);
470 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
471
472 assert_eq!(result.region_set, Some("us-east-1,eu-west-1".into()));
473 }
474}