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