Skip to main content

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::{
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
36/// Auth scheme ID for SigV4.
37pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4");
38
39/// SigV4 auth scheme.
40#[derive(Debug, Default)]
41pub struct SigV4AuthScheme {
42    signer: SigV4Signer,
43}
44
45impl SigV4AuthScheme {
46    /// Creates a new `SigV4AuthScheme`.
47    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/// SigV4 signer.
70#[derive(Debug, Default)]
71pub struct SigV4Signer;
72
73impl SigV4Signer {
74    /// Creates a new signer instance.
75    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        // Clock skew correction: sign at now() + AttemptSkew (no-op when zero, disabled, or presigning).
175        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        // `sender_and_settings` needs to include a cloned `settings` to satisfy Rust's borrow checker
195        let (signing_params, sender_and_settings) = if let Some(signer_sender) = chunk_signer_sender
196        {
197            // Clone settings since we'll need it later for the message signer
198            let signing_params =
199                Self::signing_params(settings.clone(), identity, &operation_config, request_time)?;
200            (signing_params, Some((signer_sender, settings)))
201        } else {
202            // Move settings since we won't need it later
203            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            // A body that is already in memory can be signed directly. A body that is not in memory
210            // (any sort of streaming body or presigned request) will be signed via UNSIGNED-PAYLOAD.
211            let mut signable_body = operation_config
212                .signing_options
213                .payload_override
214                .as_ref()
215                // the payload_override is a cheap clone because it contains either a
216                // reference or a short checksum (we're not cloning the entire body)
217                .cloned()
218                .unwrap_or_else(|| {
219                    request
220                        .body()
221                        .bytes()
222                        .map(SignableBody::Bytes)
223                        .unwrap_or(SignableBody::UnsignedPayload)
224                });
225
226            // Sometimes it's necessary to override the payload signing scheme.
227            // If an override exists then fetch and apply it.
228            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        // If this is an event stream operation, set up the event stream signer
268        #[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, &params)?.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, &params)?.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, &params)?.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, &params)
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}