Skip to content

Commit 7e62136

Browse files
committed
chore(core): implement NTP midpoint candidateSkew formula
1 parent a5fb0db commit 7e62136

4 files changed

Lines changed: 68 additions & 18 deletions

File tree

packages-internal/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4ASigner.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export class AwsSdkSigV4ASigner extends AwsSdkSigV4Signer {
3131
).join(",");
3232

3333
signingProperties._preRequestSystemClockOffset = config.systemClockOffset;
34+
// Capture the raw send time (no skew applied) for the midpoint skew formula.
35+
signingProperties._requestSentAt = Date.now();
3436
const signedRequest = await signer.sign(httpRequest, {
3537
signingDate: getSkewCorrectedDate(config.systemClockOffset),
3638
signingRegion: multiRegionOverride,

packages-internal/core/src/submodules/httpAuthSchemes/aws_sdk/AwsSdkSigV4Signer.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,8 @@ export class AwsSdkSigV4Signer implements HttpSigner {
118118

119119
// Capture the clock offset before signing so errorHandler can detect concurrent corrections.
120120
signingProperties._preRequestSystemClockOffset = config.systemClockOffset;
121+
// Capture the raw send time (no skew applied) for the midpoint skew formula.
122+
signingProperties._requestSentAt = Date.now();
121123
const signedRequest = await signer.sign(httpRequest, {
122124
signingDate: getSkewCorrectedDate(config.systemClockOffset),
123125
signingRegion: signingRegion,
@@ -133,7 +135,8 @@ export class AwsSdkSigV4Signer implements HttpSigner {
133135
if (serverTime) {
134136
const config = throwSigningPropertyError("config", signingProperties.config as AwsSdkSigV4Config | undefined);
135137
const preRequestOffset = signingProperties._preRequestSystemClockOffset as number | undefined;
136-
const newOffset = getUpdatedSystemClockOffset(serverTime, config.systemClockOffset);
138+
const timeRequestSent = signingProperties._requestSentAt as number | undefined;
139+
const newOffset = getUpdatedSystemClockOffset(serverTime, config.systemClockOffset, timeRequestSent);
137140

138141
const isLocalCorrection = newOffset !== config.systemClockOffset;
139142
const isConcurrentCorrection = preRequestOffset !== undefined && preRequestOffset !== newOffset;
@@ -152,7 +155,8 @@ export class AwsSdkSigV4Signer implements HttpSigner {
152155
const dateHeader = getDateHeader(httpResponse);
153156
if (dateHeader) {
154157
const config = throwSigningPropertyError("config", signingProperties.config as AwsSdkSigV4Config | undefined);
155-
config.systemClockOffset = getUpdatedSystemClockOffset(dateHeader, config.systemClockOffset);
158+
const timeRequestSent = signingProperties._requestSentAt as number | undefined;
159+
config.systemClockOffset = getUpdatedSystemClockOffset(dateHeader, config.systemClockOffset, timeRequestSent);
156160
}
157161
}
158162
}

packages-internal/core/src/submodules/httpAuthSchemes/utils/getUpdatedSystemClockOffset.spec.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,21 +19,43 @@ describe(getUpdatedSystemClockOffset.name, () => {
1919
expect(getUpdatedSystemClockOffset(mockClockTime.toString(), mockSystemClockOffset)).toEqual(mockSystemClockOffset);
2020
});
2121

22-
describe("returns difference between serverTime and current time when clock is skewed", () => {
23-
const dateDotNowFn = Date.now;
24-
22+
describe("legacy path (no timeRequestSent): returns serverTime - timeResponseReceived when clock is skewed", () => {
2523
beforeEach(() => {
2624
vi.mocked(isClockSkewed).mockReturnValue(true);
27-
vi.spyOn(Date, "now").mockReturnValueOnce(mockClockTime.getTime());
28-
});
29-
30-
afterEach(() => {
31-
Date.now = dateDotNowFn;
25+
vi.spyOn(Date, "now").mockReturnValue(mockClockTime.getTime());
3226
});
3327

3428
it.each([1000, 100000])("difference: %d", (difference) => {
3529
const updatedClockTime = new Date(mockClockTime.getTime() + difference);
3630
expect(getUpdatedSystemClockOffset(updatedClockTime.toString(), mockSystemClockOffset)).toEqual(difference);
3731
});
3832
});
33+
34+
describe("midpoint path (with timeRequestSent): returns serverTime - midpoint when clock is skewed", () => {
35+
beforeEach(() => {
36+
vi.mocked(isClockSkewed).mockReturnValue(true);
37+
});
38+
39+
it("computes candidateSkew using NTP midpoint formula", () => {
40+
// timeRequestSent = T, timeResponseReceived = T + 2000 (2s round trip)
41+
// serverTime = T + 1000 (server time halfway through round trip = zero drift)
42+
// midpoint = (T + T+2000) / 2 = T + 1000
43+
// candidateSkew = (T+1000) - (T+1000) = 0
44+
const T = mockClockTime.getTime();
45+
vi.spyOn(Date, "now").mockReturnValue(T + 2000); // timeResponseReceived
46+
const serverTime = new Date(T + 1000);
47+
expect(getUpdatedSystemClockOffset(serverTime.toUTCString(), mockSystemClockOffset, T)).toEqual(0);
48+
});
49+
50+
it("computes positive candidateSkew when server is ahead of midpoint", () => {
51+
// timeRequestSent = T, timeResponseReceived = T + 2000
52+
// serverTime = T + 5 * 60 * 1000 (5 minutes ahead)
53+
// midpoint = T + 1000
54+
// candidateSkew = (T + 300000) - (T + 1000) = 299000
55+
const T = mockClockTime.getTime();
56+
vi.spyOn(Date, "now").mockReturnValue(T + 2000);
57+
const serverTime = new Date(T + 300000);
58+
expect(getUpdatedSystemClockOffset(serverTime.toUTCString(), mockSystemClockOffset, T)).toEqual(299000);
59+
});
60+
});
3961
});

packages-internal/core/src/submodules/httpAuthSchemes/utils/getUpdatedSystemClockOffset.ts

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,38 @@ import { isClockSkewed } from "./isClockSkewed";
33
/**
44
* @internal
55
*
6-
* If clock is skewed, it returns the difference between serverTime and current time.
7-
* If clock is not skewed, it returns currentSystemClockOffset.
6+
* Computes an updated system clock offset from a server Date header.
87
*
9-
* @param clockTime The string value of the server time.
10-
* @param currentSystemClockOffset The current system clock offset.
8+
* When `timeRequestSent` is provided, uses the midpoint formula:
9+
* elapsed = timeResponseReceived - timeRequestSent
10+
* midpoint = (timeRequestSent + timeResponseReceived) / 2
11+
* candidateSkew = serverTime - midpoint
12+
*
13+
* When `timeRequestSent` is absent (legacy callers), falls back to:
14+
* candidateSkew = serverTime - timeResponseReceived
15+
*
16+
* Returns `currentSystemClockOffset` unchanged if the candidate skew does not
17+
* exceed the detection threshold.
18+
*
19+
* @param clockTime The string value of the Date response header.
20+
* @param currentSystemClockOffset The current system clock offset in milliseconds.
21+
* @param timeRequestSent The raw client time (ms) at which the request was sent.
1122
*/
12-
export const getUpdatedSystemClockOffset = (clockTime: string, currentSystemClockOffset: number): number => {
13-
const clockTimeInMs = Date.parse(clockTime);
14-
if (isClockSkewed(clockTimeInMs, currentSystemClockOffset)) {
15-
return clockTimeInMs - Date.now();
23+
export const getUpdatedSystemClockOffset = (
24+
clockTime: string,
25+
currentSystemClockOffset: number,
26+
timeRequestSent?: number
27+
): number => {
28+
const serverTime = Date.parse(clockTime);
29+
const timeResponseReceived = Date.now();
30+
31+
const candidateSkew =
32+
timeRequestSent !== undefined
33+
? serverTime - (timeRequestSent + timeResponseReceived) / 2
34+
: serverTime - timeResponseReceived;
35+
36+
if (isClockSkewed(serverTime, currentSystemClockOffset)) {
37+
return candidateSkew;
1638
}
1739
return currentSystemClockOffset;
1840
};

0 commit comments

Comments
 (0)