Skip to main content

aws_runtime/auth/
sigv4a.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use 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
29/// Auth scheme ID for SigV4a.
30pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4a");
31
32/// SigV4a auth scheme.
33#[derive(Debug, Default)]
34pub struct SigV4aAuthScheme {
35    signer: SigV4aSigner,
36}
37
38impl SigV4aAuthScheme {
39    /// Creates a new `SigV4aHttpAuthScheme`.
40    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/// SigV4a HTTP request signer.
63#[derive(Debug, Default)]
64#[non_exhaustive]
65pub struct SigV4aSigner;
66
67impl SigV4aSigner {
68    /// Creates a new signer instance.
69    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            // The service defines the region set as a string array. Here, we convert it to a comma separated list.
166            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        // Clock skew correction: sign at now() + AttemptSkew (no-op when zero, disabled, or presigning).
188        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            // A body that is already in memory can be signed directly. A body that is not in memory
204            // (any sort of streaming body or presigned request) will be signed via UNSIGNED-PAYLOAD.
205            let signable_body = operation_config
206                .signing_options
207                .payload_override
208                .as_ref()
209                // the payload_override is a cheap clone because it contains either a
210                // reference or a short checksum (we're not cloning the entire body)
211                .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}