1use crate::auth::{
7 self, extract_endpoint_auth_scheme_signing_name, extract_endpoint_auth_scheme_signing_options,
8 extract_endpoint_auth_scheme_signing_region, PayloadSigningOverride,
9 SigV4OperationSigningConfig, SigV4SessionTokenNameOverride, SigV4SigningError,
10};
11use crate::content_encoding::{DeferredSignerSender, SignChunk};
12use aws_credential_types::Credentials;
13use aws_sigv4::http_request::{
14 sign, SignableBody, SignableRequest, SigningError, SigningParams, SigningSettings,
15};
16use aws_sigv4::sign::v4::{self, sign_chunk, sign_trailer};
17use aws_smithy_async::time::{SharedTimeSource, StaticTimeSource};
18use aws_smithy_runtime_api::box_error::BoxError;
19use aws_smithy_runtime_api::client::auth::{
20 AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, Sign,
21};
22use aws_smithy_runtime_api::client::identity::{Identity, SharedIdentityResolver};
23use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
24use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
25use aws_smithy_runtime_api::http::Headers;
26use aws_smithy_types::config_bag::ConfigBag;
27use aws_types::region::SigningRegion;
28use aws_types::SigningName;
29use bytes::Bytes;
30use std::borrow::Cow;
31use std::time::SystemTime;
32
33const EXPIRATION_WARNING: &str = "Presigned request will expire before the given \
34 `expires_in` duration because the credentials used to sign it will expire first.";
35
36pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4");
38
39#[derive(Debug, Default)]
41pub struct SigV4AuthScheme {
42 signer: SigV4Signer,
43}
44
45impl SigV4AuthScheme {
46 pub fn new() -> Self {
48 Default::default()
49 }
50}
51
52impl AuthScheme for SigV4AuthScheme {
53 fn scheme_id(&self) -> AuthSchemeId {
54 SCHEME_ID
55 }
56
57 fn identity_resolver(
58 &self,
59 identity_resolvers: &dyn GetIdentityResolver,
60 ) -> Option<SharedIdentityResolver> {
61 identity_resolvers.identity_resolver(self.scheme_id())
62 }
63
64 fn signer(&self) -> &dyn Sign {
65 &self.signer
66 }
67}
68
69#[derive(Debug, Default)]
71pub struct SigV4Signer;
72
73impl SigV4Signer {
74 pub fn new() -> Self {
76 Self
77 }
78
79 fn settings(operation_config: &SigV4OperationSigningConfig) -> SigningSettings {
80 super::settings(operation_config)
81 }
82
83 fn signing_params<'a>(
84 settings: SigningSettings,
85 identity: &'a Identity,
86 operation_config: &'a SigV4OperationSigningConfig,
87 request_timestamp: SystemTime,
88 ) -> Result<v4::SigningParams<'a, SigningSettings>, SigV4SigningError> {
89 let creds = identity
90 .data::<Credentials>()
91 .ok_or_else(|| SigV4SigningError::WrongIdentityType(identity.clone()))?;
92
93 if let Some(expires_in) = settings.expires_in {
94 if let Some(creds_expires_time) = creds.expiry() {
95 let presigned_expires_time = request_timestamp + expires_in;
96 if presigned_expires_time > creds_expires_time {
97 tracing::warn!(EXPIRATION_WARNING);
98 }
99 }
100 }
101
102 Ok(v4::SigningParams::builder()
103 .identity(identity)
104 .region(
105 operation_config
106 .region
107 .as_ref()
108 .ok_or(SigV4SigningError::MissingSigningRegion)?
109 .as_ref(),
110 )
111 .name(
112 operation_config
113 .name
114 .as_ref()
115 .ok_or(SigV4SigningError::MissingSigningName)?
116 .as_ref(),
117 )
118 .time(request_timestamp)
119 .settings(settings)
120 .build()
121 .expect("all required fields set"))
122 }
123
124 fn extract_operation_config<'a>(
125 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'a>,
126 config_bag: &'a ConfigBag,
127 ) -> Result<Cow<'a, SigV4OperationSigningConfig>, SigV4SigningError> {
128 let operation_config = config_bag
129 .load::<SigV4OperationSigningConfig>()
130 .ok_or(SigV4SigningError::MissingOperationSigningConfig)?;
131
132 let name = extract_endpoint_auth_scheme_signing_name(&auth_scheme_endpoint_config)?
133 .or(config_bag.load::<SigningName>().cloned());
134
135 let region = extract_endpoint_auth_scheme_signing_region(&auth_scheme_endpoint_config)?
136 .or(config_bag.load::<SigningRegion>().cloned());
137
138 let signing_options = extract_endpoint_auth_scheme_signing_options(
139 &auth_scheme_endpoint_config,
140 &operation_config.signing_options,
141 )?;
142
143 match (region, name, signing_options) {
144 (None, None, Cow::Borrowed(_)) => Ok(Cow::Borrowed(operation_config)),
145 (region, name, signing_options) => {
146 let mut operation_config = operation_config.clone();
147 operation_config.region = region.or(operation_config.region);
148 operation_config.name = name.or(operation_config.name);
149 operation_config.signing_options = match signing_options {
150 Cow::Owned(opts) => opts,
151 Cow::Borrowed(_) => operation_config.signing_options,
152 };
153 Ok(Cow::Owned(operation_config))
154 }
155 }
156 }
157}
158
159impl Sign for SigV4Signer {
160 fn sign_http_request(
161 &self,
162 request: &mut HttpRequest,
163 identity: &Identity,
164 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
165 runtime_components: &RuntimeComponents,
166 config_bag: &ConfigBag,
167 ) -> Result<(), BoxError> {
168 if identity.data::<Credentials>().is_none() {
169 return Err(SigV4SigningError::WrongIdentityType(identity.clone()).into());
170 };
171
172 let operation_config =
173 Self::extract_operation_config(auth_scheme_endpoint_config, config_bag)?;
174 let request_time = crate::service_clock_skew::signing_time(
176 runtime_components.time_source().unwrap_or_default().now(),
177 operation_config.signing_options.signature_type,
178 config_bag,
179 );
180
181 let settings = if let Some(session_token_name_override) =
182 config_bag.load::<SigV4SessionTokenNameOverride>()
183 {
184 let mut settings = Self::settings(&operation_config);
185 let name_override = session_token_name_override.name_override(&settings, config_bag)?;
186 settings.session_token_name_override = name_override;
187 settings
188 } else {
189 Self::settings(&operation_config)
190 };
191
192 let chunk_signer_sender = config_bag.load::<DeferredSignerSender>();
193
194 let (signing_params, sender_and_settings) = if let Some(signer_sender) = chunk_signer_sender
196 {
197 let signing_params =
199 Self::signing_params(settings.clone(), identity, &operation_config, request_time)?;
200 (signing_params, Some((signer_sender, settings)))
201 } else {
202 let signing_params =
204 Self::signing_params(settings, identity, &operation_config, request_time)?;
205 (signing_params, None)
206 };
207
208 let (signing_instructions, _signature) = {
209 let mut signable_body = operation_config
212 .signing_options
213 .payload_override
214 .as_ref()
215 .cloned()
218 .unwrap_or_else(|| {
219 request
220 .body()
221 .bytes()
222 .map(SignableBody::Bytes)
223 .unwrap_or(SignableBody::UnsignedPayload)
224 });
225
226 if let Some(payload_signing_override) = config_bag.load::<PayloadSigningOverride>() {
229 tracing::trace!(
230 "payload signing was overridden, now set to {payload_signing_override:?}"
231 );
232 signable_body = payload_signing_override.clone().to_signable_body();
233 }
234
235 let signable_request = SignableRequest::new(
236 request.method(),
237 request.uri(),
238 request.headers().iter(),
239 signable_body,
240 )?;
241 sign(signable_request, &SigningParams::V4(signing_params))?
242 }
243 .into_parts();
244
245 if let Some((signer_sender, settings)) = sender_and_settings {
246 let time_source = StaticTimeSource::new(request_time).into();
247 let region = operation_config
248 .region
249 .clone()
250 .expect("`Self::signing_params` above would have errored, if region was missing");
251 let name = operation_config
252 .name
253 .clone()
254 .expect("`Self::signing_params` above would have errored, if name was missing");
255 signer_sender
256 .send(Box::new(SigV4MessageSigner::new(
257 _signature.clone(),
258 identity.clone(),
259 region,
260 name,
261 time_source,
262 settings,
263 )) as _)
264 .expect("failed to send deferred signer");
265 };
266
267 #[cfg(feature = "event-stream")]
269 {
270 use crate::auth::sigv4::SigV4MessageSigner;
271 use aws_smithy_eventstream::frame::SignMessage;
272 use aws_smithy_types::event_stream::DeferredSignerSender;
273
274 if let Some(signer_sender) = config_bag.load::<DeferredSignerSender>() {
275 let time_source = runtime_components.time_source().unwrap_or_default();
276 let region = operation_config.region.clone().expect(
277 "`Self::signing_params` above would have errored, if region was missing",
278 );
279 let name = operation_config
280 .name
281 .clone()
282 .expect("`Self::signing_params` above would have errored, if name was missing");
283 signer_sender
284 .send(Box::new(SigV4MessageSigner::new(
285 _signature,
286 identity.clone(),
287 region,
288 name,
289 time_source,
290 (),
291 )) as Box<dyn SignMessage + Send + Sync>)
292 .expect("failed to send deferred signer");
293 }
294 }
295 auth::apply_signing_instructions(signing_instructions, request)?;
296 Ok(())
297 }
298}
299
300#[derive(Debug)]
301pub(crate) struct SigV4MessageSigner<S> {
302 running_signature: String,
303 identity: Identity,
304 signing_region: SigningRegion,
305 signing_name: SigningName,
306 time: SharedTimeSource,
307 signing_settings: S,
308}
309
310impl<S> SigV4MessageSigner<S>
311where
312 S: Clone + Default,
313{
314 pub(crate) fn new(
315 running_signature: String,
316 identity: Identity,
317 signing_region: SigningRegion,
318 signing_name: SigningName,
319 time: SharedTimeSource,
320 signing_settings: S,
321 ) -> Self {
322 Self {
323 running_signature,
324 identity,
325 signing_region,
326 signing_name,
327 time,
328 signing_settings,
329 }
330 }
331
332 fn signing_params(&self) -> v4::SigningParams<'_, S> {
333 let builder = v4::SigningParams::builder()
334 .identity(&self.identity)
335 .region(self.signing_region.as_ref())
336 .name(self.signing_name.as_ref())
337 .time(self.time.now())
338 .settings(self.signing_settings.clone());
339 builder.build().unwrap()
340 }
341}
342
343impl SignChunk for SigV4MessageSigner<SigningSettings> {
344 fn chunk_signature(&mut self, chunk: &Bytes) -> Result<String, SigningError> {
345 let params = self.signing_params();
346 let (_, signature) = sign_chunk(chunk, &self.running_signature, ¶ms)?.into_parts();
347 self.running_signature = signature.clone();
348 Ok(signature)
349 }
350
351 fn trailer_signature(&mut self, trailing_headers: &Headers) -> Result<String, SigningError> {
352 let params = self.signing_params();
353 let (_, signature) =
354 sign_trailer(trailing_headers, &self.running_signature, ¶ms)?.into_parts();
355 self.running_signature = signature.clone();
356 Ok(signature)
357 }
358}
359
360#[cfg(feature = "event-stream")]
361mod event_stream {
362 use crate::auth::sigv4::SigV4MessageSigner;
363 use aws_sigv4::event_stream::{sign_empty_message, sign_message};
364 use aws_smithy_eventstream::frame::{SignMessage, SignMessageError};
365 use aws_smithy_types::event_stream::Message;
366
367 impl SignMessage for SigV4MessageSigner<()> {
368 fn sign(&mut self, message: Message) -> Result<Message, SignMessageError> {
369 let (signed_message, signature) = {
370 let params = self.signing_params();
371 sign_message(&message, &self.running_signature, ¶ms)?.into_parts()
372 };
373 self.running_signature = signature;
374 Ok(signed_message)
375 }
376
377 fn sign_empty(&mut self) -> Option<Result<Message, SignMessageError>> {
378 let (signed_message, signature) = {
379 let params = self.signing_params();
380 sign_empty_message(&self.running_signature, ¶ms)
381 .ok()?
382 .into_parts()
383 };
384 self.running_signature = signature;
385 Some(Ok(signed_message))
386 }
387 }
388
389 #[cfg(test)]
390 mod tests {
391 use crate::auth::sigv4::SigV4MessageSigner;
392 use aws_credential_types::Credentials;
393 use aws_smithy_async::time::SharedTimeSource;
394 use aws_smithy_eventstream::frame::SignMessage;
395 use aws_smithy_types::event_stream::{HeaderValue, Message};
396
397 use aws_types::region::Region;
398 use aws_types::region::SigningRegion;
399 use aws_types::SigningName;
400 use std::time::{Duration, UNIX_EPOCH};
401
402 fn check_send_sync<T: Send + Sync>(value: T) -> T {
403 value
404 }
405
406 #[test]
407 fn sign_message() {
408 let region = Region::new("us-east-1");
409 let mut signer = check_send_sync(SigV4MessageSigner::new(
410 "initial-signature".into(),
411 Credentials::for_tests_with_session_token().into(),
412 SigningRegion::from(region),
413 SigningName::from_static("transcribe"),
414 SharedTimeSource::new(UNIX_EPOCH + Duration::new(1611160427, 0)),
415 (),
416 ));
417 let mut signatures = Vec::new();
418 for _ in 0..5 {
419 let signed = signer
420 .sign(Message::new(&b"identical message"[..]))
421 .unwrap();
422 if let HeaderValue::ByteArray(signature) = signed
423 .headers()
424 .iter()
425 .find(|h| h.name().as_str() == ":chunk-signature")
426 .unwrap()
427 .value()
428 {
429 signatures.push(signature.clone());
430 } else {
431 panic!("failed to get the :chunk-signature")
432 }
433 }
434 for i in 1..signatures.len() {
435 assert_ne!(signatures[i - 1], signatures[i]);
436 }
437 }
438 }
439}
440
441#[cfg(test)]
442mod tests {
443 use super::*;
444 use crate::auth::{HttpSignatureType, SigningOptions};
445 use aws_credential_types::Credentials;
446 use aws_sigv4::http_request::SigningSettings;
447 use aws_smithy_types::config_bag::Layer;
448 use aws_smithy_types::Document;
449 use aws_types::region::SigningRegion;
450 use aws_types::SigningName;
451 use std::collections::HashMap;
452 use std::time::{Duration, SystemTime};
453 use tracing_test::traced_test;
454
455 #[test]
456 #[traced_test]
457 fn expiration_warning() {
458 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
459 let creds_expire_in = Duration::from_secs(100);
460
461 let mut settings = SigningSettings::default();
462 settings.expires_in = Some(creds_expire_in - Duration::from_secs(10));
463
464 let identity = Credentials::new(
465 "test-access-key",
466 "test-secret-key",
467 Some("test-session-token".into()),
468 Some(now + creds_expire_in),
469 "test",
470 )
471 .into();
472 let operation_config = SigV4OperationSigningConfig {
473 region: Some(SigningRegion::from_static("test")),
474 name: Some(SigningName::from_static("test")),
475 signing_options: SigningOptions {
476 double_uri_encode: true,
477 content_sha256_header: true,
478 normalize_uri_path: true,
479 omit_session_token: true,
480 signature_type: HttpSignatureType::HttpRequestHeaders,
481 signing_optional: false,
482 expires_in: None,
483 payload_override: None,
484 },
485 ..Default::default()
486 };
487 SigV4Signer::signing_params(settings, &identity, &operation_config, now).unwrap();
488 assert!(!logs_contain(EXPIRATION_WARNING));
489
490 let mut settings = SigningSettings::default();
491 settings.expires_in = Some(creds_expire_in + Duration::from_secs(10));
492
493 SigV4Signer::signing_params(settings, &identity, &operation_config, now).unwrap();
494 assert!(logs_contain(EXPIRATION_WARNING));
495 }
496
497 #[test]
498 fn endpoint_config_overrides_region_and_service() {
499 let mut layer = Layer::new("test");
500 layer.store_put(SigV4OperationSigningConfig {
501 region: Some(SigningRegion::from_static("override-this-region")),
502 name: Some(SigningName::from_static("override-this-name")),
503 ..Default::default()
504 });
505 let config = Document::Object({
506 let mut out = HashMap::new();
507 out.insert("name".to_string(), "sigv4".to_string().into());
508 out.insert(
509 "signingName".to_string(),
510 "qldb-override".to_string().into(),
511 );
512 out.insert(
513 "signingRegion".to_string(),
514 "us-east-override".to_string().into(),
515 );
516 out
517 });
518 let config = AuthSchemeEndpointConfig::from(Some(&config));
519
520 let cfg = ConfigBag::of_layers(vec![layer]);
521 let result = SigV4Signer::extract_operation_config(config, &cfg).expect("success");
522
523 assert_eq!(
524 result.region,
525 Some(SigningRegion::from_static("us-east-override"))
526 );
527 assert_eq!(result.name, Some(SigningName::from_static("qldb-override")));
528 assert!(matches!(result, Cow::Owned(_)));
529 }
530
531 #[test]
532 fn endpoint_config_supports_fallback_when_region_or_service_are_unset() {
533 let mut layer = Layer::new("test");
534 layer.store_put(SigV4OperationSigningConfig {
535 region: Some(SigningRegion::from_static("us-east-1")),
536 name: Some(SigningName::from_static("qldb")),
537 ..Default::default()
538 });
539 let cfg = ConfigBag::of_layers(vec![layer]);
540 let config = AuthSchemeEndpointConfig::empty();
541
542 let result = SigV4Signer::extract_operation_config(config, &cfg).expect("success");
543
544 assert_eq!(result.region, Some(SigningRegion::from_static("us-east-1")));
545 assert_eq!(result.name, Some(SigningName::from_static("qldb")));
546 assert!(matches!(result, Cow::Borrowed(_)));
547 }
548}