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