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