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::collections::HashMap;
248 use std::time::{Duration, SystemTime};
249 use tracing_test::traced_test;
250
251 #[test]
252 #[traced_test]
253 fn expiration_warning() {
254 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
255 let creds_expire_in = Duration::from_secs(100);
256
257 let mut settings = SigningSettings::default();
258 settings.expires_in = Some(creds_expire_in - Duration::from_secs(10));
259
260 let identity = Credentials::new(
261 "test-access-key",
262 "test-secret-key",
263 Some("test-session-token".into()),
264 Some(now + creds_expire_in),
265 "test",
266 )
267 .into();
268 let operation_config = SigV4OperationSigningConfig {
269 region_set: Some("test".into()),
270 name: Some(SigningName::from_static("test")),
271 signing_options: SigningOptions {
272 double_uri_encode: true,
273 content_sha256_header: true,
274 normalize_uri_path: true,
275 omit_session_token: true,
276 signature_type: HttpSignatureType::HttpRequestHeaders,
277 signing_optional: false,
278 expires_in: None,
279 payload_override: None,
280 },
281 ..Default::default()
282 };
283 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
284 assert!(!logs_contain(EXPIRATION_WARNING));
285
286 let mut settings = SigningSettings::default();
287 settings.expires_in = Some(creds_expire_in + Duration::from_secs(10));
288
289 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
290 assert!(logs_contain(EXPIRATION_WARNING));
291 }
292
293 #[test]
294 fn endpoint_config_overrides_region_and_service() {
295 let mut layer = Layer::new("test");
296 layer.store_put(SigV4OperationSigningConfig {
297 region_set: Some("test".into()),
298 name: Some(SigningName::from_static("override-this-service")),
299 ..Default::default()
300 });
301 let config = Document::Object({
302 let mut out = HashMap::new();
303 out.insert("name".to_owned(), "sigv4a".to_owned().into());
304 out.insert("signingName".to_owned(), "qldb-override".to_owned().into());
305 out.insert(
306 "signingRegionSet".to_string(),
307 Document::Array(vec!["us-east-override".to_string().into()]),
308 );
309 out
310 });
311 let config = AuthSchemeEndpointConfig::from(Some(&config));
312
313 let cfg = ConfigBag::of_layers(vec![layer]);
314 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
315
316 assert_eq!(result.region_set, Some("us-east-override".into()));
317 assert_eq!(result.name, Some(SigningName::from_static("qldb-override")));
318 assert!(matches!(result, Cow::Owned(_)));
319 }
320
321 #[test]
322 fn endpoint_config_supports_fallback_when_region_or_service_are_unset() {
323 let mut layer = Layer::new("test");
324 layer.store_put(SigV4OperationSigningConfig {
325 region_set: Some("us-east-1".into()),
326 name: Some(SigningName::from_static("qldb")),
327 ..Default::default()
328 });
329 let cfg = ConfigBag::of_layers(vec![layer]);
330 let config = AuthSchemeEndpointConfig::empty();
331
332 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
333
334 assert_eq!(result.region_set, Some("us-east-1".into()));
335 assert_eq!(result.name, Some(SigningName::from_static("qldb")));
336 assert!(matches!(result, Cow::Borrowed(_)));
337 }
338
339 #[test]
340 fn user_config_wins_over_endpoint_rules() {
341 let mut layer = Layer::new("test");
342 layer.store_put(SigV4OperationSigningConfig::default());
343 layer.store_put(SigningRegionSet::from("*"));
344 let config = Document::Object({
345 let mut out = HashMap::new();
346 out.insert("name".to_owned(), "sigv4a".to_owned().into());
347 out.insert(
348 "signingRegionSet".to_string(),
349 Document::Array(vec!["us-west-2".to_string().into()]),
350 );
351 out
352 });
353 let config = AuthSchemeEndpointConfig::from(Some(&config));
354
355 let cfg = ConfigBag::of_layers(vec![layer]);
356 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
357
358 assert_eq!(result.region_set, Some("*".into()));
359 }
360
361 #[test]
362 fn endpoint_rules_used_when_no_user_config() {
363 let mut layer = Layer::new("test");
364 layer.store_put(SigV4OperationSigningConfig::default());
365 let config = Document::Object({
366 let mut out = HashMap::new();
367 out.insert("name".to_owned(), "sigv4a".to_owned().into());
368 out.insert(
369 "signingRegionSet".to_string(),
370 Document::Array(vec!["*".to_string().into()]),
371 );
372 out
373 });
374 let config = AuthSchemeEndpointConfig::from(Some(&config));
375
376 let cfg = ConfigBag::of_layers(vec![layer]);
377 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
378
379 assert_eq!(result.region_set, Some("*".into()));
380 }
381
382 #[test]
383 fn falls_back_to_client_region_when_nothing_configured() {
384 let mut layer = Layer::new("test");
385 layer.store_put(SigV4OperationSigningConfig::default());
386 layer.store_put(Region::new("us-west-2"));
387 let config = AuthSchemeEndpointConfig::empty();
388
389 let cfg = ConfigBag::of_layers(vec![layer]);
390 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
391
392 assert_eq!(result.region_set, Some("us-west-2".into()));
393 }
394
395 #[test]
396 fn endpoint_rules_win_over_client_region_when_no_user_config() {
397 let mut layer = Layer::new("test");
398 layer.store_put(SigV4OperationSigningConfig::default());
399 layer.store_put(Region::new("us-west-2"));
400 let config = Document::Object({
401 let mut out = HashMap::new();
402 out.insert("name".to_owned(), "sigv4a".to_owned().into());
403 out.insert(
404 "signingRegionSet".to_string(),
405 Document::Array(vec!["*".to_string().into()]),
406 );
407 out
408 });
409 let config = AuthSchemeEndpointConfig::from(Some(&config));
410
411 let cfg = ConfigBag::of_layers(vec![layer]);
412 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
413
414 assert_eq!(result.region_set, Some("*".into()));
415 }
416
417 #[test]
418 fn user_config_wins_over_both_endpoint_and_region() {
419 let mut layer = Layer::new("test");
420 layer.store_put(SigV4OperationSigningConfig::default());
421 layer.store_put(SigningRegionSet::from("eu-west-1"));
422 layer.store_put(Region::new("us-west-2"));
423 let config = Document::Object({
424 let mut out = HashMap::new();
425 out.insert("name".to_owned(), "sigv4a".to_owned().into());
426 out.insert(
427 "signingRegionSet".to_string(),
428 Document::Array(vec!["*".to_string().into()]),
429 );
430 out
431 });
432 let config = AuthSchemeEndpointConfig::from(Some(&config));
433
434 let cfg = ConfigBag::of_layers(vec![layer]);
435 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
436
437 assert_eq!(result.region_set, Some("eu-west-1".into()));
438 }
439
440 #[test]
441 fn region_set_is_none_when_nothing_is_configured() {
442 let mut layer = Layer::new("test");
443 layer.store_put(SigV4OperationSigningConfig::default());
444 let config = AuthSchemeEndpointConfig::empty();
445
446 let cfg = ConfigBag::of_layers(vec![layer]);
447 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
448
449 assert_eq!(result.region_set, None);
450 }
451
452 #[test]
453 fn multi_region_endpoint_rules_preserved() {
454 let mut layer = Layer::new("test");
455 layer.store_put(SigV4OperationSigningConfig::default());
456 let config = Document::Object({
457 let mut out = HashMap::new();
458 out.insert("name".to_owned(), "sigv4a".to_owned().into());
459 out.insert(
460 "signingRegionSet".to_string(),
461 Document::Array(vec![
462 "us-east-1".to_string().into(),
463 "eu-west-1".to_string().into(),
464 ]),
465 );
466 out
467 });
468 let config = AuthSchemeEndpointConfig::from(Some(&config));
469
470 let cfg = ConfigBag::of_layers(vec![layer]);
471 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
472
473 assert_eq!(result.region_set, Some("us-east-1,eu-west-1".into()));
474 }
475}