Skip to content

Commit f7ab5aa

Browse files
committed
fix: lazily evaluate SUIT signature window for test mocking
1 parent 5d69b73 commit f7ab5aa

3 files changed

Lines changed: 32 additions & 9 deletions

File tree

packages/app/scripts/generate-suit-signature.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ function loadPrivateKey() {
8383
try {
8484
return readFileSync(values["key-file"], "utf-8");
8585
} catch (err) {
86-
fail(
86+
return fail(
8787
`Impossible de lire --key-file "${values["key-file"]}" : ${errorMessage(err)}`,
8888
);
8989
}
@@ -95,11 +95,11 @@ function loadPrivateKey() {
9595
try {
9696
return Buffer.from(envKey, "base64").toString("utf-8");
9797
} catch (err) {
98-
fail(`SUIT_PRIVATE_KEY_PEM invalide : ${errorMessage(err)}`);
98+
return fail(`SUIT_PRIVATE_KEY_PEM invalide : ${errorMessage(err)}`);
9999
}
100100
}
101101

102-
fail(
102+
return fail(
103103
"Clé privée manquante. Passez --key-file <chemin> ou SUIT_PRIVATE_KEY_PEM (PEM ou base64).",
104104
);
105105
}

packages/app/src/server/services/__tests__/suitApiAuth.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,5 +319,26 @@ describe("verifySuitSignature", () => {
319319
const response = result as Response;
320320
expect(response.status).toBe(403);
321321
});
322+
323+
it("accepts a timestamp in the future within the 30-day window", async () => {
324+
const verifyFn = await importWithDevEnv();
325+
const request = makeSignedRequest("/api/v1/export/declarations", {
326+
timestamp: Math.floor(Date.now() / 1000) + 60 * 60 * 24, // 1 day ahead
327+
});
328+
const result = verifyFn(request);
329+
expect(result).toBe(true);
330+
});
331+
332+
it("returns 403 when timestamp is more than 30 days in the future", async () => {
333+
const verifyFn = await importWithDevEnv();
334+
const request = makeSignedRequest("/api/v1/export/declarations", {
335+
timestamp: Math.floor(Date.now() / 1000) + 60 * 60 * 24 * 31, // 31 days ahead
336+
});
337+
const result = verifyFn(request);
338+
339+
expect(result).not.toBe(true);
340+
const response = result as Response;
341+
expect(response.status).toBe(403);
342+
});
322343
});
323344
});

packages/app/src/server/services/suitApiAuth.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { createPublicKey, timingSafeEqual, verify } from "node:crypto";
44

55
import { env } from "~/env";
66

7-
// Signature validity window.
7+
// Signature validity window, evaluated lazily so tests can override env via
8+
// `vi.doMock('~/env')` without depending on module-load timing.
89
// Dev: 30 days — allows reusing a generated signature across a working session
910
// without constantly re-signing while debugging locally.
1011
// Preprod / prod: 30 seconds — tight anti-replay window.
11-
const SIGNATURE_WINDOW_SECONDS =
12-
env.NEXT_PUBLIC_EGAPRO_ENV === "dev" ? 30 * 24 * 60 * 60 : 30;
12+
function getSignatureWindowSeconds(): number {
13+
return env.NEXT_PUBLIC_EGAPRO_ENV === "dev" ? 30 * 24 * 60 * 60 : 30;
14+
}
1315

1416
// Cache the public key at module level — parsed once, reused on every request.
1517
const suitPublicKey = (() => {
@@ -65,8 +67,8 @@ export function verifySuitApiKey(request: Request): true | Response {
6567
* SUIT signs `{timestamp}|{METHOD}|{pathname}` with its RSA private key.
6668
* The app verifies:
6769
* 1. The signature matches using SUIT's public key (EGAPRO_SUIT_PUBLIC_KEY_PEM)
68-
* 2. The timestamp is within `SIGNATURE_WINDOW_SECONDS` (anti-replay — 30s
69-
* in preprod/prod, 30d in dev)
70+
* 2. The timestamp is within the signature window (anti-replay — 30s in
71+
* preprod/prod, 30d in dev, cf. `getSignatureWindowSeconds`)
7072
*
7173
* Only active when `EGAPRO_SUIT_PUBLIC_KEY_PEM` is set. When absent, signature
7274
* verification is skipped (dev / environments without keys).
@@ -87,7 +89,7 @@ export function verifySuitSignature(request: Request): true | Response {
8789
}
8890

8991
const now = Math.floor(Date.now() / 1000);
90-
if (Math.abs(now - ts) > SIGNATURE_WINDOW_SECONDS) {
92+
if (Math.abs(now - ts) > getSignatureWindowSeconds()) {
9193
return forbiddenResponse();
9294
}
9395

0 commit comments

Comments
 (0)