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