aws_runtime/auth/
sigv4.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;
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
33/// Auth scheme ID for SigV4.
34pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4");
35
36/// SigV4 auth scheme.
37#[derive(Debug, Default)]
38pub struct SigV4AuthScheme {
39    signer: SigV4Signer,
40}
41
42impl SigV4AuthScheme {
43    /// Creates a new `SigV4AuthScheme`.
44    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/// SigV4 signer.
67#[derive(Debug, Default)]
68pub struct SigV4Signer;
69
70impl SigV4Signer {
71    /// Creates a new signer instance.
72    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            // A body that is already in memory can be signed directly. A body that is not in memory
180            // (any sort of streaming body or presigned request) will be signed via UNSIGNED-PAYLOAD.
181            let mut signable_body = operation_config
182                .signing_options
183                .payload_override
184                .as_ref()
185                // the payload_override is a cheap clone because it contains either a
186                // reference or a short checksum (we're not cloning the entire body)
187                .cloned()
188                .unwrap_or_else(|| {
189                    request
190                        .body()
191                        .bytes()
192                        .map(SignableBody::Bytes)
193                        .unwrap_or(SignableBody::UnsignedPayload)
194                });
195
196            // Sometimes it's necessary to override the payload signing scheme.
197            // If an override exists then fetch and apply it.
198            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        // If this is an event stream operation, set up the event stream signer
216        #[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    /// Event Stream SigV4 signing implementation.
253    #[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, &params)?.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, &params)
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}