1use crate::auth::{
7 apply_signing_instructions, extract_endpoint_auth_scheme_signing_name,
8 extract_endpoint_auth_scheme_signing_options, SigV4OperationSigningConfig, SigV4SigningError,
9};
10use aws_credential_types::Credentials;
11use aws_sigv4::http_request::{sign, SignableBody, SignableRequest, SigningSettings};
12use aws_sigv4::sign::v4a;
13use aws_smithy_runtime_api::box_error::BoxError;
14use aws_smithy_runtime_api::client::auth::{
15 AuthScheme, AuthSchemeEndpointConfig, AuthSchemeId, Sign,
16};
17use aws_smithy_runtime_api::client::identity::{Identity, SharedIdentityResolver};
18use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
19use aws_smithy_runtime_api::client::runtime_components::{GetIdentityResolver, RuntimeComponents};
20use aws_smithy_types::config_bag::ConfigBag;
21use aws_types::region::SigningRegionSet;
22use aws_types::SigningName;
23use std::borrow::Cow;
24use std::time::SystemTime;
25
26const EXPIRATION_WARNING: &str = "Presigned request will expire before the given \
27 `expires_in` duration because the credentials used to sign it will expire first.";
28
29pub const SCHEME_ID: AuthSchemeId = AuthSchemeId::new("sigv4a");
31
32#[derive(Debug, Default)]
34pub struct SigV4aAuthScheme {
35 signer: SigV4aSigner,
36}
37
38impl SigV4aAuthScheme {
39 pub fn new() -> Self {
41 Default::default()
42 }
43}
44
45impl AuthScheme for SigV4aAuthScheme {
46 fn scheme_id(&self) -> AuthSchemeId {
47 SCHEME_ID
48 }
49
50 fn identity_resolver(
51 &self,
52 identity_resolvers: &dyn GetIdentityResolver,
53 ) -> Option<SharedIdentityResolver> {
54 identity_resolvers.identity_resolver(self.scheme_id())
55 }
56
57 fn signer(&self) -> &dyn Sign {
58 &self.signer
59 }
60}
61
62#[derive(Debug, Default)]
64#[non_exhaustive]
65pub struct SigV4aSigner;
66
67impl SigV4aSigner {
68 pub fn new() -> Self {
70 Self
71 }
72
73 fn settings(operation_config: &SigV4OperationSigningConfig) -> SigningSettings {
74 super::settings(operation_config)
75 }
76
77 fn signing_params<'a>(
78 settings: SigningSettings,
79 identity: &'a Identity,
80 operation_config: &'a SigV4OperationSigningConfig,
81 request_timestamp: SystemTime,
82 ) -> Result<v4a::SigningParams<'a, SigningSettings>, SigV4SigningError> {
83 if let Some(expires_in) = settings.expires_in {
84 if let Some(identity_expiration) = identity.expiration() {
85 let presigned_expires_time = request_timestamp + expires_in;
86 if presigned_expires_time > identity_expiration {
87 tracing::warn!(EXPIRATION_WARNING);
88 }
89 }
90 }
91
92 Ok(v4a::SigningParams::builder()
93 .identity(identity)
94 .region_set(
95 operation_config
96 .region_set
97 .as_ref()
98 .ok_or(SigV4SigningError::MissingSigningRegionSet)?
99 .as_ref(),
100 )
101 .name(
102 operation_config
103 .name
104 .as_ref()
105 .ok_or(SigV4SigningError::MissingSigningName)?
106 .as_ref(),
107 )
108 .time(request_timestamp)
109 .settings(settings)
110 .build()
111 .expect("all required fields set"))
112 }
113
114 fn extract_operation_config<'a>(
115 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'a>,
116 config_bag: &'a ConfigBag,
117 ) -> Result<Cow<'a, SigV4OperationSigningConfig>, SigV4SigningError> {
118 let operation_config = config_bag
119 .load::<SigV4OperationSigningConfig>()
120 .ok_or(SigV4SigningError::MissingOperationSigningConfig)?;
121
122 let name = extract_endpoint_auth_scheme_signing_name(&auth_scheme_endpoint_config)?
123 .or(config_bag.load::<SigningName>().cloned());
124
125 let region_set =
126 extract_endpoint_auth_scheme_signing_region_set(&auth_scheme_endpoint_config)?
127 .or(config_bag.load::<SigningRegionSet>().cloned());
128
129 let signing_options = extract_endpoint_auth_scheme_signing_options(
130 &auth_scheme_endpoint_config,
131 &operation_config.signing_options,
132 )?;
133
134 match (region_set, name, signing_options) {
135 (None, None, Cow::Borrowed(_)) => Ok(Cow::Borrowed(operation_config)),
136 (region_set, name, signing_options) => {
137 let mut operation_config = operation_config.clone();
138 operation_config.region_set = region_set.or(operation_config.region_set);
139 operation_config.name = name.or(operation_config.name);
140 operation_config.signing_options = match signing_options {
141 Cow::Owned(opts) => opts,
142 Cow::Borrowed(_) => operation_config.signing_options,
143 };
144 Ok(Cow::Owned(operation_config))
145 }
146 }
147 }
148}
149
150fn extract_endpoint_auth_scheme_signing_region_set(
151 endpoint_config: &AuthSchemeEndpointConfig<'_>,
152) -> Result<Option<SigningRegionSet>, SigV4SigningError> {
153 use aws_smithy_types::Document::Array;
154 use SigV4SigningError::BadTypeInEndpointAuthSchemeConfig as UnexpectedType;
155
156 match super::extract_field_from_endpoint_config("signingRegionSet", endpoint_config) {
157 Some(Array(docs)) => {
158 let region_set: SigningRegionSet =
160 docs.iter().filter_map(|doc| doc.as_string()).collect();
161
162 Ok(Some(region_set))
163 }
164 None => Ok(None),
165 _it => Err(UnexpectedType("signingRegionSet")),
166 }
167}
168
169impl Sign for SigV4aSigner {
170 fn sign_http_request(
171 &self,
172 request: &mut HttpRequest,
173 identity: &Identity,
174 auth_scheme_endpoint_config: AuthSchemeEndpointConfig<'_>,
175 runtime_components: &RuntimeComponents,
176 config_bag: &ConfigBag,
177 ) -> Result<(), BoxError> {
178 let operation_config =
179 Self::extract_operation_config(auth_scheme_endpoint_config, config_bag)?;
180 let request_time = runtime_components.time_source().unwrap_or_default().now();
181
182 if identity.data::<Credentials>().is_none() {
183 return Err(SigV4SigningError::WrongIdentityType(identity.clone()).into());
184 }
185
186 let settings = Self::settings(&operation_config);
187 let signing_params =
188 Self::signing_params(settings, identity, &operation_config, request_time)?;
189
190 let (signing_instructions, _signature) = {
191 let signable_body = operation_config
194 .signing_options
195 .payload_override
196 .as_ref()
197 .cloned()
200 .unwrap_or_else(|| {
201 request
202 .body()
203 .bytes()
204 .map(SignableBody::Bytes)
205 .unwrap_or(SignableBody::UnsignedPayload)
206 });
207
208 let signable_request = SignableRequest::new(
209 request.method(),
210 request.uri().to_string(),
211 request.headers().iter(),
212 signable_body,
213 )?;
214 sign(signable_request, &signing_params.into())?
215 }
216 .into_parts();
217
218 apply_signing_instructions(signing_instructions, request)?;
219 Ok(())
220 }
221}
222
223#[cfg(test)]
224mod tests {
225 use super::{SigV4OperationSigningConfig, SigV4aSigner, EXPIRATION_WARNING};
226 use crate::auth::{HttpSignatureType, SigningOptions};
227 use aws_credential_types::Credentials;
228 use aws_sigv4::http_request::SigningSettings;
229 use aws_smithy_runtime_api::client::auth::AuthSchemeEndpointConfig;
230 use aws_smithy_types::config_bag::{ConfigBag, Layer};
231 use aws_smithy_types::Document;
232 use aws_types::SigningName;
233 use std::borrow::Cow;
234 use std::collections::HashMap;
235 use std::time::{Duration, SystemTime};
236 use tracing_test::traced_test;
237
238 #[test]
239 #[traced_test]
240 fn expiration_warning() {
241 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
242 let creds_expire_in = Duration::from_secs(100);
243
244 let mut settings = SigningSettings::default();
245 settings.expires_in = Some(creds_expire_in - Duration::from_secs(10));
246
247 let identity = Credentials::new(
248 "test-access-key",
249 "test-secret-key",
250 Some("test-session-token".into()),
251 Some(now + creds_expire_in),
252 "test",
253 )
254 .into();
255 let operation_config = SigV4OperationSigningConfig {
256 region_set: Some("test".into()),
257 name: Some(SigningName::from_static("test")),
258 signing_options: SigningOptions {
259 double_uri_encode: true,
260 content_sha256_header: true,
261 normalize_uri_path: true,
262 omit_session_token: true,
263 signature_type: HttpSignatureType::HttpRequestHeaders,
264 signing_optional: false,
265 expires_in: None,
266 payload_override: None,
267 },
268 ..Default::default()
269 };
270 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
271 assert!(!logs_contain(EXPIRATION_WARNING));
272
273 let mut settings = SigningSettings::default();
274 settings.expires_in = Some(creds_expire_in + Duration::from_secs(10));
275
276 SigV4aSigner::signing_params(settings, &identity, &operation_config, now).unwrap();
277 assert!(logs_contain(EXPIRATION_WARNING));
278 }
279
280 #[test]
281 fn endpoint_config_overrides_region_and_service() {
282 let mut layer = Layer::new("test");
283 layer.store_put(SigV4OperationSigningConfig {
284 region_set: Some("test".into()),
285 name: Some(SigningName::from_static("override-this-service")),
286 ..Default::default()
287 });
288 let config = Document::Object({
289 let mut out = HashMap::new();
290 out.insert("name".to_owned(), "sigv4a".to_owned().into());
291 out.insert("signingName".to_owned(), "qldb-override".to_owned().into());
292 out.insert(
293 "signingRegionSet".to_string(),
294 Document::Array(vec!["us-east-override".to_string().into()]),
295 );
296 out
297 });
298 let config = AuthSchemeEndpointConfig::from(Some(&config));
299
300 let cfg = ConfigBag::of_layers(vec![layer]);
301 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
302
303 assert_eq!(result.region_set, Some("us-east-override".into()));
304 assert_eq!(result.name, Some(SigningName::from_static("qldb-override")));
305 assert!(matches!(result, Cow::Owned(_)));
306 }
307
308 #[test]
309 fn endpoint_config_supports_fallback_when_region_or_service_are_unset() {
310 let mut layer = Layer::new("test");
311 layer.store_put(SigV4OperationSigningConfig {
312 region_set: Some("us-east-1".into()),
313 name: Some(SigningName::from_static("qldb")),
314 ..Default::default()
315 });
316 let cfg = ConfigBag::of_layers(vec![layer]);
317 let config = AuthSchemeEndpointConfig::empty();
318
319 let result = SigV4aSigner::extract_operation_config(config, &cfg).expect("success");
320
321 assert_eq!(result.region_set, Some("us-east-1".into()));
322 assert_eq!(result.name, Some(SigningName::from_static("qldb")));
323 assert!(matches!(result, Cow::Borrowed(_)));
324 }
325}