aws_runtime/
auth.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6use aws_sigv4::http_request::{
7    PayloadChecksumKind, PercentEncodingMode, SessionTokenMode, SignableBody, SignatureLocation,
8    SigningInstructions, SigningSettings, UriPathNormalizationMode,
9};
10use aws_smithy_runtime_api::box_error::BoxError;
11use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig;
12use aws_smithy_runtime_api::client::identity::Identity;
13use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
14use aws_smithy_runtime_api::client::runtime_plugin::RuntimePlugin;
15use aws_smithy_types::config_bag::{ConfigBag, FrozenLayer, Layer, Storable, StoreReplace};
16use aws_smithy_types::Document;
17use aws_types::region::{Region, SigningRegion, SigningRegionSet};
18use aws_types::SigningName;
19use std::borrow::Cow;
20use std::error::Error as StdError;
21use std::fmt;
22use std::time::Duration;
23
24/// Auth implementations for SigV4.
25pub mod sigv4;
26
27#[cfg(feature = "sigv4a")]
28/// Auth implementations for SigV4a.
29pub mod sigv4a;
30
31/// Type of SigV4 signature.
32#[derive(Debug, Eq, PartialEq, Clone, Copy)]
33pub enum HttpSignatureType {
34    /// A signature for a full http request should be computed, with header updates applied to the signing result.
35    HttpRequestHeaders,
36
37    /// A signature for a full http request should be computed, with query param updates applied to the signing result.
38    ///
39    /// This is typically used for presigned URLs.
40    HttpRequestQueryParams,
41}
42
43/// Signing options for SigV4.
44#[derive(Clone, Debug, Eq, PartialEq)]
45#[non_exhaustive]
46pub struct SigningOptions {
47    /// Apply URI encoding twice.
48    pub double_uri_encode: bool,
49    /// Apply a SHA-256 payload checksum.
50    pub content_sha256_header: bool,
51    /// Normalize the URI path before signing.
52    pub normalize_uri_path: bool,
53    /// Omit the session token from the signature.
54    pub omit_session_token: bool,
55    /// Optional override for the payload to be used in signing.
56    pub payload_override: Option<SignableBody<'static>>,
57    /// Signature type.
58    pub signature_type: HttpSignatureType,
59    /// Whether or not the signature is optional.
60    pub signing_optional: bool,
61    /// Optional expiration (for presigning)
62    pub expires_in: Option<Duration>,
63}
64
65impl Default for SigningOptions {
66    fn default() -> Self {
67        Self {
68            double_uri_encode: true,
69            content_sha256_header: false,
70            normalize_uri_path: true,
71            omit_session_token: false,
72            payload_override: None,
73            signature_type: HttpSignatureType::HttpRequestHeaders,
74            signing_optional: false,
75            expires_in: None,
76        }
77    }
78}
79
80pub(crate) type SessionTokenNameOverrideFn = Box<
81    dyn Fn(&SigningSettings, &ConfigBag) -> Result<Option<&'static str>, BoxError>
82        + Send
83        + Sync
84        + 'static,
85>;
86
87/// Custom config that provides the alternative session token name for [`SigningSettings`]
88pub struct SigV4SessionTokenNameOverride {
89    name_override: SessionTokenNameOverrideFn,
90}
91
92impl SigV4SessionTokenNameOverride {
93    /// Creates a new `SigV4SessionTokenNameOverride`
94    pub fn new<F>(name_override: F) -> Self
95    where
96        F: Fn(&SigningSettings, &ConfigBag) -> Result<Option<&'static str>, BoxError>
97            + Send
98            + Sync
99            + 'static,
100    {
101        Self {
102            name_override: Box::new(name_override),
103        }
104    }
105
106    /// Provides a session token name override
107    pub fn name_override(
108        &self,
109        settings: &SigningSettings,
110        config_bag: &ConfigBag,
111    ) -> Result<Option<&'static str>, BoxError> {
112        (self.name_override)(settings, config_bag)
113    }
114}
115
116impl fmt::Debug for SigV4SessionTokenNameOverride {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.debug_struct("SessionTokenNameOverride").finish()
119    }
120}
121
122impl Storable for SigV4SessionTokenNameOverride {
123    type Storer = StoreReplace<Self>;
124}
125
126/// SigV4 signing configuration for an operation
127///
128/// Although these fields MAY be customized on a per request basis, they are generally static
129/// for a given operation
130#[derive(Clone, Debug, Default, PartialEq, Eq)]
131pub struct SigV4OperationSigningConfig {
132    /// AWS region to sign for.
133    ///
134    /// For an up-to-date list of AWS regions, see <https://docs.aws.amazon.com/general/latest/gr/rande.html>
135    pub region: Option<SigningRegion>,
136    /// AWS region set to sign for.
137    ///
138    /// A comma-separated list of AWS regions. Examples include typical AWS regions as well as 'wildcard' regions
139    pub region_set: Option<SigningRegionSet>,
140    /// AWS service to sign for.
141    pub name: Option<SigningName>,
142    /// Signing options.
143    pub signing_options: SigningOptions,
144}
145
146// TODO(AuthRefactoring): Consider implementing a dedicated struct, similar to `MergeTimeoutConfig`, that allows
147// us to implement a custom merge logic for `impl Store`, enabling fold-style merging of `SigV4OperationSigningConfig`.
148impl Storable for SigV4OperationSigningConfig {
149    type Storer = StoreReplace<Self>;
150}
151
152fn settings(operation_config: &SigV4OperationSigningConfig) -> SigningSettings {
153    let mut settings = SigningSettings::default();
154    settings.percent_encoding_mode = if operation_config.signing_options.double_uri_encode {
155        PercentEncodingMode::Double
156    } else {
157        PercentEncodingMode::Single
158    };
159    settings.payload_checksum_kind = if operation_config.signing_options.content_sha256_header {
160        PayloadChecksumKind::XAmzSha256
161    } else {
162        PayloadChecksumKind::NoHeader
163    };
164    settings.uri_path_normalization_mode = if operation_config.signing_options.normalize_uri_path {
165        UriPathNormalizationMode::Enabled
166    } else {
167        UriPathNormalizationMode::Disabled
168    };
169    settings.session_token_mode = if operation_config.signing_options.omit_session_token {
170        SessionTokenMode::Exclude
171    } else {
172        SessionTokenMode::Include
173    };
174    settings.signature_location = match operation_config.signing_options.signature_type {
175        HttpSignatureType::HttpRequestHeaders => SignatureLocation::Headers,
176        HttpSignatureType::HttpRequestQueryParams => SignatureLocation::QueryParams,
177    };
178    settings.expires_in = operation_config.signing_options.expires_in;
179    settings
180}
181
182#[derive(Debug)]
183enum SigV4SigningError {
184    MissingOperationSigningConfig,
185    MissingSigningRegion,
186    #[cfg(feature = "sigv4a")]
187    MissingSigningRegionSet,
188    MissingSigningName,
189    WrongIdentityType(Identity),
190    BadTypeInEndpointAuthSchemeConfig(&'static str),
191}
192
193impl fmt::Display for SigV4SigningError {
194    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
195        use SigV4SigningError::*;
196        let mut w = |s| f.write_str(s);
197        match self {
198            MissingOperationSigningConfig => w("missing operation signing config"),
199            MissingSigningRegion => w("missing signing region"),
200            #[cfg(feature = "sigv4a")]
201            MissingSigningRegionSet => w("missing signing region set"),
202            MissingSigningName => w("missing signing name"),
203            WrongIdentityType(identity) => {
204                write!(f, "wrong identity type for SigV4/sigV4a. Expected AWS credentials but got `{identity:?}`")
205            }
206            BadTypeInEndpointAuthSchemeConfig(field_name) => {
207                write!(
208                    f,
209                    "unexpected type for `{field_name}` in endpoint auth scheme config",
210                )
211            }
212        }
213    }
214}
215
216impl StdError for SigV4SigningError {}
217
218fn extract_endpoint_auth_scheme_signing_name(
219    endpoint_config: &AuthSchemeEndpointConfig<'_>,
220) -> Result<Option<SigningName>, SigV4SigningError> {
221    match extract_field_from_endpoint_config("signingName", endpoint_config) {
222        Some(Document::String(s)) => Ok(Some(SigningName::from(s.to_string()))),
223        None => Ok(None),
224        _ => Err(SigV4SigningError::BadTypeInEndpointAuthSchemeConfig(
225            "signingName",
226        )),
227    }
228}
229
230fn extract_endpoint_auth_scheme_signing_region(
231    endpoint_config: &AuthSchemeEndpointConfig<'_>,
232) -> Result<Option<SigningRegion>, SigV4SigningError> {
233    match extract_field_from_endpoint_config("signingRegion", endpoint_config) {
234        Some(Document::String(s)) => Ok(Some(SigningRegion::from(Region::new(s.clone())))),
235        None => Ok(None),
236        _ => Err(SigV4SigningError::BadTypeInEndpointAuthSchemeConfig(
237            "signingRegion",
238        )),
239    }
240}
241
242// Extract `SigningOptions` from the given `AuthSchemeEndpointConfig`
243//
244// The implementation may need to be extended in the future to support additional fields
245// of the `authSchemes` endpoint list property.
246fn extract_endpoint_auth_scheme_signing_options<'a>(
247    endpoint_config: &AuthSchemeEndpointConfig<'_>,
248    signing_options: &'a SigningOptions,
249) -> Result<Cow<'a, SigningOptions>, SigV4SigningError> {
250    let double_uri_encode =
251        match extract_field_from_endpoint_config("disableDoubleEncoding", endpoint_config) {
252            Some(Document::Bool(b)) => Some(!b),
253            None => None,
254            _ => {
255                return Err(SigV4SigningError::BadTypeInEndpointAuthSchemeConfig(
256                    "disableDoubleEncoding",
257                ))
258            }
259        };
260    let normalize_uri_path =
261        match extract_field_from_endpoint_config("disableNormalizePath", endpoint_config) {
262            Some(Document::Bool(b)) => Some(!b),
263            None => None,
264            _ => {
265                return Err(SigV4SigningError::BadTypeInEndpointAuthSchemeConfig(
266                    "disableNormalizePath",
267                ))
268            }
269        };
270    match (double_uri_encode, normalize_uri_path) {
271        (None, None) => Ok(Cow::Borrowed(signing_options)),
272        (double_uri_encode, normalize_uri_path) => {
273            let mut signing_options = signing_options.clone();
274            signing_options.double_uri_encode =
275                double_uri_encode.unwrap_or(signing_options.double_uri_encode);
276            signing_options.normalize_uri_path =
277                normalize_uri_path.unwrap_or(signing_options.normalize_uri_path);
278            Ok(Cow::Owned(signing_options))
279        }
280    }
281}
282
283fn extract_field_from_endpoint_config<'a>(
284    field_name: &'static str,
285    endpoint_config: &'a AuthSchemeEndpointConfig<'_>,
286) -> Option<&'a Document> {
287    endpoint_config
288        .as_document()
289        .and_then(Document::as_object)
290        .and_then(|config| config.get(field_name))
291}
292
293fn apply_signing_instructions(
294    instructions: SigningInstructions,
295    request: &mut HttpRequest,
296) -> Result<(), BoxError> {
297    let (new_headers, new_query) = instructions.into_parts();
298    for header in new_headers.into_iter() {
299        let mut value = http_02x::HeaderValue::from_str(header.value()).unwrap();
300        value.set_sensitive(header.sensitive());
301        request.headers_mut().insert(header.name(), value);
302    }
303
304    if !new_query.is_empty() {
305        let mut query = aws_smithy_http::query_writer::QueryWriter::new_from_string(request.uri())?;
306        for (name, value) in new_query {
307            query.insert(name, &value);
308        }
309        request.set_uri(query.build_uri())?;
310    }
311    Ok(())
312}
313
314/// When present in the config bag, this type will signal that the default
315/// payload signing should be overridden.
316#[non_exhaustive]
317#[derive(Clone, Debug)]
318pub enum PayloadSigningOverride {
319    /// An unsigned payload
320    ///
321    /// UnsignedPayload is used for streaming requests where the contents of the body cannot be
322    /// known prior to signing
323    UnsignedPayload,
324
325    /// A precomputed body checksum. The checksum should be a SHA256 checksum of the body,
326    /// lowercase hex encoded. Eg:
327    /// `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
328    Precomputed(String),
329
330    /// Set when a streaming body has checksum trailers.
331    StreamingUnsignedPayloadTrailer,
332}
333
334impl PayloadSigningOverride {
335    /// Create a payload signing override that will prevent the payload from
336    /// being signed.
337    pub fn unsigned_payload() -> Self {
338        Self::UnsignedPayload
339    }
340
341    /// Convert this type into the type used by the signer to determine how a
342    /// request body should be signed.
343    pub fn to_signable_body(self) -> SignableBody<'static> {
344        match self {
345            Self::UnsignedPayload => SignableBody::UnsignedPayload,
346            Self::Precomputed(checksum) => SignableBody::Precomputed(checksum),
347            Self::StreamingUnsignedPayloadTrailer => SignableBody::StreamingUnsignedPayloadTrailer,
348        }
349    }
350}
351
352impl Storable for PayloadSigningOverride {
353    type Storer = StoreReplace<Self>;
354}
355
356/// A runtime plugin that, when set, will override how the signer signs request payloads.
357#[derive(Debug)]
358pub struct PayloadSigningOverrideRuntimePlugin {
359    inner: FrozenLayer,
360}
361
362impl PayloadSigningOverrideRuntimePlugin {
363    /// Create a new runtime plugin that will force the signer to skip signing
364    /// the request payload when signing an HTTP request.
365    pub fn unsigned() -> Self {
366        let mut layer = Layer::new("PayloadSigningOverrideRuntimePlugin");
367        layer.store_put(PayloadSigningOverride::UnsignedPayload);
368
369        Self {
370            inner: layer.freeze(),
371        }
372    }
373}
374
375impl RuntimePlugin for PayloadSigningOverrideRuntimePlugin {
376    fn config(&self) -> Option<FrozenLayer> {
377        Some(self.inner.clone())
378    }
379}