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_types::config_bag::{ConfigBag, Storable, StoreReplace};
15use aws_smithy_types::Document;
16use aws_types::region::{Region, SigningRegion, SigningRegionSet};
17use aws_types::SigningName;
18use std::error::Error as StdError;
19use std::fmt;
20use std::time::Duration;
21
22/// Auth implementations for SigV4.
23pub mod sigv4;
24
25#[cfg(feature = "sigv4a")]
26/// Auth implementations for SigV4a.
27pub mod sigv4a;
28
29/// Type of SigV4 signature.
30#[derive(Debug, Eq, PartialEq, Clone, Copy)]
31pub enum HttpSignatureType {
32    /// A signature for a full http request should be computed, with header updates applied to the signing result.
33    HttpRequestHeaders,
34
35    /// A signature for a full http request should be computed, with query param updates applied to the signing result.
36    ///
37    /// This is typically used for presigned URLs.
38    HttpRequestQueryParams,
39}
40
41/// Signing options for SigV4.
42#[derive(Clone, Debug, Eq, PartialEq)]
43#[non_exhaustive]
44pub struct SigningOptions {
45    /// Apply URI encoding twice.
46    pub double_uri_encode: bool,
47    /// Apply a SHA-256 payload checksum.
48    pub content_sha256_header: bool,
49    /// Normalize the URI path before signing.
50    pub normalize_uri_path: bool,
51    /// Omit the session token from the signature.
52    pub omit_session_token: bool,
53    /// Optional override for the payload to be used in signing.
54    pub payload_override: Option<SignableBody<'static>>,
55    /// Signature type.
56    pub signature_type: HttpSignatureType,
57    /// Whether or not the signature is optional.
58    pub signing_optional: bool,
59    /// Optional expiration (for presigning)
60    pub expires_in: Option<Duration>,
61}
62
63impl Default for SigningOptions {
64    fn default() -> Self {
65        Self {
66            double_uri_encode: true,
67            content_sha256_header: false,
68            normalize_uri_path: true,
69            omit_session_token: false,
70            payload_override: None,
71            signature_type: HttpSignatureType::HttpRequestHeaders,
72            signing_optional: false,
73            expires_in: None,
74        }
75    }
76}
77
78pub(crate) type SessionTokenNameOverrideFn = Box<
79    dyn Fn(&SigningSettings, &ConfigBag) -> Result<Option<&'static str>, BoxError>
80        + Send
81        + Sync
82        + 'static,
83>;
84
85/// Custom config that provides the alternative session token name for [`SigningSettings`]
86pub struct SigV4SessionTokenNameOverride {
87    name_override: SessionTokenNameOverrideFn,
88}
89
90impl SigV4SessionTokenNameOverride {
91    /// Creates a new `SigV4SessionTokenNameOverride`
92    pub fn new<F>(name_override: F) -> Self
93    where
94        F: Fn(&SigningSettings, &ConfigBag) -> Result<Option<&'static str>, BoxError>
95            + Send
96            + Sync
97            + 'static,
98    {
99        Self {
100            name_override: Box::new(name_override),
101        }
102    }
103
104    /// Provides a session token name override
105    pub fn name_override(
106        &self,
107        settings: &SigningSettings,
108        config_bag: &ConfigBag,
109    ) -> Result<Option<&'static str>, BoxError> {
110        (self.name_override)(settings, config_bag)
111    }
112}
113
114impl fmt::Debug for SigV4SessionTokenNameOverride {
115    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116        f.debug_struct("SessionTokenNameOverride").finish()
117    }
118}
119
120impl Storable for SigV4SessionTokenNameOverride {
121    type Storer = StoreReplace<Self>;
122}
123
124/// SigV4 signing configuration for an operation
125///
126/// Although these fields MAY be customized on a per request basis, they are generally static
127/// for a given operation
128#[derive(Clone, Debug, Default, PartialEq, Eq)]
129pub struct SigV4OperationSigningConfig {
130    /// AWS region to sign for.
131    ///
132    /// For an up-to-date list of AWS regions, see <https://docs.aws.amazon.com/general/latest/gr/rande.html>
133    pub region: Option<SigningRegion>,
134    /// AWS region set to sign for.
135    ///
136    /// A comma-separated list of AWS regions. Examples include typical AWS regions as well as 'wildcard' regions
137    pub region_set: Option<SigningRegionSet>,
138    /// AWS service to sign for.
139    pub name: Option<SigningName>,
140    /// Signing options.
141    pub signing_options: SigningOptions,
142}
143
144impl Storable for SigV4OperationSigningConfig {
145    type Storer = StoreReplace<Self>;
146}
147
148fn settings(operation_config: &SigV4OperationSigningConfig) -> SigningSettings {
149    let mut settings = SigningSettings::default();
150    settings.percent_encoding_mode = if operation_config.signing_options.double_uri_encode {
151        PercentEncodingMode::Double
152    } else {
153        PercentEncodingMode::Single
154    };
155    settings.payload_checksum_kind = if operation_config.signing_options.content_sha256_header {
156        PayloadChecksumKind::XAmzSha256
157    } else {
158        PayloadChecksumKind::NoHeader
159    };
160    settings.uri_path_normalization_mode = if operation_config.signing_options.normalize_uri_path {
161        UriPathNormalizationMode::Enabled
162    } else {
163        UriPathNormalizationMode::Disabled
164    };
165    settings.session_token_mode = if operation_config.signing_options.omit_session_token {
166        SessionTokenMode::Exclude
167    } else {
168        SessionTokenMode::Include
169    };
170    settings.signature_location = match operation_config.signing_options.signature_type {
171        HttpSignatureType::HttpRequestHeaders => SignatureLocation::Headers,
172        HttpSignatureType::HttpRequestQueryParams => SignatureLocation::QueryParams,
173    };
174    settings.expires_in = operation_config.signing_options.expires_in;
175    settings
176}
177
178#[derive(Debug)]
179enum SigV4SigningError {
180    MissingOperationSigningConfig,
181    MissingSigningRegion,
182    #[cfg(feature = "sigv4a")]
183    MissingSigningRegionSet,
184    MissingSigningName,
185    WrongIdentityType(Identity),
186    BadTypeInEndpointAuthSchemeConfig(&'static str),
187}
188
189impl fmt::Display for SigV4SigningError {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        use SigV4SigningError::*;
192        let mut w = |s| f.write_str(s);
193        match self {
194            MissingOperationSigningConfig => w("missing operation signing config"),
195            MissingSigningRegion => w("missing signing region"),
196            #[cfg(feature = "sigv4a")]
197            MissingSigningRegionSet => w("missing signing region set"),
198            MissingSigningName => w("missing signing name"),
199            WrongIdentityType(identity) => {
200                write!(f, "wrong identity type for SigV4/sigV4a. Expected AWS credentials but got `{identity:?}`")
201            }
202            BadTypeInEndpointAuthSchemeConfig(field_name) => {
203                write!(
204                    f,
205                    "unexpected type for `{field_name}` in endpoint auth scheme config",
206                )
207            }
208        }
209    }
210}
211
212impl StdError for SigV4SigningError {}
213
214fn extract_endpoint_auth_scheme_signing_name(
215    endpoint_config: &AuthSchemeEndpointConfig<'_>,
216) -> Result<Option<SigningName>, SigV4SigningError> {
217    use SigV4SigningError::BadTypeInEndpointAuthSchemeConfig as UnexpectedType;
218
219    match extract_field_from_endpoint_config("signingName", endpoint_config) {
220        Some(Document::String(s)) => Ok(Some(SigningName::from(s.to_string()))),
221        None => Ok(None),
222        _ => Err(UnexpectedType("signingName")),
223    }
224}
225
226fn extract_endpoint_auth_scheme_signing_region(
227    endpoint_config: &AuthSchemeEndpointConfig<'_>,
228) -> Result<Option<SigningRegion>, SigV4SigningError> {
229    use SigV4SigningError::BadTypeInEndpointAuthSchemeConfig as UnexpectedType;
230
231    match extract_field_from_endpoint_config("signingRegion", endpoint_config) {
232        Some(Document::String(s)) => Ok(Some(SigningRegion::from(Region::new(s.clone())))),
233        None => Ok(None),
234        _ => Err(UnexpectedType("signingRegion")),
235    }
236}
237
238fn extract_field_from_endpoint_config<'a>(
239    field_name: &'static str,
240    endpoint_config: &'a AuthSchemeEndpointConfig<'_>,
241) -> Option<&'a Document> {
242    endpoint_config
243        .as_document()
244        .and_then(Document::as_object)
245        .and_then(|config| config.get(field_name))
246}
247
248fn apply_signing_instructions(
249    instructions: SigningInstructions,
250    request: &mut HttpRequest,
251) -> Result<(), BoxError> {
252    let (new_headers, new_query) = instructions.into_parts();
253    for header in new_headers.into_iter() {
254        let mut value = http_02x::HeaderValue::from_str(header.value()).unwrap();
255        value.set_sensitive(header.sensitive());
256        request.headers_mut().insert(header.name(), value);
257    }
258
259    if !new_query.is_empty() {
260        let mut query = aws_smithy_http::query_writer::QueryWriter::new_from_string(request.uri())?;
261        for (name, value) in new_query {
262            query.insert(name, &value);
263        }
264        request.set_uri(query.build_uri())?;
265    }
266    Ok(())
267}