1use crate::auth::HttpSignatureType;
7use aws_smithy_runtime_api::box_error::BoxError;
8use aws_smithy_runtime_api::client::interceptors::context::{
9 BeforeDeserializationInterceptorContextMut, BeforeTransmitInterceptorContextMut,
10 BeforeTransmitInterceptorContextRef, InterceptorContext,
11};
12use aws_smithy_runtime_api::client::interceptors::{dyn_dispatch_hint, Intercept};
13use aws_smithy_runtime_api::client::orchestrator::{HttpResponse, OrchestratorError};
14use aws_smithy_runtime_api::client::retries::classifiers::{
15 ClassifyRetry, RetryAction, RetryClassifierPriority,
16};
17use aws_smithy_runtime_api::client::runtime_components::RuntimeComponents;
18use aws_smithy_types::config_bag::{ConfigBag, Storable, StoreReplace};
19use aws_smithy_types::date_time::Format;
20use aws_smithy_types::error::metadata::ProvideErrorMetadata;
21use aws_smithy_types::retry::ErrorKind;
22use aws_smithy_types::DateTime;
23use std::error::Error as StdError;
24use std::marker::PhantomData;
25use std::sync::{Arc, Mutex};
26use std::time::{Duration, SystemTime};
27
28const SKEW_DETECTION_THRESHOLD: Duration = Duration::from_secs(4 * 60);
31const MAX_TRUSTED_REQUEST_DURATION: Duration = Duration::from_secs(15 * 60);
33
34pub(crate) const CLOCK_SKEW_ERROR_CODES: &[&str] = &[
36 "InvalidSignatureException",
37 "SignatureDoesNotMatch",
38 "AuthFailure",
39 "RequestTimeTooSkewed",
40 "AccessDeniedException",
41];
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub(crate) struct ClockSkew(i64);
52
53impl ClockSkew {
54 fn abs(self) -> Duration {
56 Duration::from_millis(self.0.unsigned_abs())
57 }
58
59 pub(crate) fn apply(self, t: SystemTime) -> SystemTime {
61 if self.0 >= 0 {
62 t.checked_add(Duration::from_millis(self.0 as u64))
63 } else {
64 t.checked_sub(Duration::from_millis(self.0.unsigned_abs()))
65 }
66 .unwrap_or(t)
67 }
68}
69
70#[derive(Clone, Copy, Debug, Default)]
73pub(crate) struct AttemptSkew(pub(crate) ClockSkew);
74impl Storable for AttemptSkew {
75 type Storer = StoreReplace<Self>;
76}
77
78pub(crate) fn signing_time(
84 now: SystemTime,
85 signature_type: HttpSignatureType,
86 cfg: &ConfigBag,
87) -> SystemTime {
88 if signature_type == HttpSignatureType::HttpRequestQueryParams {
89 return now;
90 }
91 cfg.load::<AttemptSkew>()
92 .map_or(now, |skew| skew.0.apply(now))
93}
94
95#[derive(Clone, Copy, Debug)]
98struct TimeRequestSent(SystemTime);
99impl Storable for TimeRequestSent {
100 type Storer = StoreReplace<Self>;
101}
102
103#[derive(Clone, Copy, Debug)]
106pub(crate) struct ResponseClockSkew(pub(crate) ClockSkew);
107
108#[derive(Clone, Copy, Debug)]
110pub struct DisableClockSkewCorrection(bool);
111impl DisableClockSkewCorrection {
112 pub fn is_disabled(&self) -> bool {
114 self.0
115 }
116}
117impl From<bool> for DisableClockSkewCorrection {
118 fn from(disable: bool) -> Self {
119 Self(disable)
120 }
121}
122impl Storable for DisableClockSkewCorrection {
123 type Storer = StoreReplace<Self>;
124}
125
126fn disabled(cfg: &ConfigBag) -> bool {
127 cfg.load::<DisableClockSkewCorrection>()
128 .map(|d| d.0)
129 .unwrap_or(false)
130}
131
132fn server_time(response: &HttpResponse) -> Option<SystemTime> {
133 let date = response.headers().get("date")?;
134 let date_time = DateTime::from_str(date, Format::HttpDate).ok()?;
135 SystemTime::try_from(date_time).ok()
136}
137
138fn signed_skew(server: SystemTime, midpoint: SystemTime) -> ClockSkew {
141 match server.duration_since(midpoint) {
142 Ok(d) => ClockSkew(d.as_millis() as i64),
143 Err(e) => ClockSkew(-(e.duration().as_millis() as i64)),
144 }
145}
146
147#[non_exhaustive]
152#[derive(Debug, Default)]
153pub struct ServiceClockSkewInterceptor {
154 client_skew: Arc<Mutex<ClockSkew>>,
155}
156
157impl ServiceClockSkewInterceptor {
158 pub fn new() -> Self {
160 Self::default()
161 }
162
163 #[cfg(test)]
165 fn with_client_skew(client_skew: ClockSkew) -> Self {
166 Self {
167 client_skew: Arc::new(Mutex::new(client_skew)),
168 }
169 }
170
171 #[cfg(test)]
173 fn client_skew(&self) -> ClockSkew {
174 *self.client_skew.lock().unwrap()
175 }
176}
177
178#[dyn_dispatch_hint]
179impl Intercept for ServiceClockSkewInterceptor {
180 fn name(&self) -> &'static str {
181 "ServiceClockSkewInterceptor"
182 }
183
184 fn modify_before_retry_loop(
185 &self,
186 _ctx: &mut BeforeTransmitInterceptorContextMut<'_>,
187 _runtime_components: &RuntimeComponents,
188 cfg: &mut ConfigBag,
189 ) -> Result<(), BoxError> {
190 if disabled(cfg) {
191 return Ok(());
192 }
193 let seed = *self.client_skew.lock().unwrap();
196 cfg.interceptor_state().store_put(AttemptSkew(seed));
197 Ok(())
198 }
199
200 fn read_before_transmit(
201 &self,
202 _ctx: &BeforeTransmitInterceptorContextRef<'_>,
203 runtime_components: &RuntimeComponents,
204 cfg: &mut ConfigBag,
205 ) -> Result<(), BoxError> {
206 if disabled(cfg) {
207 return Ok(());
208 }
209 let now = runtime_components
210 .time_source()
211 .ok_or("a time source is required (clock skew)")?
212 .now();
213 cfg.interceptor_state().store_put(TimeRequestSent(now));
214 Ok(())
215 }
216
217 fn modify_before_deserialization(
218 &self,
219 ctx: &mut BeforeDeserializationInterceptorContextMut<'_>,
220 runtime_components: &RuntimeComponents,
221 cfg: &mut ConfigBag,
222 ) -> Result<(), BoxError> {
223 if disabled(cfg) {
224 return Ok(());
225 }
226 let time_received = runtime_components
227 .time_source()
228 .ok_or("a time source is required (clock skew)")?
229 .now();
230 let Some(time_sent) = cfg.load::<TimeRequestSent>().map(|t| t.0) else {
231 tracing::debug!("no recorded request send time; skipping clock skew measurement");
232 return Ok(());
233 };
234 if ctx.response().headers().get("age").is_some() {
236 tracing::debug!("response came from a cache; skipping clock skew measurement");
237 return Ok(());
238 }
239 let Some(server) = server_time(ctx.response()) else {
240 tracing::debug!("no usable `Date` response header; skipping clock skew measurement");
241 return Ok(());
242 };
243 let elapsed = time_received.duration_since(time_sent).unwrap_or_default();
244 if elapsed > MAX_TRUSTED_REQUEST_DURATION {
245 tracing::debug!(
246 ?elapsed,
247 "request too slow to measure clock skew reliably; skipping"
248 );
249 return Ok(());
250 }
251 let midpoint = time_sent + elapsed / 2;
252 let candidate = signed_skew(server, midpoint);
253 cfg.interceptor_state().store_put(AttemptSkew(candidate));
256 *self.client_skew.lock().unwrap() = candidate;
257 tracing::trace!(skew_ms = candidate.0, "recorded clock skew");
258 ctx.response_mut()
260 .add_extension(ResponseClockSkew(candidate));
261 Ok(())
262 }
263}
264
265#[derive(Debug)]
270pub struct ServiceClockSkewClassifier<E> {
271 _inner: PhantomData<E>,
272}
273
274impl<E> ServiceClockSkewClassifier<E> {
275 pub fn new() -> Self {
277 Self {
278 _inner: PhantomData,
279 }
280 }
281}
282
283impl<E> Default for ServiceClockSkewClassifier<E> {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289impl<E> ClassifyRetry for ServiceClockSkewClassifier<E>
290where
291 E: StdError + ProvideErrorMetadata + Send + Sync + 'static,
292{
293 fn classify_retry(&self, ctx: &InterceptorContext) -> RetryAction {
298 let Some(skew) = ctx
301 .response()
302 .and_then(|r| r.extension::<ResponseClockSkew>())
303 else {
304 return RetryAction::NoActionIndicated;
305 };
306 if skew.0.abs() <= SKEW_DETECTION_THRESHOLD {
307 return RetryAction::NoActionIndicated;
308 }
309 let error_code = match ctx.output_or_error() {
310 Some(Err(err)) => OrchestratorError::as_operation_error(err)
311 .and_then(|err| err.downcast_ref::<E>())
312 .and_then(|err| err.code()),
313 _ => return RetryAction::NoActionIndicated,
314 };
315 match error_code {
316 Some(code) if CLOCK_SKEW_ERROR_CODES.contains(&code) => {
318 RetryAction::retryable_error(ErrorKind::ServerError)
319 }
320 _ => RetryAction::NoActionIndicated,
321 }
322 }
323
324 fn name(&self) -> &'static str {
325 "ServiceClockSkew"
326 }
327
328 fn priority(&self) -> RetryClassifierPriority {
329 RetryClassifierPriority::run_after(RetryClassifierPriority::http_status_code_classifier())
330 }
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use aws_smithy_async::test_util::ManualTimeSource;
337 use aws_smithy_runtime_api::client::interceptors::context::{Error, Input, Output};
338 use aws_smithy_runtime_api::client::orchestrator::HttpRequest;
339 use aws_smithy_runtime_api::client::runtime_components::RuntimeComponentsBuilder;
340 use aws_smithy_types::body::SdkBody;
341 use aws_smithy_types::error::ErrorMetadata;
342 use serde::Deserialize;
343 use std::collections::HashMap;
344 use std::fmt;
345
346 #[derive(Debug)]
347 struct CodedError {
348 metadata: ErrorMetadata,
349 }
350
351 impl CodedError {
352 fn new(code: &str) -> Self {
353 Self {
354 metadata: ErrorMetadata::builder().code(code).build(),
355 }
356 }
357 }
358
359 impl fmt::Display for CodedError {
360 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
361 write!(f, "coded error")
362 }
363 }
364
365 impl std::error::Error for CodedError {}
366
367 impl ProvideErrorMetadata for CodedError {
368 fn meta(&self) -> &ErrorMetadata {
369 &self.metadata
370 }
371 }
372
373 #[test]
374 fn apply_adjusts_by_sign() {
375 let base = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
376 assert_eq!(ClockSkew(0).apply(base), base);
377 assert_eq!(ClockSkew(5000).apply(base), base + Duration::from_secs(5));
378 assert_eq!(ClockSkew(-5000).apply(base), base - Duration::from_secs(5));
379 }
380
381 #[test]
382 fn signed_skew_tracks_direction() {
383 let midpoint = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
384 assert_eq!(
386 signed_skew(midpoint + Duration::from_secs(10), midpoint),
387 ClockSkew(10_000)
388 );
389 assert_eq!(
391 signed_skew(midpoint - Duration::from_secs(10), midpoint),
392 ClockSkew(-10_000)
393 );
394 }
395
396 #[test]
397 fn presigning_is_not_shifted_by_skew() {
398 let now = SystemTime::UNIX_EPOCH + Duration::from_secs(1000);
399 let mut cfg = ConfigBag::base();
400
401 assert_eq!(
402 signing_time(now, HttpSignatureType::HttpRequestHeaders, &cfg),
403 now,
404 "nothing to apply before a skew is recorded",
405 );
406
407 cfg.interceptor_state().store_put(AttemptSkew(TEN_MIN));
408 assert_eq!(
409 signing_time(now, HttpSignatureType::HttpRequestHeaders, &cfg),
410 now + Duration::from_secs(10 * 60),
411 );
412 assert_eq!(
413 signing_time(now, HttpSignatureType::HttpRequestQueryParams, &cfg),
414 now,
415 "a presigned URL must not move with the client's skew",
416 );
417 }
418
419 fn ctx(code: Option<&'static str>, skew: Option<ClockSkew>) -> InterceptorContext {
421 let mut ctx = InterceptorContext::new(Input::doesnt_matter());
422 let http = http_1x::Response::builder()
423 .status(403)
424 .body(SdkBody::empty())
425 .unwrap();
426 let mut resp: HttpResponse = http.try_into().unwrap();
427 if let Some(s) = skew {
428 resp.add_extension(ResponseClockSkew(s));
429 }
430 ctx.set_response(resp);
431 match code {
432 Some(c) => ctx.set_output_or_error(Err(OrchestratorError::operation(Error::erase(
433 CodedError::new(c),
434 )))),
435 None => ctx.set_output_or_error(Ok(Output::erase("ok"))),
436 }
437 ctx
438 }
439
440 const TEN_MIN: ClockSkew = ClockSkew(10 * 60 * 1000);
441 const ONE_MIN: ClockSkew = ClockSkew(60 * 1000);
442
443 #[test]
444 fn no_attached_skew_is_not_retried() {
445 let classifier = ServiceClockSkewClassifier::<CodedError>::new();
446 assert_eq!(
447 classifier.classify_retry(&ctx(Some("InvalidSignatureException"), None)),
448 RetryAction::NoActionIndicated
449 );
450 }
451
452 #[test]
453 fn skew_below_threshold_is_not_retried() {
454 let classifier = ServiceClockSkewClassifier::<CodedError>::new();
455 assert_eq!(
456 classifier.classify_retry(&ctx(Some("InvalidSignatureException"), Some(ONE_MIN))),
457 RetryAction::NoActionIndicated
458 );
459 }
460
461 #[test]
462 fn skew_above_threshold_with_known_code_is_retried() {
463 let classifier = ServiceClockSkewClassifier::<CodedError>::new();
464 assert_eq!(
465 classifier.classify_retry(&ctx(Some("RequestTimeTooSkewed"), Some(TEN_MIN))),
466 RetryAction::retryable_error(ErrorKind::ServerError)
467 );
468 }
469
470 #[test]
471 fn skew_above_threshold_with_unknown_code_is_not_retried() {
472 let classifier = ServiceClockSkewClassifier::<CodedError>::new();
473 assert_eq!(
474 classifier.classify_retry(&ctx(Some("SomeOtherError"), Some(TEN_MIN))),
475 RetryAction::NoActionIndicated
476 );
477 }
478
479 #[test]
480 fn success_is_not_retried() {
481 let classifier = ServiceClockSkewClassifier::<CodedError>::new();
482 assert_eq!(
483 classifier.classify_retry(&ctx(None, Some(TEN_MIN))),
484 RetryAction::NoActionIndicated
485 );
486 }
487
488 #[derive(Deserialize)]
497 struct Suite {
498 tests: Vec<TestCase>,
499 }
500
501 #[derive(Deserialize)]
502 #[serde(rename_all = "camelCase")]
503 struct TestCase {
504 description: String,
505 operations: Vec<Operation>,
506 }
507
508 #[derive(Deserialize)]
509 #[serde(rename_all = "camelCase")]
510 struct Operation {
511 initial_client_skew: i64,
512 #[serde(default)]
513 max_attempts: Option<u32>,
514 attempts: Vec<Attempt>,
515 expected_client_skew: i64,
516 expected_outcome: String,
517 }
518
519 #[derive(Deserialize)]
520 #[serde(rename_all = "camelCase")]
521 struct Attempt {
522 client_time_at_send: String,
523 client_time_at_receive: String,
524 expected_signing_time: String,
525 response: ResponseSpec,
526 }
527
528 #[derive(Deserialize)]
529 #[serde(rename_all = "camelCase")]
530 struct ResponseSpec {
531 status_code: u16,
532 #[serde(default)]
533 headers: HashMap<String, String>,
534 #[serde(default)]
535 error_code: Option<String>,
536 }
537
538 const SUITE: &str = include_str!("../test-data/clock-skew-test-cases.json");
539
540 fn parse_time(s: &str) -> SystemTime {
541 SystemTime::try_from(DateTime::from_str(s, Format::DateTime).expect("valid timestamp"))
542 .expect("representable time")
543 }
544
545 fn skew_secs(secs: i64) -> ClockSkew {
547 ClockSkew(secs * 1000)
548 }
549
550 fn build_response(spec: &ResponseSpec) -> HttpResponse {
551 let mut builder = http_1x::Response::builder().status(spec.status_code);
552 for (name, value) in &spec.headers {
553 builder = builder.header(name.as_str(), value.as_str());
554 }
555 builder.body(SdkBody::empty()).unwrap().try_into().unwrap()
556 }
557
558 fn response_output_or_error(spec: &ResponseSpec) -> Result<Output, OrchestratorError<Error>> {
559 match &spec.error_code {
560 Some(code) => Err(OrchestratorError::operation(Error::erase(CodedError::new(
561 code,
562 )))),
563 None => Ok(Output::erase("ok")),
564 }
565 }
566
567 fn before_transmit_context() -> InterceptorContext {
569 let mut context = InterceptorContext::new(Input::doesnt_matter());
570 context.enter_serialization_phase();
571 context.set_request(HttpRequest::empty());
572 let _ = context.take_input();
573 context.enter_before_transmit_phase();
574 context
575 }
576
577 fn run_operation(interceptor: &ServiceClockSkewInterceptor, op: &Operation, desc: &str) {
578 let time = ManualTimeSource::new(SystemTime::UNIX_EPOCH);
579 let rc = RuntimeComponentsBuilder::for_tests()
580 .with_time_source(Some(time.clone()))
581 .build()
582 .unwrap();
583 let mut cfg = ConfigBag::base();
584
585 {
587 let mut seed = before_transmit_context();
588 let mut seed_ctx = (&mut seed).into();
589 interceptor
590 .modify_before_retry_loop(&mut seed_ctx, &rc, &mut cfg)
591 .unwrap();
592 }
593
594 for (j, attempt) in op.attempts.iter().enumerate() {
595 let send = parse_time(&attempt.client_time_at_send);
596 let receive = parse_time(&attempt.client_time_at_receive);
597
598 time.set_time(send);
600 let attempt_skew = cfg.load::<AttemptSkew>().map(|s| s.0).unwrap_or_default();
601 assert_eq!(
602 attempt_skew.apply(send),
603 parse_time(&attempt.expected_signing_time),
604 "{desc}: attempt {j} signing time",
605 );
606
607 let mut context = before_transmit_context();
608 {
609 let ref_ctx = (&context).into();
610 interceptor
611 .read_before_transmit(&ref_ctx, &rc, &mut cfg)
612 .unwrap();
613 }
614
615 context.enter_transmit_phase();
616 let _ = context.take_request();
617 context.set_response(build_response(&attempt.response));
618 context.enter_before_deserialization_phase();
619 time.set_time(receive);
620 {
621 let mut mut_ctx = (&mut context).into();
622 interceptor
623 .modify_before_deserialization(&mut mut_ctx, &rc, &mut cfg)
624 .unwrap();
625 }
626
627 context.enter_deserialization_phase();
628 context.set_output_or_error(response_output_or_error(&attempt.response));
629 let action = ServiceClockSkewClassifier::<CodedError>::new().classify_retry(&context);
630
631 let is_last = j + 1 == op.attempts.len();
632 if !is_last {
633 assert!(action.should_retry(), "{desc}: attempt {j} should retry");
635 } else if op.expected_outcome == "error"
636 && op
637 .max_attempts
638 .is_none_or(|m| (op.attempts.len() as u32) < m)
639 {
640 assert!(
642 !action.should_retry(),
643 "{desc}: final attempt should not be retried as clock skew",
644 );
645 }
646 }
647
648 let final_status = op
649 .attempts
650 .last()
651 .expect("at least one attempt")
652 .response
653 .status_code;
654 let outcome = if (200..300).contains(&final_status) {
655 "success"
656 } else {
657 "error"
658 };
659 assert_eq!(outcome, op.expected_outcome, "{desc}: outcome");
660 }
661
662 #[test]
663 fn clock_skew_conformance() {
664 let suite: Suite = serde_json::from_str(SUITE).expect("valid clock skew suite json");
665 assert_eq!(suite.tests.len(), 10, "expected the full clock skew suite");
666 for case in &suite.tests {
667 let interceptor = ServiceClockSkewInterceptor::with_client_skew(skew_secs(
670 case.operations[0].initial_client_skew,
671 ));
672 for (i, op) in case.operations.iter().enumerate() {
673 assert_eq!(
674 interceptor.client_skew(),
675 skew_secs(op.initial_client_skew),
676 "{}: operation {i} initial ClientSkew",
677 case.description,
678 );
679 run_operation(&interceptor, op, &case.description);
680 assert_eq!(
681 interceptor.client_skew(),
682 skew_secs(op.expected_client_skew),
683 "{}: operation {i} expected ClientSkew",
684 case.description,
685 );
686 }
687 }
688 }
689}