gateway-authorized.mjs
109 lines 4.0 KB
Raw
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago
1 /**
2 * Pure JS mirror of Motoko gateway / operator-export secret auth in
3 * `hub/icp/src/hub/main.mo` (SEC-KN-1 fail-closed + SEC-KN-6 constant-time compare).
4 *
5 * Contract (Pass 2 P1 / fail-closed):
6 * - Empty or missing `gateway_auth_secret` → DENY (never allow).
7 * - Missing / wrong-length / wrong `X-Gateway-Auth` → DENY.
8 * - Exact match → ALLOW.
9 *
10 * Contract (Pass 2 P14 / constant-time):
11 * - Equal-length secrets are compared by OR-of-XOR over every code point — no early
12 * exit on the first mismatch (mirrors Motoko `constantTimeTextEqual`).
13 *
14 * Health and OPTIONS bypass this check in the canister `http_request` handler
15 * (they return before calling `gatewayAuthorized`). Use
16 * {@link httpRequestRequiresGatewayAuth} to model that routing.
17 */
18
19 /**
20 * Constant-time Text equality (Motoko `constantTimeTextEqual` mirror).
21 * Length mismatch returns false without scanning content. Equal-length inputs always
22 * scan every Unicode scalar; result is whether the OR of pairwise XORs is zero.
23 *
24 * @param {unknown} a
25 * @param {unknown} b
26 * @returns {boolean}
27 */
28 export function constantTimeTextEqual(a, b) {
29 if (typeof a !== 'string' || typeof b !== 'string') return false;
30 const aa = [...a];
31 const bb = [...b];
32 if (aa.length !== bb.length) return false;
33 let acc = 0;
34 for (let i = 0; i < aa.length; i++) {
35 acc |= aa[i].codePointAt(0) ^ bb[i].codePointAt(0);
36 }
37 return acc === 0;
38 }
39
40 /**
41 * Pre-fix P14 compare — Motoko `got == expected` after a length check.
42 * Short-circuits on the first differing character (timing oracle).
43 *
44 * @param {string} a
45 * @param {string} b
46 * @returns {boolean}
47 */
48 export function textEqualEarlyExitLegacy(a, b) {
49 if (typeof a !== 'string' || typeof b !== 'string') return false;
50 if (a.length !== b.length) return false;
51 for (let i = 0; i < a.length; i++) {
52 if (a.charCodeAt(i) !== b.charCodeAt(i)) return false;
53 }
54 return true;
55 }
56
57 /**
58 * @param {string} gatewayAuthSecret — canister `storage.gateway_auth_secret`
59 * @param {string|null|undefined} headerValue — raw `X-Gateway-Auth` header value
60 * @returns {boolean}
61 */
62 export function gatewayAuthorized(gatewayAuthSecret, headerValue) {
63 const expected = typeof gatewayAuthSecret === 'string' ? gatewayAuthSecret : '';
64 if (expected.length === 0) return false;
65 if (headerValue === undefined || headerValue === null) return false;
66 if (typeof headerValue !== 'string') return false;
67 return constantTimeTextEqual(headerValue, expected);
68 }
69
70 /**
71 * Mirror of Motoko `operatorExportAuthorized` (same fail-closed + constant-time contract).
72 *
73 * @param {string} operatorExportSecret
74 * @param {string|null|undefined} headerValue — raw `X-Operator-Export-Key`
75 * @returns {boolean}
76 */
77 export function operatorExportAuthorized(operatorExportSecret, headerValue) {
78 const expected = typeof operatorExportSecret === 'string' ? operatorExportSecret : '';
79 if (expected.length === 0) return false;
80 if (headerValue === undefined || headerValue === null) return false;
81 if (typeof headerValue !== 'string') return false;
82 return constantTimeTextEqual(headerValue, expected);
83 }
84
85 /**
86 * Whether a canister HTTP request must pass `gatewayAuthorized` before serving data.
87 * Mirrors order in `http_request`: health → OPTIONS → gatewayAuthorized → …
88 *
89 * @param {string} method — HTTP method
90 * @param {string} pathKind — first element of Motoko `parsePath` result (`health`, `vaults`, …)
91 * @returns {boolean}
92 */
93 export function httpRequestRequiresGatewayAuth(method, pathKind) {
94 if (pathKind === 'health') return false;
95 if (String(method || '').toUpperCase() === 'OPTIONS') return false;
96 return true;
97 }
98
99 /**
100 * Loud health payload when gateway auth is unset — status remains 200 / ok:true.
101 *
102 * @param {string} gatewayAuthSecret
103 * @returns {{ ok: true, gateway_auth_configured: boolean }}
104 */
105 export function healthPayload(gatewayAuthSecret) {
106 const configured =
107 typeof gatewayAuthSecret === 'string' && gatewayAuthSecret.length > 0;
108 return { ok: true, gateway_auth_configured: configured };
109 }
File History 1 commit
sha256:700fafdd1afa490919f9515d660ca6e75456bcd5bb67513abcd8757a634c01f6 docs: record AIP-b SD-21 land (KN #308) Human 9 days ago